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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 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 @@ - - - - - - - - -