diff --git a/.coveragerc b/.coveragerc index 75f4cbd20fd..06cfc7d7471 100644 --- a/.coveragerc +++ b/.coveragerc @@ -6,10 +6,17 @@ omit = # omit pieces of code that rely on external devices being present homeassistant/components/alarm_control_panel/alarmdotcom.py + homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/arduino.py homeassistant/components/*/arduino.py + homeassistant/components/apcupsd.py + homeassistant/components/*/apcupsd.py + + homeassistant/components/bloomsky.py + homeassistant/components/*/bloomsky.py + homeassistant/components/insteon_hub.py homeassistant/components/*/insteon_hub.py @@ -53,6 +60,9 @@ omit = homeassistant/components/rpi_gpio.py homeassistant/components/*/rpi_gpio.py + homeassistant/components/scsgate.py + homeassistant/components/*/scsgate.py + homeassistant/components/binary_sensor/arest.py homeassistant/components/binary_sensor/rest.py homeassistant/components/browser.py @@ -73,9 +83,8 @@ omit = homeassistant/components/device_tracker/ubus.py homeassistant/components/discovery.py homeassistant/components/downloader.py + homeassistant/components/garage_door/wink.py homeassistant/components/ifttt.py - homeassistant/components/statsd.py - homeassistant/components/influxdb.py homeassistant/components/keyboard.py homeassistant/components/light/blinksticklight.py homeassistant/components/light/hue.py @@ -89,21 +98,24 @@ omit = homeassistant/components/media_player/kodi.py homeassistant/components/media_player/mpd.py homeassistant/components/media_player/plex.py + homeassistant/components/media_player/samsungtv.py + homeassistant/components/media_player/snapcast.py homeassistant/components/media_player/sonos.py homeassistant/components/media_player/squeezebox.py homeassistant/components/notify/free_mobile.py + homeassistant/components/notify/googlevoice.py homeassistant/components/notify/instapush.py homeassistant/components/notify/nma.py homeassistant/components/notify/pushbullet.py homeassistant/components/notify/pushetta.py homeassistant/components/notify/pushover.py + homeassistant/components/notify/rest.py homeassistant/components/notify/slack.py homeassistant/components/notify/smtp.py homeassistant/components/notify/syslog.py homeassistant/components/notify/telegram.py homeassistant/components/notify/twitter.py homeassistant/components/notify/xmpp.py - homeassistant/components/notify/googlevoice.py homeassistant/components/sensor/arest.py homeassistant/components/sensor/bitcoin.py homeassistant/components/sensor/cpuspeed.py @@ -118,6 +130,7 @@ omit = homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/rest.py homeassistant/components/sensor/sabnzbd.py + homeassistant/components/sensor/speedtest.py homeassistant/components/sensor/swiss_public_transport.py homeassistant/components/sensor/systemmonitor.py homeassistant/components/sensor/temper.py @@ -140,7 +153,6 @@ omit = homeassistant/components/thermostat/proliphix.py homeassistant/components/thermostat/radiotherm.py - [report] # Regexes for lines to exclude from consideration exclude_lines = diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index e97ed0c6386..5351bcf7983 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -1,13 +1,18 @@ """ Starts home assistant. """ from __future__ import print_function +from multiprocessing import Process +import signal import sys +import threading import os import argparse +import time from homeassistant import bootstrap import homeassistant.config as config_util -from homeassistant.const import __version__, EVENT_HOMEASSISTANT_START +from homeassistant.const import (__version__, EVENT_HOMEASSISTANT_START, + RESTART_EXIT_CODE) def validate_python(): @@ -73,6 +78,11 @@ def get_arguments(): '--demo-mode', action='store_true', help='Start Home Assistant in demo mode') + parser.add_argument( + '--debug', + action='store_true', + help='Start Home Assistant in debug mode. Runs in single process to ' + 'enable use of interactive debuggers.') parser.add_argument( '--open-ui', action='store_true', @@ -204,35 +214,11 @@ def uninstall_osx(): print("Home Assistant has been uninstalled.") -def main(): - """ Starts Home Assistant. """ - validate_python() - - args = get_arguments() - - config_dir = os.path.join(os.getcwd(), args.config) - ensure_config_path(config_dir) - - # os x launchd functions - if args.install_osx: - install_osx() - return - if args.uninstall_osx: - uninstall_osx() - return - if args.restart_osx: - uninstall_osx() - install_osx() - return - - # daemon functions - if args.pid_file: - check_pid(args.pid_file) - if args.daemon: - daemonize() - if args.pid_file: - write_pid(args.pid_file) - +def setup_and_run_hass(config_dir, args, top_process=False): + """ + Setup HASS and run. Block until stopped. Will assume it is running in a + subprocess unless top_process is set to true. + """ if args.demo_mode: config = { 'frontend': {}, @@ -259,7 +245,91 @@ def main(): hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser) hass.start() - hass.block_till_stopped() + exit_code = int(hass.block_till_stopped()) + + if not top_process: + sys.exit(exit_code) + return exit_code + + +def run_hass_process(hass_proc): + """ Runs a child hass process. Returns True if it should be restarted. """ + requested_stop = threading.Event() + hass_proc.daemon = True + + def request_stop(*args): + """ request hass stop, *args is for signal handler callback """ + requested_stop.set() + hass_proc.terminate() + + try: + signal.signal(signal.SIGTERM, request_stop) + except ValueError: + print('Could not bind to SIGTERM. Are you running in a thread?') + + hass_proc.start() + try: + hass_proc.join() + except KeyboardInterrupt: + request_stop() + try: + hass_proc.join() + except KeyboardInterrupt: + return False + + return (not requested_stop.isSet() and + hass_proc.exitcode == RESTART_EXIT_CODE, + hass_proc.exitcode) + + +def main(): + """ Starts Home Assistant. """ + validate_python() + + args = get_arguments() + + config_dir = os.path.join(os.getcwd(), args.config) + ensure_config_path(config_dir) + + # os x launchd functions + if args.install_osx: + install_osx() + return 0 + if args.uninstall_osx: + uninstall_osx() + return 0 + if args.restart_osx: + uninstall_osx() + # A small delay is needed on some systems to let the unload finish. + time.sleep(0.5) + install_osx() + return 0 + + # daemon functions + if args.pid_file: + check_pid(args.pid_file) + if args.daemon: + daemonize() + if args.pid_file: + write_pid(args.pid_file) + + # Run hass in debug mode if requested + if args.debug: + sys.stderr.write('Running in debug mode. ' + 'Home Assistant will not be able to restart.\n') + exit_code = setup_and_run_hass(config_dir, args, top_process=True) + if exit_code == RESTART_EXIT_CODE: + sys.stderr.write('Home Assistant requested a ' + 'restart in debug mode.\n') + return exit_code + + # Run hass as child process. Restart if necessary. + keep_running = True + while keep_running: + hass_proc = Process(target=setup_and_run_hass, args=(config_dir, args)) + keep_running, exit_code = run_hass_process(hass_proc) + return exit_code + if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 3f5e6362fb6..310a65c6184 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -61,6 +61,8 @@ def setup(hass, config): for alarm in target_alarms: getattr(alarm, method)(code) + if alarm.should_poll: + alarm.update_ha_state(True) descriptions = load_yaml_config_file( os.path.join(os.path.dirname(__file__), 'services.yaml')) diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py index 7f76680feb7..da74c02da54 100644 --- a/homeassistant/components/alarm_control_panel/alarmdotcom.py +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -57,10 +57,12 @@ class AlarmDotCom(alarm.AlarmControlPanel): @property def should_poll(self): + """ No polling needed. """ return True @property def name(self): + """ Returns the name of the device. """ return self._name @property @@ -88,7 +90,6 @@ class AlarmDotCom(alarm.AlarmControlPanel): # Open another session to alarm.com to fire off the command _alarm = Alarmdotcom(self._username, self._password, timeout=10) _alarm.disarm() - self.update_ha_state() def alarm_arm_home(self, code=None): """ Send arm home command. """ @@ -98,7 +99,6 @@ class AlarmDotCom(alarm.AlarmControlPanel): # Open another session to alarm.com to fire off the command _alarm = Alarmdotcom(self._username, self._password, timeout=10) _alarm.arm_stay() - self.update_ha_state() def alarm_arm_away(self, code=None): """ Send arm away command. """ @@ -108,7 +108,6 @@ class AlarmDotCom(alarm.AlarmControlPanel): # Open another session to alarm.com to fire off the command _alarm = Alarmdotcom(self._username, self._password, timeout=10) _alarm.arm_away() - self.update_ha_state() def _validate_code(self, code, state): """ Validate given code. """ diff --git a/homeassistant/components/alarm_control_panel/nx584.py b/homeassistant/components/alarm_control_panel/nx584.py new file mode 100644 index 00000000000..55ad9b25b9f --- /dev/null +++ b/homeassistant/components/alarm_control_panel/nx584.py @@ -0,0 +1,105 @@ +""" +homeassistant.components.alarm_control_panel.nx584 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for NX584 alarm control panels. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/alarm_control_panel.nx584/ +""" +import logging +import requests + +from homeassistant.const import (STATE_UNKNOWN, STATE_ALARM_DISARMED, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_AWAY) +import homeassistant.components.alarm_control_panel as alarm + +REQUIREMENTS = ['pynx584==0.1'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Setup nx584. """ + host = config.get('host', 'localhost:5007') + + try: + add_devices([NX584Alarm(hass, host, config.get('name', 'NX584'))]) + except requests.exceptions.ConnectionError as ex: + _LOGGER.error('Unable to connect to NX584: %s', str(ex)) + return False + + +class NX584Alarm(alarm.AlarmControlPanel): + """ NX584-based alarm panel. """ + def __init__(self, hass, host, name): + from nx584 import client + self._hass = hass + self._host = host + self._name = name + self._alarm = client.Client('http://%s' % host) + # Do an initial list operation so that we will try to actually + # talk to the API and trigger a requests exception for setup_platform() + # to catch + self._alarm.list_zones() + + @property + def should_poll(self): + """ Polling needed. """ + return True + + @property + def name(self): + """ Returns the name of the device. """ + return self._name + + @property + def code_format(self): + """ Characters if code is defined. """ + return '[0-9]{4}([0-9]{2})?' + + @property + def state(self): + """ Returns the state of the device. """ + try: + part = self._alarm.list_partitions()[0] + zones = self._alarm.list_zones() + except requests.exceptions.ConnectionError as ex: + _LOGGER.error('Unable to connect to %(host)s: %(reason)s', + dict(host=self._host, reason=ex)) + return STATE_UNKNOWN + except IndexError: + _LOGGER.error('nx584 reports no partitions') + return STATE_UNKNOWN + + bypassed = False + for zone in zones: + if zone['bypassed']: + _LOGGER.debug('Zone %(zone)s is bypassed, ' + 'assuming HOME', + dict(zone=zone['number'])) + bypassed = True + break + + if not part['armed']: + return STATE_ALARM_DISARMED + elif bypassed: + return STATE_ALARM_ARMED_HOME + else: + return STATE_ALARM_ARMED_AWAY + + def alarm_disarm(self, code=None): + """ Send disarm command. """ + self._alarm.disarm(code) + + def alarm_arm_home(self, code=None): + """ Send arm home command. """ + self._alarm.arm('home') + + def alarm_arm_away(self, code=None): + """ Send arm away command. """ + self._alarm.arm('auto') + + def alarm_trigger(self, code=None): + """ Alarm trigger command. """ + raise NotImplementedError() diff --git a/homeassistant/components/apcupsd.py b/homeassistant/components/apcupsd.py new file mode 100644 index 00000000000..b7c22b3a7d9 --- /dev/null +++ b/homeassistant/components/apcupsd.py @@ -0,0 +1,84 @@ +""" +homeassistant.components.apcupsd +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Sets up and provides access to the status output of APCUPSd via its Network +Information Server (NIS). + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/apcupsd/ +""" +import logging +from datetime import timedelta + +from homeassistant.util import Throttle + +DOMAIN = "apcupsd" +REQUIREMENTS = ("apcaccess==0.0.4",) + +CONF_HOST = "host" +CONF_PORT = "port" +CONF_TYPE = "type" + +DEFAULT_HOST = "localhost" +DEFAULT_PORT = 3551 + +KEY_STATUS = "STATUS" + +VALUE_ONLINE = "ONLINE" + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + +DATA = None + +_LOGGER = logging.getLogger(__name__) + + +def setup(hass, config): + """ Use config values to set up a function enabling status retrieval. """ + global DATA + + host = config[DOMAIN].get(CONF_HOST, DEFAULT_HOST) + port = config[DOMAIN].get(CONF_PORT, DEFAULT_PORT) + + DATA = APCUPSdData(host, port) + + # It doesn't really matter why we're not able to get the status, just that + # we can't. + # pylint: disable=broad-except + try: + DATA.update(no_throttle=True) + except Exception: + _LOGGER.exception("Failure while testing APCUPSd status retrieval.") + return False + return True + + +class APCUPSdData(object): + """ + Stores the data retrieved from APCUPSd for each entity to use, acts as the + single point responsible for fetching updates from the server. + """ + def __init__(self, host, port): + from apcaccess import status + self._host = host + self._port = port + self._status = None + self._get = status.get + self._parse = status.parse + + @property + def status(self): + """ Get latest update if throttle allows. Return status. """ + self.update() + return self._status + + def _get_status(self): + """ Get the status from APCUPSd and parse it into a dict. """ + return self._parse(self._get(host=self._host, port=self._port)) + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self, **kwargs): + """ + Fetch the latest status from APCUPSd and store it in self._status. + """ + self._status = self._get_status() diff --git a/homeassistant/components/binary_sensor/apcupsd.py b/homeassistant/components/binary_sensor/apcupsd.py new file mode 100644 index 00000000000..796a2a0df70 --- /dev/null +++ b/homeassistant/components/binary_sensor/apcupsd.py @@ -0,0 +1,44 @@ +""" +homeassistant.components.binary_sensor.apcupsd +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Provides a binary sensor to track online status of a UPS. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.apcupsd/ +""" +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components import apcupsd + +DEPENDENCIES = [apcupsd.DOMAIN] +DEFAULT_NAME = "UPS Online Status" + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """ Instantiate an OnlineStatus binary sensor entity and add it to HA. """ + add_entities((OnlineStatus(config, apcupsd.DATA),)) + + +class OnlineStatus(BinarySensorDevice): + """ Binary sensor to represent UPS online status. """ + def __init__(self, config, data): + self._config = config + self._data = data + self._state = None + self.update() + + @property + def name(self): + """ The name of the UPS online status sensor. """ + return self._config.get("name", DEFAULT_NAME) + + @property + def is_on(self): + """ True if the UPS is online, else False. """ + return self._state == apcupsd.VALUE_ONLINE + + def update(self): + """ + Get the status report from APCUPSd (or cache) and set this entity's + state. + """ + self._state = self._data.status[apcupsd.KEY_STATUS] diff --git a/homeassistant/components/binary_sensor/command_sensor.py b/homeassistant/components/binary_sensor/command_sensor.py index 8798e457e71..d69417a6a73 100644 --- a/homeassistant/components/binary_sensor/command_sensor.py +++ b/homeassistant/components/binary_sensor/command_sensor.py @@ -1,8 +1,11 @@ """ homeassistant.components.binary_sensor.command_sensor -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Allows to configure custom shell commands to turn a value into a logical value for a binary sensor. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.command/ """ import logging from datetime import timedelta diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py index 23925a1805b..0c250dcde67 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -13,10 +13,11 @@ import homeassistant.components.nest as nest from homeassistant.components.sensor.nest import NestSensor from homeassistant.components.binary_sensor import BinarySensorDevice - +DEPENDENCIES = ['nest'] BINARY_TYPES = ['fan', 'hvac_ac_state', 'hvac_aux_heater_state', + 'hvac_heater_state', 'hvac_heat_x2_state', 'hvac_heat_x3_state', 'hvac_alt_heat_state', diff --git a/homeassistant/components/binary_sensor/rest.py b/homeassistant/components/binary_sensor/rest.py index 4d82d25e473..1a592cd905a 100644 --- a/homeassistant/components/binary_sensor/rest.py +++ b/homeassistant/components/binary_sensor/rest.py @@ -21,7 +21,7 @@ DEFAULT_METHOD = 'GET' # pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup REST binary sensors.""" + """ Setup REST binary sensors. """ resource = config.get('resource', None) method = config.get('method', DEFAULT_METHOD) payload = config.get('payload', None) @@ -41,10 +41,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=too-many-arguments class RestBinarySensor(BinarySensorDevice): - """REST binary sensor.""" + """ A REST binary sensor. """ def __init__(self, hass, rest, name, value_template): - """Initialize a REST binary sensor.""" + """ Initialize a REST binary sensor. """ self._hass = hass self.rest = rest self._name = name @@ -54,12 +54,12 @@ class RestBinarySensor(BinarySensorDevice): @property def name(self): - """Name of the binary sensor.""" + """ Name of the binary sensor. """ return self._name @property def is_on(self): - """Return if the binary sensor is on.""" + """ Return if the binary sensor is on. """ if self.rest.data is None: return False @@ -69,5 +69,5 @@ class RestBinarySensor(BinarySensorDevice): return bool(int(self.rest.data)) def update(self): - """Get the latest data from REST API and updates the state.""" + """ Get the latest data from REST API and updates the state. """ self.rest.update() diff --git a/homeassistant/components/binary_sensor/zigbee.py b/homeassistant/components/binary_sensor/zigbee.py index 72b2499b190..1597cd5004f 100644 --- a/homeassistant/components/binary_sensor/zigbee.py +++ b/homeassistant/components/binary_sensor/zigbee.py @@ -1,9 +1,11 @@ """ homeassistant.components.binary_sensor.zigbee - +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Contains functionality to use a ZigBee device as a binary sensor. -""" +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.zigbee/ +""" from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.zigbee import ( ZigBeeDigitalIn, ZigBeeDigitalInConfig) @@ -13,9 +15,7 @@ DEPENDENCIES = ["zigbee"] def setup_platform(hass, config, add_entities, discovery_info=None): - """ - Create and add an entity based on the configuration. - """ + """ Create and add an entity based on the configuration. """ add_entities([ ZigBeeBinarySensor(hass, ZigBeeDigitalInConfig(config)) ]) diff --git a/homeassistant/components/bloomsky.py b/homeassistant/components/bloomsky.py new file mode 100644 index 00000000000..fe2ae1cf3ba --- /dev/null +++ b/homeassistant/components/bloomsky.py @@ -0,0 +1,77 @@ +""" +homeassistant.components.bloomsky +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for BloomSky weather station. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/bloomsky/ +""" +import logging +from datetime import timedelta +import requests +from homeassistant.util import Throttle +from homeassistant.helpers import validate_config +from homeassistant.const import CONF_API_KEY + +DOMAIN = "bloomsky" +BLOOMSKY = None + +_LOGGER = logging.getLogger(__name__) + +# The BloomSky only updates every 5-8 minutes as per the API spec so there's +# no point in polling the API more frequently +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300) + + +# pylint: disable=unused-argument,too-few-public-methods +def setup(hass, config): + """ Setup BloomSky component. """ + if not validate_config( + config, + {DOMAIN: [CONF_API_KEY]}, + _LOGGER): + return False + + api_key = config[DOMAIN][CONF_API_KEY] + + global BLOOMSKY + try: + BLOOMSKY = BloomSky(api_key) + except RuntimeError: + return False + + return True + + +class BloomSky(object): + """ Handle all communication with the BloomSky API. """ + + # API documentation at http://weatherlution.com/bloomsky-api/ + + API_URL = "https://api.bloomsky.com/api/skydata" + + def __init__(self, api_key): + self._api_key = api_key + self.devices = {} + _LOGGER.debug("Initial bloomsky device load...") + self.refresh_devices() + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def refresh_devices(self): + """ + Uses the API to retreive a list of devices associated with an + account along with all the sensors on the device. + """ + _LOGGER.debug("Fetching bloomsky update") + response = requests.get(self.API_URL, + headers={"Authorization": self._api_key}, + timeout=10) + if response.status_code == 401: + raise RuntimeError("Invalid API_KEY") + elif response.status_code != 200: + _LOGGER.error("Invalid HTTP response: %s", response.status_code) + return + # create dictionary keyed off of the device unique id + self.devices.update({ + device["DeviceID"]: device for device in response.json() + }) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index fc5c739c888..9aefe4b3b66 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -33,8 +33,6 @@ SWITCH_ACTION_SNAPSHOT = 'snapshot' SERVICE_CAMERA = 'camera_service' -STATE_RECORDING = 'recording' - DEFAULT_RECORDING_SECONDS = 30 # Maps discovered services to their platforms @@ -46,6 +44,7 @@ DIR_DATETIME_FORMAT = '%Y-%m-%d_%H-%M-%S' REC_DIR_PREFIX = 'recording-' REC_IMG_PREFIX = 'recording_image-' +STATE_RECORDING = 'recording' STATE_STREAMING = 'streaming' STATE_IDLE = 'idle' @@ -121,33 +120,7 @@ def setup(hass, config): try: camera.is_streaming = True camera.update_ha_state() - - handler.request.sendall(bytes('HTTP/1.1 200 OK\r\n', 'utf-8')) - handler.request.sendall(bytes( - 'Content-type: multipart/x-mixed-replace; \ - boundary=--jpgboundary\r\n\r\n', 'utf-8')) - handler.request.sendall(bytes('--jpgboundary\r\n', 'utf-8')) - - # MJPEG_START_HEADER.format() - - while True: - img_bytes = camera.camera_image() - if img_bytes is None: - continue - headers_str = '\r\n'.join(( - 'Content-length: {}'.format(len(img_bytes)), - 'Content-type: image/jpeg', - )) + '\r\n\r\n' - - handler.request.sendall( - bytes(headers_str, 'utf-8') + - img_bytes + - bytes('\r\n', 'utf-8')) - - handler.request.sendall( - bytes('--jpgboundary\r\n', 'utf-8')) - - time.sleep(0.5) + camera.mjpeg_stream(handler) except (requests.RequestException, IOError): camera.is_streaming = False @@ -190,6 +163,34 @@ class Camera(Entity): """ Return bytes of camera image. """ raise NotImplementedError() + def mjpeg_stream(self, handler): + """ Generate an HTTP MJPEG stream from camera images. """ + handler.request.sendall(bytes('HTTP/1.1 200 OK\r\n', 'utf-8')) + handler.request.sendall(bytes( + 'Content-type: multipart/x-mixed-replace; \ + boundary=--jpgboundary\r\n\r\n', 'utf-8')) + handler.request.sendall(bytes('--jpgboundary\r\n', 'utf-8')) + + # MJPEG_START_HEADER.format() + while True: + img_bytes = self.camera_image() + if img_bytes is None: + continue + headers_str = '\r\n'.join(( + 'Content-length: {}'.format(len(img_bytes)), + 'Content-type: image/jpeg', + )) + '\r\n\r\n' + + handler.request.sendall( + bytes(headers_str, 'utf-8') + + img_bytes + + bytes('\r\n', 'utf-8')) + + handler.request.sendall( + bytes('--jpgboundary\r\n', 'utf-8')) + + time.sleep(0.5) + @property def state(self): """ Returns the state of the entity. """ diff --git a/homeassistant/components/camera/bloomsky.py b/homeassistant/components/camera/bloomsky.py new file mode 100644 index 00000000000..5c9314963bd --- /dev/null +++ b/homeassistant/components/camera/bloomsky.py @@ -0,0 +1,60 @@ +""" +homeassistant.components.camera.bloomsky +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for a camera of a BloomSky weather station. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/camera.bloomsky/ +""" +import logging +import requests +import homeassistant.components.bloomsky as bloomsky +from homeassistant.components.camera import Camera + +DEPENDENCIES = ["bloomsky"] + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ set up access to BloomSky cameras """ + for device in bloomsky.BLOOMSKY.devices.values(): + add_devices_callback([BloomSkyCamera(bloomsky.BLOOMSKY, device)]) + + +class BloomSkyCamera(Camera): + """ Represents the images published from the BloomSky's camera. """ + + def __init__(self, bs, device): + """ set up for access to the BloomSky camera images """ + super(BloomSkyCamera, self).__init__() + self._name = device["DeviceName"] + self._id = device["DeviceID"] + self._bloomsky = bs + self._url = "" + self._last_url = "" + # _last_image will store images as they are downloaded so that the + # frequent updates in home-assistant don't keep poking the server + # to download the same image over and over + self._last_image = "" + self._logger = logging.getLogger(__name__) + + def camera_image(self): + """ Update the camera's image if it has changed. """ + try: + self._url = self._bloomsky.devices[self._id]["Data"]["ImageURL"] + self._bloomsky.refresh_devices() + # if the url hasn't changed then the image hasn't changed + if self._url != self._last_url: + response = requests.get(self._url, timeout=10) + self._last_url = self._url + self._last_image = response.content + except requests.exceptions.RequestException as error: + self._logger.error("Error getting bloomsky image: %s", error) + return None + + return self._last_image + + @property + def name(self): + """ The name of this BloomSky device. """ + return self._name diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index 0d59c8d60c7..7bbaa4846b5 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -14,6 +14,9 @@ from requests.auth import HTTPBasicAuth from homeassistant.helpers import validate_config from homeassistant.components.camera import DOMAIN, Camera +from homeassistant.const import HTTP_OK + +CONTENT_TYPE_HEADER = 'Content-Type' _LOGGER = logging.getLogger(__name__) @@ -41,6 +44,17 @@ class MjpegCamera(Camera): self._password = device_info.get('password') self._mjpeg_url = device_info['mjpeg_url'] + def camera_stream(self): + """ Return a mjpeg stream image response directly from the camera. """ + if self._username and self._password: + return requests.get(self._mjpeg_url, + auth=HTTPBasicAuth(self._username, + self._password), + stream=True) + else: + return requests.get(self._mjpeg_url, + stream=True) + def camera_image(self): """ Return a still image response from the camera. """ @@ -55,16 +69,22 @@ class MjpegCamera(Camera): jpg = data[jpg_start:jpg_end + 2] return jpg - if self._username and self._password: - with closing(requests.get(self._mjpeg_url, - auth=HTTPBasicAuth(self._username, - self._password), - stream=True)) as response: - return process_response(response) - else: - with closing(requests.get(self._mjpeg_url, - stream=True)) as response: - return process_response(response) + with closing(self.camera_stream()) as response: + return process_response(response) + + def mjpeg_stream(self, handler): + """ Generate an HTTP MJPEG stream from the camera. """ + response = self.camera_stream() + content_type = response.headers[CONTENT_TYPE_HEADER] + + handler.send_response(HTTP_OK) + handler.send_header(CONTENT_TYPE_HEADER, content_type) + handler.end_headers() + + for chunk in response.iter_content(chunk_size=1024): + if not chunk: + break + handler.wfile.write(chunk) @property def name(self): diff --git a/homeassistant/components/camera/uvc.py b/homeassistant/components/camera/uvc.py new file mode 100644 index 00000000000..eeb447be05a --- /dev/null +++ b/homeassistant/components/camera/uvc.py @@ -0,0 +1,91 @@ +""" +homeassistant.components.camera.uvc +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for Ubiquiti's UVC cameras. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/camera.uvc/ +""" +import logging +import socket + +import requests + +from homeassistant.helpers import validate_config +from homeassistant.components.camera import DOMAIN, Camera + +REQUIREMENTS = ['uvcclient==0.5'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Discover cameras on a Unifi NVR. """ + if not validate_config({DOMAIN: config}, {DOMAIN: ['nvr', 'key']}, + _LOGGER): + return None + + addr = config.get('nvr') + port = int(config.get('port', 7080)) + key = config.get('key') + + from uvcclient import nvr + nvrconn = nvr.UVCRemote(addr, port, key) + try: + cameras = nvrconn.index() + except nvr.NotAuthorized: + _LOGGER.error('Authorization failure while connecting to NVR') + return False + except nvr.NvrError: + _LOGGER.error('NVR refuses to talk to me') + return False + except requests.exceptions.ConnectionError as ex: + _LOGGER.error('Unable to connect to NVR: %s', str(ex)) + return False + + for camera in cameras: + add_devices([UnifiVideoCamera(nvrconn, + camera['uuid'], + camera['name'])]) + + +class UnifiVideoCamera(Camera): + """ A Ubiquiti Unifi Video Camera. """ + + def __init__(self, nvr, uuid, name): + super(UnifiVideoCamera, self).__init__() + self._nvr = nvr + self._uuid = uuid + self._name = name + self.is_streaming = False + + @property + def name(self): + return self._name + + @property + def is_recording(self): + caminfo = self._nvr.get_camera(self._uuid) + return caminfo['recordingSettings']['fullTimeRecordEnabled'] + + def camera_image(self): + from uvcclient import camera as uvc_camera + + caminfo = self._nvr.get_camera(self._uuid) + camera = None + for addr in [caminfo['host'], caminfo['internalHost']]: + try: + camera = uvc_camera.UVCCameraClient(addr, + caminfo['username'], + 'ubnt') + _LOGGER.debug('Logged into UVC camera %(name)s via %(addr)s', + dict(name=self._name, addr=addr)) + except socket.error: + pass + + if not camera: + _LOGGER.error('Unable to login to camera') + return None + + camera.login() + return camera.get_snapshot() diff --git a/homeassistant/components/configurator.py b/homeassistant/components/configurator.py index 6fb584635f9..591cdc0dc61 100644 --- a/homeassistant/components/configurator.py +++ b/homeassistant/components/configurator.py @@ -141,7 +141,7 @@ class Configurator(object): state = self.hass.states.get(entity_id) - new_data = state.attributes + new_data = dict(state.attributes) new_data[ATTR_ERRORS] = error self.hass.states.set(entity_id, STATE_CONFIGURE, new_data) diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index 37f93c0625d..e63f5f49551 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -21,6 +21,7 @@ COMPONENTS_WITH_DEMO_PLATFORM = [ 'binary_sensor', 'camera', 'device_tracker', + 'garage_door', 'light', 'lock', 'media_player', diff --git a/homeassistant/components/device_tracker/aruba.py b/homeassistant/components/device_tracker/aruba.py index 82183e1495c..6d94ad30d04 100644 --- a/homeassistant/components/device_tracker/aruba.py +++ b/homeassistant/components/device_tracker/aruba.py @@ -11,7 +11,6 @@ import logging from datetime import timedelta import re import threading -import telnetlib from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD from homeassistant.helpers import validate_config @@ -21,6 +20,7 @@ from homeassistant.components.device_tracker import DOMAIN # Return cached results if last scan was less then this time ago MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) +REQUIREMENTS = ['pexpect==4.0.1'] _LOGGER = logging.getLogger(__name__) _DEVICES_REGEX = re.compile( @@ -44,6 +44,7 @@ def get_scanner(hass, config): class ArubaDeviceScanner(object): """ This class queries a Aruba Acces Point for connected devices. """ + def __init__(self, config): self.host = config[CONF_HOST] self.username = config[CONF_USERNAME] @@ -93,23 +94,39 @@ class ArubaDeviceScanner(object): def get_aruba_data(self): """ Retrieve data from Aruba Access Point and return parsed result. """ - try: - telnet = telnetlib.Telnet(self.host) - telnet.read_until(b'User: ') - telnet.write((self.username + '\r\n').encode('ascii')) - telnet.read_until(b'Password: ') - telnet.write((self.password + '\r\n').encode('ascii')) - telnet.read_until(b'#') - telnet.write(('show clients\r\n').encode('ascii')) - devices_result = telnet.read_until(b'#').split(b'\r\n') - telnet.write('exit\r\n'.encode('ascii')) - except EOFError: - _LOGGER.exception("Unexpected response from router") + + import pexpect + connect = "ssh {}@{}" + ssh = pexpect.spawn(connect.format(self.username, self.host)) + query = ssh.expect(['password:', pexpect.TIMEOUT, pexpect.EOF, + 'continue connecting (yes/no)?', + 'Host key verification failed.', + 'Connection refused', + 'Connection timed out'], timeout=120) + if query == 1: + _LOGGER.error("Timeout") return - except ConnectionRefusedError: - _LOGGER.exception("Connection refused by router," + - " is telnet enabled?") + elif query == 2: + _LOGGER.error("Unexpected response from router") return + elif query == 3: + ssh.sendline('yes') + ssh.expect('password:') + elif query == 4: + _LOGGER.error("Host key Changed") + return + elif query == 5: + _LOGGER.error("Connection refused by server") + return + elif query == 6: + _LOGGER.error("Connection timed out") + return + ssh.sendline(self.password) + ssh.expect('#') + ssh.sendline('show clients') + ssh.expect('#') + devices_result = ssh.before.split(b'\r\n') + ssh.sendline('exit') devices = {} for device in devices_result: @@ -119,5 +136,5 @@ class ArubaDeviceScanner(object): 'ip': match.group('ip'), 'mac': match.group('mac').upper(), 'name': match.group('name') - } + } return devices diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index 0a924126b11..2b8e612030b 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -95,7 +95,8 @@ def setup_scanner(hass, config, see): MOBILE_BEACONS_ACTIVE[dev_id].append(location) else: # Normal region - kwargs['location_name'] = location + if not zone.attributes.get('passive'): + kwargs['location_name'] = location regions = REGIONS_ENTERED[dev_id] if location not in regions: @@ -115,7 +116,8 @@ def setup_scanner(hass, config, see): if new_region: # Exit to previous region zone = hass.states.get("zone.{}".format(new_region)) - kwargs['location_name'] = new_region + if not zone.attributes.get('passive'): + kwargs['location_name'] = new_region _set_gps_from_zone(kwargs, zone) _LOGGER.info("Exit from to %s", new_region) diff --git a/homeassistant/components/frontend/mdi_version.py b/homeassistant/components/frontend/mdi_version.py index 4fa9a33b78c..90765572d2c 100644 --- a/homeassistant/components/frontend/mdi_version.py +++ b/homeassistant/components/frontend/mdi_version.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by update_mdi script """ -VERSION = "a2605736c8d959d50c4bcbba1e6a6aa5" +VERSION = "a1a203680639ff1abcc7b68cdb29c57a" diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index ed2461d04a6..f207bcae379 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "1e89871aaae43c91b2508f52bc161b69" +VERSION = "833d09737fec24f9219efae87c5bfd2a" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 0a2643facff..09d82fce309 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -1,6 +1,7 @@ --