diff --git a/.coveragerc b/.coveragerc index 06cfc7d7471..b9ef057bc21 100644 --- a/.coveragerc +++ b/.coveragerc @@ -39,6 +39,9 @@ omit = homeassistant/components/verisure.py homeassistant/components/*/verisure.py + homeassistant/components/wemo.py + homeassistant/components/*/wemo.py + homeassistant/components/wink.py homeassistant/components/*/wink.py @@ -66,7 +69,10 @@ omit = homeassistant/components/binary_sensor/arest.py homeassistant/components/binary_sensor/rest.py homeassistant/components/browser.py - homeassistant/components/camera/* + homeassistant/components/camera/bloomsky.py + homeassistant/components/camera/foscam.py + homeassistant/components/camera/generic.py + homeassistant/components/camera/mjpeg.py homeassistant/components/device_tracker/actiontec.py homeassistant/components/device_tracker/aruba.py homeassistant/components/device_tracker/asuswrt.py @@ -110,6 +116,7 @@ omit = homeassistant/components/notify/pushetta.py homeassistant/components/notify/pushover.py homeassistant/components/notify/rest.py + homeassistant/components/notify/sendgrid.py homeassistant/components/notify/slack.py homeassistant/components/notify/smtp.py homeassistant/components/notify/syslog.py @@ -119,6 +126,7 @@ omit = homeassistant/components/sensor/arest.py homeassistant/components/sensor/bitcoin.py homeassistant/components/sensor/cpuspeed.py + homeassistant/components/sensor/deutsche_bahn.py homeassistant/components/sensor/dht.py homeassistant/components/sensor/dweet.py homeassistant/components/sensor/efergy.py @@ -126,10 +134,12 @@ omit = homeassistant/components/sensor/forecast.py homeassistant/components/sensor/glances.py homeassistant/components/sensor/netatmo.py + homeassistant/components/sensor/neurio_energy.py homeassistant/components/sensor/onewire.py homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/rest.py homeassistant/components/sensor/sabnzbd.py + homeassistant/components/sensor/steam_online.py homeassistant/components/sensor/speedtest.py homeassistant/components/sensor/swiss_public_transport.py homeassistant/components/sensor/systemmonitor.py @@ -141,15 +151,14 @@ omit = homeassistant/components/sensor/worldclock.py homeassistant/components/switch/arest.py homeassistant/components/switch/edimax.py + homeassistant/components/switch/dlink.py homeassistant/components/switch/hikvisioncam.py homeassistant/components/switch/mystrom.py homeassistant/components/switch/orvibo.py homeassistant/components/switch/rest.py homeassistant/components/switch/transmission.py - homeassistant/components/switch/wemo.py homeassistant/components/thermostat/heatmiser.py homeassistant/components/thermostat/homematic.py - homeassistant/components/thermostat/honeywell.py homeassistant/components/thermostat/proliphix.py homeassistant/components/thermostat/radiotherm.py diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000000..7b5a0f1980f --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,31 @@ +**Home Assistant release (`hass --version`):** + + +**Python release (`python3 --version`):** + + +**Component/platform:** + + +**Description of problem:** + + +**Expected:** + + +**Problem-relevant `configuration.yaml` entries and steps to reproduce:** +```yaml + +``` + +1. +2. +3. + +**Traceback (if applicable):** +```bash + +``` + +**Additional info:** + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..7f63274df03 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,28 @@ +**Description:** + + +**Related issue (if applicable):** # +**Example entry for `configuration.yaml` (if applicable):** +```yaml + +``` + +**Checklist:** + +- [ ] Local tests with `tox` ran successfully. +- [ ] No CI failures. **Your PR cannot be merged unless CI is green!** +- [ ] [Fork is up to date][fork] and was rebased on the `dev` branch before creating the PR. +- If code communicates with devices: + - [ ] 3rd party library/libraries for communication is/are added as dependencies via the `REQUIREMENTS` variable ([example][ex-requir]). + - [ ] 3rd party dependencies are imported inside functions that use them ([example][ex-import]). + - [ ] `requirements_all.txt` is up-to-date, `script/gen_requirements_all.py` ran and the updated file is included in the PR. + - [ ] New files were added to `.coveragerc`. +- If the code does not depend on external Python module: + - [ ] Tests to verify that the code works are included. +- [ ] [Commits will be squashed][squash] when the PR is ready to be merged. + +[fork]: http://stackoverflow.com/a/7244456 +[squash]: https://github.com/ginatrapani/todo.txt-android/wiki/Squash-All-Commits-Related-to-a-Single-Issue-into-a-Single-Commit +[ex-requir]: https://github.com/balloob/home-assistant/blob/dev/homeassistant/components/keyboard.py#L16 +[ex-import]: https://github.com/balloob/home-assistant/blob/dev/homeassistant/components/keyboard.py#L51 + diff --git a/.travis.yml b/.travis.yml index c01b0750360..3e1c8869d8f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,19 @@ sudo: false -language: python +matrix: + fast_finish: true + include: + - python: "3.4" + env: TOXENV=py34 + - python: "3.4" + env: TOXENV=requirements + - python: "3.5" + env: TOXENV=lint + - python: "3.5" + env: TOXENV=py35 cache: directories: - $HOME/.cache/pip - # - "$HOME/virtualenv/python$TRAVIS_PYTHON_VERSION" -python: - - 3.4 - - 3.5 -install: - - "true" -script: - - script/cibuild -matrix: - fast_finish: true +install: pip install -U tox coveralls +language: python +script: tox +after_success: coveralls diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1606149a1c7..722fcd1992a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ The process is straight-forward. - Fork the Home Assistant [git repository](https://github.com/balloob/home-assistant). - Write the code for your device, notification service, sensor, or IoT thing. - - Check it with ``pylint`` and ``flake8``. + - Ensure tests work. - Create a Pull Request against the [**dev**](https://github.com/balloob/home-assistant/tree/dev) branch of Home Assistant. Still interested? Then you should read the next sections and get more details. @@ -17,12 +17,13 @@ For help on building your component, please see the [developer documentation](ht After you finish adding support for your device: + - Check that all dependencies are included via the `REQUIREMENTS` variable in your platform/component and only imported inside functions that use them. - Add any new dependencies to `requirements_all.txt` if needed. Use `script/gen_requirements_all.py`. - - Update the `.coveragerc` file to exclude your platform if there are no tests available. + - Update the `.coveragerc` file to exclude your platform if there are no tests available or your new code uses a 3rd party library for communication with the device/service/sensor. - Provide some documentation for [home-assistant.io](https://home-assistant.io/). It's OK to just add a docstring with configuration details (sample entry for `configuration.yaml` file and alike) to the file header as a start. Visit the [website documentation](https://home-assistant.io/developers/website/) for further information on contributing to [home-assistant.io](https://github.com/balloob/home-assistant.io). - - Make sure all your code passes ``pylint`` and ``flake8`` (PEP8 and some more) validation. To check your repository, run `./script/lint`. + - Make sure all your code passes ``pylint`` and ``flake8`` (PEP8 and some more) validation. To check your repository, run `tox` or `script/lint`. - Create a Pull Request against the [**dev**](https://github.com/balloob/home-assistant/tree/dev) branch of Home Assistant. - - Check for comments and suggestions on your Pull Request and keep an eye on the [Travis output](https://travis-ci.org/balloob/home-assistant/). + - Check for comments and suggestions on your Pull Request and keep an eye on the [CI output](https://travis-ci.org/balloob/home-assistant/). If you add a platform for an existing component, there is usually no need for updating the frontend. Only if you've added a new component that should show up in the frontend, there are more steps needed: @@ -66,6 +67,21 @@ The frontend is composed of [Polymer](https://www.polymer-project.org) web-compo When you are done with development and ready to commit your changes, run `build_frontend`, set `development=0` in your config and validate that everything still works. +## Testing your code + +To test your code before submission, used the `tox` tool. + + ```shell + > pip install -U tox + > tox + ``` + +This will run unit tests against python 3.4 and 3.5 (if both are available locally), as well as run a set of tests which validate `pep8` and `pylint` style of the code. + +You can optionally run tests on only one tox target using the `-e` option to select an environment. + +For instance `tox -e lint` will run the linters only, `tox -e py34` will run unit tests only on python 3.4. + ### Notes on PyLint and PEP8 validation In case a PyLint warning cannot be avoided, add a comment to disable the PyLint check for that line. This can be done using the format `# pylint: disable=YOUR-ERROR-NAME`. Example of an unavoidable PyLint warning is if you do not use the passed in datetime if you're listening for time change. diff --git a/Dockerfile b/Dockerfile index a1f9d459295..0d41841f452 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,8 @@ VOLUME /config RUN mkdir -p /usr/src/app WORKDIR /usr/src/app +RUN pip3 install --no-cache-dir colorlog + # For the nmap tracker RUN apt-get update && \ apt-get install -y --no-install-recommends nmap net-tools && \ diff --git a/config/custom_components/example.py b/config/custom_components/example.py index 08b3f4c2a83..3f961b99569 100644 --- a/config/custom_components/example.py +++ b/config/custom_components/example.py @@ -1,6 +1,5 @@ """ -custom_components.example -~~~~~~~~~~~~~~~~~~~~~~~~~ +Example of a custom component. Example component to target an entity_id to: - turn it on at 7AM in the morning @@ -37,21 +36,21 @@ import homeassistant.components as core from homeassistant.components import device_tracker from homeassistant.components import light -# The domain of your component. Should be equal to the name of your component +# The domain of your component. Should be equal to the name of your component. DOMAIN = "example" -# List of component names (string) your component depends upon +# List of component names (string) your component depends upon. # We depend on group because group will be loaded after all the components that # initialize devices have been setup. DEPENDENCIES = ['group', 'device_tracker', 'light'] -# Configuration key for the entity id we are targetting +# Configuration key for the entity id we are targeting. CONF_TARGET = 'target' -# Variable for storing configuration parameters +# Variable for storing configuration parameters. TARGET_ID = None -# Name of the service that we expose +# Name of the service that we expose. SERVICE_FLASH = 'flash' # Shortcut for the logger @@ -59,16 +58,16 @@ _LOGGER = logging.getLogger(__name__) def setup(hass, config): - """ Setup example component. """ + """Setup example component.""" global TARGET_ID - # Validate that all required config options are given + # Validate that all required config options are given. if not validate_config(config, {DOMAIN: [CONF_TARGET]}, _LOGGER): return False TARGET_ID = config[DOMAIN][CONF_TARGET] - # Validate that the target entity id exists + # Validate that the target entity id exists. if hass.states.get(TARGET_ID) is None: _LOGGER.error("Target entity id %s does not exist", TARGET_ID) @@ -78,13 +77,13 @@ def setup(hass, config): TARGET_ID = None return False - # Tell the bootstrapper that we initialized successfully + # Tell the bootstrapper that we initialized successfully. return True @track_state_change(device_tracker.ENTITY_ID_ALL_DEVICES) def track_devices(hass, entity_id, old_state, new_state): - """ Called when the group.all devices change state. """ + """Called when the group.all devices change state.""" # If the target id is not set, return if not TARGET_ID: return @@ -94,7 +93,7 @@ def track_devices(hass, entity_id, old_state, new_state): core.turn_on(hass, TARGET_ID) - # If all people leave the house and the entity is on, turn it off + # If all people leave the house and the entity is on, turn it off. elif new_state.state == STATE_NOT_HOME and core.is_on(hass, TARGET_ID): core.turn_off(hass, TARGET_ID) @@ -116,7 +115,7 @@ def wake_up(hass, now): @track_state_change(light.ENTITY_ID_ALL_LIGHTS, STATE_ON, STATE_OFF) def all_lights_off(hass, entity_id, old_state, new_state): - """ If all lights turn off, turn off. """ + """If all lights turn off, turn off.""" if not TARGET_ID: return diff --git a/config/custom_components/hello_world.py b/config/custom_components/hello_world.py index a3d4ce762bb..f24971a1462 100644 --- a/config/custom_components/hello_world.py +++ b/config/custom_components/hello_world.py @@ -1,7 +1,7 @@ """ -custom_components.hello_world -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Implements the bare minimum that a component should implement. +The "hello world" custom component. + +This component implements the bare minimum that a component should implement. Configuration: @@ -11,18 +11,18 @@ configuration.yaml file. hello_world: """ -# The domain of your component. Should be equal to the name of your component +# The domain of your component. Should be equal to the name of your component. DOMAIN = "hello_world" -# List of component names (string) your component depends upon +# List of component names (string) your component depends upon. DEPENDENCIES = [] def setup(hass, config): - """ Setup our skeleton component. """ + """Setup our skeleton component.""" - # States are in the format DOMAIN.OBJECT_ID + # States are in the format DOMAIN.OBJECT_ID. hass.states.set('hello_world.Hello_World', 'Works!') - # return boolean to indicate that initialization was successful + # Return boolean to indicate that initialization was successfully. return True diff --git a/config/custom_components/mqtt_example.py b/config/custom_components/mqtt_example.py index 98e16b6bfa9..b4e5e89a977 100644 --- a/config/custom_components/mqtt_example.py +++ b/config/custom_components/mqtt_example.py @@ -1,6 +1,6 @@ """ -custom_components.mqtt_example -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Example of a custom MQTT component. + Shows how to communicate with MQTT. Follows a topic on MQTT and updates the state of an entity to the last message received on that topic. @@ -15,45 +15,41 @@ configuration.yaml file. mqtt_example: topic: home-assistant/mqtt_example - """ import homeassistant.loader as loader -# The domain of your component. Should be equal to the name of your component +# The domain of your component. Should be equal to the name of your component. DOMAIN = "mqtt_example" -# List of component names (string) your component depends upon +# List of component names (string) your component depends upon. DEPENDENCIES = ['mqtt'] - CONF_TOPIC = 'topic' DEFAULT_TOPIC = 'home-assistant/mqtt_example' def setup(hass, config): - """ Setup our mqtt_example component. """ + """Setup the MQTT example component.""" mqtt = loader.get_component('mqtt') topic = config[DOMAIN].get('topic', DEFAULT_TOPIC) entity_id = 'mqtt_example.last_message' - # Listen to a message on MQTT - + # Listen to a message on MQTT. def message_received(topic, payload, qos): - """ A new MQTT message has been received. """ + """A new MQTT message has been received.""" hass.states.set(entity_id, payload) mqtt.subscribe(hass, topic, message_received) hass.states.set(entity_id, 'No messages') - # Service to publish a message on MQTT - + # Service to publish a message on MQTT. def set_state_service(call): - """ Service to send a message. """ + """Service to send a message.""" mqtt.publish(hass, topic, call.data.get('new_state')) - # Register our service with Home Assistant + # Register our service with Home Assistant. hass.services.register(DOMAIN, 'set_state', set_state_service) - # return boolean to indicate that initialization was successful + # Return boolean to indicate that initialization was successfully. return True diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 5351bcf7983..f35e0c1c1f0 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -1,18 +1,18 @@ """ Starts home assistant. """ from __future__ import print_function -from multiprocessing import Process +import argparse +import os import signal import sys import threading -import os -import argparse import time +from multiprocessing import Process -from homeassistant import bootstrap import homeassistant.config as config_util -from homeassistant.const import (__version__, EVENT_HOMEASSISTANT_START, - RESTART_EXIT_CODE) +from homeassistant import bootstrap +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, RESTART_EXIT_CODE, __version__) def validate_python(): diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 3f88c6f4388..9db3f79e498 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -1,37 +1,31 @@ -""" -homeassistant.bootstrap -~~~~~~~~~~~~~~~~~~~~~~~ -Provides methods to bootstrap a home assistant instance. +"""Provides methods to bootstrap a home assistant instance.""" -Each method will return a tuple (bus, statemachine). - -After bootstrapping you can add your own components or -start by calling homeassistant.start_home_assistant(bus) -""" - -from collections import defaultdict import logging import logging.handlers import os import shutil import sys +from collections import defaultdict +from threading import RLock -import homeassistant.core as core -import homeassistant.util.dt as date_util -import homeassistant.util.package as pkg_util -import homeassistant.util.location as loc_util -import homeassistant.config as config_util -import homeassistant.loader as loader import homeassistant.components as core_components import homeassistant.components.group as group +import homeassistant.config as config_util +import homeassistant.core as core +import homeassistant.loader as loader +import homeassistant.util.dt as date_util +import homeassistant.util.location as loc_util +import homeassistant.util.package as pkg_util +from homeassistant.const import ( + CONF_CUSTOMIZE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, + CONF_TEMPERATURE_UNIT, CONF_TIME_ZONE, EVENT_COMPONENT_LOADED, + TEMP_CELCIUS, TEMP_FAHRENHEIT, __version__) from homeassistant.helpers import event_decorators, service from homeassistant.helpers.entity import Entity -from homeassistant.const import ( - __version__, EVENT_COMPONENT_LOADED, CONF_LATITUDE, CONF_LONGITUDE, - CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_TIME_ZONE, CONF_CUSTOMIZE, - TEMP_CELCIUS, TEMP_FAHRENHEIT) _LOGGER = logging.getLogger(__name__) +_SETUP_LOCK = RLock() +_CURRENT_SETUP = [] ATTR_COMPONENT = 'component' @@ -78,42 +72,57 @@ def _handle_requirements(hass, component, name): def _setup_component(hass, domain, config): - """ Setup a component for Home Assistant. """ + """Setup a component for Home Assistant.""" + # pylint: disable=too-many-return-statements if domain in hass.config.components: return True - component = loader.get_component(domain) - missing_deps = [dep for dep in getattr(component, 'DEPENDENCIES', []) - if dep not in hass.config.components] + with _SETUP_LOCK: + # It might have been loaded while waiting for lock + if domain in hass.config.components: + return True - if missing_deps: - _LOGGER.error( - 'Not initializing %s because not all dependencies loaded: %s', - domain, ", ".join(missing_deps)) - return False - - if not _handle_requirements(hass, component, domain): - return False - - try: - if not component.setup(hass, config): - _LOGGER.error('component %s failed to initialize', domain) + if domain in _CURRENT_SETUP: + _LOGGER.error('Attempt made to setup %s during setup of %s', + domain, domain) return False - except Exception: # pylint: disable=broad-except - _LOGGER.exception('Error during setup of component %s', domain) - return False - hass.config.components.append(component.DOMAIN) + component = loader.get_component(domain) + missing_deps = [dep for dep in getattr(component, 'DEPENDENCIES', []) + if dep not in hass.config.components] - # Assumption: if a component does not depend on groups - # it communicates with devices - if group.DOMAIN not in getattr(component, 'DEPENDENCIES', []): - hass.pool.add_worker() + if missing_deps: + _LOGGER.error( + 'Not initializing %s because not all dependencies loaded: %s', + domain, ", ".join(missing_deps)) + return False - hass.bus.fire( - EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN}) + if not _handle_requirements(hass, component, domain): + return False - return True + _CURRENT_SETUP.append(domain) + + try: + if not component.setup(hass, config): + _LOGGER.error('component %s failed to initialize', domain) + return False + except Exception: # pylint: disable=broad-except + _LOGGER.exception('Error during setup of component %s', domain) + return False + finally: + _CURRENT_SETUP.remove(domain) + + hass.config.components.append(component.DOMAIN) + + # Assumption: if a component does not depend on groups + # it communicates with devices + if group.DOMAIN not in getattr(component, 'DEPENDENCIES', []): + hass.pool.add_worker() + + hass.bus.fire( + EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN}) + + return True def prepare_setup_platform(hass, config, domain, platform_name): diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 310a65c6184..840350d231d 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -21,7 +21,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' # Maps discovered services to their platforms DISCOVERY_PLATFORMS = { - verisure.DISCOVER_SENSORS: 'verisure' + verisure.DISCOVER_ALARMS: 'verisure' } SERVICE_TO_METHOD = { diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py index da74c02da54..b563d57a686 100644 --- a/homeassistant/components/alarm_control_panel/alarmdotcom.py +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -9,18 +9,16 @@ https://home-assistant.io/components/alarm_control_panel.alarmdotcom/ import logging import homeassistant.components.alarm_control_panel as alarm -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD - from homeassistant.const import ( - STATE_UNKNOWN, - STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) + CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN) _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['https://github.com/Xorso/pyalarmdotcom' - '/archive/0.0.7.zip' - '#pyalarmdotcom==0.0.7'] + '/archive/0.1.1.zip' + '#pyalarmdotcom==0.1.1'] DEFAULT_NAME = 'Alarm.com' diff --git a/homeassistant/components/alarm_control_panel/demo.py b/homeassistant/components/alarm_control_panel/demo.py index 0ace53167de..9ac98924b2b 100644 --- a/homeassistant/components/alarm_control_panel/demo.py +++ b/homeassistant/components/alarm_control_panel/demo.py @@ -1,13 +1,14 @@ """ -homeassistant.components.alarm_control_panel.demo -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Demo platform that has two fake alarm control panels. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/demo/ """ import homeassistant.components.alarm_control_panel.manual as manual def setup_platform(hass, config, add_devices, discovery_info=None): - """ Sets up the Demo alarm control panels. """ + """Setup the Demo alarm control panel platform.""" add_devices([ manual.ManualAlarm(hass, 'Alarm', '1234', 5, 10), ]) diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index 2658e005aea..a6e280d1dd1 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -6,15 +6,15 @@ Support for manual alarms. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.manual/ """ -import logging import datetime -import homeassistant.components.alarm_control_panel as alarm -from homeassistant.helpers.event import track_point_in_time -import homeassistant.util.dt as dt_util +import logging +import homeassistant.components.alarm_control_panel as alarm +import homeassistant.util.dt as dt_util from homeassistant.const import ( - STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED) +from homeassistant.helpers.event import track_point_in_time _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index 168b220db1a..7ba6ba057ed 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -7,11 +7,11 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.mqtt/ """ import logging -import homeassistant.components.mqtt as mqtt -import homeassistant.components.alarm_control_panel as alarm +import homeassistant.components.alarm_control_panel as alarm +import homeassistant.components.mqtt as mqtt from homeassistant.const import ( - STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/alarm_control_panel/nx584.py b/homeassistant/components/alarm_control_panel/nx584.py index 55ad9b25b9f..e696ec682b2 100644 --- a/homeassistant/components/alarm_control_panel/nx584.py +++ b/homeassistant/components/alarm_control_panel/nx584.py @@ -7,14 +7,15 @@ 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 +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, + STATE_UNKNOWN) -REQUIREMENTS = ['pynx584==0.1'] +REQUIREMENTS = ['pynx584==0.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/alarm_control_panel/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py index ecedd163d0b..d89992eafed 100644 --- a/homeassistant/components/alarm_control_panel/verisure.py +++ b/homeassistant/components/alarm_control_panel/verisure.py @@ -8,12 +8,12 @@ https://home-assistant.io/components/verisure/ """ import logging -import homeassistant.components.verisure as verisure import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.verisure import HUB as hub from homeassistant.const import ( - STATE_UNKNOWN, - STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, + STATE_UNKNOWN) _LOGGER = logging.getLogger(__name__) @@ -21,18 +21,13 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the Verisure platform. """ - if not verisure.MY_PAGES: - _LOGGER.error('A connection has not been made to Verisure mypages.') - return False - alarms = [] - - alarms.extend([ - VerisureAlarm(value) - for value in verisure.ALARM_STATUS.values() - if verisure.SHOW_ALARM - ]) - + if int(hub.config.get('alarm', '1')): + hub.update_alarms() + alarms.extend([ + VerisureAlarm(value.id) + for value in hub.alarm_status.values() + ]) add_devices(alarms) @@ -40,9 +35,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class VerisureAlarm(alarm.AlarmControlPanel): """ Represents a Verisure alarm status. """ - def __init__(self, alarm_status): - self._id = alarm_status.id + def __init__(self, device_id): + self._id = device_id self._state = STATE_UNKNOWN + self._digits = int(hub.config.get('code_digits', '4')) @property def name(self): @@ -56,41 +52,41 @@ class VerisureAlarm(alarm.AlarmControlPanel): @property def code_format(self): - """ Four digit code required. """ - return '^\\d{%s}$' % verisure.CODE_DIGITS + """ code format as regex """ + return '^\\d{%s}$' % self._digits def update(self): """ Update alarm status """ - verisure.update_alarm() + hub.update_alarms() - if verisure.ALARM_STATUS[self._id].status == 'unarmed': + if hub.alarm_status[self._id].status == 'unarmed': self._state = STATE_ALARM_DISARMED - elif verisure.ALARM_STATUS[self._id].status == 'armedhome': + elif hub.alarm_status[self._id].status == 'armedhome': self._state = STATE_ALARM_ARMED_HOME - elif verisure.ALARM_STATUS[self._id].status == 'armed': + elif hub.alarm_status[self._id].status == 'armed': self._state = STATE_ALARM_ARMED_AWAY - elif verisure.ALARM_STATUS[self._id].status != 'pending': + elif hub.alarm_status[self._id].status != 'pending': _LOGGER.error( 'Unknown alarm state %s', - verisure.ALARM_STATUS[self._id].status) + hub.alarm_status[self._id].status) def alarm_disarm(self, code=None): """ Send disarm command. """ - verisure.MY_PAGES.alarm.set(code, 'DISARMED') + hub.my_pages.alarm.set(code, 'DISARMED') _LOGGER.info('verisure alarm disarming') - verisure.MY_PAGES.alarm.wait_while_pending() - verisure.update_alarm() + hub.my_pages.alarm.wait_while_pending() + self.update() def alarm_arm_home(self, code=None): """ Send arm home command. """ - verisure.MY_PAGES.alarm.set(code, 'ARMED_HOME') + hub.my_pages.alarm.set(code, 'ARMED_HOME') _LOGGER.info('verisure alarm arming home') - verisure.MY_PAGES.alarm.wait_while_pending() - verisure.update_alarm() + hub.my_pages.alarm.wait_while_pending() + self.update() def alarm_arm_away(self, code=None): """ Send arm away command. """ - verisure.MY_PAGES.alarm.set(code, 'ARMED_AWAY') + hub.my_pages.alarm.set(code, 'ARMED_AWAY') _LOGGER.info('verisure alarm arming away') - verisure.MY_PAGES.alarm.wait_while_pending() - verisure.update_alarm() + hub.my_pages.alarm.wait_while_pending() + self.update() diff --git a/homeassistant/components/alexa.py b/homeassistant/components/alexa.py index 66ac9de0b43..65d26a2360e 100644 --- a/homeassistant/components/alexa.py +++ b/homeassistant/components/alexa.py @@ -1,7 +1,5 @@ """ -components.alexa -~~~~~~~~~~~~~~~~ -Component to offer a service end point for an Alexa skill. +Support for Alexa skill service end point. For more details about this component, please refer to the documentation at https://home-assistant.io/components/alexa/ @@ -10,8 +8,8 @@ import enum import logging from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY -from homeassistant.util import template from homeassistant.helpers.service import call_from_config +from homeassistant.helpers import template DOMAIN = 'alexa' DEPENDENCIES = ['http'] @@ -28,7 +26,7 @@ CONF_ACTION = 'action' def setup(hass, config): - """ Activate Alexa component. """ + """Activate Alexa component.""" _CONFIG.update(config[DOMAIN].get(CONF_INTENTS, {})) hass.http.register_path('POST', API_ENDPOINT, _handle_alexa, True) @@ -37,7 +35,7 @@ def setup(hass, config): def _handle_alexa(handler, path_match, data): - """ Handle Alexa. """ + """Handle Alexa.""" _LOGGER.debug('Received Alexa request: %s', data) req = data.get('request') @@ -99,19 +97,19 @@ def _handle_alexa(handler, path_match, data): class SpeechType(enum.Enum): - """ Alexa speech types. """ + """Alexa speech types.""" plaintext = "PlainText" ssml = "SSML" class CardType(enum.Enum): - """ Alexa card types. """ + """Alexa card types.""" simple = "Simple" link_account = "LinkAccount" class AlexaResponse(object): - """ Helps generating the response for Alexa. """ + """Helps generating the response for Alexa.""" def __init__(self, hass, intent=None): self.hass = hass @@ -154,7 +152,7 @@ class AlexaResponse(object): } def add_reprompt(self, speech_type, text): - """ Add repromopt if user does not answer. """ + """Add reprompt if user does not answer.""" assert self.reprompt is None key = 'ssml' if speech_type == SpeechType.ssml else 'text' @@ -165,7 +163,7 @@ class AlexaResponse(object): } def as_dict(self): - """ Returns response in an Alexa valid dict. """ + """Returns response in an Alexa valid dict.""" response = { 'shouldEndSession': self.should_end_session } @@ -188,5 +186,5 @@ class AlexaResponse(object): } def _render(self, template_string): - """ Render a response, adding data from intent if available. """ + """Render a response, adding data from intent if available.""" return template.render(self.hass, template_string, self.variables) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index 11f1826549e..6f785f19896 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -1,31 +1,27 @@ """ -homeassistant.components.api -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Provides a Rest API for Home Assistant. +Rest API for Home Assistant. For more details about the RESTful API, please refer to the documentation at https://home-assistant.io/developers/api/ """ -import re -import logging -import threading import json +import logging +import re +import threading import homeassistant.core as ha -from homeassistant.exceptions import TemplateError -from homeassistant.helpers.state import TrackStates import homeassistant.remote as rem -from homeassistant.util import template from homeassistant.bootstrap import ERROR_LOG_FILENAME from homeassistant.const import ( - URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES, URL_API_STREAM, - URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, URL_API_COMPONENTS, - URL_API_CONFIG, URL_API_BOOTSTRAP, URL_API_ERROR_LOG, URL_API_LOG_OUT, - URL_API_TEMPLATE, EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, MATCH_ALL, - HTTP_OK, HTTP_CREATED, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, - HTTP_UNPROCESSABLE_ENTITY, HTTP_HEADER_CONTENT_TYPE, - CONTENT_TYPE_TEXT_PLAIN) - + CONTENT_TYPE_TEXT_PLAIN, EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, + HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_HEADER_CONTENT_TYPE, HTTP_NOT_FOUND, + HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, MATCH_ALL, URL_API, URL_API_COMPONENTS, + URL_API_CONFIG, URL_API_ERROR_LOG, URL_API_EVENT_FORWARD, URL_API_EVENTS, + URL_API_LOG_OUT, URL_API_SERVICES, URL_API_STATES, URL_API_STATES_ENTITY, + URL_API_STREAM, URL_API_TEMPLATE) +from homeassistant.exceptions import TemplateError +from homeassistant.helpers.state import TrackStates +from homeassistant.helpers import template DOMAIN = 'api' DEPENDENCIES = ['http'] @@ -37,7 +33,7 @@ _LOGGER = logging.getLogger(__name__) def setup(hass, config): - """ Register the API with the HTTP interface. """ + """Register the API with the HTTP interface.""" # /api - for validation purposes hass.http.register_path('GET', URL_API, _handle_get_api) @@ -48,10 +44,6 @@ def setup(hass, config): # /api/config hass.http.register_path('GET', URL_API_CONFIG, _handle_get_api_config) - # /api/bootstrap - hass.http.register_path( - 'GET', URL_API_BOOTSTRAP, _handle_get_api_bootstrap) - # /states hass.http.register_path('GET', URL_API_STATES, _handle_get_api_states) hass.http.register_path( @@ -63,6 +55,9 @@ def setup(hass, config): hass.http.register_path( 'PUT', re.compile(r'/api/states/(?P[a-zA-Z\._0-9]+)'), _handle_post_state_entity) + hass.http.register_path( + 'DELETE', re.compile(r'/api/states/(?P[a-zA-Z\._0-9]+)'), + _handle_delete_state_entity) # /events hass.http.register_path('GET', URL_API_EVENTS, _handle_get_api_events) @@ -101,12 +96,12 @@ def setup(hass, config): def _handle_get_api(handler, path_match, data): - """ Renders the debug interface. """ + """Renders the debug interface.""" handler.write_json_message("API running.") def _handle_get_api_stream(handler, path_match, data): - """ Provide a streaming interface for the event bus. """ + """Provide a streaming interface for the event bus.""" gracefully_closed = False hass = handler.server.hass wfile = handler.wfile @@ -119,7 +114,7 @@ def _handle_get_api_stream(handler, path_match, data): restrict = restrict.split(',') def write_message(payload): - """ Writes a message to the output. """ + """Writes a message to the output.""" with write_lock: msg = "data: {}\n\n".format(payload) @@ -132,7 +127,7 @@ def _handle_get_api_stream(handler, path_match, data): block.set() def forward_events(event): - """ Forwards events to the open request. """ + """Forwards events to the open request.""" nonlocal gracefully_closed if block.is_set() or event.event_type == EVENT_TIME_CHANGED: @@ -176,29 +171,17 @@ def _handle_get_api_stream(handler, path_match, data): def _handle_get_api_config(handler, path_match, data): - """ Returns the Home Assistant config. """ + """Returns the Home Assistant configuration.""" handler.write_json(handler.server.hass.config.as_dict()) -def _handle_get_api_bootstrap(handler, path_match, data): - """ Returns all data needed to bootstrap Home Assistant. """ - hass = handler.server.hass - - handler.write_json({ - 'config': hass.config.as_dict(), - 'states': hass.states.all(), - 'events': _events_json(hass), - 'services': _services_json(hass), - }) - - def _handle_get_api_states(handler, path_match, data): - """ Returns a dict containing all entity ids and their state. """ + """Returns a dict containing all entity ids and their state.""" handler.write_json(handler.server.hass.states.all()) def _handle_get_api_states_entity(handler, path_match, data): - """ Returns the state of a specific entity. """ + """Returns the state of a specific entity.""" entity_id = path_match.group('entity_id') state = handler.server.hass.states.get(entity_id) @@ -210,7 +193,7 @@ def _handle_get_api_states_entity(handler, path_match, data): def _handle_post_state_entity(handler, path_match, data): - """ Handles updating the state of an entity. + """Handles updating the state of an entity. This handles the following paths: /api/states/ @@ -240,13 +223,29 @@ def _handle_post_state_entity(handler, path_match, data): location=URL_API_STATES_ENTITY.format(entity_id)) +def _handle_delete_state_entity(handler, path_match, data): + """Handle request to delete an entity from state machine. + + This handles the following paths: + /api/states/ + """ + entity_id = path_match.group('entity_id') + + if handler.server.hass.states.remove(entity_id): + handler.write_json_message( + "Entity not found", HTTP_NOT_FOUND) + else: + handler.write_json_message( + "Entity removed", HTTP_OK) + + def _handle_get_api_events(handler, path_match, data): - """ Handles getting overview of event listeners. """ - handler.write_json(_events_json(handler.server.hass)) + """Handles getting overview of event listeners.""" + handler.write_json(events_json(handler.server.hass)) def _handle_api_post_events_event(handler, path_match, event_data): - """ Handles firing of an event. + """Handles firing of an event. This handles the following paths: /api/events/ @@ -258,6 +257,7 @@ def _handle_api_post_events_event(handler, path_match, event_data): if event_data is not None and not isinstance(event_data, dict): handler.write_json_message( "event_data should be an object", HTTP_UNPROCESSABLE_ENTITY) + return event_origin = ha.EventOrigin.remote @@ -276,13 +276,13 @@ def _handle_api_post_events_event(handler, path_match, event_data): def _handle_get_api_services(handler, path_match, data): - """ Handles getting overview of services. """ - handler.write_json(_services_json(handler.server.hass)) + """Handles getting overview of services.""" + handler.write_json(services_json(handler.server.hass)) # pylint: disable=invalid-name def _handle_post_api_services_domain_service(handler, path_match, data): - """ Handles calling a service. + """Handles calling a service. This handles the following paths: /api/services// @@ -298,8 +298,7 @@ def _handle_post_api_services_domain_service(handler, path_match, data): # pylint: disable=invalid-name def _handle_post_api_event_forward(handler, path_match, data): - """ Handles adding an event forwarding target. """ - + """Handles adding an event forwarding target.""" try: host = data['host'] api_password = data['api_password'] @@ -332,8 +331,7 @@ def _handle_post_api_event_forward(handler, path_match, data): def _handle_delete_api_event_forward(handler, path_match, data): - """ Handles deleting an event forwarding target. """ - + """Handles deleting an event forwarding target.""" try: host = data['host'] except KeyError: @@ -356,26 +354,25 @@ def _handle_delete_api_event_forward(handler, path_match, data): def _handle_get_api_components(handler, path_match, data): - """ Returns all the loaded components. """ - + """Returns all the loaded components.""" handler.write_json(handler.server.hass.config.components) def _handle_get_api_error_log(handler, path_match, data): - """ Returns the logged errors for this session. """ + """Returns the logged errors for this session.""" handler.write_file(handler.server.hass.config.path(ERROR_LOG_FILENAME), False) def _handle_post_api_log_out(handler, path_match, data): - """ Log user out. """ + """Log user out.""" handler.send_response(HTTP_OK) handler.destroy_session() handler.end_headers() def _handle_post_api_template(handler, path_match, data): - """ Log user out. """ + """Log user out.""" template_string = data.get('template', '') try: @@ -390,13 +387,13 @@ def _handle_post_api_template(handler, path_match, data): return -def _services_json(hass): - """ Generate services data to JSONify. """ +def services_json(hass): + """Generate services data to JSONify.""" return [{"domain": key, "services": value} for key, value in hass.services.services.items()] -def _events_json(hass): - """ Generate event data to JSONify. """ +def events_json(hass): + """Generate event data to JSONify.""" return [{"event": key, "listener_count": value} for key, value in hass.bus.listeners.items()] diff --git a/homeassistant/components/arduino.py b/homeassistant/components/arduino.py index f7f6240b44e..05db57394dd 100644 --- a/homeassistant/components/arduino.py +++ b/homeassistant/components/arduino.py @@ -9,9 +9,9 @@ https://home-assistant.io/components/arduino/ """ import logging +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers import validate_config -from homeassistant.const import (EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP) DOMAIN = "arduino" REQUIREMENTS = ['PyMata==2.07a'] diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 61e68aa8e8e..10c2402bb0e 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -6,13 +6,12 @@ Offers numeric state listening automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/components/automation/#numeric-state-trigger """ -from functools import partial import logging +from functools import partial from homeassistant.const import CONF_VALUE_TEMPLATE from homeassistant.helpers.event import track_state_change -from homeassistant.util import template - +from homeassistant.helpers import template CONF_ENTITY_ID = "entity_id" CONF_BELOW = "below" diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index bcf498f509a..b9c4164e584 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -7,15 +7,47 @@ For more details about this automation rule, please refer to the documentation at https://home-assistant.io/components/automation/#state-trigger """ import logging +from datetime import timedelta -from homeassistant.helpers.event import track_state_change -from homeassistant.const import MATCH_ALL +import homeassistant.util.dt as dt_util +from homeassistant.const import ( + EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL) +from homeassistant.components.automation.time import ( + CONF_HOURS, CONF_MINUTES, CONF_SECONDS) +from homeassistant.helpers.event import track_state_change, track_point_in_time CONF_ENTITY_ID = "entity_id" CONF_FROM = "from" CONF_TO = "to" CONF_STATE = "state" +CONF_FOR = "for" + + +def get_time_config(config): + """ Helper function to extract the time specified in the config """ + if CONF_FOR not in config: + return None + + hours = config[CONF_FOR].get(CONF_HOURS) + minutes = config[CONF_FOR].get(CONF_MINUTES) + seconds = config[CONF_FOR].get(CONF_SECONDS) + + if hours is None and minutes is None and seconds is None: + logging.getLogger(__name__).error( + "Received invalid value for '%s': %s", + config[CONF_FOR], CONF_FOR) + return None + + if config.get(CONF_TO) is None and config.get(CONF_STATE) is None: + logging.getLogger(__name__).error( + "For: requires a to: value'%s': %s", + config[CONF_FOR], CONF_FOR) + return None + + return timedelta(hours=(hours or 0.0), + minutes=(minutes or 0.0), + seconds=(seconds or 0.0)) def trigger(hass, config, action): @@ -25,19 +57,47 @@ def trigger(hass, config, action): if entity_id is None: logging.getLogger(__name__).error( "Missing trigger configuration key %s", CONF_ENTITY_ID) - return False + return None from_state = config.get(CONF_FROM, MATCH_ALL) to_state = config.get(CONF_TO) or config.get(CONF_STATE) or MATCH_ALL + time_delta = get_time_config(config) if isinstance(from_state, bool) or isinstance(to_state, bool): logging.getLogger(__name__).error( 'Config error. Surround to/from values with quotes.') - return False + return None + + if CONF_FOR in config and time_delta is None: + return None def state_automation_listener(entity, from_s, to_s): """ Listens for state changes and calls action. """ - action() + + def state_for_listener(now): + """ Fires on state changes after a delay and calls action. """ + hass.bus.remove_listener( + EVENT_STATE_CHANGED, for_state_listener) + action() + + def state_for_cancel_listener(entity, inner_from_s, inner_to_s): + """ Fires on state changes and cancels + for listener if state changed. """ + if inner_to_s == to_s: + return + hass.bus.remove_listener(EVENT_TIME_CHANGED, for_time_listener) + hass.bus.remove_listener( + EVENT_STATE_CHANGED, for_state_listener) + + if time_delta is not None: + target_tm = dt_util.utcnow() + time_delta + for_time_listener = track_point_in_time( + hass, state_for_listener, target_tm) + for_state_listener = track_state_change( + hass, entity_id, state_for_cancel_listener, + MATCH_ALL, MATCH_ALL) + else: + action() track_state_change( hass, entity_id, state_automation_listener, from_state, to_state) @@ -56,10 +116,18 @@ def if_action(hass, config): CONF_STATE) return None + time_delta = get_time_config(config) + if CONF_FOR in config and time_delta is None: + return None + state = str(state) def if_state(): """ Test if condition. """ - return hass.states.is_state(entity_id, state) + is_state = hass.states.is_state(entity_id, state) + return (time_delta is None and is_state or + time_delta is not None and + dt_util.utcnow() - time_delta > + hass.states.get(entity_id).last_changed) return if_state diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 6abb59eede6..9cd50fedbd9 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -9,9 +9,9 @@ at https://home-assistant.io/components/automation/#sun-trigger import logging from datetime import timedelta +import homeassistant.util.dt as dt_util from homeassistant.components import sun from homeassistant.helpers.event import track_sunrise, track_sunset -import homeassistant.util.dt as dt_util DEPENDENCIES = ['sun'] diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index 8615538c42a..4aaac359c46 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -10,7 +10,7 @@ import logging from homeassistant.const import CONF_VALUE_TEMPLATE, EVENT_STATE_CHANGED from homeassistant.exceptions import TemplateError -from homeassistant.util import template +from homeassistant.helpers import template _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index f0f800bd313..7dc551de32c 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -9,10 +9,9 @@ at https://home-assistant.io/components/automation/#zone-trigger import logging from homeassistant.components import zone -from homeassistant.helpers.event import track_state_change from homeassistant.const import ( ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, MATCH_ALL) - +from homeassistant.helpers.event import track_state_change CONF_ENTITY_ID = "entity_id" CONF_ZONE = "zone" diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index ccfd57aff8c..2fddef9ca3a 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -1,6 +1,4 @@ """ -homeassistant.components.binary_sensor -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Component to interface with binary sensors (sensors which only know two states) that can be monitored. @@ -12,17 +10,43 @@ import logging from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import Entity from homeassistant.const import (STATE_ON, STATE_OFF) +from homeassistant.components import (bloomsky, mysensors, zwave, wink) DOMAIN = 'binary_sensor' SCAN_INTERVAL = 30 ENTITY_ID_FORMAT = DOMAIN + '.{}' +SENSOR_CLASSES = [ + None, # Generic on/off + 'opening', # Door, window, etc + 'motion', # Motion sensor + 'gas', # CO, CO2, etc + 'smoke', # Smoke detector + 'moisture', # Specifically a wetness sensor + 'light', # Lightness threshold + 'power', # Power, over-current, etc + 'safety', # Generic on=unsafe, off=safe + 'heat', # On means hot (or too hot) + 'cold', # On means cold (or too cold) + 'moving', # On means moving, Off means stopped + 'sound', # On means sound detected, Off means no sound + 'vibration', # On means vibration detected, Off means no vibration +] + +# Maps discovered services to their platforms +DISCOVERY_PLATFORMS = { + bloomsky.DISCOVER_BINARY_SENSORS: 'bloomsky', + mysensors.DISCOVER_BINARY_SENSORS: 'mysensors', + zwave.DISCOVER_BINARY_SENSORS: 'zwave', + wink.DISCOVER_BINARY_SENSORS: 'wink' +} def setup(hass, config): - """ Track states and offer events for binary sensors. """ + """Track states and offer events for binary sensors.""" component = EntityComponent( - logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) + logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL, + DISCOVERY_PLATFORMS) component.setup(config) @@ -31,19 +55,29 @@ def setup(hass, config): # pylint: disable=no-self-use class BinarySensorDevice(Entity): - """ Represents a binary sensor. """ + """Represent a binary sensor.""" @property def is_on(self): - """ True if the binary sensor is on. """ + """Return True if the binary sensor is on.""" return None @property def state(self): - """ Returns the state of the binary sensor. """ + """Return the state of the binary sensor.""" return STATE_ON if self.is_on else STATE_OFF @property - def friendly_state(self): - """ Returns the friendly state of the binary sensor. """ + def sensor_class(self): + """Return the class of this sensor, from SENSOR_CLASSES.""" return None + + @property + def state_attributes(self): + """Return device specific state attributes.""" + attr = {} + + if self.sensor_class is not None: + attr['sensor_class'] = self.sensor_class + + return attr diff --git a/homeassistant/components/binary_sensor/apcupsd.py b/homeassistant/components/binary_sensor/apcupsd.py index 796a2a0df70..8a08ef3a6ac 100644 --- a/homeassistant/components/binary_sensor/apcupsd.py +++ b/homeassistant/components/binary_sensor/apcupsd.py @@ -1,25 +1,23 @@ """ -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 +from homeassistant.components.binary_sensor import BinarySensorDevice 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. """ + """Instantiate an OnlineStatus binary sensor entity.""" add_entities((OnlineStatus(config, apcupsd.DATA),)) class OnlineStatus(BinarySensorDevice): - """ Binary sensor to represent UPS online status. """ + """Binary sensor to represent UPS online status.""" def __init__(self, config, data): self._config = config self._data = data @@ -33,7 +31,7 @@ class OnlineStatus(BinarySensorDevice): @property def is_on(self): - """ True if the UPS is online, else False. """ + """True if the UPS is online, else False.""" return self._state == apcupsd.VALUE_ONLINE def update(self): diff --git a/homeassistant/components/binary_sensor/arest.py b/homeassistant/components/binary_sensor/arest.py index 7eafca9f2ae..b56d906b2e6 100644 --- a/homeassistant/components/binary_sensor/arest.py +++ b/homeassistant/components/binary_sensor/arest.py @@ -1,18 +1,16 @@ """ -homeassistant.components.binary_sensor.arest -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The arest sensor will consume an exposed aREST API of a device. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.arest/ """ -from datetime import timedelta import logging +from datetime import timedelta import requests -from homeassistant.util import Throttle from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -24,8 +22,7 @@ CONF_PIN = 'pin' def setup_platform(hass, config, add_devices, discovery_info=None): - """ Get the aREST binary sensor. """ - + """Get the aREST binary sensor.""" resource = config.get(CONF_RESOURCE) pin = config.get(CONF_PIN) @@ -56,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=too-many-instance-attributes, too-many-arguments class ArestBinarySensor(BinarySensorDevice): - """ Implements an aREST binary sensor for a pin. """ + """Implements an aREST binary sensor for a pin.""" def __init__(self, arest, resource, name, pin): self.arest = arest @@ -73,23 +70,22 @@ class ArestBinarySensor(BinarySensorDevice): @property def name(self): - """ The name of the binary sensor. """ + """The name of the binary sensor.""" return self._name @property def is_on(self): - """ True if the binary sensor is on. """ + """True if the binary sensor is on.""" return bool(self.arest.data.get('state')) def update(self): - """ Gets the latest data from aREST API. """ + """Gets the latest data from aREST API.""" self.arest.update() # pylint: disable=too-few-public-methods class ArestData(object): - """ Class for handling the data retrieval for pins. """ - + """Class for handling the data retrieval for pins.""" def __init__(self, resource, pin): self._resource = resource self._pin = pin @@ -97,7 +93,7 @@ class ArestData(object): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): - """ Gets the latest data from aREST device. """ + """Gets the latest data from aREST device.""" try: response = requests.get('{}/digital/{}'.format( self._resource, self._pin), timeout=10) diff --git a/homeassistant/components/binary_sensor/bloomsky.py b/homeassistant/components/binary_sensor/bloomsky.py new file mode 100644 index 00000000000..32ccad6df91 --- /dev/null +++ b/homeassistant/components/binary_sensor/bloomsky.py @@ -0,0 +1,74 @@ +""" +Support the binary sensors of a BloomSky weather station. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.bloomsky/ +""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.loader import get_component + +DEPENDENCIES = ["bloomsky"] + +# These are the available sensors mapped to binary_sensor class +SENSOR_TYPES = { + "Rain": "moisture", + "Night": None, +} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the available BloomSky weather binary sensors.""" + logger = logging.getLogger(__name__) + bloomsky = get_component('bloomsky') + sensors = config.get('monitored_conditions', SENSOR_TYPES) + + for device in bloomsky.BLOOMSKY.devices.values(): + for variable in sensors: + if variable in SENSOR_TYPES: + add_devices([BloomSkySensor(bloomsky.BLOOMSKY, + device, + variable)]) + else: + logger.error("Cannot find definition for device: %s", variable) + + +class BloomSkySensor(BinarySensorDevice): + """ Represents a single binary sensor in a BloomSky device. """ + + def __init__(self, bs, device, sensor_name): + """Initialize a BloomSky binary sensor.""" + self._bloomsky = bs + self._device_id = device["DeviceID"] + self._sensor_name = sensor_name + self._name = "{} {}".format(device["DeviceName"], sensor_name) + self._unique_id = "bloomsky_binary_sensor {}".format(self._name) + self.update() + + @property + def name(self): + """The name of the BloomSky device and this sensor.""" + return self._name + + @property + def unique_id(self): + """Unique ID for this sensor.""" + return self._unique_id + + @property + def sensor_class(self): + """Return the class of this sensor, from SENSOR_CLASSES.""" + return SENSOR_TYPES.get(self._sensor_name) + + @property + def is_on(self): + """If binary sensor is on.""" + return self._state + + def update(self): + """Request an update from the BloomSky API.""" + self._bloomsky.refresh_devices() + + self._state = \ + self._bloomsky.devices[self._device_id]["Data"][self._sensor_name] diff --git a/homeassistant/components/binary_sensor/command_sensor.py b/homeassistant/components/binary_sensor/command_line.py similarity index 77% rename from homeassistant/components/binary_sensor/command_sensor.py rename to homeassistant/components/binary_sensor/command_line.py index d69417a6a73..2d58951ae74 100644 --- a/homeassistant/components/binary_sensor/command_sensor.py +++ b/homeassistant/components/binary_sensor/command_line.py @@ -1,8 +1,6 @@ """ -homeassistant.components.binary_sensor.command_sensor -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Allows to configure custom shell commands to turn a value -into a logical value for a binary 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/ @@ -10,10 +8,10 @@ https://home-assistant.io/components/binary_sensor.command/ import logging from datetime import timedelta -from homeassistant.const import CONF_VALUE_TEMPLATE from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.sensor.command_sensor import CommandSensorData -from homeassistant.util import template +from homeassistant.components.sensor.command_line import CommandSensorData +from homeassistant.const import CONF_VALUE_TEMPLATE +from homeassistant.helpers import template _LOGGER = logging.getLogger(__name__) @@ -27,8 +25,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """ Add the Command Sensor. """ - + """Add the Command Sensor.""" if config.get('command') is None: _LOGGER.error('Missing required variable: "command"') return False @@ -47,8 +44,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=too-many-arguments class CommandBinarySensor(BinarySensorDevice): - """ Represents a binary sensor that is returning - a value of a shell commands. """ + """ + Represents a binary sensor that is returning a value of a shell commands. + """ def __init__(self, hass, data, name, payload_on, payload_off, value_template): self._hass = hass @@ -62,16 +60,16 @@ class CommandBinarySensor(BinarySensorDevice): @property def name(self): - """ The name of the sensor. """ + """The name of the sensor.""" return self._name @property def is_on(self): - """ True if the binary sensor is on. """ + """True if the binary sensor is on.""" return self._state def update(self): - """ Gets the latest data and updates the state. """ + """Gets the latest data and updates the state.""" self.data.update() value = self.data.value diff --git a/homeassistant/components/binary_sensor/demo.py b/homeassistant/components/binary_sensor/demo.py index 087d7405d9b..8a1a3ea30bd 100644 --- a/homeassistant/components/binary_sensor/demo.py +++ b/homeassistant/components/binary_sensor/demo.py @@ -1,37 +1,43 @@ """ -homeassistant.components.binary_sensor.demo -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Demo platform that has two fake binary sensors. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/demo/ """ from homeassistant.components.binary_sensor import BinarySensorDevice def setup_platform(hass, config, add_devices, discovery_info=None): - """ Sets up the Demo binary sensors. """ + """Setup the Demo binary sensor platform.""" add_devices([ - DemoBinarySensor('Basement Floor Wet', False), - DemoBinarySensor('Movement Backyard', True), + DemoBinarySensor('Basement Floor Wet', False, 'moisture'), + DemoBinarySensor('Movement Backyard', True, 'motion'), ]) class DemoBinarySensor(BinarySensorDevice): - """ A Demo binary sensor. """ - - def __init__(self, name, state): + """A Demo binary sensor.""" + def __init__(self, name, state, sensor_class): self._name = name self._state = state + self._sensor_type = sensor_class + + @property + def sensor_class(self): + """Return the class of this sensor.""" + return self._sensor_type @property def should_poll(self): - """ No polling needed for a demo binary sensor. """ + """No polling needed for a demo binary sensor.""" return False @property def name(self): - """ Returns the name of the binary sensor. """ + """Return the name of the binary sensor.""" return self._name @property def is_on(self): - """ True if the binary sensor is on. """ + """Return true if the binary sensor is on.""" return self._state diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index 916f1226c82..3712936e57e 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -1,6 +1,4 @@ """ -homeassistant.components.binary_sensor.mqtt -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Allows to configure a MQTT binary sensor. For more details about this platform, please refer to the documentation at @@ -8,10 +6,10 @@ https://home-assistant.io/components/binary_sensor.mqtt/ """ import logging -from homeassistant.const import CONF_VALUE_TEMPLATE -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.util import template import homeassistant.components.mqtt as mqtt +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.const import CONF_VALUE_TEMPLATE +from homeassistant.helpers import template _LOGGER = logging.getLogger(__name__) @@ -25,7 +23,7 @@ DEPENDENCIES = ['mqtt'] # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """ Add MQTT binary sensor. """ + """Add MQTT binary sensor.""" if config.get('state_topic') is None: _LOGGER.error('Missing required variable: state_topic') @@ -43,7 +41,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=too-many-arguments, too-many-instance-attributes class MqttBinarySensor(BinarySensorDevice): - """ Represents a binary sensor that is updated by MQTT. """ + """Represents a binary sensor that is updated by MQTT.""" def __init__(self, hass, name, state_topic, qos, payload_on, payload_off, value_template): self._hass = hass @@ -55,7 +53,7 @@ class MqttBinarySensor(BinarySensorDevice): self._qos = qos def message_received(topic, payload, qos): - """ A new MQTT message has been received. """ + """A new MQTT message has been received.""" if value_template is not None: payload = template.render_with_possible_json_value( hass, value_template, payload) @@ -70,15 +68,15 @@ class MqttBinarySensor(BinarySensorDevice): @property def should_poll(self): - """ No polling needed. """ + """No polling needed.""" return False @property def name(self): - """ The name of the binary sensor. """ + """The name of the binary sensor.""" return self._name @property def is_on(self): - """ True if the binary sensor is on. """ + """True if the binary sensor is on.""" return self._state diff --git a/homeassistant/components/binary_sensor/mysensors.py b/homeassistant/components/binary_sensor/mysensors.py new file mode 100644 index 00000000000..8d1b9eb2ea7 --- /dev/null +++ b/homeassistant/components/binary_sensor/mysensors.py @@ -0,0 +1,168 @@ +""" +Support for MySensors binary sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.mysensors/ +""" +import logging + +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON) +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, SENSOR_CLASSES) +from homeassistant.loader import get_component + +_LOGGER = logging.getLogger(__name__) +DEPENDENCIES = [] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the mysensors platform for sensors.""" + # Only act if loaded via mysensors by discovery event. + # Otherwise gateway is not setup. + if discovery_info is None: + return + + mysensors = get_component('mysensors') + + for gateway in mysensors.GATEWAYS.values(): + # Define the S_TYPES and V_TYPES that the platform should handle as + # states. Map them in a dict of lists. + pres = gateway.const.Presentation + set_req = gateway.const.SetReq + map_sv_types = { + pres.S_DOOR: [set_req.V_TRIPPED], + pres.S_MOTION: [set_req.V_TRIPPED], + pres.S_SMOKE: [set_req.V_TRIPPED], + } + if float(gateway.version) >= 1.5: + map_sv_types.update({ + pres.S_SPRINKLER: [set_req.V_TRIPPED], + pres.S_WATER_LEAK: [set_req.V_TRIPPED], + pres.S_SOUND: [set_req.V_TRIPPED], + pres.S_VIBRATION: [set_req.V_TRIPPED], + pres.S_MOISTURE: [set_req.V_TRIPPED], + }) + + devices = {} + gateway.platform_callbacks.append(mysensors.pf_callback_factory( + map_sv_types, devices, add_devices, MySensorsBinarySensor)) + + +class MySensorsBinarySensor(BinarySensorDevice): + """Represent the value of a MySensors child node.""" + + # pylint: disable=too-many-arguments,too-many-instance-attributes + + def __init__( + self, gateway, node_id, child_id, name, value_type, child_type): + """ + Setup class attributes on instantiation. + + Args: + gateway (GatewayWrapper): Gateway object. + node_id (str): Id of node. + child_id (str): Id of child. + name (str): Entity name. + value_type (str): Value type of child. Value is entity state. + child_type (str): Child type of child. + + Attributes: + gateway (GatewayWrapper): Gateway object. + node_id (str): Id of node. + child_id (str): Id of child. + _name (str): Entity name. + value_type (str): Value type of child. Value is entity state. + child_type (str): Child type of child. + battery_level (int): Node battery level. + _values (dict): Child values. Non state values set as state attributes. + mysensors (module): Mysensors main component module. + """ + self.gateway = gateway + self.node_id = node_id + self.child_id = child_id + self._name = name + self.value_type = value_type + self.child_type = child_type + self.battery_level = 0 + self._values = {} + self.mysensors = get_component('mysensors') + + @property + def should_poll(self): + """MySensor gateway pushes its state to HA.""" + return False + + @property + def name(self): + """The name of this entity.""" + return self._name + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + attr = { + self.mysensors.ATTR_PORT: self.gateway.port, + self.mysensors.ATTR_NODE_ID: self.node_id, + self.mysensors.ATTR_CHILD_ID: self.child_id, + ATTR_BATTERY_LEVEL: self.battery_level, + } + + set_req = self.gateway.const.SetReq + + for value_type, value in self._values.items(): + if value_type != self.value_type: + try: + attr[set_req(value_type).name] = value + except ValueError: + _LOGGER.error('value_type %s is not valid for mysensors ' + 'version %s', value_type, + self.gateway.version) + return attr + + @property + def is_on(self): + """Return True if the binary sensor is on.""" + if self.value_type in self._values: + return self._values[self.value_type] == STATE_ON + return False + + @property + def sensor_class(self): + """Return the class of this sensor, from SENSOR_CLASSES.""" + pres = self.gateway.const.Presentation + class_map = { + pres.S_DOOR: 'opening', + pres.S_MOTION: 'motion', + pres.S_SMOKE: 'smoke', + } + if float(self.gateway.version) >= 1.5: + class_map.update({ + pres.S_SPRINKLER: 'sprinkler', + pres.S_WATER_LEAK: 'leak', + pres.S_SOUND: 'sound', + pres.S_VIBRATION: 'vibration', + pres.S_MOISTURE: 'moisture', + }) + if class_map.get(self.child_type) in SENSOR_CLASSES: + return class_map.get(self.child_type) + + @property + def available(self): + """Return True if entity is available.""" + return self.value_type in self._values + + def update(self): + """Update the controller with the latest values from a sensor.""" + node = self.gateway.sensors[self.node_id] + child = node.children[self.child_id] + for value_type, value in child.values.items(): + _LOGGER.debug( + "%s: value_type %s, value = %s", self._name, value_type, value) + if value_type == self.gateway.const.SetReq.V_TRIPPED: + self._values[value_type] = STATE_ON if int( + value) == 1 else STATE_OFF + else: + self._values[value_type] = value + + self.battery_level = node.battery_level diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py index 0c250dcde67..f686a368100 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -1,6 +1,4 @@ """ -homeassistant.components.binary_sensor.nest -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Support for Nest Thermostat Binary Sensors. For more details about this platform, please refer to the documentation at @@ -8,10 +6,10 @@ https://home-assistant.io/components/binary_sensor.nest/ """ import logging import socket -import homeassistant.components.nest as nest -from homeassistant.components.sensor.nest import NestSensor +import homeassistant.components.nest as nest from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.sensor.nest import NestSensor DEPENDENCIES = ['nest'] BINARY_TYPES = ['fan', @@ -27,7 +25,7 @@ BINARY_TYPES = ['fan', def setup_platform(hass, config, add_devices, discovery_info=None): - """ Setup Nest binary sensors. """ + """Setup Nest binary sensors.""" logger = logging.getLogger(__name__) try: @@ -48,9 +46,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class NestBinarySensor(NestSensor, BinarySensorDevice): - """ Represents a Nest binary sensor. """ + """Represents a Nest binary sensor.""" @property def is_on(self): - """ True if the binary sensor is on. """ + """True if the binary sensor is on.""" return bool(getattr(self.device, self.variable)) diff --git a/homeassistant/components/binary_sensor/nx584.py b/homeassistant/components/binary_sensor/nx584.py new file mode 100644 index 00000000000..b5e36395a7a --- /dev/null +++ b/homeassistant/components/binary_sensor/nx584.py @@ -0,0 +1,131 @@ +""" +Support for exposing nx584 elements as sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.nx584/ +""" +import logging +import threading +import time + +import requests + +from homeassistant.components.binary_sensor import ( + SENSOR_CLASSES, BinarySensorDevice) + +REQUIREMENTS = ['pynx584==0.2'] +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup nx584 sensors.""" + from nx584 import client as nx584_client + + host = config.get('host', 'localhost:5007') + exclude = config.get('exclude_zones', []) + zone_types = config.get('zone_types', {}) + + if not all(isinstance(zone, int) for zone in exclude): + _LOGGER.error('Invalid excluded zone specified (use zone number)') + return False + + if not all(isinstance(zone, int) and ztype in SENSOR_CLASSES + for zone, ztype in zone_types.items()): + _LOGGER.error('Invalid zone_types entry') + return False + + try: + client = nx584_client.Client('http://%s' % host) + zones = client.list_zones() + except requests.exceptions.ConnectionError as ex: + _LOGGER.error('Unable to connect to NX584: %s', str(ex)) + return False + + version = [int(v) for v in client.get_version().split('.')] + if version < [1, 1]: + _LOGGER.error('NX584 is too old to use for sensors (>=0.2 required)') + return False + + zone_sensors = { + zone['number']: NX584ZoneSensor( + zone, + zone_types.get(zone['number'], 'opening')) + for zone in zones + if zone['number'] not in exclude} + if zone_sensors: + add_devices(zone_sensors.values()) + watcher = NX584Watcher(client, zone_sensors) + watcher.start() + else: + _LOGGER.warning('No zones found on NX584') + + return True + + +class NX584ZoneSensor(BinarySensorDevice): + """Represents a NX584 zone as a sensor.""" + + def __init__(self, zone, zone_type): + self._zone = zone + self._zone_type = zone_type + + @property + def sensor_class(self): + """Return the class of this sensor, from SENSOR_CLASSES.""" + return self._zone_type + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Name of the binary sensor.""" + return self._zone['name'] + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + # True means "faulted" or "open" or "abnormal state" + return self._zone['state'] + + +class NX584Watcher(threading.Thread): + """Event listener thread to process NX584 events.""" + + def __init__(self, client, zone_sensors): + super(NX584Watcher, self).__init__() + self.daemon = True + self._client = client + self._zone_sensors = zone_sensors + + def _process_zone_event(self, event): + zone = event['zone'] + zone_sensor = self._zone_sensors.get(zone) + # pylint: disable=protected-access + if not zone_sensor: + return + zone_sensor._zone['state'] = event['zone_state'] + zone_sensor.update_ha_state() + + def _process_events(self, events): + for event in events: + if event.get('type') == 'zone_status': + self._process_zone_event(event) + + def _run(self): + # Throw away any existing events so we don't replay history + self._client.get_events() + while True: + events = self._client.get_events() + if events: + self._process_events(events) + + def run(self): + while True: + try: + self._run() + except requests.exceptions.ConnectionError: + _LOGGER.error('Failed to reach NX584 server') + time.sleep(10) diff --git a/homeassistant/components/binary_sensor/rest.py b/homeassistant/components/binary_sensor/rest.py index 1a592cd905a..0e05a24826f 100644 --- a/homeassistant/components/binary_sensor/rest.py +++ b/homeassistant/components/binary_sensor/rest.py @@ -1,6 +1,4 @@ """ -homeassistant.components.binary_sensor.rest -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The rest binary sensor will consume responses sent by an exposed REST API. For more details about this platform, please refer to the documentation at @@ -8,10 +6,10 @@ https://home-assistant.io/components/binary_sensor.rest/ """ import logging -from homeassistant.const import CONF_VALUE_TEMPLATE -from homeassistant.util import template -from homeassistant.components.sensor.rest import RestData from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.sensor.rest import RestData +from homeassistant.const import CONF_VALUE_TEMPLATE +from homeassistant.helpers import template _LOGGER = logging.getLogger(__name__) @@ -21,7 +19,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 +39,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=too-many-arguments class RestBinarySensor(BinarySensorDevice): - """ A 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 +52,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 true if the binary sensor is on.""" if self.rest.data is None: return False @@ -69,5 +67,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/rpi_gpio.py b/homeassistant/components/binary_sensor/rpi_gpio.py index 64da1b5ea9f..22df2795076 100644 --- a/homeassistant/components/binary_sensor/rpi_gpio.py +++ b/homeassistant/components/binary_sensor/rpi_gpio.py @@ -1,16 +1,14 @@ """ -homeassistant.components.binary_sensor.rpi_gpio -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Allows to configure a binary sensor using RPi GPIO. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.rpi_gpio/ """ - import logging + import homeassistant.components.rpi_gpio as rpi_gpio from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.const import (DEVICE_DEFAULT_NAME) +from homeassistant.const import DEVICE_DEFAULT_NAME DEFAULT_PULL_MODE = "UP" DEFAULT_BOUNCETIME = 50 @@ -22,7 +20,7 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """ Sets up the Raspberry PI GPIO devices. """ + """Sets up the Raspberry PI GPIO devices.""" pull_mode = config.get('pull_mode', DEFAULT_PULL_MODE) bouncetime = config.get('bouncetime', DEFAULT_BOUNCETIME) @@ -38,7 +36,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=too-many-arguments, too-many-instance-attributes class RPiGPIOBinarySensor(BinarySensorDevice): - """ Represents a binary sensor that uses Raspberry Pi GPIO. """ + """Represents a binary sensor that uses Raspberry Pi GPIO.""" def __init__(self, name, port, pull_mode, bouncetime, invert_logic): # pylint: disable=no-member @@ -52,22 +50,22 @@ class RPiGPIOBinarySensor(BinarySensorDevice): self._state = rpi_gpio.read_input(self._port) def read_gpio(port): - """ Reads state from GPIO. """ + """Reads state from GPIO.""" self._state = rpi_gpio.read_input(self._port) self.update_ha_state() rpi_gpio.edge_detect(self._port, read_gpio, self._bouncetime) @property def should_poll(self): - """ No polling needed. """ + """No polling needed.""" return False @property def name(self): - """ The name of the sensor. """ + """The name of the sensor.""" return self._name @property def is_on(self): - """ Returns the state of the entity. """ + """Returns the state of the entity.""" return self._state != self._invert_logic diff --git a/homeassistant/components/binary_sensor/tcp.py b/homeassistant/components/binary_sensor/tcp.py new file mode 100644 index 00000000000..125e0f12b91 --- /dev/null +++ b/homeassistant/components/binary_sensor/tcp.py @@ -0,0 +1,31 @@ +""" +Provides a binary sensor which gets its values from a TCP socket. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.tcp/ +""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.sensor.tcp import Sensor, DOMAIN, CONF_VALUE_ON + +DEPENDENCIES = [DOMAIN] +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Create the binary sensor.""" + if not BinarySensor.validate_config(config): + return False + + add_entities((BinarySensor(hass, config),)) + + +class BinarySensor(BinarySensorDevice, Sensor): + """A binary sensor which is on when its state == CONF_VALUE_ON.""" + required = (CONF_VALUE_ON,) + + @property + def is_on(self): + """True if the binary sensor is on.""" + return self._state == self._config[CONF_VALUE_ON] diff --git a/homeassistant/components/binary_sensor/template.py b/homeassistant/components/binary_sensor/template.py new file mode 100644 index 00000000000..f5a8899af96 --- /dev/null +++ b/homeassistant/components/binary_sensor/template.py @@ -0,0 +1,122 @@ +""" +homeassistant.components.binary_sensor.template +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for exposing a templated binary_sensor +""" +import logging + +from homeassistant.components.binary_sensor import (BinarySensorDevice, + DOMAIN, + SENSOR_CLASSES) +from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_VALUE_TEMPLATE +from homeassistant.core import EVENT_STATE_CHANGED +from homeassistant.exceptions import TemplateError +from homeassistant.helpers.entity import generate_entity_id +from homeassistant.helpers import template +from homeassistant.util import slugify + +ENTITY_ID_FORMAT = DOMAIN + '.{}' +CONF_SENSORS = 'sensors' +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup template binary sensors.""" + + sensors = [] + if config.get(CONF_SENSORS) is None: + _LOGGER.error('Missing configuration data for binary_sensor platform') + return False + + for device, device_config in config[CONF_SENSORS].items(): + + if device != slugify(device): + _LOGGER.error('Found invalid key for binary_sensor.template: %s. ' + 'Use %s instead', device, slugify(device)) + continue + + if not isinstance(device_config, dict): + _LOGGER.error('Missing configuration data for binary_sensor %s', + device) + continue + + friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) + sensor_class = device_config.get('sensor_class') + value_template = device_config.get(CONF_VALUE_TEMPLATE) + + if sensor_class not in SENSOR_CLASSES: + _LOGGER.error('Sensor class is not valid') + continue + + if value_template is None: + _LOGGER.error( + 'Missing %s for sensor %s', CONF_VALUE_TEMPLATE, device) + continue + + sensors.append( + BinarySensorTemplate( + hass, + device, + friendly_name, + sensor_class, + value_template) + ) + if not sensors: + _LOGGER.error('No sensors added') + return False + add_devices(sensors) + + return True + + +class BinarySensorTemplate(BinarySensorDevice): + """A virtual binary_sensor that triggers from another sensor.""" + + # pylint: disable=too-many-arguments + def __init__(self, hass, device, friendly_name, sensor_class, + value_template): + self._hass = hass + self._device = device + self._name = friendly_name + self._sensor_class = sensor_class + self._template = value_template + self._state = None + + self.entity_id = generate_entity_id( + ENTITY_ID_FORMAT, device, + hass=hass) + + _LOGGER.info('Started template sensor %s', device) + hass.bus.listen(EVENT_STATE_CHANGED, self._event_listener) + + def _event_listener(self, event): + self.update_ha_state(True) + + @property + def should_poll(self): + return False + + @property + def sensor_class(self): + return self._sensor_class + + @property + def name(self): + return self._name + + @property + def is_on(self): + return self._state + + def update(self): + try: + value = template.render(self._hass, self._template) + except TemplateError as ex: + if ex.args and ex.args[0].startswith( + "UndefinedError: 'None' has no attribute"): + # Common during HA startup - so just a warning + _LOGGER.warning(ex) + return + _LOGGER.error(ex) + value = 'false' + self._state = value.lower() == 'true' diff --git a/homeassistant/components/binary_sensor/wink.py b/homeassistant/components/binary_sensor/wink.py new file mode 100644 index 00000000000..8d858901849 --- /dev/null +++ b/homeassistant/components/binary_sensor/wink.py @@ -0,0 +1,81 @@ +""" +Support for Wink sensors. + +For more details about this platform, please refer to the documentation at +at https://home-assistant.io/components/sensor.wink/ +""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['python-wink==0.6.2'] + +# These are the available sensors mapped to binary_sensor class +SENSOR_TYPES = { + "opened": "opening", + "brightness": "light", + "vibration": "vibration", + "loudness": "sound" +} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Sets up the Wink platform.""" + import pywink + + if discovery_info is None: + token = config.get(CONF_ACCESS_TOKEN) + + if token is None: + logging.getLogger(__name__).error( + "Missing wink access_token. " + "Get one at https://winkbearertoken.appspot.com/") + return + + pywink.set_bearer_token(token) + + for sensor in pywink.get_sensors(): + if sensor.capability() in SENSOR_TYPES: + add_devices([WinkBinarySensorDevice(sensor)]) + + +class WinkBinarySensorDevice(BinarySensorDevice, Entity): + """Represents a Wink sensor.""" + + def __init__(self, wink): + self.wink = wink + self._unit_of_measurement = self.wink.UNIT + self.capability = self.wink.capability() + + @property + def is_on(self): + """Return True if the binary sensor is on.""" + if self.capability == "loudness": + return self.wink.loudness_boolean() + elif self.capability == "vibration": + return self.wink.vibration_boolean() + elif self.capability == "brightness": + return self.wink.brightness_boolean() + else: + return self.wink.state() + + @property + def sensor_class(self): + """Return the class of this sensor, from SENSOR_CLASSES.""" + return SENSOR_TYPES.get(self.capability) + + @property + def unique_id(self): + """ Returns the id of this wink sensor """ + return "{}.{}".format(self.__class__, self.wink.device_id()) + + @property + def name(self): + """ Returns the name of the sensor if any. """ + return self.wink.name() + + def update(self): + """ Update state of the sensor. """ + self.wink.update_state() diff --git a/homeassistant/components/binary_sensor/zigbee.py b/homeassistant/components/binary_sensor/zigbee.py index 1597cd5004f..49b6f12ed5c 100644 --- a/homeassistant/components/binary_sensor/zigbee.py +++ b/homeassistant/components/binary_sensor/zigbee.py @@ -1,6 +1,4 @@ """ -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 @@ -10,12 +8,11 @@ from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.zigbee import ( ZigBeeDigitalIn, ZigBeeDigitalInConfig) - 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/binary_sensor/zwave.py b/homeassistant/components/binary_sensor/zwave.py new file mode 100644 index 00000000000..bd3ff69e8cf --- /dev/null +++ b/homeassistant/components/binary_sensor/zwave.py @@ -0,0 +1,131 @@ +""" +Interfaces with Z-Wave sensors. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/binary_sensor.zwave/ +""" +import logging +import datetime +import homeassistant.util.dt as dt_util +from homeassistant.helpers.event import track_point_in_time + +from homeassistant.components.zwave import ( + ATTR_NODE_ID, ATTR_VALUE_ID, + COMMAND_CLASS_SENSOR_BINARY, NETWORK, + ZWaveDeviceEntity, get_config_value) +from homeassistant.components.binary_sensor import ( + DOMAIN, + BinarySensorDevice) + +_LOGGER = logging.getLogger(__name__) +DEPENDENCIES = [] + +PHILIO = 0x013c +PHILIO_SLIM_SENSOR = 0x0002 +PHILIO_SLIM_SENSOR_MOTION = (PHILIO, PHILIO_SLIM_SENSOR, 0) + +WORKAROUND_NO_OFF_EVENT = 'trigger_no_off_event' + +DEVICE_MAPPINGS = { + PHILIO_SLIM_SENSOR_MOTION: WORKAROUND_NO_OFF_EVENT, +} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Z-Wave platform for sensors.""" + + if discovery_info is None or NETWORK is None: + return + + node = NETWORK.nodes[discovery_info[ATTR_NODE_ID]] + value = node.values[discovery_info[ATTR_VALUE_ID]] + + specific_sensor_key = (int(value.node.manufacturer_id, 16), + int(value.node.product_id, 16), + value.index) + + value.set_change_verified(False) + if specific_sensor_key in DEVICE_MAPPINGS: + if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_NO_OFF_EVENT: + # Default the multiplier to 4 + re_arm_multiplier = (get_config_value(value.node, 9) or 4) + add_devices([ + ZWaveTriggerSensor(value, "motion", + hass, re_arm_multiplier * 8) + ]) + + elif value.command_class == COMMAND_CLASS_SENSOR_BINARY: + add_devices([ZWaveBinarySensor(value, None)]) + + +class ZWaveBinarySensor(BinarySensorDevice, ZWaveDeviceEntity): + """Represents a binary sensor within Z-Wave.""" + + def __init__(self, value, sensor_class): + self._sensor_type = sensor_class + # pylint: disable=import-error + from openzwave.network import ZWaveNetwork + from pydispatch import dispatcher + + ZWaveDeviceEntity.__init__(self, value, DOMAIN) + + dispatcher.connect( + self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED) + + @property + def is_on(self): + """Return True if the binary sensor is on.""" + return self._value.data + + @property + def sensor_class(self): + """Return the class of this sensor, from SENSOR_CLASSES.""" + return self._sensor_type + + @property + def should_poll(self): + """No polling needed.""" + return False + + def value_changed(self, value): + """Called when a value has changed on the network.""" + if self._value.value_id == value.value_id: + self.update_ha_state() + + +class ZWaveTriggerSensor(ZWaveBinarySensor): + """ + Represents a stateless sensor which triggers events just 'On' + within Z-Wave. + """ + + def __init__(self, sensor_value, sensor_class, hass, re_arm_sec=60): + super(ZWaveTriggerSensor, self).__init__(sensor_value, sensor_class) + self._hass = hass + self.re_arm_sec = re_arm_sec + self.invalidate_after = dt_util.utcnow() + datetime.timedelta( + seconds=self.re_arm_sec) + # If it's active make sure that we set the timeout tracker + if sensor_value.data: + track_point_in_time( + self._hass, self.update_ha_state, + self.invalidate_after) + + def value_changed(self, value): + """Called when a value has changed on the network.""" + if self._value.value_id == value.value_id: + self.update_ha_state() + if value.data: + # only allow this value to be true for re_arm secs + self.invalidate_after = dt_util.utcnow() + datetime.timedelta( + seconds=self.re_arm_sec) + track_point_in_time( + self._hass, self.update_ha_state, + self.invalidate_after) + + @property + def is_on(self): + """Return True if movement has happened within the rearm time.""" + return self._value.data and \ + (self.invalidate_after is None or + self.invalidate_after > dt_util.utcnow()) diff --git a/homeassistant/components/bloomsky.py b/homeassistant/components/bloomsky.py index fe2ae1cf3ba..44a90007725 100644 --- a/homeassistant/components/bloomsky.py +++ b/homeassistant/components/bloomsky.py @@ -8,10 +8,13 @@ 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.components import discovery from homeassistant.const import CONF_API_KEY +from homeassistant.helpers import validate_config +from homeassistant.util import Throttle DOMAIN = "bloomsky" BLOOMSKY = None @@ -22,6 +25,10 @@ _LOGGER = logging.getLogger(__name__) # no point in polling the API more frequently MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300) +DISCOVER_SENSORS = 'bloomsky.sensors' +DISCOVER_BINARY_SENSORS = 'bloomsky.binary_sensor' +DISCOVER_CAMERAS = 'bloomsky.camera' + # pylint: disable=unused-argument,too-few-public-methods def setup(hass, config): @@ -40,6 +47,12 @@ def setup(hass, config): except RuntimeError: return False + for component, discovery_service in ( + ('camera', DISCOVER_CAMERAS), ('sensor', DISCOVER_SENSORS), + ('binary_sensor', DISCOVER_BINARY_SENSORS)): + discovery.discover(hass, discovery_service, component=component, + hass_config=config) + return True diff --git a/homeassistant/components/browser.py b/homeassistant/components/browser.py index 88548e2a1b3..d171e4b5901 100644 --- a/homeassistant/components/browser.py +++ b/homeassistant/components/browser.py @@ -1,21 +1,18 @@ """ -homeassistant.components.browser -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Provides functionality to launch a webbrowser on the host machine. +Provides functionality to launch a web browser on the host machine. For more details about this component, please refer to the documentation at https://home-assistant.io/components/browser/ """ DOMAIN = "browser" - SERVICE_BROWSE_URL = "browse_url" def setup(hass, config): - """ Listen for browse_url events and open - the url in the default webbrowser. """ - + """ + Listen for browse_url events and open the url in the default web browser. + """ import webbrowser hass.services.register(DOMAIN, SERVICE_BROWSE_URL, diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 9aefe4b3b66..2915f37dd21 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -1,8 +1,6 @@ # pylint: disable=too-many-lines """ -homeassistant.components.camera -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Component to interface with various cameras. +Component to interface with cameras. For more details about this component, please refer to the documentation at https://home-assistant.io/components/camera/ @@ -15,8 +13,8 @@ import requests from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.components import bloomsky from homeassistant.const import ( - ATTR_ENTITY_PICTURE, HTTP_NOT_FOUND, ATTR_ENTITY_ID, ) @@ -24,33 +22,19 @@ from homeassistant.const import ( DOMAIN = 'camera' DEPENDENCIES = ['http'] -GROUP_NAME_ALL_CAMERAS = 'all_cameras' SCAN_INTERVAL = 30 ENTITY_ID_FORMAT = DOMAIN + '.{}' -SWITCH_ACTION_RECORD = 'record' -SWITCH_ACTION_SNAPSHOT = 'snapshot' - -SERVICE_CAMERA = 'camera_service' - -DEFAULT_RECORDING_SECONDS = 30 - # Maps discovered services to their platforms -DISCOVERY_PLATFORMS = {} - -FILE_DATETIME_FORMAT = '%Y-%m-%d_%H-%M-%S-%f' -DIR_DATETIME_FORMAT = '%Y-%m-%d_%H-%M-%S' - -REC_DIR_PREFIX = 'recording-' -REC_IMG_PREFIX = 'recording_image-' +DISCOVERY_PLATFORMS = { + bloomsky.DISCOVER_CAMERAS: 'bloomsky', +} STATE_RECORDING = 'recording' STATE_STREAMING = 'streaming' STATE_IDLE = 'idle' -CAMERA_PROXY_URL = '/api/camera_proxy_stream/{0}' -CAMERA_STILL_URL = '/api/camera_proxy/{0}' -ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?time={1}' +ENTITY_IMAGE_URL = '/api/camera_proxy/{0}' MULTIPART_BOUNDARY = '--jpegboundary' MJPEG_START_HEADER = 'Content-type: {0}\r\n\r\n' @@ -58,8 +42,7 @@ MJPEG_START_HEADER = 'Content-type: {0}\r\n\r\n' # pylint: disable=too-many-branches def setup(hass, config): - """ Track states and offer events for cameras. """ - + """Initialize camera component.""" component = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL, DISCOVERY_PLATFORMS) @@ -78,7 +61,7 @@ def setup(hass, config): # pylint: disable=unused-argument def _proxy_camera_image(handler, path_match, data): - """ Proxies the camera image via the HA server. """ + """Serve the camera image via the HA server.""" entity_id = path_match.group(ATTR_ENTITY_ID) camera = component.entities.get(entity_id) @@ -104,7 +87,8 @@ def setup(hass, config): # pylint: disable=unused-argument def _proxy_camera_mjpeg_stream(handler, path_match, data): """ - Proxies the camera image as an mjpeg stream via the HA server. + Proxy the camera image as an mjpeg stream via the HA server. + This function takes still images from the IP camera and turns them into an MJPEG stream. This means that HA can return a live video stream even with only a still image URL available. @@ -136,35 +120,46 @@ def setup(hass, config): class Camera(Entity): - """ The base class for camera components. """ + """The base class for camera entities.""" def __init__(self): + """Initialize a camera.""" self.is_streaming = False + @property + def should_poll(self): + """No need to poll cameras.""" + return False + + @property + def entity_picture(self): + """Return a link to the camera feed as entity picture.""" + return ENTITY_IMAGE_URL.format(self.entity_id) + @property # pylint: disable=no-self-use def is_recording(self): - """ Returns true if the device is recording. """ + """Return true if the device is recording.""" return False @property # pylint: disable=no-self-use def brand(self): - """ Should return a string of the camera brand. """ + """Camera brand.""" return None @property # pylint: disable=no-self-use def model(self): - """ Returns string of camera model. """ + """Camera model.""" return None def camera_image(self): - """ Return bytes of camera image. """ + """Return bytes of camera image.""" raise NotImplementedError() def mjpeg_stream(self, handler): - """ Generate an HTTP MJPEG stream from camera images. """ + """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; \ @@ -193,7 +188,7 @@ class Camera(Entity): @property def state(self): - """ Returns the state of the entity. """ + """Camera state.""" if self.is_recording: return STATE_RECORDING elif self.is_streaming: @@ -203,11 +198,8 @@ class Camera(Entity): @property def state_attributes(self): - """ Returns optional state attributes. """ - attr = { - ATTR_ENTITY_PICTURE: ENTITY_IMAGE_URL.format( - self.entity_id, time.time()), - } + """Camera state attributes.""" + attr = {} if self.model: attr['model_name'] = self.model diff --git a/homeassistant/components/camera/bloomsky.py b/homeassistant/components/camera/bloomsky.py index 5c9314963bd..6e03a136c6b 100644 --- a/homeassistant/components/camera/bloomsky.py +++ b/homeassistant/components/camera/bloomsky.py @@ -7,9 +7,11 @@ 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 +from homeassistant.loader import get_component DEPENDENCIES = ["bloomsky"] @@ -17,6 +19,7 @@ DEPENDENCIES = ["bloomsky"] # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ set up access to BloomSky cameras """ + bloomsky = get_component('bloomsky') for device in bloomsky.BLOOMSKY.devices.values(): add_devices_callback([BloomSkyCamera(bloomsky.BLOOMSKY, device)]) diff --git a/homeassistant/components/camera/demo.py b/homeassistant/components/camera/demo.py index 0ad992db86d..15ddeb31d72 100644 --- a/homeassistant/components/camera/demo.py +++ b/homeassistant/components/camera/demo.py @@ -1,29 +1,31 @@ """ -homeassistant.components.camera.demo -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Demo platform that has a fake camera. +Demo camera platform that has a fake camera. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/demo/ """ import os -from homeassistant.components.camera import Camera + import homeassistant.util.dt as dt_util +from homeassistant.components.camera import Camera def setup_platform(hass, config, add_devices, discovery_info=None): - """ Sets up the Demo camera. """ + """Setup the Demo camera platform.""" add_devices([ DemoCamera('Demo camera') ]) class DemoCamera(Camera): - """ A Demo camera. """ + """A Demo camera.""" def __init__(self, name): super().__init__() self._name = name def camera_image(self): - """ Return a faked still image response. """ + """Return a faked still image response.""" now = dt_util.utcnow() image_path = os.path.join(os.path.dirname(__file__), @@ -33,5 +35,5 @@ class DemoCamera(Camera): @property def name(self): - """ Return the name of this device. """ + """Return the name of this camera.""" return self._name diff --git a/homeassistant/components/camera/foscam.py b/homeassistant/components/camera/foscam.py index b210e1a2f1b..47ebf1b7d5a 100644 --- a/homeassistant/components/camera/foscam.py +++ b/homeassistant/components/camera/foscam.py @@ -10,8 +10,8 @@ import logging import requests -from homeassistant.helpers import validate_config from homeassistant.components.camera import DOMAIN, Camera +from homeassistant.helpers import validate_config _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index c81febccc86..514e94db1ef 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -11,8 +11,8 @@ import logging import requests from requests.auth import HTTPBasicAuth -from homeassistant.helpers import validate_config from homeassistant.components.camera import DOMAIN, Camera +from homeassistant.helpers import validate_config _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index 7bbaa4846b5..95bf9813b39 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -6,15 +6,15 @@ Support for IP Cameras. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.mjpeg/ """ -from contextlib import closing import logging +from contextlib import closing import requests from requests.auth import HTTPBasicAuth -from homeassistant.helpers import validate_config from homeassistant.components.camera import DOMAIN, Camera from homeassistant.const import HTTP_OK +from homeassistant.helpers import validate_config CONTENT_TYPE_HEADER = 'Content-Type' diff --git a/homeassistant/components/camera/uvc.py b/homeassistant/components/camera/uvc.py index eeb447be05a..e34b0e26859 100644 --- a/homeassistant/components/camera/uvc.py +++ b/homeassistant/components/camera/uvc.py @@ -11,10 +11,10 @@ import socket import requests -from homeassistant.helpers import validate_config from homeassistant.components.camera import DOMAIN, Camera +from homeassistant.helpers import validate_config -REQUIREMENTS = ['uvcclient==0.5'] +REQUIREMENTS = ['uvcclient==0.8'] _LOGGER = logging.getLogger(__name__) @@ -26,8 +26,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return None addr = config.get('nvr') - port = int(config.get('port', 7080)) key = config.get('key') + try: + port = int(config.get('port', 7080)) + except ValueError: + _LOGGER.error('Invalid port number provided') + return False from uvcclient import nvr nvrconn = nvr.UVCRemote(addr, port, key) @@ -43,10 +47,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error('Unable to connect to NVR: %s', str(ex)) return False - for camera in cameras: - add_devices([UnifiVideoCamera(nvrconn, - camera['uuid'], - camera['name'])]) + # Filter out airCam models, which are not supported in the latest + # version of UnifiVideo and which are EOL by Ubiquiti + cameras = [camera for camera in cameras + if 'airCam' not in nvrconn.get_camera(camera['uuid'])['model']] + + add_devices([UnifiVideoCamera(nvrconn, + camera['uuid'], + camera['name']) + for camera in cameras]) + return True class UnifiVideoCamera(Camera): @@ -58,6 +68,8 @@ class UnifiVideoCamera(Camera): self._uuid = uuid self._name = name self.is_streaming = False + self._connect_addr = None + self._camera = None @property def name(self): @@ -68,24 +80,74 @@ class UnifiVideoCamera(Camera): caminfo = self._nvr.get_camera(self._uuid) return caminfo['recordingSettings']['fullTimeRecordEnabled'] - def camera_image(self): + @property + def brand(self): + return 'Ubiquiti' + + @property + def model(self): + caminfo = self._nvr.get_camera(self._uuid) + return caminfo['model'] + + def _login(self): from uvcclient import camera as uvc_camera + from uvcclient import store as uvc_store caminfo = self._nvr.get_camera(self._uuid) + if self._connect_addr: + addrs = [self._connect_addr] + else: + addrs = [caminfo['host'], caminfo['internalHost']] + + store = uvc_store.get_info_store() + password = store.get_camera_password(self._uuid) + if password is None: + _LOGGER.debug('Logging into camera %(name)s with default password', + dict(name=self._name)) + password = 'ubnt' + camera = None - for addr in [caminfo['host'], caminfo['internalHost']]: + for addr in addrs: try: camera = uvc_camera.UVCCameraClient(addr, caminfo['username'], - 'ubnt') + password) + camera.login() _LOGGER.debug('Logged into UVC camera %(name)s via %(addr)s', dict(name=self._name, addr=addr)) + self._connect_addr = addr + break except socket.error: pass - - if not camera: + except uvc_camera.CameraConnectError: + pass + except uvc_camera.CameraAuthError: + pass + if not self._connect_addr: _LOGGER.error('Unable to login to camera') return None - camera.login() - return camera.get_snapshot() + self._camera = camera + return True + + def camera_image(self): + from uvcclient import camera as uvc_camera + if not self._camera: + if not self._login(): + return + + def _get_image(retry=True): + try: + return self._camera.get_snapshot() + except uvc_camera.CameraConnectError: + _LOGGER.error('Unable to contact camera') + except uvc_camera.CameraAuthError: + if retry: + self._login() + return _get_image(retry=False) + else: + _LOGGER.error('Unable to log into camera, unable ' + 'to get snapshot') + raise + + return _get_image() diff --git a/homeassistant/components/configurator.py b/homeassistant/components/configurator.py index 591cdc0dc61..681cc80cc9c 100644 --- a/homeassistant/components/configurator.py +++ b/homeassistant/components/configurator.py @@ -11,8 +11,8 @@ the user has submitted configuration information. """ import logging -from homeassistant.helpers.entity import generate_entity_id from homeassistant.const import EVENT_TIME_CHANGED +from homeassistant.helpers.entity import generate_entity_id DOMAIN = "configurator" ENTITY_ID_FORMAT = DOMAIN + ".{}" diff --git a/homeassistant/components/conversation.py b/homeassistant/components/conversation.py index 18ddf8fcc8d..9cf70fa2b62 100644 --- a/homeassistant/components/conversation.py +++ b/homeassistant/components/conversation.py @@ -9,10 +9,9 @@ https://home-assistant.io/components/conversation/ import logging import re - from homeassistant import core from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF) + ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON) DOMAIN = "conversation" diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index e63f5f49551..3a70be00b38 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -1,16 +1,15 @@ """ -homeassistant.components.demo -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Sets up a demo environment that mimics interaction with devices. + +For more details about this component, please refer to the documentation +https://home-assistant.io/components/demo/ """ import time -import homeassistant.core as ha import homeassistant.bootstrap as bootstrap +import homeassistant.core as ha import homeassistant.loader as loader -from homeassistant.const import ( - CONF_PLATFORM, ATTR_ENTITY_ID) +from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM DOMAIN = "demo" @@ -34,7 +33,7 @@ COMPONENTS_WITH_DEMO_PLATFORM = [ def setup(hass, config): - """ Setup a demo environment. """ + """Setup a demo environment.""" group = loader.get_component('group') configurator = loader.get_component('configurator') @@ -64,14 +63,29 @@ def setup(hass, config): switches = sorted(hass.states.entity_ids('switch')) media_players = sorted(hass.states.entity_ids('media_player')) group.Group(hass, 'living room', [ - lights[2], lights[1], switches[0], media_players[1], + lights[1], switches[0], 'input_select.living_room_preset', + 'rollershutter.living_room_window', media_players[1], 'scene.romantic_lights']) - group.Group(hass, 'bedroom', [lights[0], switches[1], - media_players[0]]) - group.Group(hass, 'Rooms', [ - 'group.living_room', 'group.bedroom', + group.Group(hass, 'bedroom', [lights[0], switches[1], media_players[0]]) + group.Group(hass, 'kitchen', [ + lights[2], 'rollershutter.kitchen_window', 'lock.kitchen_door']) + group.Group(hass, 'doors', [ + 'lock.front_door', 'lock.kitchen_door', + 'garage_door.right_garage_door', 'garage_door.left_garage_door']) + group.Group(hass, 'automations', [ + 'input_select.who_cooks', 'input_boolean.notify', ]) + group.Group(hass, 'people', [ + 'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy', + 'device_tracker.demo_paulus']) + group.Group(hass, 'thermostats', [ + 'thermostat.nest', 'thermostat.thermostat']) + group.Group(hass, 'downstairs', [ + 'group.living_room', 'group.kitchen', 'scene.romantic_lights', 'rollershutter.kitchen_window', - 'rollershutter.living_room_window', + 'rollershutter.living_room_window', 'group.doors', 'thermostat.nest', + ], view=True) + group.Group(hass, 'Upstairs', [ + 'thermostat.thermostat', 'group.bedroom', ], view=True) # Setup scripts @@ -113,11 +127,28 @@ def setup(hass, config): }}, ]}) + # Set up input select + bootstrap.setup_component( + hass, 'input_select', + {'input_select': + {'living_room_preset': {'options': ['Visitors', + 'Visitors with kids', + 'Home Alone']}, + 'who_cooks': {'icon': 'mdi:panda', + 'initial': 'Anne Therese', + 'name': 'Who cooks today', + 'options': ['Paulus', 'Anne Therese']}}}) + # Set up input boolean + bootstrap.setup_component( + hass, 'input_boolean', + {'input_boolean': {'notify': {'icon': 'mdi:car', + 'initial': False, + 'name': 'Notify Anne Therese is home'}}}) # Setup configurator configurator_ids = [] def hue_configuration_callback(data): - """ Fake callback, mark config as done. """ + """Fake callback, mark config as done.""" time.sleep(2) # First time it is called, pretend it failed. diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index 4acf60bc0a2..a8cf5b5d417 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -10,10 +10,11 @@ https://home-assistant.io/components/device_sun_light_trigger/ import logging from datetime import timedelta -from homeassistant.helpers.event import track_point_in_time, track_state_change import homeassistant.util.dt as dt_util from homeassistant.const import STATE_HOME, STATE_NOT_HOME -from . import light, sun, device_tracker, group +from homeassistant.helpers.event import track_point_in_time, track_state_change + +from . import device_tracker, group, light, sun DOMAIN = "device_sun_light_trigger" DEPENDENCIES = ['light', 'device_tracker', 'group', 'sun'] diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index c5b4ccd1c16..8c3fd62a0f8 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -25,7 +25,7 @@ import homeassistant.util.dt as dt_util from homeassistant.helpers.event import track_utc_time_change from homeassistant.const import ( - ATTR_ENTITY_PICTURE, ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, + ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME) DOMAIN = "device_tracker" @@ -297,14 +297,16 @@ class Device(Entity): """ State of the device. """ return self._state + @property + def entity_picture(self): + """Picture of the device.""" + return self.config_picture + @property def state_attributes(self): """ Device state attributes. """ attr = {} - if self.config_picture: - attr[ATTR_ENTITY_PICTURE] = self.config_picture - if self.gps: attr[ATTR_LATITUDE] = self.gps[0] attr[ATTR_LONGITUDE] = self.gps[1] diff --git a/homeassistant/components/device_tracker/actiontec.py b/homeassistant/components/device_tracker/actiontec.py index f363acf1902..d9cb8718a70 100644 --- a/homeassistant/components/device_tracker/actiontec.py +++ b/homeassistant/components/device_tracker/actiontec.py @@ -8,17 +8,17 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.actiontec/ """ import logging -from datetime import timedelta -from collections import namedtuple import re -import threading import telnetlib +import threading +from collections import namedtuple +from datetime import timedelta import homeassistant.util.dt as dt_util -from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.components.device_tracker import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import validate_config from homeassistant.util import Throttle -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=5) diff --git a/homeassistant/components/device_tracker/aruba.py b/homeassistant/components/device_tracker/aruba.py index 6d94ad30d04..4bc3f9e3258 100644 --- a/homeassistant/components/device_tracker/aruba.py +++ b/homeassistant/components/device_tracker/aruba.py @@ -8,14 +8,14 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.aruba/ """ import logging -from datetime import timedelta import re import threading +from datetime import timedelta -from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.components.device_tracker import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import validate_config from homeassistant.util import Throttle -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) diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index 472440d7307..d9b6d1a809e 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -8,15 +8,15 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.asuswrt/ """ import logging -from datetime import timedelta import re -import threading import telnetlib +import threading +from datetime import timedelta -from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.components.device_tracker import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import validate_config from homeassistant.util import Throttle -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=5) diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index 268c4e5a22f..82a8ed81537 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -8,15 +8,16 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.ddwrt/ """ import logging -from datetime import timedelta import re import threading +from datetime import timedelta + import requests -from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.components.device_tracker import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import validate_config from homeassistant.util import Throttle -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=5) diff --git a/homeassistant/components/device_tracker/fritz.py b/homeassistant/components/device_tracker/fritz.py index 9fee2747c0f..3ca3461f557 100644 --- a/homeassistant/components/device_tracker/fritz.py +++ b/homeassistant/components/device_tracker/fritz.py @@ -10,10 +10,10 @@ https://home-assistant.io/components/device_tracker.fritz/ import logging from datetime import timedelta -from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.components.device_tracker import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import validate_config from homeassistant.util import Throttle -from homeassistant.components.device_tracker import DOMAIN REQUIREMENTS = ['fritzconnection==0.4.6'] diff --git a/homeassistant/components/device_tracker/icloud.py b/homeassistant/components/device_tracker/icloud.py index 76046552551..4b13098dde0 100644 --- a/homeassistant/components/device_tracker/icloud.py +++ b/homeassistant/components/device_tracker/icloud.py @@ -7,9 +7,9 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.icloud/ """ import logging - import re -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD + +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.event import track_utc_time_change _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index 11884829600..d1912b06b3d 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -9,9 +9,8 @@ https://home-assistant.io/components/device_tracker.locative/ import logging from functools import partial -from homeassistant.const import ( - HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME) from homeassistant.components.device_tracker import DOMAIN +from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/device_tracker/luci.py b/homeassistant/components/device_tracker/luci.py index 8b3e4eeb3c8..5745138bf8e 100644 --- a/homeassistant/components/device_tracker/luci.py +++ b/homeassistant/components/device_tracker/luci.py @@ -7,17 +7,18 @@ presence. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.luci/ """ -import logging import json -from datetime import timedelta +import logging import re import threading +from datetime import timedelta + import requests -from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.components.device_tracker import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import validate_config from homeassistant.util import Throttle -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=5) diff --git a/homeassistant/components/device_tracker/mqtt.py b/homeassistant/components/device_tracker/mqtt.py index 929deaae669..6fca13e6892 100644 --- a/homeassistant/components/device_tracker/mqtt.py +++ b/homeassistant/components/device_tracker/mqtt.py @@ -7,8 +7,9 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.mqtt/ """ import logging -from homeassistant import util + import homeassistant.components.mqtt as mqtt +from homeassistant import util DEPENDENCIES = ['mqtt'] diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index 233622e076e..eebb68c043a 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -8,12 +8,12 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.netgear/ """ import logging -from datetime import timedelta import threading +from datetime import timedelta -from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD -from homeassistant.util import Throttle from homeassistant.components.device_tracker import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.util import Throttle # Return cached results if last scan was less then this time ago MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py index bc8e8768be0..596986e8bb7 100644 --- a/homeassistant/components/device_tracker/nmap_tracker.py +++ b/homeassistant/components/device_tracker/nmap_tracker.py @@ -7,16 +7,16 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.nmap_scanner/ """ import logging -from datetime import timedelta -from collections import namedtuple -import subprocess import re +import subprocess +from collections import namedtuple +from datetime import timedelta import homeassistant.util.dt as dt_util +from homeassistant.components.device_tracker import DOMAIN from homeassistant.const import CONF_HOSTS from homeassistant.helpers import validate_config from homeassistant.util import Throttle, convert -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=5) diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index 2b8e612030b..3f430d798a4 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -62,7 +62,7 @@ def setup_scanner(hass, config, see): see_beacons(dev_id, kwargs) def owntracks_event_update(topic, payload, qos): - # pylint: disable=too-many-branches + # pylint: disable=too-many-branches, too-many-statements """ MQTT event (geofences) received. """ # Docs on available data: @@ -92,7 +92,10 @@ def setup_scanner(hass, config, see): if zone is None: if data['t'] == 'b': # Not a HA zone, and a beacon so assume mobile - MOBILE_BEACONS_ACTIVE[dev_id].append(location) + beacons = MOBILE_BEACONS_ACTIVE[dev_id] + if location not in beacons: + beacons.append(location) + _LOGGER.info("Added beacon %s", location) else: # Normal region if not zone.attributes.get('passive'): @@ -108,28 +111,30 @@ def setup_scanner(hass, config, see): see_beacons(dev_id, kwargs) elif data['event'] == 'leave': - regions = REGIONS_ENTERED[dev_id] - if location in regions: - regions.remove(location) - new_region = regions[-1] if regions else None + with LOCK: + regions = REGIONS_ENTERED[dev_id] + if location in regions: + regions.remove(location) + new_region = regions[-1] if regions else None - if new_region: - # Exit to previous region - zone = hass.states.get("zone.{}".format(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) + if new_region: + # Exit to previous region + zone = hass.states.get("zone.{}".format(new_region)) + if not zone.attributes.get('passive'): + kwargs['location_name'] = new_region + _set_gps_from_zone(kwargs, zone) + _LOGGER.info("Exit to %s", new_region) - else: - _LOGGER.info("Exit to GPS") + else: + _LOGGER.info("Exit to GPS") - see(**kwargs) - see_beacons(dev_id, kwargs) + see(**kwargs) + see_beacons(dev_id, kwargs) - beacons = MOBILE_BEACONS_ACTIVE[dev_id] - if location in beacons: - beacons.remove(location) + beacons = MOBILE_BEACONS_ACTIVE[dev_id] + if location in beacons: + beacons.remove(location) + _LOGGER.info("Remove beacon %s", location) else: _LOGGER.error( @@ -141,6 +146,8 @@ def setup_scanner(hass, config, see): """ Set active beacons to the current location """ kwargs = kwargs_param.copy() + # the battery state applies to the tracking device, not the beacon + kwargs.pop('battery', None) for beacon in MOBILE_BEACONS_ACTIVE[dev_id]: kwargs['dev_id'] = "{}_{}".format(BEACON_DEV_ID, beacon) kwargs['host_name'] = beacon diff --git a/homeassistant/components/device_tracker/snmp.py b/homeassistant/components/device_tracker/snmp.py index cd0e8239c38..b7b59fbf95a 100644 --- a/homeassistant/components/device_tracker/snmp.py +++ b/homeassistant/components/device_tracker/snmp.py @@ -7,15 +7,15 @@ through SNMP. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.snmp/ """ -import logging -from datetime import timedelta -import threading import binascii +import logging +import threading +from datetime import timedelta +from homeassistant.components.device_tracker import DOMAIN from homeassistant.const import CONF_HOST from homeassistant.helpers import validate_config from homeassistant.util import Throttle -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) diff --git a/homeassistant/components/device_tracker/thomson.py b/homeassistant/components/device_tracker/thomson.py index 657bb910da2..7c7667400f5 100644 --- a/homeassistant/components/device_tracker/thomson.py +++ b/homeassistant/components/device_tracker/thomson.py @@ -8,15 +8,15 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.thomson/ """ import logging -from datetime import timedelta import re -import threading import telnetlib +import threading +from datetime import timedelta -from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.components.device_tracker import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import validate_config from homeassistant.util import Throttle -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) diff --git a/homeassistant/components/device_tracker/tomato.py b/homeassistant/components/device_tracker/tomato.py index c87a50f0981..8f1e956dcb6 100644 --- a/homeassistant/components/device_tracker/tomato.py +++ b/homeassistant/components/device_tracker/tomato.py @@ -7,18 +7,18 @@ presence. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.tomato/ """ -import logging import json -from datetime import timedelta +import logging import re import threading +from datetime import timedelta import requests -from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.components.device_tracker import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import validate_config from homeassistant.util import Throttle -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=5) diff --git a/homeassistant/components/device_tracker/tplink.py b/homeassistant/components/device_tracker/tplink.py index a661dac0c1e..30a920961cb 100755 --- a/homeassistant/components/device_tracker/tplink.py +++ b/homeassistant/components/device_tracker/tplink.py @@ -9,15 +9,16 @@ https://home-assistant.io/components/device_tracker.tplink/ """ import base64 import logging -from datetime import timedelta import re import threading +from datetime import timedelta + import requests -from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.components.device_tracker import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import validate_config from homeassistant.util import Throttle -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=5) diff --git a/homeassistant/components/device_tracker/ubus.py b/homeassistant/components/device_tracker/ubus.py index 0355680a31d..073588008b0 100644 --- a/homeassistant/components/device_tracker/ubus.py +++ b/homeassistant/components/device_tracker/ubus.py @@ -7,17 +7,18 @@ presence. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.ubus/ """ -import logging import json -from datetime import timedelta +import logging import re import threading +from datetime import timedelta + import requests -from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.components.device_tracker import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import validate_config from homeassistant.util import Throttle -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=5) diff --git a/homeassistant/components/device_tracker/unifi.py b/homeassistant/components/device_tracker/unifi.py new file mode 100644 index 00000000000..24dd8e7db00 --- /dev/null +++ b/homeassistant/components/device_tracker/unifi.py @@ -0,0 +1,79 @@ +""" +homeassistant.components.device_tracker.unifi +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Device tracker platform that supports scanning a Unifi WAP controller +""" +import logging +import urllib + +from homeassistant.components.device_tracker import DOMAIN +from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers import validate_config + +# Unifi package doesn't list urllib3 as a requirement +REQUIREMENTS = ['urllib3', 'unifi==1.2.4'] +_LOGGER = logging.getLogger(__name__) +CONF_PORT = 'port' + + +def get_scanner(hass, config): + """ Sets up unifi device_tracker """ + from unifi.controller import Controller + + if not validate_config(config, {DOMAIN: [CONF_USERNAME, + CONF_PASSWORD]}, + _LOGGER): + _LOGGER.error('Invalid configuration') + return False + + this_config = config[DOMAIN] + host = this_config.get(CONF_HOST, 'localhost') + username = this_config.get(CONF_USERNAME) + password = this_config.get(CONF_PASSWORD) + + try: + port = int(this_config.get(CONF_PORT, 8443)) + except ValueError: + _LOGGER.error('Invalid port (must be numeric like 8443)') + return False + + try: + ctrl = Controller(host, username, password, port, 'v4') + except urllib.error.HTTPError as ex: + _LOGGER.error('Failed to connect to unifi: %s', ex) + return False + + return UnifiScanner(ctrl) + + +class UnifiScanner(object): + """Provide device_tracker support from Unifi WAP client data.""" + + def __init__(self, controller): + self._controller = controller + self._update() + + def _update(self): + try: + clients = self._controller.get_clients() + except urllib.error.HTTPError as ex: + _LOGGER.error('Failed to scan clients: %s', ex) + clients = [] + + self._clients = {client['mac']: client for client in clients} + + def scan_devices(self): + """ Scans for devices. """ + self._update() + return self._clients.keys() + + def get_device_name(self, mac): + """ Returns the name (if known) of the device. + + If a name has been set in Unifi, then return that, else + return the hostname if it has been detected. + """ + client = self._clients.get(mac, {}) + name = client.get('name') or client.get('hostname') + _LOGGER.debug('Device %s name %s', mac, name) + return name diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index cfd6ffd55eb..6dd4e667a75 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -1,6 +1,4 @@ """ -homeassistant.components.discovery -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Starts a service to scan in intervals for new devices. Will emit EVENT_PLATFORM_DISCOVERED whenever a new service has been discovered. @@ -13,8 +11,8 @@ import threading from homeassistant import bootstrap from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_PLATFORM_DISCOVERED, - ATTR_SERVICE, ATTR_DISCOVERED) + ATTR_DISCOVERED, ATTR_SERVICE, EVENT_HOMEASSISTANT_START, + EVENT_PLATFORM_DISCOVERED) DOMAIN = "discovery" REQUIREMENTS = ['netdisco==0.5.2'] @@ -29,7 +27,7 @@ SERVICE_SONOS = 'sonos' SERVICE_PLEX = 'plex_mediaserver' SERVICE_HANDLERS = { - SERVICE_WEMO: "switch", + SERVICE_WEMO: "wemo", SERVICE_CAST: "media_player", SERVICE_HUE: "light", SERVICE_NETGEAR: 'device_tracker', @@ -39,24 +37,41 @@ SERVICE_HANDLERS = { def listen(hass, service, callback): - """ - Setup listener for discovery of specific service. + """Setup listener for discovery of specific service. + Service can be a string or a list/tuple. """ - if isinstance(service, str): service = (service,) else: service = tuple(service) def discovery_event_listener(event): - """ Listens for discovery events. """ + """Listen for discovery events.""" if event.data[ATTR_SERVICE] in service: - callback(event.data[ATTR_SERVICE], event.data[ATTR_DISCOVERED]) + callback(event.data[ATTR_SERVICE], event.data.get(ATTR_DISCOVERED)) hass.bus.listen(EVENT_PLATFORM_DISCOVERED, discovery_event_listener) +def discover(hass, service, discovered=None, component=None, hass_config=None): + """Fire discovery event. + + Can ensure a component is loaded. + """ + if component is not None: + bootstrap.setup_component(hass, component, hass_config) + + data = { + ATTR_SERVICE: service + } + + if discovered is not None: + data[ATTR_DISCOVERED] = discovered + + hass.bus.fire(EVENT_PLATFORM_DISCOVERED, data) + + def setup(hass, config): """ Starts a discovery service. """ logger = logging.getLogger(__name__) diff --git a/homeassistant/components/downloader.py b/homeassistant/components/downloader.py index 3926495376c..6fbbd3f9473 100644 --- a/homeassistant/components/downloader.py +++ b/homeassistant/components/downloader.py @@ -6,8 +6,8 @@ Provides functionality to download files. For more details about this component, please refer to the documentation at https://home-assistant.io/components/downloader/ """ -import os import logging +import os import re import threading diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py index f1ce746b48e..f2172e7e5b6 100644 --- a/homeassistant/components/ecobee.py +++ b/homeassistant/components/ecobee.py @@ -1,40 +1,20 @@ """ homeassistant.components.ecobee -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Ecobee Component - -This component adds support for Ecobee3 Wireless Thermostats. -You will need to setup developer access to your thermostat, -and create and API key on the ecobee website. - -The first time you run this component you will see a configuration -component card in Home Assistant. This card will contain a PIN code -that you will need to use to authorize access to your thermostat. You -can do this at https://www.ecobee.com/consumerportal/index.html -Click My Apps, Add application, Enter Pin and click Authorize. - -After authorizing the application click the button in the configuration -card. Now your thermostat and sensors should shown in home-assistant. - -You can use the optional hold_temp parameter to set whether or not holds -are set indefintely or until the next scheduled event. - -ecobee: - api_key: asdfasdfasdfasdfasdfaasdfasdfasdfasdf - hold_temp: True +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Ecobee component +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/ecobee/ """ - -from datetime import timedelta import logging import os +from datetime import timedelta -from homeassistant.loader import get_component from homeassistant import bootstrap -from homeassistant.util import Throttle from homeassistant.const import ( - EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED, CONF_API_KEY) + ATTR_DISCOVERED, ATTR_SERVICE, CONF_API_KEY, EVENT_PLATFORM_DISCOVERED) +from homeassistant.loader import get_component +from homeassistant.util import Throttle DOMAIN = "ecobee" DISCOVER_THERMOSTAT = "ecobee.thermostat" @@ -82,7 +62,7 @@ def request_configuration(network, hass, config): def setup_ecobee(hass, network, config): - """ Setup ecobee thermostat """ + """ Setup Ecobee thermostat. """ # If ecobee has a PIN then it needs to be configured. if network.pin is not None: request_configuration(network, hass, config) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 06438c02140..b5e6fffb3d9 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -11,6 +11,7 @@ import logging from . import version, mdi_version import homeassistant.util as util from homeassistant.const import URL_ROOT, HTTP_OK +from homeassistant.components import api DOMAIN = 'frontend' DEPENDENCIES = ['api'] @@ -25,21 +26,23 @@ FRONTEND_URLS = [ re.compile(r'/states(/([a-zA-Z\._\-0-9/]+)|)'), ] +URL_API_BOOTSTRAP = "/api/bootstrap" + _FINGERPRINT = re.compile(r'^(\w+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE) def setup(hass, config): """ Setup serving the frontend. """ - if 'http' not in hass.config.components: - _LOGGER.error('Dependency http is not loaded') - return False - for url in FRONTEND_URLS: hass.http.register_path('GET', url, _handle_get_root, False) hass.http.register_path('GET', '/service_worker.js', _handle_get_service_worker, False) + # Bootstrap API + hass.http.register_path( + 'GET', URL_API_BOOTSTRAP, _handle_get_api_bootstrap) + # Static files hass.http.register_path( 'GET', re.compile(r'/static/(?P[a-zA-Z\._\-0-9/]+)'), @@ -54,6 +57,18 @@ def setup(hass, config): return True +def _handle_get_api_bootstrap(handler, path_match, data): + """ Returns all data needed to bootstrap Home Assistant. """ + hass = handler.server.hass + + handler.write_json({ + 'config': hass.config.as_dict(), + 'states': hass.states.all(), + 'events': api.events_json(hass), + 'services': api.services_json(hass), + }) + + def _handle_get_root(handler, path_match, data): """ Renders the frontend. """ handler.send_response(HTTP_OK) diff --git a/homeassistant/components/frontend/mdi_version.py b/homeassistant/components/frontend/mdi_version.py index 90765572d2c..cf3ee39d8f2 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 = "a1a203680639ff1abcc7b68cdb29c57a" +VERSION = "2f4adc5d3ad6d2f73bf69ed29b7594fd" diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index f207bcae379..c62b6c52b4d 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 = "833d09737fec24f9219efae87c5bfd2a" +VERSION = "a4d021cb50ed079fcfda7369ed2f0d4a" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 09d82fce309..7dd6f68c12f 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -1,7 +1,7 @@ -