From ecd09f22efec66d61f6a33943678234036365c53 Mon Sep 17 00:00:00 2001 From: Finbarr Brady Date: Sat, 30 May 2015 13:44:29 +0000 Subject: [PATCH 01/52] 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 02/52] 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 03/52] 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 04/52] 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 05/52] 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 06/52] 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 07/52] 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 08/52] 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 09/52] 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 10/52] 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 11/52] 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 12/52] 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 13/52] 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 eae52a8fafca08a88a7c00845fc48a0a9892232d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 2 Jun 2015 00:02:50 -0700 Subject: [PATCH 14/52] More target temperature nest fixes --- homeassistant/components/thermostat/nest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/thermostat/nest.py b/homeassistant/components/thermostat/nest.py index af3b02b2b15..b9d13b123fb 100644 --- a/homeassistant/components/thermostat/nest.py +++ b/homeassistant/components/thermostat/nest.py @@ -80,11 +80,11 @@ class NestThermostat(ThermostatDevice): low, high = target if self.current_temperature < low: - target = low + temp = low elif self.current_temperature > high: - target = high + temp = high else: - target = low + high + temp = (low + high)/2 else: temp = target From d91f414313c7cff5db30294e69a56d39ce9d3e5a Mon Sep 17 00:00:00 2001 From: Wolfgang Ettlinger Date: Tue, 2 Jun 2015 15:40:02 +0200 Subject: [PATCH 15/52] added generic module to switch swiches using shell commands --- README.md | 1 + .../components/switch/command_switch.py | 68 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 homeassistant/components/switch/command_switch.py diff --git a/README.md b/README.md index 5eb5ad4937a..1648b526332 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ It offers the following functionality through built-in components: * Track and control [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast) * Track running services by monitoring `ps` output * 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 * Turn off all lights and devices when everybody leaves the house diff --git a/homeassistant/components/switch/command_switch.py b/homeassistant/components/switch/command_switch.py new file mode 100644 index 00000000000..163881bbf19 --- /dev/null +++ b/homeassistant/components/switch/command_switch.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +import logging +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME +import subprocess + +_LOGGER = logging.getLogger(__name__) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Find and return switches controlled by shell commands. """ + + switches = config.get('switches', {}) + devices = [] + + for k, v in switches.items(): + devices.append( + CommandSwitch( + k, + v.get('oncmd', 'true'), + v.get('offcmd', 'true'))) + + add_devices_callback(devices) + + +class CommandSwitch(ToggleEntity): + def __init__(self, name, command_on, command_off): + self._name = name or DEVICE_DEFAULT_NAME + self._state = STATE_OFF + self._command_on = command_on + self._command_off = command_off + + @staticmethod + def _switch(command): + _LOGGER.info('Running command: {}'.format(command)) + + success = (subprocess.call(command, shell=True) == 0) + + if not success: + _LOGGER.error('Command failed: {}'.format(command)) + + return success + + @property + def should_poll(self): + return False + + @property + def name(self): + return self._name + + @property + def state(self): + return self._state + + @property + def is_on(self): + return self._state == STATE_ON + + def turn_on(self, **kwargs): + if CommandSwitch._switch(self._command_on): + self._state = STATE_ON + + def turn_off(self, **kwargs): + if CommandSwitch._switch(self._command_off): + self._state = STATE_OFF + From 458838e9c63688637bb765ce7d18142affb12148 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 2 Jun 2015 15:49:24 +0200 Subject: [PATCH 16/52] 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 b35bdc606ab47c9e920883ad65fb637a32ef708f Mon Sep 17 00:00:00 2001 From: Wolfgang Ettlinger Date: Tue, 2 Jun 2015 15:54:43 +0200 Subject: [PATCH 17/52] style adaptions --- .../components/switch/command_switch.py | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/switch/command_switch.py b/homeassistant/components/switch/command_switch.py index 163881bbf19..d57871153c9 100644 --- a/homeassistant/components/switch/command_switch.py +++ b/homeassistant/components/switch/command_switch.py @@ -1,4 +1,10 @@ # -*- coding: utf-8 -*- +""" +homeassistant.components.switch.command_switch +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Allows to configure custom shell commands to turn a switch on/off. +""" import logging from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME @@ -14,17 +20,18 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): switches = config.get('switches', {}) devices = [] - for k, v in switches.items(): + for dev_name, properties in switches.items(): devices.append( CommandSwitch( - k, - v.get('oncmd', 'true'), - v.get('offcmd', 'true'))) + dev_name, + properties.get('oncmd', 'true'), + properties.get('offcmd', 'true'))) add_devices_callback(devices) class CommandSwitch(ToggleEntity): + """ Represents a switch that can be togggled using shell commands """ def __init__(self, name, command_on, command_off): self._name = name or DEVICE_DEFAULT_NAME self._state = STATE_OFF @@ -33,6 +40,7 @@ class CommandSwitch(ToggleEntity): @staticmethod def _switch(command): + """ Execute the actual commands """ _LOGGER.info('Running command: {}'.format(command)) success = (subprocess.call(command, shell=True) == 0) @@ -44,25 +52,30 @@ class CommandSwitch(ToggleEntity): @property def should_poll(self): + """ No polling needed """ return False @property def name(self): + """ The name of the switch """ return self._name @property def state(self): + """ Returns the state of the switch. """ 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. """ if CommandSwitch._switch(self._command_on): self._state = STATE_ON def turn_off(self, **kwargs): + """ Turn the device off. """ if CommandSwitch._switch(self._command_off): self._state = STATE_OFF - From ba8d429a9f7d4ce7d0912d252971911658278a0f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 31 May 2015 22:54:16 +0200 Subject: [PATCH 18/52] 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 19/52] 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 20/52] 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 21/52] 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 22/52] 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 23/52] 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 24/52] 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 25/52] 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 26/52] 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 27/52] 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 28/52] 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 29/52] 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 30/52] 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 31/52] 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 32/52] 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 5d1e0d5c440db7c8ff470834aa4048c53ffa6cb5 Mon Sep 17 00:00:00 2001 From: Wolfgang Ettlinger Date: Tue, 2 Jun 2015 17:04:14 +0200 Subject: [PATCH 33/52] fixed pylint warnings --- homeassistant/components/switch/command_switch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/command_switch.py b/homeassistant/components/switch/command_switch.py index d57871153c9..291cd3af04d 100644 --- a/homeassistant/components/switch/command_switch.py +++ b/homeassistant/components/switch/command_switch.py @@ -41,12 +41,12 @@ class CommandSwitch(ToggleEntity): @staticmethod def _switch(command): """ Execute the actual commands """ - _LOGGER.info('Running command: {}'.format(command)) + _LOGGER.info('Running command: %s', command) success = (subprocess.call(command, shell=True) == 0) if not success: - _LOGGER.error('Command failed: {}'.format(command)) + _LOGGER.error('Command failed: %s', command) return success From bbf8420d804513cd974c760cec27d79686edaab2 Mon Sep 17 00:00:00 2001 From: Finbarr Brady Date: Tue, 2 Jun 2015 16:04:45 +0100 Subject: [PATCH 34/52] 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 35/52] 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 36/52] 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 37/52] 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 38/52] 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 39/52] 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 40/52] 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 41/52] 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 42/52] 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 43/52] 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 44/52] 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 45/52] 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 46/52] 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 47/52] 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 48/52] 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 49/52] 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 50/52] 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 @@ - - - - - - - - -