diff --git a/.coveragerc b/.coveragerc index 62de8bcc4e1..8867a0837aa 100644 --- a/.coveragerc +++ b/.coveragerc @@ -91,6 +91,7 @@ omit = homeassistant/components/knx.py homeassistant/components/switch/knx.py homeassistant/components/binary_sensor/knx.py + homeassistant/components/thermostat/knx.py homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/nx584.py @@ -129,21 +130,25 @@ omit = homeassistant/components/joaoapps_join.py homeassistant/components/keyboard.py homeassistant/components/light/blinksticklight.py + homeassistant/components/light/flux_led.py homeassistant/components/light/hue.py homeassistant/components/light/hyperion.py homeassistant/components/light/lifx.py homeassistant/components/light/limitlessled.py homeassistant/components/light/osramlightify.py + homeassistant/components/light/x10.py homeassistant/components/lirc.py homeassistant/components/media_player/braviatv.py homeassistant/components/media_player/cast.py homeassistant/components/media_player/cmus.py homeassistant/components/media_player/denon.py + homeassistant/components/media_player/directv.py homeassistant/components/media_player/firetv.py homeassistant/components/media_player/gpmdp.py homeassistant/components/media_player/itunes.py homeassistant/components/media_player/kodi.py homeassistant/components/media_player/lg_netcast.py + homeassistant/components/media_player/mpchc.py homeassistant/components/media_player/mpd.py homeassistant/components/media_player/onkyo.py homeassistant/components/media_player/panasonic_viera.py @@ -151,6 +156,7 @@ omit = homeassistant/components/media_player/pioneer.py homeassistant/components/media_player/plex.py homeassistant/components/media_player/roku.py + homeassistant/components/media_player/russound_rnet.py homeassistant/components/media_player/samsungtv.py homeassistant/components/media_player/snapcast.py homeassistant/components/media_player/sonos.py @@ -161,7 +167,6 @@ omit = homeassistant/components/notify/aws_sqs.py homeassistant/components/notify/free_mobile.py homeassistant/components/notify/gntp.py - homeassistant/components/notify/googlevoice.py homeassistant/components/notify/instapush.py homeassistant/components/notify/joaoapps_join.py homeassistant/components/notify/message_bird.py diff --git a/.gitignore b/.gitignore index 12f35dc5c3d..64ab38f2da8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,10 @@ config/custom_components/* !config/custom_components/example.py !config/custom_components/hello_world.py !config/custom_components/mqtt_example.py +!config/custom_components/react_panel -tests/config/deps -tests/config/home-assistant.log +tests/testing_config/deps +tests/testing_config/home-assistant.log # Hide sublime text stuff *.sublime-project diff --git a/.travis.yml b/.travis.yml index 3e1c8869d8f..3d575c1d778 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,8 +8,13 @@ matrix: env: TOXENV=requirements - python: "3.5" env: TOXENV=lint + - python: "3.5" + env: TOXENV=typing - python: "3.5" env: TOXENV=py35 + allow_failures: + - python: "3.5" + env: TOXENV=typing cache: directories: - $HOME/.cache/pip diff --git a/Dockerfile b/Dockerfile index d69b44cb9ff..22c0c13ddf6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,8 @@ RUN script/build_python_openzwave && \ COPY requirements_all.txt requirements_all.txt # certifi breaks Debian based installs -RUN pip3 install --no-cache-dir -r requirements_all.txt && pip3 uninstall -y certifi +RUN pip3 install --no-cache-dir -r requirements_all.txt && pip3 uninstall -y certifi && \ + pip3 install mysqlclient psycopg2 # Copy source COPY . . diff --git a/README.rst b/README.rst index 94255ff1d39..43517760ed7 100644 --- a/README.rst +++ b/README.rst @@ -67,7 +67,7 @@ Build home automation on top of your devices: - Turn on the lights when people get home after sunset - Turn on lights slowly during sunset to compensate for less light - Turn off all lights and devices when everybody leaves the house -- Offers a `REST API `__ +- Offers a `REST API `__ and can interface with MQTT for easy integration with other projects like `OwnTracks `__ - Allow sending notifications using diff --git a/config/custom_components/react_panel/__init__.py b/config/custom_components/react_panel/__init__.py new file mode 100644 index 00000000000..57073b8cddc --- /dev/null +++ b/config/custom_components/react_panel/__init__.py @@ -0,0 +1,30 @@ +""" +Custom panel example showing TodoMVC using React. + +Will add a panel to control lights and switches using React. Allows configuring +the title via configuration.yaml: + +react_panel: + title: 'home' + +""" +import os + +from homeassistant.components.frontend import register_panel + +DOMAIN = 'react_panel' +DEPENDENCIES = ['frontend'] + +PANEL_PATH = os.path.join(os.path.dirname(__file__), 'panel.html') + + +def setup(hass, config): + """Initialize custom panel.""" + title = config.get(DOMAIN, {}).get('title') + + config = None if title is None else {'title': title} + + register_panel(hass, 'react', PANEL_PATH, + title='TodoMVC', icon='mdi:checkbox-marked-outline', + config=config) + return True diff --git a/config/custom_components/react_panel/panel.html b/config/custom_components/react_panel/panel.html new file mode 100644 index 00000000000..eceee0f0616 --- /dev/null +++ b/config/custom_components/react_panel/panel.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index d92c434d22b..fb1594d5b3f 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -8,6 +8,8 @@ import subprocess import sys import threading +from typing import Optional, List + from homeassistant.const import ( __version__, EVENT_HOMEASSISTANT_START, @@ -16,7 +18,7 @@ from homeassistant.const import ( ) -def validate_python(): +def validate_python() -> None: """Validate we're running the right Python version.""" major, minor = sys.version_info[:2] req_major, req_minor = REQUIRED_PYTHON_VER @@ -27,7 +29,7 @@ def validate_python(): sys.exit(1) -def ensure_config_path(config_dir): +def ensure_config_path(config_dir: str) -> None: """Validate the configuration directory.""" import homeassistant.config as config_util lib_dir = os.path.join(config_dir, 'deps') @@ -56,7 +58,7 @@ def ensure_config_path(config_dir): sys.exit(1) -def ensure_config_file(config_dir): +def ensure_config_file(config_dir: str) -> str: """Ensure configuration file exists.""" import homeassistant.config as config_util config_path = config_util.ensure_config_exists(config_dir) @@ -68,7 +70,7 @@ def ensure_config_file(config_dir): return config_path -def get_arguments(): +def get_arguments() -> argparse.Namespace: """Get parsed passed in arguments.""" import homeassistant.config as config_util parser = argparse.ArgumentParser( @@ -125,12 +127,12 @@ def get_arguments(): arguments = parser.parse_args() if os.name != "posix" or arguments.debug or arguments.runner: - arguments.daemon = False + setattr(arguments, 'daemon', False) return arguments -def daemonize(): +def daemonize() -> None: """Move current process to daemon process.""" # Create first fork pid = os.fork() @@ -155,7 +157,7 @@ def daemonize(): os.dup2(outfd.fileno(), sys.stderr.fileno()) -def check_pid(pid_file): +def check_pid(pid_file: str) -> None: """Check that HA is not already running.""" # Check pid file try: @@ -177,7 +179,7 @@ def check_pid(pid_file): sys.exit(1) -def write_pid(pid_file): +def write_pid(pid_file: str) -> None: """Create a PID File.""" pid = os.getpid() try: @@ -187,7 +189,7 @@ def write_pid(pid_file): sys.exit(1) -def closefds_osx(min_fd, max_fd): +def closefds_osx(min_fd: int, max_fd: int) -> None: """Make sure file descriptors get closed when we restart. We cannot call close on guarded fds, and we cannot easily test which fds @@ -205,7 +207,7 @@ def closefds_osx(min_fd, max_fd): pass -def cmdline(): +def cmdline() -> List[str]: """Collect path and arguments to re-execute the current hass instance.""" if sys.argv[0].endswith('/__main__.py'): modulepath = os.path.dirname(sys.argv[0]) @@ -213,16 +215,17 @@ def cmdline(): return [sys.executable] + [arg for arg in sys.argv if arg != '--daemon'] -def setup_and_run_hass(config_dir, args): +def setup_and_run_hass(config_dir: str, + args: argparse.Namespace) -> Optional[int]: """Setup HASS and run.""" from homeassistant import bootstrap # Run a simple daemon runner process on Windows to handle restarts if os.name == 'nt' and '--runner' not in sys.argv: - args = cmdline() + ['--runner'] + nt_args = cmdline() + ['--runner'] while True: try: - subprocess.check_call(args) + subprocess.check_call(nt_args) sys.exit(0) except subprocess.CalledProcessError as exc: if exc.returncode != RESTART_EXIT_CODE: @@ -244,7 +247,7 @@ def setup_and_run_hass(config_dir, args): log_rotate_days=args.log_rotate_days) if hass is None: - return + return None if args.open_ui: def open_browser(event): @@ -261,7 +264,7 @@ def setup_and_run_hass(config_dir, args): return exit_code -def try_to_restart(): +def try_to_restart() -> None: """Attempt to clean up state and start a new homeassistant instance.""" # Things should be mostly shut down already at this point, now just try # to clean up things that may have been left behind. @@ -303,7 +306,7 @@ def try_to_restart(): os.execv(args[0], args) -def main(): +def main() -> int: """Start Home Assistant.""" validate_python() diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 8b3d3ee6f23..c62fe9e7d6b 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -7,6 +7,9 @@ import sys from collections import defaultdict from threading import RLock +from types import ModuleType +from typing import Any, Optional, Dict + import voluptuous as vol import homeassistant.components as core_components @@ -30,7 +33,8 @@ ATTR_COMPONENT = 'component' ERROR_LOG_FILENAME = 'home-assistant.log' -def setup_component(hass, domain, config=None): +def setup_component(hass: core.HomeAssistant, domain: str, + config: Optional[Dict]=None) -> bool: """Setup a component and all its dependencies.""" if domain in hass.config.components: return True @@ -53,7 +57,8 @@ def setup_component(hass, domain, config=None): return True -def _handle_requirements(hass, component, name): +def _handle_requirements(hass: core.HomeAssistant, component, + name: str) -> bool: """Install the requirements for a component.""" if hass.config.skip_pip or not hasattr(component, 'REQUIREMENTS'): return True @@ -67,9 +72,10 @@ def _handle_requirements(hass, component, name): return True -def _setup_component(hass, domain, config): +def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool: """Setup a component for Home Assistant.""" # pylint: disable=too-many-return-statements,too-many-branches + # pylint: disable=too-many-statements if domain in hass.config.components: return True @@ -147,9 +153,15 @@ def _setup_component(hass, domain, config): _CURRENT_SETUP.append(domain) try: - if not component.setup(hass, config): + result = component.setup(hass, config) + if result is False: _LOGGER.error('component %s failed to initialize', domain) return False + elif result is not True: + _LOGGER.error('component %s did not return boolean if setup ' + 'was successful. Disabling component.', domain) + loader.set_component(domain, None) + return False except Exception: # pylint: disable=broad-except _LOGGER.exception('Error during setup of component %s', domain) return False @@ -169,7 +181,8 @@ def _setup_component(hass, domain, config): return True -def prepare_setup_platform(hass, config, domain, platform_name): +def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str, + platform_name: str) -> Optional[ModuleType]: """Load a platform and makes sure dependencies are setup.""" _ensure_loader_prepared(hass) @@ -202,9 +215,14 @@ def prepare_setup_platform(hass, config, domain, platform_name): # pylint: disable=too-many-branches, too-many-statements, too-many-arguments -def from_config_dict(config, hass=None, config_dir=None, enable_log=True, - verbose=False, skip_pip=False, - log_rotate_days=None): +def from_config_dict(config: Dict[str, Any], + hass: Optional[core.HomeAssistant]=None, + config_dir: Optional[str]=None, + enable_log: bool=True, + verbose: bool=False, + skip_pip: bool=False, + log_rotate_days: Any=None) \ + -> Optional[core.HomeAssistant]: """Try to configure Home Assistant from a config dict. Dynamically loads required components and its dependencies. @@ -266,8 +284,11 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True, return hass -def from_config_file(config_path, hass=None, verbose=False, skip_pip=True, - log_rotate_days=None): +def from_config_file(config_path: str, + hass: Optional[core.HomeAssistant]=None, + verbose: bool=False, + skip_pip: bool=True, + log_rotate_days: Any=None): """Read the configuration file and try to start all the functionality. Will add functionality to 'hass' parameter if given, @@ -292,7 +313,8 @@ def from_config_file(config_path, hass=None, verbose=False, skip_pip=True, skip_pip=skip_pip) -def enable_logging(hass, verbose=False, log_rotate_days=None): +def enable_logging(hass: core.HomeAssistant, verbose: bool=False, + log_rotate_days=None) -> None: """Setup the logging.""" logging.basicConfig(level=logging.INFO) fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) " @@ -343,12 +365,12 @@ def enable_logging(hass, verbose=False, log_rotate_days=None): 'Unable to setup error log %s (access denied)', err_log_path) -def _ensure_loader_prepared(hass): +def _ensure_loader_prepared(hass: core.HomeAssistant) -> None: """Ensure Home Assistant loader is prepared.""" if not loader.PREPARED: loader.prepare(hass) -def _mount_local_lib_path(config_dir): +def _mount_local_lib_path(config_dir: str) -> None: """Add local library to Python Path.""" sys.path.insert(0, os.path.join(config_dir, 'deps')) diff --git a/homeassistant/components/binary_sensor/vera.py b/homeassistant/components/binary_sensor/vera.py index 27d604805dc..8673c0e4696 100644 --- a/homeassistant/components/binary_sensor/vera.py +++ b/homeassistant/components/binary_sensor/vera.py @@ -6,9 +6,6 @@ https://home-assistant.io/components/binary_sensor.vera/ """ import logging -import homeassistant.util.dt as dt_util -from homeassistant.const import ( - ATTR_ARMED, ATTR_BATTERY_LEVEL, ATTR_LAST_TRIP_TIME, ATTR_TRIPPED) from homeassistant.components.binary_sensor import ( BinarySensorDevice) from homeassistant.components.vera import ( @@ -34,30 +31,6 @@ class VeraBinarySensor(VeraDevice, BinarySensorDevice): self._state = False VeraDevice.__init__(self, vera_device, controller) - @property - def device_state_attributes(self): - """Return the state attributes.""" - attr = {} - if self.vera_device.has_battery: - attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%' - - if self.vera_device.is_armable: - armed = self.vera_device.is_armed - attr[ATTR_ARMED] = 'True' if armed else 'False' - - if self.vera_device.is_trippable: - last_tripped = self.vera_device.last_trip - if last_tripped is not None: - utc_time = dt_util.utc_from_timestamp(int(last_tripped)) - attr[ATTR_LAST_TRIP_TIME] = utc_time.isoformat() - else: - attr[ATTR_LAST_TRIP_TIME] = None - tripped = self.vera_device.is_tripped - attr[ATTR_TRIPPED] = 'True' if tripped else 'False' - - attr['Vera Device Id'] = self.vera_device.vera_device_id - return attr - @property def is_on(self): """Return true if sensor is on.""" diff --git a/homeassistant/components/binary_sensor/wink.py b/homeassistant/components/binary_sensor/wink.py index dc1b7a005c0..0ab8d812819 100644 --- a/homeassistant/components/binary_sensor/wink.py +++ b/homeassistant/components/binary_sensor/wink.py @@ -13,14 +13,15 @@ from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.helpers.entity import Entity from homeassistant.loader import get_component -REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2'] +REQUIREMENTS = ['python-wink==0.7.11', 'pubnub==3.8.2'] # These are the available sensors mapped to binary_sensor class SENSOR_TYPES = { "opened": "opening", "brightness": "light", "vibration": "vibration", - "loudness": "sound" + "loudness": "sound", + "liquid_detected": "moisture" } @@ -74,6 +75,8 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity): return self.wink.vibration_boolean() elif self.capability == "brightness": return self.wink.brightness_boolean() + elif self.capability == "liquid_detected": + return self.wink.liquid_boolean() else: return self.wink.state() diff --git a/homeassistant/components/binary_sensor/zwave.py b/homeassistant/components/binary_sensor/zwave.py index 0f2a13263aa..1b47b98fe9f 100644 --- a/homeassistant/components/binary_sensor/zwave.py +++ b/homeassistant/components/binary_sensor/zwave.py @@ -94,7 +94,8 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity, Entity): def value_changed(self, value): """Called when a value has changed on the network.""" - if self._value.value_id == value.value_id: + if self._value.value_id == value.value_id or \ + self._value.node == value.node: self.update_ha_state() diff --git a/homeassistant/components/browser.py b/homeassistant/components/browser.py index fc78b83bd60..041a0f9cdc6 100644 --- a/homeassistant/components/browser.py +++ b/homeassistant/components/browser.py @@ -13,7 +13,8 @@ ATTR_URL = 'url' ATTR_URL_DEFAULT = 'https://www.google.com' SERVICE_BROWSE_URL_SCHEMA = vol.Schema({ - vol.Required(ATTR_URL, default=ATTR_URL_DEFAULT): vol.Url, + # pylint: disable=no-value-for-parameter + vol.Required(ATTR_URL, default=ATTR_URL_DEFAULT): vol.Url(), }) diff --git a/homeassistant/components/device_tracker/icloud.py b/homeassistant/components/device_tracker/icloud.py index 1ff7fa9ae27..7c585244400 100644 --- a/homeassistant/components/device_tracker/icloud.py +++ b/homeassistant/components/device_tracker/icloud.py @@ -5,17 +5,28 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.icloud/ """ import logging -import re +import voluptuous as vol -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME, + EVENT_HOMEASSISTANT_START) from homeassistant.helpers.event import track_utc_time_change +from homeassistant.util import slugify +from homeassistant.components.device_tracker import (ENTITY_ID_FORMAT, + PLATFORM_SCHEMA) _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pyicloud==0.8.3'] +REQUIREMENTS = ['pyicloud==0.9.1'] CONF_INTERVAL = 'interval' -DEFAULT_INTERVAL = 8 +KEEPALIVE_INTERVAL = 4 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_USERNAME): vol.Coerce(str), + vol.Required(CONF_PASSWORD): vol.Coerce(str), + vol.Optional(CONF_INTERVAL, default=8): vol.All(vol.Coerce(int), + vol.Range(min=1)) + }) def setup_scanner(hass, config, see): @@ -23,63 +34,67 @@ def setup_scanner(hass, config, see): from pyicloud import PyiCloudService from pyicloud.exceptions import PyiCloudFailedLoginException from pyicloud.exceptions import PyiCloudNoDevicesException + logging.getLogger("pyicloud.base").setLevel(logging.WARNING) - # Get the username and password from the configuration. - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - - if username is None or password is None: - _LOGGER.error('Must specify a username and password') - return False + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] try: _LOGGER.info('Logging into iCloud Account') # Attempt the login to iCloud - api = PyiCloudService(username, - password, - verify=True) + api = PyiCloudService(username, password, verify=True) except PyiCloudFailedLoginException as error: _LOGGER.exception('Error logging into iCloud Service: %s', error) return False def keep_alive(now): - """Keep authenticating iCloud connection.""" + """Keep authenticating iCloud connection. + + The session timeouts if we are not using it so we + have to re-authenticate & this will send an email. + """ api.authenticate() _LOGGER.info("Authenticate against iCloud") - track_utc_time_change(hass, keep_alive, second=0) + seen_devices = {} def update_icloud(now): """Authenticate against iCloud and scan for devices.""" try: - # The session timeouts if we are not using it so we - # have to re-authenticate. This will send an email. - api.authenticate() + keep_alive(None) # Loop through every device registered with the iCloud account for device in api.devices: status = device.status() + dev_id = slugify(status['name'].replace(' ', '', 99)) + + # An entity will not be created by see() when track=false in + # 'known_devices.yaml', but we need to see() it at least once + entity = hass.states.get(ENTITY_ID_FORMAT.format(dev_id)) + if entity is None and dev_id in seen_devices: + continue + seen_devices[dev_id] = True + location = device.location() # If the device has a location add it. If not do nothing if location: see( - dev_id=re.sub(r"(\s|\W|')", - '', - status['name']), + dev_id=dev_id, host_name=status['name'], gps=(location['latitude'], location['longitude']), battery=status['batteryLevel']*100, gps_accuracy=location['horizontalAccuracy'] ) - else: - # No location found for the device so continue - continue except PyiCloudNoDevicesException: _LOGGER.info('No iCloud Devices found!') - track_utc_time_change( - hass, update_icloud, - minute=range(0, 60, config.get(CONF_INTERVAL, DEFAULT_INTERVAL)), - second=0 - ) + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, update_icloud) + + update_minutes = list(range(0, 60, config[CONF_INTERVAL])) + # Schedule keepalives between the updates + keepalive_minutes = list(x for x in range(0, 60, KEEPALIVE_INTERVAL) + if x not in update_minutes) + + track_utc_time_change(hass, update_icloud, second=0, minute=update_minutes) + track_utc_time_change(hass, keep_alive, second=0, minute=keepalive_minutes) return True diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index b4a865abffc..ac01202c7fb 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -13,7 +13,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.helpers.discovery import load_platform, discover DOMAIN = "discovery" -REQUIREMENTS = ['netdisco==0.6.7'] +REQUIREMENTS = ['netdisco==0.7.0'] SCAN_INTERVAL = 300 # seconds @@ -30,6 +30,7 @@ SERVICE_HANDLERS = { 'roku': ('media_player', 'roku'), 'sonos': ('media_player', 'sonos'), 'logitech_mediaserver': ('media_player', 'squeezebox'), + 'directv': ('media_player', 'directv'), } diff --git a/homeassistant/components/downloader.py b/homeassistant/components/downloader.py index e05c617bcf0..c639619d7a7 100644 --- a/homeassistant/components/downloader.py +++ b/homeassistant/components/downloader.py @@ -24,7 +24,8 @@ ATTR_URL = "url" ATTR_SUBDIR = "subdir" SERVICE_DOWNLOAD_FILE_SCHEMA = vol.Schema({ - vol.Required(ATTR_URL): vol.Url, + # pylint: disable=no-value-for-parameter + vol.Required(ATTR_URL): vol.Url(), vol.Optional(ATTR_SUBDIR): cv.string, }) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 2f6ac91f9c3..3925170694e 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -1,37 +1,130 @@ """Handle the frontend for Home Assistant.""" +import hashlib +import logging import os +from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.components import api from homeassistant.components.http import HomeAssistantView -from . import version, mdi_version +from .version import FINGERPRINTS DOMAIN = 'frontend' DEPENDENCIES = ['api'] +URL_PANEL_COMPONENT = '/frontend/panels/{}.html' +URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html' +STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static') +PANELS = {} + +# To keep track we don't register a component twice (gives a warning) +_REGISTERED_COMPONENTS = set() +_LOGGER = logging.getLogger(__name__) + + +def register_built_in_panel(hass, component_name, title=None, icon=None, + url_name=None, config=None): + """Register a built-in panel.""" + # pylint: disable=too-many-arguments + path = 'panels/ha-panel-{}.html'.format(component_name) + + if hass.wsgi.development: + url = ('/static/home-assistant-polymer/panels/' + '{0}/ha-panel-{0}.html'.format(component_name)) + else: + url = None # use default url generate mechanism + + register_panel(hass, component_name, os.path.join(STATIC_PATH, path), + FINGERPRINTS[path], title, icon, url_name, url, config) + + +def register_panel(hass, component_name, path, md5=None, title=None, icon=None, + url_name=None, url=None, config=None): + """Register a panel for the frontend. + + component_name: name of the web component + path: path to the HTML of the web component + md5: the md5 hash of the web component (for versioning, optional) + title: title to show in the sidebar (optional) + icon: icon to show next to title in sidebar (optional) + url_name: name to use in the url (defaults to component_name) + url: for the web component (for dev environment, optional) + config: config to be passed into the web component + + Warning: this API will probably change. Use at own risk. + """ + # pylint: disable=too-many-arguments + if url_name is None: + url_name = component_name + + if url_name in PANELS: + _LOGGER.warning('Overwriting component %s', url_name) + if not os.path.isfile(path): + _LOGGER.error('Panel %s component does not exist: %s', + component_name, path) + return + + if md5 is None: + with open(path) as fil: + md5 = hashlib.md5(fil.read().encode('utf-8')).hexdigest() + + data = { + 'url_name': url_name, + 'component_name': component_name, + } + + if title: + data['title'] = title + if icon: + data['icon'] = icon + if config is not None: + data['config'] = config + + if url is not None: + data['url'] = url + else: + url = URL_PANEL_COMPONENT.format(component_name) + + if url not in _REGISTERED_COMPONENTS: + hass.wsgi.register_static_path(url, path) + _REGISTERED_COMPONENTS.add(url) + + fprinted_url = URL_PANEL_COMPONENT_FP.format(component_name, md5) + data['url'] = fprinted_url + + PANELS[url_name] = data def setup(hass, config): """Setup serving the frontend.""" - hass.wsgi.register_view(IndexView) hass.wsgi.register_view(BootstrapView) - www_static_path = os.path.join(os.path.dirname(__file__), 'www_static') if hass.wsgi.development: sw_path = "home-assistant-polymer/build/service_worker.js" else: sw_path = "service_worker.js" - hass.wsgi.register_static_path( - "/service_worker.js", - os.path.join(www_static_path, sw_path), - 0 - ) - hass.wsgi.register_static_path( - "/robots.txt", - os.path.join(www_static_path, "robots.txt") - ) - hass.wsgi.register_static_path("/static", www_static_path) + hass.wsgi.register_static_path("/service_worker.js", + os.path.join(STATIC_PATH, sw_path), 0) + hass.wsgi.register_static_path("/robots.txt", + os.path.join(STATIC_PATH, "robots.txt")) + hass.wsgi.register_static_path("/static", STATIC_PATH) hass.wsgi.register_static_path("/local", hass.config.path('www')) + register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location') + + for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state', + 'dev-template'): + register_built_in_panel(hass, panel) + + def register_frontend_index(event): + """Register the frontend index urls. + + Done when Home Assistant is started so that all panels are known. + """ + hass.wsgi.register_view(IndexView( + hass, ['/{}'.format(name) for name in PANELS])) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, register_frontend_index) + return True @@ -48,6 +141,7 @@ class BootstrapView(HomeAssistantView): 'states': self.hass.states.all(), 'events': api.events_json(self.hass), 'services': api.services_json(self.hass), + 'panels': PANELS, }) @@ -57,16 +151,15 @@ class IndexView(HomeAssistantView): url = '/' name = "frontend:index" requires_auth = False - extra_urls = ['/logbook', '/history', '/map', '/devService', '/devState', - '/devEvent', '/devInfo', '/devTemplate', - '/states', '/states/'] + extra_urls = ['/states', '/states/'] - def __init__(self, hass): + def __init__(self, hass, extra_urls): """Initialize the frontend view.""" super().__init__(hass) from jinja2 import FileSystemLoader, Environment + self.extra_urls = self.extra_urls + extra_urls self.templates = Environment( loader=FileSystemLoader( os.path.join(os.path.dirname(__file__), 'templates/') @@ -76,32 +169,32 @@ class IndexView(HomeAssistantView): def get(self, request, entity_id=None): """Serve the index view.""" if self.hass.wsgi.development: - core_url = '/static/home-assistant-polymer/build/_core_compiled.js' + core_url = '/static/home-assistant-polymer/build/core.js' ui_url = '/static/home-assistant-polymer/src/home-assistant.html' - map_url = ('/static/home-assistant-polymer/src/layouts/' - 'partial-map.html') - dev_url = ('/static/home-assistant-polymer/src/entry-points/' - 'dev-tools.html') else: - core_url = '/static/core-{}.js'.format(version.CORE) - ui_url = '/static/frontend-{}.html'.format(version.UI) - map_url = '/static/partial-map-{}.html'.format(version.MAP) - dev_url = '/static/dev-tools-{}.html'.format(version.DEV) + core_url = '/static/core-{}.js'.format( + FINGERPRINTS['core.js']) + ui_url = '/static/frontend-{}.html'.format( + FINGERPRINTS['frontend.html']) + + if request.path == '/': + panel = 'states' + else: + panel = request.path.split('/')[1] + + panel_url = PANELS[panel]['url'] if panel != 'states' else '' # auto login if no password was set - if self.hass.config.api.api_password is None: - auth = 'true' - else: - auth = 'false' - - icons_url = '/static/mdi-{}.html'.format(mdi_version.VERSION) + no_auth = 'false' if self.hass.config.api.api_password else 'true' + icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html']) template = self.templates.get_template('index.html') # pylint is wrong # pylint: disable=no-member resp = template.render( - core_url=core_url, ui_url=ui_url, map_url=map_url, auth=auth, - dev_url=dev_url, icons_url=icons_url, icons=mdi_version.VERSION) + core_url=core_url, ui_url=ui_url, no_auth=no_auth, + icons_url=icons_url, icons=FINGERPRINTS['mdi.html'], + panel_url=panel_url) return self.Response(resp, mimetype='text/html') diff --git a/homeassistant/components/frontend/mdi_version.py b/homeassistant/components/frontend/mdi_version.py deleted file mode 100644 index baf3042931d..00000000000 --- a/homeassistant/components/frontend/mdi_version.py +++ /dev/null @@ -1,2 +0,0 @@ -"""DO NOT MODIFY. Auto-generated by update_mdi script.""" -VERSION = "758957b7ea989d6beca60e218ea7f7dd" diff --git a/homeassistant/components/frontend/templates/index.html b/homeassistant/components/frontend/templates/index.html index dddf826018a..31e347627fa 100644 --- a/homeassistant/components/frontend/templates/index.html +++ b/homeassistant/components/frontend/templates/index.html @@ -5,19 +5,26 @@ Home Assistant - + + href='/static/icons/favicon-apple-180x180.png'> - - - - + + + + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/dev-tools.html.gz b/homeassistant/components/frontend/www_static/dev-tools.html.gz deleted file mode 100644 index 5912606cc33..00000000000 Binary files a/homeassistant/components/frontend/www_static/dev-tools.html.gz and /dev/null differ diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 95ae493cabb..9dafede2a71 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -2,9 +2,5 @@ e._bubble()}function Bn(e,t){return $n(this,e,t,1)}function Jn(e,t){return $n(this,e,t,-1)}function Qn(e){return 0>e?Math.floor(e):Math.ceil(e)}function Xn(){var e,t,n,s,i,r=this._milliseconds,a=this._days,o=this._months,u=this._data;return r>=0&&a>=0&&o>=0||0>=r&&0>=a&&0>=o||(r+=864e5*Qn(es(o)+a),a=0,o=0),u.milliseconds=r%1e3,e=p(r/1e3),u.seconds=e%60,t=p(e/60),u.minutes=t%60,n=p(t/60),u.hours=n%24,a+=p(n/24),i=p(Kn(a)),o+=i,a-=Qn(es(i)),s=p(o/12),o%=12,u.days=a,u.months=o,u.years=s,this}function Kn(e){return 4800*e/146097}function es(e){return 146097*e/4800}function ts(e){var t,n,s=this._milliseconds;if(e=F(e),"month"===e||"year"===e)return t=this._days+s/864e5,n=this._months+Kn(t),"month"===e?n:n/12;switch(t=this._days+Math.round(es(this._months)),e){case"week":return t/7+s/6048e5;case"day":return t+s/864e5;case"hour":return 24*t+s/36e5;case"minute":return 1440*t+s/6e4;case"second":return 86400*t+s/1e3;case"millisecond":return Math.floor(864e5*t)+s;default:throw new Error("Unknown unit "+e)}}function ns(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*w(this._months/12)}function ss(e){return function(){return this.as(e)}}function is(e){return e=F(e),this[e+"s"]()}function rs(e){return function(){return this._data[e]}}function as(){return p(this.days()/7)}function os(e,t,n,s,i){return i.relativeTime(t||1,!!n,e,s)}function us(e,t,n){var s=Gt(e).abs(),i=dr(s.as("s")),r=dr(s.as("m")),a=dr(s.as("h")),o=dr(s.as("d")),u=dr(s.as("M")),d=dr(s.as("y")),l=i=r&&["m"]||r=a&&["h"]||a=o&&["d"]||o=u&&["M"]||u=d&&["y"]||["yy",d];return l[2]=t,l[3]=+e>0,l[4]=n,os.apply(null,l)}function ds(e){return void 0===e?dr:"function"==typeof e&&(dr=e,!0)}function ls(e,t){return void 0!==lr[e]&&(void 0===t?lr[e]:(lr[e]=t,!0))}function hs(e){var t=this.localeData(),n=us(this,!e,t);return e&&(n=t.pastFuture(+this,n)),t.postformat(n)}function cs(){var e,t,n,s=hr(this._milliseconds)/1e3,i=hr(this._days),r=hr(this._months);e=p(s/60),t=p(e/60),s%=60,e%=60,n=p(r/12),r%=12;var a=n,o=r,u=i,d=t,l=e,h=s,c=this.asSeconds();return c?(0>c?"-":"")+"P"+(a?a+"Y":"")+(o?o+"M":"")+(u?u+"D":"")+(d||l||h?"T":"")+(d?d+"H":"")+(l?l+"M":"")+(h?h+"S":""):"P0D"}var fs,ms;ms=Array.prototype.some?Array.prototype.some:function(e){for(var t=Object(this),n=t.length>>>0,s=0;n>s;s++)if(s in t&&e.call(this,t[s],s,t))return!0;return!1};var _s=e.momentProperties=[],ys=!1,gs={};e.suppressDeprecationWarnings=!1,e.deprecationHandler=null;var ps;ps=Object.keys?Object.keys:function(e){var t,n=[];for(t in e)o(e,t)&&n.push(t);return n};var ws,vs={sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},Ms={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},Ss="Invalid date",ks="%d",Ds=/\d{1,2}/,Ys={future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},xs={},Os={},Ts=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,bs=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,Ps={},Ws={},Rs=/\d/,Us=/\d\d/,Cs=/\d{3}/,Fs=/\d{4}/,Hs=/[+-]?\d{6}/,Ls=/\d\d?/,Gs=/\d\d\d\d?/,Vs=/\d\d\d\d\d\d?/,js=/\d{1,3}/,As=/\d{1,4}/,Es=/[+-]?\d{1,6}/,Ns=/\d+/,Is=/[+-]?\d+/,zs=/Z|[+-]\d\d:?\d\d/gi,Zs=/Z|[+-]\d\d(?::?\d\d)?/gi,qs=/[+-]?\d+(\.\d{1,3})?/,$s=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,Bs={},Js={},Qs=0,Xs=1,Ks=2,ei=3,ti=4,ni=5,si=6,ii=7,ri=8;ws=Array.prototype.indexOf?Array.prototype.indexOf:function(e){var t;for(t=0;t=e?""+e:"+"+e}),z(0,["YY",2],0,function(){return this.year()%100}),z(0,["YYYY",4],0,"year"),z(0,["YYYYY",5],0,"year"),z(0,["YYYYYY",6,!0],0,"year"),C("year","y"),L("year",1),J("Y",Is),J("YY",Ls,Us),J("YYYY",As,Fs),J("YYYYY",Es,Hs),J("YYYYYY",Es,Hs),ee(["YYYYY","YYYYYY"],Qs),ee("YYYY",function(t,n){n[Qs]=2===t.length?e.parseTwoDigitYear(t):w(t)}),ee("YY",function(t,n){n[Qs]=e.parseTwoDigitYear(t)}),ee("Y",function(e,t){t[Qs]=parseInt(e,10)}),e.parseTwoDigitYear=function(e){return w(e)+(w(e)>68?1900:2e3)};var hi=V("FullYear",!0);z("w",["ww",2],"wo","week"),z("W",["WW",2],"Wo","isoWeek"),C("week","w"),C("isoWeek","W"),L("week",5),L("isoWeek",5),J("w",Ls),J("ww",Ls,Us),J("W",Ls),J("WW",Ls,Us),te(["w","ww","W","WW"],function(e,t,n,s){t[s.substr(0,1)]=w(e)});var ci={dow:0,doy:6};z("d",0,"do","day"),z("dd",0,0,function(e){return this.localeData().weekdaysMin(this,e)}),z("ddd",0,0,function(e){return this.localeData().weekdaysShort(this,e)}),z("dddd",0,0,function(e){return this.localeData().weekdays(this,e)}),z("e",0,0,"weekday"),z("E",0,0,"isoWeekday"),C("day","d"),C("weekday","e"),C("isoWeekday","E"),L("day",11),L("weekday",11),L("isoWeekday",11),J("d",Ls),J("e",Ls),J("E",Ls),J("dd",function(e,t){return t.weekdaysMinRegex(e)}),J("ddd",function(e,t){return t.weekdaysShortRegex(e)}),J("dddd",function(e,t){return t.weekdaysRegex(e)}),te(["dd","ddd","dddd"],function(e,t,n,s){var i=n._locale.weekdaysParse(e,s,n._strict);null!=i?t.d=i:h(n).invalidWeekday=e}),te(["d","e","E"],function(e,t,n,s){t[s]=w(e)});var fi="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),mi="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),_i="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),yi=$s,gi=$s,pi=$s;z("H",["HH",2],0,"hour"),z("h",["hh",2],0,Ee),z("k",["kk",2],0,Ne),z("hmm",0,0,function(){return""+Ee.apply(this)+I(this.minutes(),2)}),z("hmmss",0,0,function(){return""+Ee.apply(this)+I(this.minutes(),2)+I(this.seconds(),2)}),z("Hmm",0,0,function(){return""+this.hours()+I(this.minutes(),2)}),z("Hmmss",0,0,function(){return""+this.hours()+I(this.minutes(),2)+I(this.seconds(),2)}),Ie("a",!0),Ie("A",!1),C("hour","h"),L("hour",13),J("a",ze),J("A",ze),J("H",Ls),J("h",Ls),J("HH",Ls,Us),J("hh",Ls,Us),J("hmm",Gs),J("hmmss",Vs),J("Hmm",Gs),J("Hmmss",Vs),ee(["H","HH"],ei),ee(["a","A"],function(e,t,n){n._isPm=n._locale.isPM(e),n._meridiem=e}),ee(["h","hh"],function(e,t,n){t[ei]=w(e),h(n).bigHour=!0}),ee("hmm",function(e,t,n){var s=e.length-2;t[ei]=w(e.substr(0,s)),t[ti]=w(e.substr(s)),h(n).bigHour=!0}),ee("hmmss",function(e,t,n){var s=e.length-4,i=e.length-2;t[ei]=w(e.substr(0,s)),t[ti]=w(e.substr(s,2)),t[ni]=w(e.substr(i)),h(n).bigHour=!0}),ee("Hmm",function(e,t,n){var s=e.length-2;t[ei]=w(e.substr(0,s)),t[ti]=w(e.substr(s))}),ee("Hmmss",function(e,t,n){var s=e.length-4,i=e.length-2;t[ei]=w(e.substr(0,s)),t[ti]=w(e.substr(s,2)),t[ni]=w(e.substr(i))});var wi,vi=/[ap]\.?m?\.?/i,Mi=V("Hours",!0),Si={calendar:vs,longDateFormat:Ms,invalidDate:Ss,ordinal:ks,ordinalParse:Ds,relativeTime:Ys,months:oi,monthsShort:ui,week:ci,weekdays:fi,weekdaysMin:_i,weekdaysShort:mi,meridiemParse:vi},ki={},Di=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?/,Yi=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?/,xi=/Z|[+-]\d\d(?::?\d\d)?/,Oi=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],Ti=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],bi=/^\/?Date\((\-?\d+)/i;e.createFromInputFallback=S("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.",function(e){e._d=new Date(e._i+(e._useUTC?" UTC":""))}),e.ISO_8601=function(){};var Pi=S("moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var e=gt.apply(null,arguments);return this.isValid()&&e.isValid()?this>e?this:e:f()}),Wi=S("moment().max is deprecated, use moment.min instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var e=gt.apply(null,arguments);return this.isValid()&&e.isValid()?e>this?this:e:f()}),Ri=function(){return Date.now?Date.now():+new Date};kt("Z",":"),kt("ZZ",""),J("Z",Zs),J("ZZ",Zs),ee(["Z","ZZ"],function(e,t,n){n._useUTC=!0,n._tzm=Dt(Zs,e)});var Ui=/([\+\-]|\d\d)/gi;e.updateOffset=function(){};var Ci=/^(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?\d*)?$/,Fi=/^(-)?P(?:(-?[0-9,.]*)Y)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)W)?(?:(-?[0-9,.]*)D)?(?:T(?:(-?[0-9,.]*)H)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)S)?)?$/;Gt.fn=Mt.prototype;var Hi=Nt(1,"add"),Li=Nt(-1,"subtract");e.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",e.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";var Gi=S("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(e){return void 0===e?this.localeData():this.locale(e)});z(0,["gg",2],0,function(){return this.weekYear()%100}),z(0,["GG",2],0,function(){return this.isoWeekYear()%100}),Dn("gggg","weekYear"),Dn("ggggg","weekYear"),Dn("GGGG","isoWeekYear"),Dn("GGGGG","isoWeekYear"),C("weekYear","gg"),C("isoWeekYear","GG"),L("weekYear",1),L("isoWeekYear",1),J("G",Is),J("g",Is),J("GG",Ls,Us),J("gg",Ls,Us),J("GGGG",As,Fs),J("gggg",As,Fs),J("GGGGG",Es,Hs),J("ggggg",Es,Hs),te(["gggg","ggggg","GGGG","GGGGG"],function(e,t,n,s){t[s.substr(0,2)]=w(e)}),te(["gg","GG"],function(t,n,s,i){n[i]=e.parseTwoDigitYear(t)}),z("Q",0,"Qo","quarter"),C("quarter","Q"),L("quarter",7),J("Q",Rs),ee("Q",function(e,t){t[Xs]=3*(w(e)-1)}),z("D",["DD",2],"Do","date"),C("date","D"),L("date",9),J("D",Ls),J("DD",Ls,Us),J("Do",function(e,t){return e?t._ordinalParse:t._ordinalParseLenient}),ee(["D","DD"],Ks),ee("Do",function(e,t){t[Ks]=w(e.match(Ls)[0],10)});var Vi=V("Date",!0);z("DDD",["DDDD",3],"DDDo","dayOfYear"),C("dayOfYear","DDD"),L("dayOfYear",4),J("DDD",js),J("DDDD",Cs),ee(["DDD","DDDD"],function(e,t,n){n._dayOfYear=w(e)}),z("m",["mm",2],0,"minute"),C("minute","m"),L("minute",14),J("m",Ls),J("mm",Ls,Us),ee(["m","mm"],ti);var ji=V("Minutes",!1);z("s",["ss",2],0,"second"),C("second","s"),L("second",15),J("s",Ls),J("ss",Ls,Us),ee(["s","ss"],ni);var Ai=V("Seconds",!1);z("S",0,0,function(){return~~(this.millisecond()/100)}),z(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),z(0,["SSS",3],0,"millisecond"),z(0,["SSSS",4],0,function(){return 10*this.millisecond()}),z(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),z(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),z(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),z(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),z(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),C("millisecond","ms"),L("millisecond",16),J("S",js,Rs),J("SS",js,Us),J("SSS",js,Cs);var Ei;for(Ei="SSSS";Ei.length<=9;Ei+="S")J(Ei,Ns);for(Ei="S";Ei.length<=9;Ei+="S")ee(Ei,Un);var Ni=V("Milliseconds",!1);z("z",0,0,"zoneAbbr"),z("zz",0,0,"zoneName");var Ii=y.prototype;Ii.add=Hi,Ii.calendar=Zt,Ii.clone=qt,Ii.diff=en,Ii.endOf=fn,Ii.format=rn,Ii.from=an,Ii.fromNow=on,Ii.to=un,Ii.toNow=dn,Ii.get=E,Ii.invalidAt=Sn,Ii.isAfter=$t,Ii.isBefore=Bt,Ii.isBetween=Jt,Ii.isSame=Qt,Ii.isSameOrAfter=Xt,Ii.isSameOrBefore=Kt,Ii.isValid=vn,Ii.lang=Gi,Ii.locale=ln,Ii.localeData=hn,Ii.max=Wi,Ii.min=Pi,Ii.parsingFlags=Mn,Ii.set=N,Ii.startOf=cn,Ii.subtract=Li,Ii.toArray=gn,Ii.toObject=pn,Ii.toDate=yn,Ii.toISOString=sn,Ii.toJSON=wn,Ii.toString=nn,Ii.unix=_n,Ii.valueOf=mn,Ii.creationData=kn,Ii.year=hi,Ii.isLeapYear=ye,Ii.weekYear=Yn,Ii.isoWeekYear=xn,Ii.quarter=Ii.quarters=Wn,Ii.month=de,Ii.daysInMonth=le,Ii.week=Ii.weeks=xe,Ii.isoWeek=Ii.isoWeeks=Oe,Ii.weeksInYear=Tn,Ii.isoWeeksInYear=On,Ii.date=Vi,Ii.day=Ii.days=Fe,Ii.weekday=He,Ii.isoWeekday=Le,Ii.dayOfYear=Rn,Ii.hour=Ii.hours=Mi,Ii.minute=Ii.minutes=ji,Ii.second=Ii.seconds=Ai,Ii.millisecond=Ii.milliseconds=Ni,Ii.utcOffset=Ot,Ii.utc=bt,Ii.local=Pt,Ii.parseZone=Wt,Ii.hasAlignedHourOffset=Rt,Ii.isDST=Ut,Ii.isLocal=Ft,Ii.isUtcOffset=Ht,Ii.isUtc=Lt,Ii.isUTC=Lt,Ii.zoneAbbr=Cn,Ii.zoneName=Fn,Ii.dates=S("dates accessor is deprecated. Use date instead.",Vi),Ii.months=S("months accessor is deprecated. Use month instead",de),Ii.years=S("years accessor is deprecated. Use year instead",hi),Ii.zone=S("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",Tt),Ii.isDSTShifted=S("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",Ct);var zi=Ii,Zi=O.prototype;Zi.calendar=T,Zi.longDateFormat=b,Zi.invalidDate=P,Zi.ordinal=W,Zi.preparse=Gn,Zi.postformat=Gn,Zi.relativeTime=R,Zi.pastFuture=U,Zi.set=Y,Zi.months=ie,Zi.monthsShort=re,Zi.monthsParse=oe,Zi.monthsRegex=ce,Zi.monthsShortRegex=he,Zi.week=ke,Zi.firstDayOfYear=Ye,Zi.firstDayOfWeek=De,Zi.weekdays=Pe,Zi.weekdaysMin=Re,Zi.weekdaysShort=We,Zi.weekdaysParse=Ce,Zi.weekdaysRegex=Ge,Zi.weekdaysShortRegex=Ve,Zi.weekdaysMinRegex=je,Zi.isPM=Ze,Zi.meridiem=qe,Qe("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(e){var t=e%10,n=1===w(e%100/10)?"th":1===t?"st":2===t?"nd":3===t?"rd":"th";return e+n}}),e.lang=S("moment.lang is deprecated. Use moment.locale instead.",Qe),e.langData=S("moment.langData is deprecated. Use moment.localeData instead.",et);var qi=Math.abs,$i=ss("ms"),Bi=ss("s"),Ji=ss("m"),Qi=ss("h"),Xi=ss("d"),Ki=ss("w"),er=ss("M"),tr=ss("y"),nr=rs("milliseconds"),sr=rs("seconds"),ir=rs("minutes"),rr=rs("hours"),ar=rs("days"),or=rs("months"),ur=rs("years"),dr=Math.round,lr={s:45,m:45,h:22,d:26,M:11},hr=Math.abs,cr=Mt.prototype;cr.abs=qn,cr.add=Bn,cr.subtract=Jn,cr.as=ts,cr.asMilliseconds=$i,cr.asSeconds=Bi,cr.asMinutes=Ji,cr.asHours=Qi,cr.asDays=Xi,cr.asWeeks=Ki,cr.asMonths=er,cr.asYears=tr,cr.valueOf=ns,cr._bubble=Xn,cr.get=is,cr.milliseconds=nr,cr.seconds=sr,cr.minutes=ir,cr.hours=rr,cr.days=ar,cr.weeks=as,cr.months=or,cr.years=ur,cr.humanize=hs,cr.toISOString=cs,cr.toString=cs,cr.toJSON=cs,cr.locale=ln,cr.localeData=hn,cr.toIsoString=S("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",cs),cr.lang=Gi,z("X",0,0,"unix"),z("x",0,0,"valueOf"),J("x",Is),J("X",qs),ee("X",function(e,t,n){n._d=new Date(1e3*parseFloat(e,10))}),ee("x",function(e,t,n){n._d=new Date(w(e))}),e.version="2.14.1",t(gt),e.fn=zi,e.min=wt,e.max=vt,e.now=Ri,e.utc=d,e.unix=Hn,e.months=En,e.isDate=r,e.locale=Qe,e.invalid=f,e.duration=Gt,e.isMoment=g,e.weekdays=In,e.parseZone=Ln,e.localeData=et,e.isDuration=St,e.monthsShort=Nn,e.weekdaysMin=Zn,e.defineLocale=Xe,e.updateLocale=Ke,e.locales=tt,e.weekdaysShort=zn,e.normalizeUnits=F,e.relativeTimeRounding=ds,e.relativeTimeThreshold=ls,e.calendarFormat=zt,e.prototype=zi;var fr=e;return fr}) \ No newline at end of file +t},updateStyles:function(e){e&&this.mixin(this.customStyle,e),i?t.updateNativeStyleProperties(this,this.customStyle):(this.isAttached?this._needsStyleProperties()?this._updateStyleProperties():this._styleProperties=null:this.__stylePropertiesInvalid=!0,this._styleCache&&this._styleCache.clear(),this._updateRootStyles())},_updateRootStyles:function(e){e=e||this.root;for(var t,n=Polymer.dom(e)._query(function(e){return e.shadyRoot||e.shadowRoot}),r=0,s=n.length;r0&&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:!0,readOnly:!0},initialCount:{type:Number,observer:"_initializeChunking"},targetFramerate:{type:Number,value:20},_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),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 h=a[i];void 0!==h&&this._detachAndRemoveInstance(h)}var c=this;if(l.length){this._filterFn&&(l=l.filter(function(e){return c._filterFn(r.getItem(e))})),l.sort(function(e,t){return c._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 aee6bcde2e4..bbc4f6c09f9 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 718384f22aa..697f9397de3 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 718384f22aa0a689190a4d3f41b5e9ed091c80a3 +Subproject commit 697f9397de357cec9662626575fc01d6f921ef22 diff --git a/homeassistant/components/frontend/www_static/favicon-1024x1024.png b/homeassistant/components/frontend/www_static/icons/favicon-1024x1024.png similarity index 100% rename from homeassistant/components/frontend/www_static/favicon-1024x1024.png rename to homeassistant/components/frontend/www_static/icons/favicon-1024x1024.png diff --git a/homeassistant/components/frontend/www_static/favicon-192x192.png b/homeassistant/components/frontend/www_static/icons/favicon-192x192.png similarity index 100% rename from homeassistant/components/frontend/www_static/favicon-192x192.png rename to homeassistant/components/frontend/www_static/icons/favicon-192x192.png diff --git a/homeassistant/components/frontend/www_static/favicon-384x384.png b/homeassistant/components/frontend/www_static/icons/favicon-384x384.png similarity index 100% rename from homeassistant/components/frontend/www_static/favicon-384x384.png rename to homeassistant/components/frontend/www_static/icons/favicon-384x384.png diff --git a/homeassistant/components/frontend/www_static/favicon-512x512.png b/homeassistant/components/frontend/www_static/icons/favicon-512x512.png similarity index 100% rename from homeassistant/components/frontend/www_static/favicon-512x512.png rename to homeassistant/components/frontend/www_static/icons/favicon-512x512.png diff --git a/homeassistant/components/frontend/www_static/favicon-apple-180x180.png b/homeassistant/components/frontend/www_static/icons/favicon-apple-180x180.png similarity index 100% rename from homeassistant/components/frontend/www_static/favicon-apple-180x180.png rename to homeassistant/components/frontend/www_static/icons/favicon-apple-180x180.png diff --git a/homeassistant/components/frontend/www_static/favicon.ico b/homeassistant/components/frontend/www_static/icons/favicon.ico similarity index 100% rename from homeassistant/components/frontend/www_static/favicon.ico rename to homeassistant/components/frontend/www_static/icons/favicon.ico diff --git a/homeassistant/components/frontend/www_static/tile-win-150x150.png b/homeassistant/components/frontend/www_static/icons/tile-win-150x150.png similarity index 100% rename from homeassistant/components/frontend/www_static/tile-win-150x150.png rename to homeassistant/components/frontend/www_static/icons/tile-win-150x150.png diff --git a/homeassistant/components/frontend/www_static/tile-win-310x150.png b/homeassistant/components/frontend/www_static/icons/tile-win-310x150.png similarity index 100% rename from homeassistant/components/frontend/www_static/tile-win-310x150.png rename to homeassistant/components/frontend/www_static/icons/tile-win-310x150.png diff --git a/homeassistant/components/frontend/www_static/tile-win-310x310.png b/homeassistant/components/frontend/www_static/icons/tile-win-310x310.png similarity index 100% rename from homeassistant/components/frontend/www_static/tile-win-310x310.png rename to homeassistant/components/frontend/www_static/icons/tile-win-310x310.png diff --git a/homeassistant/components/frontend/www_static/tile-win-70x70.png b/homeassistant/components/frontend/www_static/icons/tile-win-70x70.png similarity index 100% rename from homeassistant/components/frontend/www_static/tile-win-70x70.png rename to homeassistant/components/frontend/www_static/icons/tile-win-70x70.png diff --git a/homeassistant/components/frontend/www_static/manifest.json b/homeassistant/components/frontend/www_static/manifest.json index aa09fb0e037..4cd13ad5470 100644 --- a/homeassistant/components/frontend/www_static/manifest.json +++ b/homeassistant/components/frontend/www_static/manifest.json @@ -7,22 +7,22 @@ "background_color": "#FFFFFF", "icons": [ { - "src": "/static/favicon-192x192.png", + "src": "/static/icons/favicon-192x192.png", "sizes": "192x192", "type": "image/png" }, { - "src": "/static/favicon-384x384.png", + "src": "/static/icons/favicon-384x384.png", "sizes": "384x384", "type": "image/png" }, { - "src": "/static/favicon-512x512.png", + "src": "/static/icons/favicon-512x512.png", "sizes": "512x512", "type": "image/png" }, { - "src": "/static/favicon-1024x1024.png", + "src": "/static/icons/favicon-1024x1024.png", "sizes": "1024x1024", "type": "image/png" } diff --git a/homeassistant/components/frontend/www_static/mdi.html b/homeassistant/components/frontend/www_static/mdi.html index d73b5fb02e6..8bc40205f1b 100644 --- a/homeassistant/components/frontend/www_static/mdi.html +++ b/homeassistant/components/frontend/www_static/mdi.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/mdi.html.gz b/homeassistant/components/frontend/www_static/mdi.html.gz index e4c0aee023a..e5e42c3f490 100644 Binary files a/homeassistant/components/frontend/www_static/mdi.html.gz and b/homeassistant/components/frontend/www_static/mdi.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html new file mode 100644 index 00000000000..273947ef22f --- /dev/null +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 00000000000..5c48f4f22ec Binary files /dev/null and b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-event.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-info.html b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-info.html new file mode 100644 index 00000000000..d858d470d76 --- /dev/null +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-info.html @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-info.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-info.html.gz new file mode 100644 index 00000000000..435c85a2e5c Binary files /dev/null and b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-info.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html new file mode 100644 index 00000000000..d94782b8763 --- /dev/null +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 00000000000..32922dfd3ee Binary files /dev/null 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 new file mode 100644 index 00000000000..907e497c5d1 --- /dev/null +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-state.html @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 00000000000..e356b098b3a Binary files /dev/null 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 new file mode 100644 index 00000000000..b7afc61f34c --- /dev/null +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html @@ -0,0 +1,2 @@ + \ 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 new file mode 100644 index 00000000000..17a7e815912 Binary files /dev/null and b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-history.html b/homeassistant/components/frontend/www_static/panels/ha-panel-history.html new file mode 100644 index 00000000000..b592685f173 --- /dev/null +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-history.html @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-history.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-history.html.gz new file mode 100644 index 00000000000..061db66c3c1 Binary files /dev/null and b/homeassistant/components/frontend/www_static/panels/ha-panel-history.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-iframe.html b/homeassistant/components/frontend/www_static/panels/ha-panel-iframe.html new file mode 100644 index 00000000000..6b0731f0016 --- /dev/null +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-iframe.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-iframe.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-iframe.html.gz new file mode 100644 index 00000000000..2f23d270f6b Binary files /dev/null and b/homeassistant/components/frontend/www_static/panels/ha-panel-iframe.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html b/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html new file mode 100644 index 00000000000..bc6dc384245 --- /dev/null +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html.gz new file mode 100644 index 00000000000..b25b29bc2c0 Binary files /dev/null and b/homeassistant/components/frontend/www_static/panels/ha-panel-logbook.html.gz differ diff --git a/homeassistant/components/frontend/www_static/partial-map.html b/homeassistant/components/frontend/www_static/panels/ha-panel-map.html similarity index 77% rename from homeassistant/components/frontend/www_static/partial-map.html rename to homeassistant/components/frontend/www_static/panels/ha-panel-map.html index 0d3aaf12076..ba5d18e47dc 100644 --- a/homeassistant/components/frontend/www_static/partial-map.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-map.html @@ -1,4 +1,4 @@ - \ No newline at end of file +case"touchend":return this.addPointerListenerEnd(t,e,i,n);case"touchmove":return this.addPointerListenerMove(t,e,i,n);default:throw"Unknown touch event type"}},addPointerListenerStart:function(t,i,n,s){var a="_leaflet_",r=this._pointers,h=function(t){"mouse"!==t.pointerType&&t.pointerType!==t.MSPOINTER_TYPE_MOUSE&&o.DomEvent.preventDefault(t);for(var e=!1,i=0;i1))&&(this._moved||(o.DomUtil.addClass(e._mapPane,"leaflet-touching"),e.fire("movestart").fire("zoomstart"),this._moved=!0),o.Util.cancelAnimFrame(this._animRequest),this._animRequest=o.Util.requestAnimFrame(this._updateOnMove,this,!0,this._map._container),o.DomEvent.preventDefault(t))}},_updateOnMove:function(){var t=this._map,e=this._getScaleOrigin(),i=t.layerPointToLatLng(e),n=t.getScaleZoom(this._scale);t._animateZoom(i,n,this._startCenter,this._scale,this._delta,!1,!0)},_onTouchEnd:function(){if(!this._moved||!this._zooming)return void(this._zooming=!1);var t=this._map;this._zooming=!1,o.DomUtil.removeClass(t._mapPane,"leaflet-touching"),o.Util.cancelAnimFrame(this._animRequest),o.DomEvent.off(e,"touchmove",this._onTouchMove).off(e,"touchend",this._onTouchEnd);var i=this._getScaleOrigin(),n=t.layerPointToLatLng(i),s=t.getZoom(),a=t.getScaleZoom(this._scale)-s,r=a>0?Math.ceil(a):Math.floor(a),h=t._limitZoom(s+r),l=t.getZoomScale(h)/this._scale;t._animateZoom(n,h,i,l)},_getScaleOrigin:function(){var t=this._centerOffset.subtract(this._delta).divideBy(this._scale);return this._startCenter.add(t)}}),o.Map.addInitHook("addHandler","touchZoom",o.Map.TouchZoom),o.Map.mergeOptions({tap:!0,tapTolerance:15}),o.Map.Tap=o.Handler.extend({addHooks:function(){o.DomEvent.on(this._map._container,"touchstart",this._onDown,this)},removeHooks:function(){o.DomEvent.off(this._map._container,"touchstart",this._onDown,this)},_onDown:function(t){if(t.touches){if(o.DomEvent.preventDefault(t),this._fireClick=!0,t.touches.length>1)return this._fireClick=!1,void clearTimeout(this._holdTimeout);var i=t.touches[0],n=i.target;this._startPos=this._newPos=new o.Point(i.clientX,i.clientY),n.tagName&&"a"===n.tagName.toLowerCase()&&o.DomUtil.addClass(n,"leaflet-active"),this._holdTimeout=setTimeout(o.bind(function(){this._isTapValid()&&(this._fireClick=!1,this._onUp(),this._simulateEvent("contextmenu",i))},this),1e3),o.DomEvent.on(e,"touchmove",this._onMove,this).on(e,"touchend",this._onUp,this)}},_onUp:function(t){if(clearTimeout(this._holdTimeout),o.DomEvent.off(e,"touchmove",this._onMove,this).off(e,"touchend",this._onUp,this),this._fireClick&&t&&t.changedTouches){var i=t.changedTouches[0],n=i.target;n&&n.tagName&&"a"===n.tagName.toLowerCase()&&o.DomUtil.removeClass(n,"leaflet-active"),this._isTapValid()&&this._simulateEvent("click",i)}},_isTapValid:function(){return this._newPos.distanceTo(this._startPos)<=this._map.options.tapTolerance},_onMove:function(t){var e=t.touches[0];this._newPos=new o.Point(e.clientX,e.clientY)},_simulateEvent:function(i,n){var o=e.createEvent("MouseEvents");o._simulated=!0,n.target._simulatedClick=!0,o.initMouseEvent(i,!0,!0,t,1,n.screenX,n.screenY,n.clientX,n.clientY,!1,!1,!1,!1,0,null),n.target.dispatchEvent(o)}}),o.Browser.touch&&!o.Browser.pointer&&o.Map.addInitHook("addHandler","tap",o.Map.Tap),o.Map.mergeOptions({boxZoom:!0}),o.Map.BoxZoom=o.Handler.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane,this._moved=!1},addHooks:function(){o.DomEvent.on(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){o.DomEvent.off(this._container,"mousedown",this._onMouseDown),this._moved=!1},moved:function(){return this._moved},_onMouseDown:function(t){return this._moved=!1,!(!t.shiftKey||1!==t.which&&1!==t.button)&&(o.DomUtil.disableTextSelection(),o.DomUtil.disableImageDrag(),this._startLayerPoint=this._map.mouseEventToLayerPoint(t),void o.DomEvent.on(e,"mousemove",this._onMouseMove,this).on(e,"mouseup",this._onMouseUp,this).on(e,"keydown",this._onKeyDown,this))},_onMouseMove:function(t){this._moved||(this._box=o.DomUtil.create("div","leaflet-zoom-box",this._pane),o.DomUtil.setPosition(this._box,this._startLayerPoint),this._container.style.cursor="crosshair",this._map.fire("boxzoomstart"));var e=this._startLayerPoint,i=this._box,n=this._map.mouseEventToLayerPoint(t),s=n.subtract(e),a=new o.Point(Math.min(n.x,e.x),Math.min(n.y,e.y));o.DomUtil.setPosition(i,a),this._moved=!0,i.style.width=Math.max(0,Math.abs(s.x)-4)+"px",i.style.height=Math.max(0,Math.abs(s.y)-4)+"px"},_finish:function(){this._moved&&(this._pane.removeChild(this._box),this._container.style.cursor=""),o.DomUtil.enableTextSelection(),o.DomUtil.enableImageDrag(),o.DomEvent.off(e,"mousemove",this._onMouseMove).off(e,"mouseup",this._onMouseUp).off(e,"keydown",this._onKeyDown)},_onMouseUp:function(t){this._finish();var e=this._map,i=e.mouseEventToLayerPoint(t);if(!this._startLayerPoint.equals(i)){var n=new o.LatLngBounds(e.layerPointToLatLng(this._startLayerPoint),e.layerPointToLatLng(i));e.fitBounds(n),e.fire("boxzoomend",{boxZoomBounds:n})}},_onKeyDown:function(t){27===t.keyCode&&this._finish()}}),o.Map.addInitHook("addHandler","boxZoom",o.Map.BoxZoom),o.Map.mergeOptions({keyboard:!0,keyboardPanOffset:80,keyboardZoomOffset:1}),o.Map.Keyboard=o.Handler.extend({keyCodes:{left:[37],right:[39],down:[40],up:[38],zoomIn:[187,107,61,171],zoomOut:[189,109,173]},initialize:function(t){this._map=t,this._setPanOffset(t.options.keyboardPanOffset),this._setZoomOffset(t.options.keyboardZoomOffset)},addHooks:function(){var t=this._map._container;-1===t.tabIndex&&(t.tabIndex="0"),o.DomEvent.on(t,"focus",this._onFocus,this).on(t,"blur",this._onBlur,this).on(t,"mousedown",this._onMouseDown,this),this._map.on("focus",this._addHooks,this).on("blur",this._removeHooks,this)},removeHooks:function(){this._removeHooks();var t=this._map._container;o.DomEvent.off(t,"focus",this._onFocus,this).off(t,"blur",this._onBlur,this).off(t,"mousedown",this._onMouseDown,this),this._map.off("focus",this._addHooks,this).off("blur",this._removeHooks,this)},_onMouseDown:function(){if(!this._focused){var i=e.body,n=e.documentElement,o=i.scrollTop||n.scrollTop,s=i.scrollLeft||n.scrollLeft;this._map._container.focus(),t.scrollTo(s,o)}},_onFocus:function(){this._focused=!0,this._map.fire("focus")},_onBlur:function(){this._focused=!1,this._map.fire("blur")},_setPanOffset:function(t){var e,i,n=this._panKeys={},o=this.keyCodes;for(e=0,i=o.left.length;i>e;e++)n[o.left[e]]=[-1*t,0];for(e=0,i=o.right.length;i>e;e++)n[o.right[e]]=[t,0];for(e=0,i=o.down.length;i>e;e++)n[o.down[e]]=[0,t];for(e=0,i=o.up.length;i>e;e++)n[o.up[e]]=[0,-1*t]},_setZoomOffset:function(t){var e,i,n=this._zoomKeys={},o=this.keyCodes;for(e=0,i=o.zoomIn.length;i>e;e++)n[o.zoomIn[e]]=t;for(e=0,i=o.zoomOut.length;i>e;e++)n[o.zoomOut[e]]=-t},_addHooks:function(){o.DomEvent.on(e,"keydown",this._onKeyDown,this)},_removeHooks:function(){o.DomEvent.off(e,"keydown",this._onKeyDown,this)},_onKeyDown:function(t){var e=t.keyCode,i=this._map;if(e in this._panKeys){if(i._panAnim&&i._panAnim._inProgress)return;i.panBy(this._panKeys[e]),i.options.maxBounds&&i.panInsideBounds(i.options.maxBounds)}else{if(!(e in this._zoomKeys))return;i.setZoom(i.getZoom()+this._zoomKeys[e])}o.DomEvent.stop(t)}}),o.Map.addInitHook("addHandler","keyboard",o.Map.Keyboard),o.Handler.MarkerDrag=o.Handler.extend({initialize:function(t){this._marker=t},addHooks:function(){var t=this._marker._icon;this._draggable||(this._draggable=new o.Draggable(t,t)),this._draggable.on("dragstart",this._onDragStart,this).on("drag",this._onDrag,this).on("dragend",this._onDragEnd,this),this._draggable.enable(),o.DomUtil.addClass(this._marker._icon,"leaflet-marker-draggable")},removeHooks:function(){this._draggable.off("dragstart",this._onDragStart,this).off("drag",this._onDrag,this).off("dragend",this._onDragEnd,this),this._draggable.disable(),o.DomUtil.removeClass(this._marker._icon,"leaflet-marker-draggable")},moved:function(){return this._draggable&&this._draggable._moved},_onDragStart:function(){this._marker.closePopup().fire("movestart").fire("dragstart")},_onDrag:function(){var t=this._marker,e=t._shadow,i=o.DomUtil.getPosition(t._icon),n=t._map.layerPointToLatLng(i);e&&o.DomUtil.setPosition(e,i),t._latlng=n,t.fire("move",{latlng:n}).fire("drag")},_onDragEnd:function(t){this._marker.fire("moveend").fire("dragend",t)}}),o.Control=o.Class.extend({options:{position:"topright"},initialize:function(t){o.setOptions(this,t)},getPosition:function(){return this.options.position},setPosition:function(t){var e=this._map;return e&&e.removeControl(this),this.options.position=t,e&&e.addControl(this),this},getContainer:function(){return this._container},addTo:function(t){this._map=t;var e=this._container=this.onAdd(t),i=this.getPosition(),n=t._controlCorners[i];return o.DomUtil.addClass(e,"leaflet-control"),-1!==i.indexOf("bottom")?n.insertBefore(e,n.firstChild):n.appendChild(e),this},removeFrom:function(t){var e=this.getPosition(),i=t._controlCorners[e];return i.removeChild(this._container),this._map=null,this.onRemove&&this.onRemove(t),this},_refocusOnMap:function(){this._map&&this._map.getContainer().focus()}}),o.control=function(t){return new o.Control(t)},o.Map.include({addControl:function(t){return t.addTo(this),this},removeControl:function(t){return t.removeFrom(this),this},_initControlPos:function(){function t(t,s){var a=i+t+" "+i+s;e[t+s]=o.DomUtil.create("div",a,n)}var e=this._controlCorners={},i="leaflet-",n=this._controlContainer=o.DomUtil.create("div",i+"control-container",this._container);t("top","left"),t("top","right"),t("bottom","left"),t("bottom","right")},_clearControlPos:function(){this._container.removeChild(this._controlContainer)}}),o.Control.Zoom=o.Control.extend({options:{position:"topleft",zoomInText:"+",zoomInTitle:"Zoom in",zoomOutText:"-",zoomOutTitle:"Zoom out"},onAdd:function(t){var e="leaflet-control-zoom",i=o.DomUtil.create("div",e+" leaflet-bar");return this._map=t,this._zoomInButton=this._createButton(this.options.zoomInText,this.options.zoomInTitle,e+"-in",i,this._zoomIn,this),this._zoomOutButton=this._createButton(this.options.zoomOutText,this.options.zoomOutTitle,e+"-out",i,this._zoomOut,this),this._updateDisabled(),t.on("zoomend zoomlevelschange",this._updateDisabled,this),i},onRemove:function(t){t.off("zoomend zoomlevelschange",this._updateDisabled,this)},_zoomIn:function(t){this._map.zoomIn(t.shiftKey?3:1)},_zoomOut:function(t){this._map.zoomOut(t.shiftKey?3:1)},_createButton:function(t,e,i,n,s,a){var r=o.DomUtil.create("a",i,n);r.innerHTML=t,r.href="#",r.title=e;var h=o.DomEvent.stopPropagation;return o.DomEvent.on(r,"click",h).on(r,"mousedown",h).on(r,"dblclick",h).on(r,"click",o.DomEvent.preventDefault).on(r,"click",s,a).on(r,"click",this._refocusOnMap,a),r},_updateDisabled:function(){var t=this._map,e="leaflet-disabled";o.DomUtil.removeClass(this._zoomInButton,e),o.DomUtil.removeClass(this._zoomOutButton,e),t._zoom===t.getMinZoom()&&o.DomUtil.addClass(this._zoomOutButton,e),t._zoom===t.getMaxZoom()&&o.DomUtil.addClass(this._zoomInButton,e)}}),o.Map.mergeOptions({zoomControl:!0}),o.Map.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new o.Control.Zoom,this.addControl(this.zoomControl))}),o.control.zoom=function(t){return new o.Control.Zoom(t)},o.Control.Attribution=o.Control.extend({options:{position:"bottomright",prefix:'Leaflet'},initialize:function(t){o.setOptions(this,t),this._attributions={}},onAdd:function(t){this._container=o.DomUtil.create("div","leaflet-control-attribution"),o.DomEvent.disableClickPropagation(this._container);for(var e in t._layers)t._layers[e].getAttribution&&this.addAttribution(t._layers[e].getAttribution());return t.on("layeradd",this._onLayerAdd,this).on("layerremove",this._onLayerRemove,this),this._update(),this._container},onRemove:function(t){t.off("layeradd",this._onLayerAdd).off("layerremove",this._onLayerRemove)},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):void 0},removeAttribution:function(t){return t?(this._attributions[t]&&(this._attributions[t]--,this._update()),this):void 0},_update:function(){if(this._map){var t=[];for(var e in this._attributions)this._attributions[e]&&t.push(e);var i=[];this.options.prefix&&i.push(this.options.prefix),t.length&&i.push(t.join(", ")),this._container.innerHTML=i.join(" | ")}},_onLayerAdd:function(t){t.layer.getAttribution&&this.addAttribution(t.layer.getAttribution())},_onLayerRemove:function(t){t.layer.getAttribution&&this.removeAttribution(t.layer.getAttribution())}}),o.Map.mergeOptions({attributionControl:!0}),o.Map.addInitHook(function(){this.options.attributionControl&&(this.attributionControl=(new o.Control.Attribution).addTo(this))}),o.control.attribution=function(t){return new o.Control.Attribution(t)},o.Control.Scale=o.Control.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0,updateWhenIdle:!1},onAdd:function(t){this._map=t;var e="leaflet-control-scale",i=o.DomUtil.create("div",e),n=this.options;return this._addScales(n,e,i),t.on(n.updateWhenIdle?"moveend":"move",this._update,this),t.whenReady(this._update,this),i},onRemove:function(t){t.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(t,e,i){t.metric&&(this._mScale=o.DomUtil.create("div",e+"-line",i)),t.imperial&&(this._iScale=o.DomUtil.create("div",e+"-line",i))},_update:function(){var t=this._map.getBounds(),e=t.getCenter().lat,i=6378137*Math.PI*Math.cos(e*Math.PI/180),n=i*(t.getNorthEast().lng-t.getSouthWest().lng)/180,o=this._map.getSize(),s=this.options,a=0;o.x>0&&(a=n*(s.maxWidth/o.x)),this._updateScales(s,a)},_updateScales:function(t,e){t.metric&&e&&this._updateMetric(e),t.imperial&&e&&this._updateImperial(e)},_updateMetric:function(t){var e=this._getRoundNum(t);this._mScale.style.width=this._getScaleWidth(e/t)+"px",this._mScale.innerHTML=1e3>e?e+" m":e/1e3+" km"},_updateImperial:function(t){var e,i,n,o=3.2808399*t,s=this._iScale;o>5280?(e=o/5280,i=this._getRoundNum(e),s.style.width=this._getScaleWidth(i/e)+"px",s.innerHTML=i+" mi"):(n=this._getRoundNum(o),s.style.width=this._getScaleWidth(n/o)+"px",s.innerHTML=n+" ft")},_getScaleWidth:function(t){return Math.round(this.options.maxWidth*t)-10},_getRoundNum:function(t){var e=Math.pow(10,(Math.floor(t)+"").length-1),i=t/e;return i=i>=10?10:i>=5?5:i>=3?3:i>=2?2:1,e*i}}),o.control.scale=function(t){return new o.Control.Scale(t)},o.Control.Layers=o.Control.extend({options:{collapsed:!0,position:"topright",autoZIndex:!0},initialize:function(t,e,i){o.setOptions(this,i),this._layers={},this._lastZIndex=0,this._handlingClick=!1;for(var n in t)this._addLayer(t[n],n);for(n in e)this._addLayer(e[n],n,!0)},onAdd:function(t){return this._initLayout(),this._update(),t.on("layeradd",this._onLayerChange,this).on("layerremove",this._onLayerChange,this),this._container},onRemove:function(t){t.off("layeradd",this._onLayerChange,this).off("layerremove",this._onLayerChange,this)},addBaseLayer:function(t,e){return this._addLayer(t,e),this._update(),this},addOverlay:function(t,e){return this._addLayer(t,e,!0),this._update(),this},removeLayer:function(t){var e=o.stamp(t);return delete this._layers[e],this._update(),this},_initLayout:function(){var t="leaflet-control-layers",e=this._container=o.DomUtil.create("div",t);e.setAttribute("aria-haspopup",!0),o.Browser.touch?o.DomEvent.on(e,"click",o.DomEvent.stopPropagation):o.DomEvent.disableClickPropagation(e).disableScrollPropagation(e);var i=this._form=o.DomUtil.create("form",t+"-list");if(this.options.collapsed){o.Browser.android||o.DomEvent.on(e,"mouseover",this._expand,this).on(e,"mouseout",this._collapse,this);var n=this._layersLink=o.DomUtil.create("a",t+"-toggle",e);n.href="#",n.title="Layers",o.Browser.touch?o.DomEvent.on(n,"click",o.DomEvent.stop).on(n,"click",this._expand,this):o.DomEvent.on(n,"focus",this._expand,this),o.DomEvent.on(i,"click",function(){setTimeout(o.bind(this._onInputClick,this),0)},this),this._map.on("click",this._collapse,this)}else this._expand();this._baseLayersList=o.DomUtil.create("div",t+"-base",i),this._separator=o.DomUtil.create("div",t+"-separator",i),this._overlaysList=o.DomUtil.create("div",t+"-overlays",i),e.appendChild(i)},_addLayer:function(t,e,i){var n=o.stamp(t);this._layers[n]={layer:t,name:e,overlay:i},this.options.autoZIndex&&t.setZIndex&&(this._lastZIndex++,t.setZIndex(this._lastZIndex))},_update:function(){if(this._container){this._baseLayersList.innerHTML="",this._overlaysList.innerHTML="";var t,e,i=!1,n=!1;for(t in this._layers)e=this._layers[t],this._addItem(e),n=n||e.overlay,i=i||!e.overlay;this._separator.style.display=n&&i?"":"none"}},_onLayerChange:function(t){var e=this._layers[o.stamp(t.layer)];if(e){this._handlingClick||this._update();var i=e.overlay?"layeradd"===t.type?"overlayadd":"overlayremove":"layeradd"===t.type?"baselayerchange":null;i&&this._map.fire(i,e)}},_createRadioElement:function(t,i){var n='t;t++)e=n[t],i=this._layers[e.layerId],e.checked&&!this._map.hasLayer(i.layer)?this._map.addLayer(i.layer):!e.checked&&this._map.hasLayer(i.layer)&&this._map.removeLayer(i.layer);this._handlingClick=!1,this._refocusOnMap()},_expand:function(){o.DomUtil.addClass(this._container,"leaflet-control-layers-expanded")},_collapse:function(){this._container.className=this._container.className.replace(" leaflet-control-layers-expanded","")}}),o.control.layers=function(t,e,i){return new o.Control.Layers(t,e,i)},o.PosAnimation=o.Class.extend({includes:o.Mixin.Events,run:function(t,e,i,n){this.stop(),this._el=t,this._inProgress=!0,this._newPos=e,this.fire("start"),t.style[o.DomUtil.TRANSITION]="all "+(i||.25)+"s cubic-bezier(0,0,"+(n||.5)+",1)",o.DomEvent.on(t,o.DomUtil.TRANSITION_END,this._onTransitionEnd,this),o.DomUtil.setPosition(t,e),o.Util.falseFn(t.offsetWidth),this._stepTimer=setInterval(o.bind(this._onStep,this),50)},stop:function(){this._inProgress&&(o.DomUtil.setPosition(this._el,this._getPos()),this._onTransitionEnd(),o.Util.falseFn(this._el.offsetWidth))},_onStep:function(){var t=this._getPos();return t?(this._el._leaflet_pos=t,void this.fire("step")):void this._onTransitionEnd()},_transformRe:/([-+]?(?:\d*\.)?\d+)\D*, ([-+]?(?:\d*\.)?\d+)\D*\)/,_getPos:function(){var e,i,n,s=this._el,a=t.getComputedStyle(s);if(o.Browser.any3d){if(n=a[o.DomUtil.TRANSFORM].match(this._transformRe),!n)return;e=parseFloat(n[1]),i=parseFloat(n[2])}else e=parseFloat(a.left),i=parseFloat(a.top);return new o.Point(e,i,(!0))},_onTransitionEnd:function(){o.DomEvent.off(this._el,o.DomUtil.TRANSITION_END,this._onTransitionEnd,this),this._inProgress&&(this._inProgress=!1,this._el.style[o.DomUtil.TRANSITION]="",this._el._leaflet_pos=this._newPos,clearInterval(this._stepTimer),this.fire("step").fire("end"))}}),o.Map.include({setView:function(t,e,n){if(e=e===i?this._zoom:this._limitZoom(e),t=this._limitCenter(o.latLng(t),e,this.options.maxBounds),n=n||{},this._panAnim&&this._panAnim.stop(),this._loaded&&!n.reset&&n!==!0){n.animate!==i&&(n.zoom=o.extend({animate:n.animate},n.zoom),n.pan=o.extend({animate:n.animate},n.pan));var s=this._zoom!==e?this._tryAnimatedZoom&&this._tryAnimatedZoom(t,e,n.zoom):this._tryAnimatedPan(t,n.pan);if(s)return clearTimeout(this._sizeTimer),this}return this._resetView(t,e),this},panBy:function(t,e){if(t=o.point(t).round(),e=e||{},!t.x&&!t.y)return this;if(this._panAnim||(this._panAnim=new o.PosAnimation,this._panAnim.on({step:this._onPanTransitionStep,end:this._onPanTransitionEnd},this)),e.noMoveStart||this.fire("movestart"),e.animate!==!1){o.DomUtil.addClass(this._mapPane,"leaflet-pan-anim");var i=this._getMapPanePos().subtract(t);this._panAnim.run(this._mapPane,i,e.duration||.25,e.easeLinearity)}else this._rawPanBy(t),this.fire("move").fire("moveend");return this},_onPanTransitionStep:function(){this.fire("move")},_onPanTransitionEnd:function(){o.DomUtil.removeClass(this._mapPane,"leaflet-pan-anim"),this.fire("moveend")},_tryAnimatedPan:function(t,e){var i=this._getCenterOffset(t)._floor();return!((e&&e.animate)!==!0&&!this.getSize().contains(i))&&(this.panBy(i,e),!0)}}),o.PosAnimation=o.DomUtil.TRANSITION?o.PosAnimation:o.PosAnimation.extend({run:function(t,e,i,n){this.stop(),this._el=t,this._inProgress=!0,this._duration=i||.25,this._easeOutPower=1/Math.max(n||.5,.2),this._startPos=o.DomUtil.getPosition(t),this._offset=e.subtract(this._startPos),this._startTime=+new Date,this.fire("start"),this._animate()},stop:function(){this._inProgress&&(this._step(),this._complete())},_animate:function(){this._animId=o.Util.requestAnimFrame(this._animate,this),this._step()},_step:function(){var t=+new Date-this._startTime,e=1e3*this._duration;e>t?this._runFrame(this._easeOut(t/e)):(this._runFrame(1),this._complete())},_runFrame:function(t){var e=this._startPos.add(this._offset.multiplyBy(t));o.DomUtil.setPosition(this._el,e),this.fire("step")},_complete:function(){o.Util.cancelAnimFrame(this._animId),this._inProgress=!1,this.fire("end")},_easeOut:function(t){return 1-Math.pow(1-t,this._easeOutPower)}}),o.Map.mergeOptions({zoomAnimation:!0,zoomAnimationThreshold:4}),o.DomUtil.TRANSITION&&o.Map.addInitHook(function(){this._zoomAnimated=this.options.zoomAnimation&&o.DomUtil.TRANSITION&&o.Browser.any3d&&!o.Browser.android23&&!o.Browser.mobileOpera,this._zoomAnimated&&o.DomEvent.on(this._mapPane,o.DomUtil.TRANSITION_END,this._catchTransitionEnd,this)}),o.Map.include(o.DomUtil.TRANSITION?{_catchTransitionEnd:function(t){this._animatingZoom&&t.propertyName.indexOf("transform")>=0&&this._onZoomTransitionEnd()},_nothingToAnimate:function(){return!this._container.getElementsByClassName("leaflet-zoom-animated").length},_tryAnimatedZoom:function(t,e,i){if(this._animatingZoom)return!0;if(i=i||{},!this._zoomAnimated||i.animate===!1||this._nothingToAnimate()||Math.abs(e-this._zoom)>this.options.zoomAnimationThreshold)return!1;var n=this.getZoomScale(e),o=this._getCenterOffset(t)._divideBy(1-1/n),s=this._getCenterLayerPoint()._add(o);return!(i.animate!==!0&&!this.getSize().contains(o))&&(this.fire("movestart").fire("zoomstart"),this._animateZoom(t,e,s,n,null,!0),!0)},_animateZoom:function(t,e,i,n,s,a,r){r||(this._animatingZoom=!0),o.DomUtil.addClass(this._mapPane,"leaflet-zoom-anim"),this._animateToCenter=t,this._animateToZoom=e,o.Draggable&&(o.Draggable._disabled=!0),o.Util.requestAnimFrame(function(){this.fire("zoomanim",{center:t,zoom:e,origin:i,scale:n,delta:s,backwards:a}),setTimeout(o.bind(this._onZoomTransitionEnd,this),250)},this)},_onZoomTransitionEnd:function(){this._animatingZoom&&(this._animatingZoom=!1,o.DomUtil.removeClass(this._mapPane,"leaflet-zoom-anim"),o.Util.requestAnimFrame(function(){this._resetView(this._animateToCenter,this._animateToZoom,!0,!0),o.Draggable&&(o.Draggable._disabled=!1)},this))}}:{}),o.TileLayer.include({_animateZoom:function(t){this._animating||(this._animating=!0,this._prepareBgBuffer());var e=this._bgBuffer,i=o.DomUtil.TRANSFORM,n=t.delta?o.DomUtil.getTranslateString(t.delta):e.style[i],s=o.DomUtil.getScaleString(t.scale,t.origin);e.style[i]=t.backwards?s+" "+n:n+" "+s},_endZoomAnim:function(){var t=this._tileContainer,e=this._bgBuffer;t.style.visibility="",t.parentNode.appendChild(t),o.Util.falseFn(e.offsetWidth);var i=this._map.getZoom();(i>this.options.maxZoom||i.5&&.5>n?(t.style.visibility="hidden",void this._stopLoadingImages(t)):(e.style.visibility="hidden",e.style[o.DomUtil.TRANSFORM]="",this._tileContainer=e,e=this._bgBuffer=t,this._stopLoadingImages(e),void clearTimeout(this._clearBgBufferTimer))},_getLoadedTilesPercentage:function(t){var e,i,n=t.getElementsByTagName("img"),o=0;for(e=0,i=n.length;i>e;e++)n[e].complete&&o++;return o/i},_stopLoadingImages:function(t){var e,i,n,s=Array.prototype.slice.call(t.getElementsByTagName("img"));for(e=0,i=s.length;i>e;e++)n=s[e],n.complete||(n.onload=o.Util.falseFn,n.onerror=o.Util.falseFn,n.src=o.Util.emptyImageUrl,n.parentNode.removeChild(n))}}),o.Map.include({_defaultLocateOptions:{watch:!1,setView:!1,maxZoom:1/0,timeout:1e4,maximumAge:0,enableHighAccuracy:!1},locate:function(t){if(t=this._locateOptions=o.extend(this._defaultLocateOptions,t),!navigator.geolocation)return this._handleGeolocationError({code:0,message:"Geolocation not supported."}),this;var e=o.bind(this._handleGeolocationResponse,this),i=o.bind(this._handleGeolocationError,this);return t.watch?this._locationWatchId=navigator.geolocation.watchPosition(e,i,t):navigator.geolocation.getCurrentPosition(e,i,t),this},stopLocate:function(){return navigator.geolocation&&navigator.geolocation.clearWatch(this._locationWatchId),this._locateOptions&&(this._locateOptions.setView=!1),this},_handleGeolocationError:function(t){var e=t.code,i=t.message||(1===e?"permission denied":2===e?"position unavailable":"timeout");this._locateOptions.setView&&!this._loaded&&this.fitWorld(),this.fire("locationerror",{code:e,message:"Geolocation error: "+i+"."})},_handleGeolocationResponse:function(t){var e=t.coords.latitude,i=t.coords.longitude,n=new o.LatLng(e,i),s=180*t.coords.accuracy/40075017,a=s/Math.cos(o.LatLng.DEG_TO_RAD*e),r=o.latLngBounds([e-s,i-a],[e+s,i+a]),h=this._locateOptions;if(h.setView){var l=Math.min(this.getBoundsZoom(r),h.maxZoom);this.setView(n,l)}var u={latlng:n,bounds:r,timestamp:t.timestamp};for(var c in t.coords)"number"==typeof t.coords[c]&&(u[c]=t.coords[c]);this.fire("locationfound",u)}})}(window,document)- \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-map.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-map.html.gz new file mode 100644 index 00000000000..b871669338c Binary files /dev/null and b/homeassistant/components/frontend/www_static/panels/ha-panel-map.html.gz differ diff --git a/homeassistant/components/frontend/www_static/partial-map.html.gz b/homeassistant/components/frontend/www_static/partial-map.html.gz deleted file mode 100644 index edcd2f6c62e..00000000000 Binary files a/homeassistant/components/frontend/www_static/partial-map.html.gz and /dev/null differ diff --git a/homeassistant/components/frontend/www_static/service_worker.js b/homeassistant/components/frontend/www_static/service_worker.js index bf7a823bbd0..3337f250031 100644 --- a/homeassistant/components/frontend/www_static/service_worker.js +++ b/homeassistant/components/frontend/www_static/service_worker.js @@ -1,258 +1 @@ -/** - * Copyright 2016 Google Inc. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// This generated service worker JavaScript will precache your site's resources. -// The code needs to be saved in a .js file at the top-level of your site, and registered -// from your pages in order to be used. See -// https://github.com/googlechrome/sw-precache/blob/master/demo/app/js/service-worker-registration.js -// for an example of how you can register this script and handle various service worker events. - -/* eslint-env worker, serviceworker */ -/* eslint-disable indent, no-unused-vars, no-multiple-empty-lines, max-nested-callbacks, space-before-function-paren */ -'use strict'; - - - - - -/* eslint-disable quotes, comma-spacing */ -var PrecacheConfig = [["/","d2c67846acf9a583c29798c30503cbf1"],["/devEvent","c4cdd84093404ee3fe0896070ebde97f"],["/devInfo","c4cdd84093404ee3fe0896070ebde97f"],["/devService","c4cdd84093404ee3fe0896070ebde97f"],["/devState","c4cdd84093404ee3fe0896070ebde97f"],["/devTemplate","c4cdd84093404ee3fe0896070ebde97f"],["/history","d2c67846acf9a583c29798c30503cbf1"],["/logbook","d2c67846acf9a583c29798c30503cbf1"],["/map","df0c87260b6dd990477cda43a2440b1c"],["/states","d2c67846acf9a583c29798c30503cbf1"],["/static/core-7d80cc0e4dea6bc20fa2889be0b3cd15.js","1f35577e9f32a86a03944e5e8d15eab2"],["/static/dev-tools-b7079ac3121b95b9856e5603a6d8a263.html","4ba7c57b48c9d28a1e0d9d7624b83700"],["/static/frontend-805f8dda70419b26daabc8e8f625127f.html","d8eeb403baf5893de8404beec0135d96"],["/static/mdi-758957b7ea989d6beca60e218ea7f7dd.html","4c32b01a3a5b194630963ff7ec4df36f"],["/static/partial-map-c922306de24140afd14f857f927bf8f0.html","853772ea26ac2f4db0f123e20c1ca160"],["static/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["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/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","b0f32ad3c7749c40d486603f31c9d8b1"]]; -/* eslint-enable quotes, comma-spacing */ -var CacheNamePrefix = 'sw-precache-v1--' + (self.registration ? self.registration.scope : '') + '-'; - - -var IgnoreUrlParametersMatching = [/^utm_/]; - - - -var addDirectoryIndex = function (originalUrl, index) { - var url = new URL(originalUrl); - if (url.pathname.slice(-1) === '/') { - url.pathname += index; - } - return url.toString(); - }; - -var getCacheBustedUrl = function (url, param) { - param = param || Date.now(); - - var urlWithCacheBusting = new URL(url); - urlWithCacheBusting.search += (urlWithCacheBusting.search ? '&' : '') + - 'sw-precache=' + param; - - return urlWithCacheBusting.toString(); - }; - -var isPathWhitelisted = function (whitelist, absoluteUrlString) { - // If the whitelist is empty, then consider all URLs to be whitelisted. - if (whitelist.length === 0) { - return true; - } - - // Otherwise compare each path regex to the path of the URL passed in. - var path = (new URL(absoluteUrlString)).pathname; - return whitelist.some(function(whitelistedPathRegex) { - return path.match(whitelistedPathRegex); - }); - }; - -var populateCurrentCacheNames = function (precacheConfig, - cacheNamePrefix, baseUrl) { - var absoluteUrlToCacheName = {}; - var currentCacheNamesToAbsoluteUrl = {}; - - precacheConfig.forEach(function(cacheOption) { - var absoluteUrl = new URL(cacheOption[0], baseUrl).toString(); - var cacheName = cacheNamePrefix + absoluteUrl + '-' + cacheOption[1]; - currentCacheNamesToAbsoluteUrl[cacheName] = absoluteUrl; - absoluteUrlToCacheName[absoluteUrl] = cacheName; - }); - - return { - absoluteUrlToCacheName: absoluteUrlToCacheName, - currentCacheNamesToAbsoluteUrl: currentCacheNamesToAbsoluteUrl - }; - }; - -var stripIgnoredUrlParameters = function (originalUrl, - ignoreUrlParametersMatching) { - var url = new URL(originalUrl); - - url.search = url.search.slice(1) // Exclude initial '?' - .split('&') // Split into an array of 'key=value' strings - .map(function(kv) { - return kv.split('='); // Split each 'key=value' string into a [key, value] array - }) - .filter(function(kv) { - return ignoreUrlParametersMatching.every(function(ignoredRegex) { - return !ignoredRegex.test(kv[0]); // Return true iff the key doesn't match any of the regexes. - }); - }) - .map(function(kv) { - return kv.join('='); // Join each [key, value] array into a 'key=value' string - }) - .join('&'); // Join the array of 'key=value' strings into a string with '&' in between each - - return url.toString(); - }; - - -var mappings = populateCurrentCacheNames(PrecacheConfig, CacheNamePrefix, self.location); -var AbsoluteUrlToCacheName = mappings.absoluteUrlToCacheName; -var CurrentCacheNamesToAbsoluteUrl = mappings.currentCacheNamesToAbsoluteUrl; - -function deleteAllCaches() { - return caches.keys().then(function(cacheNames) { - return Promise.all( - cacheNames.map(function(cacheName) { - return caches.delete(cacheName); - }) - ); - }); -} - -self.addEventListener('install', function(event) { - event.waitUntil( - // Take a look at each of the cache names we expect for this version. - Promise.all(Object.keys(CurrentCacheNamesToAbsoluteUrl).map(function(cacheName) { - return caches.open(cacheName).then(function(cache) { - // Get a list of all the entries in the specific named cache. - // For caches that are already populated for a given version of a - // resource, there should be 1 entry. - return cache.keys().then(function(keys) { - // If there are 0 entries, either because this is a brand new version - // of a resource or because the install step was interrupted the - // last time it ran, then we need to populate the cache. - if (keys.length === 0) { - // Use the last bit of the cache name, which contains the hash, - // as the cache-busting parameter. - // See https://github.com/GoogleChrome/sw-precache/issues/100 - var cacheBustParam = cacheName.split('-').pop(); - var urlWithCacheBusting = getCacheBustedUrl( - CurrentCacheNamesToAbsoluteUrl[cacheName], cacheBustParam); - - var request = new Request(urlWithCacheBusting, - {credentials: 'same-origin'}); - return fetch(request).then(function(response) { - if (response.ok) { - return cache.put(CurrentCacheNamesToAbsoluteUrl[cacheName], - response); - } - - console.error('Request for %s returned a response status %d, ' + - 'so not attempting to cache it.', - urlWithCacheBusting, response.status); - // Get rid of the empty cache if we can't add a successful response to it. - return caches.delete(cacheName); - }); - } - }); - }); - })).then(function() { - return caches.keys().then(function(allCacheNames) { - return Promise.all(allCacheNames.filter(function(cacheName) { - return cacheName.indexOf(CacheNamePrefix) === 0 && - !(cacheName in CurrentCacheNamesToAbsoluteUrl); - }).map(function(cacheName) { - return caches.delete(cacheName); - }) - ); - }); - }).then(function() { - if (typeof self.skipWaiting === 'function') { - // Force the SW to transition from installing -> active state - self.skipWaiting(); - } - }) - ); -}); - -if (self.clients && (typeof self.clients.claim === 'function')) { - self.addEventListener('activate', function(event) { - event.waitUntil(self.clients.claim()); - }); -} - -self.addEventListener('message', function(event) { - if (event.data.command === 'delete_all') { - console.log('About to delete all caches...'); - deleteAllCaches().then(function() { - console.log('Caches deleted.'); - event.ports[0].postMessage({ - error: null - }); - }).catch(function(error) { - console.log('Caches not deleted:', error); - event.ports[0].postMessage({ - error: error - }); - }); - } -}); - - -self.addEventListener('fetch', function(event) { - if (event.request.method === 'GET') { - var urlWithoutIgnoredParameters = stripIgnoredUrlParameters(event.request.url, - IgnoreUrlParametersMatching); - - var cacheName = AbsoluteUrlToCacheName[urlWithoutIgnoredParameters]; - var directoryIndex = 'index.html'; - if (!cacheName && directoryIndex) { - urlWithoutIgnoredParameters = addDirectoryIndex(urlWithoutIgnoredParameters, directoryIndex); - cacheName = AbsoluteUrlToCacheName[urlWithoutIgnoredParameters]; - } - - var navigateFallback = ''; - // Ideally, this would check for event.request.mode === 'navigate', but that is not widely - // supported yet: - // https://code.google.com/p/chromium/issues/detail?id=540967 - // https://bugzilla.mozilla.org/show_bug.cgi?id=1209081 - if (!cacheName && navigateFallback && event.request.headers.has('accept') && - event.request.headers.get('accept').includes('text/html') && - /* eslint-disable quotes, comma-spacing */ - isPathWhitelisted([], event.request.url)) { - /* eslint-enable quotes, comma-spacing */ - var navigateFallbackUrl = new URL(navigateFallback, self.location); - cacheName = AbsoluteUrlToCacheName[navigateFallbackUrl.toString()]; - } - - if (cacheName) { - event.respondWith( - // Rely on the fact that each cache we manage should only have one entry, and return that. - caches.open(cacheName).then(function(cache) { - return cache.keys().then(function(keys) { - return cache.match(keys[0]).then(function(response) { - if (response) { - return response; - } - // If for some reason the response was deleted from the cache, - // raise and exception and fall back to the fetch() triggered in the catch(). - throw Error('The cache ' + cacheName + ' is empty.'); - }); - }); - }).catch(function(e) { - console.warn('Couldn\'t serve response for "%s" from cache: %O', event.request.url, e); - return fetch(event.request); - }) - ); - } - } -}); - - - - +"use strict";function deleteAllCaches(){return caches.keys().then(function(e){return Promise.all(e.map(function(e){return caches.delete(e)}))})}var PrecacheConfig=[["/","2d9bbabfa2dc5f2a651ff1141d7e306c"],["/frontend/panels/dev-event-20327fbd4fb0370aec9be4db26fd723f.html","a9b6eced242c1934a331c05c30e22148"],["/frontend/panels/dev-info-28e0a19ceb95aa714fd53228d9983a49.html","75862082477c802a12d2bf8705990d85"],["/frontend/panels/dev-service-85fd5b48600418bb5a6187539a623c38.html","353e4d80fedbcde9b51e08a78a9ddb86"],["/frontend/panels/dev-state-25d84d7b7aea779bb3bb3cd6c155f8d9.html","7fc5b1880ba4a9d6e97238e8e5a44d69"],["/frontend/panels/dev-template-d079abf61cff9690f828cafb0d29b7e7.html","6e512a2ba0eb7aeba956ca51048e701e"],["/frontend/panels/map-dfe141a3fa5fd403be554def1dd039a9.html","f061ec88561705f7787a00289450c006"],["/static/core-bc78f21f5280217aa2c78dfc5848134f.js","a09b7ee4108fae1f93c10e14a4bfd675"],["/static/frontend-6c52e8cb797bafa3124d936af5ce1fcc.html","a460549fe50b2e7c9cadd94d682c9ed7"],["/static/mdi-f6c6cc64c2ec38a80e91f801b41119b3.html","e010f32322ed6f66916c7c09dbba4acd"],["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","b0f32ad3c7749c40d486603f31c9d8b1"]],CacheNamePrefix="sw-precache-v1--"+(self.registration?self.registration.scope:"")+"-",IgnoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var a=new URL(e);return"/"===a.pathname.slice(-1)&&(a.pathname+=t),a.toString()},getCacheBustedUrl=function(e,t){t=t||Date.now();var a=new URL(e);return a.search+=(a.search?"&":"")+"sw-precache="+t,a.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var a=new URL(t).pathname;return e.some(function(e){return a.match(e)})},populateCurrentCacheNames=function(e,t,a){var n={},c={};return e.forEach(function(e){var r=new URL(e[0],a).toString(),o=t+r+"-"+e[1];c[o]=r,n[r]=o}),{absoluteUrlToCacheName:n,currentCacheNamesToAbsoluteUrl:c}},stripIgnoredUrlParameters=function(e,t){var a=new URL(e);return a.search=a.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("&"),a.toString()},mappings=populateCurrentCacheNames(PrecacheConfig,CacheNamePrefix,self.location),AbsoluteUrlToCacheName=mappings.absoluteUrlToCacheName,CurrentCacheNamesToAbsoluteUrl=mappings.currentCacheNamesToAbsoluteUrl;self.addEventListener("install",function(e){e.waitUntil(Promise.all(Object.keys(CurrentCacheNamesToAbsoluteUrl).map(function(e){return caches.open(e).then(function(t){return t.keys().then(function(a){if(0===a.length){var n=e.split("-").pop(),c=getCacheBustedUrl(CurrentCacheNamesToAbsoluteUrl[e],n),r=new Request(c,{credentials:"same-origin"});return fetch(r).then(function(a){return a.ok?t.put(CurrentCacheNamesToAbsoluteUrl[e],a):(console.error("Request for %s returned a response status %d, so not attempting to cache it.",c,a.status),caches.delete(e))})}})})})).then(function(){return caches.keys().then(function(e){return Promise.all(e.filter(function(e){return 0===e.indexOf(CacheNamePrefix)&&!(e in CurrentCacheNamesToAbsoluteUrl)}).map(function(e){return caches.delete(e)}))})}).then(function(){"function"==typeof self.skipWaiting&&self.skipWaiting()}))}),self.clients&&"function"==typeof self.clients.claim&&self.addEventListener("activate",function(e){e.waitUntil(self.clients.claim())}),self.addEventListener("message",function(e){"delete_all"===e.data.command&&(console.log("About to delete all caches..."),deleteAllCaches().then(function(){console.log("Caches deleted."),e.ports[0].postMessage({error:null})}).catch(function(t){console.log("Caches not deleted:",t),e.ports[0].postMessage({error:t})}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t=stripIgnoredUrlParameters(e.request.url,IgnoreUrlParametersMatching),a=AbsoluteUrlToCacheName[t],n="index.html";!a&&n&&(t=addDirectoryIndex(t,n),a=AbsoluteUrlToCacheName[t]);var c="/";if(!a&&c&&e.request.headers.has("accept")&&e.request.headers.get("accept").includes("text/html")&&isPathWhitelisted(["^((?!(static|api|local|service_worker.js)).)*$"],e.request.url)){var r=new URL(c,self.location);a=AbsoluteUrlToCacheName[r.toString()]}a&&e.respondWith(caches.open(a).then(function(e){return e.keys().then(function(t){return e.match(t[0]).then(function(e){if(e)return e;throw Error("The cache "+a+" is empty.")})})}).catch(function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetch(e.request)}))}}); \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/service_worker.js.gz b/homeassistant/components/frontend/www_static/service_worker.js.gz index 52a3ab5e420..e01b7e8cffe 100644 Binary files a/homeassistant/components/frontend/www_static/service_worker.js.gz and b/homeassistant/components/frontend/www_static/service_worker.js.gz differ diff --git a/homeassistant/components/frontend/www_static/webcomponents-lite.min.js.gz b/homeassistant/components/frontend/www_static/webcomponents-lite.min.js.gz new file mode 100644 index 00000000000..5d805f47bd3 Binary files /dev/null and b/homeassistant/components/frontend/www_static/webcomponents-lite.min.js.gz differ diff --git a/homeassistant/components/garage_door/wink.py b/homeassistant/components/garage_door/wink.py index 964711d679d..68ba1ae709f 100644 --- a/homeassistant/components/garage_door/wink.py +++ b/homeassistant/components/garage_door/wink.py @@ -10,7 +10,7 @@ from homeassistant.components.garage_door import GarageDoorDevice from homeassistant.components.wink import WinkDevice from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2'] +REQUIREMENTS = ['python-wink==0.7.11', 'pubnub==3.8.2'] def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/garage_door/zwave.py b/homeassistant/components/garage_door/zwave.py index b527fc0052c..e8aacf0b6ad 100644 --- a/homeassistant/components/garage_door/zwave.py +++ b/homeassistant/components/garage_door/zwave.py @@ -44,7 +44,6 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, GarageDoorDevice): from openzwave.network import ZWaveNetwork from pydispatch import dispatcher ZWaveDeviceEntity.__init__(self, value, DOMAIN) - self._node = value.node self._state = value.data dispatcher.connect( self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED) @@ -53,7 +52,7 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, GarageDoorDevice): """Called when a value has changed on the network.""" if self._value.value_id == value.value_id: self._state = value.data - self.update_ha_state(True) + self.update_ha_state() _LOGGER.debug("Value changed on network %s", value) @property diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index 6c63f2955f6..9e0c0f897e5 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -12,8 +12,8 @@ import voluptuous as vol import homeassistant.core as ha from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME, - STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_UNKNOWN, - ATTR_ASSUMED_STATE, ) + STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED, + STATE_UNLOCKED, STATE_UNKNOWN, ATTR_ASSUMED_STATE) from homeassistant.helpers.entity import ( Entity, generate_entity_id, split_entity_id) from homeassistant.helpers.event import track_state_change @@ -64,7 +64,7 @@ CONFIG_SCHEMA = vol.Schema({ # List of ON/OFF state tuples for groupable states _GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME), - (STATE_OPEN, STATE_CLOSED)] + (STATE_OPEN, STATE_CLOSED), (STATE_LOCKED, STATE_UNLOCKED)] def _get_group_on_off(state): @@ -304,8 +304,9 @@ class Group(Entity): if gr_on is None: return - if tr_state is None or (gr_state == gr_on and - tr_state.state == gr_off): + if tr_state is None or ((gr_state == gr_on and + tr_state.state == gr_off) or + tr_state.state not in (gr_on, gr_off)): if states is None: states = self._tracking_states diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py index dbd143888f2..9a09f56c474 100644 --- a/homeassistant/components/history.py +++ b/homeassistant/components/history.py @@ -4,13 +4,13 @@ Provide pre-made queries on top of the recorder component. For more details about this component, please refer to the documentation at https://home-assistant.io/components/history/ """ -import re from collections import defaultdict from datetime import timedelta from itertools import groupby import homeassistant.util.dt as dt_util from homeassistant.components import recorder, script +from homeassistant.components.frontend import register_built_in_panel from homeassistant.components.http import HomeAssistantView DOMAIN = 'history' @@ -19,9 +19,6 @@ DEPENDENCIES = ['recorder', 'http'] SIGNIFICANT_DOMAINS = ('thermostat',) IGNORE_DOMAINS = ('zone', 'scene',) -URL_HISTORY_PERIOD = re.compile( - r'/api/history/period(?:/(?P\d{4}-\d{1,2}-\d{1,2})|)') - def last_5_states(entity_id): """Return the last 5 states for entity_id.""" @@ -153,6 +150,7 @@ def setup(hass, config): """Setup the history hooks.""" hass.wsgi.register_view(Last5StatesView) hass.wsgi.register_view(HistoryPeriodView) + register_built_in_panel(hass, 'history', 'History', 'mdi:poll-box') return True @@ -173,14 +171,14 @@ class HistoryPeriodView(HomeAssistantView): url = '/api/history/period' name = 'api:history:view-period' - extra_urls = ['/api/history/period/'] + extra_urls = ['/api/history/period/'] - def get(self, request, date=None): + def get(self, request, datetime=None): """Return history over a period of time.""" one_day = timedelta(days=1) - if date: - start_time = dt_util.as_utc(dt_util.start_of_local_day(date)) + if datetime: + start_time = dt_util.as_utc(datetime) else: start_time = dt_util.utcnow() - one_day diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 593f7696b65..94cad95a3cf 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -17,7 +17,7 @@ from homeassistant.helpers import discovery from homeassistant.config import load_yaml_config_file DOMAIN = 'homematic' -REQUIREMENTS = ["pyhomematic==0.1.9"] +REQUIREMENTS = ["pyhomematic==0.1.10"] HOMEMATIC = None HOMEMATIC_LINK_DELAY = 0.5 diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 71879082862..bce4336b609 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -25,7 +25,7 @@ import homeassistant.util.dt as dt_util import homeassistant.helpers.config_validation as cv DOMAIN = "http" -REQUIREMENTS = ("cherrypy==6.0.2", "static3==0.7.0", "Werkzeug==0.11.10") +REQUIREMENTS = ("cherrypy==6.1.1", "static3==0.7.0", "Werkzeug==0.11.10") CONF_API_PASSWORD = "api_password" CONF_SERVER_HOST = "server_host" @@ -216,9 +216,29 @@ def routing_map(hass): """Convert date to url value.""" return value.isoformat() + class DateTimeValidator(BaseConverter): + """Validate datetimes in urls formatted per ISO 8601.""" + + regex = r'\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d' \ + r'\.\d+([+-][0-2]\d:[0-5]\d|Z)' + + def to_python(self, value): + """Validate and convert date.""" + parsed = dt_util.parse_datetime(value) + + if parsed is None: + raise ValidationError() + + return parsed + + def to_url(self, value): + """Convert date to url value.""" + return value.isoformat() + return Map(converters={ 'entity': EntityValidator, 'date': DateValidator, + 'datetime': DateTimeValidator, }) diff --git a/homeassistant/components/hvac/zwave.py b/homeassistant/components/hvac/zwave.py index 2a9c0726f92..2747cc8eb32 100755 --- a/homeassistant/components/hvac/zwave.py +++ b/homeassistant/components/hvac/zwave.py @@ -98,9 +98,10 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice): def value_changed(self, value): """Called when a value has changed on the network.""" - if self._value.value_id == value.value_id: + if self._value.value_id == value.value_id or \ + self._value.node == value.node: self.update_properties() - self.update_ha_state(True) + self.update_ha_state() _LOGGER.debug("Value changed on network %s", value) def update_properties(self): @@ -135,7 +136,7 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice): class_id=COMMAND_CLASS_CONFIGURATION).values(): if value.command_class == 112 and value.index == 33: self._current_swing_mode = value.data - self._swing_list = [0, 1] + self._swing_list = list(value.data_items) _LOGGER.debug("self._swing_list=%s", self._swing_list) @property @@ -235,5 +236,5 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice): for value in self._node.get_values( class_id=COMMAND_CLASS_CONFIGURATION).values(): if value.command_class == 112 and value.index == 33: - value.data = int(swing_mode) + value.data = bytes(swing_mode, 'utf-8') break diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb.py index 311d3fe83df..2070a52085d 100644 --- a/homeassistant/components/influxdb.py +++ b/homeassistant/components/influxdb.py @@ -33,6 +33,7 @@ CONF_PASSWORD = 'password' CONF_SSL = 'ssl' CONF_VERIFY_SSL = 'verify_ssl' CONF_BLACKLIST = 'blacklist' +CONF_TAGS = 'tags' # pylint: disable=too-many-locals @@ -56,6 +57,7 @@ def setup(hass, config): verify_ssl = util.convert(conf.get(CONF_VERIFY_SSL), bool, DEFAULT_VERIFY_SSL) blacklist = conf.get(CONF_BLACKLIST, []) + tags = conf.get(CONF_TAGS, {}) try: influx = InfluxDBClient(host=host, port=port, username=username, @@ -99,6 +101,9 @@ def setup(hass, config): } ] + for tag in tags: + json_body[0]['tags'][tag] = tags[tag] + try: influx.write_points(json_body) except exceptions.InfluxDBClientError: diff --git a/homeassistant/components/input_slider.py b/homeassistant/components/input_slider.py index b667854e59c..66b41d15c0c 100644 --- a/homeassistant/components/input_slider.py +++ b/homeassistant/components/input_slider.py @@ -34,7 +34,7 @@ SERVICE_SELECT_VALUE = 'select_value' SERVICE_SELECT_VALUE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_VALUE): vol.Coerce(int), + vol.Required(ATTR_VALUE): vol.Coerce(float), }) @@ -152,7 +152,7 @@ class InputSlider(Entity): def select_value(self, value): """Select new value.""" - num_value = int(value) + num_value = float(value) if num_value < self._minimum or num_value > self._maximum: _LOGGER.warning('Invalid value: %s (range %s - %s)', num_value, self._minimum, self._maximum) diff --git a/homeassistant/components/joaoapps_join.py b/homeassistant/components/joaoapps_join.py index 284567b9061..654a13cb269 100644 --- a/homeassistant/components/joaoapps_join.py +++ b/homeassistant/components/joaoapps_join.py @@ -19,26 +19,19 @@ DOMAIN = 'joaoapps_join' CONF_DEVICE_ID = 'device_id' CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [{ vol.Required(CONF_DEVICE_ID): cv.string, vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_API_KEY): cv.string - }) + }]) }, extra=vol.ALLOW_EXTRA) # pylint: disable=too-many-locals -def setup(hass, config): - """Setup Join services.""" - from pyjoin import (get_devices, ring_device, set_wallpaper, send_sms, +def register_device(hass, device_id, api_key, name): + """Method to register services for each join device listed.""" + from pyjoin import (ring_device, set_wallpaper, send_sms, send_file, send_url, send_notification) - device_id = config[DOMAIN].get(CONF_DEVICE_ID) - api_key = config[DOMAIN].get(CONF_API_KEY) - name = config[DOMAIN].get(CONF_NAME) - if api_key: - if not get_devices(api_key): - _LOGGER.error("Error connecting to Join, check API key") - return False def ring_service(service): """Service to ring devices.""" @@ -69,7 +62,6 @@ def setup(hass, config): sms_text=service.data.get('message'), api_key=api_key) - name = name.lower().replace(" ", "_") + "_" if name else "" hass.services.register(DOMAIN, name + 'ring', ring_service) hass.services.register(DOMAIN, name + 'set_wallpaper', set_wallpaper_service) @@ -77,4 +69,19 @@ def setup(hass, config): hass.services.register(DOMAIN, name + 'send_file', send_file_service) hass.services.register(DOMAIN, name + 'send_url', send_url_service) hass.services.register(DOMAIN, name + 'send_tasker', send_tasker_service) + + +def setup(hass, config): + """Setup Join services.""" + from pyjoin import get_devices + for device in config[DOMAIN]: + device_id = device.get(CONF_DEVICE_ID) + api_key = device.get(CONF_API_KEY) + name = device.get(CONF_NAME) + name = name.lower().replace(" ", "_") + "_" if name else "" + if api_key: + if not get_devices(api_key): + _LOGGER.error("Error connecting to Join, check API key") + return False + register_device(hass, device_id, api_key, name) return True diff --git a/homeassistant/components/knx.py b/homeassistant/components/knx.py index 8b43c0f2da9..763b9d81ded 100644 --- a/homeassistant/components/knx.py +++ b/homeassistant/components/knx.py @@ -10,7 +10,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers.entity import Entity DOMAIN = "knx" -REQUIREMENTS = ['knxip==0.3.0'] +REQUIREMENTS = ['knxip==0.3.2'] EVENT_KNX_FRAME_RECEIVED = "knx_frame_received" @@ -45,7 +45,12 @@ def setup(hass, config): KNXTUNNEL = KNXIPTunnel(host, port) try: - KNXTUNNEL.connect() + res = KNXTUNNEL.connect() + _LOGGER.debug("Res = %s", res) + if not res: + _LOGGER.exception("Could not connect to KNX/IP interface %s", host) + return False + except KNXException as ex: _LOGGER.exception("Can't connect to KNX/IP interface: %s", ex) KNXTUNNEL = None @@ -74,7 +79,10 @@ class KNXConfig(object): self.config = config self.should_poll = config.get("poll", True) - self._address = parse_group_address(config.get("address")) + if config.get("address"): + self._address = parse_group_address(config.get("address")) + else: + self._address = None if self.config.get("state_address"): self._state_address = parse_group_address( self.config.get("state_address")) @@ -198,7 +206,7 @@ class KNXGroupAddress(Entity): return False -class KNXMultiAddressDevice(KNXGroupAddress): +class KNXMultiAddressDevice(Entity): """Representation of devices connected to a multiple KNX group address. This is needed for devices like dimmers or shutter actuators as they have @@ -218,18 +226,21 @@ class KNXMultiAddressDevice(KNXGroupAddress): """ from knxip.core import parse_group_address, KNXException - super().__init__(self, hass, config) - - self.config = config + self._config = config + self._state = False + self._data = None + _LOGGER.debug("Initalizing KNX multi address device") # parse required addresses for name in required: + _LOGGER.info(name) paramname = name + "_address" addr = self._config.config.get(paramname) if addr is None: _LOGGER.exception("Required KNX group address %s missing", paramname) - raise KNXException("Group address missing in configuration") + raise KNXException("Group address for %s missing " + "in configuration", paramname) addr = parse_group_address(addr) self.names[addr] = name @@ -244,23 +255,25 @@ class KNXMultiAddressDevice(KNXGroupAddress): _LOGGER.exception("Cannot parse group address %s", addr) self.names[addr] = name - def handle_frame(frame): - """Handle an incoming KNX frame. + @property + def name(self): + """The entity's display name.""" + return self._config.name - Handle an incoming frame and update our status if it contains - information relating to this device. - """ - addr = frame.data[0] + @property + def config(self): + """The entity's configuration.""" + return self._config - if addr in self.names: - self.values[addr] = frame.data[1] - self.update_ha_state() + @property + def should_poll(self): + """Return the state of the polling, if needed.""" + return self._config.should_poll - hass.bus.listen(EVENT_KNX_FRAME_RECEIVED, handle_frame) - - def group_write_address(self, name, value): - """Write to the group address with the given name.""" - KNXTUNNEL.group_write(self.address, [value]) + @property + def cache(self): + """The name given to the entity.""" + return self._config.config.get("cache", True) def has_attribute(self, name): """Check if the attribute with the given name is defined. @@ -277,7 +290,7 @@ class KNXMultiAddressDevice(KNXGroupAddress): from knxip.core import KNXException addr = None - for attributename, attributeaddress in self.names.items(): + for attributeaddress, attributename in self.names.items(): if attributename == name: addr = attributeaddress @@ -293,3 +306,25 @@ class KNXMultiAddressDevice(KNXGroupAddress): return False return res + + def set_value(self, name, value): + """Set the value of a given named attribute.""" + from knxip.core import KNXException + + addr = None + for attributeaddress, attributename in self.names.items(): + if attributename == name: + addr = attributeaddress + + if addr is None: + _LOGGER.exception("Attribute %s undefined", name) + return False + + try: + KNXTUNNEL.group_write(addr, value) + except KNXException: + _LOGGER.exception("Unable to write to KNX address: %s", + addr) + return False + + return True diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py new file mode 100644 index 00000000000..27b2ca2249e --- /dev/null +++ b/homeassistant/components/light/flux_led.py @@ -0,0 +1,109 @@ +""" +Support for Flux lights. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.flux_led/ +""" + +import logging +import socket +import voluptuous as vol + +from homeassistant.components.light import Light +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.3.zip' + '#flux_led==0.3'] + +_LOGGER = logging.getLogger(__name__) +DOMAIN = "flux_led" +ATTR_NAME = 'name' + +DEVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_NAME): cv.string, +}) + +PLATFORM_SCHEMA = vol.Schema({ + vol.Required('platform'): DOMAIN, + vol.Optional('devices', default={}): {cv.string: DEVICE_SCHEMA}, + vol.Optional('automatic_add', default=False): cv.boolean, +}, extra=vol.ALLOW_EXTRA) + + +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """Setup the Flux lights.""" + import flux_led + lights = [] + light_ips = [] + for ipaddr, device_config in config["devices"].items(): + device = {} + device['id'] = device_config[ATTR_NAME] + device['ipaddr'] = ipaddr + light = FluxLight(device) + if light.is_valid: + lights.append(light) + light_ips.append(ipaddr) + + if not config['automatic_add']: + add_devices_callback(lights) + return + + # Find the bulbs on the LAN + scanner = flux_led.BulbScanner() + scanner.scan(timeout=20) + for device in scanner.getBulbInfo(): + light = FluxLight(device) + ipaddr = device['ipaddr'] + if light.is_valid and ipaddr not in light_ips: + lights.append(light) + light_ips.append(ipaddr) + + add_devices_callback(lights) + + +class FluxLight(Light): + """Representation of a Flux light.""" + + # pylint: disable=too-many-arguments + def __init__(self, device): + """Initialize the light.""" + import flux_led + + self._name = device['id'] + self._ipaddr = device['ipaddr'] + self.is_valid = True + self._bulb = None + try: + self._bulb = flux_led.WifiLedBulb(self._ipaddr) + except socket.error: + self.is_valid = False + _LOGGER.error("Failed to connect to bulb %s, %s", + self._ipaddr, self._name) + + @property + def unique_id(self): + """Return the ID of this light.""" + return "{}.{}".format( + self.__class__, self._ipaddr) + + @property + def name(self): + """Return the name of the device if any.""" + return self._name + + @property + def is_on(self): + """Return true if device is on.""" + return self._bulb.isOn() + + def turn_on(self, **kwargs): + """Turn the specified or all lights on.""" + self._bulb.turnOn() + + def turn_off(self, **kwargs): + """Turn the specified or all lights off.""" + self._bulb.turnOff() + + def update(self): + """Synchronize state with bulb.""" + self._bulb.refreshState() diff --git a/homeassistant/components/light/hyperion.py b/homeassistant/components/light/hyperion.py index cfbe11a07bd..7007f3dec34 100644 --- a/homeassistant/components/light/hyperion.py +++ b/homeassistant/components/light/hyperion.py @@ -19,24 +19,24 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Setup a Hyperion server remote.""" host = config.get(CONF_HOST, None) port = config.get("port", 19444) - device = Hyperion(config.get('name', host), host, port) + default_color = config.get("default_color", [255, 255, 255]) + device = Hyperion(config.get('name', host), host, port, default_color) if device.setup(): add_devices_callback([device]) return True - else: - return False + return False class Hyperion(Light): """Representation of a Hyperion remote.""" - def __init__(self, name, host, port): + def __init__(self, name, host, port, default_color): """Initialize the light.""" self._host = host self._port = port self._name = name - self._is_available = True - self._rgb_color = [255, 255, 255] + self._default_color = default_color + self._rgb_color = [0, 0, 0] @property def name(self): @@ -50,38 +50,43 @@ class Hyperion(Light): @property def is_on(self): - """Return true if the device is online.""" - return self._is_available + """Return true if not black.""" + return self._rgb_color != [0, 0, 0] def turn_on(self, **kwargs): """Turn the lights on.""" - if self._is_available: - if ATTR_RGB_COLOR in kwargs: - self._rgb_color = kwargs[ATTR_RGB_COLOR] + if ATTR_RGB_COLOR in kwargs: + self._rgb_color = kwargs[ATTR_RGB_COLOR] + else: + self._rgb_color = self._default_color - self.json_request({"command": "color", "priority": 128, - "color": self._rgb_color}) + self.json_request({"command": "color", "priority": 128, + "color": self._rgb_color}) def turn_off(self, **kwargs): - """Disconnect the remote.""" + """Disconnect all remotes.""" self.json_request({"command": "clearall"}) + self._rgb_color = [0, 0, 0] def update(self): - """Ping the remote.""" - # just see if the remote port is open - self._is_available = self.json_request() + """Get the remote's active color.""" + response = self.json_request({"command": "serverinfo"}) + if response: + if response["info"]["activeLedColor"] == []: + self._rgb_color = [0, 0, 0] + else: + self._rgb_color =\ + response["info"]["activeLedColor"][0]["RGB Value"] def setup(self): """Get the hostname of the remote.""" response = self.json_request({"command": "serverinfo"}) if response: - if self._name == self._host: - self._name = response["info"]["hostname"] + self._name = response["info"]["hostname"] return True - return False - def json_request(self, request=None, wait_for_response=False): + def json_request(self, request, wait_for_response=False): """Communicate with the JSON server.""" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(5) @@ -92,11 +97,6 @@ class Hyperion(Light): sock.close() return False - if not request: - # No communication needed, simple presence detection returns True - sock.close() - return True - sock.send(bytearray(json.dumps(request) + "\n", "utf-8")) try: buf = sock.recv(4096) diff --git a/homeassistant/components/light/qwikswitch.py b/homeassistant/components/light/qwikswitch.py index e0681e68b87..5612f41c942 100644 --- a/homeassistant/components/light/qwikswitch.py +++ b/homeassistant/components/light/qwikswitch.py @@ -1,35 +1,21 @@ -""" -Support for Qwikswitch Relays and Dimmers. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.qwikswitch/ -""" -import logging -import homeassistant.components.qwikswitch as qwikswitch -from homeassistant.components.light import Light - -DEPENDENCIES = ['qwikswitch'] - - -class QSLight(qwikswitch.QSToggleEntity, Light): - """Light based on a Qwikswitch relay/dimmer module.""" - - pass - - -# pylint: disable=unused-argument -def setup_platform(hass, config, add_devices, discovery_info=None): - """Store add_devices for the light components.""" - if discovery_info is None or 'qsusb_id' not in discovery_info: - logging.getLogger(__name__).error( - 'Configure main Qwikswitch component') - return False - - qsusb = qwikswitch.QSUSB[discovery_info['qsusb_id']] - - for item in qsusb.ha_devices: - if item['type'] not in ['dim', 'rel']: - continue - dev = QSLight(item, qsusb) - add_devices([dev]) - qsusb.ha_objects[item['id']] = dev +""" +Support for Qwikswitch Relays and Dimmers. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.qwikswitch/ +""" +import logging +import homeassistant.components.qwikswitch as qwikswitch + +DEPENDENCIES = ['qwikswitch'] + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Add lights from the main Qwikswitch component.""" + if discovery_info is None: + logging.getLogger(__name__).error('Configure Qwikswitch Component.') + return False + + add_devices(qwikswitch.QSUSB['light']) + return True diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index aa39fb03536..142ccbc5b22 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -6,10 +6,8 @@ https://home-assistant.io/components/light.vera/ """ import logging -import homeassistant.util.dt as dt_util from homeassistant.components.light import ATTR_BRIGHTNESS, Light from homeassistant.const import ( - ATTR_ARMED, ATTR_BATTERY_LEVEL, ATTR_LAST_TRIP_TIME, ATTR_TRIPPED, STATE_OFF, STATE_ON) from homeassistant.components.vera import ( VeraDevice, VERA_DEVICES, VERA_CONTROLLER) @@ -56,31 +54,6 @@ class VeraLight(VeraDevice, Light): self._state = STATE_OFF self.update_ha_state() - @property - def device_state_attributes(self): - """Return the state attributes.""" - attr = {} - - if self.vera_device.has_battery: - attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%' - - if self.vera_device.is_armable: - armed = self.vera_device.is_armed - attr[ATTR_ARMED] = 'True' if armed else 'False' - - if self.vera_device.is_trippable: - last_tripped = self.vera_device.last_trip - if last_tripped is not None: - utc_time = dt_util.utc_from_timestamp(int(last_tripped)) - attr[ATTR_LAST_TRIP_TIME] = utc_time.isoformat() - else: - attr[ATTR_LAST_TRIP_TIME] = None - tripped = self.vera_device.is_tripped - attr[ATTR_TRIPPED] = 'True' if tripped else 'False' - - attr['Vera Device Id'] = self.vera_device.vera_device_id - return attr - @property def is_on(self): """Return true if device is on.""" diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index 4fb009d23f0..c4a4e1bee1b 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -14,7 +14,7 @@ from homeassistant.util import color as color_util from homeassistant.util.color import \ color_temperature_mired_to_kelvin as mired_to_kelvin -REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2'] +REQUIREMENTS = ['python-wink==0.7.11', 'pubnub==3.8.2'] def setup_platform(hass, config, add_devices_callback, discovery_info=None): diff --git a/homeassistant/components/light/x10.py b/homeassistant/components/light/x10.py new file mode 100644 index 00000000000..a689d7604ba --- /dev/null +++ b/homeassistant/components/light/x10.py @@ -0,0 +1,80 @@ +""" +Support for X10 lights. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.x10/ +""" +import logging +from subprocess import check_output, CalledProcessError, STDOUT +from homeassistant.components.light import ATTR_BRIGHTNESS, Light + +_LOGGER = logging.getLogger(__name__) + + +def x10_command(command): + """Execute X10 command and check output.""" + return check_output(["heyu"] + command.split(' '), stderr=STDOUT) + + +def get_status(): + """Get on/off status for all x10 units in default housecode.""" + output = check_output("heyu info | grep monitored", shell=True) + return output.decode('utf-8').split(' ')[-1].strip('\n()') + + +def get_unit_status(code): + """Get on/off status for given unit.""" + unit = int(code[1]) + return get_status()[16 - int(unit)] == '1' + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the x10 Light platform.""" + try: + x10_command("info") + except CalledProcessError as err: + _LOGGER.error(err.output) + return False + + add_devices(X10Light(light) for light in config['lights']) + + +class X10Light(Light): + """Representation of an X10 Light.""" + + def __init__(self, light): + """Initialize an X10 Light.""" + self._name = light['name'] + self._id = light['id'] + self._is_on = False + self._brightness = 0 + + @property + def name(self): + """Return the display name of this light.""" + return self._name + + @property + def brightness(self): + """Brightness of the light (an integer in the range 1-255).""" + return self._brightness + + @property + def is_on(self): + """Return true if light is on.""" + return self._is_on + + def turn_on(self, **kwargs): + """Instruct the light to turn on.""" + x10_command("on " + self._id) + self._brightness = kwargs.get(ATTR_BRIGHTNESS, 255) + self._is_on = True + + def turn_off(self, **kwargs): + """Instruct the light to turn off.""" + x10_command("off " + self._id) + self._is_on = False + + def update(self): + """Fetch new state data for this light.""" + self._is_on = get_unit_status(self._id) diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index 2aed8d54a8c..ab41381f5e7 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -8,7 +8,6 @@ import logging # Because we do not compile openzwave on CI # pylint: disable=import-error -from threading import Timer from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \ ATTR_RGB_COLOR, DOMAIN, Light from homeassistant.components import zwave @@ -107,25 +106,10 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light): def _value_changed(self, value): """Called when a value has changed on the network.""" - if self._value.value_id != value.value_id: - return - - if self._refreshing: - self._refreshing = False + if self._value.value_id == value.value_id or \ + self._value.node == value.node: self.update_properties() - else: - def _refresh_value(): - """Used timer callback for delayed value refresh.""" - self._refreshing = True - self._value.refresh() - - if self._timer is not None and self._timer.isAlive(): - self._timer.cancel() - - self._timer = Timer(2, _refresh_value) - self._timer.start() - - self.update_ha_state() + self.update_ha_state() @property def brightness(self): diff --git a/homeassistant/components/lock/vera.py b/homeassistant/components/lock/vera.py index f10b8857499..6ac8fb2c315 100644 --- a/homeassistant/components/lock/vera.py +++ b/homeassistant/components/lock/vera.py @@ -8,7 +8,7 @@ import logging from homeassistant.components.lock import LockDevice from homeassistant.const import ( - ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED) + STATE_LOCKED, STATE_UNLOCKED) from homeassistant.components.vera import ( VeraDevice, VERA_DEVICES, VERA_CONTROLLER) @@ -32,16 +32,6 @@ class VeraLock(VeraDevice, LockDevice): self._state = None VeraDevice.__init__(self, vera_device, controller) - @property - def device_state_attributes(self): - """Return the state attributes of the device.""" - attr = {} - if self.vera_device.has_battery: - attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%' - - attr['Vera Device Id'] = self.vera_device.vera_device_id - return attr - def lock(self, **kwargs): """Lock the device.""" self.vera_device.lock() diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/lock/wink.py index 7b3b5ef0fec..c85655cbf35 100644 --- a/homeassistant/components/lock/wink.py +++ b/homeassistant/components/lock/wink.py @@ -10,7 +10,7 @@ from homeassistant.components.lock import LockDevice from homeassistant.components.wink import WinkDevice from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2'] +REQUIREMENTS = ['python-wink==0.7.11', 'pubnub==3.8.2'] def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/lock/zwave.py b/homeassistant/components/lock/zwave.py index 9a3b24deb8a..7416d5c439b 100644 --- a/homeassistant/components/lock/zwave.py +++ b/homeassistant/components/lock/zwave.py @@ -46,7 +46,8 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice): def _value_changed(self, value): """Called when a value has changed on the network.""" - if self._value.value_id == value.value_id: + if self._value.value_id == value.value_id or \ + self._value.node == value.node: self._state = value.data self.update_ha_state() diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index c6cec168aed..6508318a907 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -5,7 +5,6 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/logbook/ """ import logging -import re from datetime import timedelta from itertools import groupby @@ -14,6 +13,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util from homeassistant.components import recorder, sun +from homeassistant.components.frontend import register_built_in_panel from homeassistant.components.http import HomeAssistantView from homeassistant.const import (EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, @@ -24,9 +24,7 @@ from homeassistant.helpers import template from homeassistant.helpers.entity import split_entity_id DOMAIN = "logbook" -DEPENDENCIES = ['recorder', 'http'] - -URL_LOGBOOK = re.compile(r'/api/logbook(?:/(?P\d{4}-\d{1,2}-\d{1,2})|)') +DEPENDENCIES = ['recorder', 'frontend'] _LOGGER = logging.getLogger(__name__) @@ -75,6 +73,9 @@ def setup(hass, config): hass.wsgi.register_view(LogbookView) + register_built_in_panel(hass, 'logbook', 'Logbook', + 'mdi:format-list-bulleted-type') + hass.services.register(DOMAIN, 'log', log_message, schema=LOG_MESSAGE_SCHEMA) return True @@ -85,16 +86,11 @@ class LogbookView(HomeAssistantView): url = '/api/logbook' name = 'api:logbook' - extra_urls = ['/api/logbook/'] + extra_urls = ['/api/logbook/'] - def get(self, request, date=None): + def get(self, request, datetime=None): """Retrieve logbook entries.""" - if date: - start_day = dt_util.start_of_local_day(date) - else: - start_day = dt_util.start_of_local_day() - - start_day = dt_util.as_utc(start_day) + start_day = dt_util.as_utc(datetime or dt_util.start_of_local_day()) end_day = start_day + timedelta(days=1) events = recorder.get_model('Events') diff --git a/homeassistant/components/media_player/directv.py b/homeassistant/components/media_player/directv.py new file mode 100644 index 00000000000..7a32f02dc56 --- /dev/null +++ b/homeassistant/components/media_player/directv.py @@ -0,0 +1,172 @@ +"""Support for the DirecTV recievers.""" + +from homeassistant.components.media_player import ( + MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, + SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_STOP, + SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, MediaPlayerDevice) +from homeassistant.const import ( + CONF_HOST, CONF_NAME, STATE_OFF, STATE_PLAYING) + +REQUIREMENTS = ['directpy==0.1'] + +DEFAULT_PORT = 8080 + +SUPPORT_DTV = SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ + SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_NEXT_TRACK | \ + SUPPORT_PREVIOUS_TRACK + +KNOWN_HOSTS = [] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the DirecTV platform.""" + hosts = [] + + if discovery_info and discovery_info in KNOWN_HOSTS: + return + + if discovery_info is not None: + hosts.append([ + 'DirecTV_' + discovery_info[1], + discovery_info[0], + DEFAULT_PORT + ]) + + elif CONF_HOST in config: + hosts.append([ + config.get(CONF_NAME, 'DirecTV Receiver'), + config[CONF_HOST], DEFAULT_PORT + ]) + + dtvs = [] + + for host in hosts: + dtvs.append(DirecTvDevice(*host)) + KNOWN_HOSTS.append(host) + + add_devices(dtvs) + + return True + + +class DirecTvDevice(MediaPlayerDevice): + """Representation of a DirecTV reciever on the network.""" + + # pylint: disable=abstract-method + # pylint: disable=too-many-public-methods + def __init__(self, name, host, port): + """Initialize the device.""" + from DirectPy import DIRECTV + self.dtv = DIRECTV(host, port) + self._name = name + self._is_standby = True + self._current = None + + def update(self): + """Retrieve latest state.""" + self._is_standby = self.dtv.get_standby() + if self._is_standby: + self._current = None + else: + self._current = self.dtv.get_tuned() + + @property + def name(self): + """Return the name of the device.""" + return self._name + + # MediaPlayerDevice properties and methods + @property + def state(self): + """Return the state of the device.""" + if self._is_standby: + return STATE_OFF + # haven't determined a way to see if the content is paused + else: + return STATE_PLAYING + + @property + def media_content_id(self): + """Content ID of current playing media.""" + if self._is_standby: + return None + else: + return self._current['programId'] + + @property + def media_duration(self): + """Duration of current playing media in seconds.""" + if self._is_standby: + return None + else: + return self._current['duration'] + + @property + def media_title(self): + """Title of current playing media.""" + if self._is_standby: + return None + else: + return self._current['title'] + + @property + def media_series_title(self): + """Title of current episode of TV show.""" + if self._is_standby: + return None + else: + if 'episodeTitle' in self._current: + return self._current['episodeTitle'] + else: + return None + + @property + def supported_media_commands(self): + """Flag of media commands that are supported.""" + return SUPPORT_DTV + + @property + def media_content_type(self): + """Content type of current playing media.""" + if 'episodeTitle' in self._current: + return MEDIA_TYPE_TVSHOW + else: + return MEDIA_TYPE_VIDEO + + @property + def media_channel(self): + """Channel current playing media.""" + if self._is_standby: + return None + else: + chan = "{} ({})".format(self._current['callsign'], + self._current['major']) + return chan + + def turn_on(self): + """Turn on the reciever.""" + self.dtv.key_press('poweron') + + def turn_off(self): + """Turn off the reciever.""" + self.dtv.key_press('poweroff') + + def media_play(self): + """Send play commmand.""" + self.dtv.key_press('play') + + def media_pause(self): + """Send pause commmand.""" + self.dtv.key_press('pause') + + def media_stop(self): + """Send stop commmand.""" + self.dtv.key_press('stop') + + def media_previous_track(self): + """Send rewind commmand.""" + self.dtv.key_press('rew') + + def media_next_track(self): + """Send fast forward commmand.""" + self.dtv.key_press('ffwd') diff --git a/homeassistant/components/media_player/mpchc.py b/homeassistant/components/media_player/mpchc.py new file mode 100644 index 00000000000..8f551f8ae8f --- /dev/null +++ b/homeassistant/components/media_player/mpchc.py @@ -0,0 +1,153 @@ +""" +Support to interface with the MPC-HC Web API. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.mpchc/ +""" +import logging +import re +import requests + +from homeassistant.components.media_player import ( + SUPPORT_VOLUME_MUTE, SUPPORT_PAUSE, SUPPORT_STOP, SUPPORT_NEXT_TRACK, + SUPPORT_PREVIOUS_TRACK, SUPPORT_VOLUME_STEP, MediaPlayerDevice) +from homeassistant.const import ( + STATE_OFF, STATE_IDLE, STATE_PAUSED, STATE_PLAYING) + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_MPCHC = SUPPORT_VOLUME_MUTE | SUPPORT_PAUSE | SUPPORT_STOP | \ + SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_VOLUME_STEP + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the MPC-HC platform.""" + name = config.get("name", "MPC-HC") + url = '{}:{}'.format(config.get('host'), config.get('port', '13579')) + + if config.get('host') is None: + _LOGGER.error("Missing NPC-HC host address in config") + return False + + add_devices([MpcHcDevice(name, url)]) + + +# pylint: disable=abstract-method +class MpcHcDevice(MediaPlayerDevice): + """Representation of a MPC-HC server.""" + + def __init__(self, name, url): + """Initialize the MPC-HC device.""" + self._name = name + self._url = url + + self.update() + + def update(self): + """Get the latest details.""" + self._player_variables = dict() + + try: + response = requests.get("{}/variables.html".format(self._url), + data=None, timeout=3) + + mpchc_variables = re.findall(r'

(.+?)

', + response.text) + + self._player_variables = dict() + for var in mpchc_variables: + self._player_variables[var[0]] = var[1].lower() + except requests.exceptions.RequestException: + _LOGGER.error("Could not connect to MPC-HC at: %s", self._url) + + def _send_command(self, command_id): + """Send a command to MPC-HC via its window message ID.""" + try: + params = {"wm_command": command_id} + requests.get("{}/command.html".format(self._url), + params=params, timeout=3) + except requests.exceptions.RequestException: + _LOGGER.error("Could not send command %d to MPC-HC at: %s", + command_id, self._url) + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + state = self._player_variables.get('statestring', None) + + if state is None: + return STATE_OFF + if state == 'playing': + return STATE_PLAYING + elif state == 'paused': + return STATE_PAUSED + else: + return STATE_IDLE + + @property + def media_title(self): + """Title of current playing media.""" + return self._player_variables.get('file', None) + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + return int(self._player_variables.get('volumelevel', 0)) / 100.0 + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._player_variables.get('muted', '0') == '1' + + @property + def media_duration(self): + """Duration of current playing media in seconds.""" + duration = self._player_variables.get('durationstring', + "00:00:00").split(':') + return \ + int(duration[0]) * 3600 + \ + int(duration[1]) * 60 + \ + int(duration[2]) + + @property + def supported_media_commands(self): + """Flag of media commands that are supported.""" + return SUPPORT_MPCHC + + def volume_up(self): + """Volume up the media player.""" + self._send_command(907) + + def volume_down(self): + """Volume down media player.""" + self._send_command(908) + + def mute_volume(self, mute): + """Mute the volume.""" + self._send_command(909) + + def media_play(self): + """Send play command.""" + self._send_command(887) + + def media_pause(self): + """Send pause command.""" + self._send_command(888) + + def media_stop(self): + """Send stop command.""" + self._send_command(890) + + def media_next_track(self): + """Send next track command.""" + self._send_command(921) + + def media_previous_track(self): + """Send previous track command.""" + self._send_command(920) diff --git a/homeassistant/components/media_player/russound_rnet.py b/homeassistant/components/media_player/russound_rnet.py new file mode 100644 index 00000000000..a0405f3f531 --- /dev/null +++ b/homeassistant/components/media_player/russound_rnet.py @@ -0,0 +1,118 @@ +""" +Support for interfacing with Russound via RNET Protocol. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.russound_rnet/ +""" +import logging + +from homeassistant.components.media_player import ( + SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_SELECT_SOURCE, MediaPlayerDevice) +from homeassistant.const import ( + CONF_HOST, CONF_PORT, STATE_OFF, STATE_ON) + +REQUIREMENTS = [ + 'https://github.com/laf/russound/archive/0.1.6.zip' + '#russound==0.1.6'] + +ZONES = 'zones' +SOURCES = 'sources' + +SUPPORT_RUSSOUND = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \ + SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Russound RNET platform.""" + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + keypad = config.get('keypad', '70') + + if host is None or port is None: + _LOGGER.error('Invalid config. Expected %s and %s', + CONF_HOST, CONF_PORT) + return False + + from russound import russound + + russ = russound.Russound(host, port) + russ.connect(keypad) + + sources = [] + for source in config[SOURCES]: + sources.append(source['name']) + + if russ.is_connected(): + for zone_id, extra in config[ZONES].items(): + add_devices([RussoundRNETDevice(hass, russ, sources, zone_id, + extra)]) + else: + _LOGGER.error('Not connected to %s:%s', host, port) + + +# pylint: disable=abstract-method, too-many-public-methods, +# pylint: disable=too-many-instance-attributes, too-many-arguments +class RussoundRNETDevice(MediaPlayerDevice): + """Representation of a Russound RNET device.""" + + def __init__(self, hass, russ, sources, zone_id, extra): + """Initialise the Russound RNET device.""" + self._name = extra['name'] + self._russ = russ + self._state = STATE_OFF + self._sources = sources + self._zone_id = zone_id + self._volume = 0 + + @property + def name(self): + """Return the name of the zone.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def supported_media_commands(self): + """Flag of media commands that are supported.""" + return SUPPORT_RUSSOUND + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + return self._volume + + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + self._volume = volume * 100 + self._russ.set_volume('1', self._zone_id, self._volume) + + def turn_on(self): + """Turn the media player on.""" + self._russ.set_power('1', self._zone_id, '1') + self._state = STATE_ON + + def turn_off(self): + """Turn off media player.""" + self._russ.set_power('1', self._zone_id, '0') + self._state = STATE_OFF + + def mute_volume(self, mute): + """Send mute command.""" + self._russ.toggle_mute('1', self._zone_id) + + def select_source(self, source): + """Set the input source.""" + if source in self._sources: + index = self._sources.index(source)+1 + self._russ.set_source('1', self._zone_id, index) + + @property + def source_list(self): + """List of available input sources.""" + return self._sources diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index b4ad1c8d388..62b2aeabf51 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -6,8 +6,9 @@ https://home-assistant.io/components/media_player.sonos/ """ import datetime import logging -import socket from os import path +import socket +import voluptuous as vol from homeassistant.components.media_player import ( ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, @@ -15,8 +16,10 @@ from homeassistant.components.media_player import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST, SUPPORT_SELECT_SOURCE, MediaPlayerDevice) from homeassistant.const import ( - STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_OFF) + STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_OFF, + ATTR_ENTITY_ID) from homeassistant.config import load_yaml_config_file +import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['SoCo==0.11.1'] @@ -43,16 +46,28 @@ SUPPORT_SOURCE_LINEIN = 'Line-in' SUPPORT_SOURCE_TV = 'TV' SUPPORT_SOURCE_RADIO = 'Radio' +SONOS_SCHEMA = vol.Schema({ + ATTR_ENTITY_ID: cv.entity_ids, +}) + +# List of devices that have been registered +DEVICES = [] + # pylint: disable=unused-argument, too-many-locals def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Sonos platform.""" import soco + global DEVICES if discovery_info: player = soco.SoCo(discovery_info) if player.is_visible: - add_devices([SonosDevice(hass, player)]) + device = SonosDevice(hass, player) + add_devices([device]) + if not DEVICES: + register_services(hass) + DEVICES.append(device) return True return False @@ -74,60 +89,72 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.warning('No Sonos speakers found.') return False - devices = [SonosDevice(hass, p) for p in players] - add_devices(devices) + DEVICES = [SonosDevice(hass, p) for p in players] + add_devices(DEVICES) + register_services(hass) _LOGGER.info('Added %s Sonos speakers', len(players)) + return True - def _apply_service(service, service_func, *service_func_args): - """Internal func for applying a service.""" - entity_id = service.data.get('entity_id') - - if entity_id: - _devices = [device for device in devices - if device.entity_id == entity_id] - else: - _devices = devices - - for device in _devices: - service_func(device, *service_func_args) - device.update_ha_state(True) - - def group_players_service(service): - """Group media players, use player as coordinator.""" - _apply_service(service, SonosDevice.group_players) - - def unjoin_service(service): - """Unjoin the player from a group.""" - _apply_service(service, SonosDevice.unjoin) - - def snapshot_service(service): - """Take a snapshot.""" - _apply_service(service, SonosDevice.snapshot) - - def restore_service(service): - """Restore a snapshot.""" - _apply_service(service, SonosDevice.restore) +def register_services(hass): + """Register all services for sonos devices.""" descriptions = load_yaml_config_file( path.join(path.dirname(__file__), 'services.yaml')) hass.services.register(DOMAIN, SERVICE_GROUP_PLAYERS, - group_players_service, - descriptions.get(SERVICE_GROUP_PLAYERS)) + _group_players_service, + descriptions.get(SERVICE_GROUP_PLAYERS), + schema=SONOS_SCHEMA) hass.services.register(DOMAIN, SERVICE_UNJOIN, - unjoin_service, - descriptions.get(SERVICE_UNJOIN)) + _unjoin_service, + descriptions.get(SERVICE_UNJOIN), + schema=SONOS_SCHEMA) hass.services.register(DOMAIN, SERVICE_SNAPSHOT, - snapshot_service, - descriptions.get(SERVICE_SNAPSHOT)) + _snapshot_service, + descriptions.get(SERVICE_SNAPSHOT), + schema=SONOS_SCHEMA) hass.services.register(DOMAIN, SERVICE_RESTORE, - restore_service, - descriptions.get(SERVICE_RESTORE)) + _restore_service, + descriptions.get(SERVICE_RESTORE), + schema=SONOS_SCHEMA) - return True + +def _apply_service(service, service_func, *service_func_args): + """Internal func for applying a service.""" + entity_ids = service.data.get('entity_id') + + if entity_ids: + _devices = [device for device in DEVICES + if device.entity_id in entity_ids] + else: + _devices = DEVICES + + for device in _devices: + service_func(device, *service_func_args) + device.update_ha_state(True) + + +def _group_players_service(service): + """Group media players, use player as coordinator.""" + _apply_service(service, SonosDevice.group_players) + + +def _unjoin_service(service): + """Unjoin the player from a group.""" + _apply_service(service, SonosDevice.unjoin) + + +def _snapshot_service(service): + """Take a snapshot.""" + _apply_service(service, SonosDevice.snapshot) + + +def _restore_service(service): + """Restore a snapshot.""" + _apply_service(service, SonosDevice.restore) def only_if_coordinator(func): diff --git a/homeassistant/components/notify/gntp.py b/homeassistant/components/notify/gntp.py index ed192256cfc..5b5d377e1ea 100644 --- a/homeassistant/components/notify/gntp.py +++ b/homeassistant/components/notify/gntp.py @@ -22,7 +22,7 @@ def get_service(hass, config): """Get the GNTP notification service.""" if config.get('app_icon') is None: icon_file = os.path.join(os.path.dirname(__file__), "..", "frontend", - "www_static", "favicon-192x192.png") + "www_static", "icons", "favicon-192x192.png") app_icon = open(icon_file, 'rb').read() else: app_icon = config.get('app_icon') diff --git a/homeassistant/components/notify/googlevoice.py b/homeassistant/components/notify/googlevoice.py deleted file mode 100644 index 3f1b9d641b0..00000000000 --- a/homeassistant/components/notify/googlevoice.py +++ /dev/null @@ -1,59 +0,0 @@ -""" -Google Voice SMS platform for notify component. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.google_voice/ -""" -import logging - -from homeassistant.components.notify import ( - ATTR_TARGET, DOMAIN, BaseNotificationService) -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import validate_config - -_LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['https://github.com/w1ll1am23/pygooglevoice-sms/archive/' - '7c5ee9969b97a7992fc86a753fe9f20e3ffa3f7c.zip#' - 'pygooglevoice-sms==0.0.1'] - - -def get_service(hass, config): - """Get the Google Voice SMS notification service.""" - if not validate_config({DOMAIN: config}, - {DOMAIN: [CONF_USERNAME, - CONF_PASSWORD]}, - _LOGGER): - return None - - return GoogleVoiceSMSNotificationService(config[CONF_USERNAME], - config[CONF_PASSWORD]) - - -# pylint: disable=too-few-public-methods -class GoogleVoiceSMSNotificationService(BaseNotificationService): - """Implement the notification service for the Google Voice SMS service.""" - - def __init__(self, username, password): - """Initialize the service.""" - from googlevoicesms import Voice - self.voice = Voice() - self.username = username - self.password = password - - def send_message(self, message="", **kwargs): - """Send SMS to specified target user cell.""" - targets = kwargs.get(ATTR_TARGET) - - if not targets: - _LOGGER.info('At least 1 target is required') - return - - if not isinstance(targets, list): - targets = [targets] - - self.voice.login(self.username, self.password) - - for target in targets: - self.voice.send_sms(target, message) - - self.voice.logout() diff --git a/homeassistant/components/notify/sendgrid.py b/homeassistant/components/notify/sendgrid.py index ac3fc3deaab..6811fdbd55b 100644 --- a/homeassistant/components/notify/sendgrid.py +++ b/homeassistant/components/notify/sendgrid.py @@ -10,7 +10,7 @@ from homeassistant.components.notify import ( ATTR_TITLE, DOMAIN, BaseNotificationService) from homeassistant.helpers import validate_config -REQUIREMENTS = ['sendgrid>=1.6.0,<1.7.0'] +REQUIREMENTS = ['sendgrid==3.0.7'] _LOGGER = logging.getLogger(__name__) @@ -24,27 +24,50 @@ def get_service(hass, config): api_key = config['api_key'] sender = config['sender'] recipient = config['recipient'] + return SendgridNotificationService(api_key, sender, recipient) # pylint: disable=too-few-public-methods class SendgridNotificationService(BaseNotificationService): - """Implement the notification service for email via Sendgrid.""" + """Implementation the notification service for email via Sendgrid.""" def __init__(self, api_key, sender, recipient): """Initialize the service.""" + from sendgrid import SendGridAPIClient + self.api_key = api_key self.sender = sender self.recipient = recipient - from sendgrid import SendGridClient - self._sg = SendGridClient(self.api_key) + self._sg = SendGridAPIClient(apikey=self.api_key) def send_message(self, message='', **kwargs): """Send an email to a user via SendGrid.""" subject = kwargs.get(ATTR_TITLE) - from sendgrid import Mail - mail = Mail(from_email=self.sender, to=self.recipient, - html=message, text=message, subject=subject) - self._sg.send(mail) + data = { + "personalizations": [ + { + "to": [ + { + "email": self.recipient + } + ], + "subject": subject + } + ], + "from": { + "email": self.sender + }, + "content": [ + { + "type": "text/plain", + "value": message + } + ] + } + + response = self._sg.client.mail.send.post(request_body=data) + if response.status_code is not 202: + _LOGGER.error('Unable to send notification with SendGrid') diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py index 07c4ecd0a71..141cf6887e9 100644 --- a/homeassistant/components/notify/slack.py +++ b/homeassistant/components/notify/slack.py @@ -10,7 +10,7 @@ from homeassistant.components.notify import DOMAIN, BaseNotificationService from homeassistant.const import CONF_API_KEY from homeassistant.helpers import validate_config -REQUIREMENTS = ['slacker==0.9.21'] +REQUIREMENTS = ['slacker==0.9.24'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/notify/telegram.py b/homeassistant/components/notify/telegram.py index bab891b024c..5e0cee9a441 100644 --- a/homeassistant/components/notify/telegram.py +++ b/homeassistant/components/notify/telegram.py @@ -8,43 +8,69 @@ import io import logging import urllib import requests -from requests.auth import HTTPBasicAuth +import voluptuous as vol +import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( - ATTR_TITLE, ATTR_DATA, DOMAIN, BaseNotificationService) -from homeassistant.const import CONF_API_KEY -from homeassistant.helpers import validate_config + ATTR_TITLE, ATTR_DATA, BaseNotificationService) +from homeassistant.const import (CONF_API_KEY, CONF_NAME, ATTR_LOCATION, + ATTR_LATITUDE, ATTR_LONGITUDE, CONF_PLATFORM) _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['python-telegram-bot==4.3.3'] +REQUIREMENTS = ['python-telegram-bot==5.0.0'] ATTR_PHOTO = "photo" -ATTR_FILE = "file" -ATTR_URL = "url" ATTR_CAPTION = "caption" -ATTR_USERNAME = "username" -ATTR_PASSWORD = "password" + +CONF_CHAT_ID = 'chat_id' + +PLATFORM_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): "telegram", + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_CHAT_ID): cv.string, +}) def get_service(hass, config): """Get the Telegram notification service.""" import telegram - if not validate_config({DOMAIN: config}, - {DOMAIN: [CONF_API_KEY, 'chat_id']}, - _LOGGER): - return None - try: - bot = telegram.Bot(token=config[CONF_API_KEY]) + chat_id = config.get(CONF_CHAT_ID) + api_key = config.get(CONF_API_KEY) + bot = telegram.Bot(token=api_key) username = bot.getMe()['username'] _LOGGER.info("Telegram bot is '%s'.", username) except urllib.error.HTTPError: _LOGGER.error("Please check your access token.") return None - return TelegramNotificationService(config[CONF_API_KEY], config['chat_id']) + return TelegramNotificationService(api_key, chat_id) + + +def load_data(url=None, file=None, username=None, password=None): + """Load photo/document into ByteIO/File container from a source.""" + try: + if url is not None: + # load photo from url + if username is not None and password is not None: + req = requests.get(url, auth=(username, password), timeout=15) + else: + req = requests.get(url, timeout=15) + return io.BytesIO(req.content) + + elif file is not None: + # load photo from file + return open(file, "rb") + else: + _LOGGER.warning("Can't load photo no photo found in params!") + + except (OSError, IOError, requests.exceptions.RequestException): + _LOGGER.error("Can't load photo into ByteIO") + + return None # pylint: disable=too-few-public-methods @@ -64,7 +90,18 @@ class TelegramNotificationService(BaseNotificationService): import telegram title = kwargs.get(ATTR_TITLE) - data = kwargs.get(ATTR_DATA, {}) + data = kwargs.get(ATTR_DATA) + + # exists data for send a photo/location + if data is not None and ATTR_PHOTO in data: + photos = data.get(ATTR_PHOTO, None) + photos = photos if isinstance(photos, list) else [photos] + + for photo_data in photos: + self.send_photo(photo_data) + return + elif data is not None and ATTR_LOCATION in data: + return self.send_location(data.get(ATTR_LOCATION)) # send message try: @@ -74,41 +111,30 @@ class TelegramNotificationService(BaseNotificationService): _LOGGER.exception("Error sending message.") return + def send_photo(self, data): + """Send a photo.""" + import telegram + caption = data.pop(ATTR_CAPTION, None) + # send photo - if ATTR_PHOTO in data: - # if not a list - if not isinstance(data[ATTR_PHOTO], list): - photos = [data[ATTR_PHOTO]] - else: - photos = data[ATTR_PHOTO] + try: + photo = load_data(**data) + self.bot.sendPhoto(chat_id=self._chat_id, + photo=photo, caption=caption) + except telegram.error.TelegramError: + _LOGGER.exception("Error sending photo.") + return - try: - for photo_data in photos: - caption = photo_data.get(ATTR_CAPTION, None) + def send_location(self, gps): + """Send a location.""" + import telegram + latitude = float(gps.get(ATTR_LATITUDE, 0.0)) + longitude = float(gps.get(ATTR_LONGITUDE, 0.0)) - # file is a url - if ATTR_URL in photo_data: - # use http authenticate - if ATTR_USERNAME in photo_data and\ - ATTR_PASSWORD in photo_data: - req = requests.get( - photo_data[ATTR_URL], - auth=HTTPBasicAuth(photo_data[ATTR_USERNAME], - photo_data[ATTR_PASSWORD]) - ) - else: - req = requests.get(photo_data[ATTR_URL]) - file_id = io.BytesIO(req.content) - elif ATTR_FILE in photo_data: - file_id = open(photo_data[ATTR_FILE], "rb") - else: - _LOGGER.error("No url or path is set for photo!") - continue - - self.bot.sendPhoto(chat_id=self._chat_id, - photo=file_id, caption=caption) - - except (OSError, IOError, telegram.error.TelegramError, - urllib.error.HTTPError): - _LOGGER.exception("Error sending photo.") - return + # send location + try: + self.bot.sendLocation(chat_id=self._chat_id, + latitude=latitude, longitude=longitude) + except telegram.error.TelegramError: + _LOGGER.exception("Error sending location.") + return diff --git a/homeassistant/components/panel_iframe.py b/homeassistant/components/panel_iframe.py new file mode 100644 index 00000000000..30773296aeb --- /dev/null +++ b/homeassistant/components/panel_iframe.py @@ -0,0 +1,31 @@ +"""Add an iframe panel to Home Assistant.""" +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.frontend import register_built_in_panel + +DOMAIN = 'panel_iframe' +DEPENDENCIES = ['frontend'] + +CONF_TITLE = 'title' +CONF_ICON = 'icon' +CONF_URL = 'url' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + cv.slug: { + vol.Optional(CONF_TITLE): cv.string, + vol.Optional(CONF_ICON): cv.icon, + # pylint: disable=no-value-for-parameter + vol.Required(CONF_URL): vol.Url(), + }})}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Setup iframe frontend panels.""" + for url_name, info in config[DOMAIN].items(): + register_built_in_panel( + hass, 'iframe', info.get(CONF_TITLE), info.get(CONF_ICON), + url_name, {'url': info[CONF_URL]}) + + return True diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py index 831273840cb..a93328bc723 100644 --- a/homeassistant/components/qwikswitch.py +++ b/homeassistant/components/qwikswitch.py @@ -5,18 +5,29 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/qwikswitch/ """ import logging -from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.components.light import ATTR_BRIGHTNESS -from homeassistant.components.discovery import load_platform +import voluptuous as vol +from homeassistant.const import (EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP) +from homeassistant.helpers.discovery import load_platform +from homeassistant.components.light import ATTR_BRIGHTNESS, Light +from homeassistant.components.switch import SwitchDevice + +DOMAIN = 'qwikswitch' REQUIREMENTS = ['https://github.com/kellerza/pyqwikswitch/archive/v0.4.zip' '#pyqwikswitch==0.4'] -DEPENDENCIES = [] _LOGGER = logging.getLogger(__name__) -DOMAIN = 'qwikswitch' -QSUSB = None +CV_DIM_VALUE = vol.All(vol.Coerce(float), vol.Range(min=1, max=3)) +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required('url', default='http://127.0.0.1:2020'): vol.Coerce(str), + vol.Optional('dimmer_adjust', default=1): CV_DIM_VALUE, + vol.Optional('button_events'): vol.Coerce(str) + })}, extra=vol.ALLOW_EXTRA) + +QSUSB = {} class QSToggleEntity(object): @@ -42,6 +53,7 @@ class QSToggleEntity(object): self._value = qsitem[PQS_VALUE] self._qsusb = qsusb self._dim = qsitem[PQS_TYPE] == QSType.dimmer + QSUSB[self._id] = self @property def brightness(self): @@ -87,51 +99,70 @@ class QSToggleEntity(object): self.update_value(0) +class QSSwitch(QSToggleEntity, SwitchDevice): + """Switch based on a Qwikswitch relay module.""" + + pass + + +class QSLight(QSToggleEntity, Light): + """Light based on a Qwikswitch relay/dimmer module.""" + + pass + + # pylint: disable=too-many-locals def setup(hass, config): """Setup the QSUSB component.""" from pyqwikswitch import (QSUsb, CMD_BUTTONS, QS_NAME, QS_ID, QS_CMD, - QS_TYPE, PQS_VALUE, PQS_TYPE, QSType) + PQS_VALUE, PQS_TYPE, QSType) # Override which cmd's in /&listen packets will fire events # By default only buttons of type [TOGGLE,SCENE EXE,LEVEL] cmd_buttons = config[DOMAIN].get('button_events', ','.join(CMD_BUTTONS)) cmd_buttons = cmd_buttons.split(',') - try: - url = config[DOMAIN].get('url', 'http://127.0.0.1:2020') - dimmer_adjust = float(config[DOMAIN].get('dimmer_adjust', '1')) - qsusb = QSUsb(url, _LOGGER, dimmer_adjust) + url = config[DOMAIN]['url'] + dimmer_adjust = config[DOMAIN]['dimmer_adjust'] - # Ensure qsusb terminates threads correctly - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, - lambda event: qsusb.stop()) - except ValueError as val_err: - _LOGGER.error(str(val_err)) - return False + qsusb = QSUsb(url, _LOGGER, dimmer_adjust) - qsusb.ha_devices = qsusb.devices() - qsusb.ha_objects = {} - - # Identify switches & remove ' Switch' postfix in name - for item in qsusb.ha_devices: - if item[PQS_TYPE] == QSType.relay and \ - item[QS_NAME].lower().endswith(' switch'): - item[QS_TYPE] = 'switch' - item[QS_NAME] = item[QS_NAME][:-7] - - global QSUSB - if QSUSB is None: + def _stop(event): + """Stop the listener queue and clean up.""" + nonlocal qsusb + qsusb.stop() + qsusb = None + global QSUSB QSUSB = {} - QSUSB[id(qsusb)] = qsusb + _LOGGER.info("Waiting for long poll to QSUSB to time out") - # Load sub-components for qwikswitch + hass.bus.listen(EVENT_HOMEASSISTANT_STOP, _stop) + + # Discover all devices in QSUSB + devices = qsusb.devices() + QSUSB['switch'] = [] + QSUSB['light'] = [] + for item in devices: + if item[PQS_TYPE] == QSType.relay and (item[QS_NAME].lower() + .endswith(' switch')): + item[QS_NAME] = item[QS_NAME][:-7] # Remove ' switch' postfix + QSUSB['switch'].append(QSSwitch(item, qsusb)) + elif item[PQS_TYPE] in [QSType.relay, QSType.dimmer]: + QSUSB['light'].append(QSLight(item, qsusb)) + else: + _LOGGER.warning("Ignored unknown QSUSB device: %s", item) + + # Load platforms for comp_name in ('switch', 'light'): - load_platform(hass, comp_name, 'qwikswitch', - {'qsusb_id': id(qsusb)}, config) + if len(QSUSB[comp_name]) > 0: + load_platform(hass, comp_name, 'qwikswitch', {}, config) def qs_callback(item): """Typically a button press or update signal.""" + if qsusb is None: # Shutting down + _LOGGER.info("Done") + return + # If button pressed, fire a hass event if item.get(QS_CMD, '') in cmd_buttons: hass.bus.fire('qwikswitch.button.' + item.get(QS_ID, '@no_id')) @@ -142,9 +173,13 @@ def setup(hass, config): if qsreply is False: return for item in qsreply: - if item[QS_ID] in qsusb.ha_objects: - qsusb.ha_objects[item[QS_ID]].update_value( + if item[QS_ID] in QSUSB: + QSUSB[item[QS_ID]].update_value( round(min(item[PQS_VALUE], 100) * 2.55)) - qsusb.listen(callback=qs_callback, timeout=30) + def _start(event): + """Start listening.""" + qsusb.listen(callback=qs_callback, timeout=30) + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start) + return True diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index b52bce47c17..781736d3c6a 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -39,7 +39,8 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_PURGE_DAYS): vol.All(vol.Coerce(int), vol.Range(min=1)), - vol.Optional(CONF_DB_URL): vol.Url(''), + # pylint: disable=no-value-for-parameter + vol.Optional(CONF_DB_URL): vol.Url(), }) }, extra=vol.ALLOW_EXTRA) @@ -90,8 +91,12 @@ def run_information(point_in_time=None): def setup(hass, config): """Setup the recorder.""" # pylint: disable=global-statement - # pylint: disable=too-many-locals global _INSTANCE + + if _INSTANCE is not None: + _LOGGER.error('Only a single instance allowed.') + return False + purge_days = config.get(DOMAIN, {}).get(CONF_PURGE_DAYS) db_url = config.get(DOMAIN, {}).get(CONF_DB_URL, None) @@ -129,7 +134,7 @@ def log_error(e, retry_wait=0, rollback=True, if rollback: Session().rollback() if retry_wait: - _LOGGER.info("Retrying failed query in %s seconds", QUERY_RETRY_WAIT) + _LOGGER.info("Retrying in %s seconds", retry_wait) time.sleep(retry_wait) @@ -164,8 +169,6 @@ class Recorder(threading.Thread): from homeassistant.components.recorder.models import Events, States import sqlalchemy.exc - global _INSTANCE - while True: try: self._setup_connection() @@ -176,8 +179,12 @@ class Recorder(threading.Thread): message="Error during connection setup: %s") if self.purge_days is not None: - track_point_in_utc_time(self.hass, - lambda now: self._purge_old_data(), + def purge_ticker(event): + """Rerun purge every second day.""" + self._purge_old_data() + track_point_in_utc_time(self.hass, purge_ticker, + dt_util.utcnow() + timedelta(days=2)) + track_point_in_utc_time(self.hass, purge_ticker, dt_util.utcnow() + timedelta(minutes=5)) while True: @@ -186,42 +193,26 @@ class Recorder(threading.Thread): if event == self.quit_object: self._close_run() self._close_connection() + # pylint: disable=global-statement + global _INSTANCE _INSTANCE = None self.queue.task_done() return - elif event.event_type == EVENT_TIME_CHANGED: + if event.event_type == EVENT_TIME_CHANGED: self.queue.task_done() continue - session = Session() dbevent = Events.from_event(event) - session.add(dbevent) - - for _ in range(0, RETRIES): - try: - session.commit() - break - except sqlalchemy.exc.OperationalError as e: - log_error(e, retry_wait=QUERY_RETRY_WAIT, - rollback=True) + self._commit(dbevent) if event.event_type != EVENT_STATE_CHANGED: self.queue.task_done() continue - session = Session() dbstate = States.from_event(event) - - for _ in range(0, RETRIES): - try: - dbstate.event_id = dbevent.event_id - session.add(dbstate) - session.commit() - break - except sqlalchemy.exc.OperationalError as e: - log_error(e, retry_wait=QUERY_RETRY_WAIT, - rollback=True) + dbstate.event_id = dbevent.event_id + self._commit(dbstate) self.queue.task_done() @@ -268,6 +259,7 @@ class Recorder(threading.Thread): def _close_connection(self): """Close the connection.""" + # pylint: disable=global-statement global Session self.engine.dispose() self.engine = None @@ -289,16 +281,12 @@ class Recorder(threading.Thread): start=self.recording_start, created=dt_util.utcnow() ) - session = Session() - session.add(self._run) - session.commit() + self._commit(self._run) def _close_run(self): """Save end time for current run.""" self._run.end = dt_util.utcnow() - session = Session() - session.add(self._run) - session.commit() + self._commit(self._run) self._run = None def _purge_old_data(self): @@ -312,17 +300,24 @@ class Recorder(threading.Thread): purge_before = dt_util.utcnow() - timedelta(days=self.purge_days) - _LOGGER.info("Purging events created before %s", purge_before) - deleted_rows = Session().query(Events).filter( - (Events.created < purge_before)).delete(synchronize_session=False) - _LOGGER.debug("Deleted %s events", deleted_rows) + def _purge_states(session): + deleted_rows = session.query(States) \ + .filter((States.created < purge_before)) \ + .delete(synchronize_session=False) + _LOGGER.debug("Deleted %s states", deleted_rows) - _LOGGER.info("Purging states created before %s", purge_before) - deleted_rows = Session().query(States).filter( - (States.created < purge_before)).delete(synchronize_session=False) - _LOGGER.debug("Deleted %s states", deleted_rows) + if self._commit(_purge_states): + _LOGGER.info("Purged states created before %s", purge_before) + + def _purge_events(session): + deleted_rows = session.query(Events) \ + .filter((Events.created < purge_before)) \ + .delete(synchronize_session=False) + _LOGGER.debug("Deleted %s events", deleted_rows) + + if self._commit(_purge_events): + _LOGGER.info("Purged events created before %s", purge_before) - Session().commit() Session().expire_all() # Execute sqlite vacuum command to free up space on disk @@ -330,6 +325,23 @@ class Recorder(threading.Thread): _LOGGER.info("Vacuuming SQLite to free space") self.engine.execute("VACUUM") + @staticmethod + def _commit(work): + """Commit & retry work: Either a model or in a function.""" + import sqlalchemy.exc + session = Session() + for _ in range(0, RETRIES): + try: + if callable(work): + work(session) + else: + session.add(work) + session.commit() + return True + except sqlalchemy.exc.OperationalError as e: + log_error(e, retry_wait=QUERY_RETRY_WAIT, rollback=True) + return False + def _verify_instance(): """Throw error if recorder not initialized.""" diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 554e2f47d08..fdb5642562f 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -20,7 +20,7 @@ Base = declarative_base() _LOGGER = logging.getLogger(__name__) -class Events(Base): +class Events(Base): # type: ignore # pylint: disable=too-few-public-methods """Event history data.""" @@ -55,7 +55,7 @@ class Events(Base): return None -class States(Base): +class States(Base): # type: ignore # pylint: disable=too-few-public-methods """State change history.""" @@ -114,7 +114,7 @@ class States(Base): return None -class RecorderRuns(Base): +class RecorderRuns(Base): # type: ignore # pylint: disable=too-few-public-methods """Representation of recorder run.""" diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index e5ffba44d40..d684d319117 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -100,6 +100,7 @@ DEVICE_SCHEMA = vol.Schema({ DEVICE_SCHEMA_SENSOR = vol.Schema({ vol.Optional(ATTR_NAME, default=None): cv.string, + vol.Optional(ATTR_FIREEVENT, default=False): cv.boolean, vol.Optional(ATTR_DATA_TYPE, default=[]): vol.All(cv.ensure_list, [vol.In(DATA_TYPES.keys())]), }) diff --git a/homeassistant/components/rollershutter/wink.py b/homeassistant/components/rollershutter/wink.py index 27bd90e0275..8a791ea9b97 100644 --- a/homeassistant/components/rollershutter/wink.py +++ b/homeassistant/components/rollershutter/wink.py @@ -10,7 +10,7 @@ from homeassistant.components.rollershutter import RollershutterDevice from homeassistant.components.wink import WinkDevice from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2'] +REQUIREMENTS = ['python-wink==0.7.11', 'pubnub==3.8.2'] def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/rollershutter/zwave.py b/homeassistant/components/rollershutter/zwave.py index 81b891d7bf1..18a24e41232 100644 --- a/homeassistant/components/rollershutter/zwave.py +++ b/homeassistant/components/rollershutter/zwave.py @@ -49,14 +49,20 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice): def value_changed(self, value): """Called when a value has changed on the network.""" - if self._value.value_id == value.value_id: - self.update_ha_state(True) + if self._value.value_id == value.value_id or \ + self._value.node == value.node: + self.update_ha_state() _LOGGER.debug("Value changed on network %s", value) @property def current_position(self): """Return the current position of Zwave roller shutter.""" - return self._value.data + if self._value.data <= 5: + return 100 + elif self._value.data >= 95: + return 0 + else: + return 100 - self._value.data def move_up(self, **kwargs): """Move the roller shutter up.""" diff --git a/homeassistant/components/sensor/command_line.py b/homeassistant/components/sensor/command_line.py index 3d73017c2e7..eb1fb4603e2 100644 --- a/homeassistant/components/sensor/command_line.py +++ b/homeassistant/components/sensor/command_line.py @@ -95,7 +95,10 @@ class CommandSensorData(object): _LOGGER.info('Running command: %s', self.command) try: - return_value = subprocess.check_output(self.command, shell=True) + return_value = subprocess.check_output(self.command, shell=True, + timeout=15) self.value = return_value.strip().decode('utf-8') except subprocess.CalledProcessError: _LOGGER.error('Command failed: %s', self.command) + except subprocess.TimeoutExpired: + _LOGGER.error('Timeout for command: %s', self.command) diff --git a/homeassistant/components/sensor/google_travel_time.py b/homeassistant/components/sensor/google_travel_time.py index 4add83a87ba..be42ddf8382 100644 --- a/homeassistant/components/sensor/google_travel_time.py +++ b/homeassistant/components/sensor/google_travel_time.py @@ -244,6 +244,10 @@ class GoogleTravelTimeSensor(Entity): self.valid_api_connection = False return None + # Check if the entity has location attributes + if location.has_location(entity): + return self._get_location_from_attributes(entity) + # Check if device is in a zone zone_entity = self._hass.states.get("zone.%s" % entity.state) if location.has_location(zone_entity): @@ -257,10 +261,6 @@ class GoogleTravelTimeSensor(Entity): if entity_id.startswith("sensor."): return entity.state - # For everything else look for location attributes - if location.has_location(entity): - return self._get_location_from_attributes(entity) - # When everything fails just return nothing return None diff --git a/homeassistant/components/sensor/imap.py b/homeassistant/components/sensor/imap.py index 2d5a1484d14..c458799215f 100644 --- a/homeassistant/components/sensor/imap.py +++ b/homeassistant/components/sensor/imap.py @@ -90,12 +90,11 @@ class ImapSensor(Entity): self.connection.select() self._unread_count = len(self.connection.search( None, 'UnSeen')[1][0].split()) - except imaplib.IMAP4.abort: + except imaplib.IMAP4.error: _LOGGER.info("Connection to %s lost, attempting to reconnect", self._server) try: - self._login() - self.update() + self.connection = self._login() except imaplib.IMAP4.error: _LOGGER.error("Failed to reconnect.") diff --git a/homeassistant/components/sensor/rfxtrx.py b/homeassistant/components/sensor/rfxtrx.py index cee54644629..7560adbc93a 100644 --- a/homeassistant/components/sensor/rfxtrx.py +++ b/homeassistant/components/sensor/rfxtrx.py @@ -12,8 +12,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import slugify from homeassistant.components.rfxtrx import ( - ATTR_AUTOMATIC_ADD, ATTR_NAME, - CONF_DEVICES, ATTR_DATA_TYPE, DATA_TYPES) + ATTR_AUTOMATIC_ADD, ATTR_NAME, ATTR_FIREEVENT, + CONF_DEVICES, ATTR_DATA_TYPE, DATA_TYPES, ATTR_ENTITY_ID) DEPENDENCIES = ['rfxtrx'] @@ -48,7 +48,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): break for _data_type in data_types: new_sensor = RfxtrxSensor(None, entity_info[ATTR_NAME], - _data_type) + _data_type, entity_info[ATTR_FIREEVENT]) sensors.append(new_sensor) sub_sensors[_data_type] = new_sensor rfxtrx.RFX_DEVICES[device_id] = sub_sensors @@ -65,7 +65,17 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): if device_id in rfxtrx.RFX_DEVICES: sensors = rfxtrx.RFX_DEVICES[device_id] for key in sensors: - sensors[key].event = event + sensor = sensors[key] + sensor.event = event + # Fire event + if sensors[key].should_fire_event: + sensor.hass.bus.fire( + "signal_received", { + ATTR_ENTITY_ID: + sensors[key].entity_id, + } + ) + return # Add entity if not exist and the automatic_add is True @@ -94,10 +104,11 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): class RfxtrxSensor(Entity): """Representation of a RFXtrx sensor.""" - def __init__(self, event, name, data_type): + def __init__(self, event, name, data_type, should_fire_event=False): """Initialize the sensor.""" self.event = event self._name = name + self.should_fire_event = should_fire_event if data_type not in DATA_TYPES: data_type = "Unknown" self.data_type = data_type diff --git a/homeassistant/components/sensor/speedtest.py b/homeassistant/components/sensor/speedtest.py index ddb14f6af81..445bf8a8d4b 100644 --- a/homeassistant/components/sensor/speedtest.py +++ b/homeassistant/components/sensor/speedtest.py @@ -7,9 +7,10 @@ https://home-assistant.io/components/sensor.speedtest/ import logging import re import sys -from subprocess import check_output +from subprocess import check_output, CalledProcessError import homeassistant.util.dt as dt_util +from homeassistant.components import recorder from homeassistant.components.sensor import DOMAIN from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_time_change @@ -85,8 +86,20 @@ class SpeedtestSensor(Entity): """Get the latest data and update the states.""" data = self.speedtest_client.data if data is None: - return - + entity_id = 'sensor.speedtest_' + self._name.lower() + states = recorder.get_model('States') + try: + last_state = recorder.execute( + recorder.query('States').filter( + (states.entity_id == entity_id) & + (states.last_changed == states.last_updated) & + (states.state != 'unknown') + ).order_by(states.state_id.desc()).limit(1)) + except TypeError: + return + if not last_state: + return + self._state = last_state[0].state elif self.type == 'ping': self._state = data['ping'] elif self.type == 'download': @@ -112,9 +125,13 @@ class SpeedtestData(object): import speedtest_cli _LOGGER.info('Executing speedtest') - re_output = _SPEEDTEST_REGEX.split( - check_output([sys.executable, speedtest_cli.__file__, - '--simple']).decode("utf-8")) + try: + re_output = _SPEEDTEST_REGEX.split( + check_output([sys.executable, speedtest_cli.__file__, + '--simple']).decode("utf-8")) + except CalledProcessError as process_error: + _LOGGER.error('Error executing speedtest: %s', process_error) + return self.data = {'ping': round(float(re_output[1]), 2), 'download': round(float(re_output[2]), 2), 'upload': round(float(re_output[3]), 2)} diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index 3ea651c92da..dc3adfed415 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -25,6 +25,7 @@ SENSOR_TYPE_WINDDIRECTION = "wdir" SENSOR_TYPE_WINDAVERAGE = "wavg" SENSOR_TYPE_WINDGUST = "wgust" SENSOR_TYPE_WATT = "watt" +SENSOR_TYPE_LUMINANCE = "lum" SENSOR_TYPES = { SENSOR_TYPE_TEMP: ['Temperature', TEMP_CELSIUS, "mdi:thermometer"], @@ -35,6 +36,7 @@ SENSOR_TYPES = { SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', ""], SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', ""], SENSOR_TYPE_WATT: ['Watt', 'W', ""], + SENSOR_TYPE_LUMINANCE: ['Luminance', 'lx', ""], } @@ -93,6 +95,11 @@ class TelldusLiveSensor(Entity): """Return the value as temperature.""" return round(float(self._sensor_value), 1) + @property + def _value_as_luminance(self): + """Return the value as luminance.""" + return round(float(self._sensor_value), 1) + @property def _value_as_humidity(self): """Return the value as humidity.""" @@ -116,6 +123,8 @@ class TelldusLiveSensor(Entity): return self._value_as_temperature elif self._sensor_type == SENSOR_TYPE_HUMIDITY: return self._value_as_humidity + elif self._sensor_type == SENSOR_TYPE_LUMINANCE: + return self._value_as_luminance @property def device_state_attributes(self): diff --git a/homeassistant/components/sensor/twitch.py b/homeassistant/components/sensor/twitch.py index cc0530132dc..7b18408105d 100644 --- a/homeassistant/components/sensor/twitch.py +++ b/homeassistant/components/sensor/twitch.py @@ -12,7 +12,7 @@ ATTR_GAME = 'game' ATTR_TITLE = 'title' ICON = 'mdi:twitch' -REQUIREMENTS = ['python-twitch==1.2.0'] +REQUIREMENTS = ['python-twitch==1.3.0'] DOMAIN = 'twitch' diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index 6334a229747..537b7847b7e 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -6,9 +6,7 @@ https://home-assistant.io/components/sensor.vera/ """ import logging -import homeassistant.util.dt as dt_util from homeassistant.const import ( - ATTR_ARMED, ATTR_BATTERY_LEVEL, ATTR_LAST_TRIP_TIME, ATTR_TRIPPED, TEMP_CELSIUS, TEMP_FAHRENHEIT) from homeassistant.helpers.entity import Entity from homeassistant.components.vera import ( @@ -50,30 +48,6 @@ class VeraSensor(VeraDevice, Entity): elif self.vera_device.category == "Humidity Sensor": return '%' - @property - def device_state_attributes(self): - """Return the state attributes.""" - attr = {} - if self.vera_device.has_battery: - attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%' - - if self.vera_device.is_armable: - armed = self.vera_device.is_armed - attr[ATTR_ARMED] = 'True' if armed else 'False' - - if self.vera_device.is_trippable: - last_tripped = self.vera_device.last_trip - if last_tripped is not None: - utc_time = dt_util.utc_from_timestamp(int(last_tripped)) - attr[ATTR_LAST_TRIP_TIME] = utc_time.isoformat() - else: - attr[ATTR_LAST_TRIP_TIME] = None - tripped = self.vera_device.is_tripped - attr[ATTR_TRIPPED] = 'True' if tripped else 'False' - - attr['Vera Device Id'] = self.vera_device.vera_device_id - return attr - def update(self): """Update the state.""" if self.vera_device.category == "Temperature Sensor": diff --git a/homeassistant/components/sensor/wink.py b/homeassistant/components/sensor/wink.py index 2b081a1934f..4f39bd9f2ff 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/sensor/wink.py @@ -12,7 +12,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.components.wink import WinkDevice from homeassistant.loader import get_component -REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2'] +REQUIREMENTS = ['python-wink==0.7.11', 'pubnub==3.8.2'] SENSOR_TYPES = ['temperature', 'humidity'] diff --git a/homeassistant/components/sensor/zwave.py b/homeassistant/components/sensor/zwave.py index 8748ef86572..023ddf4db82 100644 --- a/homeassistant/components/sensor/zwave.py +++ b/homeassistant/components/sensor/zwave.py @@ -96,7 +96,8 @@ class ZWaveSensor(zwave.ZWaveDeviceEntity, Entity): def value_changed(self, value): """Called when a value has changed on the network.""" - if self._value.value_id == value.value_id: + if self._value.value_id == value.value_id or \ + self._value.node == value.node: self.update_ha_state() diff --git a/homeassistant/components/switch/acer_projector.py b/homeassistant/components/switch/acer_projector.py index 5fb7fad909d..b0a6a93cb4d 100644 --- a/homeassistant/components/switch/acer_projector.py +++ b/homeassistant/components/switch/acer_projector.py @@ -27,7 +27,7 @@ CMD_DICT = {LAMP: '* 0 Lamp ?\r', STATE_OFF: '* 0 IR 002\r'} _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pyserial<=3.0'] +REQUIREMENTS = ['pyserial<=3.1'] ICON = 'mdi:projector' diff --git a/homeassistant/components/switch/qwikswitch.py b/homeassistant/components/switch/qwikswitch.py index 1041aa020e6..c3adc33deff 100644 --- a/homeassistant/components/switch/qwikswitch.py +++ b/homeassistant/components/switch/qwikswitch.py @@ -6,29 +6,17 @@ https://home-assistant.io/components/switch.qwikswitch/ """ import logging import homeassistant.components.qwikswitch as qwikswitch -from homeassistant.components.switch import SwitchDevice DEPENDENCIES = ['qwikswitch'] -class QSSwitch(qwikswitch.QSToggleEntity, SwitchDevice): - """Switch based on a Qwikswitch relay module.""" - - pass - - # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """Store add_devices for the switch components.""" - if discovery_info is None or 'qsusb_id' not in discovery_info: + """Add switched from the main Qwikswitch component.""" + if discovery_info is None: logging.getLogger(__name__).error( 'Configure main Qwikswitch component') return False - qsusb = qwikswitch.QSUSB[discovery_info['qsusb_id']] - - for item in qsusb.ha_devices: - if item['type'] == 'switch': - dev = QSSwitch(item, qsusb) - add_devices([dev]) - qsusb.ha_objects[item['id']] = dev + add_devices(qwikswitch.QSUSB['switch']) + return True diff --git a/homeassistant/components/switch/rpi_gpio.py b/homeassistant/components/switch/rpi_gpio.py index 5daa96397dc..23a98a3f7ba 100644 --- a/homeassistant/components/switch/rpi_gpio.py +++ b/homeassistant/components/switch/rpi_gpio.py @@ -39,6 +39,7 @@ class RPiGPIOSwitch(ToggleEntity): self._invert_logic = invert_logic self._state = False rpi_gpio.setup_output(self._port) + rpi_gpio.write_output(self._port, 1 if self._invert_logic else 0) @property def name(self): diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index d71d064a3e8..e88e45a5171 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -6,10 +6,9 @@ https://home-assistant.io/components/switch.vera/ """ import logging -import homeassistant.util.dt as dt_util +from homeassistant.util import convert from homeassistant.components.switch import SwitchDevice from homeassistant.const import ( - ATTR_ARMED, ATTR_BATTERY_LEVEL, ATTR_LAST_TRIP_TIME, ATTR_TRIPPED, STATE_OFF, STATE_ON) from homeassistant.components.vera import ( VeraDevice, VERA_DEVICES, VERA_CONTROLLER) @@ -34,32 +33,6 @@ class VeraSwitch(VeraDevice, SwitchDevice): self._state = False VeraDevice.__init__(self, vera_device, controller) - @property - def device_state_attributes(self): - """Return the state attributes of the device.""" - attr = {} - - if self.vera_device.has_battery: - attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%' - - if self.vera_device.is_armable: - armed = self.vera_device.is_armed - attr[ATTR_ARMED] = 'True' if armed else 'False' - - if self.vera_device.is_trippable: - last_tripped = self.vera_device.last_trip - if last_tripped is not None: - utc_time = dt_util.utc_from_timestamp(int(last_tripped)) - attr[ATTR_LAST_TRIP_TIME] = utc_time.isoformat() - else: - attr[ATTR_LAST_TRIP_TIME] = None - tripped = self.vera_device.is_tripped - attr[ATTR_TRIPPED] = 'True' if tripped else 'False' - - attr['Vera Device Id'] = self.vera_device.vera_device_id - - return attr - def turn_on(self, **kwargs): """Turn device on.""" self.vera_device.switch_on() @@ -72,6 +45,13 @@ class VeraSwitch(VeraDevice, SwitchDevice): self._state = STATE_OFF self.update_ha_state() + @property + def current_power_mwh(self): + """Current power usage in mWh.""" + power = self.vera_device.power + if power: + return convert(power, float, 0.0) * 1000 + @property def is_on(self): """Return true if device is on.""" diff --git a/homeassistant/components/switch/wink.py b/homeassistant/components/switch/wink.py index 4f18d7d9456..1feb8e584bb 100644 --- a/homeassistant/components/switch/wink.py +++ b/homeassistant/components/switch/wink.py @@ -10,7 +10,7 @@ from homeassistant.components.wink import WinkDevice from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.helpers.entity import ToggleEntity -REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2'] +REQUIREMENTS = ['python-wink==0.7.11', 'pubnub==3.8.2'] def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/thermostat/knx.py b/homeassistant/components/thermostat/knx.py new file mode 100644 index 00000000000..621830c828e --- /dev/null +++ b/homeassistant/components/thermostat/knx.py @@ -0,0 +1,83 @@ +""" +Support for KNX thermostats. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/knx/ +""" +import logging + +from homeassistant.components.thermostat import ThermostatDevice +from homeassistant.const import TEMP_CELSIUS + +from homeassistant.components.knx import ( + KNXConfig, KNXMultiAddressDevice) + +DEPENDENCIES = ["knx"] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Create and add an entity based on the configuration.""" + add_entities([ + KNXThermostat(hass, KNXConfig(config)) + ]) + + +class KNXThermostat(KNXMultiAddressDevice, ThermostatDevice): + """Representation of a KNX thermostat. + + A KNX thermostat will has the following parameters: + - temperature (current temperature) + - setpoint (target temperature in HASS terms) + - hvac mode selection (comfort/night/frost protection) + + This version supports only polling. Messages from the KNX bus do not + automatically update the state of the thermostat (to be implemented + in future releases) + """ + + def __init__(self, hass, config): + """Initialize the thermostat based on the given configuration.""" + KNXMultiAddressDevice.__init__(self, hass, config, + ["temperature", "setpoint"], + ["mode"]) + + self._unit_of_measurement = TEMP_CELSIUS # KNX always used celcius + self._away = False # not yet supported + self._is_fan_on = False # not yet supported + + @property + def should_poll(self): + """Polling is needed for the KNX thermostat.""" + return True + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def current_temperature(self): + """Return the current temperature.""" + from knxip.conversion import knx2_to_float + + return knx2_to_float(self.value("temperature")) + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + from knxip.conversion import knx2_to_float + + return knx2_to_float(self.value("setpoint")) + + def set_temperature(self, temperature): + """Set new target temperature.""" + from knxip.conversion import float_to_knx2 + + self.set_value("setpoint", float_to_knx2(temperature)) + _LOGGER.debug("Set target temperature to %s", temperature) + + def set_hvac_mode(self, hvac_mode): + """Set hvac mode.""" + raise NotImplementedError() diff --git a/homeassistant/components/thermostat/nest.py b/homeassistant/components/thermostat/nest.py index 00a1acf07b4..2e0a8c05888 100644 --- a/homeassistant/components/thermostat/nest.py +++ b/homeassistant/components/thermostat/nest.py @@ -141,6 +141,10 @@ class NestThermostat(ThermostatDevice): temperature = (self.target_temperature_low, temperature) self.device.target = temperature + def set_hvac_mode(self, hvac_mode): + """Set hvac mode.""" + self.device.mode = hvac_mode + def turn_away_mode_on(self): """Turn away on.""" self.structure.away = True diff --git a/homeassistant/components/thermostat/zwave.py b/homeassistant/components/thermostat/zwave.py index ed653874af2..80766d47100 100644 --- a/homeassistant/components/thermostat/zwave.py +++ b/homeassistant/components/thermostat/zwave.py @@ -81,7 +81,8 @@ class ZWaveThermostat(zwave.ZWaveDeviceEntity, ThermostatDevice): def value_changed(self, value): """Called when a value has changed on the network.""" - if self._value.value_id == value.value_id: + if self._value.value_id == value.value_id or \ + self._value.node == value.node: self.update_properties() self.update_ha_state() diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py index ce7bf1556fa..514fe002568 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera.py @@ -9,34 +9,27 @@ from collections import defaultdict from requests.exceptions import RequestException + +from homeassistant.util.dt import utc_from_timestamp +from homeassistant.util import convert from homeassistant.helpers import discovery -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + ATTR_ARMED, ATTR_BATTERY_LEVEL, ATTR_LAST_TRIP_TIME, ATTR_TRIPPED, + EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyvera==0.2.13'] +REQUIREMENTS = ['pyvera==0.2.15'] _LOGGER = logging.getLogger(__name__) DOMAIN = 'vera' - VERA_CONTROLLER = None CONF_EXCLUDE = 'exclude' CONF_LIGHTS = 'lights' -DEVICE_CATEGORIES = { - 'Sensor': 'binary_sensor', - 'Temperature Sensor': 'sensor', - 'Light Sensor': 'sensor', - 'Humidity Sensor': 'sensor', - 'Dimmable Switch': 'light', - 'Switch': 'switch', - 'Armable Sensor': 'switch', - 'On/Off Switch': 'switch', - 'Doorlock': 'lock', - # 'Window Covering': NOT SUPPORTED YET -} +ATTR_CURRENT_POWER_MWH = "current_power_mwh" VERA_DEVICES = defaultdict(list) @@ -66,8 +59,7 @@ def setup(hass, base_config): hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription) try: - all_devices = VERA_CONTROLLER.get_devices( - list(DEVICE_CATEGORIES.keys())) + all_devices = VERA_CONTROLLER.get_devices() except RequestException: # There was a network related error connecting to the vera controller. _LOGGER.exception("Error communicating with Vera API") @@ -86,11 +78,9 @@ def setup(hass, base_config): for device in all_devices: if device.device_id in exclude: continue - dev_type = DEVICE_CATEGORIES.get(device.category) + dev_type = map_vera_device(device, lights_ids) if dev_type is None: continue - if dev_type == 'switch' and device.device_id in lights_ids: - dev_type = 'light' VERA_DEVICES[dev_type].append(device) for component in 'binary_sensor', 'sensor', 'light', 'switch', 'lock': @@ -99,6 +89,29 @@ def setup(hass, base_config): return True +def map_vera_device(vera_device, remap): + """Map vera classes to HA types.""" + # pylint: disable=too-many-return-statements + import pyvera as veraApi + if isinstance(vera_device, veraApi.VeraDimmer): + return 'light' + if isinstance(vera_device, veraApi.VeraBinarySensor): + return 'binary_sensor' + if isinstance(vera_device, veraApi.VeraSensor): + return 'sensor' + if isinstance(vera_device, veraApi.VeraArmableDevice): + return 'switch' + if isinstance(vera_device, veraApi.VeraLock): + return 'lock' + if isinstance(vera_device, veraApi.VeraSwitch): + if vera_device.device_id in remap: + return 'light' + else: + return 'switch' + # VeraCurtain: NOT SUPPORTED YET + return None + + class VeraDevice(Entity): """Representation of a Vera devicetity.""" @@ -123,3 +136,33 @@ class VeraDevice(Entity): def should_poll(self): """No polling needed.""" return False + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + attr = {} + + if self.vera_device.has_battery: + attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%' + + if self.vera_device.is_armable: + armed = self.vera_device.is_armed + attr[ATTR_ARMED] = 'True' if armed else 'False' + + if self.vera_device.is_trippable: + last_tripped = self.vera_device.last_trip + if last_tripped is not None: + utc_time = utc_from_timestamp(int(last_tripped)) + attr[ATTR_LAST_TRIP_TIME] = utc_time.isoformat() + else: + attr[ATTR_LAST_TRIP_TIME] = None + tripped = self.vera_device.is_tripped + attr[ATTR_TRIPPED] = 'True' if tripped else 'False' + + power = self.vera_device.power + if power: + attr[ATTR_CURRENT_POWER_MWH] = convert(power, float, 0.0) * 1000 + + attr['Vera Device Id'] = self.vera_device.vera_device_id + + return attr diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index 7be4c4d5cfe..4a45cd8576f 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -12,7 +12,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL from homeassistant.helpers.entity import Entity DOMAIN = "wink" -REQUIREMENTS = ['python-wink==0.7.10', 'pubnub==3.8.2'] +REQUIREMENTS = ['python-wink==0.7.11', 'pubnub==3.8.2'] SUBSCRIPTION_HANDLER = None CHANNELS = [] diff --git a/homeassistant/components/zwave.py b/homeassistant/components/zwave.py index f5d6e597406..e280f5adcf9 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -38,6 +38,7 @@ SERVICE_SOFT_RESET = "soft_reset" SERVICE_TEST_NETWORK = "test_network" EVENT_SCENE_ACTIVATED = "zwave.scene_activated" +EVENT_NODE_EVENT = "zwave.node_event" COMMAND_CLASS_WHATEVER = None COMMAND_CLASS_SENSOR_MULTILEVEL = 49 @@ -54,9 +55,13 @@ COMMAND_CLASS_BATTERY = 128 COMMAND_CLASS_SENSOR_ALARM = 156 GENERIC_COMMAND_CLASS_WHATEVER = None -GENERIC_COMMAND_CLASS_NOTIFICATION_SENSOR = 7 +GENERIC_COMMAND_CLASS_REMOTE_CONTROLLER = 1 +GENERIC_COMMAND_CLASS_NOTIFICATION = 7 +GENERIC_COMMAND_CLASS_REMOTE_SWITCH = 12 +GENERIC_COMMAND_CLASS_REPEATER_SLAVE = 15 GENERIC_COMMAND_CLASS_MULTILEVEL_SWITCH = 17 GENERIC_COMMAND_CLASS_BINARY_SWITCH = 16 +GENERIC_COMMAND_CLASS_WALL_CONTROLLER = 24 GENERIC_COMMAND_CLASS_ENTRY_CONTROL = 64 GENERIC_COMMAND_CLASS_BINARY_SENSOR = 32 GENERIC_COMMAND_CLASS_MULTILEVEL_SENSOR = 33 @@ -101,20 +106,37 @@ DISCOVERY_COMPONENTS = [ ('light', [GENERIC_COMMAND_CLASS_MULTILEVEL_SWITCH], [SPECIFIC_DEVICE_CLASS_MULTILEVEL_POWER_SWITCH, - SPECIFIC_DEVICE_CLASS_MULTILEVEL_SCENE], + SPECIFIC_DEVICE_CLASS_MULTILEVEL_SCENE, + SPECIFIC_DEVICE_CLASS_NOT_USED], [COMMAND_CLASS_SWITCH_MULTILEVEL], TYPE_BYTE, GENRE_USER), ('switch', - [GENERIC_COMMAND_CLASS_BINARY_SWITCH], + [GENERIC_COMMAND_CLASS_ALARM_SENSOR, + GENERIC_COMMAND_CLASS_BINARY_SENSOR, + GENERIC_COMMAND_CLASS_BINARY_SWITCH, + GENERIC_COMMAND_CLASS_ENTRY_CONTROL, + GENERIC_COMMAND_CLASS_MULTILEVEL_SENSOR, + GENERIC_COMMAND_CLASS_MULTILEVEL_SWITCH, + GENERIC_COMMAND_CLASS_NOTIFICATION, + GENERIC_COMMAND_CLASS_REMOTE_CONTROLLER, + GENERIC_COMMAND_CLASS_REMOTE_SWITCH, + GENERIC_COMMAND_CLASS_REPEATER_SLAVE, + GENERIC_COMMAND_CLASS_THERMOSTAT, + GENERIC_COMMAND_CLASS_WALL_CONTROLLER], [SPECIFIC_DEVICE_CLASS_WHATEVER], [COMMAND_CLASS_SWITCH_BINARY], TYPE_BOOL, GENRE_USER), ('binary_sensor', - [GENERIC_COMMAND_CLASS_BINARY_SENSOR, + [GENERIC_COMMAND_CLASS_ALARM_SENSOR, + GENERIC_COMMAND_CLASS_BINARY_SENSOR, + GENERIC_COMMAND_CLASS_BINARY_SWITCH, + GENERIC_COMMAND_CLASS_METER, GENERIC_COMMAND_CLASS_MULTILEVEL_SENSOR, - GENERIC_COMMAND_CLASS_NOTIFICATION_SENSOR], + GENERIC_COMMAND_CLASS_MULTILEVEL_SWITCH, + GENERIC_COMMAND_CLASS_NOTIFICATION, + GENERIC_COMMAND_CLASS_THERMOSTAT], [SPECIFIC_DEVICE_CLASS_WHATEVER], [COMMAND_CLASS_SENSOR_BINARY], TYPE_BOOL, @@ -159,8 +181,10 @@ DISCOVERY_COMPONENTS = [ ATTR_NODE_ID = "node_id" ATTR_VALUE_ID = "value_id" +ATTR_OBJECT_ID = "object_id" ATTR_SCENE_ID = "scene_id" +ATTR_BASIC_LEVEL = "basic_level" NETWORK = None @@ -185,6 +209,14 @@ def _value_name(value): return "{} {}".format(_node_name(value.node), value.label) +def _node_object_id(node): + """Return the object_id of the node.""" + node_object_id = "{}_{}".format(slugify(_node_name(node)), + node.node_id) + + return node_object_id + + def _object_id(value): """Return the object_id of the device value. @@ -272,7 +304,9 @@ def setup(hass, config): print("") print("SIGNAL *****", signal) if value and signal in (ZWaveNetwork.SIGNAL_VALUE_CHANGED, - ZWaveNetwork.SIGNAL_VALUE_ADDED): + ZWaveNetwork.SIGNAL_VALUE_ADDED, + ZWaveNetwork.SIGNAL_SCENE_EVENT, + ZWaveNetwork.SIGNAL_NODE_EVENT): pprint(_obj_to_dict(value)) print("") @@ -342,18 +376,25 @@ def setup(hass, config): def scene_activated(node, scene_id): """Called when a scene is activated on any node in the network.""" - name = _node_name(node) - object_id = "{}_{}".format(slugify(name), node.node_id) - hass.bus.fire(EVENT_SCENE_ACTIVATED, { - ATTR_ENTITY_ID: object_id, + ATTR_ENTITY_ID: _node_object_id(node), + ATTR_OBJECT_ID: _node_object_id(node), ATTR_SCENE_ID: scene_id }) + def node_event_activated(node, value): + """Called when a nodeevent is activated on any node in the network.""" + hass.bus.fire(EVENT_NODE_EVENT, { + ATTR_OBJECT_ID: _node_object_id(node), + ATTR_BASIC_LEVEL: value + }) + dispatcher.connect( value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED, weak=False) dispatcher.connect( scene_activated, ZWaveNetwork.SIGNAL_SCENE_EVENT, weak=False) + dispatcher.connect( + node_event_activated, ZWaveNetwork.SIGNAL_NODE_EVENT, weak=False) def add_node(service): """Switch into inclusion mode.""" diff --git a/homeassistant/config.py b/homeassistant/config.py index bf0cbdba23b..1061bd4f5ec 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -22,7 +22,7 @@ YAML_CONFIG_FILE = 'configuration.yaml' VERSION_FILE = '.HA_VERSION' CONFIG_DIR_NAME = '.homeassistant' -DEFAULT_CONFIG = ( +DEFAULT_CORE_CONFIG = ( # Tuples (attribute, default, auto detect property, description) (CONF_NAME, 'Home', None, 'Name of the location where Home Assistant is ' 'running'), @@ -34,17 +34,39 @@ DEFAULT_CONFIG = ( (CONF_TIME_ZONE, 'UTC', 'time_zone', 'Pick yours from here: http://en.wiki' 'pedia.org/wiki/List_of_tz_database_time_zones'), ) -DEFAULT_COMPONENTS = { - 'introduction:': 'Show links to resources in log and frontend', - 'frontend:': 'Enables the frontend', - 'updater:': 'Checks for available updates', - 'discovery:': 'Discover some devices automatically', - 'conversation:': 'Allows you to issue voice commands from the frontend', - 'history:': 'Enables support for tracking state changes over time.', - 'logbook:': 'View all events in a logbook', - 'sun:': 'Track the sun', - 'sensor:\n platform: yr': 'Weather Prediction', -} +DEFAULT_CONFIG = """ +# Show links to resources in log and frontend +introduction: + +# Enables the frontend +frontend: + +http: + # Uncomment this to add a password (recommended!) + # api_password: PASSWORD + +# Checks for available updates +updater: + +# Discover some devices automatically +discovery: + +# Allows you to issue voice commands from the frontend in enabled browsers +conversation: + +# Enables support for tracking state changes over time. +history: + +# View all events in a logbook +logbook: + +# Track the sun +sun: + +# Weather Prediction +sensor: + platform: yr +""" def _valid_customize(value): @@ -73,14 +95,14 @@ CORE_CONFIG_SCHEMA = vol.Schema({ }) -def get_default_config_dir(): +def get_default_config_dir() -> str: """Put together the default configuration directory based on OS.""" data_dir = os.getenv('APPDATA') if os.name == "nt" \ else os.path.expanduser('~') return os.path.join(data_dir, CONFIG_DIR_NAME) -def ensure_config_exists(config_dir, detect_location=True): +def ensure_config_exists(config_dir: str, detect_location: bool=True) -> str: """Ensure a config file exists in given configuration directory. Creating a default one if needed. @@ -104,7 +126,7 @@ def create_default_config(config_dir, detect_location=True): config_path = os.path.join(config_dir, YAML_CONFIG_FILE) version_path = os.path.join(config_dir, VERSION_FILE) - info = {attr: default for attr, default, _, _ in DEFAULT_CONFIG} + info = {attr: default for attr, default, _, _ in DEFAULT_CORE_CONFIG} location_info = detect_location and loc_util.detect_location_info() @@ -112,7 +134,7 @@ def create_default_config(config_dir, detect_location=True): if location_info.use_fahrenheit: info[CONF_TEMPERATURE_UNIT] = 'F' - for attr, default, prop, _ in DEFAULT_CONFIG: + for attr, default, prop, _ in DEFAULT_CORE_CONFIG: if prop is None: continue info[attr] = getattr(location_info, prop) or default @@ -127,18 +149,14 @@ def create_default_config(config_dir, detect_location=True): with open(config_path, 'w') as config_file: config_file.write("homeassistant:\n") - for attr, _, _, description in DEFAULT_CONFIG: + for attr, _, _, description in DEFAULT_CORE_CONFIG: if info[attr] is None: continue elif description: config_file.write(" # {}\n".format(description)) config_file.write(" {}: {}\n".format(attr, info[attr])) - config_file.write("\n") - - for component, description in DEFAULT_COMPONENTS.items(): - config_file.write("# {}\n".format(description)) - config_file.write("{}\n\n".format(component)) + config_file.write(DEFAULT_CONFIG) with open(version_path, 'wt') as version_file: version_file.write(__version__) diff --git a/homeassistant/const.py b/homeassistant/const.py index ff41ce60da5..5d745765fb7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """Constants used by Home Assistant components.""" -__version__ = "0.24.1" +__version__ = "0.25.0.dev0" REQUIRED_PYTHON_VER = (3, 4) PLATFORM_FORMAT = '{}.{}' diff --git a/homeassistant/core.py b/homeassistant/core.py index 82ec20c82f9..7ddf5a6c10f 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -14,6 +14,7 @@ import threading import time from types import MappingProxyType +from typing import Any, Callable import voluptuous as vol import homeassistant.helpers.temperature as temp_helper @@ -57,11 +58,34 @@ class CoreState(enum.Enum): running = "RUNNING" stopping = "STOPPING" - def __str__(self): + def __str__(self) -> str: """Return the event.""" return self.value +class JobPriority(util.OrderedEnum): + """Provides job priorities for event bus jobs.""" + + EVENT_CALLBACK = 0 + EVENT_SERVICE = 1 + EVENT_STATE = 2 + EVENT_TIME = 3 + EVENT_DEFAULT = 4 + + @staticmethod + def from_event_type(event_type): + """Return a priority based on event type.""" + if event_type == EVENT_TIME_CHANGED: + return JobPriority.EVENT_TIME + elif event_type == EVENT_STATE_CHANGED: + return JobPriority.EVENT_STATE + elif event_type == EVENT_CALL_SERVICE: + return JobPriority.EVENT_SERVICE + elif event_type == EVENT_SERVICE_EXECUTED: + return JobPriority.EVENT_CALLBACK + return JobPriority.EVENT_DEFAULT + + class HomeAssistant(object): """Root object of the Home Assistant home automation.""" @@ -69,17 +93,17 @@ class HomeAssistant(object): """Initialize new Home Assistant object.""" self.pool = pool = create_worker_pool() self.bus = EventBus(pool) - self.services = ServiceRegistry(self.bus, pool) + self.services = ServiceRegistry(self.bus, self.add_job) self.states = StateMachine(self.bus) self.config = Config() self.state = CoreState.not_running @property - def is_running(self): + def is_running(self) -> bool: """Return if Home Assistant is running.""" return self.state == CoreState.running - def start(self): + def start(self) -> None: """Start home assistant.""" _LOGGER.info( "Starting Home Assistant (%d threads)", self.pool.worker_count) @@ -90,7 +114,18 @@ class HomeAssistant(object): self.pool.block_till_done() self.state = CoreState.running - def block_till_stopped(self): + def add_job(self, + target: Callable[..., None], + *args: Any, + priority: JobPriority=JobPriority.EVENT_DEFAULT) -> None: + """Add job to the worker pool. + + target: target to call. + args: parameters for method to call. + """ + self.pool.add_job(priority, (target,) + args) + + def block_till_stopped(self) -> int: """Register service homeassistant/stop and will block until called.""" request_shutdown = threading.Event() request_restart = threading.Event() @@ -123,16 +158,16 @@ class HomeAssistant(object): except AttributeError: pass try: - while not request_shutdown.isSet(): + while not request_shutdown.is_set(): time.sleep(1) except KeyboardInterrupt: pass finally: self.stop() - return RESTART_EXIT_CODE if request_restart.isSet() else 0 + return RESTART_EXIT_CODE if request_restart.is_set() else 0 - def stop(self): + def stop(self) -> None: """Stop Home Assistant and shuts down all threads.""" _LOGGER.info("Stopping") self.state = CoreState.stopping @@ -141,30 +176,6 @@ class HomeAssistant(object): self.state = CoreState.not_running -class JobPriority(util.OrderedEnum): - """Provides job priorities for event bus jobs.""" - - EVENT_CALLBACK = 0 - EVENT_SERVICE = 1 - EVENT_STATE = 2 - EVENT_TIME = 3 - EVENT_DEFAULT = 4 - - @staticmethod - def from_event_type(event_type): - """Return a priority based on event type.""" - if event_type == EVENT_TIME_CHANGED: - return JobPriority.EVENT_TIME - elif event_type == EVENT_STATE_CHANGED: - return JobPriority.EVENT_STATE - elif event_type == EVENT_CALL_SERVICE: - return JobPriority.EVENT_SERVICE - elif event_type == EVENT_SERVICE_EXECUTED: - return JobPriority.EVENT_CALLBACK - else: - return JobPriority.EVENT_DEFAULT - - class EventOrigin(enum.Enum): """Represent the origin of an event.""" @@ -222,11 +233,11 @@ class Event(object): class EventBus(object): """Allows firing of and listening for events.""" - def __init__(self, pool=None): + def __init__(self, pool: util.ThreadPool) -> None: """Initialize a new event bus.""" self._listeners = {} self._lock = threading.Lock() - self._pool = pool or create_worker_pool() + self._pool = pool @property def listeners(self): @@ -235,7 +246,7 @@ class EventBus(object): return {key: len(self._listeners[key]) for key in self._listeners} - def fire(self, event_type, event_data=None, origin=EventOrigin.local): + def fire(self, event_type: str, event_data=None, origin=EventOrigin.local): """Fire an event.""" if not self._pool.running: raise HomeAssistantError('Home Assistant has shut down.') @@ -290,7 +301,7 @@ class EventBus(object): # available to execute this listener it might occur that the # listener gets lined up twice to be executed. # This will make sure the second time it does nothing. - onetime_listener.run = True + setattr(onetime_listener, 'run', True) self.remove_listener(event_type, onetime_listener) @@ -575,11 +586,11 @@ class ServiceCall(object): class ServiceRegistry(object): """Offers services over the eventbus.""" - def __init__(self, bus, pool=None): + def __init__(self, bus, add_job): """Initialize a service registry.""" self._services = {} self._lock = threading.Lock() - self._pool = pool or create_worker_pool() + self._add_job = add_job self._bus = bus self._cur_id = 0 bus.listen(EVENT_CALL_SERVICE, self._event_to_service_call) @@ -678,13 +689,11 @@ class ServiceRegistry(object): service_call = ServiceCall(domain, service, service_data, call_id) # Add a job to the pool that calls _execute_service - self._pool.add_job(JobPriority.EVENT_SERVICE, - (self._execute_service, - (service_handler, service_call))) + self._add_job(self._execute_service, service_handler, service_call, + priority=JobPriority.EVENT_SERVICE) - def _execute_service(self, service_and_call): + def _execute_service(self, service, call): """Execute a service and fires a SERVICE_EXECUTED event.""" - service, call = service_and_call service(call) if call.call_id is not None: @@ -783,7 +792,7 @@ def create_timer(hass, interval=TIMER_INTERVAL): calc_now = dt_util.utcnow - while not stop_event.isSet(): + while not stop_event.is_set(): now = calc_now() # First check checks if we are not on a second matching the @@ -807,7 +816,7 @@ def create_timer(hass, interval=TIMER_INTERVAL): last_fired_on_second = now.second # Event might have been set while sleeping - if not stop_event.isSet(): + if not stop_event.is_set(): try: hass.bus.fire(EVENT_TIME_CHANGED, {ATTR_NOW: now}) except HomeAssistantError: @@ -831,8 +840,8 @@ def create_worker_pool(worker_count=None): def job_handler(job): """Called whenever a job is available to do.""" try: - func, arg = job - func(arg) + func, *args = job + func(*args) except Exception: # pylint: disable=broad-except # Catch any exception our service/event_listener might throw # We do not want to crash our ThreadPool diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index 02c7eece73a..c76630b8151 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -1,10 +1,20 @@ """Helper methods for components within Home Assistant.""" import re +from typing import Any, Iterable, Tuple, List, Dict + from homeassistant.const import CONF_PLATFORM +# Typing Imports and TypeAlias +# pylint: disable=using-constant-test,unused-import +if False: + from logging import Logger # NOQA -def validate_config(config, items, logger): +# pylint: disable=invalid-name +ConfigType = Dict[str, Any] + + +def validate_config(config: ConfigType, items: Dict, logger: 'Logger') -> bool: """Validate if all items are available in the configuration. config is the general dictionary with all the configurations. @@ -29,7 +39,8 @@ def validate_config(config, items, logger): return not errors_found -def config_per_platform(config, domain): +def config_per_platform(config: ConfigType, + domain: str) -> Iterable[Tuple[Any, Any]]: """Generator to break a component config into different platforms. For example, will find 'switch', 'switch 2', 'switch 3', .. etc @@ -48,7 +59,7 @@ def config_per_platform(config, domain): yield platform, item -def extract_domain_configs(config, domain): +def extract_domain_configs(config: ConfigType, domain: str) -> List[str]: """Extract keys from config for given domain name.""" pattern = re.compile(r'^{}(| .+)$'.format(domain)) return [key for key in config.keys() if pattern.match(key)] diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 65a9fe9ebd8..1dc0cc26b5c 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -376,7 +376,9 @@ CONDITION_SCHEMA = vol.Any( _SCRIPT_DELAY_SCHEMA = vol.Schema({ vol.Optional(CONF_ALIAS): string, - vol.Required("delay"): vol.All(time_period, positive_timedelta) + vol.Required("delay"): vol.Any( + vol.All(time_period, positive_timedelta), + template) }) SCRIPT_SCHEMA = vol.All( diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index 480c786d31f..b0cf8af0747 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -72,15 +72,20 @@ def load_platform(hass, component, platform, discovered=None, Use `listen_platform` to register a callback for these events. """ - if component is not None: - bootstrap.setup_component(hass, component, hass_config) + def discover_platform(): + """Discover platform job.""" + # No need to fire event if we could not setup component + if not bootstrap.setup_component(hass, component, hass_config): + return - data = { - ATTR_SERVICE: EVENT_LOAD_PLATFORM.format(component), - ATTR_PLATFORM: platform, - } + data = { + ATTR_SERVICE: EVENT_LOAD_PLATFORM.format(component), + ATTR_PLATFORM: platform, + } - if discovered is not None: - data[ATTR_DISCOVERED] = discovered + if discovered is not None: + data[ATTR_DISCOVERED] = discovered - hass.bus.fire(EVENT_PLATFORM_DISCOVERED, data) + hass.bus.fire(EVENT_PLATFORM_DISCOVERED, data) + + hass.add_job(discover_platform) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index d120a3b2cf6..9968ad3df4a 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -2,6 +2,8 @@ import logging import re +from typing import Any, Optional, List, Dict + from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, DEVICE_DEFAULT_NAME, STATE_OFF, STATE_ON, @@ -10,8 +12,12 @@ from homeassistant.const import ( from homeassistant.exceptions import NoEntitySpecifiedError from homeassistant.util import ensure_unique_string, slugify +# pylint: disable=using-constant-test,unused-import +if False: + from homeassistant.core import HomeAssistant # NOQA + # Entity attributes that we will overwrite -_OVERWRITE = {} +_OVERWRITE = {} # type: Dict[str, Any] _LOGGER = logging.getLogger(__name__) @@ -19,7 +25,9 @@ _LOGGER = logging.getLogger(__name__) ENTITY_ID_PATTERN = re.compile(r"^(\w+)\.(\w+)$") -def generate_entity_id(entity_id_format, name, current_ids=None, hass=None): +def generate_entity_id(entity_id_format: str, name: Optional[str], + current_ids: Optional[List[str]]=None, + hass: 'Optional[HomeAssistant]'=None) -> str: """Generate a unique entity ID based on given entity IDs or used IDs.""" name = (name or DEVICE_DEFAULT_NAME).lower() if current_ids is None: @@ -32,19 +40,19 @@ def generate_entity_id(entity_id_format, name, current_ids=None, hass=None): entity_id_format.format(slugify(name)), current_ids) -def set_customize(customize): +def set_customize(customize: Dict[str, Any]) -> None: """Overwrite all current customize settings.""" global _OVERWRITE _OVERWRITE = {key.lower(): val for key, val in customize.items()} -def split_entity_id(entity_id): +def split_entity_id(entity_id: str) -> List[str]: """Split a state entity_id into domain, object_id.""" return entity_id.split(".", 1) -def valid_entity_id(entity_id): +def valid_entity_id(entity_id: str) -> bool: """Test if an entity ID is a valid format.""" return ENTITY_ID_PATTERN.match(entity_id) is not None @@ -57,7 +65,7 @@ class Entity(object): # The properties and methods here are safe to overwrite when inheriting # this class. These may be used to customize the behavior of the entity. @property - def should_poll(self): + def should_poll(self) -> bool: """Return True if entity has to be polled for state. False if entity pushes its state to HA. @@ -65,17 +73,17 @@ class Entity(object): return True @property - def unique_id(self): + def unique_id(self) -> str: """Return an unique ID.""" return "{}.{}".format(self.__class__, id(self)) @property - def name(self): + def name(self) -> Optional[str]: """Return the name of the entity.""" return None @property - def state(self): + def state(self) -> str: """Return the state of the entity.""" return STATE_UNKNOWN @@ -111,22 +119,22 @@ class Entity(object): return None @property - def hidden(self): + def hidden(self) -> bool: """Return True if the entity should be hidden from UIs.""" return False @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" return True @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return True if unable to access real state of the entity.""" return False @property - def force_update(self): + def force_update(self) -> bool: """Return True if state updates should be forced. If True, a state change will be triggered anytime the state property is @@ -138,14 +146,14 @@ class Entity(object): """Retrieve latest state.""" pass - entity_id = None + entity_id = None # type: str # DO NOT OVERWRITE # These properties and methods are either managed by Home Assistant or they # are used to perform a very specific function. Overwriting these may # produce undesirable effects in the entity's operation. - hass = None + hass = None # type: Optional[HomeAssistant] def update_ha_state(self, force_refresh=False): """Update Home Assistant with current state of entity. @@ -232,24 +240,24 @@ class ToggleEntity(Entity): # pylint: disable=no-self-use @property - def state(self): + def state(self) -> str: """Return the state.""" return STATE_ON if self.is_on else STATE_OFF @property - def is_on(self): + def is_on(self) -> bool: """Return True if entity is on.""" raise NotImplementedError() - def turn_on(self, **kwargs): + def turn_on(self, **kwargs) -> None: """Turn the entity on.""" raise NotImplementedError() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs) -> None: """Turn the entity off.""" raise NotImplementedError() - def toggle(self, **kwargs): + def toggle(self, **kwargs) -> None: """Toggle the entity off.""" if self.is_on: self.turn_off(**kwargs) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 48a4bffc6a3..bc1382ef982 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -3,10 +3,12 @@ import logging import threading from itertools import islice +import voluptuous as vol + import homeassistant.util.dt as date_util from homeassistant.const import EVENT_TIME_CHANGED, CONF_CONDITION from homeassistant.helpers.event import track_point_in_utc_time -from homeassistant.helpers import service, condition +from homeassistant.helpers import service, condition, template import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -68,9 +70,17 @@ class Script(): self._delay_listener = None self.run(variables) + delay = action[CONF_DELAY] + + if isinstance(delay, str): + delay = vol.All( + cv.time_period, + cv.positive_timedelta)( + template.render(self.hass, delay)) + self._delay_listener = track_point_in_utc_time( self.hass, script_delay, - date_util.utcnow() + action[CONF_DELAY]) + date_util.utcnow() + delay) self._cur = cur + 1 if self._change_listener: self._change_listener() diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 41de1782a1f..8ccfd1e0bf5 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -15,6 +15,7 @@ from homeassistant.util import convert, dt as dt_util, location as loc_util _LOGGER = logging.getLogger(__name__) _SENTINEL = object() +DATE_STR_FORMAT = "%Y-%m-%d %H:%M:%S" def render_with_possible_json_value(hass, template, value, @@ -248,6 +249,25 @@ def multiply(value, amount): return value +def timestamp_local(value): + """Filter to convert given timestamp to local date/time.""" + try: + return dt_util.as_local( + dt_util.utc_from_timestamp(value)).strftime(DATE_STR_FORMAT) + except (ValueError, TypeError): + # If timestamp can't be converted + return value + + +def timestamp_utc(value): + """Filter to convert gibrn timestamp to UTC date/time.""" + try: + return dt_util.utc_from_timestamp(value).strftime(DATE_STR_FORMAT) + except (ValueError, TypeError): + # If timestamp can't be converted + return value + + def forgiving_float(value): """Try to convert value to a float.""" try: @@ -266,3 +286,5 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): ENV = TemplateEnvironment() ENV.filters['round'] = forgiving_round ENV.filters['multiply'] = multiply +ENV.filters['timestamp_local'] = timestamp_local +ENV.filters['timestamp_utc'] = timestamp_utc diff --git a/homeassistant/loader.py b/homeassistant/loader.py index edc54cce61b..591e7d229a3 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -16,21 +16,30 @@ import os import pkgutil import sys +from types import ModuleType +# pylint: disable=unused-import +from typing import Optional, Sequence, Set, Dict # NOQA + from homeassistant.const import PLATFORM_FORMAT from homeassistant.util import OrderedSet +# Typing imports +# pylint: disable=using-constant-test,unused-import +if False: + from homeassistant.core import HomeAssistant # NOQA + PREPARED = False # List of available components -AVAILABLE_COMPONENTS = [] +AVAILABLE_COMPONENTS = [] # type: List[str] # Dict of loaded components mapped name => module -_COMPONENT_CACHE = {} +_COMPONENT_CACHE = {} # type: Dict[str, ModuleType] _LOGGER = logging.getLogger(__name__) -def prepare(hass): +def prepare(hass: 'HomeAssistant'): """Prepare the loading of components.""" global PREPARED # pylint: disable=global-statement @@ -71,19 +80,19 @@ def prepare(hass): PREPARED = True -def set_component(comp_name, component): +def set_component(comp_name: str, component: ModuleType) -> None: """Set a component in the cache.""" _check_prepared() _COMPONENT_CACHE[comp_name] = component -def get_platform(domain, platform): +def get_platform(domain: str, platform: str) -> Optional[ModuleType]: """Try to load specified platform.""" return get_component(PLATFORM_FORMAT.format(domain, platform)) -def get_component(comp_name): +def get_component(comp_name) -> Optional[ModuleType]: """Try to load specified component. Looks in config dir first, then built-in components. @@ -148,7 +157,7 @@ def get_component(comp_name): return None -def load_order_components(components): +def load_order_components(components: Sequence[str]) -> OrderedSet: """Take in a list of components we want to load. - filters out components we cannot load @@ -178,7 +187,7 @@ def load_order_components(components): return load_order -def load_order_component(comp_name): +def load_order_component(comp_name: str) -> OrderedSet: """Return an OrderedSet of components in the correct order of loading. Raises HomeAssistantError if a circular dependency is detected. @@ -187,7 +196,8 @@ def load_order_component(comp_name): return _load_order_component(comp_name, OrderedSet(), set()) -def _load_order_component(comp_name, load_order, loading): +def _load_order_component(comp_name: str, load_order: OrderedSet, + loading: Set) -> OrderedSet: """Recursive function to get load order of components.""" component = get_component(comp_name) @@ -224,7 +234,7 @@ def _load_order_component(comp_name, load_order, loading): return load_order -def _check_prepared(): +def _check_prepared() -> None: """Issue a warning if loader.prepare() has never been called.""" if not PREPARED: _LOGGER.warning(( diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 6c49decdff2..409d276caf5 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -21,7 +21,7 @@ import homeassistant.bootstrap as bootstrap import homeassistant.core as ha from homeassistant.const import ( HTTP_HEADER_HA_AUTH, SERVER_PORT, URL_API, URL_API_EVENT_FORWARD, - URL_API_EVENTS, URL_API_EVENTS_EVENT, URL_API_SERVICES, + URL_API_EVENTS, URL_API_EVENTS_EVENT, URL_API_SERVICES, URL_API_CONFIG, URL_API_SERVICES_SERVICE, URL_API_STATES, URL_API_STATES_ENTITY, HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON) from homeassistant.exceptions import HomeAssistantError @@ -75,7 +75,7 @@ class API(object): return self.status == APIStatus.OK - def __call__(self, method, path, data=None): + def __call__(self, method, path, data=None, timeout=5): """Make a call to the Home Assistant API.""" if data is not None: data = json.dumps(data, cls=JSONEncoder) @@ -85,10 +85,11 @@ class API(object): try: if method == METHOD_GET: return requests.get( - url, params=data, timeout=5, headers=self._headers) + url, params=data, timeout=timeout, headers=self._headers) else: return requests.request( - method, url, data=data, timeout=5, headers=self._headers) + method, url, data=data, timeout=timeout, + headers=self._headers) except requests.exceptions.ConnectionError: _LOGGER.exception("Error connecting to server") @@ -510,12 +511,12 @@ def get_services(api): return {} -def call_service(api, domain, service, service_data=None): +def call_service(api, domain, service, service_data=None, timeout=5): """Call a service at the remote API.""" try: req = api(METHOD_POST, URL_API_SERVICES_SERVICE.format(domain, service), - service_data) + service_data, timeout=timeout) if req.status_code != 200: _LOGGER.error("Error calling service: %d - %s", @@ -523,3 +524,17 @@ def call_service(api, domain, service, service_data=None): except HomeAssistantError: _LOGGER.exception("Error calling service") + + +def get_config(api): + """Return configuration.""" + try: + req = api(METHOD_GET, URL_API_CONFIG) + + return req.json() if req.status_code == 200 else {} + + except (HomeAssistantError, ValueError): + # ValueError if req.json() can't parse the JSON + _LOGGER.exception("Got unexpected configuration results") + + return {} diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py index a0e16efc7d1..87771045b66 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -3,7 +3,7 @@ import importlib import os -def run(args): +def run(args: str) -> int: """Run a script.""" scripts = [fil[:-3] for fil in os.listdir(os.path.dirname(__file__)) if fil.endswith('.py') and fil != '__init__.py'] @@ -19,4 +19,4 @@ def run(args): return 1 script = importlib.import_module('homeassistant.scripts.' + args[0]) - return script.run(args[1:]) + return script.run(args[1:]) # type: ignore diff --git a/homeassistant/scripts/ensure_config.py b/homeassistant/scripts/ensure_config.py new file mode 100644 index 00000000000..51d6e0a992e --- /dev/null +++ b/homeassistant/scripts/ensure_config.py @@ -0,0 +1,33 @@ +"""Script to ensure a configuration file exists.""" +import argparse +import os + +import homeassistant.config as config_util + + +def run(args): + """Handle ensure config commandline script.""" + parser = argparse.ArgumentParser( + description=("Ensure a Home Assistant config exists, " + "creates one if necessary.")) + parser.add_argument( + '-c', '--config', + metavar='path_to_config_dir', + default=config_util.get_default_config_dir(), + help="Directory that contains the Home Assistant configuration") + parser.add_argument( + '--script', + choices=['ensure_config']) + + args = parser.parse_args() + + config_dir = os.path.join(os.getcwd(), args.config) + + # Test if configuration directory exists + if not os.path.isdir(config_dir): + print('Creating directory', config_dir) + os.makedirs(config_dir) + + config_path = config_util.ensure_config_exists(config_dir) + print('Configuration file:', config_path) + return 0 diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 67719c18208..e0f856c7444 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -1,5 +1,5 @@ """Helper methods for various modules.""" -import collections +from collections.abc import MutableSet from itertools import chain import threading import queue @@ -12,6 +12,8 @@ import string from functools import wraps from types import MappingProxyType +from typing import Any, Sequence + from .dt import as_local, utcnow RE_SANITIZE_FILENAME = re.compile(r'(~|\.\.|/|\\)') @@ -29,14 +31,14 @@ def sanitize_path(path): return RE_SANITIZE_PATH.sub("", path) -def slugify(text): +def slugify(text: str) -> str: """Slugify a given text.""" text = text.lower().replace(" ", "_") return RE_SLUGIFY.sub("", text) -def repr_helper(inp): +def repr_helper(inp: Any) -> str: """Help creating a more readable string representation of objects.""" if isinstance(inp, (dict, MappingProxyType)): return ", ".join( @@ -57,17 +59,18 @@ def convert(value, to_type, default=None): return default -def ensure_unique_string(preferred_string, current_strings): +def ensure_unique_string(preferred_string: str, + current_strings: Sequence[str]) -> str: """Return a string that is not present in current_strings. If preferred string exists will append _2, _3, .. """ test_string = preferred_string - current_strings = set(current_strings) + current_strings_set = set(current_strings) tries = 1 - while test_string in current_strings: + while test_string in current_strings_set: tries += 1 test_string = "{}_{}".format(preferred_string, tries) @@ -128,7 +131,7 @@ class OrderedEnum(enum.Enum): return NotImplemented -class OrderedSet(collections.MutableSet): +class OrderedSet(MutableSet): """Ordered set taken from http://code.activestate.com/recipes/576694/.""" def __init__(self, iterable=None): @@ -373,8 +376,6 @@ class ThreadPool(object): def block_till_done(self): """Block till current work is done.""" self._work_queue.join() - # import traceback - # traceback.print_stack() def stop(self): """Finish all the jobs and stops all the threads.""" @@ -399,7 +400,7 @@ class ThreadPool(object): # Get new item from work_queue job = self._work_queue.get().item - if job == self._quit_task: + if job is self._quit_task: self._work_queue.task_done() return diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index dd504b57065..e9671c77328 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -1,7 +1,8 @@ """Color util methods.""" import logging import math -# pylint: disable=unused-import + +from typing import Tuple _LOGGER = logging.getLogger(__name__) @@ -36,14 +37,14 @@ def color_name_to_rgb(color_name): # http://www.developers.meethue.com/documentation/color-conversions-rgb-xy # License: Code is given as is. Use at your own risk and discretion. # pylint: disable=invalid-name -def color_RGB_to_xy(R, G, B): +def color_RGB_to_xy(iR: int, iG: int, iB: int) -> Tuple[float, float, int]: """Convert from RGB color to XY color.""" - if R + G + B == 0: - return 0, 0, 0 + if iR + iG + iB == 0: + return 0.0, 0.0, 0 - R = R / 255 - B = B / 255 - G = G / 255 + R = iR / 255 + B = iB / 255 + G = iG / 255 # Gamma correction R = pow((R + 0.055) / (1.0 + 0.055), @@ -72,9 +73,10 @@ def color_RGB_to_xy(R, G, B): # taken from # https://github.com/benknight/hue-python-rgb-converter/blob/master/rgb_cie.py # Copyright (c) 2014 Benjamin Knight / MIT License. -def color_xy_brightness_to_RGB(vX, vY, brightness): +def color_xy_brightness_to_RGB(vX: float, vY: float, + ibrightness: int) -> Tuple[int, int, int]: """Convert from XYZ to RGB.""" - brightness /= 255. + brightness = ibrightness / 255. if brightness == 0: return (0, 0, 0) @@ -106,17 +108,18 @@ def color_xy_brightness_to_RGB(vX, vY, brightness): if max_component > 1: r, g, b = map(lambda x: x / max_component, [r, g, b]) - r, g, b = map(lambda x: int(x * 255), [r, g, b]) + ir, ig, ib = map(lambda x: int(x * 255), [r, g, b]) - return (r, g, b) + return (ir, ig, ib) -def _match_max_scale(input_colors, output_colors): +def _match_max_scale(input_colors: Tuple[int, ...], + output_colors: Tuple[int, ...]) -> Tuple[int, ...]: """Match the maximum value of the output to the input.""" max_in = max(input_colors) max_out = max(output_colors) if max_out == 0: - factor = 0 + factor = 0.0 else: factor = max_in / max_out return tuple(int(round(i * factor)) for i in output_colors) @@ -176,7 +179,8 @@ def color_temperature_to_rgb(color_temperature_kelvin): return (red, green, blue) -def _bound(color_component, minimum=0, maximum=255): +def _bound(color_component: float, minimum: float=0, + maximum: float=255) -> float: """ Bound the given color component value between the given min and max values. @@ -188,7 +192,7 @@ def _bound(color_component, minimum=0, maximum=255): return min(color_component_out, maximum) -def _get_red(temperature): +def _get_red(temperature: float) -> float: """Get the red component of the temperature in RGB space.""" if temperature <= 66: return 255 @@ -196,7 +200,7 @@ def _get_red(temperature): return _bound(tmp_red) -def _get_green(temperature): +def _get_green(temperature: float) -> float: """Get the green component of the given color temp in RGB space.""" if temperature <= 66: green = 99.4708025861 * math.log(temperature) - 161.1195681661 @@ -205,13 +209,13 @@ def _get_green(temperature): return _bound(green) -def _get_blue(tmp_internal): +def _get_blue(temperature: float) -> float: """Get the blue component of the given color temperature in RGB space.""" - if tmp_internal >= 66: + if temperature >= 66: return 255 - if tmp_internal <= 19: + if temperature <= 19: return 0 - blue = 138.5177312231 * math.log(tmp_internal - 10) - 305.0447927307 + blue = 138.5177312231 * math.log(temperature - 10) - 305.0447927307 return _bound(blue) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index b8b7a691859..a5724ee90e1 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -2,10 +2,13 @@ import datetime as dt import re +# pylint: disable=unused-import +from typing import Any, Union, Optional, Tuple # NOQA + import pytz DATE_STR_FORMAT = "%Y-%m-%d" -UTC = DEFAULT_TIME_ZONE = pytz.utc +UTC = DEFAULT_TIME_ZONE = pytz.utc # type: pytz.UTC # Copyright (c) Django Software Foundation and individual contributors. @@ -19,16 +22,17 @@ DATETIME_RE = re.compile( ) -def set_default_time_zone(time_zone): +def set_default_time_zone(time_zone: dt.tzinfo) -> None: """Set a default time zone to be used when none is specified.""" global DEFAULT_TIME_ZONE # pylint: disable=global-statement + # NOTE: Remove in the future in favour of typing assert isinstance(time_zone, dt.tzinfo) DEFAULT_TIME_ZONE = time_zone -def get_time_zone(time_zone_str): +def get_time_zone(time_zone_str: str) -> Optional[dt.tzinfo]: """Get time zone from string. Return None if unable to determine.""" try: return pytz.timezone(time_zone_str) @@ -36,17 +40,17 @@ def get_time_zone(time_zone_str): return None -def utcnow(): +def utcnow() -> dt.datetime: """Get now in UTC time.""" return dt.datetime.now(UTC) -def now(time_zone=None): +def now(time_zone: dt.tzinfo=None) -> dt.datetime: """Get now in specified time zone.""" return dt.datetime.now(time_zone or DEFAULT_TIME_ZONE) -def as_utc(dattim): +def as_utc(dattim: dt.datetime) -> dt.datetime: """Return a datetime as UTC time. Assumes datetime without tzinfo to be in the DEFAULT_TIME_ZONE. @@ -70,7 +74,7 @@ def as_timestamp(dt_value): return parsed_dt.timestamp() -def as_local(dattim): +def as_local(dattim: dt.datetime) -> dt.datetime: """Convert a UTC datetime object to local time zone.""" if dattim.tzinfo == DEFAULT_TIME_ZONE: return dattim @@ -80,12 +84,13 @@ def as_local(dattim): return dattim.astimezone(DEFAULT_TIME_ZONE) -def utc_from_timestamp(timestamp): +def utc_from_timestamp(timestamp: float) -> dt.datetime: """Return a UTC time from a timestamp.""" return dt.datetime.utcfromtimestamp(timestamp).replace(tzinfo=UTC) -def start_of_local_day(dt_or_d=None): +def start_of_local_day(dt_or_d: + Union[dt.date, dt.datetime]=None) -> dt.datetime: """Return local datetime object of start of day from date or datetime.""" if dt_or_d is None: dt_or_d = now().date() @@ -98,7 +103,7 @@ def start_of_local_day(dt_or_d=None): # Copyright (c) Django Software Foundation and individual contributors. # All rights reserved. # https://github.com/django/django/blob/master/LICENSE -def parse_datetime(dt_str): +def parse_datetime(dt_str: str) -> dt.datetime: """Parse a string and return a datetime.datetime. This function supports time zone offsets. When the input contains one, @@ -109,25 +114,27 @@ def parse_datetime(dt_str): match = DATETIME_RE.match(dt_str) if not match: return None - kws = match.groupdict() + kws = match.groupdict() # type: Dict[str, Any] if kws['microsecond']: kws['microsecond'] = kws['microsecond'].ljust(6, '0') - tzinfo = kws.pop('tzinfo') - if tzinfo == 'Z': + tzinfo_str = kws.pop('tzinfo') + if tzinfo_str == 'Z': tzinfo = UTC - elif tzinfo is not None: - offset_mins = int(tzinfo[-2:]) if len(tzinfo) > 3 else 0 - offset_hours = int(tzinfo[1:3]) + elif tzinfo_str is not None: + offset_mins = int(tzinfo_str[-2:]) if len(tzinfo_str) > 3 else 0 + offset_hours = int(tzinfo_str[1:3]) offset = dt.timedelta(hours=offset_hours, minutes=offset_mins) - if tzinfo[0] == '-': + if tzinfo_str[0] == '-': offset = -offset tzinfo = dt.timezone(offset) + else: + tzinfo = None kws = {k: int(v) for k, v in kws.items() if v is not None} kws['tzinfo'] = tzinfo return dt.datetime(**kws) -def parse_date(dt_str): +def parse_date(dt_str: str) -> dt.date: """Convert a date string to a date object.""" try: return dt.datetime.strptime(dt_str, DATE_STR_FORMAT).date() @@ -154,7 +161,7 @@ def parse_time(time_str): # Found in this gist: https://gist.github.com/zhangsen/1199964 -def get_age(date): +def get_age(date: dt.datetime) -> str: # pylint: disable=too-many-return-statements """ Take a datetime and return its "age" as a string. @@ -164,14 +171,14 @@ def get_age(date): be returned. Make sure date is not in the future, or else it won't work. """ - def formatn(number, unit): + def formatn(number: int, unit: str) -> str: """Add "unit" if it's plural.""" if number == 1: return "1 %s" % unit elif number > 1: return "%d %ss" % (number, unit) - def q_n_r(first, second): + def q_n_r(first: int, second: int) -> Tuple[int, int]: """Return quotient and remaining.""" return first // second, first % second @@ -196,7 +203,5 @@ def get_age(date): minute, second = q_n_r(second, 60) if minute > 0: return formatn(minute, 'minute') - if second > 0: - return formatn(second, 'second') - return "0 second" + return formatn(second, 'second') if second > 0 else "0 seconds" diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index a9b980bc871..1cc8ffe0b9f 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -5,8 +5,11 @@ detect_location_info and elevation are mocked by default during tests. """ import collections import math +from typing import Any, Optional, Tuple, Dict + import requests + ELEVATION_URL = 'http://maps.googleapis.com/maps/api/elevation/json' FREEGEO_API = 'https://freegeoip.io/json/' IP_API = 'http://ip-api.com/json' @@ -81,7 +84,8 @@ def elevation(latitude, longitude): # Source: https://github.com/maurycyp/vincenty # License: https://github.com/maurycyp/vincenty/blob/master/LICENSE # pylint: disable=too-many-locals, invalid-name, unused-variable -def vincenty(point1, point2, miles=False): +def vincenty(point1: Tuple[float, float], point2: Tuple[float, float], + miles: bool=False) -> Optional[float]: """ Vincenty formula (inverse method) to calculate the distance. @@ -148,7 +152,7 @@ def vincenty(point1, point2, miles=False): return round(s, 6) -def _get_freegeoip(): +def _get_freegeoip() -> Optional[Dict[str, Any]]: """Query freegeoip.io for location data.""" try: raw_info = requests.get(FREEGEO_API, timeout=5).json() @@ -169,7 +173,7 @@ def _get_freegeoip(): } -def _get_ip_api(): +def _get_ip_api() -> Optional[Dict[str, Any]]: """Query ip-api.com for location data.""" try: raw_info = requests.get(IP_API, timeout=5).json() diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 6894524d963..cf65e319552 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -6,13 +6,16 @@ import sys import threading from urllib.parse import urlparse +from typing import Optional + import pkg_resources _LOGGER = logging.getLogger(__name__) INSTALL_LOCK = threading.Lock() -def install_package(package, upgrade=True, target=None): +def install_package(package: str, upgrade: bool=True, + target: Optional[str]=None) -> bool: """Install a package on PyPi. Accepts pip compatible package strings. Return boolean if install successful. @@ -36,7 +39,7 @@ def install_package(package, upgrade=True, target=None): return False -def check_package_exists(package, lib_dir): +def check_package_exists(package: str, lib_dir: str) -> bool: """Check if a package is installed globally or in lib_dir. Returns True when the requirement is met. diff --git a/homeassistant/util/temperature.py b/homeassistant/util/temperature.py index 293ddcf44cf..59112a709ca 100644 --- a/homeassistant/util/temperature.py +++ b/homeassistant/util/temperature.py @@ -3,7 +3,7 @@ import logging -def fahrenheit_to_celcius(fahrenheit): +def fahrenheit_to_celcius(fahrenheit: float) -> float: """**DEPRECATED** Convert a Fahrenheit temperature to Celsius.""" logging.getLogger(__name__).warning( 'fahrenheit_to_celcius is now fahrenheit_to_celsius ' @@ -11,12 +11,12 @@ def fahrenheit_to_celcius(fahrenheit): return fahrenheit_to_celsius(fahrenheit) -def fahrenheit_to_celsius(fahrenheit): +def fahrenheit_to_celsius(fahrenheit: float) -> float: """Convert a Fahrenheit temperature to Celsius.""" return (fahrenheit - 32.0) / 1.8 -def celcius_to_fahrenheit(celcius): +def celcius_to_fahrenheit(celcius: float) -> float: """**DEPRECATED** Convert a Celsius temperature to Fahrenheit.""" logging.getLogger(__name__).warning( 'celcius_to_fahrenheit is now celsius_to_fahrenheit correcting ' @@ -24,6 +24,6 @@ def celcius_to_fahrenheit(celcius): return celsius_to_fahrenheit(celcius) -def celsius_to_fahrenheit(celsius): +def celsius_to_fahrenheit(celsius: float) -> float: """Convert a Celsius temperature to Fahrenheit.""" return celsius * 1.8 + 32.0 diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py index feafdc2c6ff..8b2521e3e9b 100644 --- a/homeassistant/util/yaml.py +++ b/homeassistant/util/yaml.py @@ -2,6 +2,7 @@ import logging import os from collections import OrderedDict +from typing import Union, List, Dict import glob import yaml @@ -21,15 +22,16 @@ _SECRET_YAML = 'secrets.yaml' class SafeLineLoader(yaml.SafeLoader): """Loader class that keeps track of line numbers.""" - def compose_node(self, parent, index): + def compose_node(self, parent: yaml.nodes.Node, index) -> yaml.nodes.Node: """Annotate a node with the first line it was seen.""" - last_line = self.line - node = super(SafeLineLoader, self).compose_node(parent, index) + last_line = self.line # type: int + node = super(SafeLineLoader, + self).compose_node(parent, index) # type: yaml.nodes.Node node.__line__ = last_line + 1 return node -def load_yaml(fname): +def load_yaml(fname: str) -> Union[List, Dict]: """Load a YAML file.""" try: with open(fname, encoding='utf-8') as conf_file: @@ -41,7 +43,8 @@ def load_yaml(fname): raise HomeAssistantError(exc) -def _include_yaml(loader, node): +def _include_yaml(loader: SafeLineLoader, + node: yaml.nodes.Node) -> Union[List, Dict]: """Load another YAML file and embeds it using the !include tag. Example: @@ -51,9 +54,10 @@ def _include_yaml(loader, node): return load_yaml(fname) -def _include_dir_named_yaml(loader, node): +def _include_dir_named_yaml(loader: SafeLineLoader, + node: yaml.nodes.Node): """Load multiple files from directory as a dictionary.""" - mapping = OrderedDict() + mapping = OrderedDict() # type: OrderedDict files = os.path.join(os.path.dirname(loader.name), node.value, '*.yaml') for fname in glob.glob(files): filename = os.path.splitext(os.path.basename(fname))[0] @@ -61,9 +65,10 @@ def _include_dir_named_yaml(loader, node): return mapping -def _include_dir_merge_named_yaml(loader, node): +def _include_dir_merge_named_yaml(loader: SafeLineLoader, + node: yaml.nodes.Node): """Load multiple files from directory as a merged dictionary.""" - mapping = OrderedDict() + mapping = OrderedDict() # type: OrderedDict files = os.path.join(os.path.dirname(loader.name), node.value, '*.yaml') for fname in glob.glob(files): if os.path.basename(fname) == _SECRET_YAML: @@ -74,17 +79,20 @@ def _include_dir_merge_named_yaml(loader, node): return mapping -def _include_dir_list_yaml(loader, node): +def _include_dir_list_yaml(loader: SafeLineLoader, + node: yaml.nodes.Node): """Load multiple files from directory as a list.""" files = os.path.join(os.path.dirname(loader.name), node.value, '*.yaml') return [load_yaml(f) for f in glob.glob(files) if os.path.basename(f) != _SECRET_YAML] -def _include_dir_merge_list_yaml(loader, node): +def _include_dir_merge_list_yaml(loader: SafeLineLoader, + node: yaml.nodes.Node): """Load multiple files from directory as a merged list.""" - files = os.path.join(os.path.dirname(loader.name), node.value, '*.yaml') - merged_list = [] + files = os.path.join(os.path.dirname(loader.name), + node.value, '*.yaml') # type: str + merged_list = [] # type: List for fname in glob.glob(files): if os.path.basename(fname) == _SECRET_YAML: continue @@ -94,12 +102,13 @@ def _include_dir_merge_list_yaml(loader, node): return merged_list -def _ordered_dict(loader, node): +def _ordered_dict(loader: SafeLineLoader, + node: yaml.nodes.MappingNode) -> OrderedDict: """Load YAML mappings into an ordered dictionary to preserve key order.""" loader.flatten_mapping(node) nodes = loader.construct_pairs(node) - seen = {} + seen = {} # type: Dict min_line = None for (key, _), (node, _) in zip(nodes, node.value): line = getattr(node, '__line__', 'unknown') @@ -116,12 +125,13 @@ def _ordered_dict(loader, node): seen[key] = line processed = OrderedDict(nodes) - processed.__config_file__ = loader.name - processed.__line__ = min_line + setattr(processed, '__config_file__', loader.name) + setattr(processed, '__line__', min_line) return processed -def _env_var_yaml(loader, 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] @@ -131,7 +141,8 @@ def _env_var_yaml(loader, node): # pylint: disable=protected-access -def _secret_yaml(loader, node): +def _secret_yaml(loader: SafeLineLoader, + node: yaml.nodes.Node): """Load secrets and embed it into the configuration YAML.""" # Create secret cache on loader and load secrets.yaml if not hasattr(loader, '_SECRET_CACHE'): diff --git a/requirements_all.txt b/requirements_all.txt index ff8783a54b4..ab861c8f941 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,10 +1,11 @@ # Home Assistant core requests>=2,<3 pyyaml>=3.11,<4 -pytz>=2016.4 +pytz>=2016.6.1 pip>=7.0.0 jinja2>=2.8 -voluptuous==0.8.9 +voluptuous==0.9.1 +typing>=3,<4 sqlalchemy==1.0.14 # homeassistant.components.isy994 @@ -46,7 +47,10 @@ blockchain==1.3.3 boto3==1.3.1 # homeassistant.components.http -cherrypy==6.0.2 +cherrypy==6.1.1 + +# homeassistant.components.media_player.directv +directpy==0.1 # homeassistant.components.notify.xmpp dnspython3==1.12.0 @@ -97,6 +101,9 @@ hikvision==0.4 # homeassistant.components.sensor.dht # http://github.com/mala-zaba/Adafruit_Python_DHT/archive/4101340de8d2457dd194bca1e8d11cbfc237e919.zip#Adafruit_DHT==1.1.0 +# homeassistant.components.light.flux_led +https://github.com/Danielhiversen/flux_led/archive/0.3.zip#flux_led==0.3 + # homeassistant.components.switch.dlink https://github.com/LinuxChristian/pyW215/archive/v0.1.1.zip#pyW215==0.1.1 @@ -138,6 +145,9 @@ https://github.com/jamespcole/home-assistant-nzb-clients/archive/616cad591540925 # homeassistant.components.qwikswitch https://github.com/kellerza/pyqwikswitch/archive/v0.4.zip#pyqwikswitch==0.4 +# homeassistant.components.media_player.russound_rnet +https://github.com/laf/russound/archive/0.1.6.zip#russound==0.1.6 + # homeassistant.components.ecobee https://github.com/nkgilley/python-ecobee-api/archive/4856a704670c53afe1882178a89c209b5f98533d.zip#python-ecobee==0.0.6 @@ -160,9 +170,6 @@ https://github.com/sander76/powerviewApi/archive/master.zip#powerviewApi==0.2 # homeassistant.components.mysensors https://github.com/theolind/pymysensors/archive/cc5d0b325e13c2b623fa934f69eea7cd4555f110.zip#pymysensors==0.6 -# homeassistant.components.notify.googlevoice -https://github.com/w1ll1am23/pygooglevoice-sms/archive/7c5ee9969b97a7992fc86a753fe9f20e3ffa3f7c.zip#pygooglevoice-sms==0.0.1 - # homeassistant.components.alarm_control_panel.simplisafe https://github.com/w1ll1am23/simplisafe-python/archive/586fede0e85fd69e56e516aaa8e97eb644ca8866.zip#simplisafe-python==0.0.1 @@ -179,7 +186,7 @@ insteon_hub==0.4.5 jsonrpc-requests==0.3 # homeassistant.components.knx -knxip==0.3.0 +knxip==0.3.2 # homeassistant.components.light.lifx liffylights==0.9.4 @@ -198,7 +205,7 @@ messagebird==1.2.0 mficlient==0.3.0 # homeassistant.components.discovery -netdisco==0.6.7 +netdisco==0.7.0 # homeassistant.components.sensor.neurio_energy neurio==0.2.10 @@ -278,10 +285,10 @@ pyenvisalink==1.0 pyfttt==0.3 # homeassistant.components.homematic -pyhomematic==0.1.9 +pyhomematic==0.1.10 # homeassistant.components.device_tracker.icloud -pyicloud==0.8.3 +pyicloud==0.9.1 # homeassistant.components.sensor.lastfm pylast==1.6.0 @@ -303,7 +310,7 @@ pynx584==0.2 pyowm==2.3.2 # homeassistant.components.switch.acer_projector -pyserial<=3.0 +pyserial<=3.1 # homeassistant.components.device_tracker.snmp # homeassistant.components.sensor.snmp @@ -331,10 +338,10 @@ python-nmap==0.6.0 python-pushover==0.2 # homeassistant.components.notify.telegram -python-telegram-bot==4.3.3 +python-telegram-bot==5.0.0 # homeassistant.components.sensor.twitch -python-twitch==1.2.0 +python-twitch==1.3.0 # homeassistant.components.wink # homeassistant.components.binary_sensor.wink @@ -344,13 +351,13 @@ python-twitch==1.2.0 # homeassistant.components.rollershutter.wink # homeassistant.components.sensor.wink # homeassistant.components.switch.wink -python-wink==0.7.10 +python-wink==0.7.11 # homeassistant.components.keyboard pyuserinput==0.1.9 # homeassistant.components.vera -pyvera==0.2.13 +pyvera==0.2.15 # homeassistant.components.wemo pywemo==0.4.3 @@ -374,10 +381,10 @@ schiene==0.17 scsgate==0.1.0 # homeassistant.components.notify.sendgrid -sendgrid>=1.6.0,<1.7.0 +sendgrid==3.0.7 # homeassistant.components.notify.slack -slacker==0.9.21 +slacker==0.9.24 # homeassistant.components.notify.xmpp sleekxmpp==1.3.1 diff --git a/requirements_test.txt b/requirements_test.txt index 649859f2506..233856e8363 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,5 +1,6 @@ flake8>=2.6.0 pylint>=1.5.6 +astroid>=1.4.8 coveralls>=1.1 pytest>=2.9.2 pytest-cov>=2.2.1 @@ -7,3 +8,4 @@ pytest-timeout>=1.0.0 pytest-capturelog>=0.7 pydocstyle>=1.0.0 requests_mock>=1.0 +mypy-lang>=0.4 diff --git a/script/build_frontend b/script/build_frontend index 7b9dad05e79..da484a943b0 100755 --- a/script/build_frontend +++ b/script/build_frontend @@ -2,36 +2,21 @@ cd "$(dirname "$0")/.." -cd homeassistant/components/frontend/www_static/home-assistant-polymer +cd homeassistant/components/frontend/www_static +rm -rf core.js* frontend.html* webcomponents-lite.min.js* panels +cd home-assistant-polymer +npm run clean npm run frontend_prod cp bower_components/webcomponentsjs/webcomponents-lite.min.js .. -cp build/frontend.html .. -gzip build/frontend.html -c -k -9 > ../frontend.html.gz -cp build/partial-map.html .. -gzip build/partial-map.html -c -k -9 > ../partial-map.html.gz -cp build/dev-tools.html .. -gzip build/dev-tools.html -c -k -9 > ../dev-tools.html.gz -cp build/_core_compiled.js ../core.js -gzip build/_core_compiled.js -c -k -9 > ../core.js.gz - +cp -r build/* .. node script/sw-precache.js cp build/service_worker.js .. -gzip build/service_worker.js -c -k -9 > ../service_worker.js.gz + +cd .. + +gzip -f -k -9 *.html *.js ./panels/*.html # Generate the MD5 hash of the new frontend -cd ../.. -echo '"""DO NOT MODIFY. Auto-generated by build_frontend script."""' > version.py -if [ $(command -v md5) ]; then - echo 'CORE = "'`md5 -q www_static/core.js`'"' >> version.py - echo 'UI = "'`md5 -q www_static/frontend.html`'"' >> version.py - echo 'MAP = "'`md5 -q www_static/partial-map.html`'"' >> version.py - echo 'DEV = "'`md5 -q www_static/dev-tools.html`'"' >> version.py -elif [ $(command -v md5sum) ]; then - echo 'CORE = "'`md5sum www_static/core.js | cut -c-32`'"' >> version.py - echo 'UI = "'`md5sum www_static/frontend.html | cut -c-32`'"' >> version.py - echo 'MAP = "'`md5sum www_static/partial-map.html | cut -c-32`'"' >> version.py - echo 'DEV = "'`md5sum www_static/dev-tools.html | cut -c-32`'"' >> version.py -else - echo 'Could not find an MD5 utility' -fi +cd ../../../.. +script/fingerprint_frontend.py diff --git a/script/fingerprint_frontend.py b/script/fingerprint_frontend.py new file mode 100755 index 00000000000..09560cee0f0 --- /dev/null +++ b/script/fingerprint_frontend.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +"""Generate a file with all md5 hashes of the assets.""" +from collections import OrderedDict +import glob +import hashlib +import json + +fingerprint_file = 'homeassistant/components/frontend/version.py' +base_dir = 'homeassistant/components/frontend/www_static/' + + +def fingerprint(): + """Fingerprint the frontend files.""" + files = (glob.glob(base_dir + '**/*.html') + + glob.glob(base_dir + '*.html') + + glob.glob(base_dir + 'core.js')) + + md5s = OrderedDict() + + for fil in sorted(files): + name = fil[len(base_dir):] + with open(fil) as fp: + md5 = hashlib.md5(fp.read().encode('utf-8')).hexdigest() + md5s[name] = md5 + + template = """\"\"\"DO NOT MODIFY. Auto-generated by script/fingerprint_frontend.\"\"\" + +FINGERPRINTS = {} +""" + + result = template.format(json.dumps(md5s, indent=4)) + + with open(fingerprint_file, 'w') as fp: + fp.write(result) + +if __name__ == '__main__': + fingerprint() diff --git a/script/update_mdi.py b/script/update_mdi.py index 96682a26bfa..135b2be2046 100755 --- a/script/update_mdi.py +++ b/script/update_mdi.py @@ -1,38 +1,24 @@ #!/usr/bin/env python3 + """Download the latest Polymer v1 iconset for materialdesignicons.com.""" -import hashlib import gzip import os import re import requests import sys +from fingerprint_frontend import fingerprint + GETTING_STARTED_URL = ('https://raw.githubusercontent.com/Templarian/' 'MaterialDesign/master/site/getting-started.savvy') DOWNLOAD_LINK = re.compile(r'(/api/download/polymer/v1/([A-Z0-9-]{36}))') START_ICONSET = '=2,<3', 'pyyaml>=3.11,<4', - 'pytz>=2016.4', + 'pytz>=2016.6.1', 'pip>=7.0.0', 'jinja2>=2.8', - 'voluptuous==0.8.9', + 'voluptuous==0.9.1', + 'typing>=3,<4', 'sqlalchemy==1.0.14', ] diff --git a/tests/common.py b/tests/common.py index 26d466bc4b8..4fd9da96ae3 100644 --- a/tests/common.py +++ b/tests/common.py @@ -18,7 +18,7 @@ _TEST_INSTANCE_PORT = SERVER_PORT def get_test_config_dir(): """Return a path to a test config dir.""" - return os.path.join(os.path.dirname(__file__), "config") + return os.path.join(os.path.dirname(__file__), "testing_config") def get_test_home_assistant(num_threads=None): diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 388fe7db415..a88b4d18de4 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -46,17 +46,17 @@ class TestComponentsDeviceTracker(unittest.TestCase): self.assertFalse(device_tracker.is_on(self.hass, entity_id)) - def test_reading_broken_yaml_config(self): + def test_reading_broken_yaml_config(self): # pylint: disable=no-self-use """Test when known devices contains invalid data.""" - with tempfile.NamedTemporaryFile() as fp: + with tempfile.NamedTemporaryFile() as fpt: # file is empty - assert device_tracker.load_config(fp.name, None, False, 0) == [] + assert device_tracker.load_config(fpt.name, None, False, 0) == [] - fp.write('100'.encode('utf-8')) - fp.flush() + fpt.write('100'.encode('utf-8')) + fpt.flush() # file contains a non-dict format - assert device_tracker.load_config(fp.name, None, False, 0) == [] + assert device_tracker.load_config(fpt.name, None, False, 0) == [] def test_reading_yaml_config(self): """Test the rendering of the YAML configuration.""" @@ -79,6 +79,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): """Test with no YAML file.""" self.assertTrue(device_tracker.setup(self.hass, {})) + # pylint: disable=invalid-name def test_adding_unknown_device_to_config(self): """Test the adding of unknown devices to configuration file.""" scanner = get_component('device_tracker.test').SCANNER @@ -169,7 +170,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}})) self.assertTrue(self.hass.states.get(entity_id) - .attributes.get(ATTR_HIDDEN)) + .attributes.get(ATTR_HIDDEN)) def test_group_all_devices(self): """Test grouping of devices.""" @@ -211,6 +212,20 @@ class TestComponentsDeviceTracker(unittest.TestCase): mac=mac, dev_id=dev_id, host_name=host_name, location_name=location_name, gps=gps) + @patch('homeassistant.components.device_tracker.DeviceTracker.see') + def test_see_service_unicode_dev_id(self, mock_see): + """Test the see service with a unicode dev_id and NO MAC.""" + self.assertTrue(device_tracker.setup(self.hass, {})) + params = { + 'dev_id': chr(233), # e' acute accent from icloud + 'host_name': 'example.com', + 'location_name': 'Work', + 'gps': [.3, .8] + } + device_tracker.see(self.hass, **params) + self.hass.pool.block_till_done() + mock_see.assert_called_once_with(**params) + def test_not_write_duplicate_yaml_keys(self): """Test that the device tracker will not generate invalid YAML.""" self.assertTrue(device_tracker.setup(self.hass, {})) diff --git a/tests/components/light/test_rfxtrx.py b/tests/components/light/test_rfxtrx.py index 3eeb06be24e..cd4cecd8505 100644 --- a/tests/components/light/test_rfxtrx.py +++ b/tests/components/light/test_rfxtrx.py @@ -148,6 +148,32 @@ class TestLightRfxtrx(unittest.TestCase): self.assertTrue(entity.is_on) self.assertEqual(entity.brightness, 255) + entity.turn_off() + entity_id = rfxtrx_core.RFX_DEVICES['213c7f216'].entity_id + entity_hass = self.hass.states.get(entity_id) + self.assertEqual('Test', entity_hass.name) + self.assertEqual('off', entity_hass.state) + + entity.turn_on() + entity_hass = self.hass.states.get(entity_id) + self.assertEqual('on', entity_hass.state) + + entity.turn_off() + entity_hass = self.hass.states.get(entity_id) + self.assertEqual('off', entity_hass.state) + + entity.turn_on(brightness=100) + entity_hass = self.hass.states.get(entity_id) + self.assertEqual('on', entity_hass.state) + + entity.turn_on(brightness=10) + entity_hass = self.hass.states.get(entity_id) + self.assertEqual('on', entity_hass.state) + + entity.turn_on(brightness=255) + entity_hass = self.hass.states.get(entity_id) + self.assertEqual('on', entity_hass.state) + def test_several_lights(self): """Test with 3 lights.""" self.assertTrue(_setup_component(self.hass, 'light', { diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py new file mode 100644 index 00000000000..8647926445d --- /dev/null +++ b/tests/components/media_player/test_sonos.py @@ -0,0 +1,158 @@ +"""The tests for the Demo Media player platform.""" +import unittest +import soco.snapshot +from unittest import mock +import soco + +from homeassistant.components.media_player import sonos + +from tests.common import get_test_home_assistant + +ENTITY_ID = 'media_player.kitchen' + + +class socoDiscoverMock(): + """Mock class for the soco.discover method.""" + + def discover(interface_addr): + """Return tuple of soco.SoCo objects representing found speakers.""" + return {SoCoMock('192.0.2.1')} + + +class SoCoMock(): + """Mock class for the soco.SoCo object.""" + + def __init__(self, ip): + """Initialize soco object.""" + self.ip_address = ip + self.is_visible = True + + def get_speaker_info(self): + """Return a dict with various data points about the speaker.""" + return {'serial_number': 'B8-E9-37-BO-OC-BA:2', + 'software_version': '32.11-30071', + 'uid': 'RINCON_B8E937BOOCBA02500', + 'zone_icon': 'x-rincon-roomicon:kitchen', + 'mac_address': 'B8:E9:37:BO:OC:BA', + 'zone_name': 'Kitchen', + 'hardware_version': '1.8.1.2-1'} + + def get_current_transport_info(self): + """Return a dict with the current state of the speaker.""" + return {'current_transport_speed': '1', + 'current_transport_state': 'STOPPED', + 'current_transport_status': 'OK'} + + def get_current_track_info(self): + """Return a dict with the current track information.""" + return {'album': '', + 'uri': '', + 'title': '', + 'artist': '', + 'duration': '0:00:00', + 'album_art': '', + 'position': '0:00:00', + 'playlist_position': '0', + 'metadata': ''} + + def is_coordinator(self): + """Return true if coordinator.""" + return True + + def partymode(self): + """Cause the speaker to join all other speakers in the network.""" + return + + def unjoin(self): + """Cause the speaker to separate itself from other speakers.""" + return + + +class TestSonosMediaPlayer(unittest.TestCase): + """Test the media_player module.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def monkey_available(self): + return True + + # Monkey patches + self.real_available = sonos.SonosDevice.available + sonos.SonosDevice.available = monkey_available + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + # Monkey patches + sonos.SonosDevice.available = self.real_available + sonos.DEVICES = [] + self.hass.stop() + + @mock.patch('soco.SoCo', new=SoCoMock) + def test_ensure_setup_discovery(self): + """Test a single device using the autodiscovery provided by HASS.""" + sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') + + # Ensure registration took place (#2558) + self.assertEqual(len(sonos.DEVICES), 1) + self.assertEqual(sonos.DEVICES[0].name, 'Kitchen') + + @mock.patch('soco.SoCo', new=SoCoMock) + def test_ensure_setup_config(self): + """Test a single address config'd by the HASS config file.""" + sonos.setup_platform(self.hass, + {'hosts': '192.0.2.1'}, + mock.MagicMock()) + + # Ensure registration took place (#2558) + self.assertEqual(len(sonos.DEVICES), 1) + self.assertEqual(sonos.DEVICES[0].name, 'Kitchen') + + @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch.object(soco, 'discover', new=socoDiscoverMock.discover) + def test_ensure_setup_sonos_discovery(self): + """Test a single device using the autodiscovery provided by Sonos.""" + sonos.setup_platform(self.hass, {}, mock.MagicMock()) + self.assertEqual(len(sonos.DEVICES), 1) + self.assertEqual(sonos.DEVICES[0].name, 'Kitchen') + + @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch.object(SoCoMock, 'partymode') + def test_sonos_group_players(self, partymodeMock): + """Ensuring soco methods called for sonos_group_players service.""" + sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') + device = sonos.DEVICES[-1] + partymodeMock.return_value = True + device.group_players() + partymodeMock.assert_called_once_with() + + @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch.object(SoCoMock, 'unjoin') + def test_sonos_unjoin(self, unjoinMock): + """Ensuring soco methods called for sonos_unjoin service.""" + sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') + device = sonos.DEVICES[-1] + unjoinMock.return_value = True + device.unjoin() + unjoinMock.assert_called_once_with() + + @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch.object(soco.snapshot.Snapshot, 'snapshot') + def test_sonos_snapshot(self, snapshotMock): + """Ensuring soco methods called for sonos_snapshot service.""" + sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') + device = sonos.DEVICES[-1] + snapshotMock.return_value = True + device.snapshot() + snapshotMock.assert_called_once_with() + + @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch.object(soco.snapshot.Snapshot, 'restore') + def test_sonos_restore(self, restoreMock): + """Ensuring soco methods called for sonos_restor service.""" + sonos.setup_platform(self.hass, {}, mock.MagicMock(), '192.0.2.1') + device = sonos.DEVICES[-1] + restoreMock.return_value = True + device.restore() + restoreMock.assert_called_once_with(True) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 7519443f1e4..5ebd951a682 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -1,8 +1,8 @@ """The tests for the Recorder component.""" -# pylint: disable=too-many-public-methods,protected-access -import unittest +# pylint: disable=protected-access import json from datetime import datetime, timedelta +import unittest from unittest.mock import patch from homeassistant.const import MATCH_ALL diff --git a/tests/components/recorder/test_models.py b/tests/components/recorder/test_models.py index 55c3e019f15..c616f3d0af1 100644 --- a/tests/components/recorder/test_models.py +++ b/tests/components/recorder/test_models.py @@ -1,5 +1,4 @@ """The tests for the Recorder component.""" -# pylint: disable=too-many-public-methods,protected-access import unittest from datetime import datetime @@ -12,32 +11,35 @@ from homeassistant.util import dt from homeassistant.components.recorder.models import ( Base, Events, States, RecorderRuns) -engine = None -Session = None +ENGINE = None +SESSION = None -def setUpModule(): +def setUpModule(): # pylint: disable=invalid-name """Set up a database to use.""" - global engine, Session + global ENGINE + global SESSION - engine = create_engine("sqlite://") - Base.metadata.create_all(engine) - session_factory = sessionmaker(bind=engine) - Session = scoped_session(session_factory) + ENGINE = create_engine("sqlite://") + Base.metadata.create_all(ENGINE) + session_factory = sessionmaker(bind=ENGINE) + SESSION = scoped_session(session_factory) -def tearDownModule(): +def tearDownModule(): # pylint: disable=invalid-name """Close database.""" - global engine, Session + global ENGINE + global SESSION - engine.dispose() - engine = None - Session = None + ENGINE.dispose() + ENGINE = None + SESSION = None class TestEvents(unittest.TestCase): """Test Events model.""" + # pylint: disable=no-self-use def test_from_event(self): """Test converting event to db event.""" event = ha.Event('test_event', { @@ -49,6 +51,8 @@ class TestEvents(unittest.TestCase): class TestStates(unittest.TestCase): """Test States model.""" + # pylint: disable=no-self-use + def test_from_event(self): """Test converting event to db state.""" state = ha.State('sensor.temperature', '18') @@ -78,14 +82,14 @@ class TestStates(unittest.TestCase): class TestRecorderRuns(unittest.TestCase): """Test recorder run model.""" - def setUp(self): + def setUp(self): # pylint: disable=invalid-name """Set up recorder runs.""" - self.session = session = Session() + self.session = session = SESSION() session.query(Events).delete() session.query(States).delete() session.query(RecorderRuns).delete() - def tearDown(self): + def tearDown(self): # pylint: disable=invalid-name """Clean up.""" self.session.rollback() diff --git a/tests/components/sensor/test_rfxtrx.py b/tests/components/sensor/test_rfxtrx.py index 6714dc70428..2d38de5eab5 100644 --- a/tests/components/sensor/test_rfxtrx.py +++ b/tests/components/sensor/test_rfxtrx.py @@ -75,6 +75,12 @@ class TestSensorRfxtrx(unittest.TestCase): self.assertEqual(TEMP_CELSIUS, entity.unit_of_measurement) self.assertEqual(None, entity.state) + entity_id = rfxtrx_core.RFX_DEVICES['sensor_0502']['Temperature']\ + .entity_id + entity = self.hass.states.get(entity_id) + self.assertEqual('Test', entity.name) + self.assertEqual('unknown', entity.state) + def test_several_sensors(self): """Test with 3 sensors.""" self.assertTrue(_setup_component(self.hass, 'sensor', { diff --git a/tests/components/switch/test_flux.py b/tests/components/switch/test_flux.py index ee20daf07ac..78d1f5190d6 100644 --- a/tests/components/switch/test_flux.py +++ b/tests/components/switch/test_flux.py @@ -18,7 +18,6 @@ class TestSwitchFlux(unittest.TestCase): def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - # self.hass.config.components = ['flux', 'sun', 'light'] def tearDown(self): """Stop everything that was started.""" diff --git a/tests/components/switch/test_rfxtrx.py b/tests/components/switch/test_rfxtrx.py index 8a36072304b..f0146719e75 100644 --- a/tests/components/switch/test_rfxtrx.py +++ b/tests/components/switch/test_rfxtrx.py @@ -154,6 +154,17 @@ class TestSwitchRfxtrx(unittest.TestCase): entity.turn_off() self.assertFalse(entity.is_on) + entity_id = rfxtrx_core.RFX_DEVICES['213c7f216'].entity_id + entity_hass = self.hass.states.get(entity_id) + self.assertEqual('Test', entity_hass.name) + self.assertEqual('off', entity_hass.state) + entity.turn_on() + entity_hass = self.hass.states.get(entity_id) + self.assertEqual('on', entity_hass.state) + entity.turn_off() + entity_hass = self.hass.states.get(entity_id) + self.assertEqual('off', entity_hass.state) + def test_several_switches(self): """Test with 3 switches.""" self.assertTrue(_setup_component(self.hass, 'switch', { diff --git a/tests/components/test_demo.py b/tests/components/test_demo.py index c6abe66a7ce..1c9203c944c 100644 --- a/tests/components/test_demo.py +++ b/tests/components/test_demo.py @@ -2,7 +2,6 @@ import json import os import unittest -from unittest.mock import patch from homeassistant.components import demo, device_tracker from homeassistant.remote import JSONEncoder @@ -10,7 +9,6 @@ from homeassistant.remote import JSONEncoder from tests.common import mock_http_component, get_test_home_assistant -@patch('homeassistant.components.sun.setup') class TestDemo(unittest.TestCase): """Test the Demo component.""" @@ -28,19 +26,19 @@ class TestDemo(unittest.TestCase): except FileNotFoundError: pass - def test_if_demo_state_shows_by_default(self, mock_sun_setup): + def test_if_demo_state_shows_by_default(self): """Test if demo state shows if we give no configuration.""" demo.setup(self.hass, {demo.DOMAIN: {}}) self.assertIsNotNone(self.hass.states.get('a.Demo_Mode')) - def test_hiding_demo_state(self, mock_sun_setup): + def test_hiding_demo_state(self): """Test if you can hide the demo card.""" demo.setup(self.hass, {demo.DOMAIN: {'hide_demo_state': 1}}) self.assertIsNone(self.hass.states.get('a.Demo_Mode')) - def test_all_entities_can_be_loaded_over_json(self, mock_sun_setup): + def test_all_entities_can_be_loaded_over_json(self): """Test if you can hide the demo card.""" demo.setup(self.hass, {demo.DOMAIN: {'hide_demo_state': 1}}) diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index 083ebd2eb0c..679029f85ea 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -7,7 +7,7 @@ import unittest import requests import homeassistant.bootstrap as bootstrap -import homeassistant.components.http as http +from homeassistant.components import frontend, http from homeassistant.const import HTTP_HEADER_HA_AUTH from tests.common import get_test_instance_port, get_test_home_assistant @@ -48,6 +48,7 @@ def setUpModule(): # pylint: disable=invalid-name def tearDownModule(): # pylint: disable=invalid-name """Stop everything that was started.""" hass.stop() + frontend.PANELS = {} class TestFrontend(unittest.TestCase): diff --git a/tests/components/test_group.py b/tests/components/test_group.py index 5c23d6ca0cd..d815489ae21 100644 --- a/tests/components/test_group.py +++ b/tests/components/test_group.py @@ -5,7 +5,7 @@ import unittest from homeassistant.bootstrap import _setup_component from homeassistant.const import ( STATE_ON, STATE_OFF, STATE_HOME, STATE_UNKNOWN, ATTR_ICON, ATTR_HIDDEN, - ATTR_ASSUMED_STATE, ) + ATTR_ASSUMED_STATE, STATE_NOT_HOME, ) import homeassistant.components.group as group from tests.common import get_test_home_assistant @@ -294,3 +294,17 @@ class TestComponentsGroup(unittest.TestCase): state = self.hass.states.get(test_group.entity_id) self.assertIsNone(state.attributes.get(ATTR_ASSUMED_STATE)) + + def test_group_updated_after_device_tracker_zone_change(self): + """Test group state when device tracker in group changes zone.""" + self.hass.states.set('device_tracker.Adam', STATE_HOME) + self.hass.states.set('device_tracker.Eve', STATE_NOT_HOME) + self.hass.pool.block_till_done() + group.Group( + self.hass, 'peeps', + ['device_tracker.Adam', 'device_tracker.Eve']) + self.hass.states.set('device_tracker.Adam', 'cool_state_not_home') + self.hass.pool.block_till_done() + self.assertEqual(STATE_NOT_HOME, + self.hass.states.get( + group.ENTITY_ID_FORMAT.format('peeps')).state) diff --git a/tests/components/test_input_slider.py b/tests/components/test_input_slider.py index f69553a21f7..3c13f797c42 100644 --- a/tests/components/test_input_slider.py +++ b/tests/components/test_input_slider.py @@ -49,16 +49,22 @@ class TestInputSlider(unittest.TestCase): entity_id = 'input_slider.test_1' state = self.hass.states.get(entity_id) - self.assertEqual('50', state.state) + self.assertEqual(50, float(state.state)) + + input_slider.select_value(self.hass, entity_id, '30.4') + self.hass.pool.block_till_done() + + state = self.hass.states.get(entity_id) + self.assertEqual(30.4, float(state.state)) input_slider.select_value(self.hass, entity_id, '70') self.hass.pool.block_till_done() state = self.hass.states.get(entity_id) - self.assertEqual('70', state.state) + self.assertEqual(70, float(state.state)) input_slider.select_value(self.hass, entity_id, '110') self.hass.pool.block_till_done() state = self.hass.states.get(entity_id) - self.assertEqual('70', state.state) + self.assertEqual(70, float(state.state)) diff --git a/tests/components/test_panel_iframe.py b/tests/components/test_panel_iframe.py new file mode 100644 index 00000000000..7b024c9ed56 --- /dev/null +++ b/tests/components/test_panel_iframe.py @@ -0,0 +1,76 @@ +"""The tests for the panel_iframe component.""" +import unittest +from unittest.mock import patch + +from homeassistant import bootstrap +from homeassistant.components import frontend + +from tests.common import get_test_home_assistant + + +class TestPanelIframe(unittest.TestCase): + """Test the panel_iframe component.""" + + def setup_method(self, method): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + frontend.PANELS = {} + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + frontend.PANELS = {} + + def test_wrong_config(self): + """Test setup with wrong configuration.""" + to_try = [ + {'invalid space': { + 'url': 'https://home-assistant.io'}}, + {'router': { + 'url': 'not-a-url'}}] + + for conf in to_try: + assert not bootstrap.setup_component( + self.hass, 'panel_iframe', { + 'panel_iframe': conf + }) + + @patch.dict('homeassistant.components.frontend.FINGERPRINTS', { + 'panels/ha-panel-iframe.html': 'md5md5'}) + def test_correct_config(self): + """Test correct config.""" + assert bootstrap.setup_component( + self.hass, 'panel_iframe', { + 'panel_iframe': { + 'router': { + 'icon': 'mdi:network-wireless', + 'title': 'Router', + 'url': 'http://192.168.1.1', + }, + 'weather': { + 'icon': 'mdi:weather', + 'title': 'Weather', + 'url': 'https://www.wunderground.com/us/ca/san-diego', + }, + }, + }) + + # 5 dev tools + map are automatically loaded + assert len(frontend.PANELS) == 8 + assert frontend.PANELS['router'] == { + 'component_name': 'iframe', + 'config': {'url': 'http://192.168.1.1'}, + 'icon': 'mdi:network-wireless', + 'title': 'Router', + 'url': '/frontend/panels/iframe-md5md5.html', + 'url_name': 'router' + } + + assert frontend.PANELS['weather'] == { + 'component_name': 'iframe', + 'config': {'url': 'https://www.wunderground.com/us/ca/san-diego'}, + 'icon': 'mdi:weather', + 'title': 'Weather', + 'url': '/frontend/panels/iframe-md5md5.html', + 'url_name': 'weather', + } diff --git a/tests/components/test_rfxtrx.py b/tests/components/test_rfxtrx.py index 3ad9522ec53..61a8f3bd82d 100644 --- a/tests/components/test_rfxtrx.py +++ b/tests/components/test_rfxtrx.py @@ -108,7 +108,42 @@ class TestRFXTRX(unittest.TestCase): self.assertEqual(event.values['Command'], "On") self.assertEqual('on', entity.state) + self.assertEqual(self.hass.states.get('switch.test').state, 'on') self.assertEqual(1, len(rfxtrx.RFX_DEVICES)) self.assertEqual(1, len(calls)) self.assertEqual(calls[0].data, {'entity_id': 'switch.test', 'state': 'on'}) + + def test_fire_event_sensor(self): + """Test fire event.""" + self.assertTrue(_setup_component(self.hass, 'rfxtrx', { + 'rfxtrx': { + 'device': '/dev/serial/by-id/usb' + + '-RFXCOM_RFXtrx433_A1Y0NJGR-if00-port0', + 'dummy': True} + })) + self.assertTrue(_setup_component(self.hass, 'sensor', { + 'sensor': {'platform': 'rfxtrx', + 'automatic_add': True, + 'devices': + {'0a520802060100ff0e0269': { + 'name': 'Test', + rfxtrx.ATTR_FIREEVENT: True} + }}})) + + calls = [] + + def record_event(event): + """Add recorded event to set.""" + calls.append(event) + + self.hass.bus.listen("signal_received", record_event) + event = rfxtrx.get_rfx_object('0a520802060101ff0f0269') + event.data = bytearray(b'\nR\x08\x01\x07\x01\x00\xb8\x1b\x02y') + rfxtrx.RECEIVED_EVT_SUBSCRIBERS[0](event) + + self.hass.pool.block_till_done() + self.assertEqual(1, len(rfxtrx.RFX_DEVICES)) + self.assertEqual(1, len(calls)) + self.assertEqual(calls[0].data, + {'entity_id': 'sensor.test'}) diff --git a/tests/components/test_sun.py b/tests/components/test_sun.py index 444a940357c..5b9df91e9a8 100644 --- a/tests/components/test_sun.py +++ b/tests/components/test_sun.py @@ -96,7 +96,7 @@ class TestSun(unittest.TestCase): june = datetime(2016, 6, 1, tzinfo=dt_util.UTC) - with patch('homeassistant.helpers.condition.dt_util.now', + with patch('homeassistant.helpers.condition.dt_util.utcnow', return_value=june): assert sun.setup(self.hass, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) diff --git a/tests/helpers/test_discovery.py b/tests/helpers/test_discovery.py index bdc6e2ed119..b6f9ed5dec8 100644 --- a/tests/helpers/test_discovery.py +++ b/tests/helpers/test_discovery.py @@ -1,10 +1,15 @@ """Test discovery helpers.""" +import os from unittest.mock import patch +from homeassistant import loader, bootstrap, config as config_util from homeassistant.helpers import discovery -from tests.common import get_test_home_assistant +from tests.common import (get_test_home_assistant, get_test_config_dir, + MockModule, MockPlatform) + +VERSION_PATH = os.path.join(get_test_config_dir(), config_util.VERSION_FILE) class TestHelpersDiscovery: @@ -18,6 +23,9 @@ class TestHelpersDiscovery: """Stop everything that was started.""" self.hass.stop() + if os.path.isfile(VERSION_PATH): + os.remove(VERSION_PATH) + @patch('homeassistant.bootstrap.setup_component') def test_listen(self, mock_setup_component): """Test discovery listen/discover combo.""" @@ -69,6 +77,7 @@ class TestHelpersDiscovery: discovery.load_platform(self.hass, 'test_component', 'test_platform', 'discovery info') + self.hass.pool.block_till_done() assert mock_setup_component.called assert mock_setup_component.call_args[0] == \ (self.hass, 'test_component', None) @@ -88,3 +97,42 @@ class TestHelpersDiscovery: self.hass.pool.block_till_done() assert len(calls) == 1 + + def test_circular_import(self): + """Test we don't break doing circular import.""" + component_calls = [] + platform_calls = [] + + def component_setup(hass, config): + """Setup mock component.""" + discovery.load_platform(hass, 'switch', 'test_circular') + component_calls.append(1) + return True + + def setup_platform(hass, config, add_devices_callback, + discovery_info=None): + """Setup mock platform.""" + platform_calls.append(1) + + loader.set_component( + 'test_component', + MockModule('test_component', setup=component_setup)) + + loader.set_component( + 'switch.test_circular', + MockPlatform(setup_platform, + dependencies=['test_component'])) + + bootstrap.from_config_dict({ + 'test_component': None, + 'switch': [{ + 'platform': 'test_circular', + }], + }, self.hass) + + self.hass.pool.block_till_done() + + assert 'test_component' in self.hass.config.components + assert 'switch' in self.hass.config.components + assert len(component_calls) == 1 + assert len(platform_calls) == 2 diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index a9a6310eb79..2fa65f6d4ec 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -226,7 +226,8 @@ class TestHelpersEntityComponent(unittest.TestCase): @patch('homeassistant.helpers.entity_component.EntityComponent' '._setup_platform') - def test_setup_does_discovery(self, mock_setup): + @patch('homeassistant.bootstrap.setup_component', return_value=True) + def test_setup_does_discovery(self, mock_setup_component, mock_setup): """Test setup for discovery.""" component = EntityComponent(_LOGGER, DOMAIN, self.hass) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 3fcb144ac1f..ba7255a8fe4 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -141,6 +141,38 @@ class TestScriptHelper(unittest.TestCase): assert not script_obj.is_running assert len(events) == 2 + def test_delay_template(self): + """Test the delay as a template.""" + event = 'test_evnt' + events = [] + + def record_event(event): + """Add recorded event to set.""" + events.append(event) + + self.hass.bus.listen(event, record_event) + + script_obj = script.Script(self.hass, [ + {'event': event}, + {'delay': '00:00:{{ 5 }}'}, + {'event': event}]) + + script_obj.run() + + self.hass.pool.block_till_done() + + assert script_obj.is_running + assert script_obj.can_cancel + assert script_obj.last_action == event + assert len(events) == 1 + + future = dt_util.utcnow() + timedelta(seconds=5) + fire_time_changed(self.hass, future) + self.hass.pool.block_till_done() + + assert not script_obj.is_running + assert len(events) == 2 + def test_cancel_while_delay(self): """Test the cancelling while the delay is present.""" event = 'test_event' diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 9aa91c3cd1a..1529c879aab 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -117,6 +117,34 @@ class TestUtilTemplate(unittest.TestCase): template.render(self.hass, '{{ %s | multiply(10) | round }}' % inp)) + def test_timestamp_local(self): + """Test the timestamps to local filter.""" + tests = { + None: 'None', + 1469119144: '2016-07-21 16:39:04', + } + + for inp, out in tests.items(): + self.assertEqual( + out, + template.render(self.hass, + '{{ %s | timestamp_local }}' % inp)) + + def test_timestamp_utc(self): + """Test the timestamps to local filter.""" + tests = { + None: 'None', + 1469119144: '2016-07-21 16:39:04', + dt_util.as_timestamp(dt_util.utcnow()): + dt_util.now().strftime('%Y-%m-%d %H:%M:%S') + } + + for inp, out in tests.items(): + self.assertEqual( + out, + template.render(self.hass, + '{{ %s | timestamp_utc }}' % inp)) + def test_passing_vars_as_keywords(self): """.""" self.assertEqual( diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 34aaa1b83ed..d41dc60ee15 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -161,7 +161,7 @@ class TestBootstrap: def test_component_not_double_initialized(self): """Test we do not setup a component twice.""" - mock_setup = mock.MagicMock() + mock_setup = mock.MagicMock(return_value=True) loader.set_component('comp', MockModule('comp', setup=mock_setup)) @@ -302,3 +302,29 @@ class TestBootstrap: 'valid': True } }) + + def test_disable_component_if_invalid_return(self): + """Test disabling component if invalid return.""" + loader.set_component( + 'disabled_component', + MockModule('disabled_component', setup=lambda hass, config: None)) + + assert not bootstrap.setup_component(self.hass, 'disabled_component') + assert loader.get_component('disabled_component') is None + assert 'disabled_component' not in self.hass.config.components + + loader.set_component( + 'disabled_component', + MockModule('disabled_component', setup=lambda hass, config: False)) + + assert not bootstrap.setup_component(self.hass, 'disabled_component') + assert loader.get_component('disabled_component') is not None + assert 'disabled_component' not in self.hass.config.components + + loader.set_component( + 'disabled_component', + MockModule('disabled_component', setup=lambda hass, config: True)) + + assert bootstrap.setup_component(self.hass, 'disabled_component') + assert loader.get_component('disabled_component') is not None + assert 'disabled_component' in self.hass.config.components diff --git a/tests/test_core.py b/tests/test_core.py index cb698cdc53c..e9513a2adb8 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -370,7 +370,13 @@ class TestServiceRegistry(unittest.TestCase): """Setup things to be run when tests are started.""" self.pool = ha.create_worker_pool(0) self.bus = ha.EventBus(self.pool) - self.services = ha.ServiceRegistry(self.bus, self.pool) + + def add_job(*args, **kwargs): + """Forward calls to add_job on Home Assistant.""" + # self works because we also have self.pool defined. + return ha.HomeAssistant.add_job(self, *args, **kwargs) + + self.services = ha.ServiceRegistry(self.bus, add_job) self.services.register("test_domain", "test_service", lambda x: None) def tearDown(self): # pylint: disable=invalid-name diff --git a/tests/test_remote.py b/tests/test_remote.py index f3ec35daee5..8820d01b9be 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -295,3 +295,7 @@ class TestRemoteClasses(unittest.TestCase): hass.pool.block_till_done() self.assertEqual(1, len(test_value)) + + def test_get_config(self): + """Test the return of the configuration.""" + self.assertEqual(hass.config.as_dict(), remote.get_config(master_api)) diff --git a/tests/config/custom_components/device_tracker/test.py b/tests/testing_config/custom_components/device_tracker/test.py similarity index 100% rename from tests/config/custom_components/device_tracker/test.py rename to tests/testing_config/custom_components/device_tracker/test.py diff --git a/tests/config/custom_components/light/test.py b/tests/testing_config/custom_components/light/test.py similarity index 100% rename from tests/config/custom_components/light/test.py rename to tests/testing_config/custom_components/light/test.py diff --git a/tests/config/custom_components/switch/test.py b/tests/testing_config/custom_components/switch/test.py similarity index 100% rename from tests/config/custom_components/switch/test.py rename to tests/testing_config/custom_components/switch/test.py diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index bf5284a0b04..e8114e93e24 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -137,7 +137,10 @@ class TestDateUtil(unittest.TestCase): def test_get_age(self): """Test get_age.""" diff = dt_util.now() - timedelta(seconds=0) - self.assertEqual(dt_util.get_age(diff), "0 second") + self.assertEqual(dt_util.get_age(diff), "0 seconds") + + diff = dt_util.now() - timedelta(seconds=1) + self.assertEqual(dt_util.get_age(diff), "1 second") diff = dt_util.now() - timedelta(seconds=30) self.assertEqual(dt_util.get_age(diff), "30 seconds") diff --git a/tox.ini b/tox.ini index bc5acbd71cc..8371d9dcbdf 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py34, py35, lint, requirements +envlist = py34, py35, lint, requirements, typing skip_missing_interpreters = True [testenv] @@ -29,3 +29,10 @@ basepython = python3 deps = commands = python script/gen_requirements_all.py validate + +[testenv:typing] +basepython = python3 +deps = + -r{toxinidir}/requirements_test.txt +commands = + mypy --silent-imports homeassistant