diff --git a/.gitignore b/.gitignore index 8c4eec4d180..8dab1d873da 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,7 @@ nosetests.xml .pydevproject .python-version + +# venv stuff +pyvenv.cfg +pip-selfcheck.json diff --git a/README.md b/README.md index 7c6997e4750..4a8d82ba0ba 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,8 @@ Running Home Assistant requires that [Python](https://www.python.org/) 3.4 and t ```python git clone --recursive https://github.com/balloob/home-assistant.git +python3 -m venv home-assistant cd home-assistant -python3 -m pip install --user -r requirements.txt python3 -m homeassistant --open-ui ``` diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 316529ce74e..0a575afede0 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -4,15 +4,9 @@ from __future__ import print_function import sys import os import argparse -import importlib +import subprocess - -# Home Assistant dependencies, mapped module -> package name -DEPENDENCIES = { - 'requests': 'requests', - 'yaml': 'pyyaml', - 'pytz': 'pytz', -} +DEPENDENCIES = ['requests>=2.0', 'pyyaml>=3.11', 'pytz>=2015.2'] def validate_python(): @@ -24,21 +18,29 @@ def validate_python(): sys.exit() +# Copy of homeassistant.util.package because we can't import yet +def install_package(package): + """Install a package on PyPi. Accepts pip compatible package strings. + Return boolean if install successfull.""" + args = ['python3', '-m', 'pip', 'install', '--quiet', package] + if sys.base_prefix == sys.prefix: + args.append('--user') + return not subprocess.call(args) + + def validate_dependencies(): """ Validate all dependencies that HA uses. """ + print("Validating dependencies...") import_fail = False - for module, name in DEPENDENCIES.items(): - try: - importlib.import_module(module) - except ImportError: + for requirement in DEPENDENCIES: + if not install_package(requirement): import_fail = True - print( - 'Fatal Error: Unable to find dependency {}'.format(name)) + print('Fatal Error: Unable to install dependency', requirement) if import_fail: print(("Install dependencies by running: " - "pip3 install -r requirements.txt")) + "python3 -m pip install -r requirements.txt")) sys.exit() diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index f8c595255d9..2da2f4fb7b5 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -14,8 +14,9 @@ import logging from collections import defaultdict import homeassistant -import homeassistant.util as util import homeassistant.util.dt as date_util +import homeassistant.util.package as pkg_util +import homeassistant.util.location as loc_util import homeassistant.config as config_util import homeassistant.loader as loader import homeassistant.components as core_components @@ -60,6 +61,17 @@ def setup_component(hass, domain, config=None): return True +def _handle_requirements(component, name): + """ Installs requirements for component. """ + if hasattr(component, 'REQUIREMENTS'): + for req in component.REQUIREMENTS: + if not pkg_util.install_package(req): + _LOGGER.error('Not initializing %s because could not install ' + 'dependency %s', name, req) + return False + return True + + def _setup_component(hass, domain, config): """ Setup a component for Home Assistant. """ component = loader.get_component(domain) @@ -74,6 +86,9 @@ def _setup_component(hass, domain, config): return False + if not _handle_requirements(component, domain): + return False + try: if component.setup(hass, config): hass.config.components.append(component.DOMAIN) @@ -109,18 +124,22 @@ def prepare_setup_platform(hass, config, domain, platform_name): if platform is None: return None - # Already loaded or no dependencies - elif (platform_path in hass.config.components or - not hasattr(platform, 'DEPENDENCIES')): + # Already loaded + elif platform_path in hass.config.components: return platform # Load dependencies - for component in platform.DEPENDENCIES: - if not setup_component(hass, component, config): - _LOGGER.error( - 'Unable to prepare setup for platform %s because dependency ' - '%s could not be initialized', platform_path, component) - return None + if hasattr(platform, 'DEPENDENCIES'): + for component in platform.DEPENDENCIES: + if not setup_component(hass, component, config): + _LOGGER.error( + 'Unable to prepare setup for platform %s because ' + 'dependency %s could not be initialized', platform_path, + component) + return None + + if not _handle_requirements(platform, platform_path): + return None return platform @@ -276,7 +295,7 @@ def process_ha_core_config(hass, config): _LOGGER.info('Auto detecting location and temperature unit') - info = util.detect_location_info() + info = loc_util.detect_location_info() if info is None: _LOGGER.error('Could not detect location information') diff --git a/homeassistant/components/arduino.py b/homeassistant/components/arduino.py index e7131f9c9e0..db91c5e0d9c 100644 --- a/homeassistant/components/arduino.py +++ b/homeassistant/components/arduino.py @@ -27,8 +27,10 @@ every initialization the pins are set to off/low. """ import logging -from PyMata.pymata import PyMata -import serial +try: + from PyMata.pymata import PyMata +except ImportError: + PyMata = None from homeassistant.helpers import validate_config from homeassistant.const import (EVENT_HOMEASSISTANT_START, @@ -36,6 +38,7 @@ from homeassistant.const import (EVENT_HOMEASSISTANT_START, DOMAIN = "arduino" DEPENDENCIES = [] +REQUIREMENTS = ['PyMata==2.07a'] BOARD = None _LOGGER = logging.getLogger(__name__) @@ -43,12 +46,18 @@ _LOGGER = logging.getLogger(__name__) def setup(hass, config): """ Setup the Arduino component. """ + global PyMata # pylint: disable=invalid-name + if PyMata is None: + from PyMata.pymata import PyMata as PyMata_ + PyMata = PyMata_ + + import serial + if not validate_config(config, {DOMAIN: ['port']}, _LOGGER): return False - # pylint: disable=global-statement global BOARD try: BOARD = ArduinoBoard(config[DOMAIN]['port']) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 819d45b5b68..63c9a0af74f 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -22,6 +22,7 @@ from homeassistant.const import ( DOMAIN = "discovery" DEPENDENCIES = [] +REQUIREMENTS = ['zeroconf>=0.16.0'] SCAN_INTERVAL = 300 # seconds diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index d750c3b09be..dad3cd70534 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -22,6 +22,7 @@ from homeassistant.const import ( # homeassistant constants DOMAIN = "isy994" DEPENDENCIES = [] +REQUIREMENTS = ['PyISY>=1.0.5'] DISCOVER_LIGHTS = "isy994.lights" DISCOVER_SWITCHES = "isy994.switches" DISCOVER_SENSORS = "isy994.sensors" diff --git a/homeassistant/components/keyboard.py b/homeassistant/components/keyboard.py index b59fe8d39dc..5359791087e 100644 --- a/homeassistant/components/keyboard.py +++ b/homeassistant/components/keyboard.py @@ -14,6 +14,7 @@ from homeassistant.const import ( DOMAIN = "keyboard" DEPENDENCIES = [] +REQUIREMENTS = ['pyuserinput>=0.1.9'] def volume_up(hass): diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 54919a73331..58c3c3cd8f2 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -56,6 +56,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import ToggleEntity import homeassistant.util as util +import homeassistant.util.color as color_util from homeassistant.const import ( STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) from homeassistant.components import group, discovery, wink, isy994 @@ -243,9 +244,9 @@ def setup(hass, config): if len(rgb_color) == 3: params[ATTR_XY_COLOR] = \ - util.color_RGB_to_xy(int(rgb_color[0]), - int(rgb_color[1]), - int(rgb_color[2])) + color_util.color_RGB_to_xy(int(rgb_color[0]), + int(rgb_color[1]), + int(rgb_color[2])) except (TypeError, ValueError): # TypeError if rgb_color is not iterable diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 0b2cf1e2dd7..c908992eb82 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -12,6 +12,7 @@ from homeassistant.components.light import ( ATTR_FLASH, FLASH_LONG, FLASH_SHORT, ATTR_EFFECT, EFFECT_COLORLOOP) +REQUIREMENTS = ['phue>=0.8'] MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index 649f642e077..b515bb1cbac 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -27,15 +27,12 @@ from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.components.light import Light, ATTR_BRIGHTNESS _LOGGER = logging.getLogger(__name__) +REQUIREMENTS = ['ledcontroller>=1.0.7'] def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Gets the LimitlessLED lights. """ - try: - import ledcontroller - except ImportError: - _LOGGER.exception("Error while importing dependency ledcontroller.") - return + import ledcontroller led = ledcontroller.LedController(config['host']) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 4e33034d821..17ae9e6cd33 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -10,7 +10,6 @@ import logging try: import pychromecast - import pychromecast.controllers.youtube as youtube except ImportError: pychromecast = None @@ -25,6 +24,7 @@ from homeassistant.components.media_player import ( SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO) +REQUIREMENTS = ['pychromecast>=0.6.9'] CONF_IGNORE_CEC = 'ignore_cec' CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png' SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ @@ -36,14 +36,12 @@ KNOWN_HOSTS = [] # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the cast platform. """ - logger = logging.getLogger(__name__) - + global pychromecast # pylint: disable=invalid-name if pychromecast is None: - logger.error(( - "Failed to import pychromecast. Did you maybe not install the " - "'pychromecast' dependency?")) + import pychromecast as pychromecast_ + pychromecast = pychromecast_ - return False + logger = logging.getLogger(__name__) # import CEC IGNORE attributes ignore_cec = config.get(CONF_IGNORE_CEC, []) @@ -81,6 +79,7 @@ class CastDevice(MediaPlayerDevice): # pylint: disable=too-many-public-methods def __init__(self, host): + import pychromecast.controllers.youtube as youtube self.cast = pychromecast.Chromecast(host) self.youtube = youtube.YouTubeController() self.cast.register_handler(self.youtube) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index d31e71251dc..e134c1c2f7e 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -49,6 +49,7 @@ except ImportError: jsonrpc_requests = None _LOGGER = logging.getLogger(__name__) +REQUIREMENTS = ['jsonrpc-requests>=0.1'] SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK @@ -58,12 +59,10 @@ SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the kodi platform. """ + global jsonrpc_requests # pylint: disable=invalid-name if jsonrpc_requests is None: - _LOGGER.exception( - "Unable to import jsonrpc_requests. " - "Did you maybe not install the 'jsonrpc-requests' pip module?") - - return False + import jsonrpc_requests as jsonrpc_requests_ + jsonrpc_requests = jsonrpc_requests_ add_devices([ KodiDevice( diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index f8b455ae6fe..0239173f7cc 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -48,7 +48,7 @@ from homeassistant.components.media_player import ( MEDIA_TYPE_MUSIC) _LOGGER = logging.getLogger(__name__) - +REQUIREMENTS = ['python-mpd2>=0.5.4'] SUPPORT_MPD = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK @@ -62,12 +62,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): port = config.get('port', 6600) location = config.get('location', 'MPD') + global mpd # pylint: disable=invalid-name if mpd is None: - _LOGGER.exception( - "Unable to import mpd2. " - "Did you maybe not install the 'python-mpd2' package?") - - return False + import mpd as mpd_ + mpd = mpd_ # pylint: disable=no-member try: diff --git a/homeassistant/components/notify/pushbullet.py b/homeassistant/components/notify/pushbullet.py index 09bfd3244c8..5e322cfc3b5 100644 --- a/homeassistant/components/notify/pushbullet.py +++ b/homeassistant/components/notify/pushbullet.py @@ -28,6 +28,7 @@ from homeassistant.components.notify import ( from homeassistant.const import CONF_API_KEY _LOGGER = logging.getLogger(__name__) +REQUIREMENTS = ['pushbullet.py>=0.7.1'] def get_service(hass, config): diff --git a/homeassistant/components/notify/pushover.py b/homeassistant/components/notify/pushover.py index f8ec652a39c..1bc5e9ac9a3 100644 --- a/homeassistant/components/notify/pushover.py +++ b/homeassistant/components/notify/pushover.py @@ -42,6 +42,7 @@ from homeassistant.components.notify import ( DOMAIN, ATTR_TITLE, BaseNotificationService) from homeassistant.const import CONF_API_KEY +REQUIREMENTS = ['python-pushover>=0.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/notify/xmpp.py b/homeassistant/components/notify/xmpp.py index 43bb5799458..25099921f45 100644 --- a/homeassistant/components/notify/xmpp.py +++ b/homeassistant/components/notify/xmpp.py @@ -47,6 +47,8 @@ from homeassistant.helpers import validate_config from homeassistant.components.notify import ( DOMAIN, ATTR_TITLE, BaseNotificationService) +REQUIREMENTS = ['sleekxmpp>=1.3.1'] + def get_service(hass, config): """ Get the Jabber (XMPP) notification service. """ diff --git a/homeassistant/components/sensor/bitcoin.py b/homeassistant/components/sensor/bitcoin.py index bc29198e6a0..e0ecbab6db5 100644 --- a/homeassistant/components/sensor/bitcoin.py +++ b/homeassistant/components/sensor/bitcoin.py @@ -71,6 +71,7 @@ from homeassistant.util import Throttle from homeassistant.helpers.entity import Entity +REQUIREMENTS = ['blockchain>=1.1.2'] _LOGGER = logging.getLogger(__name__) OPTION_TYPES = { 'wallet': ['Wallet balance', 'BTC'], diff --git a/homeassistant/components/sensor/forecast.py b/homeassistant/components/sensor/forecast.py index 98e088d5139..abd3cdadb73 100644 --- a/homeassistant/components/sensor/forecast.py +++ b/homeassistant/components/sensor/forecast.py @@ -50,7 +50,10 @@ Details for the API : https://developer.forecast.io/docs/v2 import logging from datetime import timedelta -import forecastio +try: + import forecastio +except ImportError: + forecastio = None from homeassistant.util import Throttle from homeassistant.const import (CONF_API_KEY, TEMP_CELCIUS, TEMP_FAHRENHEIT) @@ -79,6 +82,11 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120) def setup_platform(hass, config, add_devices, discovery_info=None): """ Get the Forecast.io sensor. """ + global forecastio # pylint: disable=invalid-name + if forecastio is None: + import forecastio as forecastio_ + forecastio = forecastio_ + if None in (hass.config.latitude, hass.config.longitude): _LOGGER.error("Latitude or longitude not set in Home Assistant config") return False diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index 7fbcf65328d..4f3c2610c5a 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -39,6 +39,7 @@ ATTR_NODE_ID = "node_id" ATTR_CHILD_ID = "child_id" _LOGGER = logging.getLogger(__name__) +REQUIREMENTS = ['pyserial>=2.7'] def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/sensor/openweathermap.py b/homeassistant/components/sensor/openweathermap.py index 07413e7b1ea..22720748034 100644 --- a/homeassistant/components/sensor/openweathermap.py +++ b/homeassistant/components/sensor/openweathermap.py @@ -48,6 +48,7 @@ from homeassistant.util import Throttle from homeassistant.const import (CONF_API_KEY, TEMP_CELCIUS, TEMP_FAHRENHEIT) from homeassistant.helpers.entity import Entity +REQUIREMENTS = ['pywm>=2.2.1'] _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { 'weather': ['Condition', ''], diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 2615ab1a77b..1d1bdb1f3b5 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -66,6 +66,7 @@ import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import Entity from homeassistant.const import STATE_ON, STATE_OFF +REQUIREMENTS = ['psutil>=3.0.0'] SENSOR_TYPES = { 'disk_use_percent': ['Disk Use', '%'], 'disk_use': ['Disk Use', 'GiB'], diff --git a/homeassistant/components/sensor/transmission.py b/homeassistant/components/sensor/transmission.py index 8ce2b6951ca..b9ed3ea4e9f 100644 --- a/homeassistant/components/sensor/transmission.py +++ b/homeassistant/components/sensor/transmission.py @@ -67,6 +67,7 @@ from transmissionrpc.error import TransmissionError import logging +REQUIREMENTS = ['transmissionrpc>=0.11'] SENSOR_TYPES = { 'current_status': ['Status', ''], 'download_speed': ['Down Speed', 'MB/s'], diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index 24ef6e24e05..b1d1d755348 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -25,7 +25,7 @@ from datetime import timedelta try: import ephem except ImportError: - # Error will be raised during setup + # Will be fixed during setup ephem = None import homeassistant.util.dt as dt_util @@ -33,6 +33,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.components.scheduler import ServiceEventListener DEPENDENCIES = [] +REQUIREMENTS = ['pyephem>=3.7'] DOMAIN = "sun" ENTITY_ID = "sun.sun" @@ -100,9 +101,10 @@ def setup(hass, config): """ Tracks the state of the sun. """ logger = logging.getLogger(__name__) + global ephem # pylint: disable=invalid-name if ephem is None: - logger.exception("Error while importing dependency ephem.") - return False + import ephem as ephem_ + ephem = ephem_ if None in (hass.config.latitude, hass.config.longitude): logger.error("Latitude or longitude not set in Home Assistant config") diff --git a/homeassistant/components/switch/hikvisioncam.py b/homeassistant/components/switch/hikvisioncam.py index af9c4c6ad40..388152361d2 100644 --- a/homeassistant/components/switch/hikvisioncam.py +++ b/homeassistant/components/switch/hikvisioncam.py @@ -53,7 +53,7 @@ except ImportError: hikvision.api = None _LOGGING = logging.getLogger(__name__) - +REQUIREMENTS = ['hikvision>=0.4'] # pylint: disable=too-many-arguments # pylint: disable=too-many-instance-attributes diff --git a/homeassistant/components/switch/transmission.py b/homeassistant/components/switch/transmission.py index b3fb04dbd82..8638f11a2e9 100644 --- a/homeassistant/components/switch/transmission.py +++ b/homeassistant/components/switch/transmission.py @@ -54,6 +54,7 @@ from transmissionrpc.error import TransmissionError import logging _LOGGING = logging.getLogger(__name__) +REQUIREMENTS = ['transmissionrpc>=0.11'] # pylint: disable=unused-argument diff --git a/homeassistant/components/thermostat/nest.py b/homeassistant/components/thermostat/nest.py index b9d13b123fb..b2e48b96bcd 100644 --- a/homeassistant/components/thermostat/nest.py +++ b/homeassistant/components/thermostat/nest.py @@ -6,6 +6,8 @@ import logging from homeassistant.components.thermostat import ThermostatDevice from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, TEMP_CELCIUS) +REQUIREMENTS = ['python-nest>=2.3.1'] + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/zwave.py b/homeassistant/components/zwave.py index 1d798746a63..c5967ae0f34 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -13,6 +13,7 @@ from homeassistant.const import ( DOMAIN = "zwave" DEPENDENCIES = [] +REQUIREMENTS = ['pydispatcher>=2.0.5'] CONF_USB_STICK_PATH = "usb_path" DEFAULT_CONF_USB_STICK_PATH = "/zwaveusbstick" diff --git a/homeassistant/config.py b/homeassistant/config.py index 57c38d5bc8c..ec177776d8f 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -11,7 +11,7 @@ from homeassistant import HomeAssistantError from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_TIME_ZONE) -import homeassistant.util as util +import homeassistant.util.location as loc_util _LOGGER = logging.getLogger(__name__) @@ -55,7 +55,7 @@ def create_default_config(config_dir, detect_location=True): info = {attr: default for attr, default, *_ in DEFAULT_CONFIG} - location_info = detect_location and util.detect_location_info() + location_info = detect_location and loc_util.detect_location_info() if location_info: if location_info.use_fahrenheit: diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 5c69fd02243..90476088d25 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -16,8 +16,6 @@ import random import string from functools import wraps -import requests - # DEPRECATED AS OF 4/27/2015 - moved to homeassistant.util.dt package # pylint: disable=unused-import from .dt import ( # noqa @@ -64,46 +62,6 @@ def repr_helper(inp): return str(inp) -# Taken from: http://www.cse.unr.edu/~quiroz/inc/colortransforms.py -# 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): - """ Convert from RGB color to XY color. """ - if R + G + B == 0: - return 0, 0 - - var_R = (R / 255.) - var_G = (G / 255.) - var_B = (B / 255.) - - if var_R > 0.04045: - var_R = ((var_R + 0.055) / 1.055) ** 2.4 - else: - var_R /= 12.92 - - if var_G > 0.04045: - var_G = ((var_G + 0.055) / 1.055) ** 2.4 - else: - var_G /= 12.92 - - if var_B > 0.04045: - var_B = ((var_B + 0.055) / 1.055) ** 2.4 - else: - var_B /= 12.92 - - var_R *= 100 - var_G *= 100 - var_B *= 100 - - # Observer. = 2 deg, Illuminant = D65 - X = var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805 - Y = var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722 - Z = var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505 - - # Convert XYZ to xy, see CIE 1931 color space on wikipedia - return X / (X + Y + Z), Y / (X + Y + Z) - - def convert(value, to_type, default=None): """ Converts value to to_type, returns default if fails. """ try: @@ -154,32 +112,6 @@ def get_random_string(length=10): return ''.join(generator.choice(source_chars) for _ in range(length)) -LocationInfo = collections.namedtuple( - "LocationInfo", - ['ip', 'country_code', 'country_name', 'region_code', 'region_name', - 'city', 'zip_code', 'time_zone', 'latitude', 'longitude', - 'use_fahrenheit']) - - -def detect_location_info(): - """ Detect location information. """ - try: - raw_info = requests.get( - 'https://freegeoip.net/json/', timeout=5).json() - except requests.RequestException: - return - - data = {key: raw_info.get(key) for key in LocationInfo._fields} - - # From Wikipedia: Fahrenheit is used in the Bahamas, Belize, - # the Cayman Islands, Palau, and the United States and associated - # territories of American Samoa and the U.S. Virgin Islands - data['use_fahrenheit'] = data['country_code'] in ( - 'BS', 'BZ', 'KY', 'PW', 'US', 'AS', 'VI') - - return LocationInfo(**data) - - class OrderedEnum(enum.Enum): """ Taken from Python 3.4.0 docs. """ # pylint: disable=no-init, too-few-public-methods diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py new file mode 100644 index 00000000000..5f967fa87b9 --- /dev/null +++ b/homeassistant/util/color.py @@ -0,0 +1,41 @@ +"""Color util methods.""" + + +# Taken from: http://www.cse.unr.edu/~quiroz/inc/colortransforms.py +# 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): + """ Convert from RGB color to XY color. """ + if R + G + B == 0: + return 0, 0 + + var_R = (R / 255.) + var_G = (G / 255.) + var_B = (B / 255.) + + if var_R > 0.04045: + var_R = ((var_R + 0.055) / 1.055) ** 2.4 + else: + var_R /= 12.92 + + if var_G > 0.04045: + var_G = ((var_G + 0.055) / 1.055) ** 2.4 + else: + var_G /= 12.92 + + if var_B > 0.04045: + var_B = ((var_B + 0.055) / 1.055) ** 2.4 + else: + var_B /= 12.92 + + var_R *= 100 + var_G *= 100 + var_B *= 100 + + # Observer. = 2 deg, Illuminant = D65 + X = var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805 + Y = var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722 + Z = var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505 + + # Convert XYZ to xy, see CIE 1931 color space on wikipedia + return X / (X + Y + Z), Y / (X + Y + Z) diff --git a/homeassistant/util/environment.py b/homeassistant/util/environment.py new file mode 100644 index 00000000000..f2e41dfb306 --- /dev/null +++ b/homeassistant/util/environment.py @@ -0,0 +1,7 @@ +""" Environement helpers. """ +import sys + + +def is_virtual(): + """ Return if we run in a virtual environtment. """ + return sys.base_prefix != sys.prefix diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py new file mode 100644 index 00000000000..8cc008613cb --- /dev/null +++ b/homeassistant/util/location.py @@ -0,0 +1,30 @@ +"""Module with location helpers.""" +import collections + +import requests + + +LocationInfo = collections.namedtuple( + "LocationInfo", + ['ip', 'country_code', 'country_name', 'region_code', 'region_name', + 'city', 'zip_code', 'time_zone', 'latitude', 'longitude', + 'use_fahrenheit']) + + +def detect_location_info(): + """ Detect location information. """ + try: + raw_info = requests.get( + 'https://freegeoip.net/json/', timeout=5).json() + except requests.RequestException: + return + + data = {key: raw_info.get(key) for key in LocationInfo._fields} + + # From Wikipedia: Fahrenheit is used in the Bahamas, Belize, + # the Cayman Islands, Palau, and the United States and associated + # territories of American Samoa and the U.S. Virgin Islands + data['use_fahrenheit'] = data['country_code'] in ( + 'BS', 'BZ', 'KY', 'PW', 'US', 'AS', 'VI') + + return LocationInfo(**data) diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py new file mode 100644 index 00000000000..ec49893177f --- /dev/null +++ b/homeassistant/util/package.py @@ -0,0 +1,19 @@ +"""Helpers to install PyPi packages.""" +import subprocess + +from . import environment as env + +# If we are not in a virtual environment, install in user space +INSTALL_USER = not env.is_virtual() + + +def install_package(package, upgrade=False, user=INSTALL_USER): + """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 = ['python3', '-m', 'pip', 'install', '--quiet', package] + if upgrade: + args.append('--upgrade') + if user: + args.append('--user') + return not subprocess.call(args) diff --git a/pylintrc b/pylintrc index a9994eb70f5..54b1f80cdc5 100644 --- a/pylintrc +++ b/pylintrc @@ -9,13 +9,15 @@ reports=no # abstract-class-little-used - Prevents from setting right foundation # abstract-class-not-used - is flaky, should not show up but does # unused-argument - generic callbacks and setup methods create a lot of warnings +# global-statement - used for the on-demand requirement installation disable= locally-disabled, duplicate-code, cyclic-import, abstract-class-little-used, abstract-class-not-used, - unused-argument + unused-argument, + global-statement [EXCEPTIONS] overgeneral-exceptions=Exception,HomeAssistantError diff --git a/requirements.txt b/requirements.txt index b4b79b19763..83780721c2c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -68,7 +68,7 @@ hikvision>=0.4 # console log coloring colorlog>=2.6.0 -# JSON-RPC interface +# JSON-RPC interface (media_player.kodi) jsonrpc-requests>=0.1 # Forecast.io Bindings (sensor.forecast) diff --git a/tests/test_component_light.py b/tests/test_component_light.py index eb8a17361bf..07f8e8e14c9 100644 --- a/tests/test_component_light.py +++ b/tests/test_component_light.py @@ -9,7 +9,7 @@ import unittest import os import homeassistant.loader as loader -import homeassistant.util as util +import homeassistant.util.color as color_util from homeassistant.const import ( ATTR_ENTITY_ID, STATE_ON, STATE_OFF, CONF_PLATFORM, SERVICE_TURN_ON, SERVICE_TURN_OFF) @@ -154,7 +154,7 @@ class TestLight(unittest.TestCase): method, data = dev2.last_call('turn_on') self.assertEqual( - {light.ATTR_XY_COLOR: util.color_RGB_to_xy(255, 255, 255)}, + {light.ATTR_XY_COLOR: color_util.color_RGB_to_xy(255, 255, 255)}, data) method, data = dev3.last_call('turn_on') diff --git a/tests/test_config.py b/tests/test_config.py index 133f7d51f71..0ea18eead82 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -10,7 +10,7 @@ import unittest.mock as mock import os from homeassistant import DOMAIN, HomeAssistantError -import homeassistant.util as util +import homeassistant.util.location as location_util import homeassistant.config as config_util from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_TEMPERATURE_UNIT, CONF_NAME, @@ -31,7 +31,7 @@ def create_file(path): def mock_detect_location_info(): """ Mock implementation of util.detect_location_info. """ - return util.LocationInfo( + return location_util.LocationInfo( ip='1.1.1.1', country_code='US', country_name='United States', @@ -151,7 +151,7 @@ class TestConfig(unittest.TestCase): def test_create_default_config_detect_location(self): """ Test that detect location sets the correct config keys. """ - with mock.patch('homeassistant.util.detect_location_info', + with mock.patch('homeassistant.util.location.detect_location_info', mock_detect_location_info): config_util.ensure_config_exists(CONFIG_DIR) diff --git a/tests/test_util.py b/tests/test_util.py index f75b6db8aeb..5a4fb44b2d4 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -50,21 +50,6 @@ class TestUtil(unittest.TestCase): self.assertEqual("12:00:00 09-07-1986", util.repr_helper(datetime(1986, 7, 9, 12, 0, 0))) - # pylint: disable=invalid-name - def test_color_RGB_to_xy(self): - """ Test color_RGB_to_xy. """ - self.assertEqual((0, 0), util.color_RGB_to_xy(0, 0, 0)) - self.assertEqual((0.3127159072215825, 0.3290014805066623), - util.color_RGB_to_xy(255, 255, 255)) - - self.assertEqual((0.15001662234042554, 0.060006648936170214), - util.color_RGB_to_xy(0, 0, 255)) - - self.assertEqual((0.3, 0.6), util.color_RGB_to_xy(0, 255, 0)) - - self.assertEqual((0.6400744994567747, 0.3299705106316933), - util.color_RGB_to_xy(255, 0, 0)) - def test_convert(self): """ Test convert. """ self.assertEqual(5, util.convert("5", int)) diff --git a/tests/test_util_color.py b/tests/test_util_color.py new file mode 100644 index 00000000000..6b0d169f516 --- /dev/null +++ b/tests/test_util_color.py @@ -0,0 +1,22 @@ +""" +Tests Home Assistant color util methods. +""" +import unittest +import homeassistant.util.color as color_util + + +class TestColorUtil(unittest.TestCase): + # pylint: disable=invalid-name + def test_color_RGB_to_xy(self): + """ Test color_RGB_to_xy. """ + self.assertEqual((0, 0), color_util.color_RGB_to_xy(0, 0, 0)) + self.assertEqual((0.3127159072215825, 0.3290014805066623), + color_util.color_RGB_to_xy(255, 255, 255)) + + self.assertEqual((0.15001662234042554, 0.060006648936170214), + color_util.color_RGB_to_xy(0, 0, 255)) + + self.assertEqual((0.3, 0.6), color_util.color_RGB_to_xy(0, 255, 0)) + + self.assertEqual((0.6400744994567747, 0.3299705106316933), + color_util.color_RGB_to_xy(255, 0, 0))