From 96edd759a8d33dea8dc2fd096ec8555808a94001 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 30 May 2015 00:52:33 -0700 Subject: [PATCH 01/95] media_player.cast: support thumbnail + title --- homeassistant/components/media_player/cast.py | 24 ++++++++++++------- requirements.txt | 4 ++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 35d9748c663..0685b31f439 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -15,10 +15,12 @@ except ImportError: # We will throw error later pass +from homeassistant.const import ATTR_ENTITY_PICTURE + # ATTR_MEDIA_ALBUM, ATTR_MEDIA_IMAGE_URL, -# ATTR_MEDIA_TITLE, ATTR_MEDIA_ARTIST, +# ATTR_MEDIA_ARTIST, from homeassistant.components.media_player import ( - MediaPlayerDevice, STATE_NO_APP, ATTR_MEDIA_STATE, + MediaPlayerDevice, STATE_NO_APP, ATTR_MEDIA_STATE, ATTR_MEDIA_TITLE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_DURATION, ATTR_MEDIA_VOLUME, MEDIA_STATE_PLAYING, MEDIA_STATE_PAUSED, MEDIA_STATE_STOPPED, MEDIA_STATE_UNKNOWN) @@ -95,7 +97,8 @@ class CastDevice(MediaPlayerDevice): def state_attributes(self): """ Returns the state attributes. """ cast_status = self.cast.status - media_status = self.cast.media_status + media_controller = self.cast.media_controller + media_status = media_controller.status state_attr = { ATTR_MEDIA_STATE: self.media_state, @@ -105,12 +108,17 @@ class CastDevice(MediaPlayerDevice): if cast_status: state_attr[ATTR_MEDIA_VOLUME] = cast_status.volume_level, - if media_status: - if media_status.content_id: - state_attr[ATTR_MEDIA_CONTENT_ID] = media_status.content_id + if media_status.content_id: + state_attr[ATTR_MEDIA_CONTENT_ID] = media_status.content_id - if media_status.duration: - state_attr[ATTR_MEDIA_DURATION] = media_status.duration + if media_status.duration: + state_attr[ATTR_MEDIA_DURATION] = media_status.duration + + if media_controller.title: + state_attr[ATTR_MEDIA_TITLE] = media_controller.title + + if media_controller.thumbnail: + state_attr[ATTR_ENTITY_PICTURE] = media_controller.thumbnail return state_attr diff --git a/requirements.txt b/requirements.txt index 6d2e032af45..c1c5403be3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ phue>=0.8 ledcontroller>=1.0.7 # media_player.cast -pychromecast>=0.6 +pychromecast>=0.6.0.3 # keyboard pyuserinput>=0.1.9 @@ -41,7 +41,7 @@ pydispatcher>=2.0.5 # isy994 PyISY>=1.0.2 -# sensor.systemmonitor +# sensor.systemmonitor psutil>=2.2.1 # pushover notifications From ecd09f22efec66d61f6a33943678234036365c53 Mon Sep 17 00:00:00 2001 From: Finbarr Brady Date: Sat, 30 May 2015 13:44:29 +0000 Subject: [PATCH 02/95] Support for Hikvision camera motion detection. --- homeassistant/components/switch/hikvision.py | 221 +++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 homeassistant/components/switch/hikvision.py diff --git a/homeassistant/components/switch/hikvision.py b/homeassistant/components/switch/hikvision.py new file mode 100644 index 00000000000..31684171e1c --- /dev/null +++ b/homeassistant/components/switch/hikvision.py @@ -0,0 +1,221 @@ +""" +homeassistant.components.switch.hikvision +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Support turning on/off motion detection on Hikvision cameras. + +Note: Currently works using default https port only. + +CGI API Guide: +http://bit.ly/1RuyUuF + +Configuration: + +To use the Hikvision motion detection +switch you will need to add something like the +following to your config/configuration.yaml + +switch: + platform: hikvision + name: Hikvision Cam 1 Motion Detection + host: 192.168.1.26 + username: YOUR_USERNAME + password: YOUR_PASSWORD + +Variables: + +host +*Required +This is the IP address of your Hikvision camera. Example: 192.168.1.32 + +username +*Required +Your Hikvision camera username + +password +*Required +Your Hikvision camera username + +name +*Optional +The name to use when displaying this switch instance. + +""" +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +import logging +import requests +from requests.auth import HTTPBasicAuth +from xml.etree import ElementTree + +_LOGGING = logging.getLogger(__name__) + +# pylint: disable=too-many-arguments +# pylint: disable=too-many-instance-attributes + + +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Setup Hikvision Camera config. """ + + host = config.get(CONF_HOST, None) + port = config.get('port', "80") + name = config.get('name', "Hikvision Camera Motion Detection") + username = config.get(CONF_USERNAME, "admin") + password = config.get(CONF_PASSWORD, "12345") + channel_id = config.get('channel_id', "1") + xml_namespace = config.get( + 'xml_namespace', "http://www.hikvision.com/ver10/XMLSchema") + + # Required to parse and change xml with the host camera + _LOGGING.info('ElementTree.register_namespace: %s', xml_namespace) + ElementTree.register_namespace("", xml_namespace) + + if not host: + _LOGGING.error('Missing config variable-host') + return False + + add_devices_callback([ + HikvisionMotionSwitch( + name, host, port, username, password, channel_id, xml_namespace) + ]) + + +class HikvisionMotionSwitch(ToggleEntity): + + """ Provides a switch to toggle on/off motion detection. """ + + def __init__(self, name, host, port, username, + password, channel_id, xml_namespace): + self._name = name + self._username = username + self._password = password + self._channel_id = channel_id + self._host = host + self._port = port + self._xml_namespace = xml_namespace + self._state = STATE_OFF + self.url = 'https://%s/MotionDetection/%s/' % ( + self._host, self._channel_id) + self.xml_motion_detection_off = None + self.xml_motion_detection_on = None + self.update() + + @property + def should_poll(self): + """ Poll for status regularly. """ + return True + + @property + def name(self): + """ Returns the name of the device if any. """ + return self._name + + @property + def state(self): + """ Returns the state of the device if any. """ + return self._state + + @property + def is_on(self): + """ True if device is on. """ + return self._state == STATE_ON + + def turn_on(self, **kwargs): + """ Turn the device on. """ + + _LOGGING.info("Turning on Motion Detection ") + self.toggle_motion_detection() + + def turn_off(self, **kwargs): + """ Turn the device off. """ + + _LOGGING.info("Turning off Motion Detection ") + self.toggle_motion_detection() + + def toggle_motion_detection(self): + """ + # See http://bit.ly/1KtcW7b + """ + + if self._state == STATE_ON: + xml = self.xml_motion_detection_off + self._state = STATE_OFF + else: + self._state = STATE_ON + xml = self.xml_motion_detection_on + + _LOGGING.info('xml:') + _LOGGING.info("%s", xml) + + response = requests.put(self.url, auth=HTTPBasicAuth( + self._username, self._password), verify=False, data=xml) + _LOGGING.info('Response: %s', response.text) + + if response.status_code != 200: + _LOGGING.error("There was an error connecting to %s", self.url) + _LOGGING.error("status_code %s", response.esponsestatus_code) + return + + try: + tree = ElementTree.fromstring(response.content) + find_result = tree.findall( + './/{%s}statusString' % self._xml_namespace) + if len(find_result) == 0: + _LOGGING.error("Problem getting motion detection status") + self.update() + return + + if find_result[0].text.strip() == 'OK': + _LOGGING.info('Updated successfully') + + except AttributeError as attib_err: + _LOGGING.error( + 'There was a problem parsing the response: %s', attib_err) + self.update() + return + + def update(self): + """ + # See http://bit.ly/1KtcW7b + """ + _LOGGING.info('url: %s', self.url) + + response = requests.get(self.url, auth=HTTPBasicAuth( + self._username, self._password), verify=False) + _LOGGING.info('Response: %s', response.text) + + if response.status_code != 200: + _LOGGING.error("There was an error connecting to %s", self.url) + _LOGGING.error("status_code %s", response.status_code) + return + + try: + tree = ElementTree.fromstring(response.content) + find_result = tree.findall('.//{%s}enabled' % self._xml_namespace) + if len(find_result) == 0: + _LOGGING.error("Problem getting motion detection status") + return + + result = find_result[0].text.strip() + _LOGGING.info( + 'Current motion detection state? enabled: %s', result) + + if result == 'true': + self._state = STATE_ON + # Save this for future switch off + find_result[0].text = 'false' + self.xml_motion_detection_off = ElementTree.tostring( + tree, encoding='unicode') + else: + self._state = STATE_OFF + # Save this for future switch on + find_result[0].text = 'true' + self.xml_motion_detection_on = ElementTree.tostring( + tree, encoding='unicode') + + except AttributeError as attib_err: + _LOGGING.error( + 'There was a problem parsing ' + 'camera motion detection state: %s', attib_err) + return From 1be50d83dcecb8a5609df3f0b2bd78fad1087f6e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 31 May 2015 22:54:16 +0200 Subject: [PATCH 03/95] add timestamp to short time --- homeassistant/util/dt.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index fbe00c85527..f3c96368667 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -108,6 +108,14 @@ def str_to_datetime(dt_str): except ValueError: # If dt_str did not match our format return None +def timestamp_to_short_time_str(timestamp): + """ Converts a UNIX timestamp to a short time string format. + + @rtype: str + """ + return dt.datetime.fromtimestamp( + int(timestamp)).strftime(TIME_SHORT_STR_FORMAT) + def strip_microseconds(dattim): """ Returns a copy of dattime object but with microsecond set to 0. """ From a6b7f47d744e4bd1c3c8367a4be2b09f658f61a6 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 31 May 2015 22:55:02 +0200 Subject: [PATCH 04/95] add swiss public transport sensor --- .../sensor/swiss_public_transport.py | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 homeassistant/components/sensor/swiss_public_transport.py diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py new file mode 100644 index 00000000000..ee162a37164 --- /dev/null +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -0,0 +1,148 @@ +""" +homeassistant.components.sensor.swiss_public_transport +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The Swiss public transport sensor will give you the next two departure times +from a given location to another one. This sensor is limited to Switzerland. + +Configuration: + +To use the Swiss public transport sensor you will need to add something like +the following to your config/configuration.yaml + +sensor: + platform: swiss_public_transport + from: STATION_ID + to: STATION_ID + +Variables: + +from +*Required +Start station/stop of your trip. To search for the ID of the station, use the +an URL like this: http://transport.opendata.ch/v1/locations?query=Wankdorf +to query for the station. If the score is 100 ("score":"100" in the response), +it is a perfect match. + +to +*Required +Destination station/stop of the trip. Same procedure as for the start station. + +Details for the API : http://transport.opendata.ch +""" +import logging +from datetime import timedelta + +from homeassistant.util import Throttle +import homeassistant.util.dt as dt_util +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) +_RESOURCE = 'http://transport.opendata.ch/v1/' + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Get the Swiss public transport sensor. """ + + if None in (hass.config.latitude, hass.config.longitude): + _LOGGER.error("Latitude or longitude not set in Home Assistant config") + return False + + try: + # pylint: disable=unused-variable + from requests import get + + except ImportError: + _LOGGER.exception( + "Unable to import requests. " + "Did you maybe not install the 'Requests' package?") + + return None + + # journal contains [0] Station ID start, [1] Station ID destination + # [2] Station name start, and [3] Station name destination + journey = [] + journey.append(config.get('from', None)) + journey.append(config.get('to', 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) + journey.append(result.json()['stations'][0]['name']) + except KeyError: + _LOGGER.error( + "Unable to determine stations. " + "Check your settings and/or the availability of opendata.ch") + + return None + + dev = [] + data = PublicTransportData(journey) + dev.append(SwissPublicTransportSensor(data, journey)) + add_devices(dev) + + +# pylint: disable=too-few-public-methods +class SwissPublicTransportSensor(Entity): + """ Implements an Swiss public transport sensor. """ + + def __init__(self, data, journey): + self.data = data + self._name = journey[2] + '-' + journey[3] + self.update() + + @property + def name(self): + """ Returns the name. """ + return self._name + + @property + def state(self): + """ Returns the state of the device. """ + return self._state + + # pylint: disable=too-many-branches + def update(self): + """ Gets the latest data from opendata.ch and updates the states. """ + times = self.data.update() + if times is not None: + self._state = times[0] + ', ' + times[1] + + +# pylint: disable=too-few-public-methods +class PublicTransportData(object): + """ Class for handling the data retrieval. """ + + def __init__(self, journey): + self.times = ['n/a', 'n/a'] + self.start = journey[0] + self.destination = journey[1] + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """ Gets the latest data from opendata.ch. """ + + from requests import get + + response = get( + _RESOURCE + + 'connections?' + + 'from=' + self.start + '&' + + 'to=' + self.destination + '&' + + 'fields[]=connections/from/departureTimestamp/&' + + 'fields[]=connections/') + + try: + self.times.insert(0, dt_util.timestamp_to_short_time_str( + response.json()['connections'][0]['from'] + ['departureTimestamp'])) + self.times.insert(1, dt_util.timestamp_to_short_time_str( + response.json()['connections'][1]['from'] + ['departureTimestamp'])) + return self.times + + except KeyError: + return self.times \ No newline at end of file From b1b0b2bc651031525cd5fb34802c4a83d459179a Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 31 May 2015 22:55:47 +0200 Subject: [PATCH 05/95] add newline at the end --- homeassistant/components/sensor/swiss_public_transport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index ee162a37164..bd12cb84cf9 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -145,4 +145,4 @@ class PublicTransportData(object): return self.times except KeyError: - return self.times \ No newline at end of file + return self.times From fafea688e44f4fdb95f90c147f46863c6807bc74 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 31 May 2015 23:25:38 +0200 Subject: [PATCH 06/95] add newline --- homeassistant/util/dt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index f3c96368667..dd9d06ef674 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -108,6 +108,7 @@ def str_to_datetime(dt_str): except ValueError: # If dt_str did not match our format return None + def timestamp_to_short_time_str(timestamp): """ Converts a UNIX timestamp to a short time string format. From c8c3b825e45f01888598745d25a6964ddc8864e1 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 31 May 2015 23:31:24 +0200 Subject: [PATCH 07/95] add swiss_public_transport.py --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 4029b48d47c..e1a939dd03d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -40,6 +40,7 @@ omit = homeassistant/components/sensor/mysensors.py homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/sabnzbd.py + homeassistant/components/sensor/swiss_public_transport.py homeassistant/components/sensor/systemmonitor.py homeassistant/components/sensor/time_date.py homeassistant/components/sensor/transmission.py From 038bfde6aa493107e5c779a800c9f31d5f85d791 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 1 Jun 2015 13:37:57 +0200 Subject: [PATCH 08/95] add shortcut --- homeassistant/components/sensor/swiss_public_transport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index bd12cb84cf9..c48194d6eee 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -109,7 +109,7 @@ class SwissPublicTransportSensor(Entity): """ Gets the latest data from opendata.ch and updates the states. """ times = self.data.update() if times is not None: - self._state = times[0] + ', ' + times[1] + self._state = ', '.join(times) # pylint: disable=too-few-public-methods From 1aa98bea2be15d525622b82d5a39fd4b881647b3 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 1 Jun 2015 13:40:21 +0200 Subject: [PATCH 09/95] remove lat/long check --- homeassistant/components/sensor/swiss_public_transport.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index c48194d6eee..4a4ac58eb3d 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -47,10 +47,6 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) def setup_platform(hass, config, add_devices, discovery_info=None): """ Get the Swiss public transport sensor. """ - if None in (hass.config.latitude, hass.config.longitude): - _LOGGER.error("Latitude or longitude not set in Home Assistant config") - return False - try: # pylint: disable=unused-variable from requests import get From da68e4ab11b668f1873a760a895b0dc44c7f670f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 1 Jun 2015 13:47:32 +0200 Subject: [PATCH 10/95] update depenency handling (requests) --- .../components/sensor/swiss_public_transport.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index 4a4ac58eb3d..41fb249316e 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -32,6 +32,7 @@ Details for the API : http://transport.opendata.ch """ import logging from datetime import timedelta +from requests import get from homeassistant.util import Throttle import homeassistant.util.dt as dt_util @@ -47,17 +48,6 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) def setup_platform(hass, config, add_devices, discovery_info=None): """ Get the Swiss public transport sensor. """ - try: - # pylint: disable=unused-variable - from requests import get - - except ImportError: - _LOGGER.exception( - "Unable to import requests. " - "Did you maybe not install the 'Requests' package?") - - return None - # journal contains [0] Station ID start, [1] Station ID destination # [2] Station name start, and [3] Station name destination journey = [] @@ -121,8 +111,6 @@ class PublicTransportData(object): def update(self): """ Gets the latest data from opendata.ch. """ - from requests import get - response = get( _RESOURCE + 'connections?' + From 9d1e881f12d94b729da190995b9ea1ef56dba02d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 1 Jun 2015 13:49:08 +0200 Subject: [PATCH 11/95] use string formatting --- homeassistant/components/sensor/swiss_public_transport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index 41fb249316e..2ec16f72452 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -77,7 +77,7 @@ class SwissPublicTransportSensor(Entity): def __init__(self, data, journey): self.data = data - self._name = journey[2] + '-' + journey[3] + self._name = '{}-{}'.format(journey[2], journey[3]) self.update() @property From 90d55ef9011af26f1b00db27900a08d244bd3323 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 1 Jun 2015 13:49:46 +0200 Subject: [PATCH 12/95] switch from error to execption for logger --- homeassistant/components/sensor/swiss_public_transport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index 2ec16f72452..6e56a7bc458 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -59,7 +59,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): result = get(_RESOURCE + 'locations?query=%s' % location) journey.append(result.json()['stations'][0]['name']) except KeyError: - _LOGGER.error( + _LOGGER.exception( "Unable to determine stations. " "Check your settings and/or the availability of opendata.ch") From 1e5e06fef5cb60b8d7a07499c717d602c7acd9a0 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 1 Jun 2015 13:51:00 +0200 Subject: [PATCH 13/95] update journey --- homeassistant/components/sensor/swiss_public_transport.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index 6e56a7bc458..f2d8b65a95f 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -50,9 +50,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # journal contains [0] Station ID start, [1] Station ID destination # [2] Station name start, and [3] Station name destination - journey = [] - journey.append(config.get('from', None)) - journey.append(config.get('to', None)) + journey = [config.get('from'), config.get('to')] try: for location in [config.get('from', None), config.get('to', None)]: # transport.opendata.ch doesn't play nice with requests.Session From c8111b988eba0d3fd1f27c6878ab928fb2c3b7fb Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 1 Jun 2015 14:02:27 +0200 Subject: [PATCH 14/95] Revert "add timestamp to short time" This reverts commit 1be50d83dcecb8a5609df3f0b2bd78fad1087f6e. Conflicts: homeassistant/util/dt.py --- homeassistant/util/dt.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index dd9d06ef674..fbe00c85527 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -109,15 +109,6 @@ def str_to_datetime(dt_str): return None -def timestamp_to_short_time_str(timestamp): - """ Converts a UNIX timestamp to a short time string format. - - @rtype: str - """ - return dt.datetime.fromtimestamp( - int(timestamp)).strftime(TIME_SHORT_STR_FORMAT) - - def strip_microseconds(dattim): """ Returns a copy of dattime object but with microsecond set to 0. """ return dattim.replace(microsecond=0) From a7b79fc8b26f7886b7f4dce59d568369a1fed0c7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 1 Jun 2015 00:24:17 -0700 Subject: [PATCH 15/95] Initial refactor media player --- homeassistant/components/keyboard.py | 6 +- .../components/media_player/__init__.py | 339 +++++++++++++----- homeassistant/components/media_player/demo.py | 143 ++++---- homeassistant/const.py | 4 +- tests/test_component_media_player.py | 7 +- 5 files changed, 345 insertions(+), 154 deletions(-) diff --git a/homeassistant/components/keyboard.py b/homeassistant/components/keyboard.py index b4959b48055..b59fe8d39dc 100644 --- a/homeassistant/components/keyboard.py +++ b/homeassistant/components/keyboard.py @@ -8,7 +8,7 @@ import logging from homeassistant.const import ( SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, - SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK, + SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_PLAY_PAUSE) @@ -43,7 +43,7 @@ def media_next_track(hass): def media_prev_track(hass): """ Press the keyboard button for prev track. """ - hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK) + hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK) def setup(hass, config): @@ -79,7 +79,7 @@ def setup(hass, config): lambda service: keyboard.tap_key(keyboard.media_next_track_key)) - hass.services.register(DOMAIN, SERVICE_MEDIA_PREV_TRACK, + hass.services.register(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, lambda service: keyboard.tap_key(keyboard.media_prev_track_key)) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 8a080e828da..1d11ef2a37d 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -10,11 +10,12 @@ from homeassistant.components import discovery from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, STATE_UNKNOWN, STATE_PLAYING, + ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_SET, SERVICE_VOLUME_MUTE, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE, - SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK) + SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK) DOMAIN = 'media_player' DEPENDENCIES = [] @@ -28,29 +29,67 @@ DISCOVERY_PLATFORMS = { SERVICE_YOUTUBE_VIDEO = 'play_youtube_video' -STATE_NO_APP = 'idle' - -ATTR_STATE = 'state' -ATTR_OPTIONS = 'options' -ATTR_MEDIA_STATE = 'media_state' +ATTR_MEDIA_VOLUME_LEVEL = 'volume_level' +ATTR_MEDIA_VOLUME_MUTED = 'volume_muted' ATTR_MEDIA_CONTENT_ID = 'media_content_id' +ATTR_MEDIA_CONTENT_TYPE = 'media_content_type' +ATTR_MEDIA_DURATION = 'media_duration' ATTR_MEDIA_TITLE = 'media_title' ATTR_MEDIA_ARTIST = 'media_artist' ATTR_MEDIA_ALBUM = 'media_album' -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' +ATTR_MEDIA_SERIES_TITLE = 'media_series_title' +ATTR_MEDIA_SEASON = 'media_season' +ATTR_MEDIA_EPISODE = 'media_episode' +ATTR_APP_ID = 'app_id' +ATTR_APP_NAME = 'app_name' +ATTR_SUPPORTED_MEDIA_COMMANDS = 'supported_media_commands' -MEDIA_STATE_UNKNOWN = 'unknown' -MEDIA_STATE_PLAYING = 'playing' -MEDIA_STATE_PAUSED = 'paused' -MEDIA_STATE_STOPPED = 'stopped' +MEDIA_TYPE_MUSIC = 'music' +MEDIA_TYPE_TVSHOW = 'tvshow' +MEDIA_TYPE_VIDEO = 'movie' +SUPPORT_PAUSE = 1 +SUPPORT_SEEK = 2 +SUPPORT_VOLUME_SET = 4 +SUPPORT_VOLUME_MUTE = 8 +SUPPORT_PREVIOUS_TRACK = 16 +SUPPORT_NEXT_TRACK = 32 +SUPPORT_YOUTUBE = 64 +SUPPORT_TURN_ON = 128 +SUPPORT_TURN_OFF = 256 YOUTUBE_COVER_URL_FORMAT = 'http://img.youtube.com/vi/{}/1.jpg' +SERVICE_TO_METHOD = { + SERVICE_TURN_ON: 'turn_on', + SERVICE_TURN_OFF: 'turn_off', + SERVICE_VOLUME_UP: 'volume_up', + SERVICE_VOLUME_DOWN: 'volume_down', + SERVICE_MEDIA_PLAY_PAUSE: 'media_play_pause', + SERVICE_MEDIA_PLAY: 'media_play', + SERVICE_MEDIA_PAUSE: 'media_pause', + SERVICE_MEDIA_NEXT_TRACK: 'media_next_track', + SERVICE_MEDIA_PREVIOUS_TRACK: 'media_previous_track', +} + +ATTR_TO_PROPERTY = { + ATTR_MEDIA_VOLUME_LEVEL: 'volume_level', + ATTR_MEDIA_VOLUME_MUTED: 'is_volume_muted', + ATTR_MEDIA_CONTENT_ID: 'media_content_id', + ATTR_MEDIA_CONTENT_TYPE: 'media_content_type', + ATTR_MEDIA_DURATION: 'media_duration', + ATTR_ENTITY_PICTURE: 'media_image_url', + ATTR_MEDIA_TITLE: 'media_title', + ATTR_MEDIA_ARTIST: 'media_artist', + ATTR_MEDIA_ALBUM: 'media_album', + ATTR_MEDIA_SERIES_TITLE: 'media_series_title', + ATTR_MEDIA_SEASON: 'media_season', + ATTR_MEDIA_EPISODE: 'media_episode', + ATTR_APP_ID: 'app_id', + ATTR_APP_NAME: 'app_name', + ATTR_SUPPORTED_MEDIA_COMMANDS: 'supported_media_commands', +} + def is_on(hass, entity_id=None): """ Returns true if specified media player entity_id is on. @@ -58,7 +97,7 @@ def is_on(hass, entity_id=None): entity_ids = [entity_id] if entity_id else hass.states.entity_ids(DOMAIN) - return any(not hass.states.is_state(entity_id, STATE_NO_APP) + return any(not hass.states.is_state(entity_id, STATE_OFF) for entity_id in entity_ids) @@ -90,21 +129,22 @@ def volume_down(hass, entity_id=None): hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN, data) -def volume_mute(hass, entity_id=None): - """ Send the media player the command to toggle its mute state. """ - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} +def mute_volume(hass, mute, entity_id=None): + """ Send the media player the command for volume down. """ + data = {ATTR_MEDIA_VOLUME_MUTED: mute} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE, data) -def volume_set(hass, entity_id=None, volume=None): - """ Set volume on media player. """ - data = { - key: value for key, value in [ - (ATTR_ENTITY_ID, entity_id), - (ATTR_MEDIA_VOLUME, volume), - ] if value is not None - } +def set_volume_level(hass, volume, entity_id=None): + """ Send the media player the command for volume down. """ + data = {ATTR_MEDIA_VOLUME_LEVEL: volume} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id hass.services.call(DOMAIN, SERVICE_VOLUME_SET, data) @@ -137,24 +177,11 @@ def media_next_track(hass, entity_id=None): hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, data) -def media_prev_track(hass, entity_id=None): +def media_previous_track(hass, entity_id=None): """ Send the media player the command for prev track. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK, data) - - -SERVICE_TO_METHOD = { - SERVICE_TURN_ON: 'turn_on', - SERVICE_TURN_OFF: 'turn_off', - SERVICE_VOLUME_UP: 'volume_up', - SERVICE_VOLUME_DOWN: 'volume_down', - SERVICE_MEDIA_PLAY_PAUSE: 'media_play_pause', - SERVICE_MEDIA_PLAY: 'media_play', - SERVICE_MEDIA_PAUSE: 'media_pause', - SERVICE_MEDIA_NEXT_TRACK: 'media_next_track', - SERVICE_MEDIA_PREV_TRACK: 'media_prev_track', -} + hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data) def setup(hass, config): @@ -183,17 +210,16 @@ def setup(hass, config): def volume_set_service(service, volume): """ Set specified volume on the media player. """ target_players = component.extract_from_service(service) + volume = service.data.get(ATTR_MEDIA_VOLUME_LEVEL) - for player in target_players: - player.volume_set(volume) + if volume is not None: + for player in target_players: + player.set_volume_level(volume) - if player.should_poll: - player.update_ha_state(True) + if player.should_poll: + player.update_ha_state(True) - hass.services.register(DOMAIN, SERVICE_VOLUME_SET, - lambda service: - volume_set_service( - service, service.data.get('volume'))) + hass.services.register(DOMAIN, SERVICE_VOLUME_SET, volume_set_service) def volume_mute_service(service, mute): """ Mute (true) or unmute (false) the media player. """ @@ -239,51 +265,198 @@ def setup(hass, config): class MediaPlayerDevice(Entity): """ ABC for media player devices. """ + # pylint: disable=too-many-public-methods,no-self-use + + # Implement these for your media player + + @property + def state(self): + """ State of the player. """ + return STATE_UNKNOWN + + @property + def volume_level(self): + """ Volume level of the media player (0..1). """ + return None + + @property + def is_volume_muted(self): + """ Boolean if volume is currently muted. """ + return None + + @property + def media_content_id(self): + """ Content ID of current playing media. """ + return None + + @property + def media_content_type(self): + """ Content type of current playing media. """ + return None + + @property + def media_duration(self): + """ Duration of current playing media in seconds. """ + return None + + @property + def media_image_url(self): + """ Image url of current playing media. """ + return None + + @property + def media_title(self): + """ Title of current playing media. """ + return None + + @property + def media_artist(self): + """ Artist of current playing media. (Music track only) """ + return None + + @property + def media_album(self): + """ Album of current playing media. (Music track only) """ + return None + + @property + def media_series_title(self): + """ Series title of current playing media. (TV Show only)""" + return None + + @property + def media_season(self): + """ Season of current playing media. (TV Show only) """ + return None + + @property + def media_episode(self): + """ Episode of current playing media. (TV Show only) """ + return None + + @property + def app_id(self): + """ ID of the current running app. """ + return None + + @property + def app_name(self): + """ Name of the current running app. """ + return None + + @property + def supported_media_commands(self): + """ Flags of media commands that are supported. """ + return 0 + + @property + def device_state_attributes(self): + """ Extra attributes a device wants to expose. """ + return None def turn_on(self): - """ turn media player on. """ - pass + """ turn the media player on. """ + raise NotImplementedError() def turn_off(self): - """ turn media player off. """ - pass + """ turn the media player off. """ + raise NotImplementedError() - def volume_up(self): - """ volume_up media player. """ - pass + def mute_volume(self, mute): + """ mute the volume. """ + raise NotImplementedError() - def volume_down(self): - """ volume_down media player. """ - pass - - def volume_mute(self, mute): - """ mute (true) or unmute (false) media player. """ - pass - - def volume_set(self, volume): - """ set volume level of media player. """ - pass - - def media_play_pause(self): - """ media_play_pause media player. """ - pass + def set_volume_level(self, volume): + """ set volume level, range 0..1. """ + raise NotImplementedError() def media_play(self): - """ media_play media player. """ - pass + """ Send play commmand. """ + raise NotImplementedError() def media_pause(self): - """ media_pause media player. """ - pass + """ Send pause command. """ + raise NotImplementedError() - def media_prev_track(self): - """ media_prev_track media player. """ - pass + def media_previous_track(self): + """ Send previous track command. """ + raise NotImplementedError() def media_next_track(self): - """ media_next_track media player. """ - pass + """ Send next track command. """ + raise NotImplementedError() def play_youtube(self, media_id): """ Plays a YouTube media. """ - pass + raise NotImplementedError() + + # No need to overwrite these. + @property + def support_pause(self): + """ Boolean if pause is supported. """ + return bool(self.supported_media_commands & SUPPORT_PAUSE) + + @property + def support_seek(self): + """ Boolean if seek is supported. """ + return bool(self.supported_media_commands & SUPPORT_SEEK) + + @property + def support_volume_set(self): + """ Boolean if setting volume is supported. """ + return bool(self.supported_media_commands & SUPPORT_VOLUME_SET) + + @property + def support_volume_mute(self): + """ Boolean if muting volume is supported. """ + return bool(self.supported_media_commands & SUPPORT_VOLUME_MUTE) + + @property + def support_previous_track(self): + """ Boolean if previous track command supported. """ + return bool(self.supported_media_commands & SUPPORT_PREVIOUS_TRACK) + + @property + def support_next_track(self): + """ Boolean if next track command supported. """ + return bool(self.supported_media_commands & SUPPORT_NEXT_TRACK) + + @property + def support_youtube(self): + """ Boolean if YouTube is supported. """ + return bool(self.supported_media_commands & SUPPORT_YOUTUBE) + + def volume_up(self): + """ volume_up media player. """ + if self.volume_level < 1: + self.set_volume_level(min(1, self.volume_level + .1)) + + def volume_down(self): + """ volume_down media player. """ + if self.volume_level > 0: + self.set_volume_level(max(0, self.volume_level - .1)) + + def media_play_pause(self): + """ media_play_pause media player. """ + if self.player_state == STATE_PLAYING: + self.media_pause() + else: + self.media_play() + + @property + def state_attributes(self): + """ Return the state attributes. """ + if self.state == STATE_OFF: + state_attr = {} + else: + state_attr = { + attr: getattr(self, prop) for attr, prop + in ATTR_TO_PROPERTY.items() if getattr(self, prop) + } + + device_attr = self.device_state_attributes + + if device_attr: + state_attr.update(device_attr) + + return state_attr diff --git a/homeassistant/components/media_player/demo.py b/homeassistant/components/media_player/demo.py index fdc17594b14..da2eaec69f1 100644 --- a/homeassistant/components/media_player/demo.py +++ b/homeassistant/components/media_player/demo.py @@ -5,121 +5,136 @@ homeassistant.components.media_player.demo Demo implementation of the media player. """ +from homeassistant.const import ( + STATE_PLAYING, STATE_PAUSED, STATE_OFF) + from homeassistant.components.media_player import ( - MediaPlayerDevice, STATE_NO_APP, ATTR_MEDIA_STATE, - ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_TITLE, ATTR_MEDIA_DURATION, - ATTR_MEDIA_VOLUME, MEDIA_STATE_PLAYING, MEDIA_STATE_STOPPED, - YOUTUBE_COVER_URL_FORMAT, ATTR_MEDIA_IS_VOLUME_MUTED) -from homeassistant.const import ATTR_ENTITY_PICTURE + MediaPlayerDevice, YOUTUBE_COVER_URL_FORMAT, MEDIA_TYPE_VIDEO, + SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_YOUTUBE, + SUPPORT_TURN_ON, SUPPORT_TURN_OFF) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the cast platform. """ add_devices([ - DemoMediaPlayer( + DemoYoutubePlayer( 'Living Room', 'eyU3bRy2x44', '♥♥ The Best Fireplace Video (3 hours)'), - DemoMediaPlayer('Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours') + DemoYoutubePlayer('Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours') ]) -class DemoMediaPlayer(MediaPlayerDevice): +YOUTUBE_PLAYER_SUPPORT = \ + SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ + SUPPORT_YOUTUBE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF + + +class DemoYoutubePlayer(MediaPlayerDevice): """ A Demo media player that only supports YouTube. """ + # We only implement the methods that we support + # pylint: disable=abstract-method def __init__(self, name, youtube_id=None, media_title=None): self._name = name - self.is_playing = youtube_id is not None + self._player_state = STATE_PLAYING self.youtube_id = youtube_id - self.media_title = media_title - self.volume = 1.0 - self.is_volume_muted = False + self._media_title = media_title + self._volume_level = 1.0 + self._volume_muted = False @property def should_poll(self): - """ No polling needed for a demo componentn. """ + """ We will push an update after each command. """ return False @property def name(self): - """ Returns the name of the device. """ + """ Name of the media player. """ return self._name @property def state(self): - """ Returns the state of the device. """ - return STATE_NO_APP if self.youtube_id is None else "YouTube" + """ State of the player. """ + return self._player_state @property - def state_attributes(self): - """ Returns the state attributes. """ - if self.youtube_id is None: - return + def volume_level(self): + """ Volume level of the media player (0..1). """ + return self._volume_level - state_attr = { - ATTR_MEDIA_CONTENT_ID: self.youtube_id, - ATTR_MEDIA_TITLE: self.media_title, - ATTR_MEDIA_DURATION: 100, - ATTR_MEDIA_VOLUME: self.volume, - ATTR_MEDIA_IS_VOLUME_MUTED: self.is_volume_muted, - ATTR_ENTITY_PICTURE: - YOUTUBE_COVER_URL_FORMAT.format(self.youtube_id) - } + @property + def is_volume_muted(self): + """ Boolean if volume is currently muted. """ + return self._volume_muted - if self.is_playing: - state_attr[ATTR_MEDIA_STATE] = MEDIA_STATE_PLAYING - else: - state_attr[ATTR_MEDIA_STATE] = MEDIA_STATE_STOPPED + @property + def media_content_id(self): + """ Content ID of current playing media. """ + return self.youtube_id - return state_attr + @property + def media_content_type(self): + """ Content type of current playing media. """ + return MEDIA_TYPE_VIDEO + + @property + def media_duration(self): + """ Duration of current playing media in seconds. """ + return 360 + + @property + def media_image_url(self): + """ Image url of current playing media. """ + return YOUTUBE_COVER_URL_FORMAT.format(self.youtube_id) + + @property + def media_title(self): + """ Title of current playing media. """ + return self._media_title + + @property + def app_name(self): + """ Current running app. """ + return "YouTube" + + @property + def supported_media_commands(self): + """ Flags of media commands that are supported. """ + return YOUTUBE_PLAYER_SUPPORT def turn_on(self): - """ turn_off media player. """ - self.youtube_id = "eyU3bRy2x44" - self.is_playing = False + """ turn the media player on. """ + self._player_state = STATE_PLAYING self.update_ha_state() def turn_off(self): - """ turn_off media player. """ - self.youtube_id = None - self.is_playing = False + """ turn the media player off. """ + self._player_state = STATE_OFF self.update_ha_state() - def volume_up(self): - """ volume_up media player. """ - if self.volume < 1: - self.volume += 0.1 - self.update_ha_state() - - def volume_down(self): - """ volume_down media player. """ - if self.volume > 0: - self.volume -= 0.1 - self.update_ha_state() - - def volume_mute(self, mute): - """ mute (true) or unmute (false) media player. """ - self.is_volume_muted = mute + def mute_volume(self, mute): + """ mute the volume. """ + self._volume_muted = mute self.update_ha_state() - def media_play_pause(self): - """ media_play_pause media player. """ - self.is_playing = not self.is_playing + def set_volume_level(self, volume): + """ set volume level, range 0..1. """ + self._volume_level = volume self.update_ha_state() def media_play(self): - """ media_play media player. """ - self.is_playing = True + """ Send play commmand. """ + self._player_state = STATE_PLAYING self.update_ha_state() def media_pause(self): - """ media_pause media player. """ - self.is_playing = False + """ Send pause command. """ + self._player_state = STATE_PAUSED self.update_ha_state() def play_youtube(self, media_id): """ Plays a YouTube media. """ self.youtube_id = media_id - self.media_title = 'Demo media title' - self.is_playing = True + self._media_title = 'some YouTube video' self.update_ha_state() diff --git a/homeassistant/const.py b/homeassistant/const.py index a4ea2651d28..639f8159338 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -40,6 +40,8 @@ STATE_NOT_HOME = 'not_home' STATE_UNKNOWN = "unknown" STATE_OPEN = 'open' STATE_CLOSED = 'closed' +STATE_PLAYING = 'playing' +STATE_PAUSED = 'paused' # #### STATE AND EVENT ATTRIBUTES #### # Contains current time for a TIME_CHANGED event @@ -104,7 +106,7 @@ SERVICE_MEDIA_PLAY_PAUSE = "media_play_pause" SERVICE_MEDIA_PLAY = "media_play" SERVICE_MEDIA_PAUSE = "media_pause" SERVICE_MEDIA_NEXT_TRACK = "media_next_track" -SERVICE_MEDIA_PREV_TRACK = "media_prev_track" +SERVICE_MEDIA_PREVIOUS_TRACK = "media_previous_track" # #### API / REMOTE #### SERVER_PORT = 8123 diff --git a/tests/test_component_media_player.py b/tests/test_component_media_player.py index fdde02e5594..b7f0b847e80 100644 --- a/tests/test_component_media_player.py +++ b/tests/test_component_media_player.py @@ -10,9 +10,10 @@ import unittest import homeassistant as ha from homeassistant.const import ( + STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE, - SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK, ATTR_ENTITY_ID) + SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, ATTR_ENTITY_ID) import homeassistant.components.media_player as media_player from helpers import mock_service @@ -29,7 +30,7 @@ class TestMediaPlayer(unittest.TestCase): self.hass = ha.HomeAssistant() self.test_entity = media_player.ENTITY_ID_FORMAT.format('living_room') - self.hass.states.set(self.test_entity, media_player.STATE_NO_APP) + self.hass.states.set(self.test_entity, STATE_OFF) self.test_entity2 = media_player.ENTITY_ID_FORMAT.format('bedroom') self.hass.states.set(self.test_entity2, "YouTube") @@ -56,7 +57,7 @@ class TestMediaPlayer(unittest.TestCase): SERVICE_MEDIA_PLAY: media_player.media_play, SERVICE_MEDIA_PAUSE: media_player.media_pause, SERVICE_MEDIA_NEXT_TRACK: media_player.media_next_track, - SERVICE_MEDIA_PREV_TRACK: media_player.media_prev_track + SERVICE_MEDIA_PREVIOUS_TRACK: media_player.media_previous_track } for service_name, service_method in services.items(): From 5eaf3d40ad6e4960da585c36455e3752b588d10d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 1 Jun 2015 23:42:19 -0700 Subject: [PATCH 16/95] Add demo music player --- .../components/media_player/__init__.py | 9 +- homeassistant/components/media_player/demo.py | 182 ++++++++++++++---- 2 files changed, 150 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 1d11ef2a37d..cacdc4c6891 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -37,6 +37,7 @@ ATTR_MEDIA_DURATION = 'media_duration' ATTR_MEDIA_TITLE = 'media_title' ATTR_MEDIA_ARTIST = 'media_artist' ATTR_MEDIA_ALBUM = 'media_album' +ATTR_MEDIA_TRACK = 'media_track' ATTR_MEDIA_SERIES_TITLE = 'media_series_title' ATTR_MEDIA_SEASON = 'media_season' ATTR_MEDIA_EPISODE = 'media_episode' @@ -82,6 +83,7 @@ ATTR_TO_PROPERTY = { ATTR_MEDIA_TITLE: 'media_title', ATTR_MEDIA_ARTIST: 'media_artist', ATTR_MEDIA_ALBUM: 'media_album', + ATTR_MEDIA_TRACK: 'media_track', ATTR_MEDIA_SERIES_TITLE: 'media_series_title', ATTR_MEDIA_SEASON: 'media_season', ATTR_MEDIA_EPISODE: 'media_episode', @@ -319,6 +321,11 @@ class MediaPlayerDevice(Entity): """ Album of current playing media. (Music track only) """ return None + @property + def media_track(self): + """ Track number of current playing media. (Music track only) """ + return None + @property def media_series_title(self): """ Series title of current playing media. (TV Show only)""" @@ -438,7 +445,7 @@ class MediaPlayerDevice(Entity): def media_play_pause(self): """ media_play_pause media player. """ - if self.player_state == STATE_PLAYING: + if self.state == STATE_PLAYING: self.media_pause() else: self.media_play() diff --git a/homeassistant/components/media_player/demo.py b/homeassistant/components/media_player/demo.py index da2eaec69f1..6e1d1a019bc 100644 --- a/homeassistant/components/media_player/demo.py +++ b/homeassistant/components/media_player/demo.py @@ -9,9 +9,11 @@ from homeassistant.const import ( STATE_PLAYING, STATE_PAUSED, STATE_OFF) from homeassistant.components.media_player import ( - MediaPlayerDevice, YOUTUBE_COVER_URL_FORMAT, MEDIA_TYPE_VIDEO, + MediaPlayerDevice, YOUTUBE_COVER_URL_FORMAT, + MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_YOUTUBE, - SUPPORT_TURN_ON, SUPPORT_TURN_OFF) + SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PREVIOUS_TRACK, + SUPPORT_NEXT_TRACK) # pylint: disable=unused-argument @@ -21,7 +23,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): DemoYoutubePlayer( 'Living Room', 'eyU3bRy2x44', '♥♥ The Best Fireplace Video (3 hours)'), - DemoYoutubePlayer('Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours') + DemoYoutubePlayer('Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours'), + DemoMusicPlayer(), ]) @@ -29,17 +32,17 @@ YOUTUBE_PLAYER_SUPPORT = \ SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_YOUTUBE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF +MUSIC_PLAYER_SUPPORT = \ + SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ + SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ + SUPPORT_NEXT_TRACK -class DemoYoutubePlayer(MediaPlayerDevice): - """ A Demo media player that only supports YouTube. """ - # We only implement the methods that we support - # pylint: disable=abstract-method - def __init__(self, name, youtube_id=None, media_title=None): +class AbstractDemoPlayer(MediaPlayerDevice): + """ Base class for demo media players. """ + def __init__(self, name): self._name = name self._player_state = STATE_PLAYING - self.youtube_id = youtube_id - self._media_title = media_title self._volume_level = 1.0 self._volume_muted = False @@ -68,6 +71,47 @@ class DemoYoutubePlayer(MediaPlayerDevice): """ Boolean if volume is currently muted. """ return self._volume_muted + def turn_on(self): + """ turn the media player on. """ + self._player_state = STATE_PLAYING + self.update_ha_state() + + def turn_off(self): + """ turn the media player off. """ + self._player_state = STATE_OFF + self.update_ha_state() + + def mute_volume(self, mute): + """ mute the volume. """ + self._volume_muted = mute + self.update_ha_state() + + def set_volume_level(self, volume): + """ set volume level, range 0..1. """ + self._volume_level = volume + self.update_ha_state() + + def media_play(self): + """ Send play commmand. """ + self._player_state = STATE_PLAYING + self.update_ha_state() + + def media_pause(self): + """ Send pause command. """ + self._player_state = STATE_PAUSED + self.update_ha_state() + + +class DemoYoutubePlayer(AbstractDemoPlayer): + """ A Demo media player that only supports YouTube. """ + # We only implement the methods that we support + # pylint: disable=abstract-method + + def __init__(self, name, youtube_id=None, media_title=None): + super().__init__(name) + self.youtube_id = youtube_id + self._media_title = media_title + @property def media_content_id(self): """ Content ID of current playing media. """ @@ -103,38 +147,96 @@ class DemoYoutubePlayer(MediaPlayerDevice): """ Flags of media commands that are supported. """ return YOUTUBE_PLAYER_SUPPORT - def turn_on(self): - """ turn the media player on. """ - self._player_state = STATE_PLAYING - self.update_ha_state() - - def turn_off(self): - """ turn the media player off. """ - self._player_state = STATE_OFF - self.update_ha_state() - - def mute_volume(self, mute): - """ mute the volume. """ - self._volume_muted = mute - self.update_ha_state() - - def set_volume_level(self, volume): - """ set volume level, range 0..1. """ - self._volume_level = volume - self.update_ha_state() - - def media_play(self): - """ Send play commmand. """ - self._player_state = STATE_PLAYING - self.update_ha_state() - - def media_pause(self): - """ Send pause command. """ - self._player_state = STATE_PAUSED - self.update_ha_state() - def play_youtube(self, media_id): """ Plays a YouTube media. """ self.youtube_id = media_id self._media_title = 'some YouTube video' self.update_ha_state() + + +class DemoMusicPlayer(AbstractDemoPlayer): + """ A Demo media player that only supports YouTube. """ + # We only implement the methods that we support + # pylint: disable=abstract-method + + tracks = [ + ('Technohead', 'I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)'), + ('Paul Elstak', 'Luv U More'), + ('Dune', 'Hardcore Vibes'), + ('Nakatomi', 'Children Of The Night'), + ('Party Animals', + 'Have You Ever Been Mellow? (Flamman & Abraxas Radio Mix)'), + ('Rob G.*', 'Ecstasy, You Got What I Need'), + ('Lipstick', "I'm A Raver"), + ('4 Tune Fairytales', 'My Little Fantasy (Radio Edit)'), + ('Prophet', "The Big Boys Don't Cry"), + ('Lovechild', 'All Out Of Love (DJ Weirdo & Sim Remix)'), + ('Stingray & Sonic Driver', 'Cold As Ice (El Bruto Remix)'), + ('Highlander', 'Hold Me Now (Bass-D & King Matthew Remix)'), + ('Juggernaut', 'Ruffneck Rules Da Artcore Scene (12" Edit)'), + ('Diss Reaction', 'Jiiieehaaaa '), + ('Flamman And Abraxas', 'Good To Go (Radio Mix)'), + ('Critical Mass', 'Dancing Together'), + ('Charly Lownoise & Mental Theo', 'Ultimate Sex Track (Bass-D & King Matthew Remix)'), + ] + + def __init__(self): + super().__init__('Walkman') + self._cur_track = 0 + + @property + def media_content_id(self): + """ Content ID of current playing media. """ + return 'bounzz-1' + + @property + def media_content_type(self): + """ Content type of current playing media. """ + return MEDIA_TYPE_MUSIC + + @property + def media_duration(self): + """ Duration of current playing media in seconds. """ + return 213 + + @property + def media_image_url(self): + """ Image url of current playing media. """ + return 'http://graph.facebook.com/107771475912710/picture' + + @property + def media_title(self): + """ Title of current playing media. """ + return self.tracks[self._cur_track][1] + + @property + def media_artist(self): + """ Artist of current playing media. (Music track only) """ + return self.tracks[self._cur_track][0] + + @property + def media_album(self): + """ Album of current playing media. (Music track only) """ + return "Bounzz" + + @property + def media_track(self): + """ Track number of current playing media. (Music track only) """ + return self._cur_track + 1 + + @property + def supported_media_commands(self): + """ Flags of media commands that are supported. """ + return MUSIC_PLAYER_SUPPORT + + def media_previous_track(self): + """ Send previous track command. """ + if self._cur_track > 0: + self._cur_track -= 1 + self.update_ha_state() + + def media_next_track(self): + """ Send next track command. """ + if self._cur_track < len(self.tracks)-1: + self._cur_track += 1 + self.update_ha_state() From bacff3de8dabd0f5c80058c67a2cb0c1df5a9252 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 1 Jun 2015 23:53:36 -0700 Subject: [PATCH 17/95] Add demo TV show player --- .../components/media_player/__init__.py | 2 +- homeassistant/components/media_player/demo.py | 84 ++++++++++++++++++- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index cacdc4c6891..25cc7cf4438 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -59,7 +59,7 @@ SUPPORT_YOUTUBE = 64 SUPPORT_TURN_ON = 128 SUPPORT_TURN_OFF = 256 -YOUTUBE_COVER_URL_FORMAT = 'http://img.youtube.com/vi/{}/1.jpg' +YOUTUBE_COVER_URL_FORMAT = 'https://img.youtube.com/vi/{}/1.jpg' SERVICE_TO_METHOD = { SERVICE_TURN_ON: 'turn_on', diff --git a/homeassistant/components/media_player/demo.py b/homeassistant/components/media_player/demo.py index 6e1d1a019bc..85b45ef534e 100644 --- a/homeassistant/components/media_player/demo.py +++ b/homeassistant/components/media_player/demo.py @@ -10,7 +10,7 @@ from homeassistant.const import ( from homeassistant.components.media_player import ( MediaPlayerDevice, YOUTUBE_COVER_URL_FORMAT, - MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, + MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_YOUTUBE, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK) @@ -24,7 +24,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): 'Living Room', 'eyU3bRy2x44', '♥♥ The Best Fireplace Video (3 hours)'), DemoYoutubePlayer('Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours'), - DemoMusicPlayer(), + DemoMusicPlayer(), DemoTVShowPlayer(), ]) @@ -37,6 +37,11 @@ MUSIC_PLAYER_SUPPORT = \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ SUPPORT_NEXT_TRACK +NETFLIX_PLAYER_SUPPORT = \ + SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ + SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ + SUPPORT_NEXT_TRACK + class AbstractDemoPlayer(MediaPlayerDevice): """ Base class for demo media players. """ @@ -202,7 +207,7 @@ class DemoMusicPlayer(AbstractDemoPlayer): @property def media_image_url(self): """ Image url of current playing media. """ - return 'http://graph.facebook.com/107771475912710/picture' + return 'https://graph.facebook.com/107771475912710/picture' @property def media_title(self): @@ -240,3 +245,76 @@ class DemoMusicPlayer(AbstractDemoPlayer): if self._cur_track < len(self.tracks)-1: self._cur_track += 1 self.update_ha_state() + + +class DemoTVShowPlayer(AbstractDemoPlayer): + """ A Demo media player that only supports YouTube. """ + # We only implement the methods that we support + # pylint: disable=abstract-method + + def __init__(self): + super().__init__('Lounge room') + self._cur_episode = 1 + self._episode_count = 13 + + @property + def media_content_id(self): + """ Content ID of current playing media. """ + return 'house-of-cards-1' + + @property + def media_content_type(self): + """ Content type of current playing media. """ + return MEDIA_TYPE_TVSHOW + + @property + def media_duration(self): + """ Duration of current playing media in seconds. """ + return 3600 + + @property + def media_image_url(self): + """ Image url of current playing media. """ + return 'https://graph.facebook.com/HouseofCards/picture' + + @property + def media_title(self): + """ Title of current playing media. """ + return 'Chapter {}'.format(self._cur_episode) + + @property + def media_series_title(self): + """ Series title of current playing media. (TV Show only)""" + return 'House of Cards' + + @property + def media_season(self): + """ Season of current playing media. (TV Show only) """ + return 1 + + @property + def media_episode(self): + """ Episode of current playing media. (TV Show only) """ + return self._cur_episode + + @property + def app_name(self): + """ Current running app. """ + return "Netflix" + + @property + def supported_media_commands(self): + """ Flags of media commands that are supported. """ + return NETFLIX_PLAYER_SUPPORT + + def media_previous_track(self): + """ Send previous track command. """ + if self._cur_episode > 1: + self._cur_episode -= 1 + self.update_ha_state() + + def media_next_track(self): + """ Send next track command. """ + if self._cur_episode < self._episode_count: + self._cur_episode += 1 + self.update_ha_state() From 1585a91aecd79a7b419af9d642924869c004286b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 1 Jun 2015 23:55:54 -0700 Subject: [PATCH 18/95] Add idle state to const.py --- homeassistant/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/const.py b/homeassistant/const.py index 639f8159338..a9bc5783087 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -42,6 +42,7 @@ STATE_OPEN = 'open' STATE_CLOSED = 'closed' STATE_PLAYING = 'playing' STATE_PAUSED = 'paused' +STATE_IDLE = 'idle' # #### STATE AND EVENT ATTRIBUTES #### # Contains current time for a TIME_CHANGED event From 458838e9c63688637bb765ce7d18142affb12148 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 2 Jun 2015 15:49:24 +0200 Subject: [PATCH 19/95] implement comments from #157 --- .../sensor/swiss_public_transport.py | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index f2d8b65a95f..a9eda2754bd 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -92,16 +92,17 @@ class SwissPublicTransportSensor(Entity): def update(self): """ Gets the latest data from opendata.ch and updates the states. """ times = self.data.update() - if times is not None: + try: self._state = ', '.join(times) + except TypeError: + pass # pylint: disable=too-few-public-methods class PublicTransportData(object): - """ Class for handling the data retrieval. """ + """ Class for handling the data retrieval. """ def __init__(self, journey): - self.times = ['n/a', 'n/a'] self.start = journey[0] self.destination = journey[1] @@ -117,14 +118,15 @@ class PublicTransportData(object): 'fields[]=connections/from/departureTimestamp/&' + 'fields[]=connections/') - try: - self.times.insert(0, dt_util.timestamp_to_short_time_str( - response.json()['connections'][0]['from'] - ['departureTimestamp'])) - self.times.insert(1, dt_util.timestamp_to_short_time_str( - response.json()['connections'][1]['from'] - ['departureTimestamp'])) - return self.times + connections = response.json()['connections'][:2] + try: + return [ + dt_util.datetime_to_short_time_str( + dt_util.as_local(dt_util.utc_from_timestamp( + item['from']['departureTimestamp'])) + ) + for item in connections + ] except KeyError: - return self.times + return ['n/a'] From ba8d429a9f7d4ce7d0912d252971911658278a0f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 31 May 2015 22:54:16 +0200 Subject: [PATCH 20/95] add timestamp to short time --- homeassistant/util/dt.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index fbe00c85527..f3c96368667 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -108,6 +108,14 @@ def str_to_datetime(dt_str): except ValueError: # If dt_str did not match our format return None +def timestamp_to_short_time_str(timestamp): + """ Converts a UNIX timestamp to a short time string format. + + @rtype: str + """ + return dt.datetime.fromtimestamp( + int(timestamp)).strftime(TIME_SHORT_STR_FORMAT) + def strip_microseconds(dattim): """ Returns a copy of dattime object but with microsecond set to 0. """ From c0c92a82e22717fb90008200c03ee7132bd1126c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 31 May 2015 22:55:02 +0200 Subject: [PATCH 21/95] add swiss public transport sensor --- .../sensor/swiss_public_transport.py | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 homeassistant/components/sensor/swiss_public_transport.py diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py new file mode 100644 index 00000000000..ee162a37164 --- /dev/null +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -0,0 +1,148 @@ +""" +homeassistant.components.sensor.swiss_public_transport +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The Swiss public transport sensor will give you the next two departure times +from a given location to another one. This sensor is limited to Switzerland. + +Configuration: + +To use the Swiss public transport sensor you will need to add something like +the following to your config/configuration.yaml + +sensor: + platform: swiss_public_transport + from: STATION_ID + to: STATION_ID + +Variables: + +from +*Required +Start station/stop of your trip. To search for the ID of the station, use the +an URL like this: http://transport.opendata.ch/v1/locations?query=Wankdorf +to query for the station. If the score is 100 ("score":"100" in the response), +it is a perfect match. + +to +*Required +Destination station/stop of the trip. Same procedure as for the start station. + +Details for the API : http://transport.opendata.ch +""" +import logging +from datetime import timedelta + +from homeassistant.util import Throttle +import homeassistant.util.dt as dt_util +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) +_RESOURCE = 'http://transport.opendata.ch/v1/' + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Get the Swiss public transport sensor. """ + + if None in (hass.config.latitude, hass.config.longitude): + _LOGGER.error("Latitude or longitude not set in Home Assistant config") + return False + + try: + # pylint: disable=unused-variable + from requests import get + + except ImportError: + _LOGGER.exception( + "Unable to import requests. " + "Did you maybe not install the 'Requests' package?") + + return None + + # journal contains [0] Station ID start, [1] Station ID destination + # [2] Station name start, and [3] Station name destination + journey = [] + journey.append(config.get('from', None)) + journey.append(config.get('to', 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) + journey.append(result.json()['stations'][0]['name']) + except KeyError: + _LOGGER.error( + "Unable to determine stations. " + "Check your settings and/or the availability of opendata.ch") + + return None + + dev = [] + data = PublicTransportData(journey) + dev.append(SwissPublicTransportSensor(data, journey)) + add_devices(dev) + + +# pylint: disable=too-few-public-methods +class SwissPublicTransportSensor(Entity): + """ Implements an Swiss public transport sensor. """ + + def __init__(self, data, journey): + self.data = data + self._name = journey[2] + '-' + journey[3] + self.update() + + @property + def name(self): + """ Returns the name. """ + return self._name + + @property + def state(self): + """ Returns the state of the device. """ + return self._state + + # pylint: disable=too-many-branches + def update(self): + """ Gets the latest data from opendata.ch and updates the states. """ + times = self.data.update() + if times is not None: + self._state = times[0] + ', ' + times[1] + + +# pylint: disable=too-few-public-methods +class PublicTransportData(object): + """ Class for handling the data retrieval. """ + + def __init__(self, journey): + self.times = ['n/a', 'n/a'] + self.start = journey[0] + self.destination = journey[1] + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """ Gets the latest data from opendata.ch. """ + + from requests import get + + response = get( + _RESOURCE + + 'connections?' + + 'from=' + self.start + '&' + + 'to=' + self.destination + '&' + + 'fields[]=connections/from/departureTimestamp/&' + + 'fields[]=connections/') + + try: + self.times.insert(0, dt_util.timestamp_to_short_time_str( + response.json()['connections'][0]['from'] + ['departureTimestamp'])) + self.times.insert(1, dt_util.timestamp_to_short_time_str( + response.json()['connections'][1]['from'] + ['departureTimestamp'])) + return self.times + + except KeyError: + return self.times \ No newline at end of file From 45d67176c5fe7b0b005aece2e35b4fcba41978b5 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 31 May 2015 22:55:47 +0200 Subject: [PATCH 22/95] add newline at the end --- homeassistant/components/sensor/swiss_public_transport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index ee162a37164..bd12cb84cf9 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -145,4 +145,4 @@ class PublicTransportData(object): return self.times except KeyError: - return self.times \ No newline at end of file + return self.times From a70c32da3c3caab03b0b5381bbba10bcd92d3bc3 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 31 May 2015 23:25:38 +0200 Subject: [PATCH 23/95] add newline --- homeassistant/util/dt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index f3c96368667..dd9d06ef674 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -108,6 +108,7 @@ def str_to_datetime(dt_str): except ValueError: # If dt_str did not match our format return None + def timestamp_to_short_time_str(timestamp): """ Converts a UNIX timestamp to a short time string format. From a3cdb667ba75e4a5ec8aa4f2574ec6d48ac78c51 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 31 May 2015 23:31:24 +0200 Subject: [PATCH 24/95] add swiss_public_transport.py --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 00e10da8110..2050ac8d606 100644 --- a/.coveragerc +++ b/.coveragerc @@ -41,6 +41,7 @@ omit = homeassistant/components/sensor/mysensors.py homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/sabnzbd.py + homeassistant/components/sensor/swiss_public_transport.py homeassistant/components/sensor/systemmonitor.py homeassistant/components/sensor/time_date.py homeassistant/components/sensor/transmission.py From a50ed4695035ed07bf1fac4a5562015abfb246b1 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 1 Jun 2015 13:37:57 +0200 Subject: [PATCH 25/95] add shortcut --- homeassistant/components/sensor/swiss_public_transport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index bd12cb84cf9..c48194d6eee 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -109,7 +109,7 @@ class SwissPublicTransportSensor(Entity): """ Gets the latest data from opendata.ch and updates the states. """ times = self.data.update() if times is not None: - self._state = times[0] + ', ' + times[1] + self._state = ', '.join(times) # pylint: disable=too-few-public-methods From 57e2a8a0c9f01dffcb38c1a2c6496bbdc7182d6c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 1 Jun 2015 13:40:21 +0200 Subject: [PATCH 26/95] remove lat/long check --- homeassistant/components/sensor/swiss_public_transport.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index c48194d6eee..4a4ac58eb3d 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -47,10 +47,6 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) def setup_platform(hass, config, add_devices, discovery_info=None): """ Get the Swiss public transport sensor. """ - if None in (hass.config.latitude, hass.config.longitude): - _LOGGER.error("Latitude or longitude not set in Home Assistant config") - return False - try: # pylint: disable=unused-variable from requests import get From 713a03ad89c2be5763a91d48131a9233a4ba7455 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 1 Jun 2015 13:47:32 +0200 Subject: [PATCH 27/95] update depenency handling (requests) --- .../components/sensor/swiss_public_transport.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index 4a4ac58eb3d..41fb249316e 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -32,6 +32,7 @@ Details for the API : http://transport.opendata.ch """ import logging from datetime import timedelta +from requests import get from homeassistant.util import Throttle import homeassistant.util.dt as dt_util @@ -47,17 +48,6 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) def setup_platform(hass, config, add_devices, discovery_info=None): """ Get the Swiss public transport sensor. """ - try: - # pylint: disable=unused-variable - from requests import get - - except ImportError: - _LOGGER.exception( - "Unable to import requests. " - "Did you maybe not install the 'Requests' package?") - - return None - # journal contains [0] Station ID start, [1] Station ID destination # [2] Station name start, and [3] Station name destination journey = [] @@ -121,8 +111,6 @@ class PublicTransportData(object): def update(self): """ Gets the latest data from opendata.ch. """ - from requests import get - response = get( _RESOURCE + 'connections?' + From 284dbff2d554088318384a33e521c67dca259402 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 1 Jun 2015 13:49:08 +0200 Subject: [PATCH 28/95] use string formatting --- homeassistant/components/sensor/swiss_public_transport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index 41fb249316e..2ec16f72452 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -77,7 +77,7 @@ class SwissPublicTransportSensor(Entity): def __init__(self, data, journey): self.data = data - self._name = journey[2] + '-' + journey[3] + self._name = '{}-{}'.format(journey[2], journey[3]) self.update() @property From 2317114b049a7a43f56bb069d05a1f5228b7319a Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 1 Jun 2015 13:49:46 +0200 Subject: [PATCH 29/95] switch from error to execption for logger --- homeassistant/components/sensor/swiss_public_transport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index 2ec16f72452..6e56a7bc458 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -59,7 +59,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): result = get(_RESOURCE + 'locations?query=%s' % location) journey.append(result.json()['stations'][0]['name']) except KeyError: - _LOGGER.error( + _LOGGER.exception( "Unable to determine stations. " "Check your settings and/or the availability of opendata.ch") From 84c7149f0f9d960ef47915af8c7910474163711a Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 1 Jun 2015 13:51:00 +0200 Subject: [PATCH 30/95] update journey --- homeassistant/components/sensor/swiss_public_transport.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index 6e56a7bc458..f2d8b65a95f 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -50,9 +50,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # journal contains [0] Station ID start, [1] Station ID destination # [2] Station name start, and [3] Station name destination - journey = [] - journey.append(config.get('from', None)) - journey.append(config.get('to', None)) + journey = [config.get('from'), config.get('to')] try: for location in [config.get('from', None), config.get('to', None)]: # transport.opendata.ch doesn't play nice with requests.Session From 4292c1f207aa0f75aff25a61c21829b01e500621 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 1 Jun 2015 14:02:27 +0200 Subject: [PATCH 31/95] Revert "add timestamp to short time" This reverts commit 1be50d83dcecb8a5609df3f0b2bd78fad1087f6e. Conflicts: homeassistant/util/dt.py --- homeassistant/util/dt.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index dd9d06ef674..fbe00c85527 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -109,15 +109,6 @@ def str_to_datetime(dt_str): return None -def timestamp_to_short_time_str(timestamp): - """ Converts a UNIX timestamp to a short time string format. - - @rtype: str - """ - return dt.datetime.fromtimestamp( - int(timestamp)).strftime(TIME_SHORT_STR_FORMAT) - - def strip_microseconds(dattim): """ Returns a copy of dattime object but with microsecond set to 0. """ return dattim.replace(microsecond=0) From 512c4629b66dc3931c3ba36979f6ca6d08885bfe Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 2 Jun 2015 15:49:24 +0200 Subject: [PATCH 32/95] implement comments from #157 --- .../sensor/swiss_public_transport.py | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index f2d8b65a95f..a9eda2754bd 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -92,16 +92,17 @@ class SwissPublicTransportSensor(Entity): def update(self): """ Gets the latest data from opendata.ch and updates the states. """ times = self.data.update() - if times is not None: + try: self._state = ', '.join(times) + except TypeError: + pass # pylint: disable=too-few-public-methods class PublicTransportData(object): - """ Class for handling the data retrieval. """ + """ Class for handling the data retrieval. """ def __init__(self, journey): - self.times = ['n/a', 'n/a'] self.start = journey[0] self.destination = journey[1] @@ -117,14 +118,15 @@ class PublicTransportData(object): 'fields[]=connections/from/departureTimestamp/&' + 'fields[]=connections/') - try: - self.times.insert(0, dt_util.timestamp_to_short_time_str( - response.json()['connections'][0]['from'] - ['departureTimestamp'])) - self.times.insert(1, dt_util.timestamp_to_short_time_str( - response.json()['connections'][1]['from'] - ['departureTimestamp'])) - return self.times + connections = response.json()['connections'][:2] + try: + return [ + dt_util.datetime_to_short_time_str( + dt_util.as_local(dt_util.utc_from_timestamp( + item['from']['departureTimestamp'])) + ) + for item in connections + ] except KeyError: - return self.times + return ['n/a'] From 84e81a9e4e0c7555d6319bd9e451df6bd1674f7d Mon Sep 17 00:00:00 2001 From: Finbarr Brady Date: Tue, 2 Jun 2015 16:00:29 +0100 Subject: [PATCH 33/95] Renamed hikvision component. Added module from pip --- .../components/switch/hikvisioncam.py | 138 ++++++++++++++++++ requirements.txt | 3 + 2 files changed, 141 insertions(+) create mode 100644 homeassistant/components/switch/hikvisioncam.py diff --git a/homeassistant/components/switch/hikvisioncam.py b/homeassistant/components/switch/hikvisioncam.py new file mode 100644 index 00000000000..3db11b96b18 --- /dev/null +++ b/homeassistant/components/switch/hikvisioncam.py @@ -0,0 +1,138 @@ +""" +homeassistant.components.switch.hikvision +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Support turning on/off motion detection on Hikvision cameras. + +Note: Currently works using default https port only. + +CGI API Guide: +http://bit.ly/1RuyUuF + +Configuration: + +To use the Hikvision motion detection +switch you will need to add something like the +following to your config/configuration.yaml + +switch: + platform: hikvision + name: Hikvision Cam 1 Motion Detection + host: 192.168.1.26 + username: YOUR_USERNAME + password: YOUR_PASSWORD + +Variables: + +host +*Required +This is the IP address of your Hikvision camera. Example: 192.168.1.32 + +username +*Required +Your Hikvision camera username + +password +*Required +Your Hikvision camera username + +name +*Optional +The name to use when displaying this switch instance. + +""" +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +import logging + +try: + import hikvision.api + from hikvision.error import HikvisionError, MissingParamError +except ImportError: + hikvision.api = None + +_LOGGING = logging.getLogger(__name__) + +# pylint: disable=too-many-arguments +# pylint: disable=too-many-instance-attributes + + +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Setup Hikvision Camera config. """ + + host = config.get(CONF_HOST, None) + port = config.get('port', "80") + name = config.get('name', "Hikvision Camera Motion Detection") + username = config.get(CONF_USERNAME, "admin") + password = config.get(CONF_PASSWORD, "12345") + + if hikvision.api is None: + _LOGGING.error(( + "Failed to import hikvision. Did you maybe not install the " + "'hikvision' dependency?")) + + return False + + try: + hikvision_cam = hikvision.api.CreateDevice( + host, port=port, username=username, password=password) + except MissingParamError as param_err: + _LOGGING.error("Missing required param: %s", param_err) + return False + except HikvisionError as conn_err: + _LOGGING.error("Unable to connect: %s", conn_err) + return False + + add_devices_callback([ + HikvisionMotionSwitch(name, hikvision_cam) + ]) + + +class HikvisionMotionSwitch(ToggleEntity): + + """ Provides a switch to toggle on/off motion detection. """ + + def __init__(self, name, hikvision_cam): + self._name = name + self._hikvision_cam = hikvision_cam + self._state = STATE_OFF + + @property + def should_poll(self): + """ Poll for status regularly. """ + return True + + @property + def name(self): + """ Returns the name of the device if any. """ + return self._name + + @property + def state(self): + """ Returns the state of the device if any. """ + return self._state + + @property + def is_on(self): + """ True if device is on. """ + return self._state == STATE_ON + + def turn_on(self, **kwargs): + """ Turn the device on. """ + + _LOGGING.info("Turning on Motion Detection ") + self._hikvision_cam.enable_motion_detection() + + def turn_off(self, **kwargs): + """ Turn the device off. """ + + _LOGGING.info("Turning off Motion Detection ") + self._hikvision_cam.disable_motion_detection() + + def update(self): + """ Update Motion Detection state """ + enabled = self._hikvision_cam.hik_camera.is_motion_detection_enabled() + _LOGGING.info('enabled: %s', enabled) + + self._state = STATE_ON if enabled else STATE_OFF diff --git a/requirements.txt b/requirements.txt index 5dc0e7cc18b..22bf213c21c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -58,3 +58,6 @@ sleekxmpp>=1.3.1 # Blockchain (sensor.bitcoin) blockchain>=1.1.2 + +# Hikvision (switch.hikvision) +hikvision>=0.3 From bae530b30d9e63a65d77fc7f289a8dbb54bdc9d7 Mon Sep 17 00:00:00 2001 From: Finbarr Brady Date: Tue, 2 Jun 2015 16:03:29 +0100 Subject: [PATCH 34/95] Delete original file --- homeassistant/components/switch/hikvision.py | 221 ------------------- 1 file changed, 221 deletions(-) delete mode 100644 homeassistant/components/switch/hikvision.py diff --git a/homeassistant/components/switch/hikvision.py b/homeassistant/components/switch/hikvision.py deleted file mode 100644 index 31684171e1c..00000000000 --- a/homeassistant/components/switch/hikvision.py +++ /dev/null @@ -1,221 +0,0 @@ -""" -homeassistant.components.switch.hikvision -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Support turning on/off motion detection on Hikvision cameras. - -Note: Currently works using default https port only. - -CGI API Guide: -http://bit.ly/1RuyUuF - -Configuration: - -To use the Hikvision motion detection -switch you will need to add something like the -following to your config/configuration.yaml - -switch: - platform: hikvision - name: Hikvision Cam 1 Motion Detection - host: 192.168.1.26 - username: YOUR_USERNAME - password: YOUR_PASSWORD - -Variables: - -host -*Required -This is the IP address of your Hikvision camera. Example: 192.168.1.32 - -username -*Required -Your Hikvision camera username - -password -*Required -Your Hikvision camera username - -name -*Optional -The name to use when displaying this switch instance. - -""" -from homeassistant.helpers.entity import ToggleEntity -from homeassistant.const import STATE_ON, STATE_OFF -from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD -import logging -import requests -from requests.auth import HTTPBasicAuth -from xml.etree import ElementTree - -_LOGGING = logging.getLogger(__name__) - -# pylint: disable=too-many-arguments -# pylint: disable=too-many-instance-attributes - - -def setup_platform(hass, config, add_devices_callback, discovery_info=None): - """ Setup Hikvision Camera config. """ - - host = config.get(CONF_HOST, None) - port = config.get('port', "80") - name = config.get('name', "Hikvision Camera Motion Detection") - username = config.get(CONF_USERNAME, "admin") - password = config.get(CONF_PASSWORD, "12345") - channel_id = config.get('channel_id', "1") - xml_namespace = config.get( - 'xml_namespace', "http://www.hikvision.com/ver10/XMLSchema") - - # Required to parse and change xml with the host camera - _LOGGING.info('ElementTree.register_namespace: %s', xml_namespace) - ElementTree.register_namespace("", xml_namespace) - - if not host: - _LOGGING.error('Missing config variable-host') - return False - - add_devices_callback([ - HikvisionMotionSwitch( - name, host, port, username, password, channel_id, xml_namespace) - ]) - - -class HikvisionMotionSwitch(ToggleEntity): - - """ Provides a switch to toggle on/off motion detection. """ - - def __init__(self, name, host, port, username, - password, channel_id, xml_namespace): - self._name = name - self._username = username - self._password = password - self._channel_id = channel_id - self._host = host - self._port = port - self._xml_namespace = xml_namespace - self._state = STATE_OFF - self.url = 'https://%s/MotionDetection/%s/' % ( - self._host, self._channel_id) - self.xml_motion_detection_off = None - self.xml_motion_detection_on = None - self.update() - - @property - def should_poll(self): - """ Poll for status regularly. """ - return True - - @property - def name(self): - """ Returns the name of the device if any. """ - return self._name - - @property - def state(self): - """ Returns the state of the device if any. """ - return self._state - - @property - def is_on(self): - """ True if device is on. """ - return self._state == STATE_ON - - def turn_on(self, **kwargs): - """ Turn the device on. """ - - _LOGGING.info("Turning on Motion Detection ") - self.toggle_motion_detection() - - def turn_off(self, **kwargs): - """ Turn the device off. """ - - _LOGGING.info("Turning off Motion Detection ") - self.toggle_motion_detection() - - def toggle_motion_detection(self): - """ - # See http://bit.ly/1KtcW7b - """ - - if self._state == STATE_ON: - xml = self.xml_motion_detection_off - self._state = STATE_OFF - else: - self._state = STATE_ON - xml = self.xml_motion_detection_on - - _LOGGING.info('xml:') - _LOGGING.info("%s", xml) - - response = requests.put(self.url, auth=HTTPBasicAuth( - self._username, self._password), verify=False, data=xml) - _LOGGING.info('Response: %s', response.text) - - if response.status_code != 200: - _LOGGING.error("There was an error connecting to %s", self.url) - _LOGGING.error("status_code %s", response.esponsestatus_code) - return - - try: - tree = ElementTree.fromstring(response.content) - find_result = tree.findall( - './/{%s}statusString' % self._xml_namespace) - if len(find_result) == 0: - _LOGGING.error("Problem getting motion detection status") - self.update() - return - - if find_result[0].text.strip() == 'OK': - _LOGGING.info('Updated successfully') - - except AttributeError as attib_err: - _LOGGING.error( - 'There was a problem parsing the response: %s', attib_err) - self.update() - return - - def update(self): - """ - # See http://bit.ly/1KtcW7b - """ - _LOGGING.info('url: %s', self.url) - - response = requests.get(self.url, auth=HTTPBasicAuth( - self._username, self._password), verify=False) - _LOGGING.info('Response: %s', response.text) - - if response.status_code != 200: - _LOGGING.error("There was an error connecting to %s", self.url) - _LOGGING.error("status_code %s", response.status_code) - return - - try: - tree = ElementTree.fromstring(response.content) - find_result = tree.findall('.//{%s}enabled' % self._xml_namespace) - if len(find_result) == 0: - _LOGGING.error("Problem getting motion detection status") - return - - result = find_result[0].text.strip() - _LOGGING.info( - 'Current motion detection state? enabled: %s', result) - - if result == 'true': - self._state = STATE_ON - # Save this for future switch off - find_result[0].text = 'false' - self.xml_motion_detection_off = ElementTree.tostring( - tree, encoding='unicode') - else: - self._state = STATE_OFF - # Save this for future switch on - find_result[0].text = 'true' - self.xml_motion_detection_on = ElementTree.tostring( - tree, encoding='unicode') - - except AttributeError as attib_err: - _LOGGING.error( - 'There was a problem parsing ' - 'camera motion detection state: %s', attib_err) - return From bbf8420d804513cd974c760cec27d79686edaab2 Mon Sep 17 00:00:00 2001 From: Finbarr Brady Date: Tue, 2 Jun 2015 16:04:45 +0100 Subject: [PATCH 35/95] Updated component id --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 22bf213c21c..9b6885ef19c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -59,5 +59,5 @@ sleekxmpp>=1.3.1 # Blockchain (sensor.bitcoin) blockchain>=1.1.2 -# Hikvision (switch.hikvision) +# Hikvision (switch.hikvisioncam) hikvision>=0.3 From ebc0ffd879cabda557420e15323cf2f84f51ece2 Mon Sep 17 00:00:00 2001 From: Finbarr Brady Date: Tue, 2 Jun 2015 17:14:12 +0100 Subject: [PATCH 36/95] https false --- homeassistant/components/switch/hikvisioncam.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/switch/hikvisioncam.py b/homeassistant/components/switch/hikvisioncam.py index 3db11b96b18..9cec027c58a 100644 --- a/homeassistant/components/switch/hikvisioncam.py +++ b/homeassistant/components/switch/hikvisioncam.py @@ -76,7 +76,8 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): try: hikvision_cam = hikvision.api.CreateDevice( - host, port=port, username=username, password=password) + host, port=port, username=username, + password=password, is_https=False) except MissingParamError as param_err: _LOGGING.error("Missing required param: %s", param_err) return False From 3701a82de1d6f6625ad1d96f77bc93f68ff9d2ed Mon Sep 17 00:00:00 2001 From: Finbarr Brady Date: Tue, 2 Jun 2015 17:17:36 +0100 Subject: [PATCH 37/95] Fix typo --- homeassistant/components/switch/hikvisioncam.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/switch/hikvisioncam.py b/homeassistant/components/switch/hikvisioncam.py index 9cec027c58a..3c85cfce640 100644 --- a/homeassistant/components/switch/hikvisioncam.py +++ b/homeassistant/components/switch/hikvisioncam.py @@ -133,7 +133,7 @@ class HikvisionMotionSwitch(ToggleEntity): def update(self): """ Update Motion Detection state """ - enabled = self._hikvision_cam.hik_camera.is_motion_detection_enabled() + enabled = self._hikvision_cam.is_motion_detection_enabled() _LOGGING.info('enabled: %s', enabled) self._state = STATE_ON if enabled else STATE_OFF From b11320a5fbecfa8f49b1568b2b4abce2274a0d6e Mon Sep 17 00:00:00 2001 From: Finbarr Brady Date: Tue, 2 Jun 2015 17:18:41 +0100 Subject: [PATCH 38/95] Bump version --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9b6885ef19c..8356ca80824 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,4 +60,4 @@ sleekxmpp>=1.3.1 blockchain>=1.1.2 # Hikvision (switch.hikvisioncam) -hikvision>=0.3 +hikvision>=0.4 From b5ee05a13e5f26cba5ee6e05161ae18c03e3e570 Mon Sep 17 00:00:00 2001 From: Finbarr Brady Date: Tue, 2 Jun 2015 17:33:05 +0100 Subject: [PATCH 39/95] docs --- homeassistant/components/switch/hikvisioncam.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/switch/hikvisioncam.py b/homeassistant/components/switch/hikvisioncam.py index 3c85cfce640..af9c4c6ad40 100644 --- a/homeassistant/components/switch/hikvisioncam.py +++ b/homeassistant/components/switch/hikvisioncam.py @@ -16,7 +16,7 @@ switch you will need to add something like the following to your config/configuration.yaml switch: - platform: hikvision + platform: hikvisioncam name: Hikvision Cam 1 Motion Detection host: 192.168.1.26 username: YOUR_USERNAME From c75c123d370db7d37aa47f4857e1c5f0f258e18c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 2 Jun 2015 22:14:30 +0200 Subject: [PATCH 40/95] fix first sentence --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 48523c1a4fe..a9c467f08c2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Adding support for a new device -For help on building your component, please see the See the [developer documentation on home-assistant.io](https://home-assistant.io/developers/). +For help on building your component, please see the [developer documentation on home-assistant.io](https://home-assistant.io/developers/). After you finish adding support for your device: From a695da88df07c5387bc72229ce939263cb4f3eca Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 2 Jun 2015 22:26:26 +0200 Subject: [PATCH 41/95] separate links --- CONTRIBUTING.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9c467f08c2..87124c66cb1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,7 @@ # Adding support for a new device -For help on building your component, please see the [developer documentation on home-assistant.io](https://home-assistant.io/developers/). +For help on building your component, please see the [developer documentation](https://home-assistant.io/developers/) on [home-assistant.io](https://home-assistant.io/) . +. After you finish adding support for your device: From 3d1743d91dc061b29f704bccbc1eed10b648e8ca Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 2 Jun 2015 22:37:08 +0200 Subject: [PATCH 42/95] fix period --- CONTRIBUTING.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 87124c66cb1..22ea990aca2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,6 @@ # Adding support for a new device -For help on building your component, please see the [developer documentation](https://home-assistant.io/developers/) on [home-assistant.io](https://home-assistant.io/) . -. +For help on building your component, please see the [developer documentation](https://home-assistant.io/developers/) on [home-assistant.io](https://home-assistant.io/). After you finish adding support for your device: From 16be76a0385afdb651d28523034878bd9e62941f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 2 Jun 2015 22:41:57 +0200 Subject: [PATCH 43/95] add missing bracket --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fd234122c03..7b8480b3d3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -38,7 +38,7 @@ python-nest>=2.3.1 # Z-Wave (*.zwave) pydispatcher>=2.0.5 -# ISY994 bindings (*.isy994 +# ISY994 bindings (*.isy994) PyISY>=1.0.2 # PSutil (sensor.systemmonitor) From 718ff6b3269f170b31f169bfd10f668e21d24bdf Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 2 Jun 2015 23:02:34 +0200 Subject: [PATCH 44/95] add notifications and mpd --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1648b526332..dcdcfa4fe3f 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ Home Assistant is a home automation platform running on Python 3. The goal of Ho It offers the following functionality through built-in components: - * Track if devices are home by monitoring connected devices to a wireless router (supporting [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index)) + * Track if devices are home by monitoring connected devices to a wireless router (supporting [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), and [DD-WRT](http://www.dd-wrt.com/site/index)) * Track and control [Philips Hue](http://meethue.com) lights * Track and control [WeMo switches](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) - * Track and control [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast) - * Track running services by monitoring `ps` output + * Track and control [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast) and [Music Player Daemon](http://www.musicpd.org/) + * Track running system services and monitoring your system stats (Memory, disk usage, and more) * Track and control [Tellstick devices and sensors](http://www.telldus.se/products/tellstick) * Control low-cost 433 MHz remote control wall-socket devices (https://github.com/r10r/rcswitch-pi) and other switches that can be turned on/off with shell commands * Turn on the lights when people get home after sun set @@ -19,6 +19,7 @@ It offers the following functionality through built-in components: * Offers web interface to monitor and control Home Assistant * Offers a [REST API](https://home-assistant.io/developers/api.html) for easy integration with other projects * [Ability to have multiple instances of Home Assistant work together](https://home-assistant.io/developers/architecture.html) + * Allow sending notifications using [Instapush](https://instapush.im), [Notify My Android (NMA)](http://www.notifymyandroid.com/), [PushBullet](https://www.pushbullet.com/), [PushOver](https://pushover.net/), and [Jabber (XMPP)](http://xmpp.org) Home Assistant also includes functionality for controlling HTPCs: From bf1a6f589959e78a8ff2eaca39c4a33f1eaf34a9 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 2 Jun 2015 23:10:12 +0200 Subject: [PATCH 45/95] add other sensors --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index dcdcfa4fe3f..a57d8aa03ad 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ It offers the following functionality through built-in components: * Offers a [REST API](https://home-assistant.io/developers/api.html) for easy integration with other projects * [Ability to have multiple instances of Home Assistant work together](https://home-assistant.io/developers/architecture.html) * Allow sending notifications using [Instapush](https://instapush.im), [Notify My Android (NMA)](http://www.notifymyandroid.com/), [PushBullet](https://www.pushbullet.com/), [PushOver](https://pushover.net/), and [Jabber (XMPP)](http://xmpp.org) + * Allow to display details about a running [Transmission](http://www.transmissionbt.com/) client, the [Bitcoin](https://bitcoin.org) network, local meteorological data from [OpenWeatherMap](http://openweathermap.org/), the time, the date, and the downloads from [SABnzbd](http://sabnzbd.org) Home Assistant also includes functionality for controlling HTPCs: From 73139a76a850f0e3ff92a64bfc552b96445f8ab6 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 2 Jun 2015 23:22:27 +0200 Subject: [PATCH 46/95] add ISY994 and modbus --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a57d8aa03ad..88e7a01cc65 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,10 @@ Home Assistant is a home automation platform running on Python 3. The goal of Ho It offers the following functionality through built-in components: * Track if devices are home by monitoring connected devices to a wireless router (supporting [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), and [DD-WRT](http://www.dd-wrt.com/site/index)) - * Track and control [Philips Hue](http://meethue.com) lights - * Track and control [WeMo switches](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) + * Track and control [Philips Hue](http://meethue.com) lights, [WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) switches, and [Tellstick](http://www.telldus.se/products/tellstick) devices and sensors. * Track and control [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast) and [Music Player Daemon](http://www.musicpd.org/) + * Support for [ISY994](https://www.universal-devices.com/residential/isy994i-series/) (Insteon and X10 devices) and [Modbus](http://www.modbus.org/) * Track running system services and monitoring your system stats (Memory, disk usage, and more) - * Track and control [Tellstick devices and sensors](http://www.telldus.se/products/tellstick) * Control low-cost 433 MHz remote control wall-socket devices (https://github.com/r10r/rcswitch-pi) and other switches that can be turned on/off with shell commands * Turn on the lights when people get home after sun set * Turn on lights slowly during sun set to compensate for light loss From a6b4d539a6bcda3cb901b8ba42c140c8ad6bd9ff Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 2 Jun 2015 23:28:59 +0200 Subject: [PATCH 47/95] add zwave and nest --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 88e7a01cc65..4aadfd32335 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@ Home Assistant is a home automation platform running on Python 3. The goal of Ho It offers the following functionality through built-in components: * Track if devices are home by monitoring connected devices to a wireless router (supporting [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), and [DD-WRT](http://www.dd-wrt.com/site/index)) - * Track and control [Philips Hue](http://meethue.com) lights, [WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) switches, and [Tellstick](http://www.telldus.se/products/tellstick) devices and sensors. + * Track and control [Philips Hue](http://meethue.com) lights, [WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) switches, and [Tellstick](http://www.telldus.se/products/tellstick) devices and sensors * Track and control [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast) and [Music Player Daemon](http://www.musicpd.org/) - * Support for [ISY994](https://www.universal-devices.com/residential/isy994i-series/) (Insteon and X10 devices) and [Modbus](http://www.modbus.org/) + * Support for [ISY994](https://www.universal-devices.com/residential/isy994i-series/) (Insteon and X10 devices), [Z-Wave](http://www.z-wave.com/), [Nest Thermostats](https://nest.com/), and [Modbus](http://www.modbus.org/) * Track running system services and monitoring your system stats (Memory, disk usage, and more) * Control low-cost 433 MHz remote control wall-socket devices (https://github.com/r10r/rcswitch-pi) and other switches that can be turned on/off with shell commands * Turn on the lights when people get home after sun set From 73dab5a398c118fa14a43a6e311af579555fc151 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 2 Jun 2015 21:31:50 -0700 Subject: [PATCH 48/95] Make customize parsing more robust --- homeassistant/bootstrap.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 787e0f80562..ee2ee54df79 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -235,8 +235,13 @@ def process_ha_core_config(hass, config): set_time_zone(config.get(CONF_TIME_ZONE)) - for entity_id, attrs in config.get(CONF_CUSTOMIZE, {}).items(): - Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values()) + customize = config.get(CONF_CUSTOMIZE) + + if isinstance(customize, dict): + for entity_id, attrs in config.get(CONF_CUSTOMIZE, {}).items(): + if not isinstance(attrs, dict): + continue + Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values()) if CONF_TEMPERATURE_UNIT in config: unit = config[CONF_TEMPERATURE_UNIT] From 644a3058de3728a8229ffae4970dfe26c9dc4080 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 2 Jun 2015 21:39:33 -0700 Subject: [PATCH 49/95] Fix device tracker deadlock after exception in scanner --- .../components/device_tracker/__init__.py | 110 +++++++++--------- tests/test_component_device_tracker.py | 5 +- 2 files changed, 58 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 5cefed5c5c8..611136aac5b 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -17,7 +17,7 @@ import homeassistant.util.dt as dt_util from homeassistant.const import ( STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME, - CONF_PLATFORM) + CONF_PLATFORM, DEVICE_DEFAULT_NAME) from homeassistant.components import group DOMAIN = "device_tracker" @@ -169,66 +169,28 @@ class DeviceTracker(object): if not self.lock.acquire(False): return - found_devices = set(dev.upper() for dev in - self.device_scanner.scan_devices()) + try: + found_devices = set(dev.upper() for dev in + self.device_scanner.scan_devices()) - for device in self.tracked: - is_home = device in found_devices + for device in self.tracked: + is_home = device in found_devices - self._update_state(now, device, is_home) + self._update_state(now, device, is_home) - if is_home: - found_devices.remove(device) + if is_home: + found_devices.remove(device) - # Did we find any devices that we didn't know about yet? - new_devices = found_devices - self.untracked_devices + # Did we find any devices that we didn't know about yet? + new_devices = found_devices - self.untracked_devices - if new_devices: - if not self.track_new_devices: - self.untracked_devices.update(new_devices) + if new_devices: + if not self.track_new_devices: + self.untracked_devices.update(new_devices) - # Write new devices to known devices file - if not self.invalid_known_devices_file: - - known_dev_path = self.hass.config.path(KNOWN_DEVICES_FILE) - - try: - # If file does not exist we will write the header too - is_new_file = not os.path.isfile(known_dev_path) - - with open(known_dev_path, 'a') as outp: - _LOGGER.info( - "Found %d new devices, updating %s", - len(new_devices), known_dev_path) - - writer = csv.writer(outp) - - if is_new_file: - writer.writerow(( - "device", "name", "track", "picture")) - - for device in new_devices: - # See if the device scanner knows the name - # else defaults to unknown device - dname = self.device_scanner.get_device_name(device) - name = dname or "unknown device" - - track = 0 - if self.track_new_devices: - self._track_device(device, name) - track = 1 - - writer.writerow((device, name, track, "")) - - if self.track_new_devices: - self._generate_entity_ids(new_devices) - - except IOError: - _LOGGER.exception( - "Error updating %s with %d new devices", - known_dev_path, len(new_devices)) - - self.lock.release() + self._update_known_devices_file(new_devices) + finally: + self.lock.release() # pylint: disable=too-many-branches def _read_known_devices_file(self): @@ -309,6 +271,44 @@ class DeviceTracker(object): finally: self.lock.release() + def _update_known_devices_file(self, new_devices): + """ Add new devices to known devices file. """ + if not self.invalid_known_devices_file: + known_dev_path = self.hass.config.path(KNOWN_DEVICES_FILE) + + try: + # If file does not exist we will write the header too + is_new_file = not os.path.isfile(known_dev_path) + + with open(known_dev_path, 'a') as outp: + _LOGGER.info("Found %d new devices, updating %s", + len(new_devices), known_dev_path) + + writer = csv.writer(outp) + + if is_new_file: + writer.writerow(("device", "name", "track", "picture")) + + for device in new_devices: + # See if the device scanner knows the name + # else defaults to unknown device + name = self.device_scanner.get_device_name(device) or \ + DEVICE_DEFAULT_NAME + + track = 0 + if self.track_new_devices: + self._track_device(device, name) + track = 1 + + writer.writerow((device, name, track, "")) + + if self.track_new_devices: + self._generate_entity_ids(new_devices) + + except IOError: + _LOGGER.exception("Error updating %s with %d new devices", + known_dev_path, len(new_devices)) + def _track_device(self, device, name): """ Add a device to the list of tracked devices. diff --git a/tests/test_component_device_tracker.py b/tests/test_component_device_tracker.py index 038b2363e7b..143c28c9cdb 100644 --- a/tests/test_component_device_tracker.py +++ b/tests/test_component_device_tracker.py @@ -14,7 +14,8 @@ import homeassistant as ha import homeassistant.loader as loader import homeassistant.util.dt as dt_util from homeassistant.const import ( - STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, CONF_PLATFORM) + STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, CONF_PLATFORM, + DEVICE_DEFAULT_NAME) import homeassistant.components.device_tracker as device_tracker from helpers import get_test_home_assistant @@ -96,7 +97,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): # To ensure all the three expected lines are there, we sort the file with open(self.known_dev_path) as fil: self.assertEqual( - ['DEV1,unknown device,0,\n', 'DEV2,dev2,0,\n', + ['DEV1,{},0,\n'.format(DEVICE_DEFAULT_NAME), 'DEV2,dev2,0,\n', 'device,name,track,picture\n'], sorted(fil)) From a330f4c130281416a3bab7907873fe9003c77d1a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 2 Jun 2015 22:12:02 -0700 Subject: [PATCH 50/95] Update apple touch icon --- .../components/frontend/index.html.template | 4 ++-- .../www_static/favicon-apple-180x180.png | Bin 0 -> 15269 bytes 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/frontend/www_static/favicon-apple-180x180.png diff --git a/homeassistant/components/frontend/index.html.template b/homeassistant/components/frontend/index.html.template index b9126ed25e2..f84c8653b31 100644 --- a/homeassistant/components/frontend/index.html.template +++ b/homeassistant/components/frontend/index.html.template @@ -15,8 +15,8 @@ - + diff --git a/homeassistant/components/frontend/www_static/favicon-apple-180x180.png b/homeassistant/components/frontend/www_static/favicon-apple-180x180.png new file mode 100644 index 0000000000000000000000000000000000000000..20117d00f22756f4e77b60042ac48186e31a3861 GIT binary patch literal 15269 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 WQdxL1)rNtAfx*+&&t;ucLK6Uyi+= Date: Tue, 2 Jun 2015 22:25:44 -0700 Subject: [PATCH 51/95] Frontend: fix state card content showing outside of card --- .../www_static/polymer/cards/state-card-content.html | 8 -------- .../www_static/polymer/cards/state-card-media_player.html | 1 + 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/homeassistant/components/frontend/www_static/polymer/cards/state-card-content.html b/homeassistant/components/frontend/www_static/polymer/cards/state-card-content.html index 0feed86de00..e5b929b0804 100644 --- a/homeassistant/components/frontend/www_static/polymer/cards/state-card-content.html +++ b/homeassistant/components/frontend/www_static/polymer/cards/state-card-content.html @@ -7,14 +7,6 @@ - - - - - - - - - @@ -25761,8 +25790,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN text-transform: capitalize; } - /* Accent the power button because the user should use that first */ - paper-icon-button[focus] { + paper-icon-button[highlight] { color: var(--accent-color); } @@ -25774,7 +25802,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN transition: max-height .5s ease-in; } - .has-media_volume .volume { + .has-volume_level .volume { max-height: 40px; } @@ -25782,19 +25810,19 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN

- +
- -
- - + Log Out diff --git a/homeassistant/components/frontend/www_static/polymer/layouts/home-assistant-main.html b/homeassistant/components/frontend/www_static/polymer/layouts/home-assistant-main.html index 11ecff37f8a..a3b9ea3246b 100644 --- a/homeassistant/components/frontend/www_static/polymer/layouts/home-assistant-main.html +++ b/homeassistant/components/frontend/www_static/polymer/layouts/home-assistant-main.html @@ -39,6 +39,10 @@ cursor: pointer; } + paper-icon-item.logout { + margin-top: 16px; + } + .divider { border-top: 1px solid #e0e0e0; } @@ -62,10 +66,10 @@ -
Home Assistant
+
Home Assistant
- @@ -93,9 +97,7 @@ -
- - + Log Out diff --git a/homeassistant/components/frontend/www_static/polymer/layouts/partial-base.html b/homeassistant/components/frontend/www_static/polymer/layouts/partial-base.html index 0229af9318a..66f67015f81 100644 --- a/homeassistant/components/frontend/www_static/polymer/layouts/partial-base.html +++ b/homeassistant/components/frontend/www_static/polymer/layouts/partial-base.html @@ -10,7 +10,7 @@ -
+
From ca373b5aa579ce6d984db2e77c20cf88da0298bb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 9 Jun 2015 20:53:50 -0700 Subject: [PATCH 73/95] Frontend: Workaround for tap bug in paper-dialog --- homeassistant/components/frontend/version.py | 2 +- .../components/frontend/www_static/frontend.html | 10 ++++------ .../frontend/www_static/polymer/cards/state-card.html | 10 ++++------ 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 00184d2224b..3e032816d91 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "e53215fa1416dbbc7819452b4f38689c" +VERSION = "1cc966bcef26a859d053bd5c46769a99" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 335b5400834..a970f484450 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -22375,15 +22375,13 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN }, listeners: { - 'tap': 'cardTapped', + // listening for click instead of tap as a work around + // https://github.com/PolymerElements/iron-overlay-behavior/issues/14 + 'click': 'cardTapped', }, cardTapped: function() { - // Debounce wrapper added as workaround for bug - // https://github.com/PolymerElements/iron-overlay-behavior/issues/14 - this.debounce('show-more-info-dialog', function() { - uiActions.showMoreInfoDialog(this.stateObj.entityId); - }, 1); + uiActions.showMoreInfoDialog(this.stateObj.entityId); }, }); })(); diff --git a/homeassistant/components/frontend/www_static/polymer/cards/state-card.html b/homeassistant/components/frontend/www_static/polymer/cards/state-card.html index a1d46c07282..341d21212a1 100644 --- a/homeassistant/components/frontend/www_static/polymer/cards/state-card.html +++ b/homeassistant/components/frontend/www_static/polymer/cards/state-card.html @@ -37,15 +37,13 @@ }, listeners: { - 'tap': 'cardTapped', + // listening for click instead of tap as a work around + // https://github.com/PolymerElements/iron-overlay-behavior/issues/14 + 'click': 'cardTapped', }, cardTapped: function() { - // Debounce wrapper added as workaround for bug - // https://github.com/PolymerElements/iron-overlay-behavior/issues/14 - this.debounce('show-more-info-dialog', function() { - uiActions.showMoreInfoDialog(this.stateObj.entityId); - }, 1); + uiActions.showMoreInfoDialog(this.stateObj.entityId); }, }); })(); From 801eabe598c687d7bf9b205fc4a58f9e70c30250 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 10 Jun 2015 23:51:33 -0700 Subject: [PATCH 74/95] Bugfixes for media player more info dialog --- .../polymer/more-infos/more-info-media_player.html | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/frontend/www_static/polymer/more-infos/more-info-media_player.html b/homeassistant/components/frontend/www_static/polymer/more-infos/more-info-media_player.html index 9d16ab9e561..2008fddf41e 100644 --- a/homeassistant/components/frontend/www_static/polymer/more-infos/more-info-media_player.html +++ b/homeassistant/components/frontend/www_static/polymer/more-infos/more-info-media_player.html @@ -29,7 +29,8 @@
+ on-tap='handleTogglePower' + hidden$='[[computeHidePowerButton(isOff, supportsTurnOn, supportsTurnOff)]]'>
-