diff --git a/.travis.yml b/.travis.yml index 339ed48d424..4a4dfbc2354 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,11 +3,6 @@ language: python python: - "3.4" install: - - pip install -r requirements_all.txt - - pip install flake8 pylint coveralls + - script/bootstrap_server script: - - flake8 homeassistant - - pylint homeassistant - - coverage run -m unittest discover tests -after_success: - - coveralls + - script/cibuild diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 428845031a5..e97ed0c6386 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -103,6 +103,10 @@ def get_arguments(): '--uninstall-osx', action='store_true', help='Uninstalls from OS X.') + parser.add_argument( + '--restart-osx', + action='store_true', + help='Restarts on OS X.') if os.name != "nt": parser.add_argument( '--daemon', @@ -216,6 +220,10 @@ def main(): if args.uninstall_osx: uninstall_osx() return + if args.restart_osx: + uninstall_osx() + install_osx() + return # daemon functions if args.pid_file: diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 17b0c989f16..559832eee80 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -27,8 +27,7 @@ def trigger(hass, config, action): if CONF_AFTER in config: after = dt_util.parse_time_str(config[CONF_AFTER]) if after is None: - _LOGGER.error( - 'Received invalid after value: %s', config[CONF_AFTER]) + _error_time(config[CONF_AFTER], CONF_AFTER) return False hours, minutes, seconds = after.hour, after.minute, after.second elif (CONF_HOURS in config or CONF_MINUTES in config @@ -63,27 +62,27 @@ def if_action(hass, config): CONF_BEFORE, CONF_AFTER, CONF_WEEKDAY) return None + if before is not None: + before = dt_util.parse_time_str(before) + if before is None: + _error_time(before, CONF_BEFORE) + return None + + if after is not None: + after = dt_util.parse_time_str(after) + if after is None: + _error_time(after, CONF_AFTER) + return None + def time_if(): """ Validate time based if-condition """ now = dt_util.now() - if before is not None: - time = dt_util.parse_time_str(before) - if time is None: + if before is not None and now > now.replace(hour=before.hour, + minute=before.minute): return False - before_point = now.replace(hour=time.hour, minute=time.minute) - - if now > before_point: - return False - - if after is not None: - time = dt_util.parse_time_str(after) - if time is None: - return False - - after_point = now.replace(hour=time.hour, minute=time.minute) - - if now < after_point: + if after is not None and now < now.replace(hour=after.hour, + minute=after.minute): return False if weekday is not None: @@ -96,3 +95,11 @@ def if_action(hass, config): return True return time_if + + +def _error_time(value, key): + """ Helper method to print error. """ + _LOGGER.error( + "Received invalid value for '%s': %s", key, value) + if isinstance(value, int): + _LOGGER.error('Make sure you wrap time values in quotes') diff --git a/homeassistant/components/camera/foscam.py b/homeassistant/components/camera/foscam.py index 21f4589ca6c..78fd0f4d2e1 100644 --- a/homeassistant/components/camera/foscam.py +++ b/homeassistant/components/camera/foscam.py @@ -1,41 +1,31 @@ """ -Support for Foscam IP Cameras. - +homeassistant.components.camera.foscam +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This component provides basic support for Foscam IP cameras. As part of the basic support the following features will be provided: -MJPEG video streaming -To use this component, add the following to your config/configuration.yaml: +To use this component, add the following to your configuration.yaml file. camera: - platform: foscam - name: Door Camera - ip: 192.168.0.123 - port: 88 - username: visitor - password: password + platform: foscam + name: Door Camera + ip: 192.168.0.123 + port: 88 + username: YOUR_USERNAME + password: YOUR_PASSWORD -camera 2: - name: 'Second Camera' - ... -camera 3: - name: 'Camera Three' - ... - - -VARIABLES: - -These are the variables for the device_data array: +Variables: ip *Required -The IP address of your foscam device +The IP address of your Foscam device. username *Required -The username of a visitor or operator of your camera. -Oddly admin accounts don't seem to have access to take snapshots. +The username of a visitor or operator of your camera. Oddly admin accounts +don't seem to have access to take snapshots. password *Required @@ -49,6 +39,8 @@ port *Optional The port that the camera is running on. The default is 88. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/camera.foscam.html """ import logging from homeassistant.helpers import validate_config @@ -72,9 +64,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): # pylint: disable=too-many-instance-attributes class FoscamCamera(Camera): - """ - An implementation of a Foscam IP camera. - """ + """ An implementation of a Foscam IP camera. """ def __init__(self, device_info): super(FoscamCamera, self).__init__() @@ -94,7 +84,7 @@ class FoscamCamera(Camera): self._name, self._snap_picture_url) def camera_image(self): - """ Return a still image reponse from the camera """ + """ Return a still image reponse from the camera. """ # send the request to snap a picture response = requests.get(self._snap_picture_url) @@ -111,5 +101,5 @@ class FoscamCamera(Camera): @property def name(self): - """ Return the name of this device """ + """ Return the name of this device. """ return self._name diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index fd9e31e1810..faf4f6aa983 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -28,6 +28,14 @@ REQUIREMENTS = ['SoCo==0.11.1'] _LOGGER = logging.getLogger(__name__) +# The soco library is excessively chatty when it comes to logging and +# causes a LOT of spam in the logs due to making a http connection to each +# speaker every 10 seconds. Quiet it down a bit to just actual problems. +_SOCO_LOGGER = logging.getLogger('soco') +_SOCO_LOGGER.setLevel(logging.ERROR) +_REQUESTS_LOGGER = logging.getLogger('requests') +_REQUESTS_LOGGER.setLevel(logging.ERROR) + SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK diff --git a/homeassistant/components/recorder.py b/homeassistant/components/recorder.py index 73487163425..10f6576d23f 100644 --- a/homeassistant/components/recorder.py +++ b/homeassistant/components/recorder.py @@ -256,7 +256,7 @@ class Recorder(threading.Thread): """ Query the database. """ try: with self.conn, self.lock: - _LOGGER.info("Running query %s", sql_query) + _LOGGER.debug("Running query %s", sql_query) cur = self.conn.cursor() diff --git a/homeassistant/components/sensor/command_sensor.py b/homeassistant/components/sensor/command_sensor.py index 3a3199be627..a6e6c19fdb8 100644 --- a/homeassistant/components/sensor/command_sensor.py +++ b/homeassistant/components/sensor/command_sensor.py @@ -70,7 +70,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): data, config.get('name', DEFAULT_NAME), config.get('unit_of_measurement'), - config.get('correction_factor', None), + config.get('correction_factor', 1.0), config.get('decimal_places', 0) )]) @@ -108,12 +108,15 @@ class CommandSensor(Entity): self.data.update() value = self.data.value - if value is not None: - if self._corr_factor is not None: - self._state = round((int(value) * self._corr_factor), - self._decimal_places) - else: - self._state = value + try: + if value is not None: + if self._corr_factor is not None: + self._state = round((float(value) * self._corr_factor), + self._decimal_places) + else: + self._state = value + except ValueError: + self._state = value # pylint: disable=too-few-public-methods diff --git a/homeassistant/components/sensor/glances.py b/homeassistant/components/sensor/glances.py index 4a4d47c32f8..f6031b5a131 100644 --- a/homeassistant/components/sensor/glances.py +++ b/homeassistant/components/sensor/glances.py @@ -3,7 +3,6 @@ homeassistant.components.sensor.glances ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Gathers system information of hosts which running glances. - Configuration: To use the glances sensor you will need to add something like the following diff --git a/homeassistant/components/sensor/openweathermap.py b/homeassistant/components/sensor/openweathermap.py index 5ca292a599f..9d33264ea70 100644 --- a/homeassistant/components/sensor/openweathermap.py +++ b/homeassistant/components/sensor/openweathermap.py @@ -91,7 +91,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error( "Connection error " "Please check your settings for OpenWeatherMap.") - return None + return False data = WeatherData(owm, forecast, hass.config.latitude, hass.config.longitude) diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index 9f0b90d729f..5a07b585430 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -31,7 +31,7 @@ Details for the API : http://transport.opendata.ch """ import logging from datetime import timedelta -from requests import get +import requests from homeassistant.util import Throttle import homeassistant.util.dt as dt_util @@ -53,8 +53,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: for location in [config.get('from', None), config.get('to', None)]: # transport.opendata.ch doesn't play nice with requests.Session - result = get(_RESOURCE + 'locations?query=%s' % location, - timeout=10) + result = requests.get(_RESOURCE + 'locations?query=%s' % location, + timeout=10) journey.append(result.json()['stations'][0]['name']) except KeyError: _LOGGER.exception( @@ -110,7 +110,7 @@ class PublicTransportData(object): def update(self): """ Gets the latest data from opendata.ch. """ - response = get( + response = requests.get( _RESOURCE + 'connections?' + 'from=' + self.start + '&' + diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 5d32c087efe..966ecc1dcc2 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -1,6 +1,6 @@ """Helpers to install PyPi packages.""" -import os import logging +import os import pkg_resources import subprocess import sys @@ -15,25 +15,24 @@ def install_package(package, upgrade=True, target=None): """Install a package on PyPi. Accepts pip compatible package strings. Return boolean if install successfull.""" # Not using 'import pip; pip.main([])' because it breaks the logger - args = [sys.executable, '-m', 'pip', 'install', '--quiet', package] - - if upgrade: - args.append('--upgrade') - if target: - args += ['--target', os.path.abspath(target)] - with INSTALL_LOCK: if check_package_exists(package, target): return True _LOGGER.info('Attempting install of %s', package) + args = [sys.executable, '-m', 'pip', 'install', '--quiet', package] + if upgrade: + args.append('--upgrade') + if target: + args += ['--target', os.path.abspath(target)] + try: return 0 == subprocess.call(args) except subprocess.SubprocessError: return False -def check_package_exists(package, target=None): +def check_package_exists(package, target): """Check if a package exists. Returns True when the requirement is met. Returns False when the package is not installed or doesn't meet req.""" @@ -43,16 +42,5 @@ def check_package_exists(package, target=None): # This is a zip file req = pkg_resources.Requirement.parse(urlparse(package).fragment) - if target: - work_set = pkg_resources.WorkingSet([target]) - search_fun = work_set.find - - else: - search_fun = pkg_resources.get_distribution - - try: - result = search_fun(req) - except (pkg_resources.DistributionNotFound, pkg_resources.VersionConflict): - return False - - return bool(result) + return any(dist in req for dist in + pkg_resources.find_distributions(target)) diff --git a/script/bootstrap b/script/bootstrap new file mode 100755 index 00000000000..f4cb6753fe8 --- /dev/null +++ b/script/bootstrap @@ -0,0 +1,9 @@ +#!/bin/sh + +# script/bootstrap: Resolve all dependencies that the application requires to +# run. + +cd "$(dirname "$0")/.." + +script/bootstrap_server +script/bootstrap_frontend diff --git a/script/bootstrap_frontend b/script/bootstrap_frontend new file mode 100755 index 00000000000..6fc94f95725 --- /dev/null +++ b/script/bootstrap_frontend @@ -0,0 +1,5 @@ +echo "Bootstrapping frontend..." +cd homeassistant/components/frontend/www_static/home-assistant-polymer +npm install +npm run setup_js_dev +cd ../../../../.. diff --git a/script/bootstrap_server b/script/bootstrap_server new file mode 100755 index 00000000000..8d71e01fa78 --- /dev/null +++ b/script/bootstrap_server @@ -0,0 +1,10 @@ +cd "$(dirname "$0")/.." + +echo "Update the submodule to latest version..." +git submodule update + +echo "Installing dependencies..." +python3 -m pip install --upgrade -r requirements_all.txt + +echo "Installing development dependencies.." +python3 -m pip install --upgrade flake8 pylint coveralls pytest pytest-cov diff --git a/scripts/build_frontend b/script/build_frontend similarity index 86% rename from scripts/build_frontend rename to script/build_frontend index 9554e82256d..70eacdb6baf 100755 --- a/scripts/build_frontend +++ b/script/build_frontend @@ -1,12 +1,8 @@ # Builds the frontend for production -# If current pwd is scripts, go 1 up. -if [ ${PWD##*/} == "scripts" ]; then - cd .. -fi +cd "$(dirname "$0")/.." cd homeassistant/components/frontend/www_static/home-assistant-polymer -npm install npm run frontend_prod cp bower_components/webcomponentsjs/webcomponents-lite.min.js .. diff --git a/scripts/build_python_openzwave b/script/build_python_openzwave similarity index 87% rename from scripts/build_python_openzwave rename to script/build_python_openzwave index 24bd8e2b64f..02c088fca44 100755 --- a/scripts/build_python_openzwave +++ b/script/build_python_openzwave @@ -3,10 +3,7 @@ # apt-get install cython3 libudev-dev python-sphinx python3-setuptools # pip3 install cython -# If current pwd is scripts, go 1 up. -if [ ${PWD##*/} == "scripts" ]; then - cd .. -fi +cd "$(dirname "$0")/.." if [ ! -d build ]; then mkdir build diff --git a/script/cibuild b/script/cibuild new file mode 100755 index 00000000000..ade1b1d91c5 --- /dev/null +++ b/script/cibuild @@ -0,0 +1,7 @@ +#!/bin/sh + +# script/cibuild: Setup environment for CI to run tests. This is primarily +# designed to run on the continuous integration server. + +script/test coverage +coveralls diff --git a/scripts/dev_docker b/script/dev_docker similarity index 86% rename from scripts/dev_docker rename to script/dev_docker index b3672e56095..b63afaa36da 100755 --- a/scripts/dev_docker +++ b/script/dev_docker @@ -3,10 +3,7 @@ # Optional: pass in a timezone as first argument # If not given will attempt to mount /etc/localtime -# If current pwd is scripts, go 1 up. -if [ ${PWD##*/} == "scripts" ]; then - cd .. -fi +cd "$(dirname "$0")/.." docker build -t home-assistant-dev . diff --git a/scripts/dev_openzwave_docker b/script/dev_openzwave_docker similarity index 78% rename from scripts/dev_openzwave_docker rename to script/dev_openzwave_docker index f27816a8e39..387c38ef6da 100755 --- a/scripts/dev_openzwave_docker +++ b/script/dev_openzwave_docker @@ -1,10 +1,7 @@ # Open a docker that can be used to debug/dev python-openzwave # Pass in a command line argument to build -# If current pwd is scripts, go 1 up. -if [ ${PWD##*/} == "scripts" ]; then - cd .. -fi +cd "$(dirname "$0")/.." if [ $# -gt 0 ] then diff --git a/scripts/get_entities.py b/script/get_entities.py similarity index 100% rename from scripts/get_entities.py rename to script/get_entities.py diff --git a/scripts/hass-daemon b/script/hass-daemon old mode 100644 new mode 100755 similarity index 96% rename from scripts/hass-daemon rename to script/hass-daemon index d11c2669e87..2b396699543 --- a/scripts/hass-daemon +++ b/script/hass-daemon @@ -34,6 +34,7 @@ RUN_AS="USER" PID_FILE="/var/run/hass.pid" CONFIG_DIR="/var/opt/homeassistant" FLAGS="-v --config $CONFIG_DIR --pid-file $PID_FILE --daemon" +REDIRECT="> $CONFIG_DIR/home-assistant.log 2>&1" start() { if [ -f $PID_FILE ] && kill -0 $(cat $PID_FILE); then @@ -41,7 +42,7 @@ start() { return 1 fi echo 'Starting service…' >&2 - local CMD="$PRE_EXEC hass $FLAGS;" + local CMD="$PRE_EXEC hass $FLAGS $REDIRECT;" su -c "$CMD" $RUN_AS echo 'Service started' >&2 } diff --git a/script/lint b/script/lint new file mode 100755 index 00000000000..120f364120f --- /dev/null +++ b/script/lint @@ -0,0 +1,9 @@ +# Run style checks + +cd "$(dirname "$0")/.." + +echo "Checking style with flake8..." +flake8 homeassistant + +echo "Checking style with pylint..." +pylint homeassistant diff --git a/script/server b/script/server new file mode 100755 index 00000000000..0904bfd728e --- /dev/null +++ b/script/server @@ -0,0 +1,8 @@ +#!/bin/sh + +# script/server: Launch the application and any extra required processes +# locally. + +cd "$(dirname "$0")/.." + +python3 -m homeassistant -c config diff --git a/script/setup b/script/setup new file mode 100755 index 00000000000..6d3a774dd54 --- /dev/null +++ b/script/setup @@ -0,0 +1,5 @@ +cd "$(dirname "$0")/.." + +git submodule init +script/bootstrap +python3 setup.py develop diff --git a/script/test b/script/test new file mode 100755 index 00000000000..56fe4dcec89 --- /dev/null +++ b/script/test @@ -0,0 +1,16 @@ +#!/bin/sh + +# script/test: Run test suite for application. Optionallly pass in a path to an +# individual test file to run a single test. + +cd "$(dirname "$0")/.." + +script/lint + +echo "Running tests..." + +if [ "$1" = "coverage" ]; then + py.test --cov homeassistant tests +else + py.test tests +fi diff --git a/script/update b/script/update new file mode 100755 index 00000000000..9f8b2530a7e --- /dev/null +++ b/script/update @@ -0,0 +1,8 @@ +#!/bin/sh + +# script/update: Update application to run for its current checkout. + +cd "$(dirname "$0")/.." + +git pull +git submodule update diff --git a/scripts/check_style b/scripts/check_style deleted file mode 100755 index 5fc8861b91a..00000000000 --- a/scripts/check_style +++ /dev/null @@ -1,9 +0,0 @@ -# Run style checks - -# If current pwd is scripts, go 1 up. -if [ ${PWD##*/} == "scripts" ]; then - cd .. -fi - -flake8 homeassistant -pylint homeassistant diff --git a/scripts/run_tests b/scripts/run_tests deleted file mode 100755 index 75b25ca805a..00000000000 --- a/scripts/run_tests +++ /dev/null @@ -1,10 +0,0 @@ -# If current pwd is scripts, go 1 up. -if [ ${PWD##*/} == "scripts" ]; then - cd .. -fi - -if [ "$1" = "coverage" ]; then - coverage run -m unittest discover tests -else - python3 -m unittest discover tests -fi diff --git a/scripts/update b/scripts/update deleted file mode 100755 index be5e8fc01bf..00000000000 --- a/scripts/update +++ /dev/null @@ -1,6 +0,0 @@ -echo "The update script has been deprecated since Home Assistant v0.7" -echo -echo "Home Assistant is now distributed via PyPi and can be installed and" -echo "upgraded by running: pip3 install --upgrade homeassistant" -echo -echo "If you are developing a new feature for Home Assistant, run: git pull" diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index f7187592c66..95997bfec42 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -285,9 +285,9 @@ class TestAutomationTime(unittest.TestCase): automation.DOMAIN: { 'trigger': { 'platform': 'time', - 'hours': 0, - 'minutes': 0, - 'seconds': 0, + 'hours': 1, + 'minutes': 2, + 'seconds': 3, }, 'action': { 'execute_service': 'test.automation' @@ -296,7 +296,7 @@ class TestAutomationTime(unittest.TestCase): })) fire_time_changed(self.hass, dt_util.utcnow().replace( - hour=0, minute=0, second=0)) + hour=1, minute=2, second=3)) self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) @@ -320,6 +320,30 @@ class TestAutomationTime(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) + @patch('homeassistant.components.automation.time._LOGGER.error') + def test_if_not_fires_using_wrong_after(self, mock_error): + """ YAML translates time values to total seconds. This should break the + before rule. """ + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'time', + 'after': 3605, + # Total seconds. Hour = 3600 second + }, + 'action': { + 'execute_service': 'test.automation' + } + } + })) + + fire_time_changed(self.hass, dt_util.utcnow().replace( + hour=1, minute=0, second=5)) + + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + self.assertEqual(2, mock_error.call_count) + def test_if_action_before(self): automation.setup(self.hass, { automation.DOMAIN: {