diff --git a/.coveragerc b/.coveragerc index 4029b48d47cc9459718e2bff5e0b72a576da45fd..00e10da81100a1d8c04ad9d8178041ace2feb9c9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -31,6 +31,7 @@ omit = homeassistant/components/keyboard.py homeassistant/components/light/hue.py homeassistant/components/media_player/cast.py + homeassistant/components/media_player/mpd.py homeassistant/components/notify/instapush.py homeassistant/components/notify/nma.py homeassistant/components/notify/pushbullet.py diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 115ceab89c79af404ee6b1d1bf4b96619ba98cb5..8a080e828dab31a9a9f47ed1590d1fb8b2e779fd 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -2,7 +2,7 @@ homeassistant.components.media_player ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Component to interface with various media players +Component to interface with various media players. """ import logging @@ -41,6 +41,7 @@ ATTR_MEDIA_IMAGE_URL = 'media_image_url' ATTR_MEDIA_VOLUME = 'media_volume' ATTR_MEDIA_IS_VOLUME_MUTED = 'media_is_volume_muted' ATTR_MEDIA_DURATION = 'media_duration' +ATTR_MEDIA_DATE = 'media_date' MEDIA_STATE_UNKNOWN = 'unknown' MEDIA_STATE_PLAYING = 'playing' diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py new file mode 100644 index 0000000000000000000000000000000000000000..53faa37a605a6602fc2072d6632a749fc84e73d8 --- /dev/null +++ b/homeassistant/components/media_player/mpd.py @@ -0,0 +1,195 @@ +""" +homeassistant.components.media_player.mpd +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Provides functionality to interact with a Music Player Daemon. + +Configuration: + +To use MPD you will need to add something like the following to your +config/configuration.yaml + +media_player: + platform: mpd + server: 127.0.0.1 + port: 6600 + location: bedroom + +Variables: + +server +*Required +IP address of the Music Player Daemon. Example: 192.168.1.32 + +port +*Optional +Port of the Music Player Daemon, defaults to 6600. Example: 6600 + +location +*Optional +Location of your Music Player Daemon. +""" +import logging +import socket + +from homeassistant.components.media_player import ( + MediaPlayerDevice, STATE_NO_APP, ATTR_MEDIA_STATE, + ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_TITLE, ATTR_MEDIA_ARTIST, + ATTR_MEDIA_ALBUM, ATTR_MEDIA_DATE, ATTR_MEDIA_DURATION, + ATTR_MEDIA_VOLUME, MEDIA_STATE_PAUSED, MEDIA_STATE_PLAYING, + MEDIA_STATE_STOPPED, MEDIA_STATE_UNKNOWN) + +_LOGGER = logging.getLogger(__name__) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the MPD platform. """ + + daemon = config.get('server', None) + port = config.get('port', 6600) + location = config.get('location', 'MPD') + + try: + from mpd import MPDClient + + except ImportError: + _LOGGER.exception( + "Unable to import mpd2. " + "Did you maybe not install the 'python-mpd2' package?") + + return False + + # pylint: disable=no-member + try: + mpd_client = MPDClient() + mpd_client.connect(daemon, port) + mpd_client.close() + mpd_client.disconnect() + except socket.error: + _LOGGER.error( + "Unable to connect to MPD. " + "Please check your settings") + + return False + + mpd = [] + mpd.append(MpdDevice(daemon, port, location)) + add_devices(mpd) + + +class MpdDevice(MediaPlayerDevice): + """ Represents a MPD server. """ + + def __init__(self, server, port, location): + from mpd import MPDClient + + self.server = server + self.port = port + self._name = location + self.state_attr = {ATTR_MEDIA_STATE: MEDIA_STATE_STOPPED} + + self.client = MPDClient() + self.client.timeout = 10 + self.client.idletimeout = None + self.client.connect(self.server, self.port) + + @property + def name(self): + """ Returns the name of the device. """ + return self._name + + # pylint: disable=no-member + @property + def state(self): + """ Returns the state of the device. """ + status = self.client.status() + + if status is None: + return STATE_NO_APP + else: + return self.client.currentsong()['artist'] + + @property + def media_state(self): + """ Returns the media state. """ + media_controller = self.client.status() + + if media_controller['state'] == 'play': + return MEDIA_STATE_PLAYING + elif media_controller['state'] == 'pause': + return MEDIA_STATE_PAUSED + elif media_controller['state'] == 'stop': + return MEDIA_STATE_STOPPED + else: + return MEDIA_STATE_UNKNOWN + + # pylint: disable=no-member + @property + def state_attributes(self): + """ Returns the state attributes. """ + status = self.client.status() + current_song = self.client.currentsong() + + if not status and not current_song: + state_attr = {} + + if current_song['id']: + state_attr[ATTR_MEDIA_CONTENT_ID] = current_song['id'] + + if current_song['date']: + state_attr[ATTR_MEDIA_DATE] = current_song['date'] + + if current_song['title']: + state_attr[ATTR_MEDIA_TITLE] = current_song['title'] + + if current_song['time']: + state_attr[ATTR_MEDIA_DURATION] = current_song['time'] + + if current_song['artist']: + state_attr[ATTR_MEDIA_ARTIST] = current_song['artist'] + + if current_song['album']: + state_attr[ATTR_MEDIA_ALBUM] = current_song['album'] + + state_attr[ATTR_MEDIA_VOLUME] = status['volume'] + + return state_attr + + def turn_off(self): + """ Service to exit the running MPD. """ + self.client.stop() + + def volume_up(self): + """ Service to send the MPD the command for volume up. """ + current_volume = self.client.status()['volume'] + + if int(current_volume) <= 100: + self.client.setvol(int(current_volume) + 5) + + def volume_down(self): + """ Service to send the MPD the command for volume down. """ + current_volume = self.client.status()['volume'] + + if int(current_volume) >= 0: + self.client.setvol(int(current_volume) - 5) + + def media_play_pause(self): + """ Service to send the MPD the command for play/pause. """ + self.client.pause() + + def media_play(self): + """ Service to send the MPD the command for play/pause. """ + self.client.start() + + def media_pause(self): + """ Service to send the MPD the command for play/pause. """ + self.client.pause() + + def media_next_track(self): + """ Service to send the MPD the command for next track. """ + self.client.next() + + def media_prev_track(self): + """ Service to send the MPD the command for previous track. """ + self.client.previous() diff --git a/homeassistant/components/thermostat/nest.py b/homeassistant/components/thermostat/nest.py index 06adb49a708eee03d312f15f8df37ea1558050c1..af3b02b2b159395ac179353d6e1d1ddab0a037be 100644 --- a/homeassistant/components/thermostat/nest.py +++ b/homeassistant/components/thermostat/nest.py @@ -74,7 +74,21 @@ class NestThermostat(ThermostatDevice): @property def target_temperature(self): """ Returns the temperature we try to reach. """ - return round(self.device.target, 1) + target = self.device.target + + if isinstance(target, tuple): + low, high = target + + if self.current_temperature < low: + target = low + elif self.current_temperature > high: + target = high + else: + target = low + high + else: + temp = target + + return round(temp, 1) @property def is_away_mode_on(self): diff --git a/homeassistant/config.py b/homeassistant/config.py index b24c224f543503964cff10b8860d09f70b219ba0..57c38d5bc8cffee50b810e60163c19a6bf6ec33f 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -19,8 +19,20 @@ _LOGGER = logging.getLogger(__name__) YAML_CONFIG_FILE = 'configuration.yaml' CONF_CONFIG_FILE = 'home-assistant.conf' + +DEFAULT_CONFIG = [ + # Tuples (attribute, default, auto detect property, description) + (CONF_NAME, 'Home', None, 'Name of the location where Home Assistant is ' + 'running'), + (CONF_LATITUDE, None, 'latitude', 'Location required to calculate the time' + ' the sun rises and sets'), + (CONF_LONGITUDE, None, 'longitude', None), + (CONF_TEMPERATURE_UNIT, 'C', None, 'C for Celcius, F for Fahrenheit'), + (CONF_TIME_ZONE, 'UTC', 'time_zone', 'Pick yours from here: http://en.wiki' + 'pedia.org/wiki/List_of_tz_database_time_zones'), +] DEFAULT_COMPONENTS = [ - 'discovery', 'frontend', 'conversation', 'history', 'logbook'] + 'discovery', 'frontend', 'conversation', 'history', 'logbook', 'sun'] def ensure_config_exists(config_dir, detect_location=True): @@ -41,29 +53,33 @@ def create_default_config(config_dir, detect_location=True): Returns path to new config file if success, None if failed. """ config_path = os.path.join(config_dir, YAML_CONFIG_FILE) + info = {attr: default for attr, default, *_ in DEFAULT_CONFIG} + + location_info = detect_location and util.detect_location_info() + + if location_info: + if location_info.use_fahrenheit: + info[CONF_TEMPERATURE_UNIT] = 'F' + + for attr, default, prop, _ in DEFAULT_CONFIG: + if prop is None: + continue + info[attr] = getattr(location_info, prop) or default + # Writing files with YAML does not create the most human readable results # So we're hard coding a YAML template. try: with open(config_path, 'w') as config_file: - location_info = detect_location and util.detect_location_info() - - if location_info: - temp_unit = 'F' if location_info.use_fahrenheit else 'C' - - auto_config = { - CONF_NAME: 'Home', - CONF_LATITUDE: location_info.latitude, - CONF_LONGITUDE: location_info.longitude, - CONF_TEMPERATURE_UNIT: temp_unit, - CONF_TIME_ZONE: location_info.time_zone, - } - - config_file.write("homeassistant:\n") + config_file.write("homeassistant:\n") - for key, value in auto_config.items(): - config_file.write(" {}: {}\n".format(key, value)) + for attr, _, _, description in DEFAULT_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") + config_file.write("\n") for component in DEFAULT_COMPONENTS: config_file.write("{}:\n\n".format(component)) diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index 4d92df432820ef4f1d6422ce0d7792f01bb9cf4b..086cddc35e2447cac4f3a94c739b2a0bdc92a8b2 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -2,7 +2,8 @@ Helper methods for components within Home Assistant. """ from homeassistant.loader import get_component -from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM +from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_PLATFORM, DEVICE_DEFAULT_NAME) from homeassistant.util import ensure_unique_string, slugify # Deprecated 3/5/2015 - Moved to homeassistant.helpers.entity @@ -12,6 +13,7 @@ from .entity import Entity as Device, ToggleEntity as ToggleDevice # noqa def generate_entity_id(entity_id_format, name, current_ids=None, hass=None): """ Generate a unique entity ID based on given entity IDs or used ids. """ + name = name.lower() or DEVICE_DEFAULT_NAME.lower() if current_ids is None: if hass is None: raise RuntimeError("Missing required parameter currentids or hass") diff --git a/requirements.txt b/requirements.txt index e5ef482d54ec9c9ff96061f4eb183c651e4b82d5..fd234122c0308c1835b01e34f750da037a48b8aa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,56 +1,56 @@ -# required for Home Assistant core +# Required for Home Assistant core requests>=2.0 pyyaml>=3.11 pytz>=2015.2 -# optional, needed for specific components +# Optional, needed for specific components -# discovery +# Discovery platform (discovery) zeroconf>=0.16.0 -# sun +# Sun (sun) pyephem>=3.7 -# lights.hue +# Philips Hue library (lights.hue) phue>=0.8 -# lights.limitlessled +# Limitlessled/Easybulb/Milight library (lights.limitlessled) ledcontroller>=1.0.7 -# media_player.cast +# Chromecast bindings (media_player.cast) pychromecast>=0.6.4 -# keyboard +# Keyboard (keyboard) pyuserinput>=0.1.9 -# switch.tellstick, sensor.tellstick +# Tellstick bindings (*.tellstick) tellcore-py>=1.0.4 -# device_tracker.nmap +# Nmap bindings (device_tracker.nmap) python-libnmap>=0.6.2 -# notify.pushbullet +# PushBullet bindings (notify.pushbullet) pushbullet.py>=0.7.1 -# thermostat.nest -python-nest>=2.1 +# Nest Thermostat bindings (thermostat.nest) +python-nest>=2.3.1 -# z-wave +# Z-Wave (*.zwave) pydispatcher>=2.0.5 -# isy994 +# ISY994 bindings (*.isy994 PyISY>=1.0.2 -# sensor.systemmonitor +# PSutil (sensor.systemmonitor) psutil>=2.2.1 -# pushover notifications +# Pushover bindings (notify.pushover) python-pushover>=0.2 -# Transmission Torrent Client +# Transmission Torrent Client (*.transmission) transmissionrpc>=0.11 -# OpenWeatherMap Web API +# OpenWeatherMap Web API (sensor.openweathermap) pyowm>=2.2.0 # XMPP Bindings (notify.xmpp) @@ -58,3 +58,6 @@ sleekxmpp>=1.3.1 # Blockchain (sensor.bitcoin) blockchain>=1.1.2 + +# MPD Bindings (media_player.mpd) +python-mpd2>=0.5.4