diff --git a/.coveragerc b/.coveragerc index 3ec0b119cb8..c4051af5136 100644 --- a/.coveragerc +++ b/.coveragerc @@ -172,6 +172,9 @@ omit = homeassistant/components/twilio.py homeassistant/components/notify/twilio_sms.py homeassistant/components/notify/twilio_call.py + + homeassistant/components/velbus.py + homeassistant/components/*/velbus.py homeassistant/components/velux.py homeassistant/components/*/velux.py @@ -211,6 +214,7 @@ omit = homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/concord232.py + homeassistant/components/alarm_control_panel/manual_mqtt.py homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/alarm_control_panel/simplisafe.py homeassistant/components/alarm_control_panel/totalconnect.py @@ -305,6 +309,7 @@ omit = homeassistant/components/light/piglow.py homeassistant/components/light/sensehat.py homeassistant/components/light/tikteck.py + homeassistant/components/light/tplink.py homeassistant/components/light/tradfri.py homeassistant/components/light/x10.py homeassistant/components/light/yeelight.py diff --git a/README.rst b/README.rst index 40a393517da..039e8a922af 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ -Home Assistant |Build Status| |Coverage Status| | Join the chat `at discord `_ | Join the dev chat `at discord `_ | -============================================================================================================================================================================================== +Home Assistant |Build Status| |Coverage Status| |Chat Status| +============================================================= Home Assistant is a home automation platform running on Python 3. It is able to track and control all devices at home and offer a platform for automating control. @@ -31,6 +31,8 @@ of a component, check the `Home Assistant help section \ + dt_util.utcnow(): + return STATE_ALARM_PENDING + + if self._state == STATE_ALARM_TRIGGERED and self._trigger_time: + if self._state_ts + self._pending_time > dt_util.utcnow(): + return STATE_ALARM_PENDING + elif (self._state_ts + self._pending_time + + self._trigger_time) < dt_util.utcnow(): + if self._disarm_after_trigger: + return STATE_ALARM_DISARMED + return self._pre_trigger_state + + return self._state + + @property + def code_format(self): + """One or more characters.""" + return None if self._code is None else '.+' + + def alarm_disarm(self, code=None): + """Send disarm command.""" + if not self._validate_code(code, STATE_ALARM_DISARMED): + return + + self._state = STATE_ALARM_DISARMED + self._state_ts = dt_util.utcnow() + self.schedule_update_ha_state() + + def alarm_arm_home(self, code=None): + """Send arm home command.""" + if not self._validate_code(code, STATE_ALARM_ARMED_HOME): + return + + self._state = STATE_ALARM_ARMED_HOME + self._state_ts = dt_util.utcnow() + self.schedule_update_ha_state() + + if self._pending_time: + track_point_in_time( + self._hass, self.async_update_ha_state, + self._state_ts + self._pending_time) + + def alarm_arm_away(self, code=None): + """Send arm away command.""" + if not self._validate_code(code, STATE_ALARM_ARMED_AWAY): + return + + self._state = STATE_ALARM_ARMED_AWAY + self._state_ts = dt_util.utcnow() + self.schedule_update_ha_state() + + if self._pending_time: + track_point_in_time( + self._hass, self.async_update_ha_state, + self._state_ts + self._pending_time) + + def alarm_trigger(self, code=None): + """Send alarm trigger command. No code needed.""" + self._pre_trigger_state = self._state + self._state = STATE_ALARM_TRIGGERED + self._state_ts = dt_util.utcnow() + self.schedule_update_ha_state() + + if self._trigger_time: + track_point_in_time( + self._hass, self.async_update_ha_state, + self._state_ts + self._pending_time) + + track_point_in_time( + self._hass, self.async_update_ha_state, + self._state_ts + self._pending_time + self._trigger_time) + + def _validate_code(self, code, state): + """Validate given code.""" + check = self._code is None or code == self._code + if not check: + _LOGGER.warning("Invalid code given for %s", state) + return check + + def async_added_to_hass(self): + """Subscribe mqtt events. + + This method must be run in the event loop and returns a coroutine. + """ + async_track_state_change( + self.hass, self.entity_id, self._async_state_changed_listener + ) + + @callback + def message_received(topic, payload, qos): + """Run when new MQTT message has been received.""" + if payload == self._payload_disarm: + self.async_alarm_disarm(self._code) + elif payload == self._payload_arm_home: + self.async_alarm_arm_home(self._code) + elif payload == self._payload_arm_away: + self.async_alarm_arm_away(self._code) + else: + _LOGGER.warning("Received unexpected payload: %s", payload) + return + + return mqtt.async_subscribe( + self.hass, self._command_topic, message_received, self._qos) + + @asyncio.coroutine + def _async_state_changed_listener(self, entity_id, old_state, new_state): + """Publish state change to MQTT.""" + mqtt.async_publish(self.hass, self._state_topic, new_state.state, + self._qos, True) diff --git a/homeassistant/components/amcrest.py b/homeassistant/components/amcrest.py index 9760ee5d607..157b9574a06 100644 --- a/homeassistant/components/amcrest.py +++ b/homeassistant/components/amcrest.py @@ -17,7 +17,7 @@ from homeassistant.const import ( from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['amcrest==1.2.0'] +REQUIREMENTS = ['amcrest==1.2.1'] DEPENDENCIES = ['ffmpeg'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/binary_sensor/velbus.py b/homeassistant/components/binary_sensor/velbus.py new file mode 100644 index 00000000000..214edcf9463 --- /dev/null +++ b/homeassistant/components/binary_sensor/velbus.py @@ -0,0 +1,96 @@ +""" +Support for Velbus Binary Sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.velbus/ +""" +import asyncio +import logging + + +import voluptuous as vol + +from homeassistant.const import CONF_NAME, CONF_DEVICES +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA +from homeassistant.components.velbus import DOMAIN +import homeassistant.helpers.config_validation as cv + + +DEPENDENCIES = ['velbus'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [ + { + vol.Required('module'): cv.positive_int, + vol.Required('channel'): cv.positive_int, + vol.Required(CONF_NAME): cv.string, + vol.Optional('is_pushbutton'): cv.boolean + } + ]) +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up Velbus binary sensors.""" + velbus = hass.data[DOMAIN] + + add_devices(VelbusBinarySensor(sensor, velbus) + for sensor in config[CONF_DEVICES]) + + +class VelbusBinarySensor(BinarySensorDevice): + """Representation of a Velbus Binary Sensor.""" + + def __init__(self, binary_sensor, velbus): + """Initialize a Velbus light.""" + self._velbus = velbus + self._name = binary_sensor[CONF_NAME] + self._module = binary_sensor['module'] + self._channel = binary_sensor['channel'] + self._is_pushbutton = 'is_pushbutton' in binary_sensor \ + and binary_sensor['is_pushbutton'] + self._state = False + + @asyncio.coroutine + def async_added_to_hass(self): + """Add listener for Velbus messages on bus.""" + yield from self.hass.async_add_job( + self._velbus.subscribe, self._on_message) + + def _on_message(self, message): + import velbus + if isinstance(message, velbus.PushButtonStatusMessage): + if message.address == self._module and \ + self._channel in message.get_channels(): + if self._is_pushbutton: + if self._channel in message.closed: + self._toggle() + else: + pass + else: + self._toggle() + + def _toggle(self): + if self._state is True: + self._state = False + else: + self._state = True + self.schedule_update_ha_state() + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the display name of this sensor.""" + return self._name + + @property + def is_on(self): + """Return true if the sensor is on.""" + return self._state diff --git a/homeassistant/components/camera/onvif.py b/homeassistant/components/camera/onvif.py index f1c94f79c0b..711eb75a744 100644 --- a/homeassistant/components/camera/onvif.py +++ b/homeassistant/components/camera/onvif.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/camera.onvif/ """ import asyncio import logging +import os import voluptuous as vol @@ -54,6 +55,7 @@ class ONVIFCamera(Camera): def __init__(self, hass, config): """Initialize a ONVIF camera.""" from onvif import ONVIFService + import onvif super().__init__() self._name = config.get(CONF_NAME) @@ -63,7 +65,7 @@ class ONVIFCamera(Camera): config.get(CONF_HOST), config.get(CONF_PORT)), config.get(CONF_USERNAME), config.get(CONF_PASSWORD), - '{}/deps/onvif/wsdl/media.wsdl'.format(hass.config.config_dir) + '{}/wsdl/media.wsdl'.format(os.path.dirname(onvif.__file__)) ) self._input = media.GetStreamUri().Uri _LOGGER.debug("ONVIF Camera Using the following URL for %s: %s", diff --git a/homeassistant/components/conversation.py b/homeassistant/components/conversation.py index b682b318c7b..303e83e7e0e 100644 --- a/homeassistant/components/conversation.py +++ b/homeassistant/components/conversation.py @@ -151,7 +151,7 @@ def _process(hass, text): if not entity_ids: _LOGGER.error( "Could not find entity id %s from text %s", name, text) - return + return None if command == 'on': yield from hass.services.async_call( @@ -169,6 +169,8 @@ def _process(hass, text): _LOGGER.error('Got unsupported command %s from text %s', command, text) + return None + class ConversationProcessView(http.HomeAssistantView): """View to retrieve shopping list content.""" @@ -194,4 +196,8 @@ class ConversationProcessView(http.HomeAssistantView): intent_result = yield from _process(hass, text) + if intent_result is None: + intent_result = intent.IntentResponse() + intent_result.async_set_speech("Sorry, I didn't understand that") + return self.json(intent_result) diff --git a/homeassistant/components/cover/velbus.py b/homeassistant/components/cover/velbus.py new file mode 100644 index 00000000000..ab5d6e8ef79 --- /dev/null +++ b/homeassistant/components/cover/velbus.py @@ -0,0 +1,160 @@ +""" +Support for Velbus covers. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/cover.velbus/ +""" +import logging +import asyncio +import time + +import voluptuous as vol + +from homeassistant.components.cover import ( + CoverDevice, PLATFORM_SCHEMA, SUPPORT_OPEN, SUPPORT_CLOSE, + SUPPORT_STOP) +from homeassistant.components.velbus import DOMAIN +from homeassistant.const import (CONF_COVERS, CONF_NAME) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +COVER_SCHEMA = vol.Schema({ + vol.Required('module'): cv.positive_int, + vol.Required('open_channel'): cv.positive_int, + vol.Required('close_channel'): cv.positive_int, + vol.Required(CONF_NAME): cv.string +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}), +}) + +DEPENDENCIES = ['velbus'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up cover controlled by Velbus.""" + devices = config.get(CONF_COVERS, {}) + covers = [] + + velbus = hass.data[DOMAIN] + for device_name, device_config in devices.items(): + covers.append( + VelbusCover( + velbus, + device_config.get(CONF_NAME, device_name), + device_config.get('module'), + device_config.get('open_channel'), + device_config.get('close_channel') + ) + ) + + if not covers: + _LOGGER.error("No covers added") + return False + + add_devices(covers) + + +class VelbusCover(CoverDevice): + """Representation a Velbus cover.""" + + def __init__(self, velbus, name, module, open_channel, close_channel): + """Initialize the cover.""" + self._velbus = velbus + self._name = name + self._close_channel_state = None + self._open_channel_state = None + self._module = module + self._open_channel = open_channel + self._close_channel = close_channel + + @asyncio.coroutine + def async_added_to_hass(self): + """Add listener for Velbus messages on bus.""" + def _init_velbus(): + """Initialize Velbus on startup.""" + self._velbus.subscribe(self._on_message) + self.get_status() + + yield from self.hass.async_add_job(_init_velbus) + + def _on_message(self, message): + import velbus + if isinstance(message, velbus.RelayStatusMessage): + if message.address == self._module: + if message.channel == self._close_channel: + self._close_channel_state = message.is_on() + self.schedule_update_ha_state() + if message.channel == self._open_channel: + self._open_channel_state = message.is_on() + self.schedule_update_ha_state() + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP + + @property + def should_poll(self): + """Disable polling.""" + return False + + @property + def name(self): + """Return the name of the cover.""" + return self._name + + @property + def is_closed(self): + """Return if the cover is closed.""" + return self._close_channel_state + + @property + def current_cover_position(self): + """Return current position of cover. + + None is unknown. + """ + return None + + def _relay_off(self, channel): + import velbus + message = velbus.SwitchRelayOffMessage() + message.set_defaults(self._module) + message.relay_channels = [channel] + self._velbus.send(message) + + def _relay_on(self, channel): + import velbus + message = velbus.SwitchRelayOnMessage() + message.set_defaults(self._module) + message.relay_channels = [channel] + self._velbus.send(message) + + def open_cover(self, **kwargs): + """Open the cover.""" + self._relay_off(self._close_channel) + time.sleep(0.3) + self._relay_on(self._open_channel) + + def close_cover(self, **kwargs): + """Close the cover.""" + self._relay_off(self._open_channel) + time.sleep(0.3) + self._relay_on(self._close_channel) + + def stop_cover(self, **kwargs): + """Stop the cover.""" + self._relay_off(self._open_channel) + time.sleep(0.3) + self._relay_off(self._close_channel) + + def get_status(self): + """Retrieve current status.""" + import velbus + message = velbus.ModuleStatusRequestMessage() + message.set_defaults(self._module) + message.channels = [self._open_channel, self._close_channel] + self._velbus.send(message) diff --git a/homeassistant/components/device_tracker/actiontec.py b/homeassistant/components/device_tracker/actiontec.py index 882df575385..64e1a60ad08 100644 --- a/homeassistant/components/device_tracker/actiontec.py +++ b/homeassistant/components/device_tracker/actiontec.py @@ -7,9 +7,7 @@ https://home-assistant.io/components/device_tracker.actiontec/ import logging import re import telnetlib -import threading from collections import namedtuple -from datetime import timedelta import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -17,9 +15,6 @@ import homeassistant.util.dt as dt_util from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.util import Throttle - -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) @@ -54,7 +49,6 @@ class ActiontecDeviceScanner(DeviceScanner): self.host = config[CONF_HOST] self.username = config[CONF_USERNAME] self.password = config[CONF_PASSWORD] - self.lock = threading.Lock() self.last_results = [] data = self.get_actiontec_data() self.success_init = data is not None @@ -74,7 +68,6 @@ class ActiontecDeviceScanner(DeviceScanner): return client.ip return None - @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """Ensure the information from the router is up to date. @@ -84,16 +77,15 @@ class ActiontecDeviceScanner(DeviceScanner): if not self.success_init: return False - with self.lock: - now = dt_util.now() - actiontec_data = self.get_actiontec_data() - if not actiontec_data: - return False - self.last_results = [Device(data['mac'], name, now) - for name, data in actiontec_data.items() - if data['timevalid'] > -60] - _LOGGER.info("Scan successful") - return True + now = dt_util.now() + actiontec_data = self.get_actiontec_data() + if not actiontec_data: + return False + self.last_results = [Device(data['mac'], name, now) + for name, data in actiontec_data.items() + if data['timevalid'] > -60] + _LOGGER.info("Scan successful") + return True def get_actiontec_data(self): """Retrieve data from Actiontec MI424WR and return parsed result.""" diff --git a/homeassistant/components/device_tracker/aruba.py b/homeassistant/components/device_tracker/aruba.py index bfb1588b323..cef5eabd901 100644 --- a/homeassistant/components/device_tracker/aruba.py +++ b/homeassistant/components/device_tracker/aruba.py @@ -6,8 +6,6 @@ https://home-assistant.io/components/device_tracker.aruba/ """ import logging import re -import threading -from datetime import timedelta import voluptuous as vol @@ -15,14 +13,11 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['pexpect==4.0.1'] -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) - _DEVICES_REGEX = re.compile( r'(?P([^\s]+))\s+' + r'(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})\s+' + @@ -52,8 +47,6 @@ class ArubaDeviceScanner(DeviceScanner): self.username = config[CONF_USERNAME] self.password = config[CONF_PASSWORD] - self.lock = threading.Lock() - self.last_results = {} # Test the router is accessible. @@ -74,7 +67,6 @@ class ArubaDeviceScanner(DeviceScanner): return client['name'] return None - @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """Ensure the information from the Aruba Access Point is up to date. @@ -83,13 +75,12 @@ class ArubaDeviceScanner(DeviceScanner): if not self.success_init: return False - with self.lock: - data = self.get_aruba_data() - if not data: - return False + data = self.get_aruba_data() + if not data: + return False - self.last_results = data.values() - return True + self.last_results = data.values() + return True def get_aruba_data(self): """Retrieve data from Aruba Access Point and return parsed result.""" diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index b28d16cc4a1..9b214441ac9 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -8,9 +8,7 @@ import logging import re import socket import telnetlib -import threading from collections import namedtuple -from datetime import timedelta import voluptuous as vol @@ -18,7 +16,6 @@ from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT) -from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pexpect==4.0.1'] @@ -32,8 +29,6 @@ CONF_SSH_KEY = 'ssh_key' DEFAULT_SSH_PORT = 22 -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) - SECRET_GROUP = 'Password or SSH Key' PLATFORM_SCHEMA = vol.All( @@ -123,8 +118,6 @@ class AsusWrtDeviceScanner(DeviceScanner): self.password, self.mode == "ap") - self.lock = threading.Lock() - self.last_results = {} # Test the router is accessible. @@ -145,7 +138,6 @@ class AsusWrtDeviceScanner(DeviceScanner): return client['host'] return None - @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """Ensure the information from the ASUSWRT router is up to date. @@ -154,19 +146,18 @@ class AsusWrtDeviceScanner(DeviceScanner): if not self.success_init: return False - with self.lock: - _LOGGER.info('Checking Devices') - data = self.get_asuswrt_data() - if not data: - return False + _LOGGER.info('Checking Devices') + data = self.get_asuswrt_data() + if not data: + return False - active_clients = [client for client in data.values() if - client['status'] == 'REACHABLE' or - client['status'] == 'DELAY' or - client['status'] == 'STALE' or - client['status'] == 'IN_ASSOCLIST'] - self.last_results = active_clients - return True + active_clients = [client for client in data.values() if + client['status'] == 'REACHABLE' or + client['status'] == 'DELAY' or + client['status'] == 'STALE' or + client['status'] == 'IN_ASSOCLIST'] + self.last_results = active_clients + return True def get_asuswrt_data(self): """Retrieve data from ASUSWRT and return parsed result.""" diff --git a/homeassistant/components/device_tracker/bt_home_hub_5.py b/homeassistant/components/device_tracker/bt_home_hub_5.py index 5c1a14b446b..a3b5bcac77c 100644 --- a/homeassistant/components/device_tracker/bt_home_hub_5.py +++ b/homeassistant/components/device_tracker/bt_home_hub_5.py @@ -6,8 +6,6 @@ https://home-assistant.io/components/device_tracker.bt_home_hub_5/ """ import logging import re -import threading -from datetime import timedelta import xml.etree.ElementTree as ET import json from urllib.parse import unquote @@ -19,13 +17,10 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import CONF_HOST -from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) _MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})') -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string }) @@ -46,11 +41,7 @@ class BTHomeHub5DeviceScanner(DeviceScanner): """Initialise the scanner.""" _LOGGER.info("Initialising BT Home Hub 5") self.host = config.get(CONF_HOST, '192.168.1.254') - - self.lock = threading.Lock() - self.last_results = {} - self.url = 'http://{}/nonAuth/home_status.xml'.format(self.host) # Test the router is accessible @@ -65,17 +56,15 @@ class BTHomeHub5DeviceScanner(DeviceScanner): def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" - with self.lock: - # If not initialised and not already scanned and not found. - if device not in self.last_results: - self._update_info() + # If not initialised and not already scanned and not found. + if device not in self.last_results: + self._update_info() - if not self.last_results: - return None + if not self.last_results: + return None - return self.last_results.get(device) + return self.last_results.get(device) - @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """Ensure the information from the BT Home Hub 5 is up to date. @@ -84,18 +73,17 @@ class BTHomeHub5DeviceScanner(DeviceScanner): if not self.success_init: return False - with self.lock: - _LOGGER.info("Scanning") + _LOGGER.info("Scanning") - data = _get_homehub_data(self.url) + data = _get_homehub_data(self.url) - if not data: - _LOGGER.warning("Error scanning devices") - return False + if not data: + _LOGGER.warning("Error scanning devices") + return False - self.last_results = data + self.last_results = data - return True + return True def _get_homehub_data(url): diff --git a/homeassistant/components/device_tracker/cisco_ios.py b/homeassistant/components/device_tracker/cisco_ios.py index 99ed06de486..0978ba99593 100644 --- a/homeassistant/components/device_tracker/cisco_ios.py +++ b/homeassistant/components/device_tracker/cisco_ios.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.cisco_ios/ """ import logging -from datetime import timedelta import voluptuous as vol @@ -14,9 +13,6 @@ from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, \ CONF_PORT -from homeassistant.util import Throttle - -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) @@ -65,7 +61,6 @@ class CiscoDeviceScanner(DeviceScanner): return self.last_results - @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """ Ensure the information from the Cisco router is up to date. diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index 4f1efcdb27c..3d36a1b428c 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -6,8 +6,6 @@ https://home-assistant.io/components/device_tracker.ddwrt/ """ import logging import re -import threading -from datetime import timedelta import requests import voluptuous as vol @@ -16,9 +14,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.util import Throttle - -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) @@ -50,8 +45,6 @@ class DdWrtDeviceScanner(DeviceScanner): self.username = config[CONF_USERNAME] self.password = config[CONF_PASSWORD] - self.lock = threading.Lock() - self.last_results = {} self.mac2name = {} @@ -69,68 +62,65 @@ class DdWrtDeviceScanner(DeviceScanner): def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" - with self.lock: - # If not initialised and not already scanned and not found. - if device not in self.mac2name: - url = 'http://{}/Status_Lan.live.asp'.format(self.host) - data = self.get_ddwrt_data(url) + # If not initialised and not already scanned and not found. + if device not in self.mac2name: + url = 'http://{}/Status_Lan.live.asp'.format(self.host) + data = self.get_ddwrt_data(url) - if not data: - return None + if not data: + return None - dhcp_leases = data.get('dhcp_leases', None) + dhcp_leases = data.get('dhcp_leases', None) - if not dhcp_leases: - return None + if not dhcp_leases: + return None - # Remove leading and trailing quotes and spaces - cleaned_str = dhcp_leases.replace( - "\"", "").replace("\'", "").replace(" ", "") - elements = cleaned_str.split(',') - num_clients = int(len(elements) / 5) - self.mac2name = {} - for idx in range(0, num_clients): - # The data is a single array - # every 5 elements represents one host, the MAC - # is the third element and the name is the first. - mac_index = (idx * 5) + 2 - if mac_index < len(elements): - mac = elements[mac_index] - self.mac2name[mac] = elements[idx * 5] + # Remove leading and trailing quotes and spaces + cleaned_str = dhcp_leases.replace( + "\"", "").replace("\'", "").replace(" ", "") + elements = cleaned_str.split(',') + num_clients = int(len(elements) / 5) + self.mac2name = {} + for idx in range(0, num_clients): + # The data is a single array + # every 5 elements represents one host, the MAC + # is the third element and the name is the first. + mac_index = (idx * 5) + 2 + if mac_index < len(elements): + mac = elements[mac_index] + self.mac2name[mac] = elements[idx * 5] - return self.mac2name.get(device) + return self.mac2name.get(device) - @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """Ensure the information from the DD-WRT router is up to date. Return boolean if scanning successful. """ - with self.lock: - _LOGGER.info("Checking ARP") + _LOGGER.info("Checking ARP") - url = 'http://{}/Status_Wireless.live.asp'.format(self.host) - data = self.get_ddwrt_data(url) + url = 'http://{}/Status_Wireless.live.asp'.format(self.host) + data = self.get_ddwrt_data(url) - if not data: - return False + if not data: + return False - self.last_results = [] + self.last_results = [] - active_clients = data.get('active_wireless', None) - if not active_clients: - return False + active_clients = data.get('active_wireless', None) + if not active_clients: + return False - # The DD-WRT UI uses its own data format and then - # regex's out values so this is done here too - # Remove leading and trailing single quotes. - clean_str = active_clients.strip().strip("'") - elements = clean_str.split("','") + # The DD-WRT UI uses its own data format and then + # regex's out values so this is done here too + # Remove leading and trailing single quotes. + clean_str = active_clients.strip().strip("'") + elements = clean_str.split("','") - self.last_results.extend(item for item in elements - if _MAC_REGEX.match(item)) + self.last_results.extend(item for item in elements + if _MAC_REGEX.match(item)) - return True + return True def get_ddwrt_data(self, url): """Retrieve data from DD-WRT and return parsed result.""" diff --git a/homeassistant/components/device_tracker/fritz.py b/homeassistant/components/device_tracker/fritz.py index 25de0a35c82..5210329179f 100644 --- a/homeassistant/components/device_tracker/fritz.py +++ b/homeassistant/components/device_tracker/fritz.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.fritz/ """ import logging -from datetime import timedelta import voluptuous as vol @@ -13,12 +12,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.util import Throttle REQUIREMENTS = ['fritzconnection==0.6.3'] -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) - _LOGGER = logging.getLogger(__name__) CONF_DEFAULT_IP = '169.254.1.1' # This IP is valid for all FRITZ!Box routers. @@ -88,7 +84,6 @@ class FritzBoxScanner(DeviceScanner): return None return ret - @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """Retrieve latest information from the FRITZ!Box.""" if not self.success_init: diff --git a/homeassistant/components/device_tracker/linksys_ap.py b/homeassistant/components/device_tracker/linksys_ap.py index 01f97eb6e42..196235f32f4 100644 --- a/homeassistant/components/device_tracker/linksys_ap.py +++ b/homeassistant/components/device_tracker/linksys_ap.py @@ -6,8 +6,6 @@ https://home-assistant.io/components/device_tracker.linksys_ap/ """ import base64 import logging -import threading -from datetime import timedelta import requests import voluptuous as vol @@ -16,9 +14,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL) -from homeassistant.util import Throttle -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) INTERFACES = 2 DEFAULT_TIMEOUT = 10 @@ -51,8 +47,6 @@ class LinksysAPDeviceScanner(object): self.username = config[CONF_USERNAME] self.password = config[CONF_PASSWORD] self.verify_ssl = config[CONF_VERIFY_SSL] - - self.lock = threading.Lock() self.last_results = [] # Check if the access point is accessible @@ -76,24 +70,22 @@ class LinksysAPDeviceScanner(object): """ return None - @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """Check for connected devices.""" from bs4 import BeautifulSoup as BS - with self.lock: - _LOGGER.info("Checking Linksys AP") + _LOGGER.info("Checking Linksys AP") - self.last_results = [] - for interface in range(INTERFACES): - request = self._make_request(interface) - self.last_results.extend( - [x.find_all('td')[1].text - for x in BS(request.content, "html.parser") - .find_all(class_='section-row')] - ) + self.last_results = [] + for interface in range(INTERFACES): + request = self._make_request(interface) + self.last_results.extend( + [x.find_all('td')[1].text + for x in BS(request.content, "html.parser") + .find_all(class_='section-row')] + ) - return True + return True def _make_request(self, unit=0): # No, the '&&' is not a typo - this is expected by the web interface. diff --git a/homeassistant/components/device_tracker/linksys_smart.py b/homeassistant/components/device_tracker/linksys_smart.py index e71502ba5ee..4bcbb600b8b 100644 --- a/homeassistant/components/device_tracker/linksys_smart.py +++ b/homeassistant/components/device_tracker/linksys_smart.py @@ -1,7 +1,5 @@ """Support for Linksys Smart Wifi routers.""" import logging -import threading -from datetime import timedelta import requests import voluptuous as vol @@ -10,9 +8,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import CONF_HOST -from homeassistant.util import Throttle -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) DEFAULT_TIMEOUT = 10 _LOGGER = logging.getLogger(__name__) @@ -36,8 +32,6 @@ class LinksysSmartWifiDeviceScanner(DeviceScanner): def __init__(self, config): """Initialize the scanner.""" self.host = config[CONF_HOST] - - self.lock = threading.Lock() self.last_results = {} # Check if the access point is accessible @@ -55,48 +49,46 @@ class LinksysSmartWifiDeviceScanner(DeviceScanner): """Return the name (if known) of the device.""" return self.last_results.get(mac) - @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """Check for connected devices.""" - with self.lock: - _LOGGER.info("Checking Linksys Smart Wifi") + _LOGGER.info("Checking Linksys Smart Wifi") - self.last_results = {} - response = self._make_request() - if response.status_code != 200: - _LOGGER.error( - "Got HTTP status code %d when getting device list", - response.status_code) - return False - try: - data = response.json() - result = data["responses"][0] - devices = result["output"]["devices"] - for device in devices: - macs = device["knownMACAddresses"] - if not macs: - _LOGGER.warning( - "Skipping device without known MAC address") - continue - mac = macs[-1] - connections = device["connections"] - if not connections: - _LOGGER.debug("Device %s is not connected", mac) - continue + self.last_results = {} + response = self._make_request() + if response.status_code != 200: + _LOGGER.error( + "Got HTTP status code %d when getting device list", + response.status_code) + return False + try: + data = response.json() + result = data["responses"][0] + devices = result["output"]["devices"] + for device in devices: + macs = device["knownMACAddresses"] + if not macs: + _LOGGER.warning( + "Skipping device without known MAC address") + continue + mac = macs[-1] + connections = device["connections"] + if not connections: + _LOGGER.debug("Device %s is not connected", mac) + continue - name = None - for prop in device["properties"]: - if prop["name"] == "userDeviceName": - name = prop["value"] - if not name: - name = device.get("friendlyName", device["deviceID"]) + name = None + for prop in device["properties"]: + if prop["name"] == "userDeviceName": + name = prop["value"] + if not name: + name = device.get("friendlyName", device["deviceID"]) - _LOGGER.debug("Device %s is connected", mac) - self.last_results[mac] = name - except (KeyError, IndexError): - _LOGGER.exception("Router returned unexpected response") - return False - return True + _LOGGER.debug("Device %s is connected", mac) + self.last_results[mac] = name + except (KeyError, IndexError): + _LOGGER.exception("Router returned unexpected response") + return False + return True def _make_request(self): # Weirdly enough, this doesn't seem to require authentication diff --git a/homeassistant/components/device_tracker/luci.py b/homeassistant/components/device_tracker/luci.py index 24af81b281e..a4b826a009f 100644 --- a/homeassistant/components/device_tracker/luci.py +++ b/homeassistant/components/device_tracker/luci.py @@ -7,8 +7,6 @@ https://home-assistant.io/components/device_tracker.luci/ import json import logging import re -import threading -from datetime import timedelta import requests import voluptuous as vol @@ -18,9 +16,6 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.util import Throttle - -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) @@ -55,12 +50,8 @@ class LuciDeviceScanner(DeviceScanner): self.parse_api_pattern = re.compile(r"(?P\w*) = (?P.*);") - self.lock = threading.Lock() - self.last_results = {} - self.refresh_token() - self.mac2name = None self.success_init = self.token is not None @@ -75,24 +66,22 @@ class LuciDeviceScanner(DeviceScanner): def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" - with self.lock: - if self.mac2name is None: - url = 'http://{}/cgi-bin/luci/rpc/uci'.format(self.host) - result = _req_json_rpc(url, 'get_all', 'dhcp', - params={'auth': self.token}) - if result: - hosts = [x for x in result.values() - if x['.type'] == 'host' and - 'mac' in x and 'name' in x] - mac2name_list = [ - (x['mac'].upper(), x['name']) for x in hosts] - self.mac2name = dict(mac2name_list) - else: - # Error, handled in the _req_json_rpc - return - return self.mac2name.get(device.upper(), None) + if self.mac2name is None: + url = 'http://{}/cgi-bin/luci/rpc/uci'.format(self.host) + result = _req_json_rpc(url, 'get_all', 'dhcp', + params={'auth': self.token}) + if result: + hosts = [x for x in result.values() + if x['.type'] == 'host' and + 'mac' in x and 'name' in x] + mac2name_list = [ + (x['mac'].upper(), x['name']) for x in hosts] + self.mac2name = dict(mac2name_list) + else: + # Error, handled in the _req_json_rpc + return + return self.mac2name.get(device.upper(), None) - @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """Ensure the information from the Luci router is up to date. @@ -101,31 +90,30 @@ class LuciDeviceScanner(DeviceScanner): if not self.success_init: return False - with self.lock: - _LOGGER.info("Checking ARP") + _LOGGER.info("Checking ARP") - url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host) - - try: - result = _req_json_rpc(url, 'net.arptable', - params={'auth': self.token}) - except InvalidLuciTokenError: - _LOGGER.info("Refreshing token") - self.refresh_token() - return False - - if result: - self.last_results = [] - for device_entry in result: - # Check if the Flags for each device contain - # NUD_REACHABLE and if so, add it to last_results - if int(device_entry['Flags'], 16) & 0x2: - self.last_results.append(device_entry['HW address']) - - return True + url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host) + try: + result = _req_json_rpc(url, 'net.arptable', + params={'auth': self.token}) + except InvalidLuciTokenError: + _LOGGER.info("Refreshing token") + self.refresh_token() return False + if result: + self.last_results = [] + for device_entry in result: + # Check if the Flags for each device contain + # NUD_REACHABLE and if so, add it to last_results + if int(device_entry['Flags'], 16) & 0x2: + self.last_results.append(device_entry['HW address']) + + return True + + return False + def _req_json_rpc(url, method, *args, **kwargs): """Perform one JSON RPC operation.""" diff --git a/homeassistant/components/device_tracker/mikrotik.py b/homeassistant/components/device_tracker/mikrotik.py index fc1918f08cc..4e43b6ac10d 100644 --- a/homeassistant/components/device_tracker/mikrotik.py +++ b/homeassistant/components/device_tracker/mikrotik.py @@ -5,25 +5,17 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.mikrotik/ """ import logging -import threading -from datetime import timedelta import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) -from homeassistant.const import (CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, - CONF_PORT) -from homeassistant.util import Throttle +from homeassistant.const import ( + CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT) REQUIREMENTS = ['librouteros==1.0.2'] -# Return cached results if last scan was less then this time ago. -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) - MTK_DEFAULT_API_PORT = '8728' _LOGGER = logging.getLogger(__name__) @@ -54,12 +46,9 @@ class MikrotikScanner(DeviceScanner): self.username = config[CONF_USERNAME] self.password = config[CONF_PASSWORD] - self.lock = threading.Lock() - self.connected = False self.success_init = False self.client = None - self.wireless_exist = None self.success_init = self.connect_to_device() @@ -118,51 +107,48 @@ class MikrotikScanner(DeviceScanner): def get_device_name(self, mac): """Return the name of the given device or None if we don't know.""" - with self.lock: - return self.last_results.get(mac) + return self.last_results.get(mac) - @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """Retrieve latest information from the Mikrotik box.""" - with self.lock: - if self.wireless_exist: - devices_tracker = 'wireless' - else: - devices_tracker = 'ip' + if self.wireless_exist: + devices_tracker = 'wireless' + else: + devices_tracker = 'ip' - _LOGGER.info( - "Loading %s devices from Mikrotik (%s) ...", - devices_tracker, - self.host + _LOGGER.info( + "Loading %s devices from Mikrotik (%s) ...", + devices_tracker, + self.host + ) + + device_names = self.client(cmd='/ip/dhcp-server/lease/getall') + if self.wireless_exist: + devices = self.client( + cmd='/interface/wireless/registration-table/getall' ) + else: + devices = device_names - device_names = self.client(cmd='/ip/dhcp-server/lease/getall') - if self.wireless_exist: - devices = self.client( - cmd='/interface/wireless/registration-table/getall' - ) - else: - devices = device_names + if device_names is None and devices is None: + return False - if device_names is None and devices is None: - return False + mac_names = {device.get('mac-address'): device.get('host-name') + for device in device_names + if device.get('mac-address')} - mac_names = {device.get('mac-address'): device.get('host-name') - for device in device_names - if device.get('mac-address')} + if self.wireless_exist: + self.last_results = { + device.get('mac-address'): + mac_names.get(device.get('mac-address')) + for device in devices + } + else: + self.last_results = { + device.get('mac-address'): + mac_names.get(device.get('mac-address')) + for device in device_names + if device.get('active-address') + } - if self.wireless_exist: - self.last_results = { - device.get('mac-address'): - mac_names.get(device.get('mac-address')) - for device in devices - } - else: - self.last_results = { - device.get('mac-address'): - mac_names.get(device.get('mac-address')) - for device in device_names - if device.get('active-address') - } - - return True + return True diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index b3ec442198e..d2b8bc274ca 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -5,8 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.netgear/ """ import logging -import threading -from datetime import timedelta import voluptuous as vol @@ -15,14 +13,11 @@ from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT) -from homeassistant.util import Throttle REQUIREMENTS = ['pynetgear==0.3.3'] _LOGGER = logging.getLogger(__name__) -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) - DEFAULT_HOST = 'routerlogin.net' DEFAULT_USER = 'admin' DEFAULT_PORT = 5000 @@ -56,8 +51,6 @@ class NetgearDeviceScanner(DeviceScanner): import pynetgear self.last_results = [] - self.lock = threading.Lock() - self._api = pynetgear.Netgear(password, host, username, port) _LOGGER.info("Logging in") @@ -85,7 +78,6 @@ class NetgearDeviceScanner(DeviceScanner): except StopIteration: return None - @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """Retrieve latest information from the Netgear router. @@ -94,12 +86,11 @@ class NetgearDeviceScanner(DeviceScanner): if not self.success_init: return - with self.lock: - _LOGGER.info("Scanning") + _LOGGER.info("Scanning") - results = self._api.get_attached_devices() + results = self._api.get_attached_devices() - if results is None: - _LOGGER.warning("Error scanning devices") + if results is None: + _LOGGER.warning("Error scanning devices") - self.last_results = results or [] + self.last_results = results or [] diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py index 8a845adf0b8..e9d70142ad1 100644 --- a/homeassistant/components/device_tracker/nmap_tracker.py +++ b/homeassistant/components/device_tracker/nmap_tracker.py @@ -4,11 +4,11 @@ Support for scanning a network with nmap. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.nmap_tracker/ """ +from datetime import timedelta import logging import re import subprocess from collections import namedtuple -from datetime import timedelta import voluptuous as vol @@ -17,7 +17,6 @@ import homeassistant.util.dt as dt_util from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import CONF_HOSTS -from homeassistant.util import Throttle REQUIREMENTS = ['python-nmap==0.6.1'] @@ -29,8 +28,6 @@ CONF_HOME_INTERVAL = 'home_interval' CONF_OPTIONS = 'scan_options' DEFAULT_OPTIONS = '-F --host-timeout 5s' -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOSTS): cv.ensure_list, @@ -97,7 +94,6 @@ class NmapDeviceScanner(DeviceScanner): return filter_named[0] return None - @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """Scan the network for devices. diff --git a/homeassistant/components/device_tracker/sky_hub.py b/homeassistant/components/device_tracker/sky_hub.py index ef58c50991c..c48c9bd029b 100644 --- a/homeassistant/components/device_tracker/sky_hub.py +++ b/homeassistant/components/device_tracker/sky_hub.py @@ -6,8 +6,6 @@ https://home-assistant.io/components/device_tracker.sky_hub/ """ import logging import re -import threading -from datetime import timedelta import requests import voluptuous as vol @@ -16,13 +14,10 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import CONF_HOST -from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) _MAC_REGEX = re.compile(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})') -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string }) @@ -43,11 +38,7 @@ class SkyHubDeviceScanner(DeviceScanner): """Initialise the scanner.""" _LOGGER.info("Initialising Sky Hub") self.host = config.get(CONF_HOST, '192.168.1.254') - - self.lock = threading.Lock() - self.last_results = {} - self.url = 'http://{}/'.format(self.host) # Test the router is accessible @@ -62,17 +53,15 @@ class SkyHubDeviceScanner(DeviceScanner): def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" - with self.lock: - # If not initialised and not already scanned and not found. - if device not in self.last_results: - self._update_info() + # If not initialised and not already scanned and not found. + if device not in self.last_results: + self._update_info() - if not self.last_results: - return None + if not self.last_results: + return None - return self.last_results.get(device) + return self.last_results.get(device) - @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """Ensure the information from the Sky Hub is up to date. @@ -81,18 +70,17 @@ class SkyHubDeviceScanner(DeviceScanner): if not self.success_init: return False - with self.lock: - _LOGGER.info("Scanning") + _LOGGER.info("Scanning") - data = _get_skyhub_data(self.url) + data = _get_skyhub_data(self.url) - if not data: - _LOGGER.warning('Error scanning devices') - return False + if not data: + _LOGGER.warning('Error scanning devices') + return False - self.last_results = data + self.last_results = data - return True + return True def _get_skyhub_data(url): diff --git a/homeassistant/components/device_tracker/snmp.py b/homeassistant/components/device_tracker/snmp.py index a3be40036cb..46b1686b21d 100644 --- a/homeassistant/components/device_tracker/snmp.py +++ b/homeassistant/components/device_tracker/snmp.py @@ -6,8 +6,6 @@ https://home-assistant.io/components/device_tracker.snmp/ """ import binascii import logging -import threading -from datetime import timedelta import voluptuous as vol @@ -15,7 +13,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import CONF_HOST -from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -28,8 +25,6 @@ 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, @@ -68,9 +63,6 @@ class SnmpScanner(DeviceScanner): privProtocol=cfg.usmAesCfb128Protocol ) self.baseoid = cmdgen.MibVariable(config[CONF_BASEOID]) - - self.lock = threading.Lock() - self.last_results = [] # Test the router is accessible @@ -90,7 +82,6 @@ class SnmpScanner(DeviceScanner): # We have no names return None - @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """Ensure the information from the device is up to date. @@ -99,13 +90,12 @@ class SnmpScanner(DeviceScanner): if not self.success_init: return False - with self.lock: - data = self.get_snmp_data() - if not data: - return False + data = self.get_snmp_data() + if not data: + return False - self.last_results = data - return True + self.last_results = data + return True def get_snmp_data(self): """Fetch MAC addresses from access point via SNMP.""" diff --git a/homeassistant/components/device_tracker/swisscom.py b/homeassistant/components/device_tracker/swisscom.py index d2a5a57e491..e64d30942ca 100644 --- a/homeassistant/components/device_tracker/swisscom.py +++ b/homeassistant/components/device_tracker/swisscom.py @@ -5,8 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.swisscom/ """ import logging -import threading -from datetime import timedelta import requests import voluptuous as vol @@ -15,9 +13,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import CONF_HOST -from homeassistant.util import Throttle - -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) @@ -41,9 +36,6 @@ class SwisscomDeviceScanner(DeviceScanner): def __init__(self, config): """Initialize the scanner.""" self.host = config[CONF_HOST] - - self.lock = threading.Lock() - self.last_results = {} # Test the router is accessible. @@ -64,7 +56,6 @@ class SwisscomDeviceScanner(DeviceScanner): return client['host'] return None - @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """Ensure the information from the Swisscom router is up to date. @@ -73,16 +64,15 @@ class SwisscomDeviceScanner(DeviceScanner): if not self.success_init: return False - with self.lock: - _LOGGER.info("Loading data from Swisscom Internet Box") - data = self.get_swisscom_data() - if not data: - return False + _LOGGER.info("Loading data from Swisscom Internet Box") + data = self.get_swisscom_data() + if not data: + return False - active_clients = [client for client in data.values() if - client['status']] - self.last_results = active_clients - return True + active_clients = [client for client in data.values() if + client['status']] + self.last_results = active_clients + return True def get_swisscom_data(self): """Retrieve data from Swisscom and return parsed result.""" diff --git a/homeassistant/components/device_tracker/thomson.py b/homeassistant/components/device_tracker/thomson.py index 6efe8d59beb..3fa161e467d 100644 --- a/homeassistant/components/device_tracker/thomson.py +++ b/homeassistant/components/device_tracker/thomson.py @@ -7,8 +7,6 @@ https://home-assistant.io/components/device_tracker.thomson/ import logging import re import telnetlib -import threading -from datetime import timedelta import voluptuous as vol @@ -16,9 +14,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.util import Throttle - -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) _LOGGER = logging.getLogger(__name__) @@ -54,9 +49,6 @@ class ThomsonDeviceScanner(DeviceScanner): self.host = config[CONF_HOST] self.username = config[CONF_USERNAME] self.password = config[CONF_PASSWORD] - - self.lock = threading.Lock() - self.last_results = {} # Test the router is accessible. @@ -77,7 +69,6 @@ class ThomsonDeviceScanner(DeviceScanner): return client['host'] return None - @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """Ensure the information from the THOMSON router is up to date. @@ -86,17 +77,16 @@ class ThomsonDeviceScanner(DeviceScanner): if not self.success_init: return False - with self.lock: - _LOGGER.info("Checking ARP") - data = self.get_thomson_data() - if not data: - return False + _LOGGER.info("Checking ARP") + data = self.get_thomson_data() + if not data: + return False - # Flag C stands for CONNECTED - active_clients = [client for client in data.values() if - client['status'].find('C') != -1] - self.last_results = active_clients - return True + # Flag C stands for CONNECTED + active_clients = [client for client in data.values() if + client['status'].find('C') != -1] + self.last_results = active_clients + return True def get_thomson_data(self): """Retrieve data from THOMSON and return parsed result.""" diff --git a/homeassistant/components/device_tracker/tomato.py b/homeassistant/components/device_tracker/tomato.py index 0b330c933d8..57e83eaeb94 100644 --- a/homeassistant/components/device_tracker/tomato.py +++ b/homeassistant/components/device_tracker/tomato.py @@ -7,8 +7,6 @@ https://home-assistant.io/components/device_tracker.tomato/ import json import logging import re -import threading -from datetime import timedelta import requests import voluptuous as vol @@ -17,9 +15,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.util import Throttle - -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) CONF_HTTP_ID = 'http_id' @@ -54,8 +49,6 @@ class TomatoDeviceScanner(DeviceScanner): self.parse_api_pattern = re.compile(r"(?P\w*) = (?P.*);") self.logger = logging.getLogger("{}.{}".format(__name__, "Tomato")) - self.lock = threading.Lock() - self.last_results = {"wldev": [], "dhcpd_lease": []} self.success_init = self._update_tomato_info() @@ -76,50 +69,48 @@ class TomatoDeviceScanner(DeviceScanner): return filter_named[0] - @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_tomato_info(self): """Ensure the information from the Tomato router is up to date. Return boolean if scanning successful. """ - with self.lock: - self.logger.info("Scanning") + self.logger.info("Scanning") - try: - response = requests.Session().send(self.req, timeout=3) - # Calling and parsing the Tomato api here. We only need the - # wldev and dhcpd_lease values. - if response.status_code == 200: + try: + response = requests.Session().send(self.req, timeout=3) + # Calling and parsing the Tomato api here. We only need the + # wldev and dhcpd_lease values. + if response.status_code == 200: - for param, value in \ - self.parse_api_pattern.findall(response.text): + for param, value in \ + self.parse_api_pattern.findall(response.text): - if param == 'wldev' or param == 'dhcpd_lease': - self.last_results[param] = \ - json.loads(value.replace("'", '"')) - return True + if param == 'wldev' or param == 'dhcpd_lease': + self.last_results[param] = \ + json.loads(value.replace("'", '"')) + return True - elif response.status_code == 401: - # Authentication error - self.logger.exception(( - "Failed to authenticate, " - "please check your username and password")) - return False - - except requests.exceptions.ConnectionError: - # We get this if we could not connect to the router or - # an invalid http_id was supplied. - self.logger.exception("Failed to connect to the router or " - "invalid http_id supplied") + elif response.status_code == 401: + # Authentication error + self.logger.exception(( + "Failed to authenticate, " + "please check your username and password")) return False - except requests.exceptions.Timeout: - # We get this if we could not connect to the router or - # an invalid http_id was supplied. - self.logger.exception("Connection to the router timed out") - return False + except requests.exceptions.ConnectionError: + # We get this if we could not connect to the router or + # an invalid http_id was supplied. + self.logger.exception("Failed to connect to the router or " + "invalid http_id supplied") + return False - except ValueError: - # If JSON decoder could not parse the response. - self.logger.exception("Failed to parse response from router") - return False + except requests.exceptions.Timeout: + # We get this if we could not connect to the router or + # an invalid http_id was supplied. + self.logger.exception("Connection to the router timed out") + return False + + except ValueError: + # If JSON decoder could not parse the response. + self.logger.exception("Failed to parse response from router") + return False diff --git a/homeassistant/components/device_tracker/tplink.py b/homeassistant/components/device_tracker/tplink.py index ccf0c2d01af..a52de48d061 100755 --- a/homeassistant/components/device_tracker/tplink.py +++ b/homeassistant/components/device_tracker/tplink.py @@ -8,8 +8,7 @@ import base64 import hashlib import logging import re -import threading -from datetime import timedelta, datetime +from datetime import datetime import requests import voluptuous as vol @@ -18,9 +17,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.util import Throttle - -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) @@ -59,7 +55,6 @@ class TplinkDeviceScanner(DeviceScanner): self.password = password self.last_results = {} - self.lock = threading.Lock() self.success_init = self._update_info() def scan_devices(self): @@ -72,28 +67,26 @@ class TplinkDeviceScanner(DeviceScanner): """Get firmware doesn't save the name of the wireless device.""" return None - @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """Ensure the information from the TP-Link router is up to date. Return boolean if scanning successful. """ - with self.lock: - _LOGGER.info("Loading wireless clients...") + _LOGGER.info("Loading wireless clients...") - url = 'http://{}/userRpm/WlanStationRpm.htm'.format(self.host) - referer = 'http://{}'.format(self.host) - page = requests.get( - url, auth=(self.username, self.password), - headers={'referer': referer}, timeout=4) + url = 'http://{}/userRpm/WlanStationRpm.htm'.format(self.host) + referer = 'http://{}'.format(self.host) + page = requests.get( + url, auth=(self.username, self.password), + headers={'referer': referer}, timeout=4) - result = self.parse_macs.findall(page.text) + result = self.parse_macs.findall(page.text) - if result: - self.last_results = [mac.replace("-", ":") for mac in result] - return True + if result: + self.last_results = [mac.replace("-", ":") for mac in result] + return True - return False + return False class Tplink2DeviceScanner(TplinkDeviceScanner): @@ -109,48 +102,46 @@ class Tplink2DeviceScanner(TplinkDeviceScanner): """Get firmware doesn't save the name of the wireless device.""" return self.last_results.get(device) - @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """Ensure the information from the TP-Link router is up to date. Return boolean if scanning successful. """ - with self.lock: - _LOGGER.info("Loading wireless clients...") + _LOGGER.info("Loading wireless clients...") - url = 'http://{}/data/map_access_wireless_client_grid.json' \ - .format(self.host) - referer = 'http://{}'.format(self.host) + url = 'http://{}/data/map_access_wireless_client_grid.json' \ + .format(self.host) + referer = 'http://{}'.format(self.host) - # Router uses Authorization cookie instead of header - # Let's create the cookie - username_password = '{}:{}'.format(self.username, self.password) - b64_encoded_username_password = base64.b64encode( - username_password.encode('ascii') - ).decode('ascii') - cookie = 'Authorization=Basic {}' \ - .format(b64_encoded_username_password) + # Router uses Authorization cookie instead of header + # Let's create the cookie + username_password = '{}:{}'.format(self.username, self.password) + b64_encoded_username_password = base64.b64encode( + username_password.encode('ascii') + ).decode('ascii') + cookie = 'Authorization=Basic {}' \ + .format(b64_encoded_username_password) - response = requests.post( - url, headers={'referer': referer, 'cookie': cookie}, - timeout=4) - - try: - result = response.json().get('data') - except ValueError: - _LOGGER.error("Router didn't respond with JSON. " - "Check if credentials are correct.") - return False - - if result: - self.last_results = { - device['mac_addr'].replace('-', ':'): device['name'] - for device in result - } - return True + response = requests.post( + url, headers={'referer': referer, 'cookie': cookie}, + timeout=4) + try: + result = response.json().get('data') + except ValueError: + _LOGGER.error("Router didn't respond with JSON. " + "Check if credentials are correct.") return False + if result: + self.last_results = { + device['mac_addr'].replace('-', ':'): device['name'] + for device in result + } + return True + + return False + class Tplink3DeviceScanner(TplinkDeviceScanner): """This class queries the Archer C9 router with version 150811 or high.""" @@ -202,70 +193,67 @@ class Tplink3DeviceScanner(TplinkDeviceScanner): response.text) return False - @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """Ensure the information from the TP-Link router is up to date. Return boolean if scanning successful. """ - with self.lock: - if (self.stok == '') or (self.sysauth == ''): - self._get_auth_tokens() + if (self.stok == '') or (self.sysauth == ''): + self._get_auth_tokens() - _LOGGER.info("Loading wireless clients...") + _LOGGER.info("Loading wireless clients...") - url = ('http://{}/cgi-bin/luci/;stok={}/admin/wireless?' - 'form=statistics').format(self.host, self.stok) - referer = 'http://{}/webpages/index.html'.format(self.host) + url = ('http://{}/cgi-bin/luci/;stok={}/admin/wireless?' + 'form=statistics').format(self.host, self.stok) + referer = 'http://{}/webpages/index.html'.format(self.host) - response = requests.post(url, - params={'operation': 'load'}, - headers={'referer': referer}, - cookies={'sysauth': self.sysauth}, - timeout=5) + response = requests.post(url, + params={'operation': 'load'}, + headers={'referer': referer}, + cookies={'sysauth': self.sysauth}, + timeout=5) - try: - json_response = response.json() + try: + json_response = response.json() - if json_response.get('success'): - result = response.json().get('data') - else: - if json_response.get('errorcode') == 'timeout': - _LOGGER.info("Token timed out. Relogging on next scan") - self.stok = '' - self.sysauth = '' - return False - _LOGGER.error( - "An unknown error happened while fetching data") + if json_response.get('success'): + result = response.json().get('data') + else: + if json_response.get('errorcode') == 'timeout': + _LOGGER.info("Token timed out. Relogging on next scan") + self.stok = '' + self.sysauth = '' return False - except ValueError: - _LOGGER.error("Router didn't respond with JSON. " - "Check if credentials are correct") + _LOGGER.error( + "An unknown error happened while fetching data") return False - - if result: - self.last_results = { - device['mac'].replace('-', ':'): device['mac'] - for device in result - } - return True - + except ValueError: + _LOGGER.error("Router didn't respond with JSON. " + "Check if credentials are correct") return False + if result: + self.last_results = { + device['mac'].replace('-', ':'): device['mac'] + for device in result + } + return True + + return False + def _log_out(self): - with self.lock: - _LOGGER.info("Logging out of router admin interface...") + _LOGGER.info("Logging out of router admin interface...") - url = ('http://{}/cgi-bin/luci/;stok={}/admin/system?' - 'form=logout').format(self.host, self.stok) - referer = 'http://{}/webpages/index.html'.format(self.host) + url = ('http://{}/cgi-bin/luci/;stok={}/admin/system?' + 'form=logout').format(self.host, self.stok) + referer = 'http://{}/webpages/index.html'.format(self.host) - requests.post(url, - params={'operation': 'write'}, - headers={'referer': referer}, - cookies={'sysauth': self.sysauth}) - self.stok = '' - self.sysauth = '' + requests.post(url, + params={'operation': 'write'}, + headers={'referer': referer}, + cookies={'sysauth': self.sysauth}) + self.stok = '' + self.sysauth = '' class Tplink4DeviceScanner(TplinkDeviceScanner): @@ -318,38 +306,36 @@ class Tplink4DeviceScanner(TplinkDeviceScanner): _LOGGER.error("Couldn't fetch auth tokens") return False - @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """Ensure the information from the TP-Link router is up to date. Return boolean if scanning successful. """ - with self.lock: - if (self.credentials == '') or (self.token == ''): - self._get_auth_tokens() + if (self.credentials == '') or (self.token == ''): + self._get_auth_tokens() - _LOGGER.info("Loading wireless clients...") + _LOGGER.info("Loading wireless clients...") - mac_results = [] + mac_results = [] - # Check both the 2.4GHz and 5GHz client list URLs - for clients_url in ('WlanStationRpm.htm', 'WlanStationRpm_5g.htm'): - url = 'http://{}/{}/userRpm/{}' \ - .format(self.host, self.token, clients_url) - referer = 'http://{}'.format(self.host) - cookie = 'Authorization=Basic {}'.format(self.credentials) + # Check both the 2.4GHz and 5GHz client list URLs + for clients_url in ('WlanStationRpm.htm', 'WlanStationRpm_5g.htm'): + url = 'http://{}/{}/userRpm/{}' \ + .format(self.host, self.token, clients_url) + referer = 'http://{}'.format(self.host) + cookie = 'Authorization=Basic {}'.format(self.credentials) - page = requests.get(url, headers={ - 'cookie': cookie, - 'referer': referer - }) - mac_results.extend(self.parse_macs.findall(page.text)) + page = requests.get(url, headers={ + 'cookie': cookie, + 'referer': referer + }) + mac_results.extend(self.parse_macs.findall(page.text)) - if not mac_results: - return False + if not mac_results: + return False - self.last_results = [mac.replace("-", ":") for mac in mac_results] - return True + self.last_results = [mac.replace("-", ":") for mac in mac_results] + return True class Tplink5DeviceScanner(TplinkDeviceScanner): @@ -365,69 +351,67 @@ class Tplink5DeviceScanner(TplinkDeviceScanner): """Get firmware doesn't save the name of the wireless device.""" return None - @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """Ensure the information from the TP-Link AP is up to date. Return boolean if scanning successful. """ - with self.lock: - _LOGGER.info("Loading wireless clients...") + _LOGGER.info("Loading wireless clients...") - base_url = 'http://{}'.format(self.host) + base_url = 'http://{}'.format(self.host) - header = { - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12;" - " rv:53.0) Gecko/20100101 Firefox/53.0", - "Accept": "application/json, text/javascript, */*; q=0.01", - "Accept-Language": "Accept-Language: en-US,en;q=0.5", - "Accept-Encoding": "gzip, deflate", - "Content-Type": "application/x-www-form-urlencoded; " - "charset=UTF-8", - "X-Requested-With": "XMLHttpRequest", - "Referer": "http://" + self.host + "/", - "Connection": "keep-alive", - "Pragma": "no-cache", - "Cache-Control": "no-cache" - } + header = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12;" + " rv:53.0) Gecko/20100101 Firefox/53.0", + "Accept": "application/json, text/javascript, */*; q=0.01", + "Accept-Language": "Accept-Language: en-US,en;q=0.5", + "Accept-Encoding": "gzip, deflate", + "Content-Type": "application/x-www-form-urlencoded; " + "charset=UTF-8", + "X-Requested-With": "XMLHttpRequest", + "Referer": "http://" + self.host + "/", + "Connection": "keep-alive", + "Pragma": "no-cache", + "Cache-Control": "no-cache" + } - password_md5 = hashlib.md5( - self.password.encode('utf')).hexdigest().upper() + password_md5 = hashlib.md5( + self.password.encode('utf')).hexdigest().upper() - # create a session to handle cookie easier - session = requests.session() - session.get(base_url, headers=header) + # create a session to handle cookie easier + session = requests.session() + session.get(base_url, headers=header) - login_data = {"username": self.username, "password": password_md5} - session.post(base_url, login_data, headers=header) + login_data = {"username": self.username, "password": password_md5} + session.post(base_url, login_data, headers=header) - # a timestamp is required to be sent as get parameter - timestamp = int(datetime.now().timestamp() * 1e3) + # a timestamp is required to be sent as get parameter + timestamp = int(datetime.now().timestamp() * 1e3) - client_list_url = '{}/data/monitor.client.client.json'.format( - base_url) + client_list_url = '{}/data/monitor.client.client.json'.format( + base_url) - get_params = { - 'operation': 'load', - '_': timestamp - } - - response = session.get(client_list_url, - headers=header, - params=get_params) - session.close() - try: - list_of_devices = response.json() - except ValueError: - _LOGGER.error("AP didn't respond with JSON. " - "Check if credentials are correct.") - return False - - if list_of_devices: - self.last_results = { - device['MAC'].replace('-', ':'): device['DeviceName'] - for device in list_of_devices['data'] - } - return True + get_params = { + 'operation': 'load', + '_': timestamp + } + response = session.get(client_list_url, + headers=header, + params=get_params) + session.close() + try: + list_of_devices = response.json() + except ValueError: + _LOGGER.error("AP didn't respond with JSON. " + "Check if credentials are correct.") return False + + if list_of_devices: + self.last_results = { + device['MAC'].replace('-', ':'): device['DeviceName'] + for device in list_of_devices['data'] + } + return True + + return False diff --git a/homeassistant/components/device_tracker/ubus.py b/homeassistant/components/device_tracker/ubus.py index e3cef60c376..64b9a633cbd 100644 --- a/homeassistant/components/device_tracker/ubus.py +++ b/homeassistant/components/device_tracker/ubus.py @@ -7,8 +7,6 @@ https://home-assistant.io/components/device_tracker.ubus/ import json import logging import re -import threading -from datetime import timedelta import requests import voluptuous as vol @@ -17,12 +15,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.util import Throttle from homeassistant.exceptions import HomeAssistantError -# Return cached results if last scan was less then this time ago. -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) - _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -70,7 +64,6 @@ class UbusDeviceScanner(DeviceScanner): self.password = config[CONF_PASSWORD] self.parse_api_pattern = re.compile(r"(?P\w*) = (?P.*);") - self.lock = threading.Lock() self.last_results = {} self.url = 'http://{}/ubus'.format(host) @@ -89,33 +82,31 @@ class UbusDeviceScanner(DeviceScanner): @_refresh_on_acccess_denied def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" - with self.lock: - if self.leasefile is None: - result = _req_json_rpc( - self.url, self.session_id, 'call', 'uci', 'get', - config="dhcp", type="dnsmasq") - if result: - values = result["values"].values() - self.leasefile = next(iter(values))["leasefile"] - else: - return + if self.leasefile is None: + result = _req_json_rpc( + self.url, self.session_id, 'call', 'uci', 'get', + config="dhcp", type="dnsmasq") + if result: + values = result["values"].values() + self.leasefile = next(iter(values))["leasefile"] + else: + return - if self.mac2name is None: - result = _req_json_rpc( - self.url, self.session_id, 'call', 'file', 'read', - path=self.leasefile) - if result: - self.mac2name = dict() - for line in result["data"].splitlines(): - hosts = line.split(" ") - self.mac2name[hosts[1].upper()] = hosts[3] - else: - # Error, handled in the _req_json_rpc - return + if self.mac2name is None: + result = _req_json_rpc( + self.url, self.session_id, 'call', 'file', 'read', + path=self.leasefile) + if result: + self.mac2name = dict() + for line in result["data"].splitlines(): + hosts = line.split(" ") + self.mac2name[hosts[1].upper()] = hosts[3] + else: + # Error, handled in the _req_json_rpc + return - return self.mac2name.get(device.upper(), None) + return self.mac2name.get(device.upper(), None) - @Throttle(MIN_TIME_BETWEEN_SCANS) @_refresh_on_acccess_denied def _update_info(self): """Ensure the information from the Luci router is up to date. @@ -125,25 +116,24 @@ class UbusDeviceScanner(DeviceScanner): if not self.success_init: return False - with self.lock: - _LOGGER.info("Checking ARP") + _LOGGER.info("Checking ARP") - if not self.hostapd: - hostapd = _req_json_rpc( - self.url, self.session_id, 'list', 'hostapd.*', '') - self.hostapd.extend(hostapd.keys()) + if not self.hostapd: + hostapd = _req_json_rpc( + self.url, self.session_id, 'list', 'hostapd.*', '') + self.hostapd.extend(hostapd.keys()) - self.last_results = [] - results = 0 - for hostapd in self.hostapd: - result = _req_json_rpc( - self.url, self.session_id, 'call', hostapd, 'get_clients') + self.last_results = [] + results = 0 + for hostapd in self.hostapd: + result = _req_json_rpc( + self.url, self.session_id, 'call', hostapd, 'get_clients') - if result: - results = results + 1 - self.last_results.extend(result['clients'].keys()) + if result: + results = results + 1 + self.last_results.extend(result['clients'].keys()) - return bool(results) + return bool(results) def _req_json_rpc(url, session_id, rpcmethod, subsystem, method, **params): diff --git a/homeassistant/components/device_tracker/volvooncall.py b/homeassistant/components/device_tracker/volvooncall.py index 9bd5727510a..4312c5dd54a 100644 --- a/homeassistant/components/device_tracker/volvooncall.py +++ b/homeassistant/components/device_tracker/volvooncall.py @@ -9,8 +9,7 @@ import logging from homeassistant.util import slugify from homeassistant.helpers.dispatcher import ( dispatcher_connect, dispatcher_send) -from homeassistant.components.volvooncall import ( - DATA_KEY, SIGNAL_VEHICLE_SEEN) +from homeassistant.components.volvooncall import DATA_KEY, SIGNAL_VEHICLE_SEEN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/device_tracker/xiaomi.py b/homeassistant/components/device_tracker/xiaomi.py index a7b0a1ad326..8b8db3da2d8 100644 --- a/homeassistant/components/device_tracker/xiaomi.py +++ b/homeassistant/components/device_tracker/xiaomi.py @@ -5,8 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.xiaomi/ """ import logging -import threading -from datetime import timedelta import requests import voluptuous as vol @@ -15,12 +13,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME, default='admin'): cv.string, @@ -47,8 +42,6 @@ class XiaomiDeviceScanner(DeviceScanner): self.username = config[CONF_USERNAME] self.password = config[CONF_PASSWORD] - self.lock = threading.Lock() - self.last_results = {} self.token = _get_token(self.host, self.username, self.password) @@ -62,21 +55,19 @@ class XiaomiDeviceScanner(DeviceScanner): def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" - with self.lock: - if self.mac2name is None: - result = self._retrieve_list_with_retry() - if result: - hosts = [x for x in result - if 'mac' in x and 'name' in x] - mac2name_list = [ - (x['mac'].upper(), x['name']) for x in hosts] - self.mac2name = dict(mac2name_list) - else: - # Error, handled in the _retrieve_list_with_retry - return - return self.mac2name.get(device.upper(), None) + if self.mac2name is None: + result = self._retrieve_list_with_retry() + if result: + hosts = [x for x in result + if 'mac' in x and 'name' in x] + mac2name_list = [ + (x['mac'].upper(), x['name']) for x in hosts] + self.mac2name = dict(mac2name_list) + else: + # Error, handled in the _retrieve_list_with_retry + return + return self.mac2name.get(device.upper(), None) - @Throttle(MIN_TIME_BETWEEN_SCANS) def _update_info(self): """Ensure the informations from the router are up to date. @@ -85,12 +76,11 @@ class XiaomiDeviceScanner(DeviceScanner): if not self.success_init: return False - with self.lock: - result = self._retrieve_list_with_retry() - if result: - self._store_result(result) - return True - return False + result = self._retrieve_list_with_retry() + if result: + self._store_result(result) + return True + return False def _retrieve_list_with_retry(self): """Retrieve the device list with a retry if token is invalid. diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 3dfe4b9731c..af4604cb7d7 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.discovery import async_load_platform, async_discover import homeassistant.util.dt as dt_util -REQUIREMENTS = ['netdisco==1.0.1'] +REQUIREMENTS = ['netdisco==1.1.0'] DOMAIN = 'discovery' diff --git a/homeassistant/components/fan/velbus.py b/homeassistant/components/fan/velbus.py new file mode 100644 index 00000000000..c0d125aa5ab --- /dev/null +++ b/homeassistant/components/fan/velbus.py @@ -0,0 +1,187 @@ +""" +Support for Velbus platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/fan.velbus/ +""" +import asyncio +import logging +import voluptuous as vol + +from homeassistant.components.fan import ( + SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, FanEntity, SUPPORT_SET_SPEED, + PLATFORM_SCHEMA) +from homeassistant.components.velbus import DOMAIN +from homeassistant.const import CONF_NAME, CONF_DEVICES, STATE_OFF +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ['velbus'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [ + { + vol.Required('module'): cv.positive_int, + vol.Required('channel_low'): cv.positive_int, + vol.Required('channel_medium'): cv.positive_int, + vol.Required('channel_high'): cv.positive_int, + vol.Required(CONF_NAME): cv.string, + } + ]) +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up Fans.""" + velbus = hass.data[DOMAIN] + add_devices(VelbusFan(fan, velbus) for fan in config[CONF_DEVICES]) + + +class VelbusFan(FanEntity): + """Representation of a Velbus Fan.""" + + def __init__(self, fan, velbus): + """Initialize a Velbus light.""" + self._velbus = velbus + self._name = fan[CONF_NAME] + self._module = fan['module'] + self._channel_low = fan['channel_low'] + self._channel_medium = fan['channel_medium'] + self._channel_high = fan['channel_high'] + self._channels = [self._channel_low, self._channel_medium, + self._channel_high] + self._channels_state = [False, False, False] + self._speed = STATE_OFF + + @asyncio.coroutine + def async_added_to_hass(self): + """Add listener for Velbus messages on bus.""" + def _init_velbus(): + """Initialize Velbus on startup.""" + self._velbus.subscribe(self._on_message) + self.get_status() + + yield from self.hass.async_add_job(_init_velbus) + + def _on_message(self, message): + import velbus + if isinstance(message, velbus.RelayStatusMessage) and \ + message.address == self._module and \ + message.channel in self._channels: + if message.channel == self._channel_low: + self._channels_state[0] = message.is_on() + elif message.channel == self._channel_medium: + self._channels_state[1] = message.is_on() + elif message.channel == self._channel_high: + self._channels_state[2] = message.is_on() + self._calculate_speed() + self.schedule_update_ha_state() + + def _calculate_speed(self): + if self._is_off(): + self._speed = STATE_OFF + elif self._is_low(): + self._speed = SPEED_LOW + elif self._is_medium(): + self._speed = SPEED_MEDIUM + elif self._is_high(): + self._speed = SPEED_HIGH + + def _is_off(self): + return self._channels_state[0] is False and \ + self._channels_state[1] is False and \ + self._channels_state[2] is False + + def _is_low(self): + return self._channels_state[0] is True and \ + self._channels_state[1] is False and \ + self._channels_state[2] is False + + def _is_medium(self): + return self._channels_state[0] is True and \ + self._channels_state[1] is True and \ + self._channels_state[2] is False + + def _is_high(self): + return self._channels_state[0] is True and \ + self._channels_state[1] is False and \ + self._channels_state[2] is True + + @property + def name(self): + """Return the display name of this light.""" + return self._name + + @property + def should_poll(self): + """Disable polling.""" + return False + + @property + def speed(self): + """Return the current speed.""" + return self._speed + + @property + def speed_list(self): + """Get the list of available speeds.""" + return [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + + def turn_on(self, speed, **kwargs): + """Turn on the entity.""" + if speed is None: + speed = SPEED_MEDIUM + self.set_speed(speed) + + def turn_off(self): + """Turn off the entity.""" + self.set_speed(STATE_OFF) + + def set_speed(self, speed): + """Set the speed of the fan.""" + channels_off = [] + channels_on = [] + if speed == STATE_OFF: + channels_off = self._channels + elif speed == SPEED_LOW: + channels_off = [self._channel_medium, self._channel_high] + channels_on = [self._channel_low] + elif speed == SPEED_MEDIUM: + channels_off = [self._channel_high] + channels_on = [self._channel_low, self._channel_medium] + elif speed == SPEED_HIGH: + channels_off = [self._channel_medium] + channels_on = [self._channel_low, self._channel_high] + for channel in channels_off: + self._relay_off(channel) + for channel in channels_on: + self._relay_on(channel) + self.schedule_update_ha_state() + + def _relay_on(self, channel): + import velbus + message = velbus.SwitchRelayOnMessage() + message.set_defaults(self._module) + message.relay_channels = [channel] + self._velbus.send(message) + + def _relay_off(self, channel): + import velbus + message = velbus.SwitchRelayOffMessage() + message.set_defaults(self._module) + message.relay_channels = [channel] + self._velbus.send(message) + + def get_status(self): + """Retrieve current status.""" + import velbus + message = velbus.ModuleStatusRequestMessage() + message.set_defaults(self._module) + message.channels = self._channels + self._velbus.send(message) + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_SET_SPEED diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 6ab94fe5a78..a0958f65d95 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -3,22 +3,22 @@ FINGERPRINTS = { "compatibility.js": "8e4c44b5f4288cc48ec1ba94a9bec812", "core.js": "d4a7cb8c80c62b536764e0e81385f6aa", - "frontend.html": "7bd9aa75b2602768e66cf7e20845d7c4", + "frontend.html": "c44e49b9a0d9b9e4a626b7af34ca97d0", "mdi.html": "e91f61a039ed0a9936e7ee5360da3870", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", - "panels/ha-panel-automation.html": "72a5c1856cece8d9246328e84185ab0b", - "panels/ha-panel-config.html": "c0e043028cfa75d6d4dc5e0de0bb6dc1", - "panels/ha-panel-dev-event.html": "4886c821235492b1b92739b580d21c61", + "panels/ha-panel-automation.html": "1982116c49ad26ee8d89295edc797084", + "panels/ha-panel-config.html": "fafeac72f83dd6cc42218f8978f6a7af", + "panels/ha-panel-dev-event.html": "77784d5f0c73fcc3b29b6cc050bdf324", "panels/ha-panel-dev-info.html": "24e888ec7a8acd0c395b34396e9001bc", - "panels/ha-panel-dev-service.html": "ac2c50e486927dc4443e93d79f08c06e", - "panels/ha-panel-dev-state.html": "8f1a27c04db6329d31cfcc7d0d6a0869", - "panels/ha-panel-dev-template.html": "82cd543177c417e5c6612e07df851e6b", - "panels/ha-panel-hassio.html": "96d563215cf7bf7b0eeaf8625bafa4ef", + "panels/ha-panel-dev-service.html": "86a42a17f4894478b6b77bc636beafd0", + "panels/ha-panel-dev-state.html": "31ef6ffe3347cdda5bb0cbbc54b62cde", + "panels/ha-panel-dev-template.html": "d1d76e20fe9622cddee33e67318abde8", + "panels/ha-panel-hassio.html": "262d31efd9add719e0325da5cf79a096", "panels/ha-panel-history.html": "35177e2046c9a4191c8f51f8160255ce", "panels/ha-panel-iframe.html": "238189f21e670b6dcfac937e5ebd7d3b", "panels/ha-panel-kiosk.html": "2ac2df41bd447600692a0054892fc094", "panels/ha-panel-logbook.html": "7c45bd41c146ec38b9938b8a5188bb0d", - "panels/ha-panel-map.html": "b4923812c695dd8a69ad3da380ffe7b4", - "panels/ha-panel-shopping-list.html": "75602d06b41702c8093bd91c10374101", - "panels/ha-panel-zwave.html": "8c8e7844d33163f560e1f691550a8369" + "panels/ha-panel-map.html": "50501cd53eb4304e9e46eb719aa894b7", + "panels/ha-panel-shopping-list.html": "c04af28c6475b90cbf2cf63ba1b841d0", + "panels/ha-panel-zwave.html": "422f95f820f8b6b231265351ffcf4dd1" } diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 3205a1d7d4f..ca86fd55d22 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -1,5 +1,5 @@ - \ No newline at end of file +;if(t)for(var n in e){var r=t[n];if(r)for(var s,i=0,o=r.length;i1){for(var a=0;a+~])"},resolveCss:Polymer.ResolveUrl.resolveCss,parser:Polymer.CssParse,ruleTypes:Polymer.CssParse.types}}(),Polymer.StyleTransformer=function(){var e=Polymer.StyleUtil,t=Polymer.Settings,n={dom:function(e,t,n,r){this._transformDom(e,t||"",n,r)},_transformDom:function(e,t,n,r){e.setAttribute&&this.element(e,t,n,r);for(var s=Polymer.dom(e).childNodes,i=0;i *"),r&&r(e)}}for(var u,f=0,p=i.length;f *"),e=e.replace(P,l+" $1"),e=e.replace(o,function(e,i,o){if(r)o=o.replace(_," ");else{var l=a._transformCompoundSelector(o,i,t,n);r=r||l.stop,s=s||l.hostContext,i=l.combinator,o=l.value}return i+o}),s&&(e=e.replace(f,function(e,t,r,s){return t+r+" "+n+s+i+" "+t+n+r+s})),e},_transformCompoundSelector:function(e,t,n,r){var s=e.search(_),i=!1;e.indexOf(u)>=0?i=!0:e.indexOf(l)>=0?e=this._transformHostSelector(e,r):0!==s&&(e=n?this._transformSimpleSelector(e,n):e),e.indexOf(p)>=0&&(t="");var o;return s>=0&&(e=e.replace(_," "),o=!0),{value:e,combinator:t,stop:o,hostContext:i}},_transformSimpleSelector:function(e,t){var n=e.split(v);return n[0]+=t,n.join(v)},_transformHostSelector:function(e,t){var n=e.match(h),r=n&&n[2].trim()||"";if(r){if(r[0].match(a))return e.replace(h,function(e,n,r){return t+r});return r.split(a)[0]===t?r:S}return e.replace(l,t)},documentRule:function(e){e.selector=e.parsedSelector,this.normalizeRootSelector(e),t.useNativeShadow||this._transformRule(e,this._transformDocumentSelector)},normalizeRootSelector:function(e){e.selector=e.selector.replace(c,"html");var t=e.selector.split(i);t=t.filter(function(e){return!e.match(C)}),e.selector=t.join(i)},_transformDocumentSelector:function(e){return e.match(_)?this._transformComplexSelector(e,s):this._transformSimpleSelector(e.trim(),s)},_slottedToContent:function(e){return e.replace(E,p+"> $1")},SCOPE_NAME:"style-scope"},r=n.SCOPE_NAME,s=":not(["+r+"]):not(."+r+")",i=",",o=/(^|[\s>+~]+)((?:\[.+?\]|[^\s>+~=\[])+)/g,a=/[[.:#*]/,l=":host",c=":root",h=/(:host)(?:\(((?:\([^)(]*\)|[^)(]*)+?)\))/,u=":host-context",f=/(.*)(?::host-context)(?:\(((?:\([^)(]*\)|[^)(]*)+?)\))(.*)/,p="::content",_=/::content|::shadow|\/deep\//,d=".",m="["+r+"~=",y="]",v=":",g="class",P=new RegExp("^("+p+")"),S="should_not_match",E=/(?:::slotted)(?:\(((?:\([^)(]*\)|[^)(]*)+?)\))/g,C=/:host(?:\s*>\s*\*)?/;return n}(),Polymer.StyleExtends=function(){var e=Polymer.StyleUtil;return{hasExtends:function(e){return Boolean(e.match(this.rx.EXTEND))},transform:function(t){var n=e.rulesForStyle(t),r=this;return e.forEachRule(n,function(e){if(r._mapRuleOntoParent(e),e.parent)for(var t;t=r.rx.EXTEND.exec(e.cssText);){var n=t[1],s=r._findExtendor(n,e);s&&r._extendRule(e,s)}e.cssText=e.cssText.replace(r.rx.EXTEND,"")}),e.toCssText(n,function(e){e.selector.match(r.rx.STRIP)&&(e.cssText="")},!0)},_mapRuleOntoParent:function(e){if(e.parent){for(var t,n=e.parent.map||(e.parent.map={}),r=e.selector.split(","),s=0;s1&&(t=i[0].trim(),r=n(t,i.slice(1).join(":")),a[t]=r));return a}function s(e){var t=m.__currentElementProto,n=t&&t.is;for(var r in e.dependants)r!==n&&(e.dependants[r].__applyShimInvalid=!0)}function i(n,i,o,a){if(o&&c.processVariableAndFallback(o,function(e,n){n&&t(n)&&(a="@apply "+n+";")}),!a)return n;var h=l(a),u=n.slice(0,n.indexOf("--")),f=r(h),p=f,d=t(i),m=d&&d.properties;m?(p=Object.create(m),p=Polymer.Base.mixin(p,f)):e(i,p);var y,v,g=[],P=!1;for(y in p)v=f[y],void 0===v&&(v="initial"),!m||y in m||(P=!0),g.push(i+_+y+": "+v);return P&&s(d),d&&(d.properties=p),o&&(u=n+";"+u),u+g.join("; ")+";"}function o(e,t,n){return"var("+t+",var("+n+"))"}function a(n,r){n=n.replace(f,"");var s=[],i=t(n);if(i||(e(n,{}),i=t(n)),i){var o=m.__currentElementProto;o&&(i.dependants[o.is]=o);var a,l,c;for(a in i.properties)c=r&&r[a],l=[a,": var(",n,_,a],c&&l.push(",",c),l.push(")"),s.push(l.join(""))}return s.join("; ")}function l(e){for(var t;t=h.exec(e);){var n=t[0],s=t[1],i=t.index,o=i+n.indexOf("@apply"),l=i+n.length,c=e.slice(0,o),u=e.slice(l),f=r(c),p=a(s,f);e=[c,p,u].join(""),h.lastIndex=i+p.length}return e}var c=Polymer.StyleUtil,h=c.rx.MIXIN_MATCH,u=c.rx.VAR_ASSIGN,f=/;\s*/m,p=/^\s*(initial)|(inherit)\s*$/,_="_-_",d={},m={_measureElement:null,_map:d,_separator:_,transform:function(e,t){this.__currentElementProto=t,c.forRulesInStyles(e,this._boundFindDefinitions),c.forRulesInStyles(e,this._boundFindApplications),t&&(t.__applyShimInvalid=!1),this.__currentElementProto=null},_findDefinitions:function(e){var t=e.parsedCssText;t=t.replace(/var\(\s*(--[^,]*),\s*(--[^)]*)\)/g,o),t=t.replace(u,i),e.cssText=t,":root"===e.selector&&(e.selector=":host > *")},_findApplications:function(e){e.cssText=l(e.cssText)},transformRule:function(e){this._findDefinitions(e),this._findApplications(e)},_getInitialValueForProperty:function(e){return this._measureElement||(this._measureElement=document.createElement("meta"),this._measureElement.style.all="initial",document.head.appendChild(this._measureElement)),window.getComputedStyle(this._measureElement).getPropertyValue(e)}};return m._boundTransformRule=m.transformRule.bind(m),m._boundFindDefinitions=m._findDefinitions.bind(m),m._boundFindApplications=m._findApplications.bind(m),m}(),function(){var e=Polymer.Base._prepElement,t=Polymer.Settings.useNativeShadow,n=Polymer.StyleUtil,r=Polymer.StyleTransformer,s=Polymer.StyleExtends,i=Polymer.ApplyShim,o=Polymer.Settings;Polymer.Base._addFeature({_prepElement:function(t){this._encapsulateStyle&&"shady"!==this.__cssBuild&&r.element(t,this.is,this._scopeCssViaAttr),e.call(this,t)},_prepStyles:function(){void 0===this._encapsulateStyle&&(this._encapsulateStyle=!t),t||(this._scopeStyle=n.applyStylePlaceHolder(this.is)),this.__cssBuild=n.cssBuildTypeForModule(this.is)},_prepShimStyles:function(){if(this._template){var e=n.isTargetedBuild(this.__cssBuild);if(o.useNativeCSSProperties&&"shadow"===this.__cssBuild&&e)return void(o.preserveStyleIncludes&&n.styleIncludesToTemplate(this._template));this._styles=this._styles||this._collectStyles(),o.useNativeCSSProperties&&!this.__cssBuild&&i.transform(this._styles,this);var s=o.useNativeCSSProperties&&e?this._styles.length&&this._styles[0].textContent.trim():r.elementStyles(this);this._prepStyleProperties(),!this._needsStyleProperties()&&s&&n.applyCss(s,this.is,t?this._template.content:null,this._scopeStyle)}else this._styles=[]},_collectStyles:function(){var e=[],t="",r=this.styleModules;if(r)for(var i,o=0,a=r.length;o=0)e=this.valueForProperties(e,t);else{var r=this,s=function(e,n,s,i){var o=r.valueForProperty(t[n],t);return o&&"initial"!==o?"apply-shim-inherit"===o&&(o="inherit"):o=r.valueForProperty(t[s]||s,t)||s,e+(o||"")+i};e=n.processVariableAndFallback(e,s)}return e&&e.trim()||""},valueForProperties:function(e,t){for(var n,r,s=e.split(";"),i=0;i\s*\*/,_checkRoot:function(e,t){return Boolean(t.match(this._rootSelector))||"html"===e&&t.indexOf("html")>-1},whenHostOrRootRule:function(e,t,n,s){if(t.propertyInfo||self.decorateRule(t),t.propertyInfo.properties){var o=e.is?r._calcHostScope(e.is,e.extends):"html",a=t.parsedSelector,l=this._checkRoot(o,a),c=!l&&0===a.indexOf(":host");if("shady"===(e.__cssBuild||n.__cssBuild)&&(l=a===o+" > *."+o||a.indexOf("html")>-1,c=!l&&0===a.indexOf(o)),l||c){var h=o;c&&(i.useNativeShadow&&!t.transformedSelector&&(t.transformedSelector=r._transformRuleCss(t,r._transformComplexSelector,e.is,o)),h=t.transformedSelector||t.parsedSelector),l&&"html"===o&&(h=t.transformedSelector||t.parsedSelector),s({selector:h,isHost:c,isRoot:l})}}},hostAndRootPropertiesForScope:function(e){var r={},s={},i=this;return n.forActiveRulesInStyles(e._styles,function(n,o){i.whenHostOrRootRule(e,n,o,function(o){var a=e._element||e;t.call(a,o.selector)&&(o.isHost?i.collectProperties(n,r):i.collectProperties(n,s))})}),{rootProps:s,hostProps:r}},transformStyles:function(e,t,n){var s=this,o=r._calcHostScope(e.is,e.extends),a=e.extends?"\\"+o.slice(0,-1)+"\\]":o,l=new RegExp(this.rx.HOST_PREFIX+a+this.rx.HOST_SUFFIX),c=this._elementKeyframeTransforms(e,n);return r.elementStyles(e,function(r){s.applyProperties(r,t),i.useNativeShadow||Polymer.StyleUtil.isKeyframesSelector(r)||!r.cssText||(s.applyKeyframeTransforms(r,c),s._scopeSelector(r,l,o,e._scopeCssViaAttr,n))})},_elementKeyframeTransforms:function(e,t){var n=e._styles._keyframes,r={};if(!i.useNativeShadow&&n)for(var s=0,o=n[s];s-1&&(o.textContent=a),n.applyStyle(o,null,e._scopeStyle)):a&&(o=n.applyCss(a,r,null,e._scopeStyle)),o&&(o._useCount=o._useCount||0,e._customStyle!=o&&o._useCount++,e._customStyle=o),o},mixinCustomStyle:function(e,t){var n;for(var r in t)((n=t[r])||0===n)&&(e[r]=n)},updateNativeStyleProperties:function(e,t){var n=e.__customStyleProperties;if(n)for(var r=0;rthis.MAX&&s.shift()},retrieve:function(e,t,n){var r=this.cache[e];if(r)for(var s,i=r.length-1;i>=0;i--)if(s=r[i],n===s.styles&&this._objectsEqual(t,s.keyValues))return s},clear:function(){this.cache={}},_objectsEqual:function(e,t){var n,r;for(var s in e)if(n=e[s],r=t[s],!("object"==typeof n&&n?this._objectsStrictlyEqual(n,r):n===r))return!1;return!Array.isArray(e)||e.length===t.length},_objectsStrictlyEqual:function(e,t){return this._objectsEqual(e,t)&&this._objectsEqual(t,e)}}}(),Polymer.StyleDefaults=function(){var e=Polymer.StyleProperties,t=Polymer.StyleCache,n=Polymer.Settings.useNativeCSSProperties;return{_styles:[],_properties:null,customStyle:{},_styleCache:new t,_element:Polymer.DomApi.wrap(document.documentElement),addStyle:function(e){this._styles.push(e),this._properties=null},get _styleProperties(){return this._properties||(e.decorateStyles(this._styles,this),this._styles._scopeStyleProperties=null,this._properties=e.hostAndRootPropertiesForScope(this).rootProps,e.mixinCustomStyle(this._properties,this.customStyle),e.reify(this._properties)),this._properties},hasStyleProperties:function(){return Boolean(this._properties)},_needsStyleProperties:function(){},_computeStyleProperties:function(){return this._styleProperties},updateStyles:function(t){this._properties=null,t&&Polymer.Base.mixin(this.customStyle,t),this._styleCache.clear() +;for(var r,s=0;s0&&l.push(t);return[{removed:a,added:l}]}},Polymer.Collection.get=function(e){return Polymer._collections.get(e)||new Polymer.Collection(e)},Polymer.Collection.applySplices=function(e,t){var n=Polymer._collections.get(e);return n?n._applySplices(t):null},Polymer({is:"dom-repeat",extends:"template",_template:null,properties:{items:{type:Array},as:{type:String,value:"item"},indexAs:{type:String,value:"index"},sort:{type:Function,observer:"_sortChanged"},filter:{type:Function,observer:"_filterChanged"},observe:{type:String,observer:"_observeChanged"},delay:Number,renderedItemCount:{type:Number,notify:!Polymer.Settings.suppressTemplateNotifications,readOnly:!0},initialCount:{type:Number,observer:"_initializeChunking"},targetFramerate:{type:Number,value:20},notifyDomChange:{type:Boolean},_targetFrameTime:{type:Number,computed:"_computeFrameTime(targetFramerate)"}},behaviors:[Polymer.Templatizer],observers:["_itemsChanged(items.*)"],created:function(){this._instances=[],this._pool=[],this._limit=1/0;var e=this;this._boundRenderChunk=function(){e._renderChunk()}},detached:function(){this.__isDetached=!0;for(var e=0;e=0;t--){var n=this._instances[t];n.isPlaceholder&&t=this._limit&&(n=this._downgradeInstance(t,n.__key__)),e[n.__key__]=t,n.isPlaceholder||n.__setProperty(this.indexAs,t,!0)}this._pool.length=0,this._setRenderedItemCount(this._instances.length),Polymer.Settings.suppressTemplateNotifications&&!this.notifyDomChange||this.fire("dom-change"),this._tryRenderChunk()},_applyFullRefresh:function(){var e,t=this.collection;if(this._sortFn)e=t?t.getKeys():[];else{e=[];var n=this.items;if(n)for(var r=0;r=r;a--)this._detachAndRemoveInstance(a)},_numericSort:function(e,t){return e-t},_applySplicesUserSort:function(e){for(var t,n,r=this.collection,s={},i=0;i=0;i--){var c=a[i];void 0!==c&&this._detachAndRemoveInstance(c)}var h=this;if(l.length){this._filterFn&&(l=l.filter(function(e){return h._filterFn(r.getItem(e))})),l.sort(function(e,t){return h._sortFn(r.getItem(e),r.getItem(t))});var u=0;for(i=0;i>1,a=this._instances[o].__key__,l=this._sortFn(n.getItem(a),r);if(l<0)e=o+1;else{if(!(l>0)){i=o;break}s=o-1}}return i<0&&(i=s+1),this._insertPlaceholder(i,t),i},_applySplicesArrayOrder:function(e){for(var t,n=0;n=0?(e=this.as+"."+e.substring(n+1),i._notifyPath(e,t,!0)):i.__setProperty(this.as,t,!0))}},itemForElement:function(e){var t=this.modelForElement(e);return t&&t[this.as]},keyForElement:function(e){var t=this.modelForElement(e);return t&&t.__key__},indexForElement:function(e){var t=this.modelForElement(e);return t&&t[this.indexAs]}}),Polymer({is:"array-selector",_template:null,properties:{items:{type:Array,observer:"clearSelection"},multi:{type:Boolean,value:!1,observer:"clearSelection"},selected:{type:Object,notify:!0},selectedItem:{type:Object,notify:!0},toggle:{type:Boolean,value:!1}},clearSelection:function(){if(Array.isArray(this.selected))for(var e=0;e \ 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 baceb29a985..a55204f203c 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 c7be840a9d7..19b903dfb7c 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit c7be840a9d73b6a32a0d74b1a5c0fb4475018d75 +Subproject commit 19b903dfb7c940bfafdecc5407526647d0aed2f2 diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-automation.html b/homeassistant/components/frontend/www_static/panels/ha-panel-automation.html index 4da09363373..376d9b8a378 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-automation.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-automation.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-automation.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-automation.html.gz index 7d3cf0d8152..051b7e0552a 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-automation.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-automation.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-config.html b/homeassistant/components/frontend/www_static/panels/ha-panel-config.html index e54ba31fb3e..104b090ed61 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-config.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-config.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-config.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-config.html.gz index a0745b91349..8e8ba12d1b4 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-config.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-config.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 6f501f7db26..236e0fb286a 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 1a8faa4f3e6..2f78956badf 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-service.html b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html index ed1ff9fe85d..aeab2aaf465 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 c76c9bc6de1..c479a26990f 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 6f03db94edf..75d9dfa9d6e 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 742f5eef9ee..e28e4ae8f30 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 bb27f9a2d7d..c1be1953639 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 +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-template.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html.gz index 9c3063c0d6c..d5877c44dc0 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-hassio.html b/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html index 2ec3ee876b9..f339403019b 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.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-hassio.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html.gz index 03b8a09f320..6a1c2905cf0 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.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 0b22672f887..33e972c2349 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-map.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-map.html @@ -2,4 +2,4 @@ pt(this._mapPane,"leaflet-pan-anim");var e=this._getMapPanePos().subtract(t).round();this._panAnim.run(this._mapPane,e,i.duration||.25,i.easeLinearity)}else this._rawPanBy(t),this.fire("move").fire("moveend");return this},flyTo:function(t,i,e){function n(t){var i=t?-1:1,e=t?g:m,n=g*g-m*m+i*x*x*v*v,o=2*e*x*v,s=n/o,r=Math.sqrt(s*s+1)-s;return r<1e-9?-18:Math.log(r)}function o(t){return(Math.exp(t)-Math.exp(-t))/2}function s(t){return(Math.exp(t)+Math.exp(-t))/2}function r(t){return o(t)/s(t)}function a(t){return m*(s(w)/s(w+y*t))}function h(t){return m*(s(w)*r(w+y*t)-o(w))/x}function u(t){return 1-Math.pow(1-t,1.5)}function l(){var e=(Date.now()-L)/b,n=u(e)*P;e<=1?(this._flyToFrame=f(l,this),this._move(this.unproject(c.add(_.subtract(c).multiplyBy(h(n)/v)),p),this.getScaleZoom(m/a(n),p),{flyTo:!0})):this._move(t,i)._moveEnd(!0)}if(e=e||{},!1===e.animate||!Ki)return this.setView(t,i,e);this._stop();var c=this.project(this.getCenter()),_=this.project(t),d=this.getSize(),p=this._zoom;t=C(t),i=void 0===i?p:i;var m=Math.max(d.x,d.y),g=m*this.getZoomScale(p,i),v=_.distanceTo(c)||1,y=1.42,x=y*y,w=n(0),L=Date.now(),P=(n(1)-w)/y,b=e.duration?1e3*e.duration:1e3*P*.8;return this._moveStart(!0),l.call(this),this},flyToBounds:function(t,i){var e=this._getBoundsCenterZoom(t,i);return this.flyTo(e.center,e.zoom,i)},setMaxBounds:function(t){return t=z(t),t.isValid()?(this.options.maxBounds&&this.off("moveend",this._panInsideMaxBounds),this.options.maxBounds=t,this._loaded&&this._panInsideMaxBounds(),this.on("moveend",this._panInsideMaxBounds)):(this.options.maxBounds=null,this.off("moveend",this._panInsideMaxBounds))},setMinZoom:function(t){return this.options.minZoom=t,this._loaded&&this.getZoom()this.options.maxZoom?this.setZoom(t):this},panInsideBounds:function(t,i){this._enforcingBounds=!0;var e=this.getCenter(),n=this._limitCenter(e,this._zoom,z(t));return e.equals(n)||this.panTo(n,i),this._enforcingBounds=!1,this},invalidateSize:function(t){if(!this._loaded)return this;t=i({animate:!1,pan:!0},!0===t?{animate:!0}:t);var n=this.getSize();this._sizeChanged=!0,this._lastCenter=null;var o=this.getSize(),s=n.divideBy(2).round(),r=o.divideBy(2).round(),a=s.subtract(r);return a.x||a.y?(t.animate&&t.pan?this.panBy(a):(t.pan&&this._rawPanBy(a),this.fire("move"),t.debounceMoveend?(clearTimeout(this._sizeTimer),this._sizeTimer=setTimeout(e(this.fire,this,"moveend"),200)):this.fire("moveend")),this.fire("resize",{oldSize:n,newSize:o})):this},stop:function(){return this.setZoom(this._limitZoom(this._zoom)),this.options.zoomSnap||this.fire("viewreset"),this._stop()},locate:function(t){if(t=this._locateOptions=i({timeout:1e4,watch:!1},t),!("geolocation"in navigator))return this._handleGeolocationError({code:0,message:"Geolocation not supported."}),this;var n=e(this._handleGeolocationResponse,this),o=e(this._handleGeolocationError,this);return t.watch?this._locationWatchId=navigator.geolocation.watchPosition(n,o,t):navigator.geolocation.getCurrentPosition(n,o,t),this},stopLocate:function(){return navigator.geolocation&&navigator.geolocation.clearWatch&&navigator.geolocation.clearWatch(this._locationWatchId),this._locateOptions&&(this._locateOptions.setView=!1),this},_handleGeolocationError:function(t){var i=t.code,e=t.message||(1===i?"permission denied":2===i?"position unavailable":"timeout");this._locateOptions.setView&&!this._loaded&&this.fitWorld(),this.fire("locationerror",{code:i,message:"Geolocation error: "+e+"."})},_handleGeolocationResponse:function(t){var i=t.coords.latitude,e=t.coords.longitude,n=new M(i,e),o=n.toBounds(t.coords.accuracy),s=this._locateOptions;if(s.setView){var r=this.getBoundsZoom(o);this.setView(n,s.maxZoom?Math.min(r,s.maxZoom):r)}var a={latlng:n,bounds:o,timestamp:t.timestamp};for(var h in t.coords)"number"==typeof t.coords[h]&&(a[h]=t.coords[h]);this.fire("locationfound",a)},addHandler:function(t,i){if(!i)return this;var e=this[t]=new i(this);return this._handlers.push(e),this.options[t]&&e.enable(),this},remove:function(){if(this._initEvents(!0),this._containerId!==this._container._leaflet_id)throw new Error("Map container is being reused by another instance");try{delete this._container._leaflet_id,delete this._containerId}catch(t){this._container._leaflet_id=void 0,this._containerId=void 0}ut(this._mapPane),this._clearControlPos&&this._clearControlPos(),this._clearHandlers(),this._loaded&&this.fire("unload");var t;for(t in this._layers)this._layers[t].remove();for(t in this._panes)ut(this._panes[t]);return this._layers=[],this._panes=[],delete this._mapPane,delete this._renderer,this},createPane:function(t,i){var e="leaflet-pane"+(t?" leaflet-"+t.replace("Pane","")+"-pane":""),n=ht("div",e,i||this._mapPane);return t&&(this._panes[t]=n),n},getCenter:function(){return this._checkIfLoaded(),this._lastCenter&&!this._moved()?this._lastCenter:this.layerPointToLatLng(this._getCenterLayerPoint())},getZoom:function(){return this._zoom},getBounds:function(){var t=this.getPixelBounds();return new T(this.unproject(t.getBottomLeft()),this.unproject(t.getTopRight()))},getMinZoom:function(){return void 0===this.options.minZoom?this._layersMinZoom||0:this.options.minZoom},getMaxZoom:function(){return void 0===this.options.maxZoom?void 0===this._layersMaxZoom?1/0:this._layersMaxZoom:this.options.maxZoom},getBoundsZoom:function(t,i,e){t=z(t),e=w(e||[0,0]);var n=this.getZoom()||0,o=this.getMinZoom(),s=this.getMaxZoom(),r=t.getNorthWest(),a=t.getSouthEast(),h=this.getSize().subtract(e),u=b(this.project(a,n),this.project(r,n)).getSize(),l=Ki?this.options.zoomSnap:1,c=h.x/u.x,_=h.y/u.y,d=i?Math.max(c,_):Math.min(c,_);return n=this.getScaleZoom(d,n),l&&(n=Math.round(n/(l/100))*(l/100),n=i?Math.ceil(n/l)*l:Math.floor(n/l)*l),Math.max(o,Math.min(s,n))},getSize:function(){return this._size&&!this._sizeChanged||(this._size=new x(this._container.clientWidth||0,this._container.clientHeight||0),this._sizeChanged=!1),this._size.clone()},getPixelBounds:function(t,i){var e=this._getTopLeftPoint(t,i);return new P(e,e.add(this.getSize()))},getPixelOrigin:function(){return this._checkIfLoaded(),this._pixelOrigin},getPixelWorldBounds:function(t){return this.options.crs.getProjectedBounds(void 0===t?this.getZoom():t)},getPane:function(t){return"string"==typeof t?this._panes[t]:t},getPanes:function(){return this._panes},getContainer:function(){return this._container},getZoomScale:function(t,i){var e=this.options.crs;return i=void 0===i?this._zoom:i,e.scale(t)/e.scale(i)},getScaleZoom:function(t,i){var e=this.options.crs;i=void 0===i?this._zoom:i;var n=e.zoom(t*e.scale(i));return isNaN(n)?1/0:n},project:function(t,i){return i=void 0===i?this._zoom:i,this.options.crs.latLngToPoint(C(t),i)},unproject:function(t,i){return i=void 0===i?this._zoom:i,this.options.crs.pointToLatLng(w(t),i)},layerPointToLatLng:function(t){var i=w(t).add(this.getPixelOrigin());return this.unproject(i)},latLngToLayerPoint:function(t){return this.project(C(t))._round()._subtract(this.getPixelOrigin())},wrapLatLng:function(t){return this.options.crs.wrapLatLng(C(t))},wrapLatLngBounds:function(t){return this.options.crs.wrapLatLngBounds(z(t))},distance:function(t,i){return this.options.crs.distance(C(t),C(i))},containerPointToLayerPoint:function(t){return w(t).subtract(this._getMapPanePos())},layerPointToContainerPoint:function(t){return w(t).add(this._getMapPanePos())},containerPointToLatLng:function(t){var i=this.containerPointToLayerPoint(w(t));return this.layerPointToLatLng(i)},latLngToContainerPoint:function(t){return this.layerPointToContainerPoint(this.latLngToLayerPoint(C(t)))},mouseEventToContainerPoint:function(t){return tt(t,this._container)},mouseEventToLayerPoint:function(t){return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(t))},mouseEventToLatLng:function(t){return this.layerPointToLatLng(this.mouseEventToLayerPoint(t))},_initContainer:function(t){var i=this._container=rt(t);if(!i)throw new Error("Map container not found.");if(i._leaflet_id)throw new Error("Map container is already initialized.");V(i,"scroll",this._onScroll,this),this._containerId=n(i)},_initLayout:function(){var t=this._container;this._fadeAnimated=this.options.fadeAnimation&&Ki,pt(t,"leaflet-container"+(te?" leaflet-touch":"")+(ne?" leaflet-retina":"")+(Bi?" leaflet-oldie":"")+(Wi?" leaflet-safari":"")+(this._fadeAnimated?" leaflet-fade-anim":""));var i=at(t,"position");"absolute"!==i&&"relative"!==i&&"fixed"!==i&&(t.style.position="relative"),this._initPanes(),this._initControlPos&&this._initControlPos()},_initPanes:function(){var t=this._panes={};this._paneRenderers={},this._mapPane=this.createPane("mapPane",this._container),Lt(this._mapPane,new x(0,0)),this.createPane("tilePane"),this.createPane("shadowPane"),this.createPane("overlayPane"),this.createPane("markerPane"),this.createPane("tooltipPane"),this.createPane("popupPane"),this.options.markerZoomAnimation||(pt(t.markerPane,"leaflet-zoom-hide"),pt(t.shadowPane,"leaflet-zoom-hide"))},_resetView:function(t,i){Lt(this._mapPane,new x(0,0));var e=!this._loaded;this._loaded=!0,i=this._limitZoom(i),this.fire("viewprereset");var n=this._zoom!==i;this._moveStart(n)._move(t,i)._moveEnd(n),this.fire("viewreset"),e&&this.fire("load")},_moveStart:function(t){return t&&this.fire("zoomstart"),this.fire("movestart")},_move:function(t,i,e){void 0===i&&(i=this._zoom);var n=this._zoom!==i;return this._zoom=i,this._lastCenter=t,this._pixelOrigin=this._getNewPixelOrigin(t),(n||e&&e.pinch)&&this.fire("zoom",e),this.fire("move",e)},_moveEnd:function(t){return t&&this.fire("zoomend"),this.fire("moveend")},_stop:function(){return g(this._flyToFrame),this._panAnim&&this._panAnim.stop(),this},_rawPanBy:function(t){Lt(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(t){this._targets={},this._targets[n(this._container)]=this;var i=t?G:V;i(this._container,"click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu keypress",this._handleDOMEvent,this),this.options.trackResize&&i(window,"resize",this._onResize,this),Ki&&this.options.transform3DLimit&&(t?this.off:this.on).call(this,"moveend",this._onMoveEnd)},_onResize:function(){g(this._resizeRequest),this._resizeRequest=f(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,i){for(var e,o=[],s="mouseout"===i||"mouseover"===i,r=t.target||t.srcElement,a=!1;r;){if((e=this._targets[n(r)])&&("click"===i||"preclick"===i)&&!t._simulated&&this._draggableMoved(e)){a=!0;break}if(e&&e.listens(i,!0)){if(s&&!ot(r,t))break;if(o.push(e),s)break}if(r===this._container)break;r=r.parentNode}return o.length||a||s||!ot(r,t)||(o=[this]),o},_handleDOMEvent:function(t){if(this._loaded&&!nt(t)){var i=t.type;"mousedown"!==i&&"keypress"!==i||zt(t.target||t.srcElement),this._fireDOMEvent(t,i)}},_mouseEvents:["click","dblclick","mouseover","mouseout","contextmenu"],_fireDOMEvent:function(t,e,n){if("click"===t.type){var o=i({},t);o.type="preclick",this._fireDOMEvent(o,o.type,n)}if(!t._stopped&&(n=(n||[]).concat(this._findEventTargets(t,e)),n.length)){var s=n[0];"contextmenu"===e&&s.listens(e,!0)&&$(t);var r={originalEvent:t};if("keypress"!==t.type){var a=s.options&&"icon"in s.options;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-i)/2:Math.max(0,Math.ceil(t))-Math.max(0,Math.floor(i))},_limitZoom:function(t){var i=this.getMinZoom(),e=this.getMaxZoom(),n=Ki?this.options.zoomSnap:1;return n&&(t=Math.round(t/n)*n),Math.max(i,Math.min(e,t))},_onPanTransitionStep:function(){this.fire("move")},_onPanTransitionEnd:function(){mt(this._mapPane,"leaflet-pan-anim"),this.fire("moveend")},_tryAnimatedPan:function(t,i){var e=this._getCenterOffset(t)._floor();return!(!0!==(i&&i.animate)&&!this.getSize().contains(e)||(this.panBy(e,i),0))},_createAnimProxy:function(){var t=this._proxy=ht("div","leaflet-proxy leaflet-zoom-animated");this._panes.mapPane.appendChild(t),this.on("zoomanim",function(t){var i=Pe,e=this._proxy.style[i];wt(this._proxy,this.project(t.center,t.zoom),this.getZoomScale(t.zoom,1)),e===this._proxy.style[i]&&this._animatingZoom&&this._onZoomTransitionEnd()},this),this.on("load moveend",function(){var t=this.getCenter(),i=this.getZoom();wt(this._proxy,this.project(t,i),this.getZoomScale(i,1))},this),this._on("unload",this._destroyAnimProxy,this)},_destroyAnimProxy:function(){ut(this._proxy),delete this._proxy},_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,i,e){if(this._animatingZoom)return!0;if(e=e||{},!this._zoomAnimated||!1===e.animate||this._nothingToAnimate()||Math.abs(i-this._zoom)>this.options.zoomAnimationThreshold)return!1;var n=this.getZoomScale(i),o=this._getCenterOffset(t)._divideBy(1-1/n);return!(!0!==e.animate&&!this.getSize().contains(o)||(f(function(){this._moveStart(!0)._animateZoom(t,i,!0)},this),0))},_animateZoom:function(t,i,n,o){n&&(this._animatingZoom=!0,this._animateToCenter=t,this._animateToZoom=i,pt(this._mapPane,"leaflet-zoom-anim")),this.fire("zoomanim",{center:t,zoom:i,noUpdate:o}),setTimeout(e(this._onZoomTransitionEnd,this),250)},_onZoomTransitionEnd:function(){this._animatingZoom&&(mt(this._mapPane,"leaflet-zoom-anim"),this._animatingZoom=!1,this._move(this._animateToCenter,this._animateToZoom),f(function(){this._moveEnd(!0)},this))}}),ke=v.extend({options:{position:"topright"},initialize:function(t){l(this,t)},getPosition:function(){return this.options.position},setPosition:function(t){var i=this._map;return i&&i.removeControl(this),this.options.position=t,i&&i.addControl(this),this},getContainer:function(){return this._container},addTo:function(t){this.remove(),this._map=t;var i=this._container=this.onAdd(t),e=this.getPosition(),n=t._controlCorners[e];return pt(i,"leaflet-control"),-1!==e.indexOf("bottom")?n.insertBefore(i,n.firstChild):n.appendChild(i),this},remove:function(){return this._map?(ut(this._container),this.onRemove&&this.onRemove(this._map),this._map=null,this):this},_refocusOnMap:function(t){this._map&&t&&t.screenX>0&&t.screenY>0&&this._map.getContainer().focus()}}),Be=function(t){return new ke(t)};Se.include({addControl:function(t){return t.addTo(this),this},removeControl:function(t){return t.remove(),this},_initControlPos:function(){function t(t,o){var s=e+t+" "+e+o;i[t+o]=ht("div",s,n)}var i=this._controlCorners={},e="leaflet-",n=this._controlContainer=ht("div",e+"control-container",this._container);t("top","left"),t("top","right"),t("bottom","left"),t("bottom","right")},_clearControlPos:function(){for(var t in this._controlCorners)ut(this._controlCorners[t]);ut(this._controlContainer),delete this._controlCorners,delete this._controlContainer}});var Ie=ke.extend({options:{collapsed:!0,position:"topright",autoZIndex:!0,hideSingleBase:!1,sortLayers:!1,sortFunction:function(t,i,e,n){return e1,this._baseLayersList.style.display=t?"":"none"),this._separator.style.display=i&&t?"":"none",this},_onLayerChange:function(t){this._handlingClick||this._update();var i=this._getLayer(n(t.target)),e=i.overlay?"add"===t.type?"overlayadd":"overlayremove":"add"===t.type?"baselayerchange":null;e&&this._map.fire(e,i)},_createRadioElement:function(t,i){var e='",n=document.createElement("div");return n.innerHTML=e,n.firstChild},_addItem:function(t){var i,e=document.createElement("label"),o=this._map.hasLayer(t.layer);t.overlay?(i=document.createElement("input"),i.type="checkbox",i.className="leaflet-control-layers-selector",i.defaultChecked=o):i=this._createRadioElement("leaflet-base-layers",o),this._layerControlInputs.push(i),i.layerId=n(t.layer),V(i,"click",this._onInputClick,this);var s=document.createElement("span");s.innerHTML=" "+t.name;var r=document.createElement("div");return e.appendChild(r),r.appendChild(i),r.appendChild(s),(t.overlay?this._overlaysList:this._baseLayersList).appendChild(e),this._checkDisabledLayers(),e},_onInputClick:function(){var t,i,e,n=this._layerControlInputs,o=[],s=[];this._handlingClick=!0;for(var r=n.length-1;r>=0;r--)t=n[r],i=this._getLayer(t.layerId).layer,e=this._map.hasLayer(i),t.checked&&!e?o.push(i):!t.checked&&e&&s.push(i);for(r=0;r=0;o--)t=e[o],i=this._getLayer(t.layerId).layer,t.disabled=void 0!==i.options.minZoom&&ni.options.maxZoom},_expandIfNotCollapsed:function(){return this._map&&!this.options.collapsed&&this.expand(),this},_expand:function(){return this.expand()},_collapse:function(){return this.collapse()}}),Ae=function(t,i,e){return new Ie(t,i,e)},Oe=ke.extend({options:{position:"topleft",zoomInText:"+",zoomInTitle:"Zoom in",zoomOutText:"−",zoomOutTitle:"Zoom out"},onAdd:function(t){var i="leaflet-control-zoom",e=ht("div",i+" leaflet-bar"),n=this.options;return this._zoomInButton=this._createButton(n.zoomInText,n.zoomInTitle,i+"-in",e,this._zoomIn),this._zoomOutButton=this._createButton(n.zoomOutText,n.zoomOutTitle,i+"-out",e,this._zoomOut),this._updateDisabled(),t.on("zoomend zoomlevelschange",this._updateDisabled,this),e},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,i,e,n,o){var s=ht("a",e,n);return s.innerHTML=t,s.href="#",s.title=i,s.setAttribute("role","button"),s.setAttribute("aria-label",i),J(s),V(s,"click",Q),V(s,"click",o,this),V(s,"click",this._refocusOnMap,this),s},_updateDisabled:function(){var t=this._map,i="leaflet-disabled";mt(this._zoomInButton,i),mt(this._zoomOutButton,i),(this._disabled||t._zoom===t.getMinZoom())&&pt(this._zoomOutButton,i),(this._disabled||t._zoom===t.getMaxZoom())&&pt(this._zoomInButton,i)}});Se.mergeOptions({zoomControl:!0}),Se.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new Oe,this.addControl(this.zoomControl))});var Re=function(t){return new Oe(t)},De=ke.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0},onAdd:function(t){var i="leaflet-control-scale",e=ht("div",i),n=this.options;return this._addScales(n,i+"-line",e),t.on(n.updateWhenIdle?"moveend":"move",this._update,this),t.whenReady(this._update,this),e},onRemove:function(t){t.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(t,i,e){t.metric&&(this._mScale=ht("div",i,e)),t.imperial&&(this._iScale=ht("div",i,e))},_update:function(){var t=this._map,i=t.getSize().y/2,e=t.distance(t.containerPointToLatLng([0,i]),t.containerPointToLatLng([this.options.maxWidth,i]));this._updateScales(e)},_updateScales:function(t){this.options.metric&&t&&this._updateMetric(t),this.options.imperial&&t&&this._updateImperial(t)},_updateMetric:function(t){var i=this._getRoundNum(t),e=i<1e3?i+" m":i/1e3+" km";this._updateScale(this._mScale,e,i/t)},_updateImperial:function(t){var i,e,n,o=3.2808399*t;o>5280?(i=o/5280,e=this._getRoundNum(i),this._updateScale(this._iScale,e+" mi",e/i)):(n=this._getRoundNum(o),this._updateScale(this._iScale,n+" ft",n/o))},_updateScale:function(t,i,e){t.style.width=Math.round(this.options.maxWidth*e)+"px",t.innerHTML=i},_getRoundNum:function(t){var i=Math.pow(10,(Math.floor(t)+"").length-1),e=t/i;return e=e>=10?10:e>=5?5:e>=3?3:e>=2?2:1,i*e}}),Ne=function(t){return new De(t)},je=ke.extend({options:{position:"bottomright",prefix:'Leaflet'},initialize:function(t){l(this,t),this._attributions={}},onAdd:function(t){t.attributionControl=this,this._container=ht("div","leaflet-control-attribution"),J(this._container);for(var i in t._layers)t._layers[i].getAttribution&&this.addAttribution(t._layers[i].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 i in this._attributions)this._attributions[i]&&t.push(i);var e=[];this.options.prefix&&e.push(this.options.prefix),t.length&&e.push(t.join(", ")),this._container.innerHTML=e.join(" | ")}}});Se.mergeOptions({attributionControl:!0}),Se.addInitHook(function(){this.options.attributionControl&&(new je).addTo(this)});var We=function(t){return new je(t)};ke.Layers=Ie,ke.Zoom=Oe,ke.Scale=De,ke.Attribution=je,Be.layers=Ae,Be.zoom=Re,Be.scale=Ne,Be.attribution=We;var He,Fe=v.extend({initialize:function(t){this._map=t},enable:function(){return this._enabled?this:(this._enabled=!0,this.addHooks(),this)},disable:function(){return this._enabled?(this._enabled=!1,this.removeHooks(),this):this},enabled:function(){return!!this._enabled}}),Ue={Events:xi},Ve=!1,Ge=te?"touchstart mousedown":"mousedown",qe={mousedown:"mouseup",touchstart:"touchend",pointerdown:"touchend",MSPointerDown:"touchend"},Ke={mousedown:"mousemove",touchstart:"touchmove",pointerdown:"touchmove",MSPointerDown:"touchmove"},Ye=wi.extend({options:{clickTolerance:3},initialize:function(t,i,e,n){l(this,n),this._element=t,this._dragStartTarget=i||t,this._preventOutline=e},enable:function(){this._enabled||(V(this._dragStartTarget,Ge,this._onDown,this),this._enabled=!0)},disable:function(){this._enabled&&(L.Draggable._dragging===this&&this.finishDrag(),G(this._dragStartTarget,Ge,this._onDown,this),this._enabled=!1,this._moved=!1)},_onDown:function(t){if(!t._simulated&&this._enabled&&(this._moved=!1,!dt(this._element,"leaflet-zoom-anim")&&!(Ve||t.shiftKey||1!==t.which&&1!==t.button&&!t.touches||(Ve=this,this._preventOutline&&zt(this._element),bt(),zi(),this._moving)))){this.fire("down");var i=t.touches?t.touches[0]:t;this._startPoint=new x(i.clientX,i.clientY),V(document,Ke[t.type],this._onMove,this),V(document,qe[t.type],this._onUp,this)}},_onMove:function(t){if(!t._simulated&&this._enabled){if(t.touches&&t.touches.length>1)return void(this._moved=!0);var i=t.touches&&1===t.touches.length?t.touches[0]:t,e=new x(i.clientX,i.clientY),n=e.subtract(this._startPoint);(n.x||n.y)&&(Math.abs(n.x)+Math.abs(n.y)1e-7;h++)i=s*Math.sin(a),i=Math.pow((1-i)/(1+i),s/2),u=Math.PI/2-2*Math.atan(r*i)-a,a+=u;return new M(a*e,t.x*e/n)}},tn=(Object.freeze||Object)({LonLat:$e,Mercator:Qe,SphericalMercator:bi}),en=i({},Pi,{code:"EPSG:3395",projection:Qe,transformation:function(){var t=.5/(Math.PI*Qe.R);return E(t,.5,-t,.5)}()}),nn=i({},Pi,{code:"EPSG:4326",projection:$e,transformation:E(1/180,1,-1/180,.5)}),on=i({},Li,{projection:$e,transformation:E(1,0,-1,0),scale:function(t){return Math.pow(2,t)},zoom:function(t){return Math.log(t)/Math.LN2},distance:function(t,i){var e=i.lng-t.lng,n=i.lat-t.lat;return Math.sqrt(e*e+n*n)},infinite:!0});Li.Earth=Pi,Li.EPSG3395=en,Li.EPSG3857=Zi,Li.EPSG900913=Ei,Li.EPSG4326=nn,Li.Simple=on;var sn=wi.extend({options:{pane:"overlayPane",attribution:null,bubblingMouseEvents:!0},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[n(t)]=this,this}, removeInteractiveTarget:function(t){return delete this._map._targets[n(t)],this},getAttribution:function(){return this.options.attribution},_layerAdd:function(t){var i=t.target;if(i.hasLayer(this)){if(this._map=i,this._zoomAnimated=i._zoomAnimated,this.getEvents){var e=this.getEvents();i.on(e,this),this.once("remove",function(){i.off(e,this)},this)}this.onAdd(i),this.getAttribution&&i.attributionControl&&i.attributionControl.addAttribution(this.getAttribution()),this.fire("add"),i.fire("layeradd",{layer:this})}}});Se.include({addLayer:function(t){var i=n(t);return this._layers[i]?this:(this._layers[i]=t,t._mapToAdd=this,t.beforeAdd&&t.beforeAdd(this),this.whenReady(t._layerAdd,t),this)},removeLayer:function(t){var i=n(t);return this._layers[i]?(this._loaded&&t.onRemove(this),t.getAttribution&&this.attributionControl&&this.attributionControl.removeAttribution(t.getAttribution()),delete this._layers[i],this._loaded&&(this.fire("layerremove",{layer:t}),t.fire("remove")),t._map=t._mapToAdd=null,this):this},hasLayer:function(t){return!!t&&n(t)in this._layers},eachLayer:function(t,i){for(var e in this._layers)t.call(i,this._layers[e]);return this},_addLayers:function(t){t=t?pi(t)?t:[t]:[];for(var i=0,e=t.length;ithis._layersMaxZoom&&this.setZoom(this._layersMaxZoom),void 0===this.options.minZoom&&this._layersMinZoom&&this.getZoom()i)return r=(n-i)/e,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,i){return i=i||this._defaultShape(),t=C(t),i.push(t),this._bounds.extend(t),this.redraw()},_setLatLngs:function(t){this._bounds=new T,this._latlngs=this._convertLatLngs(t)},_defaultShape:function(){return jt(this._latlngs)?this._latlngs:this._latlngs[0]},_convertLatLngs:function(t){for(var i=[],e=jt(t),n=0,o=t.length;n=2&&i[0]instanceof M&&i[0].equals(i[e-1])&&i.pop(),i},_setLatLngs:function(t){gn.prototype._setLatLngs.call(this,t),jt(this._latlngs)&&(this._latlngs=[this._latlngs])},_defaultShape:function(){return jt(this._latlngs[0])?this._latlngs[0]:this._latlngs[0][0]},_clipPoints:function(){var t=this._renderer._bounds,i=this.options.weight,e=new x(i,i);if(t=new P(t.min.subtract(e),t.max.add(e)),this._parts=[],this._pxBounds&&this._pxBounds.intersects(t)){if(this.options.noClip)return void(this._parts=this._rings);for(var n,o=0,s=this._rings.length;ot.y!=n.y>t.y&&t.x<(n.x-e.x)*(t.y-e.y)/(n.y-e.y)+e.x&&(u=!u);return u||gn.prototype._containsPoint.call(this,t,!0)}}),yn=hn.extend({initialize:function(t,i){l(this,i),this._layers={},t&&this.addData(t)},addData:function(t){var i,e,n,o=pi(t)?t:t.features;if(o){for(i=0,e=o.length;io?(i.height=o+"px",pt(t,s)):mt(t,s),this._containerWidth=this._container.offsetWidth},_animateZoom:function(t){var i=this._map._latLngToNewLayerPoint(this._latlng,t.zoom,t.center),e=this._getAnchor();Lt(this._container,i.add(e))},_adjustPan:function(){if(!(!this.options.autoPan||this._map._panAnim&&this._map._panAnim._inProgress)){var t=this._map,i=parseInt(at(this._container,"marginBottom"),10)||0,e=this._container.offsetHeight+i,n=this._containerWidth,o=new x(this._containerLeft,-e-this._containerBottom);o._add(Pt(this._container));var s=t.layerPointToContainerPoint(o),r=w(this.options.autoPanPadding),a=w(this.options.autoPanPaddingTopLeft||r),h=w(this.options.autoPanPaddingBottomRight||r),u=t.getSize(),l=0,c=0;s.x+n+h.x>u.x&&(l=s.x+n-u.x+h.x),s.x-l-a.x<0&&(l=s.x-a.x),s.y+e+h.y>u.y&&(c=s.y+e-u.y+h.y),s.y-c-a.y<0&&(c=s.y-a.y),(l||c)&&t.fire("autopanstart").panBy([l,c])}},_onCloseButtonClick:function(t){this._close(),Q(t)},_getAnchor:function(){return w(this._source&&this._source._getPopupAnchor?this._source._getPopupAnchor():[0,0])}}),Mn=function(t,i){return new zn(t,i)};Se.mergeOptions({closePopupOnClick:!0}),Se.include({openPopup:function(t,i,e){return t instanceof zn||(t=new zn(e).setContent(t)),i&&t.setLatLng(i),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}}),sn.include({bindPopup:function(t,i){return t instanceof zn?(l(t,i),this._popup=t,t._source=this):(this._popup&&!i||(this._popup=new zn(i,this)),this._popup.setContent(t)),this._popupHandlersAdded||(this.on({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!0),this},unbindPopup:function(){return this._popup&&(this.off({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!1,this._popup=null),this},openPopup:function(t,i){if(t instanceof sn||(i=t,t=this),t instanceof hn)for(var e in this._layers){t=this._layers[e];break}return i||(i=t.getCenter?t.getCenter():t.getLatLng()),this._popup&&this._map&&(this._popup._source=t,this._popup.update(),this._map.openPopup(this._popup,i)),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&&this._popup.isOpen()},setPopupContent:function(t){return this._popup&&this._popup.setContent(t),this},getPopup:function(){return this._popup},_openPopup:function(t){var i=t.layer||t.target;if(this._popup&&this._map)return Q(t),i instanceof pn?void this.openPopup(t.layer||t.target,t.latlng):void(this._map.hasLayer(this._popup)&&this._popup._source===i?this.closePopup():this.openPopup(i,t.latlng))},_movePopup:function(t){this._popup.setLatLng(t.latlng)},_onKeyPress:function(t){13===t.originalEvent.keyCode&&this._openPopup(t)}});var Cn=Tn.extend({options:{pane:"tooltipPane",offset:[0,0],direction:"auto",permanent:!1,sticky:!1,interactive:!1,opacity:.9},onAdd:function(t){Tn.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){Tn.prototype.onRemove.call(this,t),t.fire("tooltipclose",{tooltip:this}),this._source&&this._source.fire("tooltipclose",{tooltip:this},!0)},getEvents:function(){var t=Tn.prototype.getEvents.call(this);return te&&!this.options.permanent&&(t.preclick=this._close),t},_close:function(){this._map&&this._map.closeTooltip(this)},_initLayout:function(){var t="leaflet-tooltip "+(this.options.className||"")+" leaflet-zoom-"+(this._zoomAnimated?"animated":"hide");this._contentNode=this._container=ht("div",t)},_updateLayout:function(){},_adjustPan:function(){},_setPosition:function(t){var i=this._map,e=this._container,n=i.latLngToContainerPoint(i.getCenter()),o=i.layerPointToContainerPoint(t),s=this.options.direction,r=e.offsetWidth,a=e.offsetHeight,h=w(this.options.offset),u=this._getAnchor();"top"===s?t=t.add(w(-r/2+h.x,-a+h.y+u.y,!0)):"bottom"===s?t=t.subtract(w(r/2-h.x,-h.y,!0)):"center"===s?t=t.subtract(w(r/2+h.x,a/2-u.y+h.y,!0)):"right"===s||"auto"===s&&o.xthis.options.maxZoom||en&&this._retainParent(o,s,r,n))},_retainChildren:function(t,i,e,n){for(var o=2*t;o<2*t+2;o++)for(var s=2*i;s<2*i+2;s++){var r=new x(o,s);r.z=e+1;var a=this._tileCoordsToKey(r),h=this._tiles[a];h&&h.active?h.retain=!0:(h&&h.loaded&&(h.retain=!0),e+1this.options.maxZoom||void 0!==this.options.minZoom&&o1)return void this._setView(t,e);for(var c=o.min.y;c<=o.max.y;c++)for(var _=o.min.x;_<=o.max.x;_++){var d=new x(_,c);d.z=this._tileZoom,this._isValidTile(d)&&(this._tiles[this._tileCoordsToKey(d)]||r.push(d))}if(r.sort(function(t,i){return t.distanceTo(s)-i.distanceTo(s)}),0!==r.length){this._loading||(this._loading=!0,this.fire("loading"));var p=document.createDocumentFragment();for(_=0;_e.max.x)||!i.wrapLat&&(t.ye.max.y))return!1}if(!this.options.bounds)return!0;var n=this._tileCoordsToBounds(t);return z(this.options.bounds).overlaps(n)},_keyToBounds:function(t){return this._tileCoordsToBounds(this._keyToTileCoords(t))},_tileCoordsToBounds:function(t){var i=this._map,e=this.getTileSize(),n=t.scaleBy(e),o=n.add(e),s=i.unproject(n,t.z),r=i.unproject(o,t.z),a=new T(s,r);return this.options.noWrap||i.wrapLatLngBounds(a),a},_tileCoordsToKey:function(t){return t.x+":"+t.y+":"+t.z},_keyToTileCoords:function(t){var i=t.split(":"),e=new x(+i[0],+i[1]);return e.z=+i[2],e},_removeTile:function(t){var i=this._tiles[t];i&&(ut(i.el),delete this._tiles[t],this.fire("tileunload",{tile:i.el,coords:this._keyToTileCoords(t)}))},_initTile:function(t){pt(t,"leaflet-tile");var i=this.getTileSize();t.style.width=i.x+"px",t.style.height=i.y+"px",t.onselectstart=r,t.onmousemove=r,Bi&&this.options.opacity<1&&vt(t,this.options.opacity),Oi&&!Ri&&(t.style.WebkitBackfaceVisibility="hidden")},_addTile:function(t,i){var n=this._getTilePos(t),o=this._tileCoordsToKey(t),s=this.createTile(this._wrapCoords(t),e(this._tileReady,this,t));this._initTile(s),this.createTile.length<2&&f(e(this._tileReady,this,t,null,s)),Lt(s,n),this._tiles[o]={el:s,coords:t,current:!0},i.appendChild(s),this.fire("tileloadstart",{tile:s,coords:t})},_tileReady:function(t,i,n){if(this._map){i&&this.fire("tileerror",{error:i,tile:n,coords:t});var o=this._tileCoordsToKey(t);(n=this._tiles[o])&&(n.loaded=+new Date,this._map._fadeAnimated?(vt(n.el,0),g(this._fadeFrame),this._fadeFrame=f(this._updateOpacity,this)):(n.active=!0,this._pruneTiles()),i||(pt(n.el,"leaflet-tile-loaded"),this.fire("tileload",{tile:n.el,coords:t})),this._noTilesToLoad()&&(this._loading=!1,this.fire("load"),Bi||!this._map._fadeAnimated?f(this._pruneTiles,this):setTimeout(e(this._pruneTiles,this),250)))}},_getTilePos:function(t){return t.scaleBy(this.getTileSize()).subtract(this._level.origin)},_wrapCoords:function(t){var i=new x(this._wrapX?s(t.x,this._wrapX):t.x,this._wrapY?s(t.y,this._wrapY):t.y);return i.z=t.z,i},_pxBoundsToTileRange:function(t){var i=this.getTileSize();return new P(t.min.unscaleBy(i).floor(),t.max.unscaleBy(i).ceil().subtract([1,1]))},_noTilesToLoad:function(){for(var t in this._tiles)if(!this._tiles[t].loaded)return!1;return!0}}),kn=Sn.extend({options:{minZoom:0,maxZoom:18,subdomains:"abc",errorTileUrl:"",zoomOffset:0,tms:!1,zoomReverse:!1,detectRetina:!1,crossOrigin:!1},initialize:function(t,i){this._url=t,i=l(this,i),i.detectRetina&&ne&&i.maxZoom>0&&(i.tileSize=Math.floor(i.tileSize/2),i.zoomReverse?(i.zoomOffset--,i.minZoom++):(i.zoomOffset++,i.maxZoom--),i.minZoom=Math.max(0,i.minZoom)),"string"==typeof i.subdomains&&(i.subdomains=i.subdomains.split("")),Oi||this.on("tileunload",this._onTileRemove)},setUrl:function(t,i){return this._url=t,i||this.redraw(),this},createTile:function(t,i){var n=document.createElement("img");return V(n,"load",e(this._tileOnLoad,this,i,n)),V(n,"error",e(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:ne?"@2x":"",s:this._getSubdomain(t),x:t.x,y:t.y,z:this._getZoomForUrl()};if(this._map&&!this._map.options.crs.infinite){var n=this._globalTileRange.max.y-t.y;this.options.tms&&(e.y=n),e["-y"]=n}return _(this._url,i(e,this.options))},_tileOnLoad:function(t,i){Bi?setTimeout(e(t,this,null,i),0):t(null,i)},_tileOnError:function(t,i,e){var n=this.options.errorTileUrl;n&&i.src!==n&&(i.src=n),t(e,i)},_onTileRemove:function(t){t.tile.onload=null},_getZoomForUrl:function(){var t=this._tileZoom,i=this.options.maxZoom,e=this.options.zoomReverse,n=this.options.zoomOffset;return e&&(t=i-t),t+n},_getSubdomain:function(t){var i=Math.abs(t.x+t.y)%this.options.subdomains.length;return this.options.subdomains[i]},_abortLoading:function(){var t,i;for(t in this._tiles)this._tiles[t].coords.z!==this._tileZoom&&(i=this._tiles[t].el,i.onload=r,i.onerror=r,i.complete||(i.src=mi,ut(i)))}}),Bn=kn.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 n=i({},this.defaultWmsParams);for(var o in e)o in this.options||(n[o]=e[o]);e=l(this,e),n.width=n.height=e.tileSize*(e.detectRetina&&ne?2:1),this.wmsParams=n},onAdd:function(t){this._crs=this.options.crs||t.options.crs,this._wmsVersion=parseFloat(this.wmsParams.version);var i=this._wmsVersion>=1.3?"crs":"srs";this.wmsParams[i]=this._crs.code,kn.prototype.onAdd.call(this,t)},getTileUrl:function(t){var i=this._tileCoordsToBounds(t),e=this._crs.project(i.getNorthWest()),n=this._crs.project(i.getSouthEast()),o=(this._wmsVersion>=1.3&&this._crs===nn?[n.y,e.x,e.y,n.x]:[e.x,n.y,n.x,e.y]).join(","),s=kn.prototype.getTileUrl.call(this,t);return s+c(this.wmsParams,s,this.options.uppercase)+(this.options.uppercase?"&BBOX=":"&bbox=")+o},setParams:function(t,e){return i(this.wmsParams,t),e||this.redraw(),this}});kn.WMS=Bn,si.wms=ri;var In=sn.extend({options:{padding:.1},initialize:function(t){l(this,t),n(this),this._layers=this._layers||{}},onAdd:function(){this._container||(this._initContainer(),this._zoomAnimated&&pt(this._container,"leaflet-zoom-animated")),this.getPane().appendChild(this._container),this._update(),this.on("update",this._updatePaths,this)},onRemove:function(){this.off("update",this._updatePaths,this),this._destroyContainer()},getEvents:function(){var t={viewreset:this._reset,zoom:this._onZoom,moveend:this._update,zoomend:this._onZoomEnd};return this._zoomAnimated&&(t.zoomanim=this._onAnimZoom),t},_onAnimZoom:function(t){this._updateTransform(t.center,t.zoom)},_onZoom:function(){this._updateTransform(this._map.getCenter(),this._map.getZoom())},_updateTransform:function(t,i){var e=this._map.getZoomScale(i,this._zoom),n=Pt(this._container),o=this._map.getSize().multiplyBy(.5+this.options.padding),s=this._map.project(this._center,i),r=this._map.project(t,i),a=r.subtract(s),h=o.multiplyBy(-e).add(n).add(o).subtract(a);Ki?wt(this._container,h,e):Lt(this._container,h)},_reset:function(){this._update(),this._updateTransform(this._center,this._zoom);for(var t in this._layers)this._layers[t]._reset()},_onZoomEnd:function(){for(var t in this._layers)this._layers[t]._project()},_updatePaths:function(){for(var t in this._layers)this._layers[t]._update()},_update:function(){var t=this.options.padding,i=this._map.getSize(),e=this._map.containerPointToLayerPoint(i.multiplyBy(-t)).round();this._bounds=new P(e,e.add(i.multiplyBy(1+2*t)).round()),this._center=this._map.getCenter(),this._zoom=this._map.getZoom()}}),An=In.extend({getEvents:function(){var t=In.prototype.getEvents.call(this);return t.viewprereset=this._onViewPreReset,t},_onViewPreReset:function(){this._postponeUpdatePaths=!0},onAdd:function(){In.prototype.onAdd.call(this),this._draw()},_initContainer:function(){var t=this._container=document.createElement("canvas");V(t,"mousemove",o(this._onMouseMove,32,this),this),V(t,"click dblclick mousedown mouseup contextmenu",this._onClick,this),V(t,"mouseout",this._handleMouseOut,this),this._ctx=t.getContext("2d")},_destroyContainer:function(){delete this._ctx,ut(this._container),G(this._container),delete this._container},_updatePaths:function(){if(!this._postponeUpdatePaths){var t;this._redrawBounds=null;for(var i in this._layers)t=this._layers[i],t._update();this._redraw()}},_update:function(){if(!this._map._animatingZoom||!this._bounds){this._drawnLayers={},In.prototype._update.call(this);var t=this._bounds,i=this._container,e=t.getSize(),n=ne?2:1;Lt(i,t.min),i.width=n*e.x,i.height=n*e.y,i.style.width=e.x+"px",i.style.height=e.y+"px",ne&&this._ctx.scale(2,2),this._ctx.translate(-t.min.x,-t.min.y),this.fire("update")}},_reset:function(){In.prototype._reset.call(this),this._postponeUpdatePaths&&(this._postponeUpdatePaths=!1,this._updatePaths())},_initPath:function(t){this._updateDashArray(t),this._layers[n(t)]=t;var i=t._order={layer:t,prev:this._drawLast,next:null};this._drawLast&&(this._drawLast.next=i),this._drawLast=i,this._drawFirst=this._drawFirst||this._drawLast},_addPath:function(t){this._requestRedraw(t)},_removePath:function(t){var i=t._order,e=i.next,n=i.prev;e?e.prev=n:this._drawLast=n,n?n.next=e:this._drawFirst=e,delete t._order,delete this._layers[L.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 i,e=t.options.dashArray.split(","),n=[];for(i=0;i')}}catch(t){return function(t){return document.createElement("<"+t+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}}(),Rn={_initContainer:function(){this._container=ht("div","leaflet-vml-container")},_update:function(){this._map._animatingZoom||(In.prototype._update.call(this),this.fire("update"))},_initPath:function(t){var i=t._container=On("shape");pt(i,"leaflet-vml-shape "+(this.options.className||"")),i.coordsize="1 1",t._path=On("path"),i.appendChild(t._path),this._updateStyle(t),this._layers[n(t)]=t},_addPath:function(t){var i=t._container;this._container.appendChild(i),t.options.interactive&&t.addInteractiveTarget(i)},_removePath:function(t){var i=t._container;ut(i),t.removeInteractiveTarget(i),delete this._layers[n(t)]},_updateStyle:function(t){var i=t._stroke,e=t._fill,n=t.options,o=t._container;o.stroked=!!n.stroke,o.filled=!!n.fill,n.stroke?(i||(i=t._stroke=On("stroke")),o.appendChild(i),i.weight=n.weight+"px",i.color=n.color,i.opacity=n.opacity,n.dashArray?i.dashStyle=pi(n.dashArray)?n.dashArray.join(" "):n.dashArray.replace(/( *, *)/g," "):i.dashStyle="",i.endcap=n.lineCap.replace("butt","flat"),i.joinstyle=n.lineJoin):i&&(o.removeChild(i),t._stroke=null),n.fill?(e||(e=t._fill=On("fill")),o.appendChild(e),e.color=n.fillColor||n.color,e.opacity=n.fillOpacity):e&&(o.removeChild(e),t._fill=null)},_updateCircle:function(t){var i=t._point.round(),e=Math.round(t._radius),n=Math.round(t._radiusY||e);this._setPath(t,t._empty()?"M0 0":"AL "+i.x+","+i.y+" "+e+","+n+" 0,23592600")},_setPath:function(t,i){t._path.v=i},_bringToFront:function(t){ct(t._container)},_bringToBack:function(t){_t(t._container)}},Dn=re?On:S,Nn=In.extend({getEvents:function(){var t=In.prototype.getEvents.call(this);return t.zoomstart=this._onZoomStart,t},_initContainer:function(){this._container=Dn("svg"),this._container.setAttribute("pointer-events","none"),this._rootGroup=Dn("g"),this._container.appendChild(this._rootGroup)},_destroyContainer:function(){ut(this._container),G(this._container),delete this._container,delete this._rootGroup},_onZoomStart:function(){this._update()},_update:function(){if(!this._map._animatingZoom||!this._bounds){In.prototype._update.call(this);var t=this._bounds,i=t.getSize(),e=this._container;this._svgSize&&this._svgSize.equals(i)||(this._svgSize=i,e.setAttribute("width",i.x),e.setAttribute("height",i.y)),Lt(e,t.min),e.setAttribute("viewBox",[t.min.x,t.min.y,i.x,i.y].join(" ")),this.fire("update")}},_initPath:function(t){var i=t._path=Dn("path");t.options.className&&pt(i,t.options.className),t.options.interactive&&pt(i,"leaflet-interactive"),this._updateStyle(t),this._layers[n(t)]=t},_addPath:function(t){this._rootGroup||this._initContainer(),this._rootGroup.appendChild(t._path),t.addInteractiveTarget(t._path)},_removePath:function(t){ut(t._path),t.removeInteractiveTarget(t._path),delete this._layers[n(t)]},_updatePath:function(t){t._project(),t._update()},_updateStyle:function(t){var i=t._path,e=t.options;i&&(e.stroke?(i.setAttribute("stroke",e.color),i.setAttribute("stroke-opacity",e.opacity),i.setAttribute("stroke-width",e.weight),i.setAttribute("stroke-linecap",e.lineCap),i.setAttribute("stroke-linejoin",e.lineJoin),e.dashArray?i.setAttribute("stroke-dasharray",e.dashArray):i.removeAttribute("stroke-dasharray"),e.dashOffset?i.setAttribute("stroke-dashoffset",e.dashOffset):i.removeAttribute("stroke-dashoffset")):i.setAttribute("stroke","none"),e.fill?(i.setAttribute("fill",e.fillColor||e.color),i.setAttribute("fill-opacity",e.fillOpacity),i.setAttribute("fill-rule",e.fillRule||"evenodd")):i.setAttribute("fill","none"))},_updatePoly:function(t,i){this._setPath(t,k(t._parts,i))},_updateCircle:function(t){var i=t._point,e=t._radius,n=t._radiusY||e,o="a"+e+","+n+" 0 1,0 ",s=t._empty()?"M0 0":"M"+(i.x-e)+","+i.y+o+2*e+",0 "+o+2*-e+",0 ";this._setPath(t,s)},_setPath:function(t,i){t._path.setAttribute("d",i)},_bringToFront:function(t){ct(t._path)},_bringToBack:function(t){_t(t._path)}});re&&Nn.include(Rn),Se.include({getRenderer:function(t){var i=t.options.renderer||this._getPaneRenderer(t.options.pane)||this.options.renderer||this._renderer;return i||(i=this._renderer=this.options.preferCanvas&&ai()||hi()),this.hasLayer(i)||this.addLayer(i),i},_getPaneRenderer:function(t){if("overlayPane"===t||void 0===t)return!1;var i=this._paneRenderers[t];return void 0===i&&(i=Nn&&hi({pane:t})||An&&ai({pane:t}),this._paneRenderers[t]=i),i}});var jn=vn.extend({initialize:function(t,i){vn.prototype.initialize.call(this,this._boundsToLatLngs(t),i)},setBounds:function(t){return this.setLatLngs(this._boundsToLatLngs(t))},_boundsToLatLngs:function(t){return t=z(t),[t.getSouthWest(),t.getNorthWest(),t.getNorthEast(),t.getSouthEast()]}});Nn.create=Dn,Nn.pointsToPath=k,yn.geometryToLayer=Kt,yn.coordsToLatLng=Yt,yn.coordsToLatLngs=Xt,yn.latLngToCoords=Jt,yn.latLngsToCoords=$t,yn.getFeature=Qt,yn.asFeature=ti,Se.mergeOptions({boxZoom:!0});var Wn=Fe.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane,this._resetStateTimeout=0,t.on("unload",this._destroy,this)},addHooks:function(){V(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){G(this._container,"mousedown",this._onMouseDown,this)},moved:function(){return this._moved},_destroy:function(){ut(this._pane),delete this._pane},_resetState:function(){this._resetStateTimeout=0,this._moved=!1},_clearDeferredResetState:function(){0!==this._resetStateTimeout&&(clearTimeout(this._resetStateTimeout),this._resetStateTimeout=0)},_onMouseDown:function(t){return!(!t.shiftKey||1!==t.which&&1!==t.button)&&(this._clearDeferredResetState(),this._resetState(),zi(),bt(),this._startPoint=this._map.mouseEventToContainerPoint(t),void V(document,{contextmenu:Q,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this))},_onMouseMove:function(t){this._moved||(this._moved=!0,this._box=ht("div","leaflet-zoom-box",this._container),pt(this._container,"leaflet-crosshair"),this._map.fire("boxzoomstart")),this._point=this._map.mouseEventToContainerPoint(t);var i=new P(this._point,this._startPoint),e=i.getSize();Lt(this._box,i.min),this._box.style.width=e.x+"px",this._box.style.height=e.y+"px"},_finish:function(){this._moved&&(ut(this._box),mt(this._container,"leaflet-crosshair")),Mi(),Tt(),G(document,{contextmenu:Q,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseUp:function(t){if((1===t.which||1===t.button)&&(this._finish(),this._moved)){this._clearDeferredResetState(),this._resetStateTimeout=setTimeout(e(this._resetState,this),0);var i=new T(this._map.containerPointToLatLng(this._startPoint),this._map.containerPointToLatLng(this._point));this._map.fitBounds(i).fire("boxzoomend",{boxZoomBounds:i})}},_onKeyDown:function(t){27===t.keyCode&&this._finish()}});Se.addInitHook("addHandler","boxZoom",Wn),Se.mergeOptions({doubleClickZoom:!0});var Hn=Fe.extend({addHooks:function(){this._map.on("dblclick",this._onDoubleClick,this)},removeHooks:function(){this._map.off("dblclick",this._onDoubleClick,this)},_onDoubleClick:function(t){var i=this._map,e=i.getZoom(),n=i.options.zoomDelta,o=t.originalEvent.shiftKey?e-n:e+n;"center"===i.options.doubleClickZoom?i.setZoom(o):i.setZoomAround(t.containerPoint,o)}});Se.addInitHook("addHandler","doubleClickZoom",Hn),Se.mergeOptions({dragging:!0,inertia:!Ri,inertiaDeceleration:3400,inertiaMaxSpeed:1/0,easeLinearity:.2,worldCopyJump:!1,maxBoundsViscosity:0});var Fn=Fe.extend({addHooks:function(){if(!this._draggable){var t=this._map;this._draggable=new Ye(t._mapPane,t._container),this._draggable.on({dragstart:this._onDragStart,drag:this._onDrag,dragend:this._onDragEnd},this),this._draggable.on("predrag",this._onPreDragLimit,this),t.options.worldCopyJump&&(this._draggable.on("predrag",this._onPreDragWrap,this),t.on("zoomend",this._onZoomEnd,this),t.whenReady(this._onZoomEnd,this))}pt(this._map._container,"leaflet-grab leaflet-touch-drag"),this._draggable.enable(),this._positions=[],this._times=[]},removeHooks:function(){mt(this._map._container,"leaflet-grab"),mt(this._map._container,"leaflet-touch-drag"),this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},moving:function(){return this._draggable&&this._draggable._moving},_onDragStart:function(){var t=this._map;if(t._stop(),this._map.options.maxBounds&&this._map.options.maxBoundsViscosity){var i=z(this._map.options.maxBounds);this._offsetLimit=b(this._map.latLngToContainerPoint(i.getNorthWest()).multiplyBy(-1),this._map.latLngToContainerPoint(i.getSouthEast()).multiplyBy(-1).add(this._map.getSize())),this._viscosity=Math.min(1,Math.max(0,this._map.options.maxBoundsViscosity))}else this._offsetLimit=null;t.fire("movestart").fire("dragstart"),t.options.inertia&&(this._positions=[],this._times=[])},_onDrag:function(t){if(this._map.options.inertia){var i=this._lastTime=+new Date,e=this._lastPos=this._draggable._absPos||this._draggable._newPos;this._positions.push(e),this._times.push(i),i-this._times[0]>50&&(this._positions.shift(),this._times.shift())}this._map.fire("move",t).fire("drag",t)},_onZoomEnd:function(){var t=this._map.getSize().divideBy(2),i=this._map.latLngToLayerPoint([0,0]);this._initialWorldOffset=i.subtract(t).x,this._worldWidth=this._map.getPixelWorldBounds().getSize().x},_viscousLimit:function(t,i){return t-(t-i)*this._viscosity},_onPreDragLimit:function(){if(this._viscosity&&this._offsetLimit){var t=this._draggable._newPos.subtract(this._draggable._startPos),i=this._offsetLimit;t.xi.max.x&&(t.x=this._viscousLimit(t.x,i.max.x)),t.y>i.max.y&&(t.y=this._viscousLimit(t.y,i.max.y)),this._draggable._newPos=this._draggable._startPos.add(t)}},_onPreDragWrap:function(){ -var t=this._worldWidth,i=Math.round(t/2),e=this._initialWorldOffset,n=this._draggable._newPos.x,o=(n-i+e)%t+i-e,s=(n+i+e)%t-i-e,r=Math.abs(o+e)0?s:-s))-i;this._delta=0,this._startTime=null,r&&("center"===t.options.scrollWheelZoom?t.setZoom(i+r):t.setZoomAround(this._lastMousePos,i+r))}});Se.addInitHook("addHandler","scrollWheelZoom",Vn),Se.mergeOptions({tap:!0,tapTolerance:15});var Gn=Fe.extend({addHooks:function(){V(this._map._container,"touchstart",this._onDown,this)},removeHooks:function(){G(this._map._container,"touchstart",this._onDown,this)},_onDown:function(t){if(t.touches){if($(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 x(i.clientX,i.clientY),n.tagName&&"a"===n.tagName.toLowerCase()&&pt(n,"leaflet-active"),this._holdTimeout=setTimeout(e(function(){this._isTapValid()&&(this._fireClick=!1,this._onUp(),this._simulateEvent("contextmenu",i))},this),1e3),this._simulateEvent("mousedown",i),V(document,{touchmove:this._onMove,touchend:this._onUp},this)}},_onUp:function(t){if(clearTimeout(this._holdTimeout),G(document,{touchmove:this._onMove,touchend:this._onUp},this),this._fireClick&&t&&t.changedTouches){var i=t.changedTouches[0],e=i.target;e&&e.tagName&&"a"===e.tagName.toLowerCase()&&mt(e,"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 i=t.touches[0];this._newPos=new x(i.clientX,i.clientY),this._simulateEvent("mousemove",i)},_simulateEvent:function(t,i){var e=document.createEvent("MouseEvents");e._simulated=!0,i.target._simulatedClick=!0,e.initMouseEvent(t,!0,!0,window,1,i.screenX,i.screenY,i.clientX,i.clientY,!1,!1,!1,!1,0,null),i.target.dispatchEvent(e)}});te&&!Qi&&Se.addInitHook("addHandler","tap",Gn),Se.mergeOptions({touchZoom:te&&!Ri,bounceAtZoomLimits:!0});var qn=Fe.extend({addHooks:function(){pt(this._map._container,"leaflet-touch-zoom"),V(this._map._container,"touchstart",this._onTouchStart,this)},removeHooks:function(){mt(this._map._container,"leaflet-touch-zoom"),G(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 e=i.mouseEventToContainerPoint(t.touches[0]),n=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(e.add(n)._divideBy(2))),this._startDist=e.distanceTo(n),this._startZoom=i.getZoom(),this._moved=!1,this._zooming=!0,i._stop(),V(document,"touchmove",this._onTouchMove,this),V(document,"touchend",this._onTouchEnd,this),$(t)}},_onTouchMove:function(t){if(t.touches&&2===t.touches.length&&this._zooming){var i=this._map,n=i.mouseEventToContainerPoint(t.touches[0]),o=i.mouseEventToContainerPoint(t.touches[1]),s=n.distanceTo(o)/this._startDist;if(this._zoom=i.getScaleZoom(s,this._startZoom),!i.options.bounceAtZoomLimits&&(this._zoomi.getMaxZoom()&&s>1)&&(this._zoom=i._limitZoom(this._zoom)),"center"===i.options.touchZoom){if(this._center=this._startLatLng,1===s)return}else{var r=n._add(o)._divideBy(2)._subtract(this._centerPoint);if(1===s&&0===r.x&&0===r.y)return;this._center=i.unproject(i.project(this._pinchStartLatLng,this._zoom).subtract(r),this._zoom)}this._moved||(i._moveStart(!0),this._moved=!0),g(this._animRequest);var a=e(i._move,i,this._center,this._zoom,{pinch:!0,round:!1});this._animRequest=f(a,this,!0),$(t)}},_onTouchEnd:function(){return this._moved&&this._zooming?(this._zooming=!1,g(this._animRequest),G(document,"touchmove",this._onTouchMove),G(document,"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)}});Se.addInitHook("addHandler","touchZoom",qn),Se.BoxZoom=Wn,Se.DoubleClickZoom=Hn,Se.Drag=Fn,Se.Keyboard=Un,Se.ScrollWheelZoom=Vn,Se.Tap=Gn,Se.TouchZoom=qn;var Kn=window.L;window.L=t,t.version="1.1.0",t.noConflict=li,t.Control=ke,t.control=Be,t.Browser=ae,t.Evented=wi,t.Mixin=Ue,t.Util=yi,t.Class=v,t.Handler=Fe,t.extend=i,t.bind=e,t.stamp=n,t.setOptions=l,t.DomEvent=Le,t.DomUtil=Ze,t.PosAnimation=Ee,t.Draggable=Ye,t.LineUtil=Xe,t.PolyUtil=Je,t.Point=x,t.point=w,t.Bounds=P,t.bounds=b,t.Transformation=Z,t.transformation=E,t.Projection=tn,t.LatLng=M,t.latLng=C,t.LatLngBounds=T,t.latLngBounds=z,t.CRS=Li,t.GeoJSON=yn,t.geoJSON=ii,t.geoJson=wn,t.Layer=sn,t.LayerGroup=rn,t.layerGroup=an,t.FeatureGroup=hn,t.featureGroup=un,t.ImageOverlay=Ln,t.imageOverlay=Pn,t.VideoOverlay=bn,t.videoOverlay=ei,t.DivOverlay=Tn,t.Popup=zn,t.popup=Mn,t.Tooltip=Cn,t.tooltip=Zn,t.Icon=ln,t.icon=Ht,t.DivIcon=En,t.divIcon=ni,t.Marker=dn,t.marker=Ft,t.TileLayer=kn,t.tileLayer=si,t.GridLayer=Sn,t.gridLayer=oi,t.SVG=Nn,t.svg=hi,t.Renderer=In,t.Canvas=An,t.canvas=ai,t.Path=pn,t.CircleMarker=mn,t.circleMarker=Ut,t.Circle=fn,t.circle=Vt,t.Polyline=gn,t.polyline=Gt,t.Polygon=vn,t.polygon=qt,t.Rectangle=jn,t.rectangle=ui,t.Map=Se,t.map=Ct}) \ No newline at end of file +var t=this._worldWidth,i=Math.round(t/2),e=this._initialWorldOffset,n=this._draggable._newPos.x,o=(n-i+e)%t+i-e,s=(n+i+e)%t-i-e,r=Math.abs(o+e)0?s:-s))-i;this._delta=0,this._startTime=null,r&&("center"===t.options.scrollWheelZoom?t.setZoom(i+r):t.setZoomAround(this._lastMousePos,i+r))}});Se.addInitHook("addHandler","scrollWheelZoom",Vn),Se.mergeOptions({tap:!0,tapTolerance:15});var Gn=Fe.extend({addHooks:function(){V(this._map._container,"touchstart",this._onDown,this)},removeHooks:function(){G(this._map._container,"touchstart",this._onDown,this)},_onDown:function(t){if(t.touches){if($(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 x(i.clientX,i.clientY),n.tagName&&"a"===n.tagName.toLowerCase()&&pt(n,"leaflet-active"),this._holdTimeout=setTimeout(e(function(){this._isTapValid()&&(this._fireClick=!1,this._onUp(),this._simulateEvent("contextmenu",i))},this),1e3),this._simulateEvent("mousedown",i),V(document,{touchmove:this._onMove,touchend:this._onUp},this)}},_onUp:function(t){if(clearTimeout(this._holdTimeout),G(document,{touchmove:this._onMove,touchend:this._onUp},this),this._fireClick&&t&&t.changedTouches){var i=t.changedTouches[0],e=i.target;e&&e.tagName&&"a"===e.tagName.toLowerCase()&&mt(e,"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 i=t.touches[0];this._newPos=new x(i.clientX,i.clientY),this._simulateEvent("mousemove",i)},_simulateEvent:function(t,i){var e=document.createEvent("MouseEvents");e._simulated=!0,i.target._simulatedClick=!0,e.initMouseEvent(t,!0,!0,window,1,i.screenX,i.screenY,i.clientX,i.clientY,!1,!1,!1,!1,0,null),i.target.dispatchEvent(e)}});te&&!Qi&&Se.addInitHook("addHandler","tap",Gn),Se.mergeOptions({touchZoom:te&&!Ri,bounceAtZoomLimits:!0});var qn=Fe.extend({addHooks:function(){pt(this._map._container,"leaflet-touch-zoom"),V(this._map._container,"touchstart",this._onTouchStart,this)},removeHooks:function(){mt(this._map._container,"leaflet-touch-zoom"),G(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 e=i.mouseEventToContainerPoint(t.touches[0]),n=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(e.add(n)._divideBy(2))),this._startDist=e.distanceTo(n),this._startZoom=i.getZoom(),this._moved=!1,this._zooming=!0,i._stop(),V(document,"touchmove",this._onTouchMove,this),V(document,"touchend",this._onTouchEnd,this),$(t)}},_onTouchMove:function(t){if(t.touches&&2===t.touches.length&&this._zooming){var i=this._map,n=i.mouseEventToContainerPoint(t.touches[0]),o=i.mouseEventToContainerPoint(t.touches[1]),s=n.distanceTo(o)/this._startDist;if(this._zoom=i.getScaleZoom(s,this._startZoom),!i.options.bounceAtZoomLimits&&(this._zoomi.getMaxZoom()&&s>1)&&(this._zoom=i._limitZoom(this._zoom)),"center"===i.options.touchZoom){if(this._center=this._startLatLng,1===s)return}else{var r=n._add(o)._divideBy(2)._subtract(this._centerPoint);if(1===s&&0===r.x&&0===r.y)return;this._center=i.unproject(i.project(this._pinchStartLatLng,this._zoom).subtract(r),this._zoom)}this._moved||(i._moveStart(!0),this._moved=!0),g(this._animRequest);var a=e(i._move,i,this._center,this._zoom,{pinch:!0,round:!1});this._animRequest=f(a,this,!0),$(t)}},_onTouchEnd:function(){return this._moved&&this._zooming?(this._zooming=!1,g(this._animRequest),G(document,"touchmove",this._onTouchMove),G(document,"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)}});Se.addInitHook("addHandler","touchZoom",qn),Se.BoxZoom=Wn,Se.DoubleClickZoom=Hn,Se.Drag=Fn,Se.Keyboard=Un,Se.ScrollWheelZoom=Vn,Se.Tap=Gn,Se.TouchZoom=qn;var Kn=window.L;window.L=t,t.version="1.1.0",t.noConflict=li,t.Control=ke,t.control=Be,t.Browser=ae,t.Evented=wi,t.Mixin=Ue,t.Util=yi,t.Class=v,t.Handler=Fe,t.extend=i,t.bind=e,t.stamp=n,t.setOptions=l,t.DomEvent=Le,t.DomUtil=Ze,t.PosAnimation=Ee,t.Draggable=Ye,t.LineUtil=Xe,t.PolyUtil=Je,t.Point=x,t.point=w,t.Bounds=P,t.bounds=b,t.Transformation=Z,t.transformation=E,t.Projection=tn,t.LatLng=M,t.latLng=C,t.LatLngBounds=T,t.latLngBounds=z,t.CRS=Li,t.GeoJSON=yn,t.geoJSON=ii,t.geoJson=wn,t.Layer=sn,t.LayerGroup=rn,t.layerGroup=an,t.FeatureGroup=hn,t.featureGroup=un,t.ImageOverlay=Ln,t.imageOverlay=Pn,t.VideoOverlay=bn,t.videoOverlay=ei,t.DivOverlay=Tn,t.Popup=zn,t.popup=Mn,t.Tooltip=Cn,t.tooltip=Zn,t.Icon=ln,t.icon=Ht,t.DivIcon=En,t.divIcon=ni,t.Marker=dn,t.marker=Ft,t.TileLayer=kn,t.tileLayer=si,t.GridLayer=Sn,t.gridLayer=oi,t.SVG=Nn,t.svg=hi,t.Renderer=In,t.Canvas=An,t.canvas=ai,t.Path=pn,t.CircleMarker=mn,t.circleMarker=Ut,t.Circle=fn,t.circle=Vt,t.Polyline=gn,t.polyline=Gt,t.Polygon=vn,t.polygon=qt,t.Rectangle=jn,t.rectangle=ui,t.Map=Se,t.map=Ct}) \ 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 ac93508be61..7f5a96d3c93 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/panels/ha-panel-shopping-list.html b/homeassistant/components/frontend/www_static/panels/ha-panel-shopping-list.html index 99d4b1db86e..20d85cfdee9 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-shopping-list.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-shopping-list.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-shopping-list.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-shopping-list.html.gz index f80112f596d..6c71d75c390 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-shopping-list.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-shopping-list.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-zwave.html b/homeassistant/components/frontend/www_static/panels/ha-panel-zwave.html index f4142c54f23..884618cd34c 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-zwave.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-zwave.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-zwave.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-zwave.html.gz index 271ecd66614..9c93970e336 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-zwave.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-zwave.html.gz differ diff --git a/homeassistant/components/frontend/www_static/service_worker.js b/homeassistant/components/frontend/www_static/service_worker.js index 62d99bab65c..bb8dcaf4735 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=[["/","0db8ddbf9b3ec817eebadc6866a9499d"],["/frontend/panels/dev-event-4886c821235492b1b92739b580d21c61.html","0f16df49a7d965ddc1fd55f7bd3ffd3f"],["/frontend/panels/dev-info-24e888ec7a8acd0c395b34396e9001bc.html","7bb116813e8dbab7bcfabdf4de3ec83f"],["/frontend/panels/dev-service-ac2c50e486927dc4443e93d79f08c06e.html","1daf8c159d2fab036f7094b0e737f1a0"],["/frontend/panels/dev-state-8f1a27c04db6329d31cfcc7d0d6a0869.html","002ea95ab67f5c06f9112008a81e571b"],["/frontend/panels/dev-template-82cd543177c417e5c6612e07df851e6b.html","81c4dbc540739dcf49c351cf565db71b"],["/frontend/panels/map-b4923812c695dd8a69ad3da380ffe7b4.html","e88c1c3fc6d3dd0db561ce0a1f5beaf5"],["/static/compatibility-8e4c44b5f4288cc48ec1ba94a9bec812.js","4704a985ad259e324c3d8a0a40f6d937"],["/static/core-d4a7cb8c80c62b536764e0e81385f6aa.js","37e34ec6aa0fa155c7d50e2883be1ead"],["/static/frontend-7bd9aa75b2602768e66cf7e20845d7c4.html","054d60e22570068f7b2f43cb893d40f7"],["/static/mdi-e91f61a039ed0a9936e7ee5360da3870.html","5e587bc82719b740a4f0798722a83aee"],["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","32b5a9b7ada86304bec6b43d3f2194f0"]],cacheName="sw-precache-v3--"+(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()},cleanResponse=function(e){return e.redirected?("body"in e?Promise.resolve(e.body):e.blob()).then(function(t){return new Response(t,{headers:e.headers,status:e.status,statusText:e.statusText})}):Promise.resolve(e)},createCacheKey=function(e,t,n,a){var c=new URL(e);return a&&c.pathname.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)){var a=new Request(n,{credentials:"same-origin"});return fetch(a).then(function(t){if(!t.ok)throw new Error("Request for "+n+" returned a response with status "+t.status);return cleanResponse(t).then(function(t){return e.put(n,t)})})}}))})}).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);t||(n=addDirectoryIndex(n,"index.html"),t=urlsToCacheKeys.has(n));!t&&"navigate"===e.request.mode&&isPathWhitelisted(["^((?!(static|api|local|service_worker.js|manifest.json)).)*$"],e.request.url)&&(n=new URL("/",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)&&e.waitUntil(clients.matchAll({type:"window"}).then(function(e){var n,a;for(n=0;n= run.start) & - (States.created < utc_point_in_time) & - (~States.domain.in_(IGNORE_DOMAINS))) + if entity_ids and len(entity_ids) == 1: + # Use an entirely different (and extremely fast) query if we only + # have a single entity id + most_recent_state_ids = session.query( + States.state_id.label('max_state_id') + ).filter( + (States.created < utc_point_in_time) & + (States.entity_id.in_(entity_ids)) + ).order_by( + States.created.desc()) - if filters: - most_recent_state_ids = filters.apply(most_recent_state_ids, - entity_ids) + if filters: + most_recent_state_ids = filters.apply(most_recent_state_ids, + entity_ids) - most_recent_state_ids = most_recent_state_ids.group_by( - States.entity_id).subquery() + most_recent_state_ids = most_recent_state_ids.limit(1) + + else: + # We have more than one entity to look at (most commonly we want + # all entities,) so we need to do a search on all states since the + # last recorder run started. + most_recent_state_ids = session.query( + func.max(States.state_id).label('max_state_id') + ).filter( + (States.created >= run.start) & + (States.created < utc_point_in_time) & + (~States.domain.in_(IGNORE_DOMAINS))) + + if filters: + most_recent_state_ids = filters.apply(most_recent_state_ids, + entity_ids) + + most_recent_state_ids = most_recent_state_ids.group_by( + States.entity_id) + + most_recent_state_ids = most_recent_state_ids.subquery() query = session.query(States).join(most_recent_state_ids, and_( States.state_id == most_recent_state_ids.c.max_state_id)) diff --git a/homeassistant/components/keyboard_remote.py b/homeassistant/components/keyboard_remote.py index 88d406ace0b..5a81f6d2a9e 100644 --- a/homeassistant/components/keyboard_remote.py +++ b/homeassistant/components/keyboard_remote.py @@ -94,20 +94,25 @@ class KeyboardRemote(threading.Thread): if self.dev is not None: _LOGGER.debug("Keyboard connected, %s", self.device_id) else: - id_folder = '/dev/input/by-id/' - device_names = [InputDevice(file_name).name - for file_name in list_devices()] _LOGGER.debug( 'Keyboard not connected, %s.\n\ - Check /dev/input/event* permissions.\ - Possible device names are:\n %s.\n \ - Possible device descriptors are %s:\n %s', - self.device_id, - device_names, - id_folder, - os.listdir(id_folder) + Check /dev/input/event* permissions.', + self.device_id ) + id_folder = '/dev/input/by-id/' + + if os.path.isdir(id_folder): + device_names = [InputDevice(file_name).name + for file_name in list_devices()] + _LOGGER.debug( + 'Possible device names are:\n %s.\n \ + Possible device descriptors are %s:\n %s', + device_names, + id_folder, + os.listdir(id_folder) + ) + threading.Thread.__init__(self) self.stopped = threading.Event() self.hass = hass diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index a6c5f855875..908a9d24e04 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -33,7 +33,7 @@ import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['aiolifx==0.5.2', 'aiolifx_effects==0.1.1'] +REQUIREMENTS = ['aiolifx==0.5.4', 'aiolifx_effects==0.1.1'] UDP_BROADCAST_PORT = 56700 diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/light/tplink.py new file mode 100644 index 00000000000..333661870d1 --- /dev/null +++ b/homeassistant/components/light/tplink.py @@ -0,0 +1,116 @@ +""" +Support for TPLink lights. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/light.tplink/ +""" +import logging +from homeassistant.const import (CONF_HOST, CONF_NAME) +from homeassistant.components.light import ( + Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_KELVIN, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP) +from homeassistant.util.color import \ + color_temperature_mired_to_kelvin as mired_to_kelvin +from homeassistant.util.color import \ + color_temperature_kelvin_to_mired as kelvin_to_mired + +REQUIREMENTS = ['pyHS100==0.2.4.2'] + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_TPLINK = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Initialise pyLB100 SmartBulb.""" + from pyHS100 import SmartBulb + host = config.get(CONF_HOST) + name = config.get(CONF_NAME) + add_devices([TPLinkSmartBulb(SmartBulb(host), name)], True) + + +def brightness_to_percentage(byt): + """Convert brightness from absolute 0..255 to percentage.""" + return (byt*100.0)/255.0 + + +def brightness_from_percentage(percent): + """Convert percentage to absolute value 0..255.""" + return (percent*255.0)/100.0 + + +class TPLinkSmartBulb(Light): + """Representation of a TPLink Smart Bulb.""" + + def __init__(self, smartbulb, name): + """Initialize the bulb.""" + self.smartbulb = smartbulb + + # Use the name set on the device if not set + if name is None: + self._name = self.smartbulb.alias + else: + self._name = name + + self._state = None + _LOGGER.debug("Setting up TP-Link Smart Bulb") + + @property + def name(self): + """Return the name of the Smart Bulb, if any.""" + return self._name + + def turn_on(self, **kwargs): + """Turn the light on.""" + if ATTR_COLOR_TEMP in kwargs: + self.smartbulb.color_temp = \ + mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) + if ATTR_KELVIN in kwargs: + self.smartbulb.color_temp = kwargs[ATTR_KELVIN] + if ATTR_BRIGHTNESS in kwargs: + brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255) + self.smartbulb.brightness = brightness_to_percentage(brightness) + + self.smartbulb.state = self.smartbulb.BULB_STATE_ON + + def turn_off(self): + """Turn the light off.""" + self.smartbulb.state = self.smartbulb.BULB_STATE_OFF + + @property + def color_temp(self): + """Return the color temperature of this light in mireds for HA.""" + if self.smartbulb.is_color: + if (self.smartbulb.color_temp is not None and + self.smartbulb.color_temp != 0): + return kelvin_to_mired(self.smartbulb.color_temp) + else: + return None + else: + return None + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return brightness_from_percentage(self.smartbulb.brightness) + + @property + def is_on(self): + """True if device is on.""" + return self.smartbulb.state == \ + self.smartbulb.BULB_STATE_ON + + def update(self): + """Update the TP-Link Bulb's state.""" + from pyHS100 import SmartPlugException + try: + self._state = self.smartbulb.state == \ + self.smartbulb.BULB_STATE_ON + + except (SmartPlugException, OSError) as ex: + _LOGGER.warning('Could not read state for %s: %s', self.name, ex) + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_TPLINK diff --git a/homeassistant/components/light/velbus.py b/homeassistant/components/light/velbus.py new file mode 100644 index 00000000000..8a02b36b75f --- /dev/null +++ b/homeassistant/components/light/velbus.py @@ -0,0 +1,104 @@ +""" +Support for Velbus lights. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.velbus/ +""" +import asyncio +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_NAME, CONF_DEVICES +from homeassistant.components.light import Light, PLATFORM_SCHEMA +from homeassistant.components.velbus import DOMAIN +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ['velbus'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [ + { + vol.Required('module'): cv.positive_int, + vol.Required('channel'): cv.positive_int, + vol.Required(CONF_NAME): cv.string + } + ]) +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up Lights.""" + velbus = hass.data[DOMAIN] + add_devices(VelbusLight(light, velbus) for light in config[CONF_DEVICES]) + + +class VelbusLight(Light): + """Representation of a Velbus Light.""" + + def __init__(self, light, velbus): + """Initialize a Velbus light.""" + self._velbus = velbus + self._name = light[CONF_NAME] + self._module = light['module'] + self._channel = light['channel'] + self._state = False + + @asyncio.coroutine + def async_added_to_hass(self): + """Add listener for Velbus messages on bus.""" + def _init_velbus(): + """Initialize Velbus on startup.""" + self._velbus.subscribe(self._on_message) + self.get_status() + + yield from self.hass.async_add_job(_init_velbus) + + def _on_message(self, message): + import velbus + if isinstance(message, velbus.RelayStatusMessage) and \ + message.address == self._module and \ + message.channel == self._channel: + self._state = message.is_on() + self.schedule_update_ha_state() + + @property + def name(self): + """Return the display name of this light.""" + return self._name + + @property + def should_poll(self): + """Disable polling.""" + return False + + @property + def is_on(self): + """Return true if the light is on.""" + return self._state + + def turn_on(self, **kwargs): + """Instruct the light to turn on.""" + import velbus + message = velbus.SwitchRelayOnMessage() + message.set_defaults(self._module) + message.relay_channels = [self._channel] + self._velbus.send(message) + + def turn_off(self, **kwargs): + """Instruct the light to turn off.""" + import velbus + message = velbus.SwitchRelayOffMessage() + message.set_defaults(self._module) + message.relay_channels = [self._channel] + self._velbus.send(message) + + def get_status(self): + """Retrieve current status.""" + import velbus + message = velbus.ModuleStatusRequestMessage() + message.set_defaults(self._module) + message.channels = [self._channel] + self._velbus.send(message) diff --git a/homeassistant/components/light/zha.py b/homeassistant/components/light/zha.py index 619723d3168..2a3ce18d74e 100644 --- a/homeassistant/components/light/zha.py +++ b/homeassistant/components/light/zha.py @@ -9,11 +9,14 @@ import logging from homeassistant.components import light, zha from homeassistant.util.color import color_RGB_to_xy +from homeassistant.const import STATE_UNKNOWN _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['zha'] +DEFAULT_DURATION = 0.5 + @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): @@ -48,6 +51,7 @@ class Light(zha.Entity, light.Light): import bellows.zigbee.zcl.clusters as zcl_clusters if zcl_clusters.general.LevelControl.cluster_id in self._in_clusters: self._supported_features |= light.SUPPORT_BRIGHTNESS + self._supported_features |= light.SUPPORT_TRANSITION self._brightness = 0 if zcl_clusters.lighting.Color.cluster_id in self._in_clusters: # Not sure all color lights necessarily support this directly @@ -62,14 +66,15 @@ class Light(zha.Entity, light.Light): @property def is_on(self) -> bool: """Return true if entity is on.""" - if self._state == 'unknown': + if self._state == STATE_UNKNOWN: return False return bool(self._state) @asyncio.coroutine def async_turn_on(self, **kwargs): """Turn the entity on.""" - duration = 5 # tenths of s + duration = kwargs.get(light.ATTR_TRANSITION, DEFAULT_DURATION) + duration = duration * 10 # tenths of s if light.ATTR_COLOR_TEMP in kwargs: temperature = kwargs[light.ATTR_COLOR_TEMP] yield from self._endpoint.light_color.move_to_color_temp( @@ -91,7 +96,8 @@ class Light(zha.Entity, light.Light): ) if self._brightness is not None: - brightness = kwargs.get('brightness', self._brightness or 255) + brightness = kwargs.get( + light.ATTR_BRIGHTNESS, self._brightness or 255) self._brightness = brightness # Move to level with on/off: yield from self._endpoint.level.move_to_level_with_on_off( diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 51acf68d819..c416157169e 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.8.1'] +REQUIREMENTS = ['pychromecast==0.8.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/prometheus.py b/homeassistant/components/prometheus.py index 18a3a932d36..f244bcdd740 100644 --- a/homeassistant/components/prometheus.py +++ b/homeassistant/components/prometheus.py @@ -216,6 +216,9 @@ class Metrics: value = state_helper.state_as_number(state) metric.labels(**self._labels(state)).set(value) + def _handle_zwave(self, state): + self._battery(state) + class PrometheusView(HomeAssistantView): """Handle Prometheus requests.""" diff --git a/homeassistant/components/sensor/fitbit.py b/homeassistant/components/sensor/fitbit.py index 0c10d2159ea..c0256e3a88b 100644 --- a/homeassistant/components/sensor/fitbit.py +++ b/homeassistant/components/sensor/fitbit.py @@ -433,9 +433,8 @@ class FitbitSensor(Entity): def update(self): """Get the latest data from the Fitbit API and update the states.""" - if self.resource_type == 'devices/battery': - response = self.client.get_devices() - self._state = response[0].get('battery') + if self.resource_type == 'devices/battery' and self.extra: + self._state = self.extra.get('battery') else: container = self.resource_type.replace("/", "-") response = self.client.time_series(self.resource_type, period='7d') diff --git a/homeassistant/components/sensor/lyft.py b/homeassistant/components/sensor/lyft.py index c16fae9f5d5..11ca07f7fb8 100644 --- a/homeassistant/components/sensor/lyft.py +++ b/homeassistant/components/sensor/lyft.py @@ -45,20 +45,27 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Lyft sensor.""" from lyft_rides.auth import ClientCredentialGrant + from lyft_rides.errors import APIError auth_flow = ClientCredentialGrant(client_id=config.get(CONF_CLIENT_ID), client_secret=config.get( CONF_CLIENT_SECRET), scopes="public", is_sandbox_mode=False) - session = auth_flow.get_session() + try: + session = auth_flow.get_session() + + timeandpriceest = LyftEstimate( + session, config[CONF_START_LATITUDE], config[CONF_START_LONGITUDE], + config.get(CONF_END_LATITUDE), config.get(CONF_END_LONGITUDE)) + timeandpriceest.fetch_data() + except APIError as exc: + _LOGGER.error("Error setting up Lyft platform: %s", exc) + return False wanted_product_ids = config.get(CONF_PRODUCT_IDS) dev = [] - timeandpriceest = LyftEstimate( - session, config[CONF_START_LATITUDE], config[CONF_START_LONGITUDE], - config.get(CONF_END_LATITUDE), config.get(CONF_END_LONGITUDE)) for product_id, product in timeandpriceest.products.items(): if (wanted_product_ids is not None) and \ (product_id not in wanted_product_ids): @@ -188,14 +195,18 @@ class LyftEstimate(object): self.end_latitude = end_latitude self.end_longitude = end_longitude self.products = None - self.__real_update() @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest product info and estimates from the Lyft API.""" - self.__real_update() + from lyft_rides.errors import APIError + try: + self.fetch_data() + except APIError as exc: + _LOGGER.error("Error fetching Lyft data: %s", exc) - def __real_update(self): + def fetch_data(self): + """Get the latest product info and estimates from the Lyft API.""" from lyft_rides.client import LyftRidesClient client = LyftRidesClient(self._session) diff --git a/homeassistant/components/sensor/uk_transport.py b/homeassistant/components/sensor/uk_transport.py new file mode 100644 index 00000000000..b9ce98ec257 --- /dev/null +++ b/homeassistant/components/sensor/uk_transport.py @@ -0,0 +1,275 @@ +"""Support for UK public transport data provided by transportapi.com. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.uk_transport/ +""" +import logging +import re +from datetime import datetime, timedelta +import requests +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +ATTR_ATCOCODE = 'atcocode' +ATTR_LOCALITY = 'locality' +ATTR_STOP_NAME = 'stop_name' +ATTR_REQUEST_TIME = 'request_time' +ATTR_NEXT_BUSES = 'next_buses' +ATTR_STATION_CODE = 'station_code' +ATTR_CALLING_AT = 'calling_at' +ATTR_NEXT_TRAINS = 'next_trains' + +CONF_API_APP_KEY = 'app_key' +CONF_API_APP_ID = 'app_id' +CONF_QUERIES = 'queries' +CONF_MODE = 'mode' +CONF_ORIGIN = 'origin' +CONF_DESTINATION = 'destination' + +_QUERY_SCHEME = vol.Schema({ + vol.Required(CONF_MODE): + vol.All(cv.ensure_list, [vol.In(list(['bus', 'train']))]), + vol.Required(CONF_ORIGIN): cv.string, + vol.Required(CONF_DESTINATION): cv.string, +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_APP_ID): cv.string, + vol.Required(CONF_API_APP_KEY): cv.string, + vol.Required(CONF_QUERIES): [_QUERY_SCHEME], +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Get the uk_transport sensor.""" + sensors = [] + number_sensors = len(config.get(CONF_QUERIES)) + interval = timedelta(seconds=87*number_sensors) + + for query in config.get(CONF_QUERIES): + if 'bus' in query.get(CONF_MODE): + stop_atcocode = query.get(CONF_ORIGIN) + bus_direction = query.get(CONF_DESTINATION) + sensors.append( + UkTransportLiveBusTimeSensor( + config.get(CONF_API_APP_ID), + config.get(CONF_API_APP_KEY), + stop_atcocode, + bus_direction, + interval)) + + elif 'train' in query.get(CONF_MODE): + station_code = query.get(CONF_ORIGIN) + calling_at = query.get(CONF_DESTINATION) + sensors.append( + UkTransportLiveTrainTimeSensor( + config.get(CONF_API_APP_ID), + config.get(CONF_API_APP_KEY), + station_code, + calling_at, + interval)) + + add_devices(sensors, True) + + +class UkTransportSensor(Entity): + """ + Sensor that reads the UK transport web API. + + transportapi.com provides comprehensive transport data for UK train, tube + and bus travel across the UK via simple JSON API. Subclasses of this + base class can be used to access specific types of information. + """ + + TRANSPORT_API_URL_BASE = "https://transportapi.com/v3/uk/" + ICON = 'mdi:train' + + def __init__(self, name, api_app_id, api_app_key, url): + """Initialize the sensor.""" + self._data = {} + self._api_app_id = api_app_id + self._api_app_key = api_app_key + self._url = self.TRANSPORT_API_URL_BASE + url + self._name = name + self._state = None + + @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 this state is expressed in.""" + return "min" + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self.ICON + + def _do_api_request(self, params): + """Perform an API request.""" + request_params = dict({ + 'app_id': self._api_app_id, + 'app_key': self._api_app_key, + }, **params) + + response = requests.get(self._url, params=request_params) + if response.status_code != 200: + _LOGGER.warning('Invalid response from API') + elif 'error' in response.json(): + if 'exceeded' in response.json()['error']: + self._state = 'Useage limites exceeded' + if 'invalid' in response.json()['error']: + self._state = 'Credentials invalid' + else: + self._data = response.json() + + +class UkTransportLiveBusTimeSensor(UkTransportSensor): + """Live bus time sensor from UK transportapi.com.""" + + ICON = 'mdi:bus' + + def __init__(self, api_app_id, api_app_key, + stop_atcocode, bus_direction, interval): + """Construct a live bus time sensor.""" + self._stop_atcocode = stop_atcocode + self._bus_direction = bus_direction + self._next_buses = [] + self._destination_re = re.compile( + '{}'.format(bus_direction), re.IGNORECASE + ) + + sensor_name = 'Next bus to {}'.format(bus_direction) + stop_url = 'bus/stop/{}/live.json'.format(stop_atcocode) + + UkTransportSensor.__init__( + self, sensor_name, api_app_id, api_app_key, stop_url + ) + self.update = Throttle(interval)(self._update) + + def _update(self): + """Get the latest live departure data for the specified stop.""" + params = {'group': 'route', 'nextbuses': 'no'} + + self._do_api_request(params) + + if self._data != {}: + self._next_buses = [] + + for (route, departures) in self._data['departures'].items(): + for departure in departures: + if self._destination_re.search(departure['direction']): + self._next_buses.append({ + 'route': route, + 'direction': departure['direction'], + 'scheduled': departure['aimed_departure_time'], + 'estimated': departure['best_departure_estimate'] + }) + + self._state = min(map( + _delta_mins, [bus['scheduled'] for bus in self._next_buses] + )) + + @property + def device_state_attributes(self): + """Return other details about the sensor state.""" + attrs = {} + if self._data is not None: + for key in [ + ATTR_ATCOCODE, ATTR_LOCALITY, ATTR_STOP_NAME, + ATTR_REQUEST_TIME + ]: + attrs[key] = self._data.get(key) + attrs[ATTR_NEXT_BUSES] = self._next_buses + return attrs + + +class UkTransportLiveTrainTimeSensor(UkTransportSensor): + """Live train time sensor from UK transportapi.com.""" + + ICON = 'mdi:train' + + def __init__(self, api_app_id, api_app_key, + station_code, calling_at, interval): + """Construct a live bus time sensor.""" + self._station_code = station_code + self._calling_at = calling_at + self._next_trains = [] + + sensor_name = 'Next train to {}'.format(calling_at) + query_url = 'train/station/{}/live.json'.format(station_code) + + UkTransportSensor.__init__( + self, sensor_name, api_app_id, api_app_key, query_url + ) + self.update = Throttle(interval)(self._update) + + def _update(self): + """Get the latest live departure data for the specified stop.""" + params = {'darwin': 'false', + 'calling_at': self._calling_at, + 'train_status': 'passenger'} + + self._do_api_request(params) + self._next_trains = [] + + if self._data != {}: + if self._data['departures']['all'] == []: + self._state = 'No departures' + else: + for departure in self._data['departures']['all']: + self._next_trains.append({ + 'origin_name': departure['origin_name'], + 'destination_name': departure['destination_name'], + 'status': departure['status'], + 'scheduled': departure['aimed_departure_time'], + 'estimated': departure['expected_departure_time'], + 'platform': departure['platform'], + 'operator_name': departure['operator_name'] + }) + + self._state = min(map( + _delta_mins, + [train['scheduled'] for train in self._next_trains] + )) + + @property + def device_state_attributes(self): + """Return other details about the sensor state.""" + attrs = {} + if self._data is not None: + attrs[ATTR_STATION_CODE] = self._station_code + attrs[ATTR_CALLING_AT] = self._calling_at + if self._next_trains: + attrs[ATTR_NEXT_TRAINS] = self._next_trains + return attrs + + +def _delta_mins(hhmm_time_str): + """Calculate time delta in minutes to a time in hh:mm format.""" + now = datetime.now() + hhmm_time = datetime.strptime(hhmm_time_str, '%H:%M') + + hhmm_datetime = datetime( + now.year, now.month, now.day, + hour=hhmm_time.hour, minute=hhmm_time.minute + ) + if hhmm_datetime < now: + hhmm_datetime += timedelta(days=1) + + delta_mins = (hhmm_datetime - now).seconds // 60 + return delta_mins diff --git a/homeassistant/components/shopping_list.py b/homeassistant/components/shopping_list.py index 7247579fa39..ac59c15572a 100644 --- a/homeassistant/components/shopping_list.py +++ b/homeassistant/components/shopping_list.py @@ -70,11 +70,16 @@ class ListTopItemsIntent(intent.IntentHandler): @asyncio.coroutine def async_handle(self, intent_obj): """Handle the intent.""" + items = intent_obj.hass.data[DOMAIN][-5:] response = intent_obj.create_response() - response.async_set_speech( - "These are the top 5 items in your shopping list: {}".format( - ', '.join(reversed(intent_obj.hass.data[DOMAIN][-5:])))) - intent_obj.hass.bus.async_fire(EVENT) + + if len(items) == 0: + response.async_set_speech( + "There are no items on your shopping list") + else: + response.async_set_speech( + "These are the top {} items on your shopping list: {}".format( + min(len(items), 5), ', '.join(reversed(items)))) return response diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index dea4285e3a9..5613bcbb19e 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -15,6 +15,7 @@ from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.const import CONF_NAME, CONF_PLATFORM from homeassistant.helpers.event import track_time_change from homeassistant.helpers.sun import get_astral_event_date +from homeassistant.util import slugify from homeassistant.util.color import ( color_temperature_to_rgb, color_RGB_to_xy, color_temperature_kelvin_to_mired) @@ -111,7 +112,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Update lights.""" flux.flux_update() - hass.services.register(DOMAIN, name + '_update', update) + service_name = slugify("{} {}".format(name, 'update')) + hass.services.register(DOMAIN, service_name, update) class FluxSwitch(SwitchDevice): diff --git a/homeassistant/components/switch/velbus.py b/homeassistant/components/switch/velbus.py new file mode 100644 index 00000000000..15090091a52 --- /dev/null +++ b/homeassistant/components/switch/velbus.py @@ -0,0 +1,111 @@ +""" +Support for Velbus switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.velbus/ +""" + +import asyncio +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_NAME, CONF_DEVICES +from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.velbus import DOMAIN +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +SWITCH_SCHEMA = { + vol.Required('module'): cv.positive_int, + vol.Required('channel'): cv.positive_int, + vol.Required(CONF_NAME): cv.string +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DEVICES): + vol.All(cv.ensure_list, [SWITCH_SCHEMA]) +}) + +DEPENDENCIES = ['velbus'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Switch.""" + velbus = hass.data[DOMAIN] + devices = [] + + for switch in config[CONF_DEVICES]: + devices.append(VelbusSwitch(switch, velbus)) + add_devices(devices) + return True + + +class VelbusSwitch(SwitchDevice): + """Representation of a switch.""" + + def __init__(self, switch, velbus): + """Initialize a Velbus switch.""" + self._velbus = velbus + self._name = switch[CONF_NAME] + self._module = switch['module'] + self._channel = switch['channel'] + self._state = False + + @asyncio.coroutine + def async_added_to_hass(self): + """Add listener for Velbus messages on bus.""" + def _init_velbus(): + """Initialize Velbus on startup.""" + self._velbus.subscribe(self._on_message) + self.get_status() + + yield from self.hass.async_add_job(_init_velbus) + + def _on_message(self, message): + import velbus + if isinstance(message, velbus.RelayStatusMessage) and \ + message.address == self._module and \ + message.channel == self._channel: + self._state = message.is_on() + self.schedule_update_ha_state() + + @property + def name(self): + """Return the display name of this switch.""" + return self._name + + @property + def should_poll(self): + """Disable polling.""" + return False + + @property + def is_on(self): + """Return true if the switch is on.""" + return self._state + + def turn_on(self, **kwargs): + """Instruct the switch to turn on.""" + import velbus + message = velbus.SwitchRelayOnMessage() + message.set_defaults(self._module) + message.relay_channels = [self._channel] + self._velbus.send(message) + + def turn_off(self, **kwargs): + """Instruct the switch to turn off.""" + import velbus + message = velbus.SwitchRelayOffMessage() + message.set_defaults(self._module) + message.relay_channels = [self._channel] + self._velbus.send(message) + + def get_status(self): + """Retrieve current status.""" + import velbus + message = velbus.ModuleStatusRequestMessage() + message.set_defaults(self._module) + message.channels = [self._channel] + self._velbus.send(message) diff --git a/homeassistant/components/velbus.py b/homeassistant/components/velbus.py new file mode 100644 index 00000000000..ff2db955d31 --- /dev/null +++ b/homeassistant/components/velbus.py @@ -0,0 +1,43 @@ +""" +Support for Velbus platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/velbus/ +""" +import logging +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_PORT + +REQUIREMENTS = ['python-velbus==2.0.11'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'velbus' + + +VELBUS_MESSAGE = 'velbus.message' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_PORT): cv.string, + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Velbus platform.""" + import velbus + port = config[DOMAIN].get(CONF_PORT) + connection = velbus.VelbusUSBConnection(port) + controller = velbus.Controller(connection) + hass.data[DOMAIN] = controller + + def stop_velbus(event): + """Disconnect from serial port.""" + _LOGGER.debug("Shutting down ") + connection.stop() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_velbus) + return True diff --git a/homeassistant/components/weather/yweather.py b/homeassistant/components/weather/yweather.py index 0e216273d65..687d919ed95 100644 --- a/homeassistant/components/weather/yweather.py +++ b/homeassistant/components/weather/yweather.py @@ -11,17 +11,21 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.weather import ( - WeatherEntity, PLATFORM_SCHEMA, ATTR_FORECAST_TEMP) + WeatherEntity, PLATFORM_SCHEMA, + ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME) from homeassistant.const import (TEMP_CELSIUS, CONF_NAME, STATE_UNKNOWN) REQUIREMENTS = ["yahooweather==0.8"] _LOGGER = logging.getLogger(__name__) +DATA_CONDITION = 'yahoo_condition' + ATTR_FORECAST_CONDITION = 'condition' ATTRIBUTION = "Weather details provided by Yahoo! Inc." -CONF_FORECAST = 'forecast' +ATTR_FORECAST_TEMP_LOW = 'templow' + CONF_WOEID = 'woeid' DEFAULT_NAME = 'Yweather' @@ -33,23 +37,22 @@ CONDITION_CLASSES = { 'fog': [19, 20, 21, 22, 23], 'hail': [17, 18, 35], 'lightning': [37], - 'lightning-rainy': [38, 39], + 'lightning-rainy': [38, 39, 47], 'partlycloudy': [44], 'pouring': [40, 45], 'rainy': [9, 11, 12], 'snowy': [8, 13, 14, 15, 16, 41, 42, 43], - 'snowy-rainy': [5, 6, 7, 10, 46, 47], - 'sunny': [32], + 'snowy-rainy': [5, 6, 7, 10, 46], + 'sunny': [32, 33, 34], 'windy': [24], 'windy-variant': [], 'exceptional': [0, 1, 2, 3, 4, 25, 36], } + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_WOEID, default=None): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_FORECAST, default=0): - vol.All(vol.Coerce(int), vol.Range(min=0, max=5)), }) @@ -59,7 +62,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): unit = hass.config.units.temperature_unit woeid = config.get(CONF_WOEID) - forecast = config.get(CONF_FORECAST) name = config.get(CONF_NAME) yunit = UNIT_C if unit == TEMP_CELSIUS else UNIT_F @@ -77,22 +79,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.critical("Can't retrieve weather data from Yahoo!") return False - if forecast >= len(yahoo_api.yahoo.Forecast): - _LOGGER.error("Yahoo! only support %d days forecast", - len(yahoo_api.yahoo.Forecast)) - return False + # create condition helper + if DATA_CONDITION not in hass.data: + hass.data[DATA_CONDITION] = [str(x) for x in range(0, 50)] + for cond, condlst in CONDITION_CLASSES.items(): + for condi in condlst: + hass.data[DATA_CONDITION][condi] = cond - add_devices([YahooWeatherWeather(yahoo_api, name, forecast)], True) + add_devices([YahooWeatherWeather(yahoo_api, name)], True) class YahooWeatherWeather(WeatherEntity): """Representation of Yahoo! weather data.""" - def __init__(self, weather_data, name, forecast): + def __init__(self, weather_data, name): """Initialize the sensor.""" self._name = name self._data = weather_data - self._forecast = forecast @property def name(self): @@ -103,9 +106,9 @@ class YahooWeatherWeather(WeatherEntity): def condition(self): """Return the current condition.""" try: - return [k for k, v in CONDITION_CLASSES.items() if - int(self._data.yahoo.Now['code']) in v][0] - except IndexError: + return self.hass.data[DATA_CONDITION][int( + self._data.yahoo.Now['code'])] + except (ValueError, IndexError): return STATE_UNKNOWN @property @@ -138,6 +141,11 @@ class YahooWeatherWeather(WeatherEntity): """Return the wind speed.""" return self._data.yahoo.Wind['speed'] + @property + def wind_bearing(self): + """Return the wind direction.""" + return self._data.yahoo.Wind['direction'] + @property def attribution(self): """Return the attribution.""" @@ -147,19 +155,17 @@ class YahooWeatherWeather(WeatherEntity): def forecast(self): """Return the forecast array.""" try: - forecast_condition = \ - [k for k, v in CONDITION_CLASSES.items() if - int(self._data.yahoo.Forecast[self._forecast]['code']) - in v][0] - except IndexError: + return [ + { + ATTR_FORECAST_TIME: v['date'], + ATTR_FORECAST_TEMP:int(v['high']), + ATTR_FORECAST_TEMP_LOW: int(v['low']), + ATTR_FORECAST_CONDITION: + self.hass.data[DATA_CONDITION][int(v['code'])] + } for v in self._data.yahoo.Forecast] + except (ValueError, IndexError): return STATE_UNKNOWN - return [{ - ATTR_FORECAST_CONDITION: forecast_condition, - ATTR_FORECAST_TEMP: - self._data.yahoo.Forecast[self._forecast]['high'], - }] - def update(self): """Get the latest data from Yahoo! and updates the states.""" self._data.update() diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 459d89a83ce..8843bf53df9 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -134,7 +134,7 @@ class Intent: class IntentResponse: """Response to an intent.""" - def __init__(self, intent): + def __init__(self, intent=None): """Initialize an IntentResponse.""" self.intent = intent self.speech = {} diff --git a/homeassistant/scripts/credstash.py b/homeassistant/scripts/credstash.py new file mode 100644 index 00000000000..9ba945626e2 --- /dev/null +++ b/homeassistant/scripts/credstash.py @@ -0,0 +1,71 @@ +"""Script to get, put and delete secrets stored in credstash.""" +import argparse +import getpass + +from homeassistant.util.yaml import _SECRET_NAMESPACE + +REQUIREMENTS = ['credstash==1.13.2', 'botocore==1.4.93'] + + +def run(args): + """Handle credstash script.""" + parser = argparse.ArgumentParser( + description=("Modify Home-Assistant secrets in credstash." + "Use the secrets in configuration files with: " + "!secret ")) + parser.add_argument( + '--script', choices=['credstash']) + parser.add_argument( + 'action', choices=['get', 'put', 'del', 'list'], + help="Get, put or delete a secret, or list all available secrets") + parser.add_argument( + 'name', help="Name of the secret", nargs='?', default=None) + parser.add_argument( + 'value', help="The value to save when putting a secret", + nargs='?', default=None) + + import credstash + import botocore + + args = parser.parse_args(args) + table = _SECRET_NAMESPACE + + try: + credstash.listSecrets(table=table) + except botocore.errorfactory.ClientError: + credstash.createDdbTable(table=table) + + if args.action == 'list': + secrets = [i['name'] for i in credstash.listSecrets(table=table)] + deduped_secrets = sorted(set(secrets)) + + print('Saved secrets:') + for secret in deduped_secrets: + print(secret) + return 0 + + if args.name is None: + parser.print_help() + return 1 + + if args.action == 'put': + if args.value: + the_secret = args.value + else: + the_secret = getpass.getpass('Please enter the secret for {}: ' + .format(args.name)) + current_version = credstash.getHighestVersion(args.name, table=table) + credstash.putSecret(args.name, + the_secret, + version=int(current_version) + 1, + table=table) + print('Secret {} put successfully'.format(args.name)) + elif args.action == 'get': + the_secret = credstash.getSecret(args.name, table=table) + if the_secret is None: + print('Secret {} not found'.format(args.name)) + else: + print('Secret {}={}'.format(args.name, the_secret)) + elif args.action == 'del': + credstash.deleteSecrets(args.name, table=table) + print('Deleted secret {}'.format(args.name)) diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py index 7827f484fdf..4129a67bf57 100644 --- a/homeassistant/util/yaml.py +++ b/homeassistant/util/yaml.py @@ -12,6 +12,11 @@ try: except ImportError: keyring = None +try: + import credstash +except ImportError: + credstash = None + from homeassistant.exceptions import HomeAssistantError _LOGGER = logging.getLogger(__name__) @@ -200,8 +205,13 @@ def _construct_seq(loader: SafeLineLoader, node: yaml.nodes.Node): def _env_var_yaml(loader: SafeLineLoader, node: yaml.nodes.Node): """Load environment variables and embed it into the configuration YAML.""" - if node.value in os.environ: - return os.environ[node.value] + args = node.value.split() + + # Check for a default value + if len(args) > 1: + return os.getenv(args[0], ' '.join(args[1:])) + elif args[0] in os.environ: + return os.environ[args[0]] else: _LOGGER.error("Environment variable %s not defined.", node.value) raise HomeAssistantError(node.value) @@ -257,6 +267,15 @@ def _secret_yaml(loader: SafeLineLoader, _LOGGER.debug("Secret %s retrieved from keyring", node.value) return pwd + if credstash: + try: + pwd = credstash.getSecret(node.value, table=_SECRET_NAMESPACE) + if pwd: + _LOGGER.debug("Secret %s retrieved from credstash", node.value) + return pwd + except credstash.ItemNotFound: + pass + _LOGGER.error("Secret %s not defined", node.value) raise HomeAssistantError(node.value) diff --git a/pylintrc b/pylintrc index e94cbffe9f9..1ed8d2af336 100644 --- a/pylintrc +++ b/pylintrc @@ -14,6 +14,8 @@ reports=no # too-few-* - same as too-many-* # abstract-method - with intro of async there are always methods missing +generated-members=botocore.errorfactory + disable= abstract-class-little-used, abstract-class-not-used, diff --git a/requirements_all.txt b/requirements_all.txt index 97066df38da..aad16af8592 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -49,7 +49,7 @@ aiodns==1.1.1 aiohttp_cors==0.5.3 # homeassistant.components.light.lifx -aiolifx==0.5.2 +aiolifx==0.5.4 # homeassistant.components.light.lifx aiolifx_effects==0.1.1 @@ -61,7 +61,7 @@ aiopvapi==1.4 alarmdecoder==0.12.3 # homeassistant.components.amcrest -amcrest==1.2.0 +amcrest==1.2.1 # homeassistant.components.media_player.anthemav anthemav==1.1.8 @@ -115,6 +115,9 @@ blockchain==1.3.3 # homeassistant.components.tts.amazon_polly boto3==1.4.3 +# homeassistant.scripts.credstash +botocore==1.4.93 + # homeassistant.components.sensor.broadlink # homeassistant.components.switch.broadlink broadlink==0.5 @@ -136,6 +139,9 @@ colorlog>2.1,<3 # homeassistant.components.binary_sensor.concord232 concord232==0.14 +# homeassistant.scripts.credstash +credstash==1.13.2 + # homeassistant.components.sensor.crimereports crimereports==1.0.0 @@ -404,7 +410,7 @@ myusps==1.1.2 nad_receiver==0.0.6 # homeassistant.components.discovery -netdisco==1.0.1 +netdisco==1.1.0 # homeassistant.components.sensor.neurio_energy neurio==0.3.1 @@ -503,6 +509,7 @@ py-cpuinfo==3.3.0 # homeassistant.components.hdmi_cec pyCEC==0.4.13 +# homeassistant.components.light.tplink # homeassistant.components.switch.tplink pyHS100==0.2.4.2 @@ -535,7 +542,7 @@ pybbox==0.0.5-alpha # pybluez==0.22 # homeassistant.components.media_player.cast -pychromecast==0.8.1 +pychromecast==0.8.2 # homeassistant.components.media_player.cmus pycmus==0.1.0 @@ -754,6 +761,9 @@ python-telegram-bot==6.1.0 # homeassistant.components.sensor.twitch python-twitch==1.3.0 +# homeassistant.components.velbus +python-velbus==2.0.11 + # homeassistant.components.media_player.vlc python-vlc==1.1.2 diff --git a/tests/components/alarm_control_panel/test_manual_mqtt.py b/tests/components/alarm_control_panel/test_manual_mqtt.py new file mode 100644 index 00000000000..c4dcd57ca39 --- /dev/null +++ b/tests/components/alarm_control_panel/test_manual_mqtt.py @@ -0,0 +1,559 @@ +"""The tests for the manual_mqtt Alarm Control Panel component.""" +from datetime import timedelta +import unittest +from unittest.mock import patch + +from homeassistant.setup import setup_component +from homeassistant.const import ( + STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED) +from homeassistant.components import alarm_control_panel +import homeassistant.util.dt as dt_util + +from tests.common import ( + fire_time_changed, get_test_home_assistant, + mock_mqtt_component, fire_mqtt_message, assert_setup_component) + +CODE = 'HELLO_CODE' + + +class TestAlarmControlPanelManualMqtt(unittest.TestCase): + """Test the manual_mqtt alarm module.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.mock_publish = mock_mqtt_component(self.hass) + + def tearDown(self): # pylint: disable=invalid-name + """Stop down everything that was started.""" + self.hass.stop() + + def test_fail_setup_without_state_topic(self): + """Test for failing with no state topic.""" + with assert_setup_component(0) as config: + assert setup_component(self.hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'mqtt_alarm', + 'command_topic': 'alarm/command' + } + }) + assert not config[alarm_control_panel.DOMAIN] + + def test_fail_setup_without_command_topic(self): + """Test failing with no command topic.""" + with assert_setup_component(0): + assert setup_component(self.hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'mqtt_alarm', + 'state_topic': 'alarm/state' + } + }) + + def test_arm_home_no_pending(self): + """Test arm home method.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'code': CODE, + 'pending_time': 0, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state', + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_home(self.hass, CODE) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_HOME, + self.hass.states.get(entity_id).state) + + def test_arm_home_with_pending(self): + """Test arm home method.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'code': CODE, + 'pending_time': 1, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state', + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_home(self.hass, CODE, entity_id) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_PENDING, + self.hass.states.get(entity_id).state) + + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_HOME, + self.hass.states.get(entity_id).state) + + def test_arm_home_with_invalid_code(self): + """Attempt to arm home without a valid code.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'code': CODE, + 'pending_time': 1, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state', + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_home(self.hass, CODE + '2') + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + def test_arm_away_no_pending(self): + """Test arm home method.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'code': CODE, + 'pending_time': 0, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state', + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE, entity_id) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_AWAY, + self.hass.states.get(entity_id).state) + + def test_arm_away_with_pending(self): + """Test arm home method.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'code': CODE, + 'pending_time': 1, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state', + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_PENDING, + self.hass.states.get(entity_id).state) + + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_AWAY, + self.hass.states.get(entity_id).state) + + def test_arm_away_with_invalid_code(self): + """Attempt to arm away without a valid code.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'code': CODE, + 'pending_time': 1, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state', + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_away(self.hass, CODE + '2') + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + def test_trigger_no_pending(self): + """Test triggering when no pending submitted method.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'trigger_time': 1, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state', + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_PENDING, + self.hass.states.get(entity_id).state) + + future = dt_util.utcnow() + timedelta(seconds=60) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_TRIGGERED, + self.hass.states.get(entity_id).state) + + def test_trigger_with_pending(self): + """Test arm home method.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'pending_time': 2, + 'trigger_time': 3, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state', + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_PENDING, + self.hass.states.get(entity_id).state) + + future = dt_util.utcnow() + timedelta(seconds=2) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_TRIGGERED, + self.hass.states.get(entity_id).state) + + future = dt_util.utcnow() + timedelta(seconds=5) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + def test_trigger_with_disarm_after_trigger(self): + """Test disarm after trigger.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'trigger_time': 5, + 'pending_time': 0, + 'disarm_after_trigger': True, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state', + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_TRIGGERED, + self.hass.states.get(entity_id).state) + + future = dt_util.utcnow() + timedelta(seconds=5) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + def test_disarm_while_pending_trigger(self): + """Test disarming while pending state.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'trigger_time': 5, + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state', + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_PENDING, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_disarm(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + future = dt_util.utcnow() + timedelta(seconds=5) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + def test_disarm_during_trigger_with_invalid_code(self): + """Test disarming while code is invalid.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual_mqtt', + 'name': 'test', + 'pending_time': 5, + 'code': CODE + '2', + 'disarm_after_trigger': False, + 'command_topic': 'alarm/command', + 'state_topic': 'alarm/state', + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_PENDING, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_disarm(self.hass, entity_id=entity_id) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_PENDING, + self.hass.states.get(entity_id).state) + + future = dt_util.utcnow() + timedelta(seconds=5) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_TRIGGERED, + self.hass.states.get(entity_id).state) + + def test_arm_home_via_command_topic(self): + """Test arming home via command topic.""" + assert setup_component(self.hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'manual_mqtt', + 'name': 'test', + 'pending_time': 1, + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + 'payload_arm_home': 'ARM_HOME', + } + }) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + # Fire the arm command via MQTT; ensure state changes to pending + fire_mqtt_message(self.hass, 'alarm/command', 'ARM_HOME') + self.hass.block_till_done() + self.assertEqual(STATE_ALARM_PENDING, + self.hass.states.get(entity_id).state) + + # Fast-forward a little bit + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_HOME, + self.hass.states.get(entity_id).state) + + def test_arm_away_via_command_topic(self): + """Test arming away via command topic.""" + assert setup_component(self.hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'manual_mqtt', + 'name': 'test', + 'pending_time': 1, + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + 'payload_arm_away': 'ARM_AWAY', + } + }) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + # Fire the arm command via MQTT; ensure state changes to pending + fire_mqtt_message(self.hass, 'alarm/command', 'ARM_AWAY') + self.hass.block_till_done() + self.assertEqual(STATE_ALARM_PENDING, + self.hass.states.get(entity_id).state) + + # Fast-forward a little bit + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_AWAY, + self.hass.states.get(entity_id).state) + + def test_disarm_pending_via_command_topic(self): + """Test disarming pending alarm via command topic.""" + assert setup_component(self.hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'manual_mqtt', + 'name': 'test', + 'pending_time': 1, + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + 'payload_disarm': 'DISARM', + } + }) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_trigger(self.hass) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_PENDING, + self.hass.states.get(entity_id).state) + + # Now that we're pending, receive a command to disarm + fire_mqtt_message(self.hass, 'alarm/command', 'DISARM') + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + def test_state_changes_are_published_to_mqtt(self): + """Test publishing of MQTT messages when state changes.""" + assert setup_component(self.hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'manual_mqtt', + 'name': 'test', + 'pending_time': 1, + 'trigger_time': 1, + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + } + }) + + # Component should send disarmed alarm state on startup + self.hass.block_till_done() + self.assertEqual(('alarm/state', STATE_ALARM_DISARMED, 0, True), + self.mock_publish.mock_calls[-2][1]) + + # Arm in home mode + alarm_control_panel.alarm_arm_home(self.hass) + self.hass.block_till_done() + self.assertEqual(('alarm/state', STATE_ALARM_PENDING, 0, True), + self.mock_publish.mock_calls[-2][1]) + # Fast-forward a little bit + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + self.assertEqual(('alarm/state', STATE_ALARM_ARMED_HOME, 0, True), + self.mock_publish.mock_calls[-2][1]) + + # Arm in away mode + alarm_control_panel.alarm_arm_away(self.hass) + self.hass.block_till_done() + self.assertEqual(('alarm/state', STATE_ALARM_PENDING, 0, True), + self.mock_publish.mock_calls[-2][1]) + # Fast-forward a little bit + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + self.assertEqual(('alarm/state', STATE_ALARM_ARMED_AWAY, 0, True), + self.mock_publish.mock_calls[-2][1]) + + # Disarm + alarm_control_panel.alarm_disarm(self.hass) + self.hass.block_till_done() + self.assertEqual(('alarm/state', STATE_ALARM_DISARMED, 0, True), + self.mock_publish.mock_calls[-2][1]) diff --git a/tests/components/sensor/test_uk_transport.py b/tests/components/sensor/test_uk_transport.py new file mode 100644 index 00000000000..b051d8e1a1b --- /dev/null +++ b/tests/components/sensor/test_uk_transport.py @@ -0,0 +1,93 @@ +"""The tests for the uk_transport platform.""" +import re + +import requests_mock +import unittest + +from homeassistant.components.sensor.uk_transport import ( + UkTransportSensor, + ATTR_ATCOCODE, ATTR_LOCALITY, ATTR_STOP_NAME, ATTR_NEXT_BUSES, + ATTR_STATION_CODE, ATTR_CALLING_AT, ATTR_NEXT_TRAINS, + CONF_API_APP_KEY, CONF_API_APP_ID) +from homeassistant.setup import setup_component +from tests.common import load_fixture, get_test_home_assistant + +BUS_ATCOCODE = '340000368SHE' +BUS_DIRECTION = 'Wantage' +TRAIN_STATION_CODE = 'WIM' +TRAIN_DESTINATION_NAME = 'WAT' + +VALID_CONFIG = { + 'platform': 'uk_transport', + CONF_API_APP_ID: 'foo', + CONF_API_APP_KEY: 'ebcd1234', + 'queries': [{ + 'mode': 'bus', + 'origin': BUS_ATCOCODE, + 'destination': BUS_DIRECTION}, + { + 'mode': 'train', + 'origin': TRAIN_STATION_CODE, + 'destination': TRAIN_DESTINATION_NAME}] + } + + +class TestUkTransportSensor(unittest.TestCase): + """Test the uk_transport platform.""" + + def setUp(self): + """Initialize values for this testcase class.""" + self.hass = get_test_home_assistant() + self.config = VALID_CONFIG + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + @requests_mock.Mocker() + def test_bus(self, mock_req): + """Test for operational uk_transport sensor with proper attributes.""" + with requests_mock.Mocker() as mock_req: + uri = re.compile(UkTransportSensor.TRANSPORT_API_URL_BASE + '*') + mock_req.get(uri, text=load_fixture('uk_transport_bus.json')) + self.assertTrue( + setup_component(self.hass, 'sensor', {'sensor': self.config})) + + bus_state = self.hass.states.get('sensor.next_bus_to_wantage') + + assert type(bus_state.state) == str + assert bus_state.name == 'Next bus to {}'.format(BUS_DIRECTION) + assert bus_state.attributes.get(ATTR_ATCOCODE) == BUS_ATCOCODE + assert bus_state.attributes.get(ATTR_LOCALITY) == 'Harwell Campus' + assert bus_state.attributes.get(ATTR_STOP_NAME) == 'Bus Station' + assert len(bus_state.attributes.get(ATTR_NEXT_BUSES)) == 2 + + direction_re = re.compile(BUS_DIRECTION) + for bus in bus_state.attributes.get(ATTR_NEXT_BUSES): + print(bus['direction'], direction_re.match(bus['direction'])) + assert direction_re.search(bus['direction']) is not None + + @requests_mock.Mocker() + def test_train(self, mock_req): + """Test for operational uk_transport sensor with proper attributes.""" + with requests_mock.Mocker() as mock_req: + uri = re.compile(UkTransportSensor.TRANSPORT_API_URL_BASE + '*') + mock_req.get(uri, text=load_fixture('uk_transport_train.json')) + self.assertTrue( + setup_component(self.hass, 'sensor', {'sensor': self.config})) + + train_state = self.hass.states.get('sensor.next_train_to_WAT') + + assert type(train_state.state) == str + assert train_state.name == 'Next train to {}'.format( + TRAIN_DESTINATION_NAME) + assert train_state.attributes.get( + ATTR_STATION_CODE) == TRAIN_STATION_CODE + assert train_state.attributes.get( + ATTR_CALLING_AT) == TRAIN_DESTINATION_NAME + assert len(train_state.attributes.get(ATTR_NEXT_TRAINS)) == 25 + + assert train_state.attributes.get( + ATTR_NEXT_TRAINS)[0]['destination_name'] == 'London Waterloo' + assert train_state.attributes.get( + ATTR_NEXT_TRAINS)[0]['estimated'] == '06:13' diff --git a/tests/components/test_shopping_list.py b/tests/components/test_shopping_list.py index 867e695e2d5..c25ce593c54 100644 --- a/tests/components/test_shopping_list.py +++ b/tests/components/test_shopping_list.py @@ -38,7 +38,7 @@ def test_recent_items_intent(hass): ) assert response.speech['plain']['speech'] == \ - "These are the top 5 items in your shopping list: soda, wine, beer" + "These are the top 3 items on your shopping list: soda, wine, beer" @asyncio.coroutine diff --git a/tests/fixtures/uk_transport_bus.json b/tests/fixtures/uk_transport_bus.json new file mode 100644 index 00000000000..5e1e27a4ba3 --- /dev/null +++ b/tests/fixtures/uk_transport_bus.json @@ -0,0 +1,110 @@ +{ + "atcocode": "340000368SHE", + "bearing": "", + "departures": { + "32A": [{ + "aimed_departure_time": "10:18", + "best_departure_estimate": "10:18", + "date": "2017-05-09", + "dir": "outbound", + "direction": "Market Place (Wantage)", + "expected_departure_date": null, + "expected_departure_time": null, + "id": "https://transportapi.com/v3/uk/bus/route/THTR/32A/outbound/340000368SHE/2017-05-09/10:18/timetable.json?app_id=e99&app_key=058", + "line": "32A", + "line_name": "32A", + "mode": "bus", + "operator": "THTR", + "operator_name": "Thames Travel", + "source": "Traveline timetable (not a nextbuses live region)" + }, + { + "aimed_departure_time": "11:00", + "best_departure_estimate": "11:00", + "date": "2017-05-09", + "dir": "outbound", + "direction": "Stratton Way (Abingdon Town Centre)", + "expected_departure_date": null, + "expected_departure_time": null, + "id": "https://transportapi.com/v3/uk/bus/route/THTR/32A/outbound/340000368SHE/2017-05-09/11:00/timetable.json?app_id=e99&app_key=058", + "line": "32A", + "line_name": "32A", + "mode": "bus", + "operator": "THTR", + "operator_name": "Thames Travel", + "source": "Traveline timetable (not a nextbuses live region)" + }, + { + "aimed_departure_time": "11:18", + "best_departure_estimate": "11:18", + "date": "2017-05-09", + "dir": "outbound", + "direction": "Market Place (Wantage)", + "expected_departure_date": null, + "expected_departure_time": null, + "id": "https://transportapi.com/v3/uk/bus/route/THTR/32A/outbound/340000368SHE/2017-05-09/11:18/timetable.json?app_id=e99&app_key=058", + "line": "32A", + "line_name": "32A", + "mode": "bus", + "operator": "THTR", + "operator_name": "Thames Travel", + "source": "Traveline timetable (not a nextbuses live region)" + } + ], + "X32": [{ + "aimed_departure_time": "10:09", + "best_departure_estimate": "10:09", + "date": "2017-05-09", + "dir": "inbound", + "direction": "Parkway Station (Didcot)", + "expected_departure_date": null, + "expected_departure_time": null, + "id": "https://transportapi.com/v3/uk/bus/route/THTR/X32/inbound/340000368SHE/2017-05-09/10:09/timetable.json?app_id=e99&app_key=058", + "line": "X32", + "line_name": "X32", + "mode": "bus", + "operator": "THTR", + "operator_name": "Thames Travel", + "source": "Traveline timetable (not a nextbuses live region)" + }, + { + "aimed_departure_time": "10:30", + "best_departure_estimate": "10:30", + "date": "2017-05-09", + "dir": "inbound", + "direction": "Parks Road (Oxford City Centre)", + "expected_departure_date": null, + "expected_departure_time": null, + "id": "https://transportapi.com/v3/uk/bus/route/THTR/X32/inbound/340000368SHE/2017-05-09/10:30/timetable.json?app_id=e99&app_key=058", + "line": "X32", + "line_name": "X32", + "mode": "bus", + "operator": "THTR", + "operator_name": "Thames Travel", + "source": "Traveline timetable (not a nextbuses live region)" + }, + { + "aimed_departure_time": "10:39", + "best_departure_estimate": "10:39", + "date": "2017-05-09", + "dir": "inbound", + "direction": "Parkway Station (Didcot)", + "expected_departure_date": null, + "expected_departure_time": null, + "id": "https://transportapi.com/v3/uk/bus/route/THTR/X32/inbound/340000368SHE/2017-05-09/10:39/timetable.json?app_id=e99&app_key=058", + "line": "X32", + "line_name": "X32", + "mode": "bus", + "operator": "THTR", + "operator_name": "Thames Travel", + "source": "Traveline timetable (not a nextbuses live region)" + } + ] + }, + "indicator": "in", + "locality": "Harwell Campus", + "name": "Bus Station (in)", + "request_time": "2017-05-09T10:03:41+01:00", + "smscode": "oxfajwgp", + "stop_name": "Bus Station" +} diff --git a/tests/fixtures/uk_transport_train.json b/tests/fixtures/uk_transport_train.json new file mode 100644 index 00000000000..b06e8db6ca7 --- /dev/null +++ b/tests/fixtures/uk_transport_train.json @@ -0,0 +1,511 @@ +{ + "date": "2017-07-10", + "time_of_day": "06:10", + "request_time": "2017-07-10T06:10:05+01:00", + "station_name": "Wimbledon", + "station_code": "WIM", + "departures": { + "all": [ + { + "mode": "train", + "service": "24671405", + "train_uid": "W36814", + "platform": "8", + "operator": "SW", + "operator_name": "South West Trains", + "aimed_departure_time": "06:13", + "aimed_arrival_time": null, + "aimed_pass_time": null, + "origin_name": "Wimbledon", + "source": "Network Rail", + "destination_name": "London Waterloo", + "category": "OO", + "status": "STARTS HERE", + "expected_arrival_time": null, + "expected_departure_time": "06:13", + "best_arrival_estimate_mins": null, + "best_departure_estimate_mins": 2 + }, + { + "mode": "train", + "service": "24673205", + "train_uid": "W36613", + "platform": "5", + "operator": "SW", + "operator_name": "South West Trains", + "aimed_departure_time": "06:14", + "aimed_arrival_time": "06:13", + "aimed_pass_time": null, + "origin_name": "Hampton Court", + "source": "Network Rail", + "destination_name": "London Waterloo", + "category": "OO", + "status": "EARLY", + "expected_arrival_time": "06:13", + "expected_departure_time": "06:14", + "best_arrival_estimate_mins": 2, + "best_departure_estimate_mins": 3 + }, + { + "mode": "train", + "service": "24673505", + "train_uid": "W36012", + "platform": "5", + "operator": "SW", + "operator_name": "South West Trains", + "aimed_departure_time": "06:20", + "aimed_arrival_time": "06:20", + "aimed_pass_time": null, + "origin_name": "Guildford", + "source": "Network Rail", + "destination_name": "London Waterloo", + "category": "OO", + "status": "ON TIME", + "expected_arrival_time": "06:20", + "expected_departure_time": "06:20", + "best_arrival_estimate_mins": 9, + "best_departure_estimate_mins": 9 + }, + { + "mode": "train", + "service": "24673305", + "train_uid": "W34087", + "platform": "5", + "operator": "SW", + "operator_name": "South West Trains", + "aimed_departure_time": "06:23", + "aimed_arrival_time": "06:23", + "aimed_pass_time": null, + "origin_name": "Dorking", + "source": "Network Rail", + "destination_name": "London Waterloo", + "category": "XX", + "status": "ON TIME", + "expected_arrival_time": "06:23", + "expected_departure_time": "06:23", + "best_arrival_estimate_mins": 12, + "best_departure_estimate_mins": 12 + }, + { + "mode": "train", + "service": "24671505", + "train_uid": "W37471", + "platform": "5", + "operator": "SW", + "operator_name": "South West Trains", + "aimed_departure_time": "06:32", + "aimed_arrival_time": "06:31", + "aimed_pass_time": null, + "origin_name": "London Waterloo", + "source": "Network Rail", + "destination_name": "London Waterloo", + "category": "OO", + "status": "ON TIME", + "expected_arrival_time": "06:31", + "expected_departure_time": "06:32", + "best_arrival_estimate_mins": 20, + "best_departure_estimate_mins": 21 + }, + { + "mode": "train", + "service": "24673605", + "train_uid": "W35790", + "platform": "5", + "operator": "SW", + "operator_name": "South West Trains", + "aimed_departure_time": "06:35", + "aimed_arrival_time": "06:35", + "aimed_pass_time": null, + "origin_name": "Woking", + "source": "Network Rail", + "destination_name": "London Waterloo", + "category": "OO", + "status": "ON TIME", + "expected_arrival_time": "06:35", + "expected_departure_time": "06:35", + "best_arrival_estimate_mins": 24, + "best_departure_estimate_mins": 24 + }, + { + "mode": "train", + "service": "24673705", + "train_uid": "W35665", + "platform": "5", + "operator": "SW", + "operator_name": "South West Trains", + "aimed_departure_time": "06:38", + "aimed_arrival_time": "06:38", + "aimed_pass_time": null, + "origin_name": "Epsom", + "source": "Network Rail", + "destination_name": "London Waterloo", + "category": "OO", + "status": "ON TIME", + "expected_arrival_time": "06:38", + "expected_departure_time": "06:38", + "best_arrival_estimate_mins": 27, + "best_departure_estimate_mins": 27 + }, + { + "mode": "train", + "service": "24671405", + "train_uid": "W36816", + "platform": "8", + "operator": "SW", + "operator_name": "South West Trains", + "aimed_departure_time": "06:43", + "aimed_arrival_time": "06:43", + "aimed_pass_time": null, + "origin_name": "London Waterloo", + "source": "Network Rail", + "destination_name": "London Waterloo", + "category": "OO", + "status": "ON TIME", + "expected_arrival_time": "06:43", + "expected_departure_time": "06:43", + "best_arrival_estimate_mins": 32, + "best_departure_estimate_mins": 32 + }, + { + "mode": "train", + "service": "24673205", + "train_uid": "W36618", + "platform": "5", + "operator": "SW", + "operator_name": "South West Trains", + "aimed_departure_time": "06:44", + "aimed_arrival_time": "06:43", + "aimed_pass_time": null, + "origin_name": "Hampton Court", + "source": "Network Rail", + "destination_name": "London Waterloo", + "category": "OO", + "status": "ON TIME", + "expected_arrival_time": "06:43", + "expected_departure_time": "06:44", + "best_arrival_estimate_mins": 32, + "best_departure_estimate_mins": 33 + }, + { + "mode": "train", + "service": "24673105", + "train_uid": "W36429", + "platform": "5", + "operator": "SW", + "operator_name": "South West Trains", + "aimed_departure_time": "06:47", + "aimed_arrival_time": "06:46", + "aimed_pass_time": null, + "origin_name": "Shepperton", + "source": "Network Rail", + "destination_name": "London Waterloo", + "category": "OO", + "status": "ON TIME", + "expected_arrival_time": "06:46", + "expected_departure_time": "06:47", + "best_arrival_estimate_mins": 35, + "best_departure_estimate_mins": 36 + }, + { + "mode": "train", + "service": "24629204", + "train_uid": "W36916", + "platform": "6", + "operator": "SW", + "operator_name": "South West Trains", + "aimed_departure_time": "06:47", + "aimed_arrival_time": "06:47", + "aimed_pass_time": null, + "origin_name": "Basingstoke", + "source": "Network Rail", + "destination_name": "London Waterloo", + "category": "OO", + "status": "LATE", + "expected_arrival_time": "06:48", + "expected_departure_time": "06:48", + "best_arrival_estimate_mins": 37, + "best_departure_estimate_mins": 37 + }, + { + "mode": "train", + "service": "24673505", + "train_uid": "W36016", + "platform": "5", + "operator": "SW", + "operator_name": "South West Trains", + "aimed_departure_time": "06:50", + "aimed_arrival_time": "06:49", + "aimed_pass_time": null, + "origin_name": "Guildford", + "source": "Network Rail", + "destination_name": "London Waterloo", + "category": "OO", + "status": "ON TIME", + "expected_arrival_time": "06:49", + "expected_departure_time": "06:50", + "best_arrival_estimate_mins": 38, + "best_departure_estimate_mins": 39 + }, + { + "mode": "train", + "service": "24673705", + "train_uid": "W35489", + "platform": "5", + "operator": "SW", + "operator_name": "South West Trains", + "aimed_departure_time": "06:53", + "aimed_arrival_time": "06:52", + "aimed_pass_time": null, + "origin_name": "Guildford", + "source": "Network Rail", + "destination_name": "London Waterloo", + "category": "OO", + "status": "EARLY", + "expected_arrival_time": "06:52", + "expected_departure_time": "06:53", + "best_arrival_estimate_mins": 41, + "best_departure_estimate_mins": 42 + }, + { + "mode": "train", + "service": "24673405", + "train_uid": "W37107", + "platform": "5", + "operator": "SW", + "operator_name": "South West Trains", + "aimed_departure_time": "06:58", + "aimed_arrival_time": "06:57", + "aimed_pass_time": null, + "origin_name": "Chessington South", + "source": "Network Rail", + "destination_name": "London Waterloo", + "category": "OO", + "status": "ON TIME", + "expected_arrival_time": "06:57", + "expected_departure_time": "06:58", + "best_arrival_estimate_mins": 46, + "best_departure_estimate_mins": 47 + }, + { + "mode": "train", + "service": "24671505", + "train_uid": "W37473", + "platform": "5", + "operator": "SW", + "operator_name": "South West Trains", + "aimed_departure_time": "07:02", + "aimed_arrival_time": "07:01", + "aimed_pass_time": null, + "origin_name": "London Waterloo", + "source": "Network Rail", + "destination_name": "London Waterloo", + "category": "OO", + "status": "EARLY", + "expected_arrival_time": "07:01", + "expected_departure_time": "07:02", + "best_arrival_estimate_mins": 50, + "best_departure_estimate_mins": 51 + }, + { + "mode": "train", + "service": "24673605", + "train_uid": "W35795", + "platform": "5", + "operator": "SW", + "operator_name": "South West Trains", + "aimed_departure_time": "07:05", + "aimed_arrival_time": "07:04", + "aimed_pass_time": null, + "origin_name": "Woking", + "source": "Network Rail", + "destination_name": "London Waterloo", + "category": "OO", + "status": "ON TIME", + "expected_arrival_time": "07:04", + "expected_departure_time": "07:05", + "best_arrival_estimate_mins": 53, + "best_departure_estimate_mins": 54 + }, + { + "mode": "train", + "service": "24673305", + "train_uid": "W34090", + "platform": "5", + "operator": "SW", + "operator_name": "South West Trains", + "aimed_departure_time": "07:08", + "aimed_arrival_time": "07:07", + "aimed_pass_time": null, + "origin_name": "Dorking", + "source": "Network Rail", + "destination_name": "London Waterloo", + "category": "XX", + "status": "ON TIME", + "expected_arrival_time": "07:07", + "expected_departure_time": "07:08", + "best_arrival_estimate_mins": 56, + "best_departure_estimate_mins": 57 + }, + { + "mode": "train", + "service": "24673205", + "train_uid": "W36623", + "platform": "5", + "operator": "SW", + "operator_name": "South West Trains", + "aimed_departure_time": "07:13", + "aimed_arrival_time": "07:12", + "aimed_pass_time": null, + "origin_name": "Hampton Court", + "source": "Network Rail", + "destination_name": "London Waterloo", + "category": "OO", + "status": "ON TIME", + "expected_arrival_time": "07:12", + "expected_departure_time": "07:13", + "best_arrival_estimate_mins": 61, + "best_departure_estimate_mins": 62 + }, + { + "mode": "train", + "service": "24671405", + "train_uid": "W36819", + "platform": "8", + "operator": "SW", + "operator_name": "South West Trains", + "aimed_departure_time": "07:13", + "aimed_arrival_time": "07:13", + "aimed_pass_time": null, + "origin_name": "London Waterloo", + "source": "Network Rail", + "destination_name": "London Waterloo", + "category": "OO", + "status": "ON TIME", + "expected_arrival_time": "07:13", + "expected_departure_time": "07:13", + "best_arrival_estimate_mins": 62, + "best_departure_estimate_mins": 62 + }, + { + "mode": "train", + "service": "24673105", + "train_uid": "W36434", + "platform": "5", + "operator": "SW", + "operator_name": "South West Trains", + "aimed_departure_time": "07:16", + "aimed_arrival_time": "07:15", + "aimed_pass_time": null, + "origin_name": "Shepperton", + "source": "Network Rail", + "destination_name": "London Waterloo", + "category": "OO", + "status": "ON TIME", + "expected_arrival_time": "07:15", + "expected_departure_time": "07:16", + "best_arrival_estimate_mins": 64, + "best_departure_estimate_mins": 65 + }, + { + "mode": "train", + "service": "24673505", + "train_uid": "W36019", + "platform": "5", + "operator": "SW", + "operator_name": "South West Trains", + "aimed_departure_time": "07:19", + "aimed_arrival_time": "07:18", + "aimed_pass_time": null, + "origin_name": "Guildford", + "source": "Network Rail", + "destination_name": "London Waterloo", + "category": "OO", + "status": "ON TIME", + "expected_arrival_time": "07:18", + "expected_departure_time": "07:19", + "best_arrival_estimate_mins": 67, + "best_departure_estimate_mins": 68 + }, + { + "mode": "train", + "service": "24673705", + "train_uid": "W35494", + "platform": "5", + "operator": "SW", + "operator_name": "South West Trains", + "aimed_departure_time": "07:22", + "aimed_arrival_time": "07:21", + "aimed_pass_time": null, + "origin_name": "Guildford", + "source": "Network Rail", + "destination_name": "London Waterloo", + "category": "OO", + "status": "ON TIME", + "expected_arrival_time": "07:21", + "expected_departure_time": "07:22", + "best_arrival_estimate_mins": 70, + "best_departure_estimate_mins": 71 + }, + { + "mode": "train", + "service": "24673205", + "train_uid": "W36810", + "platform": "5", + "operator": "SW", + "operator_name": "South West Trains", + "aimed_departure_time": "07:25", + "aimed_arrival_time": "07:24", + "aimed_pass_time": null, + "origin_name": "Esher", + "source": "Network Rail", + "destination_name": "London Waterloo", + "category": "OO", + "status": "ON TIME", + "expected_arrival_time": "07:24", + "expected_departure_time": "07:25", + "best_arrival_estimate_mins": 73, + "best_departure_estimate_mins": 74 + }, + { + "mode": "train", + "service": "24673405", + "train_uid": "W37112", + "platform": "5", + "operator": "SW", + "operator_name": "South West Trains", + "aimed_departure_time": "07:28", + "aimed_arrival_time": "07:27", + "aimed_pass_time": null, + "origin_name": "Chessington South", + "source": "Network Rail", + "destination_name": "London Waterloo", + "category": "OO", + "status": "ON TIME", + "expected_arrival_time": "07:27", + "expected_departure_time": "07:28", + "best_arrival_estimate_mins": 76, + "best_departure_estimate_mins": 77 + }, + { + "mode": "train", + "service": "24671505", + "train_uid": "W37476", + "platform": "5", + "operator": "SW", + "operator_name": "South West Trains", + "aimed_departure_time": "07:32", + "aimed_arrival_time": "07:31", + "aimed_pass_time": null, + "origin_name": "London Waterloo", + "source": "Network Rail", + "destination_name": "London Waterloo", + "category": "OO", + "status": "ON TIME", + "expected_arrival_time": "07:31", + "expected_departure_time": "07:32", + "best_arrival_estimate_mins": 80, + "best_departure_estimate_mins": 81 + } + ] + } +} diff --git a/tests/util/test_yaml.py b/tests/util/test_yaml.py index 0ccb6f5d6d0..a15efb7a77e 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/test_yaml.py @@ -2,6 +2,7 @@ import io import os import unittest +import logging from unittest.mock import patch from homeassistant.exceptions import HomeAssistantError @@ -59,6 +60,13 @@ class TestYaml(unittest.TestCase): assert doc['password'] == "secret_password" del os.environ["PASSWORD"] + def test_environment_variable_default(self): + """Test config file with default value for environment variable.""" + conf = "password: !env_var PASSWORD secret_password" + with io.StringIO(conf) as file: + doc = yaml.yaml.safe_load(file) + assert doc['password'] == "secret_password" + def test_invalid_enviroment_variable(self): """Test config file with no enviroment variable sat.""" conf = "password: !env_var PASSWORD" @@ -372,6 +380,16 @@ class TestSecrets(unittest.TestCase): _yaml = load_yaml(self._yaml_path, yaml_str) self.assertEqual({'http': {'api_password': 'yeah'}}, _yaml) + @patch.object(yaml, 'credstash') + def test_secrets_credstash(self, mock_credstash): + """Test credstash fallback & get_password.""" + mock_credstash.getSecret.return_value = 'yeah' + yaml_str = 'http:\n api_password: !secret http_pw_credstash' + _yaml = load_yaml(self._yaml_path, yaml_str) + log = logging.getLogger() + log.error(_yaml['http']) + self.assertEqual({'api_password': 'yeah'}, _yaml['http']) + def test_secrets_logger_removed(self): """Ensure logger: debug was removed.""" with self.assertRaises(yaml.HomeAssistantError):