diff --git a/.gitignore b/.gitignore
index ec8fbb301ae..fc7e301a093 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@ homeassistant/components/http/www_static/polymer/bower_components/*
!config/custom_components
config/custom_components/*
!config/custom_components/example.py
+!config/custom_components/hello_world.py
# Hide sublime text stuff
*.sublime-project
diff --git a/.travis.yml b/.travis.yml
index ff38ef3ced1..61ed87bf6b5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,6 +7,6 @@ install:
script:
- flake8 homeassistant --exclude bower_components,external
- pylint homeassistant
- - coverage run --source=homeassistant -m unittest discover test
+ - coverage run --source=homeassistant -m unittest discover ha_test
after_success:
- coveralls
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1d8ef8016e1..0f1c2c658bb 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,23 +1,6 @@
# Adding support for a new device
-You've probably came here beacuse you noticed that your favorite device is not supported and want to add it.
-
-First step is to decide under which component the device has to reside. Each component is responsible for a specific domain within Home Assistant. An example is the switch component, which is responsible for interaction with different types of switches. The switch component consists of the following files:
-
-**homeassistant/components/switch/\_\_init\_\_.py**
-Contains the Switch component code.
-
-**homeassistant/components/switch/wemo.py**
-Contains the code to interact with WeMo switches. Called if type=wemo in switch config.
-
-**homeassistant/components/switch/tellstick.py**
-Contains the code to interact with Tellstick switches. Called if type=tellstick in switch config.
-
-If a component exists, your job is easy. Have a look at how the component works with other platforms and create a similar file for the platform that you would like to add. If you cannot find a suitable component, you'll have to add it yourself. When writing a component try to structure it after the Switch component to maximize reusability.
-
-Communication between Home Assistant and devices should happen via third-party libraries that implement the device API. This will make sure the platform support code stays as small as possible.
-
-For help on building your component, please see the See the documentation on [further customizing Home Assistant](https://github.com/balloob/home-assistant#further-customizing-home-assistant).
+For help on building your component, please see the See the [developer documentation on home-assistant.io](https://home-assistant.io/developers/).
After you finish adding support for your device:
diff --git a/README.md b/README.md
index 956176e4962..62d20f7df4c 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
# Home Assistant [](https://travis-ci.org/balloob/home-assistant) [](https://coveralls.io/r/balloob/home-assistant?branch=master)
+This is the source for Home Assistant. For installation instructions, tutorials and the docs, please see [https://home-assistant.io](https://home-assistant.io). For a functioning demo frontend of Home Assistant, [click here](https://home-assistant.io/demo/).
+
Home Assistant is a home automation platform running on Python 3. The goal of Home Assistant is to be able to track and control all devices at home and offer a platform for automating control.
It offers the following functionality through built-in components:
@@ -23,7 +25,7 @@ Home Assistant also includes functionality for controlling HTPCs:
* Download files
* Open URLs in the default browser
-
+[](https://home-assistant.io/demo/)
The system is built modular so support for other devices or actions can be implemented easily. See also the [section on architecture](#architecture) and the [section on customizing](#customizing).
@@ -55,6 +57,8 @@ After you got the demo mode running it is time to enable some real components an
*Note:* you can append `?api_password=YOUR_PASSWORD` to the url of the web interface to log in automatically.
+*Note:* for the light and switch component, you can specify multiple platforms by using sequential sections: [switch], [switch 2], [switch 3] etc
+
### Philips Hue
To get Philips Hue working you will have to connect Home Assistant to the Hue bridge.
@@ -68,7 +72,7 @@ After that add the following lines to your `home-assistant.conf`:
```
[light]
-type=hue
+platform=hue
```
### Wireless router
@@ -77,7 +81,7 @@ Your wireless router is used to track which devices are connected. Three differe
```
[device_tracker]
-type=netgear
+platform=netgear
host=192.168.1.1
username=admin
password=MY_PASSWORD
@@ -87,336 +91,12 @@ password=MY_PASSWORD
*Note on luci:* before the Luci scanner can be used you have to install the luci RPC package on OpenWRT: `opkg install luci-mod-rpc`.
-Once tracking the `device_tracker` component will maintain a file in your config dir called `known_devices.csv`. Edit this file to adjust which devices have to be tracked.
+Once tracking, the `device_tracker` component will maintain a file in your config dir called `known_devices.csv`. Edit this file to adjust which devices have to be tracked.
-
-## Further customizing Home Assistant
-
-Home Assistant can be extended by components. Components can listen for- or trigger events and offer services. Components are written in Python and can do all the goodness that Python has to offer.
-
-Home Assistant offers [built-in components](#components) but it is easy to built your own. An example component can be found in [`/config/custom_components/example.py`](https://github.com/balloob/home-assistant/blob/master/config/custom_components/example.py).
-
-*Note:* Home Assistant will use the directory that contains your config file as the directory that holds your customizations. By default this is the `./config` folder but this can be pointed anywhere on the filesystem by using the `--config /YOUR/CONFIG/PATH/` argument.
-
-A component will be loaded on start if a section (ie. `[light]`) for it exists in the config file or a module that depends on the component is loaded. When loading a component Home Assistant will check the following paths:
-
- * <config file directory>/custom_components/<component name>.py
- * homeassistant/components/<component name>.py (built-in components)
-
-Once loaded, a component will only be setup if all dependencies can be loaded and are able to setup. Keep an eye on the logs to see if loading and setup of your component went well.
-
-*Warning:* You can override a built-in component by offering a component with the same name in your custom_components folder. This is not recommended and may lead to unexpected behavior!
-
-After a component is loaded the bootstrapper will call its setup method `setup(hass, config)`:
-
-| Parameter | Description |
-| --------- | ----------- |
-| hass | The Home Assistant object. Call its methods to track time, register services or listen for events. [Overview of available methods.](https://github.com/balloob/home-assistant/blob/master/homeassistant/__init__.py#L54) |
-| config | A dict containing the configuration. The keys of the config-dict are component names and the value is another dict with configuration attributes. |
-
-**Tips on using the Home Assistant object parameter**
-The Home Assistant object contains three objects to help you interact with the system.
-
-| Object | Description |
-| ------ | ----------- |
-| hass.states | This is the StateMachine. The StateMachine allows you to see which states are available and set/test states for specified entities. [See API](https://github.com/balloob/home-assistant/blob/master/homeassistant/__init__.py#L460). |
-| hass.events | This is the EventBus. The EventBus allows you to listen and trigger events. [See API](https://github.com/balloob/home-assistant/blob/master/homeassistant/__init__.py#L319). |
-| hass.services | This is the ServiceRegistry. The ServiceRegistry allows you to register services. [See API](https://github.com/balloob/home-assistant/blob/master/homeassistant/__init__.py#L541). |
-
-**Example on using the configuration parameter**
-If your configuration file containes the following lines:
+As an alternative to the router-based device tracking, it is possible to directly scan the network for devices by using nmap. The IP addresses to scan can be specified in any format that nmap understands, including the network-prefix notation (`192.168.1.1/24`) and the range notation (`192.168.1.1-255`).
```
-[example]
-host=paulusschoutsen.nl
+[device_tracker]
+platform=nmap_tracker
+hosts=192.168.1.1/24
```
-
-Then in the setup-method of your component you will be able to refer to `config[example][host]` to get the value `paulusschoutsen.nl`.
-
-If you want to get your component included with the Home Assistant distribution, please take a look at the [contributing page](https://github.com/balloob/home-assistant/blob/master/CONTRIBUTING.md).
-
-
-## Architecture
-
-The core of Home Assistant exists of three parts; an Event Bus for firing events, a State Machine that keeps track of the state of things and a Service Registry to manage services.
-
-
-
-For example to control the lights there are two components. One is the device_tracker that polls the wireless router for connected devices and updates the state of the tracked devices in the State Machine to be either 'Home' or 'Not Home'.
-
-When a state is changed a state_changed event is fired for which the device_sun_light_trigger component is listening. Based on the new state of the device combined with the state of the sun it will decide if it should turn the lights on or off:
-
- In the event that the state of device 'Paulus Nexus 5' changes to the 'Home' state:
- If the sun has set and the lights are not on:
- Turn on the lights
-
- In the event that the combined state of all tracked devices changes to 'Not Home':
- If the lights are on:
- Turn off the lights
-
- In the event of the sun setting:
- If the lights are off and the combined state of all tracked device equals 'Home':
- Turn on the lights
-
-By using the Bus as a central communication hub between components it is easy to replace components or add functionality. For example if you would want to change the way devices are detected you only have to write a component that updates the device states in the State Machine.
-
-
-### Components
-
-**sun**
-Tracks the state of the sun and when the next sun rising and setting will occur.
-Depends on: config variables common/latitude and common/longitude
-Action: maintains state of `weather.sun` including attributes `next_rising` and `next_setting`
-
-**device_tracker**
-Keeps track of which devices are currently home.
-Action: sets the state per device and maintains a combined state called `all_devices`. Keeps track of known devices in the file `config/known_devices.csv`.
-
-**light**
-Keeps track which lights are turned on and can control the lights. It has [4 built-in light profiles](https://github.com/balloob/home-assistant/blob/master/homeassistant/components/light/light_profiles.csv) which you're able to extend by putting a light_profiles.csv file in your config dir.
-
-Registers services `light/turn_on` and `light/turn_off` to turn a or all lights on or off.
-
-Optional service data:
- - `entity_id` - only act on specific light. Else targets all.
- - `transition_seconds` - seconds to take to swithc to new state.
- - `profile` - which light profile to use.
- - `xy_color` - two comma seperated floats that represent the color in XY
- - `rgb_color` - three comma seperated integers that represent the color in RGB
- - `brightness` - integer between 0 and 255 for how bright the color should be
-
-**switch**
-Keeps track which switches are in the network, their state and allows you to control them.
-
-Registers services `switch/turn_on` and `switch/turn_off` to turn a or all switches on or off.
-
-Optional service data:
- - `entity_id` - only act on specific switch. Else targets all.
-
-**device_sun_light_trigger**
-Turns lights on or off using a light control component based on state of the sun and devices that are home.
-Depends on: light control, track_sun, device_tracker
-Action:
-
- * Turns lights off when all devices leave home.
- * Turns lights on when a device is home while sun is setting.
- * Turns lights on when a device gets home after sun set.
-
-**chromecast**
-Registers 7 services to control playback on a Chromecast: `turn_off`, `volume_up`, `volume_down`, `media_play_pause`, `media_play`, `media_pause`, `media_next_track`.
-
-Registers three services to start playing YouTube video's on the ChromeCast.
-
-Service `chromecast/play_youtube_video` starts playing the specified video on the YouTube app on the ChromeCast. Specify video using `video` in service_data.
-
-Service `chromecast/start_fireplace` will start a YouTube movie simulating a fireplace and the `chromecast/start_epic_sax` service will start playing Epic Sax Guy 10h version.
-
-**keyboard**
-Registers services that will simulate key presses on the keyboard. It currently offers the following Buttons as a Service (BaaS): `keyboard/volume_up`, `keyboard/volume_down` and `keyboard/media_play_pause`
-This actor depends on: PyUserInput
-
-**downloader**
-Registers service `downloader/download_file` that will download files. File to download is specified in the `url` field in the service data.
-
-**browser**
-Registers service `browser/browse_url` that opens `url` as specified in event_data in the system default browser.
-
-**tellstick_sensor**
-Shows the values of that sensors that is connected to your Tellstick.
-
-
-## Rest API
-
-Home Assistent runs a webserver accessible on port 8123.
-
- * At http://127.0.0.1:8123/ it will provide an interface allowing you to control Home Assistant.
- * At http://localhost:8123/api/ it provides a password protected API.
-
-In the package `homeassistant.remote` a Python API on top of the HTTP API can be found.
-
-All API calls have to be accompanied by the header "HA-Access" with as value the api password (as specified in `home-assistant.conf`). The API returns only JSON encoded objects. Successful calls will return status code 200 or 201.
-
-Other status codes that can occur are:
- - 400 (Bad Request)
- - 401 (Unauthorized)
- - 404 (Not Found)
- - 405 (Method not allowed)
-
-The api supports the following actions:
-
-**/api - GET**
-Returns message if API is up and running.
-
-```json
-{
- "message": "API running."
-}
-```
-
-**/api/events - GET**
-Returns a dict with as keys the events and as value the number of listeners.
-
-```json
-[
- {
- "event": "state_changed",
- "listener_count": 5
- },
- {
- "event": "time_changed",
- "listener_count": 2
- }
-]
-```
-
-**/api/services - GET**
-Returns a dict with as keys the domain and as value a list of published services.
-
-```json
-[
- {
- "domain": "browser",
- "services": [
- "browse_url"
- ]
- },
- {
- "domain": "keyboard",
- "services": [
- "volume_up",
- "volume_down"
- ]
- }
-]
-```
-
-**/api/states - GET**
-Returns a dict with as keys the entity_ids and as value the state.
-
-```json
-[
- {
- "attributes": {
- "next_rising": "07:04:15 29-10-2013",
- "next_setting": "18:00:31 29-10-2013"
- },
- "entity_id": "sun.sun",
- "last_changed": "23:24:33 28-10-2013",
- "state": "below_horizon"
- },
- {
- "attributes": {},
- "entity_id": "process.Dropbox",
- "last_changed": "23:24:33 28-10-2013",
- "state": "on"
- }
-]
-```
-
-**/api/states/<entity_id>** - GET
-Returns the current state from an entity
-
-```json
-{
- "attributes": {
- "next_rising": "07:04:15 29-10-2013",
- "next_setting": "18:00:31 29-10-2013"
- },
- "entity_id": "sun.sun",
- "last_changed": "23:24:33 28-10-2013",
- "state": "below_horizon"
-}
-```
-
-**/api/states/<entity_id>** - POST
-Updates the current state of an entity. Returns status code 201 if successful with location header of updated resource and the new state in the body.
-parameter: new_state - string
-optional parameter: attributes - JSON encoded object
-
-```json
-{
- "attributes": {
- "next_rising": "07:04:15 29-10-2013",
- "next_setting": "18:00:31 29-10-2013"
- },
- "entity_id": "weather.sun",
- "last_changed": "23:24:33 28-10-2013",
- "state": "below_horizon"
-}
-```
-
-**/api/events/<event_type>** - POST
-Fires an event with event_type
-optional body: JSON encoded object that represents event_data
-
-```json
-{
- "message": "Event download_file fired."
-}
-```
-
-**/api/services/<domain>/<service>** - POST
-Calls a service within a specific domain.
-optional body: JSON encoded object that represents service_data
-
-```json
-{
- "message": "Service keyboard/volume_up called."
-}
-```
-
-**/api/event_forwarding** - POST
-Setup event forwarding to another Home Assistant instance.
-parameter: host - string
-parameter: api_password - string
-optional parameter: port - int
-
-```json
-{
- "message": "Event forwarding setup."
-}
-```
-
-**/api/event_forwarding** - DELETE
-Cancel event forwarding to another Home Assistant instance.
-parameter: host - string
-optional parameter: port - int
-
-If your client does not support DELETE HTTP requests you can add an optional attribute _METHOD and set its value to DELETE.
-
-```json
-{
- "message": "Event forwarding cancelled."
-}
-```
-
-
-## Connect multiple instances of Home Assistant
-
-Home Assistant supports running multiple synchronzied instances using a master-slave model. Slaves forward all local events fired and states set to the master instance which will then replicate it to each slave.
-
-Because each slave maintains its own ServiceRegistry it is possible to have multiple slaves respond to one service call.
-
-
-
-A slave instance can be started with the following code and has the same support for components as a master-instance.
-
-```python
-import homeassistant.remote as remote
-import homeassistant.components.http as http
-
-remote_api = remote.API("remote_host_or_ip", "remote_api_password")
-
-hass = remote.HomeAssistant(remote_api)
-
-http.setup(hass, "my_local_api_password")
-
-hass.start()
-hass.block_till_stopped()
-```
-
-
-## Related projects
-
-[Home Assistant API client in Ruby](https://github.com/balloob/home-assistant-ruby)
-[Home Assistant API client for Tasker for Android](https://github.com/balloob/home-assistant-android-tasker)
diff --git a/config/custom_components/example.py b/config/custom_components/example.py
index f7ece4db5ea..ee422174377 100644
--- a/config/custom_components/example.py
+++ b/config/custom_components/example.py
@@ -1,32 +1,120 @@
"""
custom_components.example
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~~~~~~~~
-Bare minimum what is needed for a component to be valid.
+Example component to target an entity_id to:
+ - turn it on at 7AM in the morning
+ - turn it on if anyone comes home and it is off
+ - turn it off if all lights are turned off
+ - turn it off if all people leave the house
+ - offer a service to turn it on for 10 seconds
"""
+import time
+import logging
+
+from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_ON, STATE_OFF
+import homeassistant.loader as loader
+from homeassistant.helpers import validate_config
+import homeassistant.components as core
# 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
-# If you are setting up a group but not using a group for anything,
-# don't depend on group
-DEPENDENCIES = []
+# We depend on group because group will be loaded after all the components that
+# initalize devices have been setup.
+DEPENDENCIES = ['group']
+
+# Configuration key for the entity id we are targetting
+CONF_TARGET = 'target'
+
+# Name of the service that we expose
+SERVICE_FLASH = 'flash'
+
+_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def setup(hass, config):
- """ Register services or listen for events that your component needs. """
+ """ Setup example component. """
- # Example of a service that prints the service call to the command-line.
- hass.services.register(DOMAIN, "example_service_name", print)
+ # Validate that all required config options are given
+ if not validate_config(config, {DOMAIN: [CONF_TARGET]}, _LOGGER):
+ return False
- # This prints a time change event to the command-line twice a minute.
- hass.track_time_change(print, second=[0, 30])
+ target_id = config[DOMAIN][CONF_TARGET]
- # See also (defined in homeassistant/__init__.py):
- # hass.track_state_change
- # hass.track_point_in_time
+ # 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)
+
+ # Tell the bootstrapper that we failed to initialize
+ return False
+
+ # We will use the component helper methods to check the states.
+ device_tracker = loader.get_component('device_tracker')
+ light = loader.get_component('light')
+
+ def track_devices(entity_id, old_state, new_state):
+ """ Called when the group.all devices change state. """
+
+ # If anyone comes home and the core is not on, turn it on.
+ if new_state.state == STATE_HOME and not core.is_on(hass, target_id):
+
+ core.turn_on(hass, target_id)
+
+ # If all people leave the house and the core 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)
+
+ # Register our track_devices method to receive state changes of the
+ # all tracked devices group.
+ hass.states.track_change(
+ device_tracker.ENTITY_ID_ALL_DEVICES, track_devices)
+
+ def wake_up(now):
+ """ Turn it on in the morning if there are people home and
+ it is not already on. """
+
+ if device_tracker.is_on(hass) and not core.is_on(hass, target_id):
+ _LOGGER.info('People home at 7AM, turning it on')
+ core.turn_on(hass, target_id)
+
+ # Register our wake_up service to be called at 7AM in the morning
+ hass.track_time_change(wake_up, hour=7, minute=0, second=0)
+
+ def all_lights_off(entity_id, old_state, new_state):
+ """ If all lights turn off, turn off. """
+
+ if core.is_on(hass, target_id):
+ _LOGGER.info('All lights have been turned off, turning it off')
+ core.turn_off(hass, target_id)
+
+ # Register our all_lights_off method to be called when all lights turn off
+ hass.states.track_change(
+ light.ENTITY_ID_ALL_LIGHTS, all_lights_off, STATE_ON, STATE_OFF)
+
+ def flash_service(call):
+ """ Service that will turn the target off for 10 seconds
+ if on and vice versa. """
+
+ if core.is_on(hass, target_id):
+ core.turn_off(hass, target_id)
+
+ time.sleep(10)
+
+ core.turn_on(hass, target_id)
+
+ else:
+ core.turn_on(hass, target_id)
+
+ time.sleep(10)
+
+ core.turn_off(hass, target_id)
+
+ # Register our service with HASS.
+ hass.services.register(DOMAIN, SERVICE_FLASH, flash_service)
# Tells the bootstrapper that the component was succesfully initialized
return True
diff --git a/config/custom_components/hello_world.py b/config/custom_components/hello_world.py
new file mode 100644
index 00000000000..be1b935c8ad
--- /dev/null
+++ b/config/custom_components/hello_world.py
@@ -0,0 +1,22 @@
+"""
+custom_components.hello_world
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Implements the bare minimum that a component should implement.
+"""
+
+# 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
+DEPENDENCIES = []
+
+
+def setup(hass, config):
+ """ Setup our skeleton component. """
+
+ # 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 True
diff --git a/config/home-assistant.conf.example b/config/home-assistant.conf.example
index 9eb0cf4a90e..0edd11e98f6 100644
--- a/config/home-assistant.conf.example
+++ b/config/home-assistant.conf.example
@@ -9,16 +9,19 @@ api_password=mypass
# development=1
[light]
-type=hue
+platform=hue
[device_tracker]
-# The following types are available: netgear, tomato, luci
-type=netgear
+# The following types are available: netgear, tomato, luci, nmap_tracker
+platform=netgear
host=192.168.1.1
username=admin
password=PASSWORD
# http_id is needed for Tomato routers only
# http_id=ABCDEFGHH
+# For nmap_tracker, only the IP addresses to scan are needed:
+# hosts=192.168.1.1/24 # netmask prefix notation or
+# hosts=192.168.1.1-255 # address range
[chromecast]
# Optional: hard code the hosts (comma seperated) to find chromecasts
@@ -26,7 +29,7 @@ password=PASSWORD
# hosts=192.168.1.9,192.168.1.12
[switch]
-type=wemo
+platform=wemo
# Optional: hard code the hosts (comma seperated) to avoid scanning the network
# hosts=192.168.1.9,192.168.1.12
@@ -53,6 +56,12 @@ xbmc=XBMC.App
[example]
+[simple_alarm]
+# Which light/light group has to flash when a known device comes home
+known_light=light.Bowl
+# Which light/light group has to flash red when light turns on while no one home
+unknown_light=group.living_room
+
[browser]
[keyboard]
diff --git a/ha_test/config/custom_components/device_tracker/test.py b/ha_test/config/custom_components/device_tracker/test.py
new file mode 100644
index 00000000000..50cc2bff9d4
--- /dev/null
+++ b/ha_test/config/custom_components/device_tracker/test.py
@@ -0,0 +1,41 @@
+"""
+custom_components.device_tracker.test
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Provides a mock device scanner.
+"""
+
+
+def get_scanner(hass, config):
+ """ Returns a mock scanner. """
+ return SCANNER
+
+
+class MockScanner(object):
+ """ Mock device scanner. """
+
+ def __init__(self):
+ """ Initialize the MockScanner. """
+ self.devices_home = []
+
+ def come_home(self, device):
+ """ Make a device come home. """
+ self.devices_home.append(device)
+
+ def leave_home(self, device):
+ """ Make a device leave the house. """
+ self.devices_home.remove(device)
+
+ def scan_devices(self):
+ """ Returns a list of fake devices. """
+
+ return list(self.devices_home)
+
+ def get_device_name(self, device):
+ """
+ Returns a name for a mock device.
+ Returns None for dev1 for testing.
+ """
+ return None if device == 'dev1' else device.upper()
+
+SCANNER = MockScanner()
diff --git a/ha_test/config/custom_components/light/test.py b/ha_test/config/custom_components/light/test.py
new file mode 100644
index 00000000000..0ed04a21717
--- /dev/null
+++ b/ha_test/config/custom_components/light/test.py
@@ -0,0 +1,29 @@
+"""
+custom_components.light.test
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Provides a mock switch platform.
+
+Call init before using it in your tests to ensure clean test data.
+"""
+from homeassistant.const import STATE_ON, STATE_OFF
+from ha_test.helpers import MockToggleDevice
+
+
+DEVICES = []
+
+
+def init(empty=False):
+ """ (re-)initalizes the platform with devices. """
+ global DEVICES
+
+ DEVICES = [] if empty else [
+ MockToggleDevice('Ceiling', STATE_ON),
+ MockToggleDevice('Ceiling', STATE_OFF),
+ MockToggleDevice(None, STATE_OFF)
+ ]
+
+
+def get_lights(hass, config):
+ """ Returns mock devices. """
+ return DEVICES
diff --git a/ha_test/config/custom_components/switch/test.py b/ha_test/config/custom_components/switch/test.py
new file mode 100644
index 00000000000..682c27f695f
--- /dev/null
+++ b/ha_test/config/custom_components/switch/test.py
@@ -0,0 +1,29 @@
+"""
+custom_components.switch.test
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Provides a mock switch platform.
+
+Call init before using it in your tests to ensure clean test data.
+"""
+from homeassistant.const import STATE_ON, STATE_OFF
+from ha_test.helpers import MockToggleDevice
+
+
+DEVICES = []
+
+
+def init(empty=False):
+ """ (re-)initalizes the platform with devices. """
+ global DEVICES
+
+ DEVICES = [] if empty else [
+ MockToggleDevice('AC', STATE_ON),
+ MockToggleDevice('AC', STATE_OFF),
+ MockToggleDevice(None, STATE_OFF)
+ ]
+
+
+def get_switches(hass, config):
+ """ Returns mock devices. """
+ return DEVICES
diff --git a/ha_test/helpers.py b/ha_test/helpers.py
new file mode 100644
index 00000000000..f04dac72553
--- /dev/null
+++ b/ha_test/helpers.py
@@ -0,0 +1,77 @@
+"""
+ha_test.helper
+~~~~~~~~~~~~~
+
+Helper method for writing tests.
+"""
+import os
+
+import homeassistant as ha
+from homeassistant.helpers import ToggleDevice
+from homeassistant.const import STATE_ON, STATE_OFF
+
+
+def get_test_home_assistant():
+ """ Returns a Home Assistant object pointing at test config dir. """
+ hass = ha.HomeAssistant()
+ hass.config_dir = os.path.join(os.path.dirname(__file__), "config")
+
+ return hass
+
+
+def mock_service(hass, domain, service):
+ """
+ Sets up a fake service.
+ Returns a list that logs all calls to fake service.
+ """
+ calls = []
+
+ hass.services.register(
+ domain, service, lambda call: calls.append(call))
+
+ return calls
+
+
+class MockModule(object):
+ """ Provides a fake module. """
+
+ def __init__(self, domain, dependencies=[], setup=None):
+ self.DOMAIN = domain
+ self.DEPENDENCIES = dependencies
+ # Setup a mock setup if none given.
+ self.setup = lambda hass, config: False if setup is None else setup
+
+
+class MockToggleDevice(ToggleDevice):
+ """ Provides a mock toggle device. """
+ def __init__(self, name, state):
+ self.name = name
+ self.state = state
+ self.calls = []
+
+ def get_name(self):
+ """ Returns the name of the device if any. """
+ self.calls.append(('get_name', {}))
+ return self.name
+
+ def turn_on(self, **kwargs):
+ """ Turn the device on. """
+ self.calls.append(('turn_on', kwargs))
+ self.state = STATE_ON
+
+ def turn_off(self, **kwargs):
+ """ Turn the device off. """
+ self.calls.append(('turn_off', kwargs))
+ self.state = STATE_OFF
+
+ def is_on(self):
+ """ True if device is on. """
+ self.calls.append(('is_on', {}))
+ return self.state == STATE_ON
+
+ def last_call(self, method=None):
+ if method is None:
+ return self.calls[-1]
+ else:
+ return next(call for call in reversed(self.calls)
+ if call[0] == method)
diff --git a/test/test_component_chromecast.py b/ha_test/test_component_chromecast.py
similarity index 58%
rename from test/test_component_chromecast.py
rename to ha_test/test_component_chromecast.py
index 27e5bbb0370..75ac9765c63 100644
--- a/test/test_component_chromecast.py
+++ b/ha_test/test_component_chromecast.py
@@ -1,6 +1,6 @@
"""
-test.test_component_chromecast
-~~~~~~~~~~~
+ha_test.test_component_chromecast
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests Chromecast component.
"""
@@ -9,9 +9,13 @@ import logging
import unittest
import homeassistant as ha
-import homeassistant.components as components
+from homeassistant.const import (
+ SERVICE_TURN_OFF, SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN,
+ SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE,
+ SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK, ATTR_ENTITY_ID,
+ CONF_HOSTS)
import homeassistant.components.chromecast as chromecast
-from helper import mock_service
+from helpers import mock_service
def setUpModule(): # pylint: disable=invalid-name
@@ -33,7 +37,7 @@ class TestChromecast(unittest.TestCase):
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
- self.hass._pool.stop()
+ self.hass.stop()
def test_is_on(self):
""" Test is_on method. """
@@ -45,37 +49,36 @@ class TestChromecast(unittest.TestCase):
Test if the call service methods conver to correct service calls.
"""
services = {
- components.SERVICE_TURN_OFF: chromecast.turn_off,
- components.SERVICE_VOLUME_UP: chromecast.volume_up,
- components.SERVICE_VOLUME_DOWN: chromecast.volume_down,
- components.SERVICE_MEDIA_PLAY_PAUSE: chromecast.media_play_pause,
- components.SERVICE_MEDIA_PLAY: chromecast.media_play,
- components.SERVICE_MEDIA_PAUSE: chromecast.media_pause,
- components.SERVICE_MEDIA_NEXT_TRACK: chromecast.media_next_track,
- components.SERVICE_MEDIA_PREV_TRACK: chromecast.media_prev_track
+ SERVICE_TURN_OFF: chromecast.turn_off,
+ SERVICE_VOLUME_UP: chromecast.volume_up,
+ SERVICE_VOLUME_DOWN: chromecast.volume_down,
+ SERVICE_MEDIA_PLAY_PAUSE: chromecast.media_play_pause,
+ SERVICE_MEDIA_PLAY: chromecast.media_play,
+ SERVICE_MEDIA_PAUSE: chromecast.media_pause,
+ SERVICE_MEDIA_NEXT_TRACK: chromecast.media_next_track,
+ SERVICE_MEDIA_PREV_TRACK: chromecast.media_prev_track
}
for service_name, service_method in services.items():
calls = mock_service(self.hass, chromecast.DOMAIN, service_name)
service_method(self.hass)
- self.hass._pool.block_till_done()
+ self.hass.pool.block_till_done()
self.assertEqual(1, len(calls))
call = calls[-1]
- self.assertEqual(call.domain, chromecast.DOMAIN)
- self.assertEqual(call.service, service_name)
- self.assertEqual(call.data, {})
+ self.assertEqual(chromecast.DOMAIN, call.domain)
+ self.assertEqual(service_name, call.service)
service_method(self.hass, self.test_entity)
- self.hass._pool.block_till_done()
+ self.hass.pool.block_till_done()
self.assertEqual(2, len(calls))
call = calls[-1]
- self.assertEqual(call.domain, chromecast.DOMAIN)
- self.assertEqual(call.service, service_name)
- self.assertEqual(call.data,
- {components.ATTR_ENTITY_ID: self.test_entity})
+ self.assertEqual(chromecast.DOMAIN, call.domain)
+ self.assertEqual(service_name, call.service)
+ self.assertEqual(self.test_entity,
+ call.data.get(ATTR_ENTITY_ID))
def test_setup(self):
"""
@@ -84,4 +87,4 @@ class TestChromecast(unittest.TestCase):
In an ideal world we would create a mock pychromecast API..
"""
self.assertFalse(chromecast.setup(
- self.hass, {chromecast.DOMAIN: {ha.CONF_HOSTS: '127.0.0.1'}}))
+ self.hass, {chromecast.DOMAIN: {CONF_HOSTS: '127.0.0.1'}}))
diff --git a/test/test_component_core.py b/ha_test/test_component_core.py
similarity index 57%
rename from test/test_component_core.py
rename to ha_test/test_component_core.py
index eb56112f906..2c53d578277 100644
--- a/test/test_component_core.py
+++ b/ha_test/test_component_core.py
@@ -1,6 +1,6 @@
"""
-test.test_component_core
-~~~~~~~~~~~~~~~~~~~~~~~~
+ha_test.test_component_core
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests core compoments.
"""
@@ -9,6 +9,8 @@ import unittest
import homeassistant as ha
import homeassistant.loader as loader
+from homeassistant.const import (
+ STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF)
import homeassistant.components as comps
@@ -21,12 +23,12 @@ class TestComponentsCore(unittest.TestCase):
loader.prepare(self.hass)
self.assertTrue(comps.setup(self.hass, {}))
- self.hass.states.set('light.Bowl', comps.STATE_ON)
- self.hass.states.set('light.Ceiling', comps.STATE_OFF)
+ self.hass.states.set('light.Bowl', STATE_ON)
+ self.hass.states.set('light.Ceiling', STATE_OFF)
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
- self.hass._pool.stop()
+ self.hass.stop()
def test_is_on(self):
""" Test is_on method. """
@@ -38,11 +40,11 @@ class TestComponentsCore(unittest.TestCase):
""" Test turn_on method. """
runs = []
self.hass.services.register(
- 'light', comps.SERVICE_TURN_ON, lambda x: runs.append(1))
+ 'light', SERVICE_TURN_ON, lambda x: runs.append(1))
comps.turn_on(self.hass, 'light.Ceiling')
- self.hass._pool.block_till_done()
+ self.hass.pool.block_till_done()
self.assertEqual(1, len(runs))
@@ -50,24 +52,10 @@ class TestComponentsCore(unittest.TestCase):
""" Test turn_off method. """
runs = []
self.hass.services.register(
- 'light', comps.SERVICE_TURN_OFF, lambda x: runs.append(1))
+ 'light', SERVICE_TURN_OFF, lambda x: runs.append(1))
comps.turn_off(self.hass, 'light.Bowl')
- self.hass._pool.block_till_done()
+ self.hass.pool.block_till_done()
self.assertEqual(1, len(runs))
-
- def test_extract_entity_ids(self):
- """ Test extract_entity_ids method. """
- call = ha.ServiceCall('light', 'turn_on',
- {comps.ATTR_ENTITY_ID: 'light.Bowl'})
-
- self.assertEqual(['light.Bowl'],
- comps.extract_entity_ids(self.hass, call))
-
- call = ha.ServiceCall('light', 'turn_on',
- {comps.ATTR_ENTITY_ID: ['light.Bowl']})
-
- self.assertEqual(['light.Bowl'],
- comps.extract_entity_ids(self.hass, call))
diff --git a/ha_test/test_component_demo.py b/ha_test/test_component_demo.py
new file mode 100644
index 00000000000..b687653a0bc
--- /dev/null
+++ b/ha_test/test_component_demo.py
@@ -0,0 +1,74 @@
+"""
+ha_test.test_component_demo
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Tests demo component.
+"""
+# pylint: disable=too-many-public-methods,protected-access
+import unittest
+
+import homeassistant as ha
+import homeassistant.components.demo as demo
+from homeassistant.const import (
+ SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON, STATE_OFF, ATTR_ENTITY_ID)
+
+
+class TestDemo(unittest.TestCase):
+ """ Test the demo module. """
+
+ def setUp(self): # pylint: disable=invalid-name
+ self.hass = ha.HomeAssistant()
+
+ def tearDown(self): # pylint: disable=invalid-name
+ """ Stop down stuff we started. """
+ self.hass.stop()
+
+ def test_services(self):
+ """ Test the demo services. """
+ # Test turning on and off different types
+ demo.setup(self.hass, {})
+
+ for domain in ('light', 'switch'):
+ # Focus on 1 entity
+ entity_id = self.hass.states.entity_ids(domain)[0]
+
+ self.hass.services.call(
+ domain, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id})
+
+ self.hass.pool.block_till_done()
+
+ self.assertEqual(STATE_ON, self.hass.states.get(entity_id).state)
+
+ self.hass.services.call(
+ domain, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id})
+
+ self.hass.pool.block_till_done()
+
+ self.assertEqual(STATE_OFF, self.hass.states.get(entity_id).state)
+
+ # Act on all
+ self.hass.services.call(domain, SERVICE_TURN_ON)
+
+ self.hass.pool.block_till_done()
+
+ for entity_id in self.hass.states.entity_ids(domain):
+ self.assertEqual(
+ STATE_ON, self.hass.states.get(entity_id).state)
+
+ self.hass.services.call(domain, SERVICE_TURN_OFF)
+
+ self.hass.pool.block_till_done()
+
+ for entity_id in self.hass.states.entity_ids(domain):
+ self.assertEqual(
+ STATE_OFF, self.hass.states.get(entity_id).state)
+
+ def test_hiding_demo_state(self):
+ """ Test if you can hide the demo card. """
+ demo.setup(self.hass, {demo.DOMAIN: {'hide_demo_state': '1'}})
+
+ self.assertIsNone(self.hass.states.get('a.Demo_Mode'))
+
+ demo.setup(self.hass, {demo.DOMAIN: {'hide_demo_state': '0'}})
+
+ self.assertIsNotNone(self.hass.states.get('a.Demo_Mode'))
diff --git a/ha_test/test_component_device_scanner.py b/ha_test/test_component_device_scanner.py
new file mode 100644
index 00000000000..3c6385bc42f
--- /dev/null
+++ b/ha_test/test_component_device_scanner.py
@@ -0,0 +1,190 @@
+"""
+ha_test.test_component_group
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Tests the group compoments.
+"""
+# pylint: disable=protected-access,too-many-public-methods
+import unittest
+from datetime import datetime, timedelta
+import logging
+import os
+
+import homeassistant as ha
+import homeassistant.loader as loader
+from homeassistant.const import (
+ STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, CONF_PLATFORM)
+import homeassistant.components.device_tracker as device_tracker
+
+from helpers import get_test_home_assistant
+
+
+def setUpModule(): # pylint: disable=invalid-name
+ """ Setup to ignore group errors. """
+ logging.disable(logging.CRITICAL)
+
+
+class TestComponentsDeviceTracker(unittest.TestCase):
+ """ Tests homeassistant.components.device_tracker module. """
+
+ def setUp(self): # pylint: disable=invalid-name
+ """ Init needed objects. """
+ self.hass = get_test_home_assistant()
+ loader.prepare(self.hass)
+
+ self.known_dev_path = self.hass.get_config_path(
+ device_tracker.KNOWN_DEVICES_FILE)
+
+ def tearDown(self): # pylint: disable=invalid-name
+ """ Stop down stuff we started. """
+ self.hass.stop()
+
+ if os.path.isfile(self.known_dev_path):
+ os.remove(self.known_dev_path)
+
+ def test_is_on(self):
+ """ Test is_on method. """
+ entity_id = device_tracker.ENTITY_ID_FORMAT.format('test')
+
+ self.hass.states.set(entity_id, STATE_HOME)
+
+ self.assertTrue(device_tracker.is_on(self.hass, entity_id))
+
+ self.hass.states.set(entity_id, STATE_NOT_HOME)
+
+ self.assertFalse(device_tracker.is_on(self.hass, entity_id))
+
+ def test_setup(self):
+ """ Test setup method. """
+ # Bogus config
+ self.assertFalse(device_tracker.setup(self.hass, {}))
+
+ self.assertFalse(
+ device_tracker.setup(self.hass, {device_tracker.DOMAIN: {}}))
+
+ # Test with non-existing component
+ self.assertFalse(device_tracker.setup(
+ self.hass, {device_tracker.DOMAIN: {CONF_PLATFORM: 'nonexisting'}}
+ ))
+
+ # Test with a bad known device file around
+ with open(self.known_dev_path, 'w') as fil:
+ fil.write("bad data\nbad data\n")
+
+ self.assertFalse(device_tracker.setup(self.hass, {
+ device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}
+ }))
+
+ def test_device_tracker(self):
+ """ Test the device tracker class. """
+ scanner = loader.get_component(
+ 'device_tracker.test').get_scanner(None, None)
+
+ scanner.come_home('dev1')
+ scanner.come_home('dev2')
+
+ self.assertTrue(device_tracker.setup(self.hass, {
+ device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}
+ }))
+
+ # Ensure a new known devices file has been created.
+ # Since the device_tracker uses a set internally we cannot
+ # know what the order of the devices in the known devices file is.
+ # To ensure all the three expected lines are there, we sort the file
+ with open(self.known_dev_path) as fil:
+ self.assertEqual(
+ ['dev1,unknown_device,0,\n', 'dev2,DEV2,0,\n',
+ 'device,name,track,picture\n'],
+ sorted(fil))
+
+ # Write one where we track dev1, dev2
+ with open(self.known_dev_path, 'w') as fil:
+ fil.write('device,name,track,picture\n')
+ fil.write('dev1,Device 1,1,http://example.com/dev1.jpg\n')
+ fil.write('dev2,Device 2,1,http://example.com/dev2.jpg\n')
+
+ scanner.leave_home('dev1')
+ scanner.come_home('dev3')
+
+ self.hass.services.call(
+ device_tracker.DOMAIN,
+ device_tracker.SERVICE_DEVICE_TRACKER_RELOAD)
+
+ self.hass.pool.block_till_done()
+
+ dev1 = device_tracker.ENTITY_ID_FORMAT.format('Device_1')
+ dev2 = device_tracker.ENTITY_ID_FORMAT.format('Device_2')
+ dev3 = device_tracker.ENTITY_ID_FORMAT.format('DEV3')
+
+ now = datetime.now()
+ nowNext = now + timedelta(seconds=ha.TIMER_INTERVAL)
+ nowAlmostMinGone = (now + device_tracker.TIME_DEVICE_NOT_FOUND -
+ timedelta(seconds=1))
+ nowMinGone = nowAlmostMinGone + timedelta(seconds=2)
+
+ # Test initial is correct
+ self.assertTrue(device_tracker.is_on(self.hass))
+ self.assertFalse(device_tracker.is_on(self.hass, dev1))
+ self.assertTrue(device_tracker.is_on(self.hass, dev2))
+ self.assertIsNone(self.hass.states.get(dev3))
+
+ self.assertEqual(
+ 'http://example.com/dev1.jpg',
+ self.hass.states.get(dev1).attributes.get(ATTR_ENTITY_PICTURE))
+ self.assertEqual(
+ 'http://example.com/dev2.jpg',
+ self.hass.states.get(dev2).attributes.get(ATTR_ENTITY_PICTURE))
+
+ # Test if dev3 got added to known dev file
+ with open(self.known_dev_path) as fil:
+ self.assertEqual('dev3,DEV3,0,\n', list(fil)[-1])
+
+ # Change dev3 to track
+ with open(self.known_dev_path, 'w') as fil:
+ fil.write("device,name,track,picture\n")
+ fil.write('dev1,Device 1,1,http://example.com/picture.jpg\n')
+ fil.write('dev2,Device 2,1,http://example.com/picture.jpg\n')
+ fil.write('dev3,DEV3,1,\n')
+
+ # reload dev file
+ scanner.come_home('dev1')
+ scanner.leave_home('dev2')
+
+ self.hass.services.call(
+ device_tracker.DOMAIN,
+ device_tracker.SERVICE_DEVICE_TRACKER_RELOAD)
+
+ self.hass.pool.block_till_done()
+
+ # Test what happens if a device comes home and another leaves
+ self.assertTrue(device_tracker.is_on(self.hass))
+ self.assertTrue(device_tracker.is_on(self.hass, dev1))
+ # Dev2 will still be home because of the error margin on time
+ self.assertTrue(device_tracker.is_on(self.hass, dev2))
+ # dev3 should be tracked now after we reload the known devices
+ self.assertTrue(device_tracker.is_on(self.hass, dev3))
+
+ self.assertIsNone(
+ self.hass.states.get(dev3).attributes.get(ATTR_ENTITY_PICTURE))
+
+ # Test if device leaves what happens, test the time span
+ self.hass.bus.fire(
+ ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: nowAlmostMinGone})
+
+ self.hass.pool.block_till_done()
+
+ self.assertTrue(device_tracker.is_on(self.hass))
+ self.assertTrue(device_tracker.is_on(self.hass, dev1))
+ # Dev2 will still be home because of the error time
+ self.assertTrue(device_tracker.is_on(self.hass, dev2))
+ self.assertTrue(device_tracker.is_on(self.hass, dev3))
+
+ # Now test if gone for longer then error margin
+ self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: nowMinGone})
+
+ self.hass.pool.block_till_done()
+
+ self.assertTrue(device_tracker.is_on(self.hass))
+ self.assertTrue(device_tracker.is_on(self.hass, dev1))
+ self.assertFalse(device_tracker.is_on(self.hass, dev2))
+ self.assertTrue(device_tracker.is_on(self.hass, dev3))
diff --git a/test/test_component_group.py b/ha_test/test_component_group.py
similarity index 74%
rename from test/test_component_group.py
rename to ha_test/test_component_group.py
index 2af99e3c209..4e6307aa2b5 100644
--- a/test/test_component_group.py
+++ b/ha_test/test_component_group.py
@@ -1,6 +1,6 @@
"""
-test.test_component_group
-~~~~~~~~~~~~~~~~~~~~~~~~~
+ha_test.test_component_group
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests the group compoments.
"""
@@ -9,7 +9,7 @@ import unittest
import logging
import homeassistant as ha
-import homeassistant.components as comps
+from homeassistant.const import STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME
import homeassistant.components.group as group
@@ -25,9 +25,9 @@ class TestComponentsGroup(unittest.TestCase):
""" Init needed objects. """
self.hass = ha.HomeAssistant()
- self.hass.states.set('light.Bowl', comps.STATE_ON)
- self.hass.states.set('light.Ceiling', comps.STATE_OFF)
- self.hass.states.set('switch.AC', comps.STATE_OFF)
+ self.hass.states.set('light.Bowl', STATE_ON)
+ self.hass.states.set('light.Ceiling', STATE_OFF)
+ self.hass.states.set('switch.AC', STATE_OFF)
group.setup_group(self.hass, 'init_group',
['light.Bowl', 'light.Ceiling'], False)
group.setup_group(self.hass, 'mixed_group',
@@ -40,43 +40,27 @@ class TestComponentsGroup(unittest.TestCase):
""" Stop down stuff we started. """
self.hass.stop()
- def test_setup_and_monitor_group(self):
+ def test_setup_group(self):
""" Test setup_group method. """
-
- # Test if group setup in our init mode is ok
- self.assertIn(self.group_name, self.hass.states.entity_ids)
-
- group_state = self.hass.states.get(self.group_name)
- self.assertEqual(comps.STATE_ON, group_state.state)
- self.assertTrue(group_state.attributes[group.ATTR_AUTO])
-
- # Turn the Bowl off and see if group turns off
- self.hass.states.set('light.Bowl', comps.STATE_OFF)
-
- self.hass._pool.block_till_done()
-
- group_state = self.hass.states.get(self.group_name)
- self.assertEqual(comps.STATE_OFF, group_state.state)
-
- # Turn the Ceiling on and see if group turns on
- self.hass.states.set('light.Ceiling', comps.STATE_ON)
-
- self.hass._pool.block_till_done()
-
- group_state = self.hass.states.get(self.group_name)
- self.assertEqual(comps.STATE_ON, group_state.state)
-
# Try to setup a group with mixed groupable states
- self.hass.states.set('device_tracker.Paulus', comps.STATE_HOME)
- self.assertFalse(group.setup_group(
+ self.hass.states.set('device_tracker.Paulus', STATE_HOME)
+ self.assertTrue(group.setup_group(
self.hass, 'person_and_light',
['light.Bowl', 'device_tracker.Paulus']))
+ self.assertEqual(
+ STATE_ON,
+ self.hass.states.get(
+ group.ENTITY_ID_FORMAT.format('person_and_light')).state)
# Try to setup a group with a non existing state
- self.assertNotIn('non.existing', self.hass.states.entity_ids)
- self.assertFalse(group.setup_group(
+ self.assertNotIn('non.existing', self.hass.states.entity_ids())
+ self.assertTrue(group.setup_group(
self.hass, 'light_and_nothing',
['light.Bowl', 'non.existing']))
+ self.assertEqual(
+ STATE_ON,
+ self.hass.states.get(
+ group.ENTITY_ID_FORMAT.format('light_and_nothing')).state)
# Try to setup a group with non groupable states
self.hass.states.set('cast.living_room', "Plex")
@@ -89,23 +73,37 @@ class TestComponentsGroup(unittest.TestCase):
# Try to setup an empty group
self.assertFalse(group.setup_group(self.hass, 'nothing', []))
- def test__get_group_type(self):
- """ Test _get_group_type method. """
- self.assertEqual('on_off', group._get_group_type(comps.STATE_ON))
- self.assertEqual('on_off', group._get_group_type(comps.STATE_OFF))
- self.assertEqual('home_not_home',
- group._get_group_type(comps.STATE_HOME))
- self.assertEqual('home_not_home',
- group._get_group_type(comps.STATE_NOT_HOME))
+ def test_monitor_group(self):
+ """ Test if the group keeps track of states. """
- # Unsupported state
- self.assertIsNone(group._get_group_type('unsupported_state'))
+ # Test if group setup in our init mode is ok
+ self.assertIn(self.group_name, self.hass.states.entity_ids())
+
+ group_state = self.hass.states.get(self.group_name)
+ self.assertEqual(STATE_ON, group_state.state)
+ self.assertTrue(group_state.attributes[group.ATTR_AUTO])
+
+ # Turn the Bowl off and see if group turns off
+ self.hass.states.set('light.Bowl', STATE_OFF)
+
+ self.hass.pool.block_till_done()
+
+ group_state = self.hass.states.get(self.group_name)
+ self.assertEqual(STATE_OFF, group_state.state)
+
+ # Turn the Ceiling on and see if group turns on
+ self.hass.states.set('light.Ceiling', STATE_ON)
+
+ self.hass.pool.block_till_done()
+
+ group_state = self.hass.states.get(self.group_name)
+ self.assertEqual(STATE_ON, group_state.state)
def test_is_on(self):
""" Test is_on method. """
self.assertTrue(group.is_on(self.hass, self.group_name))
- self.hass.states.set('light.Bowl', comps.STATE_OFF)
- self.hass._pool.block_till_done()
+ self.hass.states.set('light.Bowl', STATE_OFF)
+ self.hass.pool.block_till_done()
self.assertFalse(group.is_on(self.hass, self.group_name))
# Try on non existing state
@@ -159,5 +157,5 @@ class TestComponentsGroup(unittest.TestCase):
group_state = self.hass.states.get(
group.ENTITY_ID_FORMAT.format('second_group'))
- self.assertEqual(comps.STATE_ON, group_state.state)
+ self.assertEqual(STATE_ON, group_state.state)
self.assertFalse(group_state.attributes[group.ATTR_AUTO])
diff --git a/test/test_component_http.py b/ha_test/test_component_http.py
similarity index 64%
rename from test/test_component_http.py
rename to ha_test/test_component_http.py
index 640600becca..98b976cf099 100644
--- a/test/test_component_http.py
+++ b/ha_test/test_component_http.py
@@ -1,6 +1,6 @@
"""
-test.test_component_http
-~~~~~~~~~~~~~~~~~~~~~~~~
+ha_test.test_component_http
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests Home Assistant HTTP component does what it should do.
"""
@@ -52,30 +52,50 @@ def setUpModule(): # pylint: disable=invalid-name
def tearDownModule(): # pylint: disable=invalid-name
""" Stops the Home Assistant server. """
- global hass
-
hass.stop()
class TestHTTP(unittest.TestCase):
""" Test the HTTP debug interface and API. """
- def test_get_frontend(self):
+ def test_setup(self):
+ """ Test http.setup. """
+ self.assertFalse(http.setup(hass, {}))
+ self.assertFalse(http.setup(hass, {http.DOMAIN: {}}))
+
+ def test_frontend_and_static(self):
""" Tests if we can get the frontend. """
req = requests.get(_url(""))
self.assertEqual(200, req.status_code)
+ # Test we can retrieve frontend.js
frontendjs = re.search(
r'(?P\/static\/frontend-[A-Za-z0-9]{32}.html)',
- req.text).groups(0)[0]
+ req.text)
self.assertIsNotNone(frontendjs)
- req = requests.get(_url(frontendjs))
+ req = requests.head(_url(frontendjs.groups(0)[0]))
self.assertEqual(200, req.status_code)
+ # Test auto filling in api password
+ req = requests.get(
+ _url("?{}={}".format(http.DATA_API_PASSWORD, API_PASSWORD)))
+
+ self.assertEqual(200, req.status_code)
+
+ auth_text = re.search(r"auth='{}'".format(API_PASSWORD), req.text)
+
+ self.assertIsNotNone(auth_text)
+
+ # Test 404
+ self.assertEqual(404, requests.get(_url("/not-existing")).status_code)
+
+ # Test we cannot POST to /
+ self.assertEqual(405, requests.post(_url("")).status_code)
+
def test_api_password(self):
""" Test if we get access denied if we omit or provide
a wrong api password. """
@@ -127,8 +147,8 @@ class TestHTTP(unittest.TestCase):
hass.states.set("test.test", "not_to_be_set")
requests.post(_url(remote.URL_API_STATES_ENTITY.format("test.test")),
- data=json.dumps({"state": "debug_state_change2",
- "api_password": API_PASSWORD}))
+ data=json.dumps({"state": "debug_state_change2"}),
+ headers=HA_HEADERS)
self.assertEqual("debug_state_change2",
hass.states.get("test.test").state)
@@ -143,8 +163,8 @@ class TestHTTP(unittest.TestCase):
req = requests.post(
_url(remote.URL_API_STATES_ENTITY.format(
"test_entity.that_does_not_exist")),
- data=json.dumps({"state": new_state,
- "api_password": API_PASSWORD}))
+ data=json.dumps({'state': new_state}),
+ headers=HA_HEADERS)
cur_state = (hass.states.
get("test_entity.that_does_not_exist").state)
@@ -152,6 +172,18 @@ class TestHTTP(unittest.TestCase):
self.assertEqual(201, req.status_code)
self.assertEqual(cur_state, new_state)
+ # pylint: disable=invalid-name
+ def test_api_state_change_with_bad_data(self):
+ """ Test if API sends appropriate error if we omit state. """
+
+ req = requests.post(
+ _url(remote.URL_API_STATES_ENTITY.format(
+ "test_entity.that_does_not_exist")),
+ data=json.dumps({}),
+ headers=HA_HEADERS)
+
+ self.assertEqual(400, req.status_code)
+
# pylint: disable=invalid-name
def test_api_fire_event_with_no_data(self):
""" Test if the API allows us to fire an event. """
@@ -161,13 +193,13 @@ class TestHTTP(unittest.TestCase):
""" Helper method that will verify our event got called. """
test_value.append(1)
- hass.listen_once_event("test.event_no_data", listener)
+ hass.bus.listen_once("test.event_no_data", listener)
requests.post(
_url(remote.URL_API_EVENTS_EVENT.format("test.event_no_data")),
headers=HA_HEADERS)
- hass._pool.block_till_done()
+ hass.pool.block_till_done()
self.assertEqual(1, len(test_value))
@@ -182,14 +214,14 @@ class TestHTTP(unittest.TestCase):
if "test" in event.data:
test_value.append(1)
- hass.listen_once_event("test_event_with_data", listener)
+ hass.bus.listen_once("test_event_with_data", listener)
requests.post(
_url(remote.URL_API_EVENTS_EVENT.format("test_event_with_data")),
data=json.dumps({"test": 1}),
headers=HA_HEADERS)
- hass._pool.block_till_done()
+ hass.pool.block_till_done()
self.assertEqual(1, len(test_value))
@@ -202,14 +234,25 @@ class TestHTTP(unittest.TestCase):
""" Helper method that will verify our event got called. """
test_value.append(1)
- hass.listen_once_event("test_event_bad_data", listener)
+ hass.bus.listen_once("test_event_bad_data", listener)
req = requests.post(
_url(remote.URL_API_EVENTS_EVENT.format("test_event_bad_data")),
data=json.dumps('not an object'),
headers=HA_HEADERS)
- hass._pool.block_till_done()
+ hass.pool.block_till_done()
+
+ self.assertEqual(422, req.status_code)
+ self.assertEqual(0, len(test_value))
+
+ # Try now with valid but unusable JSON
+ req = requests.post(
+ _url(remote.URL_API_EVENTS_EVENT.format("test_event_bad_data")),
+ data=json.dumps([1, 2, 3]),
+ headers=HA_HEADERS)
+
+ hass.pool.block_till_done()
self.assertEqual(422, req.status_code)
self.assertEqual(0, len(test_value))
@@ -254,7 +297,7 @@ class TestHTTP(unittest.TestCase):
"test_domain", "test_service")),
headers=HA_HEADERS)
- hass._pool.block_till_done()
+ hass.pool.block_till_done()
self.assertEqual(1, len(test_value))
@@ -276,6 +319,82 @@ class TestHTTP(unittest.TestCase):
data=json.dumps({"test": 1}),
headers=HA_HEADERS)
- hass._pool.block_till_done()
+ hass.pool.block_till_done()
self.assertEqual(1, len(test_value))
+
+ def test_api_event_forward(self):
+ """ Test setting up event forwarding. """
+
+ req = requests.post(
+ _url(remote.URL_API_EVENT_FORWARD),
+ headers=HA_HEADERS)
+ self.assertEqual(400, req.status_code)
+
+ req = requests.post(
+ _url(remote.URL_API_EVENT_FORWARD),
+ data=json.dumps({'host': '127.0.0.1'}),
+ headers=HA_HEADERS)
+ self.assertEqual(400, req.status_code)
+
+ req = requests.post(
+ _url(remote.URL_API_EVENT_FORWARD),
+ data=json.dumps({'api_password': 'bla-di-bla'}),
+ headers=HA_HEADERS)
+ self.assertEqual(400, req.status_code)
+
+ req = requests.post(
+ _url(remote.URL_API_EVENT_FORWARD),
+ data=json.dumps({
+ 'api_password': 'bla-di-bla',
+ 'host': '127.0.0.1',
+ 'port': 'abcd'
+ }),
+ headers=HA_HEADERS)
+ self.assertEqual(422, req.status_code)
+
+ req = requests.post(
+ _url(remote.URL_API_EVENT_FORWARD),
+ data=json.dumps({
+ 'api_password': 'bla-di-bla',
+ 'host': '127.0.0.1',
+ 'port': '8125'
+ }),
+ headers=HA_HEADERS)
+ self.assertEqual(422, req.status_code)
+
+ # Setup a real one
+ req = requests.post(
+ _url(remote.URL_API_EVENT_FORWARD),
+ data=json.dumps({
+ 'api_password': API_PASSWORD,
+ 'host': '127.0.0.1',
+ 'port': SERVER_PORT
+ }),
+ headers=HA_HEADERS)
+ self.assertEqual(200, req.status_code)
+
+ # Delete it again..
+ req = requests.delete(
+ _url(remote.URL_API_EVENT_FORWARD),
+ data=json.dumps({}),
+ headers=HA_HEADERS)
+ self.assertEqual(400, req.status_code)
+
+ req = requests.delete(
+ _url(remote.URL_API_EVENT_FORWARD),
+ data=json.dumps({
+ 'host': '127.0.0.1',
+ 'port': 'abcd'
+ }),
+ headers=HA_HEADERS)
+ self.assertEqual(422, req.status_code)
+
+ req = requests.delete(
+ _url(remote.URL_API_EVENT_FORWARD),
+ data=json.dumps({
+ 'host': '127.0.0.1',
+ 'port': SERVER_PORT
+ }),
+ headers=HA_HEADERS)
+ self.assertEqual(200, req.status_code)
diff --git a/test/test_component_light.py b/ha_test/test_component_light.py
similarity index 73%
rename from test/test_component_light.py
rename to ha_test/test_component_light.py
index 04db6d9ec13..e9cb219d07b 100644
--- a/test/test_component_light.py
+++ b/ha_test/test_component_light.py
@@ -1,6 +1,6 @@
"""
-test.test_component_switch
-~~~~~~~~~~~~~~~~~~~~~~~~~~
+ha_test.test_component_switch
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests switch component.
"""
@@ -11,12 +11,12 @@ import os
import homeassistant as ha
import homeassistant.loader as loader
import homeassistant.util as util
-import homeassistant.components as components
+from homeassistant.const import (
+ ATTR_ENTITY_ID, STATE_ON, STATE_OFF, CONF_TYPE,
+ SERVICE_TURN_ON, SERVICE_TURN_OFF)
import homeassistant.components.light as light
-import mock_toggledevice_platform
-
-from helper import mock_service, get_test_home_assistant
+from helpers import mock_service, get_test_home_assistant
class TestLight(unittest.TestCase):
@@ -25,11 +25,10 @@ class TestLight(unittest.TestCase):
def setUp(self): # pylint: disable=invalid-name
self.hass = get_test_home_assistant()
loader.prepare(self.hass)
- loader.set_component('light.test', mock_toggledevice_platform)
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
- self.hass._pool.stop()
+ self.hass.stop()
user_light_file = self.hass.get_config_path(light.LIGHT_PROFILES_FILE)
@@ -39,21 +38,21 @@ class TestLight(unittest.TestCase):
def test_methods(self):
""" Test if methods call the services as expected. """
# Test is_on
- self.hass.states.set('light.test', components.STATE_ON)
+ self.hass.states.set('light.test', STATE_ON)
self.assertTrue(light.is_on(self.hass, 'light.test'))
- self.hass.states.set('light.test', components.STATE_OFF)
+ self.hass.states.set('light.test', STATE_OFF)
self.assertFalse(light.is_on(self.hass, 'light.test'))
- self.hass.states.set(light.ENTITY_ID_ALL_LIGHTS, components.STATE_ON)
+ self.hass.states.set(light.ENTITY_ID_ALL_LIGHTS, STATE_ON)
self.assertTrue(light.is_on(self.hass))
- self.hass.states.set(light.ENTITY_ID_ALL_LIGHTS, components.STATE_OFF)
+ self.hass.states.set(light.ENTITY_ID_ALL_LIGHTS, STATE_OFF)
self.assertFalse(light.is_on(self.hass))
# Test turn_on
turn_on_calls = mock_service(
- self.hass, light.DOMAIN, components.SERVICE_TURN_ON)
+ self.hass, light.DOMAIN, SERVICE_TURN_ON)
light.turn_on(
self.hass,
@@ -64,44 +63,48 @@ class TestLight(unittest.TestCase):
xy_color='xy_color_val',
profile='profile_val')
- self.hass._pool.block_till_done()
+ self.hass.pool.block_till_done()
self.assertEqual(1, len(turn_on_calls))
call = turn_on_calls[-1]
self.assertEqual(light.DOMAIN, call.domain)
- self.assertEqual(components.SERVICE_TURN_ON, call.service)
- self.assertEqual('entity_id_val', call.data[components.ATTR_ENTITY_ID])
- self.assertEqual('transition_val', call.data[light.ATTR_TRANSITION])
- self.assertEqual('brightness_val', call.data[light.ATTR_BRIGHTNESS])
- self.assertEqual('rgb_color_val', call.data[light.ATTR_RGB_COLOR])
- self.assertEqual('xy_color_val', call.data[light.ATTR_XY_COLOR])
- self.assertEqual('profile_val', call.data[light.ATTR_PROFILE])
+ self.assertEqual(SERVICE_TURN_ON, call.service)
+ self.assertEqual('entity_id_val', call.data.get(ATTR_ENTITY_ID))
+ self.assertEqual(
+ 'transition_val', call.data.get(light.ATTR_TRANSITION))
+ self.assertEqual(
+ 'brightness_val', call.data.get(light.ATTR_BRIGHTNESS))
+ self.assertEqual('rgb_color_val', call.data.get(light.ATTR_RGB_COLOR))
+ self.assertEqual('xy_color_val', call.data.get(light.ATTR_XY_COLOR))
+ self.assertEqual('profile_val', call.data.get(light.ATTR_PROFILE))
# Test turn_off
turn_off_calls = mock_service(
- self.hass, light.DOMAIN, components.SERVICE_TURN_OFF)
+ self.hass, light.DOMAIN, SERVICE_TURN_OFF)
light.turn_off(
self.hass, entity_id='entity_id_val', transition='transition_val')
- self.hass._pool.block_till_done()
+ self.hass.pool.block_till_done()
self.assertEqual(1, len(turn_off_calls))
call = turn_off_calls[-1]
self.assertEqual(light.DOMAIN, call.domain)
- self.assertEqual(components.SERVICE_TURN_OFF, call.service)
- self.assertEqual('entity_id_val', call.data[components.ATTR_ENTITY_ID])
+ self.assertEqual(SERVICE_TURN_OFF, call.service)
+ self.assertEqual('entity_id_val', call.data[ATTR_ENTITY_ID])
self.assertEqual('transition_val', call.data[light.ATTR_TRANSITION])
def test_services(self):
""" Test the provided services. """
- mock_toggledevice_platform.init()
- self.assertTrue(
- light.setup(self.hass, {light.DOMAIN: {ha.CONF_TYPE: 'test'}}))
+ platform = loader.get_component('light.test')
- dev1, dev2, dev3 = mock_toggledevice_platform.get_lights(None, None)
+ platform.init()
+ self.assertTrue(
+ light.setup(self.hass, {light.DOMAIN: {CONF_TYPE: 'test'}}))
+
+ dev1, dev2, dev3 = platform.get_lights(None, None)
# Test init
self.assertTrue(light.is_on(self.hass, dev1.entity_id))
@@ -112,7 +115,7 @@ class TestLight(unittest.TestCase):
light.turn_off(self.hass, entity_id=dev1.entity_id)
light.turn_on(self.hass, entity_id=dev2.entity_id)
- self.hass._pool.block_till_done()
+ self.hass.pool.block_till_done()
self.assertFalse(light.is_on(self.hass, dev1.entity_id))
self.assertTrue(light.is_on(self.hass, dev2.entity_id))
@@ -120,7 +123,7 @@ class TestLight(unittest.TestCase):
# turn on all lights
light.turn_on(self.hass)
- self.hass._pool.block_till_done()
+ self.hass.pool.block_till_done()
self.assertTrue(light.is_on(self.hass, dev1.entity_id))
self.assertTrue(light.is_on(self.hass, dev2.entity_id))
@@ -129,7 +132,7 @@ class TestLight(unittest.TestCase):
# turn off all lights
light.turn_off(self.hass)
- self.hass._pool.block_till_done()
+ self.hass.pool.block_till_done()
self.assertFalse(light.is_on(self.hass, dev1.entity_id))
self.assertFalse(light.is_on(self.hass, dev2.entity_id))
@@ -142,7 +145,7 @@ class TestLight(unittest.TestCase):
self.hass, dev2.entity_id, rgb_color=[255, 255, 255])
light.turn_on(self.hass, dev3.entity_id, xy_color=[.4, .6])
- self.hass._pool.block_till_done()
+ self.hass.pool.block_till_done()
method, data = dev1.last_call('turn_on')
self.assertEqual(
@@ -168,7 +171,7 @@ class TestLight(unittest.TestCase):
self.hass, dev2.entity_id,
profile=prof_name, brightness=100, xy_color=[.4, .6])
- self.hass._pool.block_till_done()
+ self.hass.pool.block_till_done()
method, data = dev1.last_call('turn_on')
self.assertEqual(
@@ -187,7 +190,7 @@ class TestLight(unittest.TestCase):
light.turn_on(self.hass, dev2.entity_id, xy_color=["bla-di-bla", 5])
light.turn_on(self.hass, dev3.entity_id, rgb_color=[255, None, 2])
- self.hass._pool.block_till_done()
+ self.hass.pool.block_till_done()
method, data = dev1.last_call('turn_on')
self.assertEqual({}, data)
@@ -203,7 +206,7 @@ class TestLight(unittest.TestCase):
self.hass, dev1.entity_id,
profile=prof_name, brightness='bright', rgb_color='yellowish')
- self.hass._pool.block_till_done()
+ self.hass.pool.block_till_done()
method, data = dev1.last_call('turn_on')
self.assertEqual(
@@ -220,22 +223,23 @@ class TestLight(unittest.TestCase):
# Test with non-existing component
self.assertFalse(light.setup(
- self.hass, {light.DOMAIN: {ha.CONF_TYPE: 'nonexisting'}}
+ self.hass, {light.DOMAIN: {CONF_TYPE: 'nonexisting'}}
))
# Test if light component returns 0 lightes
- mock_toggledevice_platform.init(True)
+ platform = loader.get_component('light.test')
+ platform.init(True)
- self.assertEqual(
- [], mock_toggledevice_platform.get_lights(None, None))
+ self.assertEqual([], platform.get_lights(None, None))
self.assertFalse(light.setup(
- self.hass, {light.DOMAIN: {ha.CONF_TYPE: 'test'}}
+ self.hass, {light.DOMAIN: {CONF_TYPE: 'test'}}
))
def test_light_profiles(self):
""" Test light profiles. """
- mock_toggledevice_platform.init()
+ platform = loader.get_component('light.test')
+ platform.init()
user_light_file = self.hass.get_config_path(light.LIGHT_PROFILES_FILE)
@@ -245,7 +249,7 @@ class TestLight(unittest.TestCase):
user_file.write('I,WILL,NOT,WORK\n')
self.assertFalse(light.setup(
- self.hass, {light.DOMAIN: {ha.CONF_TYPE: 'test'}}
+ self.hass, {light.DOMAIN: {CONF_TYPE: 'test'}}
))
# Clean up broken file
@@ -256,14 +260,14 @@ class TestLight(unittest.TestCase):
user_file.write('test,.4,.6,100\n')
self.assertTrue(light.setup(
- self.hass, {light.DOMAIN: {ha.CONF_TYPE: 'test'}}
+ self.hass, {light.DOMAIN: {CONF_TYPE: 'test'}}
))
- dev1, dev2, dev3 = mock_toggledevice_platform.get_lights(None, None)
+ dev1, dev2, dev3 = platform.get_lights(None, None)
light.turn_on(self.hass, dev1.entity_id, profile='test')
- self.hass._pool.block_till_done()
+ self.hass.pool.block_till_done()
method, data = dev1.last_call('turn_on')
diff --git a/test/test_component_sun.py b/ha_test/test_component_sun.py
similarity index 81%
rename from test/test_component_sun.py
rename to ha_test/test_component_sun.py
index f7cf7c90dab..a587f60bff5 100644
--- a/test/test_component_sun.py
+++ b/ha_test/test_component_sun.py
@@ -1,6 +1,6 @@
"""
-test.test_component_sun
-~~~~~~~~~~~~~~~~~~~~~~~
+ha_test.test_component_sun
+~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests Sun component.
"""
@@ -11,6 +11,7 @@ import datetime as dt
import ephem
import homeassistant as ha
+from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
import homeassistant.components.sun as sun
@@ -22,7 +23,7 @@ class TestSun(unittest.TestCase):
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
- self.hass._pool.stop()
+ self.hass.stop()
def test_is_on(self):
""" Test is_on method. """
@@ -37,8 +38,8 @@ class TestSun(unittest.TestCase):
self.assertTrue(sun.setup(
self.hass,
{ha.DOMAIN: {
- ha.CONF_LATITUDE: '32.87336',
- ha.CONF_LONGITUDE: '117.22743'
+ CONF_LATITUDE: '32.87336',
+ CONF_LONGITUDE: '117.22743'
}}))
observer = ephem.Observer()
@@ -76,8 +77,8 @@ class TestSun(unittest.TestCase):
self.assertTrue(sun.setup(
self.hass,
{ha.DOMAIN: {
- ha.CONF_LATITUDE: '32.87336',
- ha.CONF_LONGITUDE: '117.22743'
+ CONF_LATITUDE: '32.87336',
+ CONF_LONGITUDE: '117.22743'
}}))
if sun.is_on(self.hass):
@@ -92,7 +93,7 @@ class TestSun(unittest.TestCase):
self.hass.bus.fire(ha.EVENT_TIME_CHANGED,
{ha.ATTR_NOW: test_time + dt.timedelta(seconds=5)})
- self.hass._pool.block_till_done()
+ self.hass.pool.block_till_done()
self.assertEqual(test_state, self.hass.states.get(sun.ENTITY_ID).state)
@@ -101,24 +102,24 @@ class TestSun(unittest.TestCase):
self.assertFalse(sun.setup(self.hass, {}))
self.assertFalse(sun.setup(self.hass, {sun.DOMAIN: {}}))
self.assertFalse(sun.setup(
- self.hass, {ha.DOMAIN: {ha.CONF_LATITUDE: '32.87336'}}))
+ self.hass, {ha.DOMAIN: {CONF_LATITUDE: '32.87336'}}))
self.assertFalse(sun.setup(
- self.hass, {ha.DOMAIN: {ha.CONF_LONGITUDE: '117.22743'}}))
+ self.hass, {ha.DOMAIN: {CONF_LONGITUDE: '117.22743'}}))
self.assertFalse(sun.setup(
- self.hass, {ha.DOMAIN: {ha.CONF_LATITUDE: 'hello'}}))
+ self.hass, {ha.DOMAIN: {CONF_LATITUDE: 'hello'}}))
self.assertFalse(sun.setup(
- self.hass, {ha.DOMAIN: {ha.CONF_LONGITUDE: 'how are you'}}))
+ self.hass, {ha.DOMAIN: {CONF_LONGITUDE: 'how are you'}}))
self.assertFalse(sun.setup(
self.hass, {ha.DOMAIN: {
- ha.CONF_LATITUDE: 'wrong', ha.CONF_LONGITUDE: '117.22743'
+ CONF_LATITUDE: 'wrong', CONF_LONGITUDE: '117.22743'
}}))
self.assertFalse(sun.setup(
self.hass, {ha.DOMAIN: {
- ha.CONF_LATITUDE: '32.87336', ha.CONF_LONGITUDE: 'wrong'
+ CONF_LATITUDE: '32.87336', CONF_LONGITUDE: 'wrong'
}}))
# Test with correct config
self.assertTrue(sun.setup(
self.hass, {ha.DOMAIN: {
- ha.CONF_LATITUDE: '32.87336', ha.CONF_LONGITUDE: '117.22743'
+ CONF_LATITUDE: '32.87336', CONF_LONGITUDE: '117.22743'
}}))
diff --git a/test/test_component_switch.py b/ha_test/test_component_switch.py
similarity index 67%
rename from test/test_component_switch.py
rename to ha_test/test_component_switch.py
index ca207972720..687df62ed5f 100644
--- a/test/test_component_switch.py
+++ b/ha_test/test_component_switch.py
@@ -1,6 +1,6 @@
"""
-test.test_component_switch
-~~~~~~~~~~~~~~~~~~~~~~~~~~
+ha_test.test_component_switch
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests switch component.
"""
@@ -9,38 +9,39 @@ import unittest
import homeassistant as ha
import homeassistant.loader as loader
-import homeassistant.components as components
+from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM
import homeassistant.components.switch as switch
-import mock_toggledevice_platform
+from helpers import get_test_home_assistant
class TestSwitch(unittest.TestCase):
""" Test the switch module. """
def setUp(self): # pylint: disable=invalid-name
- self.hass = ha.HomeAssistant()
+ self.hass = get_test_home_assistant()
loader.prepare(self.hass)
- loader.set_component('switch.test', mock_toggledevice_platform)
- mock_toggledevice_platform.init()
+ platform = loader.get_component('switch.test')
+
+ platform.init()
self.assertTrue(switch.setup(
- self.hass, {switch.DOMAIN: {ha.CONF_TYPE: 'test'}}
+ self.hass, {switch.DOMAIN: {CONF_PLATFORM: 'test'}}
))
# Switch 1 is ON, switch 2 is OFF
self.switch_1, self.switch_2, self.switch_3 = \
- mock_toggledevice_platform.get_switches(None, None)
+ platform.get_switches(None, None)
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
- self.hass._pool.stop()
+ self.hass.stop()
def test_methods(self):
""" Test is_on, turn_on, turn_off methods. """
self.assertTrue(switch.is_on(self.hass))
self.assertEqual(
- components.STATE_ON,
+ STATE_ON,
self.hass.states.get(switch.ENTITY_ID_ALL_SWITCHES).state)
self.assertTrue(switch.is_on(self.hass, self.switch_1.entity_id))
self.assertFalse(switch.is_on(self.hass, self.switch_2.entity_id))
@@ -49,7 +50,7 @@ class TestSwitch(unittest.TestCase):
switch.turn_off(self.hass, self.switch_1.entity_id)
switch.turn_on(self.hass, self.switch_2.entity_id)
- self.hass._pool.block_till_done()
+ self.hass.pool.block_till_done()
self.assertTrue(switch.is_on(self.hass))
self.assertFalse(switch.is_on(self.hass, self.switch_1.entity_id))
@@ -58,11 +59,11 @@ class TestSwitch(unittest.TestCase):
# Turn all off
switch.turn_off(self.hass)
- self.hass._pool.block_till_done()
+ self.hass.pool.block_till_done()
self.assertFalse(switch.is_on(self.hass))
self.assertEqual(
- components.STATE_OFF,
+ STATE_OFF,
self.hass.states.get(switch.ENTITY_ID_ALL_SWITCHES).state)
self.assertFalse(switch.is_on(self.hass, self.switch_1.entity_id))
self.assertFalse(switch.is_on(self.hass, self.switch_2.entity_id))
@@ -71,11 +72,11 @@ class TestSwitch(unittest.TestCase):
# Turn all on
switch.turn_on(self.hass)
- self.hass._pool.block_till_done()
+ self.hass.pool.block_till_done()
self.assertTrue(switch.is_on(self.hass))
self.assertEqual(
- components.STATE_ON,
+ STATE_ON,
self.hass.states.get(switch.ENTITY_ID_ALL_SWITCHES).state)
self.assertTrue(switch.is_on(self.hass, self.switch_1.entity_id))
self.assertTrue(switch.is_on(self.hass, self.switch_2.entity_id))
@@ -89,15 +90,27 @@ class TestSwitch(unittest.TestCase):
# Test with non-existing component
self.assertFalse(switch.setup(
- self.hass, {switch.DOMAIN: {ha.CONF_TYPE: 'nonexisting'}}
+ self.hass, {switch.DOMAIN: {CONF_PLATFORM: 'nonexisting'}}
))
# Test if switch component returns 0 switches
- mock_toggledevice_platform.init(True)
+ test_platform = loader.get_component('switch.test')
+ test_platform.init(True)
self.assertEqual(
- [], mock_toggledevice_platform.get_switches(None, None))
+ [], test_platform.get_switches(None, None))
self.assertFalse(switch.setup(
- self.hass, {switch.DOMAIN: {ha.CONF_TYPE: 'test'}}
+ self.hass, {switch.DOMAIN: {CONF_PLATFORM: 'test'}}
+ ))
+
+ # Test if we can load 2 platforms
+ loader.set_component('switch.test2', test_platform)
+ test_platform.init(False)
+
+ self.assertTrue(switch.setup(
+ self.hass, {
+ switch.DOMAIN: {CONF_PLATFORM: 'test'},
+ '{} 2'.format(switch.DOMAIN): {CONF_PLATFORM: 'test2'},
+ }
))
diff --git a/test/test_core.py b/ha_test/test_core.py
similarity index 83%
rename from test/test_core.py
rename to ha_test/test_core.py
index 91a56df14fa..c7a35d83842 100644
--- a/test/test_core.py
+++ b/ha_test/test_core.py
@@ -1,6 +1,6 @@
"""
-test.test_core
-~~~~~~~~~~~~~~
+ha_test.test_core
+~~~~~~~~~~~~~~~~~
Provides tests to verify that Home Assistant core works.
"""
@@ -30,7 +30,7 @@ class TestHomeAssistant(unittest.TestCase):
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
- self.hass._pool.stop()
+ self.hass.stop()
def test_get_config_path(self):
""" Test get_config_path method. """
@@ -52,81 +52,18 @@ class TestHomeAssistant(unittest.TestCase):
self.assertTrue(blocking_thread.is_alive())
- self.hass.call_service(ha.DOMAIN, ha.SERVICE_HOMEASSISTANT_STOP)
- self.hass._pool.block_till_done()
+ self.hass.services.call(ha.DOMAIN, ha.SERVICE_HOMEASSISTANT_STOP)
+ self.hass.pool.block_till_done()
# hass.block_till_stopped checks every second if it should quit
# we have to wait worst case 1 second
wait_loops = 0
- while blocking_thread.is_alive() and wait_loops < 10:
+ while blocking_thread.is_alive() and wait_loops < 50:
wait_loops += 1
time.sleep(0.1)
self.assertFalse(blocking_thread.is_alive())
- def test_get_entity_ids(self):
- """ Test get_entity_ids method. """
- ent_ids = self.hass.get_entity_ids()
- self.assertEqual(2, len(ent_ids))
- self.assertTrue('light.Bowl' in ent_ids)
- self.assertTrue('switch.AC' in ent_ids)
-
- ent_ids = self.hass.get_entity_ids('light')
- self.assertEqual(1, len(ent_ids))
- self.assertTrue('light.Bowl' in ent_ids)
-
- def test_track_state_change(self):
- """ Test track_state_change. """
- # 2 lists to track how often our callbacks got called
- specific_runs = []
- wildcard_runs = []
-
- self.hass.track_state_change(
- 'light.Bowl', lambda a, b, c: specific_runs.append(1), 'on', 'off')
-
- self.hass.track_state_change(
- 'light.Bowl', lambda a, b, c: wildcard_runs.append(1),
- ha.MATCH_ALL, ha.MATCH_ALL)
-
- # Set same state should not trigger a state change/listener
- self.hass.states.set('light.Bowl', 'on')
- self.hass._pool.block_till_done()
- self.assertEqual(0, len(specific_runs))
- self.assertEqual(0, len(wildcard_runs))
-
- # State change off -> on
- self.hass.states.set('light.Bowl', 'off')
- self.hass._pool.block_till_done()
- self.assertEqual(1, len(specific_runs))
- self.assertEqual(1, len(wildcard_runs))
-
- # State change off -> off
- self.hass.states.set('light.Bowl', 'off', {"some_attr": 1})
- self.hass._pool.block_till_done()
- self.assertEqual(1, len(specific_runs))
- self.assertEqual(2, len(wildcard_runs))
-
- # State change off -> on
- self.hass.states.set('light.Bowl', 'on')
- self.hass._pool.block_till_done()
- self.assertEqual(1, len(specific_runs))
- self.assertEqual(3, len(wildcard_runs))
-
- def test_listen_once_event(self):
- """ Test listen_once_event method. """
- runs = []
-
- self.hass.listen_once_event('test_event', lambda x: runs.append(1))
-
- self.hass.bus.fire('test_event')
- self.hass._pool.block_till_done()
- self.assertEqual(1, len(runs))
-
- # Second time it should not increase runs
- self.hass.bus.fire('test_event')
- self.hass._pool.block_till_done()
- self.assertEqual(1, len(runs))
-
def test_track_point_in_time(self):
""" Test track point in time. """
before_birthday = datetime(1985, 7, 9, 12, 0, 0)
@@ -139,23 +76,23 @@ class TestHomeAssistant(unittest.TestCase):
lambda x: runs.append(1), birthday_paulus)
self._send_time_changed(before_birthday)
- self.hass._pool.block_till_done()
+ self.hass.pool.block_till_done()
self.assertEqual(0, len(runs))
self._send_time_changed(birthday_paulus)
- self.hass._pool.block_till_done()
+ self.hass.pool.block_till_done()
self.assertEqual(1, len(runs))
# A point in time tracker will only fire once, this should do nothing
self._send_time_changed(birthday_paulus)
- self.hass._pool.block_till_done()
+ self.hass.pool.block_till_done()
self.assertEqual(1, len(runs))
self.hass.track_point_in_time(
lambda x: runs.append(1), birthday_paulus)
self._send_time_changed(after_birthday)
- self.hass._pool.block_till_done()
+ self.hass.pool.block_till_done()
self.assertEqual(2, len(runs))
def test_track_time_change(self):
@@ -168,17 +105,17 @@ class TestHomeAssistant(unittest.TestCase):
lambda x: specific_runs.append(1), second=[0, 30])
self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0))
- self.hass._pool.block_till_done()
+ self.hass.pool.block_till_done()
self.assertEqual(1, len(specific_runs))
self.assertEqual(1, len(wildcard_runs))
self._send_time_changed(datetime(2014, 5, 24, 12, 0, 15))
- self.hass._pool.block_till_done()
+ self.hass.pool.block_till_done()
self.assertEqual(1, len(specific_runs))
self.assertEqual(2, len(wildcard_runs))
self._send_time_changed(datetime(2014, 5, 24, 12, 0, 30))
- self.hass._pool.block_till_done()
+ self.hass.pool.block_till_done()
self.assertEqual(2, len(specific_runs))
self.assertEqual(3, len(wildcard_runs))
@@ -234,6 +171,21 @@ class TestEventBus(unittest.TestCase):
# Try deleting listener while category doesn't exist either
self.bus.remove_listener('test', listener)
+ def test_listen_once_event(self):
+ """ Test listen_once_event method. """
+ runs = []
+
+ self.bus.listen_once('test_event', lambda x: runs.append(1))
+
+ self.bus.fire('test_event')
+ self.bus._pool.block_till_done()
+ self.assertEqual(1, len(runs))
+
+ # Second time it should not increase runs
+ self.bus.fire('test_event')
+ self.bus._pool.block_till_done()
+ self.assertEqual(1, len(runs))
+
class TestState(unittest.TestCase):
""" Test EventBus methods. """
@@ -276,15 +228,76 @@ class TestStateMachine(unittest.TestCase):
self.assertFalse(self.states.is_state('light.Bowl', 'off'))
self.assertFalse(self.states.is_state('light.Non_existing', 'on'))
+ def test_entity_ids(self):
+ """ Test get_entity_ids method. """
+ ent_ids = self.states.entity_ids()
+ self.assertEqual(2, len(ent_ids))
+ self.assertTrue('light.Bowl' in ent_ids)
+ self.assertTrue('switch.AC' in ent_ids)
+
+ ent_ids = self.states.entity_ids('light')
+ self.assertEqual(1, len(ent_ids))
+ self.assertTrue('light.Bowl' in ent_ids)
+
def test_remove(self):
""" Test remove method. """
- self.assertTrue('light.Bowl' in self.states.entity_ids)
+ self.assertTrue('light.Bowl' in self.states.entity_ids())
self.assertTrue(self.states.remove('light.Bowl'))
- self.assertFalse('light.Bowl' in self.states.entity_ids)
+ self.assertFalse('light.Bowl' in self.states.entity_ids())
# If it does not exist, we should get False
self.assertFalse(self.states.remove('light.Bowl'))
+ def test_track_change(self):
+ """ Test states.track_change. """
+ # 2 lists to track how often our callbacks got called
+ specific_runs = []
+ wildcard_runs = []
+
+ self.states.track_change(
+ 'light.Bowl', lambda a, b, c: specific_runs.append(1), 'on', 'off')
+
+ self.states.track_change(
+ 'light.Bowl', lambda a, b, c: wildcard_runs.append(1),
+ ha.MATCH_ALL, ha.MATCH_ALL)
+
+ # Set same state should not trigger a state change/listener
+ self.states.set('light.Bowl', 'on')
+ self.bus._pool.block_till_done()
+ self.assertEqual(0, len(specific_runs))
+ self.assertEqual(0, len(wildcard_runs))
+
+ # State change off -> on
+ self.states.set('light.Bowl', 'off')
+ self.bus._pool.block_till_done()
+ self.assertEqual(1, len(specific_runs))
+ self.assertEqual(1, len(wildcard_runs))
+
+ # State change off -> off
+ self.states.set('light.Bowl', 'off', {"some_attr": 1})
+ self.bus._pool.block_till_done()
+ self.assertEqual(1, len(specific_runs))
+ self.assertEqual(2, len(wildcard_runs))
+
+ # State change off -> on
+ self.states.set('light.Bowl', 'on')
+ self.bus._pool.block_till_done()
+ self.assertEqual(1, len(specific_runs))
+ self.assertEqual(3, len(wildcard_runs))
+
+ def test_case_insensitivty(self):
+ runs = []
+
+ self.states.track_change(
+ 'light.BoWl', lambda a, b, c: runs.append(1),
+ ha.MATCH_ALL, ha.MATCH_ALL)
+
+ self.states.set('light.BOWL', 'off')
+ self.bus._pool.block_till_done()
+
+ self.assertTrue(self.states.is_state('light.bowl', 'off'))
+ self.assertEqual(1, len(runs))
+
class TestServiceCall(unittest.TestCase):
""" Test ServiceCall class. """
diff --git a/ha_test/test_helpers.py b/ha_test/test_helpers.py
new file mode 100644
index 00000000000..f61204c837f
--- /dev/null
+++ b/ha_test/test_helpers.py
@@ -0,0 +1,49 @@
+"""
+ha_test.test_helpers
+~~~~~~~~~~~~~~~~~~~~
+
+Tests component helpers.
+"""
+# pylint: disable=protected-access,too-many-public-methods
+import unittest
+
+from helpers import get_test_home_assistant
+
+import homeassistant as ha
+import homeassistant.loader as loader
+from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ENTITY_ID
+from homeassistant.helpers import extract_entity_ids
+
+
+class TestComponentsCore(unittest.TestCase):
+ """ Tests homeassistant.components module. """
+
+ def setUp(self): # pylint: disable=invalid-name
+ """ Init needed objects. """
+ self.hass = get_test_home_assistant()
+ loader.prepare(self.hass)
+
+ self.hass.states.set('light.Bowl', STATE_ON)
+ self.hass.states.set('light.Ceiling', STATE_OFF)
+ self.hass.states.set('light.Kitchen', STATE_OFF)
+
+ loader.get_component('group').setup_group(
+ self.hass, 'test', ['light.Ceiling', 'light.Kitchen'])
+
+ def tearDown(self): # pylint: disable=invalid-name
+ """ Stop down stuff we started. """
+ self.hass.stop()
+
+ def test_extract_entity_ids(self):
+ """ Test extract_entity_ids method. """
+ call = ha.ServiceCall('light', 'turn_on',
+ {ATTR_ENTITY_ID: 'light.Bowl'})
+
+ self.assertEqual(['light.Bowl'],
+ extract_entity_ids(self.hass, call))
+
+ call = ha.ServiceCall('light', 'turn_on',
+ {ATTR_ENTITY_ID: 'group.test'})
+
+ self.assertEqual(['light.Ceiling', 'light.Kitchen'],
+ extract_entity_ids(self.hass, call))
diff --git a/ha_test/test_loader.py b/ha_test/test_loader.py
new file mode 100644
index 00000000000..b7ae75c0e2a
--- /dev/null
+++ b/ha_test/test_loader.py
@@ -0,0 +1,87 @@
+"""
+ha_ha_test.test_loader
+~~~~~~~~~~~~~~~~~~~~~~
+
+Provides tests to verify that we can load components.
+"""
+# pylint: disable=too-many-public-methods,protected-access
+import unittest
+
+import homeassistant.loader as loader
+import homeassistant.components.http as http
+
+from helpers import get_test_home_assistant, MockModule
+
+
+class TestLoader(unittest.TestCase):
+ """ Test the loader module. """
+ def setUp(self): # pylint: disable=invalid-name
+ self.hass = get_test_home_assistant()
+ loader.prepare(self.hass)
+
+ def tearDown(self): # pylint: disable=invalid-name
+ """ Stop down stuff we started. """
+ self.hass.stop()
+
+ def test_set_component(self):
+ """ Test if set_component works. """
+ loader.set_component('switch.test', http)
+
+ self.assertEqual(http, loader.get_component('switch.test'))
+
+ def test_get_component(self):
+ """ Test if get_component works. """
+ self.assertEqual(http, loader.get_component('http'))
+
+ self.assertIsNotNone(loader.get_component('switch.test'))
+
+ def test_load_order_component(self):
+ """ Test if we can get the proper load order of components. """
+ loader.set_component('mod1', MockModule('mod1'))
+ loader.set_component('mod2', MockModule('mod2', ['mod1']))
+ loader.set_component('mod3', MockModule('mod3', ['mod2']))
+
+ self.assertEqual(
+ ['mod1', 'mod2', 'mod3'], loader.load_order_component('mod3'))
+
+ # Create circular dependency
+ loader.set_component('mod1', MockModule('mod1', ['mod3']))
+
+ self.assertEqual([], loader.load_order_component('mod3'))
+
+ # Depend on non-existing component
+ loader.set_component('mod1', MockModule('mod1', ['nonexisting']))
+
+ self.assertEqual([], loader.load_order_component('mod1'))
+
+ # Try to get load order for non-existing component
+ self.assertEqual([], loader.load_order_component('mod1'))
+
+ def test_load_order_components(self):
+ loader.set_component('mod1', MockModule('mod1', ['group']))
+ loader.set_component('mod2', MockModule('mod2', ['mod1', 'sun']))
+ loader.set_component('mod3', MockModule('mod3', ['mod2']))
+ loader.set_component('mod4', MockModule('mod4', ['group']))
+
+ self.assertEqual(
+ ['group', 'mod4', 'mod1', 'sun', 'mod2', 'mod3'],
+ loader.load_order_components(['mod4', 'mod3', 'mod2']))
+
+ loader.set_component('mod1', MockModule('mod1'))
+ loader.set_component('mod2', MockModule('mod2', ['group']))
+
+ self.assertEqual(
+ ['mod1', 'group', 'mod2'],
+ loader.load_order_components(['mod2', 'mod1']))
+
+ # Add a non existing one
+ self.assertEqual(
+ ['mod1', 'group', 'mod2'],
+ loader.load_order_components(['mod2', 'nonexisting', 'mod1']))
+
+ # Depend on a non existing one
+ loader.set_component('mod1', MockModule('mod1', ['nonexisting']))
+
+ self.assertEqual(
+ ['group', 'mod2'],
+ loader.load_order_components(['mod2', 'mod1']))
diff --git a/test/test_remote.py b/ha_test/test_remote.py
similarity index 69%
rename from test/test_remote.py
rename to ha_test/test_remote.py
index 0d0f92c6a33..f6de538e54a 100644
--- a/test/test_remote.py
+++ b/ha_test/test_remote.py
@@ -1,8 +1,10 @@
"""
-test.remote
-~~~~~~~~~~~
+ha_test.remote
+~~~~~~~~~~~~~~
Tests Home Assistant remote methods and classes.
+Uses port 8122 for master, 8123 for slave
+Uses port 8125 as a port that nothing runs on
"""
# pylint: disable=protected-access,too-many-public-methods
import unittest
@@ -13,11 +15,11 @@ import homeassistant.components.http as http
API_PASSWORD = "test1234"
-HTTP_BASE_URL = "http://127.0.0.1:{}".format(remote.SERVER_PORT)
+HTTP_BASE_URL = "http://127.0.0.1:8122"
HA_HEADERS = {remote.AUTH_HEADER: API_PASSWORD}
-hass, slave, master_api = None, None, None
+hass, slave, master_api, broken_api = None, None, None, None
def _url(path=""):
@@ -27,7 +29,7 @@ def _url(path=""):
def setUpModule(): # pylint: disable=invalid-name
""" Initalizes a Home Assistant server and Slave instance. """
- global hass, slave, master_api
+ global hass, slave, master_api, broken_api
hass = ha.HomeAssistant()
@@ -35,29 +37,28 @@ def setUpModule(): # pylint: disable=invalid-name
hass.states.set('test.test', 'a_state')
http.setup(hass,
- {http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD}})
+ {http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD,
+ http.CONF_SERVER_PORT: 8122}})
hass.start()
- master_api = remote.API("127.0.0.1", API_PASSWORD)
+ master_api = remote.API("127.0.0.1", API_PASSWORD, 8122)
# Start slave
- local_api = remote.API("127.0.0.1", API_PASSWORD, 8124)
- slave = remote.HomeAssistant(master_api, local_api)
-
- http.setup(slave,
- {http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD,
- http.CONF_SERVER_PORT: 8124}})
+ slave = remote.HomeAssistant(master_api)
slave.start()
+ # Setup API pointing at nothing
+ broken_api = remote.API("127.0.0.1", "", 8125)
+
def tearDownModule(): # pylint: disable=invalid-name
""" Stops the Home Assistant server and slave. """
global hass, slave
- hass.stop()
slave.stop()
+ hass.stop()
class TestRemoteMethods(unittest.TestCase):
@@ -71,6 +72,9 @@ class TestRemoteMethods(unittest.TestCase):
remote.validate_api(
remote.API("127.0.0.1", API_PASSWORD + "A")))
+ self.assertEqual(remote.APIStatus.CANNOT_CONNECT,
+ remote.validate_api(broken_api))
+
def test_get_event_listeners(self):
""" Test Python API get_event_listeners. """
local_data = hass.bus.listeners
@@ -82,6 +86,8 @@ class TestRemoteMethods(unittest.TestCase):
self.assertEqual(len(local_data), 0)
+ self.assertEqual({}, remote.get_event_listeners(broken_api))
+
def test_fire_event(self):
""" Test Python API fire_event. """
test_value = []
@@ -90,14 +96,17 @@ class TestRemoteMethods(unittest.TestCase):
""" Helper method that will verify our event got called. """
test_value.append(1)
- hass.listen_once_event("test.event_no_data", listener)
+ hass.bus.listen_once("test.event_no_data", listener)
remote.fire_event(master_api, "test.event_no_data")
- hass._pool.block_till_done()
+ hass.pool.block_till_done()
self.assertEqual(1, len(test_value))
+ # Should not trigger any exception
+ remote.fire_event(broken_api, "test.event_no_data")
+
def test_get_state(self):
""" Test Python API get_state. """
@@ -105,11 +114,13 @@ class TestRemoteMethods(unittest.TestCase):
hass.states.get('test.test'),
remote.get_state(master_api, 'test.test'))
+ self.assertEqual(None, remote.get_state(broken_api, 'test.test'))
+
def test_get_states(self):
""" Test Python API get_state_entity_ids. """
- self.assertEqual(
- remote.get_states(master_api), hass.states.all())
+ self.assertEqual(hass.states.all(), remote.get_states(master_api))
+ self.assertEqual([], remote.get_states(broken_api))
def test_set_state(self):
""" Test Python API set_state. """
@@ -117,6 +128,8 @@ class TestRemoteMethods(unittest.TestCase):
self.assertEqual('set_test', hass.states.get('test.test').state)
+ self.assertFalse(remote.set_state(broken_api, 'test.test', 'set_test'))
+
def test_is_state(self):
""" Test Python API is_state. """
@@ -124,6 +137,10 @@ class TestRemoteMethods(unittest.TestCase):
remote.is_state(master_api, 'test.test',
hass.states.get('test.test').state))
+ self.assertFalse(
+ remote.is_state(broken_api, 'test.test',
+ hass.states.get('test.test').state))
+
def test_get_services(self):
""" Test Python API get_services. """
@@ -134,8 +151,10 @@ class TestRemoteMethods(unittest.TestCase):
self.assertEqual(local, serv_domain["services"])
+ self.assertEqual({}, remote.get_services(broken_api))
+
def test_call_service(self):
- """ Test Python API call_service. """
+ """ Test Python API services.call. """
test_value = []
def listener(service_call): # pylint: disable=unused-argument
@@ -146,20 +165,29 @@ class TestRemoteMethods(unittest.TestCase):
remote.call_service(master_api, "test_domain", "test_service")
- hass._pool.block_till_done()
+ hass.pool.block_till_done()
self.assertEqual(1, len(test_value))
+ # Should not raise an exception
+ remote.call_service(broken_api, "test_domain", "test_service")
+
class TestRemoteClasses(unittest.TestCase):
""" Test the homeassistant.remote module. """
def test_home_assistant_init(self):
""" Test HomeAssistant init. """
+ # Wrong password
self.assertRaises(
ha.HomeAssistantError, remote.HomeAssistant,
remote.API('127.0.0.1', API_PASSWORD + 'A', 8124))
+ # Wrong port
+ self.assertRaises(
+ ha.HomeAssistantError, remote.HomeAssistant,
+ remote.API('127.0.0.1', API_PASSWORD, 8125))
+
def test_statemachine_init(self):
""" Tests if remote.StateMachine copies all states on init. """
self.assertEqual(len(hass.states.all()),
@@ -176,7 +204,7 @@ class TestRemoteClasses(unittest.TestCase):
# Wait till slave tells master
slave._pool.block_till_done()
# Wait till master gives updated state
- hass._pool.block_till_done()
+ hass.pool.block_till_done()
self.assertEqual("remote.statemachine test",
slave.states.get("remote.test").state)
@@ -189,13 +217,23 @@ class TestRemoteClasses(unittest.TestCase):
""" Helper method that will verify our event got called. """
test_value.append(1)
- slave.listen_once_event("test.event_no_data", listener)
+ slave.bus.listen_once("test.event_no_data", listener)
slave.bus.fire("test.event_no_data")
# Wait till slave tells master
slave._pool.block_till_done()
# Wait till master gives updated event
- hass._pool.block_till_done()
+ hass.pool.block_till_done()
self.assertEqual(1, len(test_value))
+
+ def test_json_encoder(self):
+ """ Test the JSON Encoder. """
+ ha_json_enc = remote.JSONEncoder()
+ state = hass.states.get('test.test')
+
+ self.assertEqual(state.as_dict(), ha_json_enc.default(state))
+
+ # Default method raises TypeError if non HA object
+ self.assertRaises(TypeError, ha_json_enc.default, 1)
diff --git a/ha_test/test_util.py b/ha_test/test_util.py
new file mode 100644
index 00000000000..0f606fb45f2
--- /dev/null
+++ b/ha_test/test_util.py
@@ -0,0 +1,256 @@
+"""
+ha_test.test_util
+~~~~~~~~~~~~~~~~~
+
+Tests Home Assistant util methods.
+"""
+# pylint: disable=too-many-public-methods
+import unittest
+import time
+from datetime import datetime, timedelta
+
+import homeassistant.util as util
+
+
+class TestUtil(unittest.TestCase):
+ """ Tests util methods. """
+ def test_sanitize_filename(self):
+ """ Test sanitize_filename. """
+ self.assertEqual("test", util.sanitize_filename("test"))
+ self.assertEqual("test", util.sanitize_filename("/test"))
+ self.assertEqual("test", util.sanitize_filename("..test"))
+ self.assertEqual("test", util.sanitize_filename("\\test"))
+ self.assertEqual("test", util.sanitize_filename("\\../test"))
+
+ def test_sanitize_path(self):
+ """ Test sanitize_path. """
+ self.assertEqual("test/path", util.sanitize_path("test/path"))
+ self.assertEqual("test/path", util.sanitize_path("~test/path"))
+ self.assertEqual("//test/path",
+ util.sanitize_path("~/../test/path"))
+
+ def test_slugify(self):
+ """ Test slugify. """
+ self.assertEqual("Test", util.slugify("T-!@#$!#@$!$est"))
+ self.assertEqual("Test_More", util.slugify("Test More"))
+ self.assertEqual("Test_More", util.slugify("Test_(More)"))
+
+ def test_datetime_to_str(self):
+ """ Test datetime_to_str. """
+ self.assertEqual("12:00:00 09-07-1986",
+ util.datetime_to_str(datetime(1986, 7, 9, 12, 0, 0)))
+
+ def test_str_to_datetime(self):
+ """ Test str_to_datetime. """
+ self.assertEqual(datetime(1986, 7, 9, 12, 0, 0),
+ util.str_to_datetime("12:00:00 09-07-1986"))
+ self.assertIsNone(util.str_to_datetime("not a datetime string"))
+
+ def test_split_entity_id(self):
+ """ Test split_entity_id. """
+ self.assertEqual(['domain', 'object_id'],
+ util.split_entity_id('domain.object_id'))
+
+ def test_repr_helper(self):
+ """ Test repr_helper. """
+ self.assertEqual("A", util.repr_helper("A"))
+ self.assertEqual("5", util.repr_helper(5))
+ self.assertEqual("True", util.repr_helper(True))
+ self.assertEqual("test=1",
+ util.repr_helper({"test": 1}))
+ self.assertEqual("12:00:00 09-07-1986",
+ util.repr_helper(datetime(1986, 7, 9, 12, 0, 0)))
+
+ # pylint: disable=invalid-name
+ def test_color_RGB_to_xy(self):
+ """ Test color_RGB_to_xy. """
+ self.assertEqual((0, 0), util.color_RGB_to_xy(0, 0, 0))
+ self.assertEqual((0.3127159072215825, 0.3290014805066623),
+ util.color_RGB_to_xy(255, 255, 255))
+
+ self.assertEqual((0.15001662234042554, 0.060006648936170214),
+ util.color_RGB_to_xy(0, 0, 255))
+
+ self.assertEqual((0.3, 0.6), util.color_RGB_to_xy(0, 255, 0))
+
+ self.assertEqual((0.6400744994567747, 0.3299705106316933),
+ util.color_RGB_to_xy(255, 0, 0))
+
+ def test_convert(self):
+ """ Test convert. """
+ self.assertEqual(5, util.convert("5", int))
+ self.assertEqual(5.0, util.convert("5", float))
+ self.assertEqual(True, util.convert("True", bool))
+ self.assertEqual(1, util.convert("NOT A NUMBER", int, 1))
+ self.assertEqual(1, util.convert(None, int, 1))
+
+ def test_ensure_unique_string(self):
+ """ Test ensure_unique_string. """
+ self.assertEqual(
+ "Beer_3",
+ util.ensure_unique_string("Beer", ["Beer", "Beer_2"]))
+ self.assertEqual(
+ "Beer",
+ util.ensure_unique_string("Beer", ["Wine", "Soda"]))
+
+ def test_ordered_enum(self):
+ """ Test the ordered enum class. """
+
+ class TestEnum(util.OrderedEnum):
+ """ Test enum that can be ordered. """
+ FIRST = 1
+ SECOND = 2
+ THIRD = 3
+
+ self.assertTrue(TestEnum.SECOND >= TestEnum.FIRST)
+ self.assertTrue(TestEnum.SECOND >= TestEnum.SECOND)
+ self.assertFalse(TestEnum.SECOND >= TestEnum.THIRD)
+
+ self.assertTrue(TestEnum.SECOND > TestEnum.FIRST)
+ self.assertFalse(TestEnum.SECOND > TestEnum.SECOND)
+ self.assertFalse(TestEnum.SECOND > TestEnum.THIRD)
+
+ self.assertFalse(TestEnum.SECOND <= TestEnum.FIRST)
+ self.assertTrue(TestEnum.SECOND <= TestEnum.SECOND)
+ self.assertTrue(TestEnum.SECOND <= TestEnum.THIRD)
+
+ self.assertFalse(TestEnum.SECOND < TestEnum.FIRST)
+ self.assertFalse(TestEnum.SECOND < TestEnum.SECOND)
+ self.assertTrue(TestEnum.SECOND < TestEnum.THIRD)
+
+ # Python will raise a TypeError if the <, <=, >, >= methods
+ # raise a NotImplemented error.
+ self.assertRaises(TypeError,
+ lambda x, y: x < y, TestEnum.FIRST, 1)
+
+ self.assertRaises(TypeError,
+ lambda x, y: x <= y, TestEnum.FIRST, 1)
+
+ self.assertRaises(TypeError,
+ lambda x, y: x > y, TestEnum.FIRST, 1)
+
+ self.assertRaises(TypeError,
+ lambda x, y: x >= y, TestEnum.FIRST, 1)
+
+ def test_ordered_set(self):
+ set1 = util.OrderedSet([1, 2, 3, 4])
+ set2 = util.OrderedSet([3, 4, 5])
+
+ self.assertEqual(4, len(set1))
+ self.assertEqual(3, len(set2))
+
+ self.assertIn(1, set1)
+ self.assertIn(2, set1)
+ self.assertIn(3, set1)
+ self.assertIn(4, set1)
+ self.assertNotIn(5, set1)
+
+ self.assertNotIn(1, set2)
+ self.assertNotIn(2, set2)
+ self.assertIn(3, set2)
+ self.assertIn(4, set2)
+ self.assertIn(5, set2)
+
+ set1.add(5)
+ self.assertIn(5, set1)
+
+ set1.discard(5)
+ self.assertNotIn(5, set1)
+
+ # Try again while key is not in
+ set1.discard(5)
+ self.assertNotIn(5, set1)
+
+ self.assertEqual([1, 2, 3, 4], list(set1))
+ self.assertEqual([4, 3, 2, 1], list(reversed(set1)))
+
+ self.assertEqual(1, set1.pop(False))
+ self.assertEqual([2, 3, 4], list(set1))
+
+ self.assertEqual(4, set1.pop())
+ self.assertEqual([2, 3], list(set1))
+
+ self.assertEqual('OrderedSet()', str(util.OrderedSet()))
+ self.assertEqual('OrderedSet([2, 3])', str(set1))
+
+ self.assertEqual(set1, util.OrderedSet([2, 3]))
+ self.assertNotEqual(set1, util.OrderedSet([3, 2]))
+ self.assertEqual(set1, set([2, 3]))
+ self.assertEqual(set1, {3, 2})
+ self.assertEqual(set1, [2, 3])
+ self.assertEqual(set1, [3, 2])
+ self.assertNotEqual(set1, {2})
+
+ set3 = util.OrderedSet(set1)
+ set3.update(set2)
+
+ self.assertEqual([3, 4, 5, 2], set3)
+ self.assertEqual([3, 4, 5, 2], set1 | set2)
+ self.assertEqual([3], set1 & set2)
+ self.assertEqual([2], set1 - set2)
+
+ set1.update([1, 2], [5, 6])
+ self.assertEqual([2, 3, 1, 5, 6], set1)
+
+ def test_throttle(self):
+ """ Test the add cooldown decorator. """
+ calls1 = []
+
+ @util.Throttle(timedelta(milliseconds=500))
+ def test_throttle1():
+ calls1.append(1)
+
+ calls2 = []
+
+ @util.Throttle(
+ timedelta(milliseconds=500), timedelta(milliseconds=250))
+ def test_throttle2():
+ calls2.append(1)
+
+ # Ensure init is ok
+ self.assertEqual(0, len(calls1))
+ self.assertEqual(0, len(calls2))
+
+ # Call first time and ensure methods got called
+ test_throttle1()
+ test_throttle2()
+
+ self.assertEqual(1, len(calls1))
+ self.assertEqual(1, len(calls2))
+
+ # Call second time. Methods should not get called
+ test_throttle1()
+ test_throttle2()
+
+ self.assertEqual(1, len(calls1))
+ self.assertEqual(1, len(calls2))
+
+ # Call again, overriding throttle, only first one should fire
+ test_throttle1(no_throttle=True)
+ test_throttle2(no_throttle=True)
+
+ self.assertEqual(2, len(calls1))
+ self.assertEqual(1, len(calls2))
+
+ # Sleep past the no throttle interval for throttle2
+ time.sleep(.3)
+
+ test_throttle1()
+ test_throttle2()
+
+ self.assertEqual(2, len(calls1))
+ self.assertEqual(1, len(calls2))
+
+ test_throttle1(no_throttle=True)
+ test_throttle2(no_throttle=True)
+
+ self.assertEqual(3, len(calls1))
+ self.assertEqual(2, len(calls2))
+
+ time.sleep(.5)
+
+ test_throttle1()
+ test_throttle2()
+
+ self.assertEqual(4, len(calls1))
+ self.assertEqual(3, len(calls2))
diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py
index 7b836af8d05..e0a2316dfd8 100644
--- a/homeassistant/__init__.py
+++ b/homeassistant/__init__.py
@@ -15,37 +15,27 @@ import re
import datetime as dt
import functools as ft
+from requests.structures import CaseInsensitiveDict
+
+from homeassistant.const import (
+ EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
+ SERVICE_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED,
+ EVENT_CALL_SERVICE, ATTR_NOW, ATTR_DOMAIN, ATTR_SERVICE, MATCH_ALL,
+ EVENT_SERVICE_EXECUTED, ATTR_SERVICE_CALL_ID)
import homeassistant.util as util
-MATCH_ALL = '*'
-
DOMAIN = "homeassistant"
-SERVICE_HOMEASSISTANT_STOP = "stop"
-
-EVENT_HOMEASSISTANT_START = "homeassistant_start"
-EVENT_HOMEASSISTANT_STOP = "homeassistant_stop"
-EVENT_STATE_CHANGED = "state_changed"
-EVENT_TIME_CHANGED = "time_changed"
-EVENT_CALL_SERVICE = "call_service"
-
-ATTR_NOW = "now"
-ATTR_DOMAIN = "domain"
-ATTR_SERVICE = "service"
-
-CONF_LATITUDE = "latitude"
-CONF_LONGITUDE = "longitude"
-CONF_TYPE = "type"
-CONF_HOST = "host"
-CONF_HOSTS = "hosts"
-CONF_USERNAME = "username"
-CONF_PASSWORD = "password"
-
# How often time_changed event should fire
TIMER_INTERVAL = 10 # seconds
-# Number of worker threads
-POOL_NUM_THREAD = 4
+# How long we wait for the result of a service call
+SERVICE_CALL_LIMIT = 10 # seconds
+
+# Define number of MINIMUM worker threads.
+# During bootstrap of HA (see bootstrap.from_config_dict()) worker threads
+# will be added for each component that polls devices.
+MIN_WORKER_THREAD = 2
# Pattern for validating entity IDs (format: .)
ENTITY_ID_PATTERN = re.compile(r"^(?P\w+)\.(?P\w+)$")
@@ -57,8 +47,7 @@ class HomeAssistant(object):
""" Core class to route all communication to right components. """
def __init__(self):
- self._pool = pool = create_worker_pool()
-
+ self.pool = pool = create_worker_pool()
self.bus = EventBus(pool)
self.services = ServiceRegistry(self.bus, pool)
self.states = StateMachine(self.bus)
@@ -71,6 +60,9 @@ class HomeAssistant(object):
def start(self):
""" Start home assistant. """
+ _LOGGER.info(
+ "Starting Home Assistant (%d threads)", self.pool.worker_count)
+
Timer(self)
self.bus.fire(EVENT_HOMEASSISTANT_START)
@@ -92,50 +84,6 @@ class HomeAssistant(object):
self.stop()
- def call_service(self, domain, service, service_data=None):
- """ Fires event to call specified service. """
- event_data = service_data or {}
- event_data[ATTR_DOMAIN] = domain
- event_data[ATTR_SERVICE] = service
-
- self.bus.fire(EVENT_CALL_SERVICE, event_data)
-
- def get_entity_ids(self, domain_filter=None):
- """ Returns known entity ids. """
- if domain_filter:
- return [entity_id for entity_id in self.states.entity_ids
- if entity_id.startswith(domain_filter)]
- else:
- return self.states.entity_ids
-
- def track_state_change(self, entity_ids, action,
- from_state=None, to_state=None):
- """
- Track specific state changes.
- entity_ids, from_state and to_state can be string or list.
- Use list to match multiple.
- """
- from_state = _process_match_param(from_state)
- to_state = _process_match_param(to_state)
-
- # Ensure it is a list with entity ids we want to match on
- if isinstance(entity_ids, str):
- entity_ids = [entity_ids]
-
- @ft.wraps(action)
- def state_listener(event):
- """ The listener that listens for specific state changes. """
- if event.data['entity_id'] in entity_ids and \
- 'old_state' in event.data and \
- _matcher(event.data['old_state'].state, from_state) and \
- _matcher(event.data['new_state'].state, to_state):
-
- action(event.data['entity_id'],
- event.data['old_state'],
- event.data['new_state'])
-
- self.bus.listen(EVENT_STATE_CHANGED, state_listener)
-
def track_point_in_time(self, action, point_in_time):
"""
Adds a listener that fires once at or after a spefic point in time.
@@ -202,31 +150,6 @@ class HomeAssistant(object):
self.bus.listen(EVENT_TIME_CHANGED, time_listener)
- def listen_once_event(self, event_type, listener):
- """ Listen once for event of a specific type.
-
- To listen to all events specify the constant ``MATCH_ALL``
- as event_type.
-
- Note: at the moment it is impossible to remove a one time listener.
- """
- @ft.wraps(listener)
- def onetime_listener(event):
- """ Removes listener from eventbus and then fires listener. """
- if not hasattr(onetime_listener, 'run'):
- # Set variable so that we will never run twice.
- # Because the event bus might have to wait till a thread comes
- # available to execute this listener it might occur that the
- # listener gets lined up twice to be executed.
- # This will make sure the second time it does nothing.
- onetime_listener.run = True
-
- self.bus.remove_listener(event_type, onetime_listener)
-
- listener(event)
-
- self.bus.listen(event_type, onetime_listener)
-
def stop(self):
""" Stops Home Assistant and shuts down all threads. """
_LOGGER.info("Stopping")
@@ -234,14 +157,67 @@ class HomeAssistant(object):
self.bus.fire(EVENT_HOMEASSISTANT_STOP)
# Wait till all responses to homeassistant_stop are done
- self._pool.block_till_done()
+ self.pool.block_till_done()
- self._pool.stop()
+ self.pool.stop()
+
+ def get_entity_ids(self, domain_filter=None):
+ """
+ Returns known entity ids.
+
+ THIS METHOD IS DEPRECATED. Use hass.states.entity_ids
+ """
+ _LOGGER.warning(
+ "hass.get_entiy_ids is deprecated. Use hass.states.entity_ids")
+
+ return self.states.entity_ids(domain_filter)
+
+ def listen_once_event(self, event_type, listener):
+ """ Listen once for event of a specific type.
+
+ To listen to all events specify the constant ``MATCH_ALL``
+ as event_type.
+
+ Note: at the moment it is impossible to remove a one time listener.
+
+ THIS METHOD IS DEPRECATED. Please use hass.events.listen_once.
+ """
+ _LOGGER.warning(
+ "hass.listen_once_event is deprecated. Use hass.bus.listen_once")
+
+ self.bus.listen_once(event_type, listener)
+
+ def track_state_change(self, entity_ids, action,
+ from_state=None, to_state=None):
+ """
+ Track specific state changes.
+ entity_ids, from_state and to_state can be string or list.
+ Use list to match multiple.
+
+ THIS METHOD IS DEPRECATED. Use hass.states.track_change
+ """
+ _LOGGER.warning((
+ "hass.track_state_change is deprecated. "
+ "Use hass.states.track_change"))
+
+ self.states.track_change(entity_ids, action, from_state, to_state)
+
+ def call_service(self, domain, service, service_data=None):
+ """
+ Fires event to call specified service.
+
+ THIS METHOD IS DEPRECATED. Use hass.services.call
+ """
+ _LOGGER.warning((
+ "hass.services.call is deprecated. "
+ "Use hass.services.call"))
+
+ self.services.call(domain, service, service_data)
def _process_match_param(parameter):
""" Wraps parameter in a list if it is not one and returns it. """
- if not parameter or parameter == MATCH_ALL:
+ if parameter is None or parameter == MATCH_ALL:
return MATCH_ALL
elif isinstance(parameter, list):
return parameter
@@ -261,6 +237,7 @@ class JobPriority(util.OrderedEnum):
""" Provides priorities for bus events. """
# pylint: disable=no-init,too-few-public-methods
+ EVENT_CALLBACK = 0
EVENT_SERVICE = 1
EVENT_STATE = 2
EVENT_TIME = 3
@@ -275,11 +252,13 @@ class JobPriority(util.OrderedEnum):
return JobPriority.EVENT_STATE
elif event_type == EVENT_CALL_SERVICE:
return JobPriority.EVENT_SERVICE
+ elif event_type == EVENT_SERVICE_EXECUTED:
+ return JobPriority.EVENT_CALLBACK
else:
return JobPriority.EVENT_DEFAULT
-def create_worker_pool(thread_count=POOL_NUM_THREAD):
+def create_worker_pool():
""" Creates a worker pool to be used. """
def job_handler(job):
@@ -292,18 +271,18 @@ def create_worker_pool(thread_count=POOL_NUM_THREAD):
# We do not want to crash our ThreadPool
_LOGGER.exception("BusHandler:Exception doing job")
- def busy_callback(current_jobs, pending_jobs_count):
+ def busy_callback(worker_count, current_jobs, pending_jobs_count):
""" Callback to be called when the pool queue gets too big. """
- _LOGGER.error(
+ _LOGGER.warning(
"WorkerPool:All %d threads are busy and %d jobs pending",
- thread_count, pending_jobs_count)
+ worker_count, pending_jobs_count)
for start, job in current_jobs:
- _LOGGER.error("WorkerPool:Current job from %s: %s",
- util.datetime_to_str(start), job)
+ _LOGGER.warning("WorkerPool:Current job from %s: %s",
+ util.datetime_to_str(start), job)
- return util.ThreadPool(thread_count, job_handler, busy_callback)
+ return util.ThreadPool(job_handler, MIN_WORKER_THREAD, busy_callback)
class EventOrigin(enum.Enum):
@@ -374,9 +353,10 @@ class EventBus(object):
if not listeners:
return
+ job_priority = JobPriority.from_event_type(event_type)
+
for func in listeners:
- self._pool.add_job(JobPriority.from_event_type(event_type),
- (func, event))
+ self._pool.add_job(job_priority, (func, event))
def listen(self, event_type, listener):
""" Listen for all events or events of a specific type.
@@ -390,6 +370,31 @@ class EventBus(object):
else:
self._listeners[event_type] = [listener]
+ def listen_once(self, event_type, listener):
+ """ Listen once for event of a specific type.
+
+ To listen to all events specify the constant ``MATCH_ALL``
+ as event_type.
+
+ Note: at the moment it is impossible to remove a one time listener.
+ """
+ @ft.wraps(listener)
+ def onetime_listener(event):
+ """ Removes listener from eventbus and then fires listener. """
+ if not hasattr(onetime_listener, 'run'):
+ # Set variable so that we will never run twice.
+ # Because the event bus might have to wait till a thread comes
+ # available to execute this listener it might occur that the
+ # listener gets lined up twice to be executed.
+ # This will make sure the second time it does nothing.
+ onetime_listener.run = True
+
+ self.remove_listener(event_type, onetime_listener)
+
+ listener(event)
+
+ self.listen(event_type, onetime_listener)
+
def remove_listener(self, event_type, listener):
""" Removes a listener of a specific event_type. """
with self._lock:
@@ -420,17 +425,13 @@ class State(object):
self.entity_id = entity_id
self.state = state
self.attributes = attributes or {}
- last_changed = last_changed or dt.datetime.now()
# Strip microsecond from last_changed else we cannot guarantee
# state == State.from_dict(state.as_dict())
# This behavior occurs because to_dict uses datetime_to_str
- # which strips microseconds
- if last_changed.microsecond:
- self.last_changed = last_changed - dt.timedelta(
- microseconds=last_changed.microsecond)
- else:
- self.last_changed = last_changed
+ # which does not preserve microseconds
+ self.last_changed = util.strip_microseconds(
+ last_changed or dt.datetime.now())
def copy(self):
""" Creates a copy of itself. """
@@ -483,14 +484,20 @@ class StateMachine(object):
""" Helper class that tracks the state of different entities. """
def __init__(self, bus):
- self._states = {}
+ self._states = CaseInsensitiveDict()
self._bus = bus
self._lock = threading.Lock()
- @property
- def entity_ids(self):
+ def entity_ids(self, domain_filter=None):
""" List of entity ids that are being tracked. """
- return list(self._states.keys())
+ if domain_filter is not None:
+ domain_filter = domain_filter.lower()
+
+ return [state.entity_id for key, state
+ in self._states.lower_items()
+ if util.split_entity_id(key)[0] == domain_filter]
+ else:
+ return list(self._states.keys())
def all(self):
""" Returns a list of all states. """
@@ -503,15 +510,28 @@ class StateMachine(object):
# Make a copy so people won't mutate the state
return state.copy() if state else None
+ def get_since(self, point_in_time):
+ """
+ Returns all states that have been changed since point_in_time.
+
+ Note: States keep track of last_changed -without- microseconds.
+ Therefore your point_in_time will also be stripped of microseconds.
+ """
+ point_in_time = util.strip_microseconds(point_in_time)
+
+ with self._lock:
+ return [state for state in self._states.values()
+ if state.last_changed >= point_in_time]
+
def is_state(self, entity_id, state):
""" Returns True if entity exists and is specified state. """
return (entity_id in self._states and
self._states[entity_id].state == state)
def remove(self, entity_id):
- """ Removes a entity from the state machine.
+ """ Removes an entity from the state machine.
- Returns boolean to indicate if a entity was removed. """
+ Returns boolean to indicate if an entity was removed. """
with self._lock:
return self._states.pop(entity_id, None) is not None
@@ -540,6 +560,40 @@ class StateMachine(object):
self._bus.fire(EVENT_STATE_CHANGED, event_data)
+ def track_change(self, entity_ids, action, from_state=None, to_state=None):
+ """
+ Track specific state changes.
+ entity_ids, from_state and to_state can be string or list.
+ Use list to match multiple.
+
+ Returns the listener that listens on the bus for EVENT_STATE_CHANGED.
+ Pass the return value into hass.bus.remove_listener to remove it.
+ """
+ from_state = _process_match_param(from_state)
+ to_state = _process_match_param(to_state)
+
+ # Ensure it is a lowercase list with entity ids we want to match on
+ if isinstance(entity_ids, str):
+ entity_ids = [entity_ids.lower()]
+ else:
+ entity_ids = [entity_id.lower() for entity_id in entity_ids]
+
+ @ft.wraps(action)
+ def state_listener(event):
+ """ The listener that listens for specific state changes. """
+ if event.data['entity_id'].lower() in entity_ids and \
+ 'old_state' in event.data and \
+ _matcher(event.data['old_state'].state, from_state) and \
+ _matcher(event.data['new_state'].state, to_state):
+
+ action(event.data['entity_id'],
+ event.data['old_state'],
+ event.data['new_state'])
+
+ self._bus.listen(EVENT_STATE_CHANGED, state_listener)
+
+ return state_listener
+
# pylint: disable=too-few-public-methods
class ServiceCall(object):
@@ -567,6 +621,8 @@ class ServiceRegistry(object):
self._services = {}
self._lock = threading.Lock()
self._pool = pool or create_worker_pool()
+ self._bus = bus
+ self._cur_id = 0
bus.listen(EVENT_CALL_SERVICE, self._event_to_service_call)
@property
@@ -588,6 +644,57 @@ class ServiceRegistry(object):
else:
self._services[domain] = {service: service_func}
+ def call(self, domain, service, service_data=None, blocking=False):
+ """
+ Calls specified service.
+ Specify blocking=True to wait till service is executed.
+ Waits a maximum of SERVICE_CALL_LIMIT.
+
+ If blocking = True, will return boolean if service executed
+ succesfully within SERVICE_CALL_LIMIT.
+
+ This method will fire an event to call the service.
+ This event will be picked up by this ServiceRegistry and any
+ other ServiceRegistry that is listening on the EventBus.
+
+ Because the service is sent as an event you are not allowed to use
+ the keys ATTR_DOMAIN and ATTR_SERVICE in your service_data.
+ """
+ call_id = self._generate_unique_id()
+ event_data = service_data or {}
+ event_data[ATTR_DOMAIN] = domain
+ event_data[ATTR_SERVICE] = service
+ event_data[ATTR_SERVICE_CALL_ID] = call_id
+
+ if blocking:
+ executed_event = threading.Event()
+
+ def service_executed(call):
+ """
+ Called when a service is executed.
+ Will set the event if matches our service call.
+ """
+ if call.data[ATTR_SERVICE_CALL_ID] == call_id:
+ executed_event.set()
+
+ self._bus.remove_listener(
+ EVENT_SERVICE_EXECUTED, service_executed)
+
+ self._bus.listen(EVENT_SERVICE_EXECUTED, service_executed)
+
+ self._bus.fire(EVENT_CALL_SERVICE, event_data)
+
+ if blocking:
+ # wait will return False if event not set after our limit has
+ # passed. If not set, clean up the listener
+ if not executed_event.wait(SERVICE_CALL_LIMIT):
+ self._bus.remove_listener(
+ EVENT_SERVICE_EXECUTED, service_executed)
+
+ return False
+
+ return True
+
def _event_to_service_call(self, event):
""" Calls a service from an event. """
service_data = dict(event.data)
@@ -598,9 +705,27 @@ class ServiceRegistry(object):
if domain in self._services and service in self._services[domain]:
service_call = ServiceCall(domain, service, service_data)
+ # Add a job to the pool that calls _execute_service
self._pool.add_job(JobPriority.EVENT_SERVICE,
- (self._services[domain][service],
- service_call))
+ (self._execute_service,
+ (self._services[domain][service],
+ service_call)))
+
+ def _execute_service(self, service_and_call):
+ """ Executes a service and fires a SERVICE_EXECUTED event. """
+ service, call = service_and_call
+
+ service(call)
+
+ self._bus.fire(
+ EVENT_SERVICE_EXECUTED, {
+ ATTR_SERVICE_CALL_ID: call.data[ATTR_SERVICE_CALL_ID]
+ })
+
+ def _generate_unique_id(self):
+ """ Generates a unique service call id. """
+ self._cur_id += 1
+ return "{}-{}".format(id(self), self._cur_id)
class Timer(threading.Thread):
@@ -610,7 +735,7 @@ class Timer(threading.Thread):
threading.Thread.__init__(self)
self.daemon = True
- self._bus = hass.bus
+ self.hass = hass
self.interval = interval or TIMER_INTERVAL
self._stop = threading.Event()
@@ -619,15 +744,15 @@ class Timer(threading.Thread):
# every minute.
assert 60 % self.interval == 0, "60 % TIMER_INTERVAL should be 0!"
- hass.listen_once_event(EVENT_HOMEASSISTANT_START,
- lambda event: self.start())
-
- hass.listen_once_event(EVENT_HOMEASSISTANT_STOP,
- lambda event: self._stop.set())
+ hass.bus.listen_once(EVENT_HOMEASSISTANT_START,
+ lambda event: self.start())
def run(self):
""" Start the timer. """
+ self.hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
+ lambda event: self._stop.set())
+
_LOGGER.info("Timer:starting")
last_fired_on_second = -1
@@ -658,7 +783,7 @@ class Timer(threading.Thread):
last_fired_on_second = now.second
- self._bus.fire(EVENT_TIME_CHANGED, {ATTR_NOW: now})
+ self.hass.bus.fire(EVENT_TIME_CHANGED, {ATTR_NOW: now})
class HomeAssistantError(Exception):
diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py
index 81eaefdba24..1b2a8ee7312 100644
--- a/homeassistant/bootstrap.py
+++ b/homeassistant/bootstrap.py
@@ -13,12 +13,10 @@ import os
import configparser
import logging
from collections import defaultdict
-from itertools import chain
import homeassistant
import homeassistant.loader as loader
import homeassistant.components as core_components
-import homeassistant.components.group as group
# pylint: disable=too-many-branches, too-many-statements
@@ -33,123 +31,49 @@ def from_config_dict(config, hass=None):
logger = logging.getLogger(__name__)
+ loader.prepare(hass)
+
# Make a copy because we are mutating it.
# Convert it to defaultdict so components can always have config dict
config = defaultdict(dict, config)
- # List of loaded components
- components = {}
+ # Filter out the repeating and common config section [homeassistant]
+ components = (key for key in config.keys()
+ if ' ' not in key and key != homeassistant.DOMAIN)
- # List of components to validate
- to_validate = []
-
- # List of validated components
- validated = []
-
- # List of components we are going to load
- to_load = [key for key in config.keys() if key != homeassistant.DOMAIN]
-
- loader.prepare(hass)
-
- # Load required components
- while to_load:
- domain = to_load.pop()
-
- component = loader.get_component(domain)
-
- # if None it does not exist, error already thrown by get_component
- if component is not None:
- components[domain] = component
-
- # Special treatment for GROUP, we want to load it as late as
- # possible. We do this by loading it if all other to be loaded
- # modules depend on it.
- if component.DOMAIN == group.DOMAIN:
- pass
-
- # Components with no dependencies are valid
- elif not component.DEPENDENCIES:
- validated.append(domain)
-
- # If dependencies we'll validate it later
- else:
- to_validate.append(domain)
-
- # Make sure to load all dependencies that are not being loaded
- for dependency in component.DEPENDENCIES:
- if dependency not in chain(components.keys(), to_load):
- to_load.append(dependency)
-
- # Validate dependencies
- group_added = False
-
- while to_validate:
- newly_validated = []
-
- for domain in to_validate:
- if all(domain in validated for domain
- in components[domain].DEPENDENCIES):
-
- newly_validated.append(domain)
-
- # We validated new domains this iteration, add them to validated
- if newly_validated:
-
- # Add newly validated domains to validated
- validated.extend(newly_validated)
-
- # remove domains from to_validate
- for domain in newly_validated:
- to_validate.remove(domain)
-
- newly_validated.clear()
-
- # Nothing validated this iteration. Add group dependency and try again.
- elif not group_added:
- group_added = True
- validated.append(group.DOMAIN)
-
- # Group has already been added and we still can't validate all.
- # Report missing deps as error and skip loading of these domains
- else:
- for domain in to_validate:
- missing_deps = [dep for dep in components[domain].DEPENDENCIES
- if dep not in validated]
-
- logger.error(
- "Could not validate all dependencies for %s: %s",
- domain, ", ".join(missing_deps))
-
- break
-
- # Make sure we load groups if not in list yet.
- if not group_added:
- validated.append(group.DOMAIN)
-
- if group.DOMAIN not in components:
- components[group.DOMAIN] = \
- loader.get_component(group.DOMAIN)
-
- # Setup the components
- if core_components.setup(hass, config):
- logger.info("Home Assistant core initialized")
-
- for domain in validated:
- component = components[domain]
-
- try:
- if component.setup(hass, config):
- logger.info("component %s initialized", domain)
- else:
- logger.error("component %s failed to initialize", domain)
-
- except Exception: # pylint: disable=broad-except
- logger.exception("Error during setup of component %s", domain)
-
- else:
+ if not core_components.setup(hass, config):
logger.error(("Home Assistant core failed to initialize. "
"Further initialization aborted."))
+ return hass
+
+ logger.info("Home Assistant core initialized")
+
+ # Setup the components
+
+ # We assume that all components that load before the group component loads
+ # are components that poll devices. As their tasks are IO based, we will
+ # add an extra worker for each of them.
+ add_worker = True
+
+ for domain in loader.load_order_components(components):
+ component = loader.get_component(domain)
+
+ try:
+ if component.setup(hass, config):
+ logger.info("component %s initialized", domain)
+
+ add_worker = add_worker and domain != "group"
+
+ if add_worker:
+ hass.pool.add_worker()
+
+ else:
+ logger.error("component %s failed to initialize", domain)
+
+ except Exception: # pylint: disable=broad-except
+ logger.exception("Error during setup of component %s", domain)
+
return hass
@@ -181,7 +105,7 @@ def from_config_file(config_path, hass=None, enable_logging=True):
err_handler = logging.FileHandler(
err_log_path, mode='w', delay=True)
- err_handler.setLevel(logging.ERROR)
+ err_handler.setLevel(logging.WARNING)
err_handler.setFormatter(
logging.Formatter('%(asctime)s %(name)s: %(message)s',
datefmt='%H:%M %d-%m-%y'))
diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py
index a7eeadabc69..6720ae2a2d9 100644
--- a/homeassistant/components/__init__.py
+++ b/homeassistant/components/__init__.py
@@ -19,36 +19,10 @@ import logging
import homeassistant as ha
import homeassistant.util as util
+from homeassistant.helpers import extract_entity_ids
from homeassistant.loader import get_component
-
-# Contains one string or a list of strings, each being an entity id
-ATTR_ENTITY_ID = 'entity_id'
-
-# String with a friendly name for the entity
-ATTR_FRIENDLY_NAME = "friendly_name"
-
-# A picture to represent entity
-ATTR_ENTITY_PICTURE = "entity_picture"
-
-# The unit of measurement if applicable
-ATTR_UNIT_OF_MEASUREMENT = "unit_of_measurement"
-
-STATE_ON = 'on'
-STATE_OFF = 'off'
-STATE_HOME = 'home'
-STATE_NOT_HOME = 'not_home'
-
-SERVICE_TURN_ON = 'turn_on'
-SERVICE_TURN_OFF = 'turn_off'
-
-SERVICE_VOLUME_UP = "volume_up"
-SERVICE_VOLUME_DOWN = "volume_down"
-SERVICE_VOLUME_MUTE = "volume_mute"
-SERVICE_MEDIA_PLAY_PAUSE = "media_play_pause"
-SERVICE_MEDIA_PLAY = "media_play"
-SERVICE_MEDIA_PAUSE = "media_pause"
-SERVICE_MEDIA_NEXT_TRACK = "media_next_track"
-SERVICE_MEDIA_PREV_TRACK = "media_prev_track"
+from homeassistant.const import (
+ ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
_LOGGER = logging.getLogger(__name__)
@@ -61,7 +35,7 @@ def is_on(hass, entity_id=None):
entity_ids = group.expand_entity_ids(hass, [entity_id])
else:
- entity_ids = hass.states.entity_ids
+ entity_ids = hass.states.entity_ids()
for entity_id in entity_ids:
domain = util.split_entity_id(entity_id)[0]
@@ -85,7 +59,7 @@ def turn_on(hass, entity_id=None, **service_data):
if entity_id is not None:
service_data[ATTR_ENTITY_ID] = entity_id
- hass.call_service(ha.DOMAIN, SERVICE_TURN_ON, service_data)
+ hass.services.call(ha.DOMAIN, SERVICE_TURN_ON, service_data)
def turn_off(hass, entity_id=None, **service_data):
@@ -93,80 +67,7 @@ def turn_off(hass, entity_id=None, **service_data):
if entity_id is not None:
service_data[ATTR_ENTITY_ID] = entity_id
- hass.call_service(ha.DOMAIN, SERVICE_TURN_OFF, service_data)
-
-
-def extract_entity_ids(hass, service):
- """
- Helper method to extract a list of entity ids from a service call.
- Will convert group entity ids to the entity ids it represents.
- """
- entity_ids = []
-
- if service.data and ATTR_ENTITY_ID in service.data:
- group = get_component('group')
-
- # Entity ID attr can be a list or a string
- service_ent_id = service.data[ATTR_ENTITY_ID]
- if isinstance(service_ent_id, list):
- ent_ids = service_ent_id
- else:
- ent_ids = [service_ent_id]
-
- entity_ids.extend(
- ent_id for ent_id
- in group.expand_entity_ids(hass, ent_ids)
- if ent_id not in entity_ids)
-
- return entity_ids
-
-
-class ToggleDevice(object):
- """ ABC for devices that can be turned on and off. """
- # pylint: disable=no-self-use
-
- entity_id = None
-
- def get_name(self):
- """ Returns the name of the device if any. """
- return None
-
- def turn_on(self, **kwargs):
- """ Turn the device on. """
- pass
-
- def turn_off(self, **kwargs):
- """ Turn the device off. """
- pass
-
- def is_on(self):
- """ True if device is on. """
- return False
-
- def get_state_attributes(self):
- """ Returns optional state attributes. """
- return {}
-
- def update(self):
- """ Retrieve latest state from the real device. """
- pass
-
- def update_ha_state(self, hass, force_refresh=False):
- """
- Updates Home Assistant with current state of device.
- If force_refresh == True will update device before setting state.
- """
- if self.entity_id is None:
- raise ha.NoEntitySpecifiedError(
- "No entity specified for device {}".format(self.get_name()))
-
- if force_refresh:
- self.update()
-
- state = STATE_ON if self.is_on() else STATE_OFF
-
- return hass.states.set(self.entity_id, state,
- self.get_state_attributes())
+ hass.services.call(ha.DOMAIN, SERVICE_TURN_OFF, service_data)
# pylint: disable=unused-argument
@@ -195,7 +96,7 @@ def setup(hass, config):
# ent_ids is a generator, convert it to a list.
data[ATTR_ENTITY_ID] = list(ent_ids)
- hass.call_service(domain, service.service, data)
+ hass.services.call(domain, service.service, data, True)
hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service)
hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)
diff --git a/homeassistant/components/chromecast.py b/homeassistant/components/chromecast.py
index c5b0c180d99..fc5f7e73dc3 100644
--- a/homeassistant/components/chromecast.py
+++ b/homeassistant/components/chromecast.py
@@ -6,9 +6,14 @@ Provides functionality to interact with Chromecasts.
"""
import logging
-import homeassistant as ha
import homeassistant.util as util
-import homeassistant.components as components
+from homeassistant.helpers import extract_entity_ids
+from homeassistant.const import (
+ ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_VOLUME_UP,
+ SERVICE_VOLUME_DOWN, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY,
+ SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK,
+ CONF_HOSTS)
+
DOMAIN = 'chromecast'
DEPENDENCIES = []
@@ -38,7 +43,7 @@ def is_on(hass, entity_id=None):
""" Returns true if specified ChromeCast entity_id is on.
Will check all chromecasts if no entity_id specified. """
- entity_ids = [entity_id] if entity_id else hass.get_entity_ids(DOMAIN)
+ entity_ids = [entity_id] if entity_id else hass.states.entity_ids(DOMAIN)
return any(not hass.states.is_state(entity_id, STATE_NO_APP)
for entity_id in entity_ids)
@@ -46,58 +51,58 @@ def is_on(hass, entity_id=None):
def turn_off(hass, entity_id=None):
""" Will turn off specified Chromecast or all. """
- data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {}
+ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
- hass.call_service(DOMAIN, components.SERVICE_TURN_OFF, data)
+ hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
def volume_up(hass, entity_id=None):
""" Send the chromecast the command for volume up. """
- data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {}
+ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
- hass.call_service(DOMAIN, components.SERVICE_VOLUME_UP, data)
+ hass.services.call(DOMAIN, SERVICE_VOLUME_UP, data)
def volume_down(hass, entity_id=None):
""" Send the chromecast the command for volume down. """
- data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {}
+ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
- hass.call_service(DOMAIN, components.SERVICE_VOLUME_DOWN, data)
+ hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN, data)
def media_play_pause(hass, entity_id=None):
""" Send the chromecast the command for play/pause. """
- data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {}
+ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
- hass.call_service(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE, data)
+ hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, data)
def media_play(hass, entity_id=None):
""" Send the chromecast the command for play/pause. """
- data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {}
+ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
- hass.call_service(DOMAIN, components.SERVICE_MEDIA_PLAY, data)
+ hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY, data)
def media_pause(hass, entity_id=None):
""" Send the chromecast the command for play/pause. """
- data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {}
+ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
- hass.call_service(DOMAIN, components.SERVICE_MEDIA_PAUSE, data)
+ hass.services.call(DOMAIN, SERVICE_MEDIA_PAUSE, data)
def media_next_track(hass, entity_id=None):
""" Send the chromecast the command for next track. """
- data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {}
+ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
- hass.call_service(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK, data)
+ hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, data)
def media_prev_track(hass, entity_id=None):
""" Send the chromecast the command for prev track. """
- data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {}
+ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
- hass.call_service(DOMAIN, components.SERVICE_MEDIA_PREV_TRACK, data)
+ hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK, data)
# pylint: disable=too-many-locals, too-many-branches
@@ -114,8 +119,8 @@ def setup(hass, config):
return False
- if ha.CONF_HOSTS in config[DOMAIN]:
- hosts = config[DOMAIN][ha.CONF_HOSTS].split(",")
+ if CONF_HOSTS in config[DOMAIN]:
+ hosts = config[DOMAIN][CONF_HOSTS].split(",")
# If no hosts given, scan for chromecasts
else:
@@ -131,7 +136,7 @@ def setup(hass, config):
entity_id = util.ensure_unique_string(
ENTITY_ID_FORMAT.format(
util.slugify(cast.device.friendly_name)),
- list(casts.keys()))
+ casts.keys())
casts[entity_id] = cast
@@ -148,7 +153,7 @@ def setup(hass, config):
status = chromecast.app
- state_attr = {components.ATTR_FRIENDLY_NAME:
+ state_attr = {ATTR_FRIENDLY_NAME:
chromecast.device.friendly_name}
if status and status.app_id != pychromecast.APP_ID['HOME']:
@@ -196,7 +201,7 @@ def setup(hass, config):
def _service_to_entities(service):
""" Helper method to get entities from service. """
- entity_ids = components.extract_entity_ids(hass, service)
+ entity_ids = extract_entity_ids(hass, service)
if entity_ids:
for entity_id in entity_ids:
@@ -274,25 +279,25 @@ def setup(hass, config):
hass.track_time_change(update_chromecast_states)
- hass.services.register(DOMAIN, components.SERVICE_TURN_OFF,
+ hass.services.register(DOMAIN, SERVICE_TURN_OFF,
turn_off_service)
- hass.services.register(DOMAIN, components.SERVICE_VOLUME_UP,
+ hass.services.register(DOMAIN, SERVICE_VOLUME_UP,
volume_up_service)
- hass.services.register(DOMAIN, components.SERVICE_VOLUME_DOWN,
+ hass.services.register(DOMAIN, SERVICE_VOLUME_DOWN,
volume_down_service)
- hass.services.register(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE,
+ hass.services.register(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE,
media_play_pause_service)
- hass.services.register(DOMAIN, components.SERVICE_MEDIA_PLAY,
+ hass.services.register(DOMAIN, SERVICE_MEDIA_PLAY,
media_play_service)
- hass.services.register(DOMAIN, components.SERVICE_MEDIA_PAUSE,
+ hass.services.register(DOMAIN, SERVICE_MEDIA_PAUSE,
media_pause_service)
- hass.services.register(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK,
+ hass.services.register(DOMAIN, SERVICE_MEDIA_NEXT_TRACK,
media_next_track_service)
hass.services.register(DOMAIN, "start_fireplace",
diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py
index 67127f7e3cd..3bd7d2f1e33 100644
--- a/homeassistant/components/demo.py
+++ b/homeassistant/components/demo.py
@@ -8,11 +8,12 @@ import random
import homeassistant as ha
import homeassistant.loader as loader
-from homeassistant.components import (SERVICE_TURN_ON, SERVICE_TURN_OFF,
- STATE_ON, STATE_OFF, ATTR_ENTITY_PICTURE,
- extract_entity_ids)
-from homeassistant.components.light import (ATTR_XY_COLOR, ATTR_BRIGHTNESS,
- GROUP_NAME_ALL_LIGHTS)
+from homeassistant.helpers import extract_entity_ids
+from homeassistant.const import (
+ SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON, STATE_OFF,
+ ATTR_ENTITY_PICTURE, ATTR_ENTITY_ID, CONF_LATITUDE, CONF_LONGITUDE)
+from homeassistant.components.light import (
+ ATTR_XY_COLOR, ATTR_BRIGHTNESS, GROUP_NAME_ALL_LIGHTS)
from homeassistant.util import split_entity_id
DOMAIN = "demo"
@@ -24,6 +25,9 @@ def setup(hass, config):
""" Setup a demo environment. """
group = loader.get_component('group')
+ config.setdefault(ha.DOMAIN, {})
+ config.setdefault(DOMAIN, {})
+
if config[DOMAIN].get('hide_demo_state') != '1':
hass.states.set('a.Demo_Mode', 'Enabled')
@@ -35,7 +39,12 @@ def setup(hass, config):
def mock_turn_on(service):
""" Will fake the component has been turned on. """
- for entity_id in extract_entity_ids(hass, service):
+ if service.data and ATTR_ENTITY_ID in service.data:
+ entity_ids = extract_entity_ids(hass, service)
+ else:
+ entity_ids = hass.states.entity_ids(service.domain)
+
+ for entity_id in entity_ids:
domain, _ = split_entity_id(entity_id)
if domain == "light":
@@ -48,15 +57,20 @@ def setup(hass, config):
def mock_turn_off(service):
""" Will fake the component has been turned off. """
- for entity_id in extract_entity_ids(hass, service):
+ if service.data and ATTR_ENTITY_ID in service.data:
+ entity_ids = extract_entity_ids(hass, service)
+ else:
+ entity_ids = hass.states.entity_ids(service.domain)
+
+ for entity_id in entity_ids:
hass.states.set(entity_id, STATE_OFF)
# Setup sun
- if ha.CONF_LATITUDE not in config[ha.DOMAIN]:
- config[ha.DOMAIN][ha.CONF_LATITUDE] = '32.87336'
+ if CONF_LATITUDE not in config[ha.DOMAIN]:
+ config[ha.DOMAIN][CONF_LATITUDE] = '32.87336'
- if ha.CONF_LONGITUDE not in config[ha.DOMAIN]:
- config[ha.DOMAIN][ha.CONF_LONGITUDE] = '-117.22743'
+ if CONF_LONGITUDE not in config[ha.DOMAIN]:
+ config[ha.DOMAIN][CONF_LONGITUDE] = '-117.22743'
loader.get_component('sun').setup(hass, config)
diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py
index 0fc2b99ee31..c9668853c73 100644
--- a/homeassistant/components/device_sun_light_trigger.py
+++ b/homeassistant/components/device_sun_light_trigger.py
@@ -8,7 +8,7 @@ the state of the sun and devices.
import logging
from datetime import datetime, timedelta
-import homeassistant.components as components
+from homeassistant.const import STATE_HOME, STATE_NOT_HOME
from . import light, sun, device_tracker, group
DOMAIN = "device_sun_light_trigger"
@@ -21,6 +21,7 @@ LIGHT_PROFILE = 'relax'
CONF_LIGHT_PROFILE = 'light_profile'
CONF_LIGHT_GROUP = 'light_group'
+CONF_DEVICE_GROUP = 'device_group'
# pylint: disable=too-many-branches
@@ -30,13 +31,17 @@ def setup(hass, config):
disable_turn_off = 'disable_turn_off' in config[DOMAIN]
light_group = config[DOMAIN].get(CONF_LIGHT_GROUP,
- light.GROUP_NAME_ALL_LIGHTS)
+ light.ENTITY_ID_ALL_LIGHTS)
light_profile = config[DOMAIN].get(CONF_LIGHT_PROFILE, LIGHT_PROFILE)
+ device_group = config[DOMAIN].get(CONF_DEVICE_GROUP,
+ device_tracker.ENTITY_ID_ALL_DEVICES)
+
logger = logging.getLogger(__name__)
- device_entity_ids = hass.get_entity_ids(device_tracker.DOMAIN)
+ device_entity_ids = group.get_entity_ids(hass, device_group,
+ device_tracker.DOMAIN)
if not device_entity_ids:
logger.error("No devices found to track")
@@ -92,8 +97,8 @@ def setup(hass, config):
# Track every time sun rises so we can schedule a time-based
# pre-sun set event
- hass.track_state_change(sun.ENTITY_ID, schedule_light_on_sun_rise,
- sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON)
+ hass.states.track_change(sun.ENTITY_ID, schedule_light_on_sun_rise,
+ sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON)
# If the sun is already above horizon
# schedule the time-based pre-sun set event
@@ -108,7 +113,7 @@ def setup(hass, config):
# Specific device came home ?
if entity != device_tracker.ENTITY_ID_ALL_DEVICES and \
- new_state.state == components.STATE_HOME:
+ new_state.state == STATE_HOME:
# These variables are needed for the elif check
now = datetime.now()
@@ -142,8 +147,8 @@ def setup(hass, config):
break
# Did all devices leave the house?
- elif (entity == device_tracker.ENTITY_ID_ALL_DEVICES and
- new_state.state == components.STATE_NOT_HOME and lights_are_on
+ elif (entity == device_group and
+ new_state.state == STATE_NOT_HOME and lights_are_on
and not disable_turn_off):
logger.info(
@@ -152,12 +157,13 @@ def setup(hass, config):
light.turn_off(hass)
# Track home coming of each device
- hass.track_state_change(device_entity_ids, check_light_on_dev_state_change,
- components.STATE_NOT_HOME, components.STATE_HOME)
+ hass.states.track_change(
+ device_entity_ids, check_light_on_dev_state_change,
+ STATE_NOT_HOME, STATE_HOME)
# Track when all devices are gone to shut down lights
- hass.track_state_change(device_tracker.ENTITY_ID_ALL_DEVICES,
- check_light_on_dev_state_change,
- components.STATE_HOME, components.STATE_NOT_HOME)
+ hass.states.track_change(
+ device_group, check_light_on_dev_state_change,
+ STATE_HOME, STATE_NOT_HOME)
return True
diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py
index 85e7d206e3e..c478e118036 100644
--- a/homeassistant/components/device_tracker/__init__.py
+++ b/homeassistant/components/device_tracker/__init__.py
@@ -1,6 +1,6 @@
"""
homeassistant.components.tracker
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to keep track of devices.
"""
@@ -10,11 +10,13 @@ import os
import csv
from datetime import datetime, timedelta
-import homeassistant as ha
from homeassistant.loader import get_component
+from homeassistant.helpers import validate_config
import homeassistant.util as util
-import homeassistant.components as components
+from homeassistant.const import (
+ STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME,
+ CONF_PLATFORM, CONF_TYPE)
from homeassistant.components import group
DOMAIN = "device_tracker"
@@ -30,7 +32,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
# After how much time do we consider a device not home if
# it does not show up on scans
-TIME_SPAN_FOR_ERROR_IN_SCANNING = timedelta(minutes=3)
+TIME_DEVICE_NOT_FOUND = timedelta(minutes=3)
# Filename to save known devices to
KNOWN_DEVICES_FILE = "known_devices.csv"
@@ -43,16 +45,26 @@ def is_on(hass, entity_id=None):
""" Returns if any or specified device is home. """
entity = entity_id or ENTITY_ID_ALL_DEVICES
- return hass.states.is_state(entity, components.STATE_HOME)
+ return hass.states.is_state(entity, STATE_HOME)
def setup(hass, config):
""" Sets up the device tracker. """
- if not util.validate_config(config, {DOMAIN: [ha.CONF_TYPE]}, _LOGGER):
+ # CONF_TYPE is deprecated for CONF_PLATOFRM. We keep supporting it for now.
+ if not (validate_config(config, {DOMAIN: [CONF_PLATFORM]}, _LOGGER)
+ or validate_config(config, {DOMAIN: [CONF_TYPE]}, _LOGGER)):
+
return False
- tracker_type = config[DOMAIN][ha.CONF_TYPE]
+ tracker_type = config[DOMAIN].get(CONF_PLATFORM)
+
+ if tracker_type is None:
+ tracker_type = config[DOMAIN][CONF_TYPE]
+
+ _LOGGER.warning((
+ "Please update your config for %s to use 'platform' "
+ "instead of 'type'"), tracker_type)
tracker_implementation = get_component(
'device_tracker.{}'.format(tracker_type))
@@ -70,105 +82,109 @@ def setup(hass, config):
return False
- DeviceTracker(hass, device_scanner)
+ tracker = DeviceTracker(hass, device_scanner)
- return True
+ # We only succeeded if we got to parse the known devices file
+ return not tracker.invalid_known_devices_file
-# pylint: disable=too-many-instance-attributes
class DeviceTracker(object):
""" Class that tracks which devices are home and which are not. """
def __init__(self, hass, device_scanner):
- self.states = hass.states
+ self.hass = hass
self.device_scanner = device_scanner
- self.error_scanning = TIME_SPAN_FOR_ERROR_IN_SCANNING
-
self.lock = threading.Lock()
- self.path_known_devices_file = hass.get_config_path(KNOWN_DEVICES_FILE)
-
# Dictionary to keep track of known devices and devices we track
- self.known_devices = {}
+ self.tracked = {}
+ self.untracked_devices = set()
# Did we encounter an invalid known devices file
self.invalid_known_devices_file = False
- self._read_known_devices_file()
-
# Wrap it in a func instead of lambda so it can be identified in
# the bus by its __name__ attribute.
- def update_device_state(time): # pylint: disable=unused-argument
+ def update_device_state(now):
""" Triggers update of the device states. """
- self.update_devices()
+ self.update_devices(now)
+
+ # pylint: disable=unused-argument
+ def reload_known_devices_service(service):
+ """ Reload known devices file. """
+ group.remove_group(self.hass, GROUP_NAME_ALL_DEVICES)
+
+ self._read_known_devices_file()
+
+ self.update_devices(datetime.now())
+
+ if self.tracked:
+ group.setup_group(
+ self.hass, GROUP_NAME_ALL_DEVICES,
+ self.device_entity_ids, False)
+
+ reload_known_devices_service(None)
+
+ if self.invalid_known_devices_file:
+ return
hass.track_time_change(update_device_state)
hass.services.register(DOMAIN,
SERVICE_DEVICE_TRACKER_RELOAD,
- lambda service: self._read_known_devices_file())
-
- self.update_devices()
-
- group.setup_group(
- hass, GROUP_NAME_ALL_DEVICES, self.device_entity_ids, False)
+ reload_known_devices_service)
@property
def device_entity_ids(self):
""" Returns a set containing all device entity ids
that are being tracked. """
- return set([self.known_devices[device]['entity_id'] for device
- in self.known_devices
- if self.known_devices[device]['track']])
+ return set(device['entity_id'] for device in self.tracked.values())
- def update_devices(self, found_devices=None):
+ def _update_state(self, now, device, is_home):
+ """ Update the state of a device. """
+ dev_info = self.tracked[device]
+
+ if is_home:
+ # Update last seen if at home
+ dev_info['last_seen'] = now
+ else:
+ # State remains at home if it has been seen in the last
+ # TIME_DEVICE_NOT_FOUND
+ is_home = now - dev_info['last_seen'] < TIME_DEVICE_NOT_FOUND
+
+ state = STATE_HOME if is_home else STATE_NOT_HOME
+
+ self.hass.states.set(
+ dev_info['entity_id'], state,
+ dev_info['state_attr'])
+
+ def update_devices(self, now):
""" Update device states based on the found devices. """
self.lock.acquire()
- found_devices = found_devices or self.device_scanner.scan_devices()
+ found_devices = set(self.device_scanner.scan_devices())
- now = datetime.now()
+ for device in self.tracked:
+ is_home = device in found_devices
- known_dev = self.known_devices
+ self._update_state(now, device, is_home)
- temp_tracking_devices = [device for device in known_dev
- if known_dev[device]['track']]
+ if is_home:
+ found_devices.remove(device)
- for device in found_devices:
- # Are we tracking this device?
- if device in temp_tracking_devices:
- temp_tracking_devices.remove(device)
+ # Did we find any devices that we didn't know about yet?
+ new_devices = found_devices - self.untracked_devices
- known_dev[device]['last_seen'] = now
+ if new_devices:
+ self.untracked_devices.update(new_devices)
- self.states.set(
- known_dev[device]['entity_id'], components.STATE_HOME,
- known_dev[device]['default_state_attr'])
+ # Write new devices to known devices file
+ if not self.invalid_known_devices_file:
- # For all devices we did not find, set state to NH
- # But only if they have been gone for longer then the error time span
- # Because we do not want to have stuff happening when the device does
- # not show up for 1 scan beacuse of reboot etc
- for device in temp_tracking_devices:
- if now - known_dev[device]['last_seen'] > self.error_scanning:
+ known_dev_path = self.hass.get_config_path(KNOWN_DEVICES_FILE)
- self.states.set(known_dev[device]['entity_id'],
- components.STATE_NOT_HOME,
- known_dev[device]['default_state_attr'])
-
- # If we come along any unknown devices we will write them to the
- # known devices file but only if we did not encounter an invalid
- # known devices file
- if not self.invalid_known_devices_file:
-
- known_dev_path = self.path_known_devices_file
-
- unknown_devices = [device for device in found_devices
- if device not in known_dev]
-
- if unknown_devices:
try:
# If file does not exist we will write the header too
is_new_file = not os.path.isfile(known_dev_path)
@@ -176,7 +192,7 @@ class DeviceTracker(object):
with open(known_dev_path, 'a') as outp:
_LOGGER.info(
"Found %d new devices, updating %s",
- len(unknown_devices), known_dev_path)
+ len(new_devices), known_dev_path)
writer = csv.writer(outp)
@@ -184,109 +200,114 @@ class DeviceTracker(object):
writer.writerow((
"device", "name", "track", "picture"))
- for device in unknown_devices:
+ for device in new_devices:
# See if the device scanner knows the name
# else defaults to unknown device
name = (self.device_scanner.get_device_name(device)
or "unknown_device")
writer.writerow((device, name, 0, ""))
- known_dev[device] = {'name': name,
- 'track': False,
- 'picture': ""}
except IOError:
_LOGGER.exception(
"Error updating %s with %d new devices",
- known_dev_path, len(unknown_devices))
+ known_dev_path, len(new_devices))
self.lock.release()
+ # pylint: disable=too-many-branches
def _read_known_devices_file(self):
""" Parse and process the known devices file. """
+ known_dev_path = self.hass.get_config_path(KNOWN_DEVICES_FILE)
- # Read known devices if file exists
- if os.path.isfile(self.path_known_devices_file):
- self.lock.acquire()
+ # Return if no known devices file exists
+ if not os.path.isfile(known_dev_path):
+ return
- known_devices = {}
+ self.lock.acquire()
- with open(self.path_known_devices_file) as inp:
- default_last_seen = datetime(1990, 1, 1)
+ self.untracked_devices.clear()
- # Temp variable to keep track of which entity ids we use
- # so we can ensure we have unique entity ids.
- used_entity_ids = []
+ with open(known_dev_path) as inp:
+ default_last_seen = datetime(1990, 1, 1)
- try:
- for row in csv.DictReader(inp):
- device = row['device']
+ # To track which devices need an entity_id assigned
+ need_entity_id = []
- row['track'] = True if row['track'] == '1' else False
+ # All devices that are still in this set after we read the CSV file
+ # have been removed from the file and thus need to be cleaned up.
+ removed_devices = set(self.tracked.keys())
+
+ try:
+ for row in csv.DictReader(inp):
+ device = row['device']
+
+ if row['track'] == '1':
+ if device in self.tracked:
+ # Device exists
+ removed_devices.remove(device)
+ else:
+ # We found a new device
+ need_entity_id.append(device)
+
+ self.tracked[device] = {
+ 'name': row['name'],
+ 'last_seen': default_last_seen
+ }
+
+ # Update state_attr with latest from file
+ state_attr = {
+ ATTR_FRIENDLY_NAME: row['name']
+ }
if row['picture']:
- row['default_state_attr'] = {
- components.ATTR_ENTITY_PICTURE: row['picture']}
+ state_attr[ATTR_ENTITY_PICTURE] = row['picture']
- else:
- row['default_state_attr'] = None
+ self.tracked[device]['state_attr'] = state_attr
- # If we track this device setup tracking variables
- if row['track']:
- row['last_seen'] = default_last_seen
+ else:
+ self.untracked_devices.add(device)
- # Make sure that each device is mapped
- # to a unique entity_id name
- name = util.slugify(row['name']) if row['name'] \
- else "unnamed_device"
+ # Remove existing devices that we no longer track
+ for device in removed_devices:
+ entity_id = self.tracked[device]['entity_id']
- entity_id = ENTITY_ID_FORMAT.format(name)
- tries = 1
+ _LOGGER.info("Removing entity %s", entity_id)
- while entity_id in used_entity_ids:
- tries += 1
+ self.hass.states.remove(entity_id)
- suffix = "_{}".format(tries)
+ self.tracked.pop(device)
- entity_id = ENTITY_ID_FORMAT.format(
- name + suffix)
+ # Setup entity_ids for the new devices
+ used_entity_ids = [info['entity_id'] for device, info
+ in self.tracked.items()
+ if device not in need_entity_id]
- row['entity_id'] = entity_id
- used_entity_ids.append(entity_id)
+ for device in need_entity_id:
+ name = self.tracked[device]['name']
- row['picture'] = row['picture']
+ entity_id = util.ensure_unique_string(
+ ENTITY_ID_FORMAT.format(util.slugify(name)),
+ used_entity_ids)
- known_devices[device] = row
+ used_entity_ids.append(entity_id)
- if not known_devices:
- _LOGGER.warning(
- "No devices to track. Please update %s.",
- self.path_known_devices_file)
+ self.tracked[device]['entity_id'] = entity_id
- # Remove entities that are no longer maintained
- new_entity_ids = set([known_devices[dev]['entity_id']
- for dev in known_devices
- if known_devices[dev]['track']])
-
- for entity_id in \
- self.device_entity_ids - new_entity_ids:
-
- _LOGGER.info("Removing entity %s", entity_id)
- self.states.remove(entity_id)
-
- # File parsed, warnings given if necessary
- # entities cleaned up, make it available
- self.known_devices = known_devices
-
- _LOGGER.info("Loaded devices from %s",
- self.path_known_devices_file)
-
- except KeyError:
- self.invalid_known_devices_file = True
+ if not self.tracked:
_LOGGER.warning(
- ("Invalid known devices file: %s. "
- "We won't update it with new found devices."),
- self.path_known_devices_file)
+ "No devices to track. Please update %s.",
+ known_dev_path)
- finally:
- self.lock.release()
+ _LOGGER.info("Loaded devices from %s", known_dev_path)
+
+ except KeyError:
+ self.invalid_known_devices_file = True
+
+ _LOGGER.warning(
+ ("Invalid known devices file: %s. "
+ "We won't update it with new found devices."),
+ known_dev_path)
+
+ finally:
+ self.lock.release()
diff --git a/homeassistant/components/device_tracker/luci.py b/homeassistant/components/device_tracker/luci.py
index 89d50f0239f..9ed73f21375 100644
--- a/homeassistant/components/device_tracker/luci.py
+++ b/homeassistant/components/device_tracker/luci.py
@@ -1,13 +1,14 @@
""" Supports scanning a OpenWRT router. """
import logging
import json
-from datetime import datetime, timedelta
+from datetime import timedelta
import re
import threading
import requests
-import homeassistant as ha
-import homeassistant.util as util
+from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
+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
@@ -19,10 +20,9 @@ _LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def get_scanner(hass, config):
""" Validates config and returns a Luci scanner. """
- if not util.validate_config(config,
- {DOMAIN: [ha.CONF_HOST, ha.CONF_USERNAME,
- ha.CONF_PASSWORD]},
- _LOGGER):
+ if not validate_config(config,
+ {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
+ _LOGGER):
return None
scanner = LuciDeviceScanner(config[DOMAIN])
@@ -45,14 +45,13 @@ class LuciDeviceScanner(object):
"""
def __init__(self, config):
- host = config[ha.CONF_HOST]
- username, password = config[ha.CONF_USERNAME], config[ha.CONF_PASSWORD]
+ host = config[CONF_HOST]
+ username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
self.parse_api_pattern = re.compile(r"(?P\w*) = (?P.*);")
self.lock = threading.Lock()
- self.date_updated = None
self.last_results = {}
self.token = _get_token(host, username, password)
@@ -88,29 +87,25 @@ class LuciDeviceScanner(object):
return
return self.mac2name.get(device, None)
+ @Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
""" Ensures the information from the Luci router is up to date.
Returns boolean if scanning successful. """
if not self.success_init:
return False
+
with self.lock:
- # if date_updated is None or the date is too old we scan
- # for new data
- if not self.date_updated or \
- datetime.now() - self.date_updated > MIN_TIME_BETWEEN_SCANS:
+ _LOGGER.info("Checking ARP")
- _LOGGER.info("Checking ARP")
+ url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host)
+ result = _req_json_rpc(url, 'net.arptable',
+ params={'auth': self.token})
+ if result:
+ self.last_results = [x['HW address'] for x in result]
- url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host)
- result = _req_json_rpc(url, 'net.arptable',
- params={'auth': self.token})
- if result:
- self.last_results = [x['HW address'] for x in result]
- self.date_updated = datetime.now()
- return True
- return False
+ return True
- return True
+ return False
def _req_json_rpc(url, method, *args, **kwargs):
diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py
index 23eda17fff8..0b0f1107b21 100644
--- a/homeassistant/components/device_tracker/netgear.py
+++ b/homeassistant/components/device_tracker/netgear.py
@@ -1,10 +1,11 @@
""" Supports scanning a Netgear router. """
import logging
-from datetime import datetime, timedelta
+from datetime import timedelta
import threading
-import homeassistant as ha
-import homeassistant.util as util
+from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
+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
@@ -16,10 +17,9 @@ _LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def get_scanner(hass, config):
""" Validates config and returns a Netgear scanner. """
- if not util.validate_config(config,
- {DOMAIN: [ha.CONF_HOST, ha.CONF_USERNAME,
- ha.CONF_PASSWORD]},
- _LOGGER):
+ if not validate_config(config,
+ {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
+ _LOGGER):
return None
scanner = NetgearDeviceScanner(config[DOMAIN])
@@ -31,10 +31,9 @@ class NetgearDeviceScanner(object):
""" This class queries a Netgear wireless router using the SOAP-api. """
def __init__(self, config):
- host = config[ha.CONF_HOST]
- username, password = config[ha.CONF_USERNAME], config[ha.CONF_PASSWORD]
+ host = config[CONF_HOST]
+ username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
- self.date_updated = None
self.last_results = []
try:
@@ -75,10 +74,6 @@ class NetgearDeviceScanner(object):
def get_device_name(self, mac):
""" Returns the name of the given device or None if we don't know. """
- # Make sure there are results
- if not self.date_updated:
- self._update_info()
-
filter_named = [device.name for device in self.last_results
if device.mac == mac]
@@ -87,6 +82,7 @@ class NetgearDeviceScanner(object):
else:
return None
+ @Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
""" Retrieves latest information from the Netgear router.
Returns boolean if scanning successful. """
@@ -94,18 +90,6 @@ class NetgearDeviceScanner(object):
return
with self.lock:
- # if date_updated is None or the date is too old we scan for
- # new data
- if not self.date_updated or \
- datetime.now() - self.date_updated > MIN_TIME_BETWEEN_SCANS:
+ _LOGGER.info("Scanning")
- _LOGGER.info("Scanning")
-
- self.last_results = self._api.get_attached_devices()
-
- self.date_updated = datetime.now()
-
- return
-
- else:
- return
+ self.last_results = self._api.get_attached_devices()
diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py
new file mode 100644
index 00000000000..4d914d5c1c0
--- /dev/null
+++ b/homeassistant/components/device_tracker/nmap_tracker.py
@@ -0,0 +1,123 @@
+""" Supports scanning using nmap. """
+import logging
+from datetime import timedelta
+import threading
+from collections import namedtuple
+import subprocess
+import re
+
+from libnmap.process import NmapProcess
+from libnmap.parser import NmapParser, NmapParserException
+
+from homeassistant.const import CONF_HOSTS
+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)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+# pylint: disable=unused-argument
+def get_scanner(hass, config):
+ """ Validates config and returns a Nmap scanner. """
+ if not validate_config(config, {DOMAIN: [CONF_HOSTS]},
+ _LOGGER):
+ return None
+
+ scanner = NmapDeviceScanner(config[DOMAIN])
+
+ return scanner if scanner.success_init else None
+
+Device = namedtuple("Device", ["mac", "name"])
+
+
+def _arp(ip_address):
+ """ Get the MAC address for a given IP """
+ cmd = ['arp', '-n', ip_address]
+ arp = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+ out, _ = arp.communicate()
+ match = re.search(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})', str(out))
+ if match:
+ return match.group(0)
+ _LOGGER.info("No MAC address found for %s", ip_address)
+ return ''
+
+
+class NmapDeviceScanner(object):
+ """ This class scans for devices using nmap """
+
+ def __init__(self, config):
+ self.last_results = []
+
+ self.lock = threading.Lock()
+ self.hosts = config[CONF_HOSTS]
+
+ self.success_init = True
+ self._update_info()
+ _LOGGER.info("nmap scanner initialized")
+
+ def scan_devices(self):
+ """ Scans for new devices and return a
+ list containing found device ids. """
+
+ self._update_info()
+
+ return [device.mac for device in self.last_results]
+
+ def get_device_name(self, mac):
+ """ Returns the name of the given device or None if we don't know. """
+
+ filter_named = [device.name for device in self.last_results
+ if device.mac == mac]
+
+ if filter_named:
+ return filter_named[0]
+ else:
+ return None
+
+ @Throttle(MIN_TIME_BETWEEN_SCANS)
+ def _update_info(self):
+ """ Scans the network for devices.
+ Returns boolean if scanning successful. """
+ if not self.success_init:
+ return False
+
+ with self.lock:
+ _LOGGER.info("Scanning")
+
+ nmap = NmapProcess(targets=self.hosts, options="-F")
+
+ nmap.run()
+
+ if nmap.rc == 0:
+ try:
+ results = NmapParser.parse(nmap.stdout)
+ self.last_results = []
+ for host in results.hosts:
+ if host.is_up():
+ if host.hostnames:
+ name = host.hostnames[0]
+ else:
+ name = host.ipv4
+ if host.mac:
+ mac = host.mac
+ else:
+ mac = _arp(host.ipv4)
+ if mac:
+ device = Device(mac, name)
+ self.last_results.append(device)
+ _LOGGER.info("nmap scan successful")
+ return True
+ except NmapParserException as parse_exc:
+ _LOGGER.error("failed to parse nmap results: %s",
+ parse_exc.msg)
+ self.last_results = []
+ return False
+
+ else:
+ self.last_results = []
+ _LOGGER.error(nmap.stderr)
+ return False
diff --git a/homeassistant/components/device_tracker/tomato.py b/homeassistant/components/device_tracker/tomato.py
index 748ad53f534..81755f42c66 100644
--- a/homeassistant/components/device_tracker/tomato.py
+++ b/homeassistant/components/device_tracker/tomato.py
@@ -1,14 +1,15 @@
""" Supports scanning a Tomato router. """
import logging
import json
-from datetime import datetime, timedelta
+from datetime import timedelta
import re
import threading
import requests
-import homeassistant as ha
-import homeassistant.util as util
+from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
+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
@@ -22,10 +23,10 @@ _LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def get_scanner(hass, config):
""" Validates config and returns a Tomato scanner. """
- if not util.validate_config(config,
- {DOMAIN: [ha.CONF_HOST, ha.CONF_USERNAME,
- ha.CONF_PASSWORD, CONF_HTTP_ID]},
- _LOGGER):
+ if not validate_config(config,
+ {DOMAIN: [CONF_HOST, CONF_USERNAME,
+ CONF_PASSWORD, CONF_HTTP_ID]},
+ _LOGGER):
return None
return TomatoDeviceScanner(config[DOMAIN])
@@ -40,8 +41,8 @@ class TomatoDeviceScanner(object):
"""
def __init__(self, config):
- host, http_id = config[ha.CONF_HOST], config[CONF_HTTP_ID]
- username, password = config[ha.CONF_USERNAME], config[ha.CONF_PASSWORD]
+ host, http_id = config[CONF_HOST], config[CONF_HTTP_ID]
+ username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
self.req = requests.Request('POST',
'http://{}/update.cgi'.format(host),
@@ -55,7 +56,6 @@ class TomatoDeviceScanner(object):
self.logger = logging.getLogger("{}.{}".format(__name__, "Tomato"))
self.lock = threading.Lock()
- self.date_updated = None
self.last_results = {"wldev": [], "dhcpd_lease": []}
self.success_init = self._update_tomato_info()
@@ -71,10 +71,6 @@ class TomatoDeviceScanner(object):
def get_device_name(self, device):
""" Returns the name of the given device or None if we don't know. """
- # Make sure there are results
- if not self.date_updated:
- self._update_tomato_info()
-
filter_named = [item[0] for item in self.last_results['dhcpd_lease']
if item[2] == device]
@@ -83,16 +79,12 @@ class TomatoDeviceScanner(object):
else:
return filter_named[0]
+ @Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_tomato_info(self):
""" Ensures the information from the Tomato router is up to date.
Returns boolean if scanning successful. """
- self.lock.acquire()
-
- # if date_updated is None or the date is too old we scan for new data
- if not self.date_updated or \
- datetime.now() - self.date_updated > MIN_TIME_BETWEEN_SCANS:
-
+ with self.lock:
self.logger.info("Scanning")
try:
@@ -111,8 +103,6 @@ class TomatoDeviceScanner(object):
self.last_results[param] = \
json.loads(value.replace("'", '"'))
- self.date_updated = datetime.now()
-
return True
elif response.status_code == 401:
@@ -146,13 +136,3 @@ class TomatoDeviceScanner(object):
"Failed to parse response from router")
return False
-
- finally:
- self.lock.release()
-
- else:
- # We acquired the lock before the IF check,
- # release it before we return True
- self.lock.release()
-
- return True
diff --git a/homeassistant/components/downloader.py b/homeassistant/components/downloader.py
index 362f7d43e04..6978dbd7fa9 100644
--- a/homeassistant/components/downloader.py
+++ b/homeassistant/components/downloader.py
@@ -9,7 +9,8 @@ import logging
import re
import threading
-import homeassistant.util as util
+from homeassistant.helpers import validate_config
+from homeassistant.util import sanitize_filename
DOMAIN = "downloader"
DEPENDENCIES = []
@@ -36,7 +37,7 @@ def setup(hass, config):
return False
- if not util.validate_config(config, {DOMAIN: [CONF_DOWNLOAD_DIR]}, logger):
+ if not validate_config(config, {DOMAIN: [CONF_DOWNLOAD_DIR]}, logger):
return False
download_path = config[DOMAIN][CONF_DOWNLOAD_DIR]
@@ -64,7 +65,7 @@ def setup(hass, config):
subdir = service.data.get(ATTR_SUBDIR)
if subdir:
- subdir = util.sanitize_filename(subdir)
+ subdir = sanitize_filename(subdir)
final_path = None
@@ -88,7 +89,7 @@ def setup(hass, config):
filename = "ha_download"
# Remove stuff to ruin paths
- filename = util.sanitize_filename(filename)
+ filename = sanitize_filename(filename)
# Do we want to download to subdir, create if needed
if subdir:
diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py
index 8c0ff0b763a..eac63ee845b 100644
--- a/homeassistant/components/group.py
+++ b/homeassistant/components/group.py
@@ -7,10 +7,10 @@ Provides functionality to group devices that can be turned on or off.
import logging
+import homeassistant as ha
import homeassistant.util as util
-from homeassistant.components import (STATE_ON, STATE_OFF,
- STATE_HOME, STATE_NOT_HOME,
- ATTR_ENTITY_ID)
+from homeassistant.const import (
+ ATTR_ENTITY_ID, STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME)
DOMAIN = "group"
DEPENDENCIES = []
@@ -19,19 +19,19 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}"
ATTR_AUTO = "auto"
-_GROUP_TYPES = {
- "on_off": (STATE_ON, STATE_OFF),
- "home_not_home": (STATE_HOME, STATE_NOT_HOME)
-}
+# List of ON/OFF state tuples for groupable states
+_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME)]
+
+_GROUPS = {}
-def _get_group_type(state):
- """ Determine the group type based on the given group type. """
- for group_type, states in _GROUP_TYPES.items():
+def _get_group_on_off(state):
+ """ Determine the group on/off states based on a state. """
+ for states in _GROUP_TYPES:
if state in states:
- return group_type
+ return states
- return None
+ return None, None
def is_on(hass, entity_id):
@@ -39,10 +39,10 @@ def is_on(hass, entity_id):
state = hass.states.get(entity_id)
if state:
- group_type = _get_group_type(state.state)
+ group_on, _ = _get_group_on_off(state.state)
# If we found a group_type, compare to ON-state
- return group_type and state.state == _GROUP_TYPES[group_type][0]
+ return group_on is not None and state.state == group_on
return False
@@ -101,93 +101,114 @@ def setup(hass, config):
return True
-# pylint: disable=too-many-branches
def setup_group(hass, name, entity_ids, user_defined=True):
""" Sets up a group state that is the combined state of
several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """
+ logger = logging.getLogger(__name__)
# In case an iterable is passed in
entity_ids = list(entity_ids)
+ if not entity_ids:
+ logger.error(
+ 'Error setting up group %s: no entities passed in to track', name)
+
+ return False
+
# Loop over the given entities to:
# - determine which group type this is (on_off, device_home)
- # - if all states exist and have valid states
- # - retrieve the current state of the group
- errors = []
- group_type, group_on, group_off, group_state = None, None, None, None
+ # - determine which states exist and have groupable states
+ # - determine the current state of the group
+ warnings = []
+ group_ids = []
+ group_on, group_off = None, None
+ group_state = False
for entity_id in entity_ids:
state = hass.states.get(entity_id)
# Try to determine group type if we didn't yet
- if not group_type and state:
- group_type = _get_group_type(state.state)
+ if group_on is None and state:
+ group_on, group_off = _get_group_on_off(state.state)
- if group_type:
- group_on, group_off = _GROUP_TYPES[group_type]
- group_state = group_off
-
- else:
+ if group_on is None:
# We did not find a matching group_type
- errors.append(
+ warnings.append(
"Entity {} has ungroupable state '{}'".format(
name, state.state))
- # Stop check all other entity IDs and report as error
- break
+ continue
# Check if entity exists
if not state:
- errors.append("Entity {} does not exist".format(entity_id))
+ warnings.append("Entity {} does not exist".format(entity_id))
- # Check if entity is valid state
+ # Check if entity is invalid state
elif state.state != group_off and state.state != group_on:
- errors.append("State of {} is {} (expected: {} or {})".format(
+ warnings.append("State of {} is {} (expected: {} or {})".format(
entity_id, state.state, group_off, group_on))
- # Keep track of the group state to init later on
- elif state.state == group_on:
- group_state = group_on
+ # We have a valid group state
+ else:
+ group_ids.append(entity_id)
- if group_type is None and not errors:
- errors.append('Unable to determine group type for {}'.format(name))
+ # Keep track of the group state to init later on
+ group_state = group_state or state.state == group_on
- if errors:
- logging.getLogger(__name__).error(
- "Error setting up group %s: %s", name, ", ".join(errors))
+ # If none of the entities could be found during setup
+ if not group_ids:
+ logger.error('Unable to find any entities to track for group %s', name)
return False
- else:
- group_entity_id = ENTITY_ID_FORMAT.format(name)
- state_attr = {ATTR_ENTITY_ID: entity_ids, ATTR_AUTO: not user_defined}
+ elif warnings:
+ logger.warning(
+ 'Warnings during setting up group %s: %s',
+ name, ", ".join(warnings))
- # pylint: disable=unused-argument
- def update_group_state(entity_id, old_state, new_state):
- """ Updates the group state based on a state change by
- a tracked entity. """
+ group_entity_id = ENTITY_ID_FORMAT.format(util.slugify(name))
+ state = group_on if group_state else group_off
+ state_attr = {ATTR_ENTITY_ID: group_ids, ATTR_AUTO: not user_defined}
- cur_gr_state = hass.states.get(group_entity_id).state
+ # pylint: disable=unused-argument
+ def update_group_state(entity_id, old_state, new_state):
+ """ Updates the group state based on a state change by
+ a tracked entity. """
- # if cur_gr_state = OFF and new_state = ON: set ON
- # if cur_gr_state = ON and new_state = OFF: research
- # else: ignore
+ cur_gr_state = hass.states.get(group_entity_id).state
- if cur_gr_state == group_off and new_state.state == group_on:
+ # if cur_gr_state = OFF and new_state = ON: set ON
+ # if cur_gr_state = ON and new_state = OFF: research
+ # else: ignore
- hass.states.set(group_entity_id, group_on, state_attr)
+ if cur_gr_state == group_off and new_state.state == group_on:
- elif cur_gr_state == group_on and new_state.state == group_off:
+ hass.states.set(group_entity_id, group_on, state_attr)
- # Check if any of the other states is still on
- if not any([hass.states.is_state(ent_id, group_on)
- for ent_id in entity_ids
- if entity_id != ent_id]):
- hass.states.set(group_entity_id, group_off, state_attr)
+ elif cur_gr_state == group_on and new_state.state == group_off:
- hass.track_state_change(entity_ids, update_group_state)
+ # Check if any of the other states is still on
+ if not any([hass.states.is_state(ent_id, group_on)
+ for ent_id in group_ids
+ if entity_id != ent_id]):
+ hass.states.set(group_entity_id, group_off, state_attr)
- hass.states.set(group_entity_id, group_state, state_attr)
+ _GROUPS[group_entity_id] = hass.states.track_change(
+ group_ids, update_group_state)
- return True
+ hass.states.set(group_entity_id, state, state_attr)
+
+ return True
+
+
+def remove_group(hass, name):
+ """ Remove a group and its state listener from Home Assistant. """
+ group_entity_id = ENTITY_ID_FORMAT.format(util.slugify(name))
+
+ if hass.states.get(group_entity_id) is not None:
+ hass.states.remove(group_entity_id)
+
+ if group_entity_id in _GROUPS:
+ hass.bus.remove_listener(
+ ha.EVENT_STATE_CHANGED, _GROUPS.pop(group_entity_id))
diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py
index 8c6ddc1e5e0..a50ed7c9845 100644
--- a/homeassistant/components/http/__init__.py
+++ b/homeassistant/components/http/__init__.py
@@ -83,6 +83,10 @@ from socketserver import ThreadingMixIn
from urllib.parse import urlparse, parse_qs
import homeassistant as ha
+from homeassistant.const import (
+ SERVER_PORT, URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES,
+ URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, AUTH_HEADER)
+from homeassistant.helpers import validate_config, TrackStates
import homeassistant.remote as rem
import homeassistant.util as util
from . import frontend
@@ -108,22 +112,23 @@ CONF_SERVER_HOST = "server_host"
CONF_SERVER_PORT = "server_port"
CONF_DEVELOPMENT = "development"
+DATA_API_PASSWORD = 'api_password'
+
_LOGGER = logging.getLogger(__name__)
def setup(hass, config):
""" Sets up the HTTP API and debug interface. """
- if not util.validate_config(config, {DOMAIN: [CONF_API_PASSWORD]},
- _LOGGER):
+ if not validate_config(config, {DOMAIN: [CONF_API_PASSWORD]}, _LOGGER):
return False
- api_password = config[DOMAIN]['api_password']
+ api_password = config[DOMAIN][CONF_API_PASSWORD]
# If no server host is given, accept all incoming requests
server_host = config[DOMAIN].get(CONF_SERVER_HOST, '0.0.0.0')
- server_port = config[DOMAIN].get(CONF_SERVER_PORT, rem.SERVER_PORT)
+ server_port = config[DOMAIN].get(CONF_SERVER_PORT, SERVER_PORT)
development = config[DOMAIN].get(CONF_DEVELOPMENT, "") == "1"
@@ -131,15 +136,11 @@ def setup(hass, config):
RequestHandler, hass, api_password,
development)
- hass.listen_once_event(
+ hass.bus.listen_once(
ha.EVENT_HOMEASSISTANT_START,
lambda event:
threading.Thread(target=server.start, daemon=True).start())
- hass.listen_once_event(
- ha.EVENT_HOMEASSISTANT_STOP,
- lambda event: server.shutdown())
-
# If no local api set, set one with known information
if isinstance(hass, rem.HomeAssistant) and hass.local_api is None:
hass.local_api = \
@@ -156,9 +157,9 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
daemon_threads = True
# pylint: disable=too-many-arguments
- def __init__(self, server_address, RequestHandlerClass,
+ def __init__(self, server_address, request_handler_class,
hass, api_password, development=False):
- super().__init__(server_address, RequestHandlerClass)
+ super().__init__(server_address, request_handler_class)
self.server_address = server_address
self.hass = hass
@@ -173,6 +174,10 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
def start(self):
""" Starts the server. """
+ self.hass.bus.listen_once(
+ ha.EVENT_HOMEASSISTANT_STOP,
+ lambda event: self.shutdown())
+
_LOGGER.info(
"Starting web interface at http://%s:%d", *self.server_address)
@@ -192,13 +197,12 @@ class RequestHandler(SimpleHTTPRequestHandler):
PATHS = [ # debug interface
('GET', URL_ROOT, '_handle_get_root'),
- ('POST', URL_ROOT, '_handle_get_root'),
# /api - for validation purposes
- ('GET', rem.URL_API, '_handle_get_api'),
+ ('GET', URL_API, '_handle_get_api'),
# /states
- ('GET', rem.URL_API_STATES, '_handle_get_api_states'),
+ ('GET', URL_API_STATES, '_handle_get_api_states'),
('GET',
re.compile(r'/api/states/(?P[a-zA-Z\._0-9]+)'),
'_handle_get_api_states_entity'),
@@ -210,13 +214,13 @@ class RequestHandler(SimpleHTTPRequestHandler):
'_handle_post_state_entity'),
# /events
- ('GET', rem.URL_API_EVENTS, '_handle_get_api_events'),
+ ('GET', URL_API_EVENTS, '_handle_get_api_events'),
('POST',
re.compile(r'/api/events/(?P[a-zA-Z\._0-9]+)'),
'_handle_api_post_events_event'),
# /services
- ('GET', rem.URL_API_SERVICES, '_handle_get_api_services'),
+ ('GET', URL_API_SERVICES, '_handle_get_api_services'),
('POST',
re.compile((r'/api/services/'
r'(?P[a-zA-Z\._0-9]+)/'
@@ -224,12 +228,14 @@ class RequestHandler(SimpleHTTPRequestHandler):
'_handle_post_api_services_domain_service'),
# /event_forwarding
- ('POST', rem.URL_API_EVENT_FORWARD, '_handle_post_api_event_forward'),
- ('DELETE', rem.URL_API_EVENT_FORWARD,
+ ('POST', URL_API_EVENT_FORWARD, '_handle_post_api_event_forward'),
+ ('DELETE', URL_API_EVENT_FORWARD,
'_handle_delete_api_event_forward'),
- # Statis files
+ # Static files
('GET', re.compile(r'/static/(?P[a-zA-Z\._\-0-9/]+)'),
+ '_handle_get_static'),
+ ('HEAD', re.compile(r'/static/(?P[a-zA-Z\._\-0-9/]+)'),
'_handle_get_static')
]
@@ -255,24 +261,22 @@ class RequestHandler(SimpleHTTPRequestHandler):
if content_length:
body_content = self.rfile.read(content_length).decode("UTF-8")
- if self.use_json:
- try:
- data.update(json.loads(body_content))
- except ValueError:
- _LOGGER.exception("Exception parsing JSON: %s",
- body_content)
+ try:
+ data.update(json.loads(body_content))
+ except (TypeError, ValueError):
+ # TypeError is JSON object is not a dict
+ # ValueError if we could not parse JSON
+ _LOGGER.exception("Exception parsing JSON: %s",
+ body_content)
- self._message(
- "Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY)
- return
- else:
- data.update({key: value[-1] for key, value in
- parse_qs(body_content).items()})
+ self._json_message(
+ "Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY)
+ return
- api_password = self.headers.get(rem.AUTH_HEADER)
+ api_password = self.headers.get(AUTH_HEADER)
- if not api_password and 'api_password' in data:
- api_password = data['api_password']
+ if not api_password and DATA_API_PASSWORD in data:
+ api_password = data[DATA_API_PASSWORD]
if '_METHOD' in data:
method = data.pop('_METHOD')
@@ -307,7 +311,7 @@ class RequestHandler(SimpleHTTPRequestHandler):
# For API calls we need a valid password
if self.use_json and api_password != self.server.api_password:
- self._message(
+ self._json_message(
"API password missing or incorrect.", HTTP_UNAUTHORIZED)
else:
@@ -315,9 +319,11 @@ class RequestHandler(SimpleHTTPRequestHandler):
elif path_matched_but_not_method:
self.send_response(HTTP_METHOD_NOT_ALLOWED)
+ self.end_headers()
else:
self.send_response(HTTP_NOT_FOUND)
+ self.end_headers()
def do_HEAD(self): # pylint: disable=invalid-name
""" HEAD request handler. """
@@ -377,7 +383,7 @@ class RequestHandler(SimpleHTTPRequestHandler):
# pylint: disable=unused-argument
def _handle_get_api(self, path_match, data):
""" Renders the debug interface. """
- self._message("API running.")
+ self._json_message("API running.")
# pylint: disable=unused-argument
def _handle_get_api_states(self, path_match, data):
@@ -394,7 +400,7 @@ class RequestHandler(SimpleHTTPRequestHandler):
if state:
self._write_json(state)
else:
- self._message("State does not exist.", HTTP_NOT_FOUND)
+ self._json_message("State does not exist.", HTTP_NOT_FOUND)
def _handle_post_state_entity(self, path_match, data):
""" Handles updating the state of an entity.
@@ -407,7 +413,7 @@ class RequestHandler(SimpleHTTPRequestHandler):
try:
new_state = data['state']
except KeyError:
- self._message("state not specified", HTTP_BAD_REQUEST)
+ self._json_message("state not specified", HTTP_BAD_REQUEST)
return
attributes = data['attributes'] if 'attributes' in data else None
@@ -417,19 +423,14 @@ class RequestHandler(SimpleHTTPRequestHandler):
# Write state
self.server.hass.states.set(entity_id, new_state, attributes)
- # Return state if json, else redirect to main page
- if self.use_json:
- state = self.server.hass.states.get(entity_id)
+ state = self.server.hass.states.get(entity_id)
- status_code = HTTP_CREATED if is_new_state else HTTP_OK
+ status_code = HTTP_CREATED if is_new_state else HTTP_OK
- self._write_json(
- state.as_dict(),
- status_code=status_code,
- location=rem.URL_API_STATES_ENTITY.format(entity_id))
- else:
- self._message(
- "State of {} changed to {}".format(entity_id, new_state))
+ self._write_json(
+ state.as_dict(),
+ status_code=status_code,
+ location=URL_API_STATES_ENTITY.format(entity_id))
def _handle_get_api_events(self, path_match, data):
""" Handles getting overview of event listeners. """
@@ -448,8 +449,8 @@ class RequestHandler(SimpleHTTPRequestHandler):
event_type = path_match.group('event_type')
if event_data is not None and not isinstance(event_data, dict):
- self._message("event_data should be an object",
- HTTP_UNPROCESSABLE_ENTITY)
+ self._json_message("event_data should be an object",
+ HTTP_UNPROCESSABLE_ENTITY)
event_origin = ha.EventOrigin.remote
@@ -464,7 +465,7 @@ class RequestHandler(SimpleHTTPRequestHandler):
self.server.hass.bus.fire(event_type, event_data, event_origin)
- self._message("Event {} fired.".format(event_type))
+ self._json_message("Event {} fired.".format(event_type))
def _handle_get_api_services(self, path_match, data):
""" Handles getting overview of services. """
@@ -483,9 +484,10 @@ class RequestHandler(SimpleHTTPRequestHandler):
domain = path_match.group('domain')
service = path_match.group('service')
- self.server.hass.call_service(domain, service, data)
+ with TrackStates(self.server.hass) as changed_states:
+ self.server.hass.services.call(domain, service, data, True)
- self._message("Service {}/{} called.".format(domain, service))
+ self._write_json(changed_states)
# pylint: disable=invalid-name
def _handle_post_api_event_forward(self, path_match, data):
@@ -495,26 +497,31 @@ class RequestHandler(SimpleHTTPRequestHandler):
host = data['host']
api_password = data['api_password']
except KeyError:
- self._message("No host or api_password received.",
- HTTP_BAD_REQUEST)
+ self._json_message("No host or api_password received.",
+ HTTP_BAD_REQUEST)
return
try:
port = int(data['port']) if 'port' in data else None
except ValueError:
- self._message(
+ self._json_message(
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
return
+ api = rem.API(host, api_password, port)
+
+ if not api.validate_api():
+ self._json_message(
+ "Unable to validate API", HTTP_UNPROCESSABLE_ENTITY)
+ return
+
if self.server.event_forwarder is None:
self.server.event_forwarder = \
rem.EventForwarder(self.server.hass)
- api = rem.API(host, api_password, port)
-
self.server.event_forwarder.connect(api)
- self._message("Event forwarding setup.")
+ self._json_message("Event forwarding setup.")
def _handle_delete_api_event_forward(self, path_match, data):
""" Handles deleting an event forwarding target. """
@@ -522,14 +529,14 @@ class RequestHandler(SimpleHTTPRequestHandler):
try:
host = data['host']
except KeyError:
- self._message("No host received.",
- HTTP_BAD_REQUEST)
+ self._json_message("No host received.",
+ HTTP_BAD_REQUEST)
return
try:
port = int(data['port']) if 'port' in data else None
except ValueError:
- self._message(
+ self._json_message(
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
return
@@ -538,7 +545,7 @@ class RequestHandler(SimpleHTTPRequestHandler):
self.server.event_forwarder.disconnect(api)
- self._message("Event forwarding cancelled.")
+ self._json_message("Event forwarding cancelled.")
def _handle_get_static(self, path_match, data):
""" Returns a static file. """
@@ -585,7 +592,10 @@ class RequestHandler(SimpleHTTPRequestHandler):
self.end_headers()
- if do_gzip:
+ if self.command == 'HEAD':
+ return
+
+ elif do_gzip:
self.wfile.write(gzip_data)
else:
@@ -599,22 +609,9 @@ class RequestHandler(SimpleHTTPRequestHandler):
if inp:
inp.close()
- def _message(self, message, status_code=HTTP_OK):
+ def _json_message(self, message, status_code=HTTP_OK):
""" Helper method to return a message to the caller. """
- if self.use_json:
- self._write_json({'message': message}, status_code=status_code)
- else:
- self.send_error(status_code, message)
-
- def _redirect(self, location):
- """ Helper method to redirect caller. """
- self.send_response(HTTP_MOVED_PERMANENTLY)
-
- self.send_header(
- "Location", "{}?api_password={}".format(
- location, self.server.api_password))
-
- self.end_headers()
+ self._write_json({'message': message}, status_code=status_code)
def _write_json(self, data=None, status_code=HTTP_OK, location=None):
""" Helper method to return JSON to the caller. """
diff --git a/homeassistant/components/http/frontend.py b/homeassistant/components/http/frontend.py
index 0392f4b5c78..2ec7cc69630 100644
--- a/homeassistant/components/http/frontend.py
+++ b/homeassistant/components/http/frontend.py
@@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """
-VERSION = "12ba7bca8ad0c196cb04ada4fe85a76b"
+VERSION = "78343829ea70bf07a9e939b321587122"
diff --git a/homeassistant/components/http/www_static/frontend.html b/homeassistant/components/http/www_static/frontend.html
index 322cba2605f..35d51134f0c 100644
--- a/homeassistant/components/http/www_static/frontend.html
+++ b/homeassistant/components/http/www_static/frontend.html
@@ -2,9 +2,7 @@
- {{label}}
-
+}},dispatchMethod:function(a,c,d){if(a){b.events&&console.group("[%s] dispatch [%s]",a.localName,c);var e="function"==typeof c?c:a[c];e&&e[d?"apply":"call"](a,d),b.events&&console.groupEnd(),Polymer.flush()}}};a.api.instance.events=d,a.addEventListener=function(a,b,c,d){PolymerGestures.addEventListener(wrap(a),b,c,d)},a.removeEventListener=function(a,b,c,d){PolymerGestures.removeEventListener(wrap(a),b,c,d)}}(Polymer),function(a){var b={copyInstanceAttributes:function(){var a=this._instanceAttributes;for(var b in a)this.hasAttribute(b)||this.setAttribute(b,a[b])},takeAttributes:function(){if(this._publishLC)for(var a,b=0,c=this.attributes,d=c.length;(a=c[b])&&d>b;b++)this.attributeToProperty(a.name,a.value)},attributeToProperty:function(b,c){var b=this.propertyForAttribute(b);if(b){if(c&&c.search(a.bindPattern)>=0)return;var d=this[b],c=this.deserializeValue(c,d);c!==d&&(this[b]=c)}},propertyForAttribute:function(a){var b=this._publishLC&&this._publishLC[a];return b},deserializeValue:function(b,c){return a.deserializeValue(b,c)},serializeValue:function(a,b){return"boolean"===b?a?"":void 0:"object"!==b&&"function"!==b&&void 0!==a?a:void 0},reflectPropertyToAttribute:function(a){var b=typeof this[a],c=this.serializeValue(this[a],b);void 0!==c?this.setAttribute(a,c):"boolean"===b&&this.removeAttribute(a)}};a.api.instance.attributes=b}(Polymer),function(a){function b(a,b){return a===b?0!==a||1/a===1/b:f(a)&&f(b)?!0:a!==a&&b!==b}function c(a,b){return void 0===b&&null===a?b:null===b||void 0===b?a:b}var d=window.WebComponents?WebComponents.flags.log:{},e={object:void 0,type:"update",name:void 0,oldValue:void 0},f=Number.isNaN||function(a){return"number"==typeof a&&isNaN(a)},g={createPropertyObserver:function(){var a=this._observeNames;if(a&&a.length){var b=this._propertyObserver=new CompoundObserver(!0);this.registerObserver(b);for(var c,d=0,e=a.length;e>d&&(c=a[d]);d++)b.addPath(this,c),this.observeArrayValue(c,this[c],null)}},openPropertyObserver:function(){this._propertyObserver&&this._propertyObserver.open(this.notifyPropertyChanges,this)},notifyPropertyChanges:function(a,b,c){var d,e,f={};for(var g in b)if(d=c[2*g+1],e=this.observe[d]){var h=b[g],i=a[g];this.observeArrayValue(d,i,h),f[e]||(void 0!==h&&null!==h||void 0!==i&&null!==i)&&(f[e]=!0,this.invokeMethod(e,[h,i,arguments]))}},invokeMethod:function(a,b){var c=this[a]||a;"function"==typeof c&&c.apply(this,b)},deliverChanges:function(){this._propertyObserver&&this._propertyObserver.deliver()},observeArrayValue:function(a,b,c){var e=this.observe[a];if(e&&(Array.isArray(c)&&(d.observe&&console.log("[%s] observeArrayValue: unregister observer [%s]",this.localName,a),this.closeNamedObserver(a+"__array")),Array.isArray(b))){d.observe&&console.log("[%s] observeArrayValue: register observer [%s]",this.localName,a,b);var f=new ArrayObserver(b);f.open(function(a){this.invokeMethod(e,[a])},this),this.registerNamedObserver(a+"__array",f)}},emitPropertyChangeRecord:function(a,c,d){if(!b(c,d)&&(this._propertyChanged(a,c,d),Observer.hasObjectObserve)){var f=this._objectNotifier;f||(f=this._objectNotifier=Object.getNotifier(this)),e.object=this,e.name=a,e.oldValue=d,f.notify(e)}},_propertyChanged:function(a){this.reflect[a]&&this.reflectPropertyToAttribute(a)},bindProperty:function(a,b,d){if(d)return void(this[a]=b);var e=this.element.prototype.computed;if(e&&e[a]){var f=a+"ComputedBoundObservable_";return void(this[f]=b)}return this.bindToAccessor(a,b,c)},bindToAccessor:function(a,c,d){function e(b,c){j[f]=b;var d=j[h];d&&"function"==typeof d.setValue&&d.setValue(b),j.emitPropertyChangeRecord(a,b,c)}var f=a+"_",g=a+"Observable_",h=a+"ComputedBoundObservable_";this[g]=c;var i=this[f],j=this,k=c.open(e);if(d&&!b(i,k)){var l=d(i,k);b(k,l)||(k=l,c.setValue&&c.setValue(k))}e(k,i);var m={close:function(){c.close(),j[g]=void 0,j[h]=void 0}};return this.registerObserver(m),m},createComputedProperties:function(){if(this._computedNames)for(var a=0;ae&&(c=d[e]);e++)b[c.id]=c},onMutation:function(a,b){var c=new MutationObserver(function(a){b.call(this,c,a),c.disconnect()}.bind(this));c.observe(a,{childList:!0,subtree:!0})}};c.prototype=d,d.constructor=c,a.Base=c,a.isBase=b,a.api.instance.base=d}(Polymer),function(a){function b(a){return a.__proto__}function c(a,b){var c="",d=!1;b&&(c=b.localName,d=b.hasAttribute("is"));var e=WebComponents.ShadowCSS.makeScopeSelector(c,d);return WebComponents.ShadowCSS.shimCssText(a,e)}var d=(window.WebComponents?WebComponents.flags.log:{},window.ShadowDOMPolyfill),e="element",f="controller",g={STYLE_SCOPE_ATTRIBUTE:e,installControllerStyles:function(){var a=this.findStyleScope();if(a&&!this.scopeHasNamedStyle(a,this.localName)){for(var c=b(this),d="";c&&c.element;)d+=c.element.cssTextForScope(f),c=b(c);d&&this.installScopeCssText(d,a)}},installScopeStyle:function(a,b,c){var c=c||this.findStyleScope(),b=b||"";if(c&&!this.scopeHasNamedStyle(c,this.localName+b)){var d="";if(a instanceof Array)for(var e,f=0,g=a.length;g>f&&(e=a[f]);f++)d+=e.textContent+"\n\n";else d=a.textContent;this.installScopeCssText(d,c,b)}},installScopeCssText:function(a,b,e){if(b=b||this.findStyleScope(),e=e||"",b){d&&(a=c(a,b.host));var g=this.element.cssTextToScopeStyle(a,f);Polymer.applyStyleToScope(g,b),this.styleCacheForScope(b)[this.localName+e]=!0}},findStyleScope:function(a){for(var b=a||this;b.parentNode;)b=b.parentNode;return b},scopeHasNamedStyle:function(a,b){var c=this.styleCacheForScope(a);return c[b]},styleCacheForScope:function(a){if(d){var b=a.host?a.host.localName:a.localName;return h[b]||(h[b]={})}return a._scopeStyles=a._scopeStyles||{}}},h={};a.api.instance.styles=g}(Polymer),function(a){function b(a,b){if("string"!=typeof a){var c=b||document._currentScript;if(b=a,a=c&&c.parentNode&&c.parentNode.getAttribute?c.parentNode.getAttribute("name"):"",!a)throw"Element name could not be inferred."}if(f(a))throw"Already registered (Polymer) prototype for element "+a;e(a,b),d(a)}function c(a,b){i[a]=b}function d(a){i[a]&&(i[a].registerWhenReady(),delete i[a])}function e(a,b){return j[a]=b||{}}function f(a){return j[a]}function g(a,b){if("string"!=typeof b)return!1;var c=HTMLElement.getPrototypeForTag(b),d=c&&c.constructor;return d?CustomElements.instanceof?CustomElements.instanceof(a,d):a instanceof d:!1}var h=a.extend,i=(a.api,{}),j={};a.getRegisteredPrototype=f,a.waitingForPrototype=c,a.instanceOfType=g,window.Polymer=b,h(Polymer,a),WebComponents.consumeDeclarations&&WebComponents.consumeDeclarations(function(a){if(a)for(var c,d=0,e=a.length;e>d&&(c=a[d]);d++)b.apply(null,c)})}(Polymer),function(a){var b={resolveElementPaths:function(a){Polymer.urlResolver.resolveDom(a)},addResolvePathApi:function(){var a=this.getAttribute("assetpath")||"",b=new URL(a,this.ownerDocument.baseURI);this.prototype.resolvePath=function(a,c){var d=new URL(a,c||b);return d.href}}};a.api.declaration.path=b}(Polymer),function(a){function b(a,b){var c=new URL(a.getAttribute("href"),b).href;return"@import '"+c+"';"}function c(a,b){if(a){b===document&&(b=document.head),i&&(b=document.head);var c=d(a.textContent),e=a.getAttribute(h);e&&c.setAttribute(h,e);var f=b.firstElementChild;if(b===document.head){var g="style["+h+"]",j=document.head.querySelectorAll(g);j.length&&(f=j[j.length-1].nextElementSibling)}b.insertBefore(c,f)}}function d(a,b){b=b||document,b=b.createElement?b:b.ownerDocument;var c=b.createElement("style");return c.textContent=a,c}function e(a){return a&&a.__resource||""}function f(a,b){return q?q.call(a,b):void 0}var g=(window.WebComponents?WebComponents.flags.log:{},a.api.instance.styles),h=g.STYLE_SCOPE_ATTRIBUTE,i=window.ShadowDOMPolyfill,j="style",k="@import",l="link[rel=stylesheet]",m="global",n="polymer-scope",o={loadStyles:function(a){var b=this.fetchTemplate(),c=b&&this.templateContent();if(c){this.convertSheetsToStyles(c);var d=this.findLoadableStyles(c);if(d.length){var e=b.ownerDocument.baseURI;return Polymer.styleResolver.loadStyles(d,e,a)}}a&&a()},convertSheetsToStyles:function(a){for(var c,e,f=a.querySelectorAll(l),g=0,h=f.length;h>g&&(c=f[g]);g++)e=d(b(c,this.ownerDocument.baseURI),this.ownerDocument),this.copySheetAttributes(e,c),c.parentNode.replaceChild(e,c)},copySheetAttributes:function(a,b){for(var c,d=0,e=b.attributes,f=e.length;(c=e[d])&&f>d;d++)"rel"!==c.name&&"href"!==c.name&&a.setAttribute(c.name,c.value)},findLoadableStyles:function(a){var b=[];if(a)for(var c,d=a.querySelectorAll(j),e=0,f=d.length;f>e&&(c=d[e]);e++)c.textContent.match(k)&&b.push(c);return b},installSheets:function(){this.cacheSheets(),this.cacheStyles(),this.installLocalSheets(),this.installGlobalStyles()},cacheSheets:function(){this.sheets=this.findNodes(l),this.sheets.forEach(function(a){a.parentNode&&a.parentNode.removeChild(a)})},cacheStyles:function(){this.styles=this.findNodes(j+"["+n+"]"),this.styles.forEach(function(a){a.parentNode&&a.parentNode.removeChild(a)})},installLocalSheets:function(){var a=this.sheets.filter(function(a){return!a.hasAttribute(n)}),b=this.templateContent();if(b){var c="";if(a.forEach(function(a){c+=e(a)+"\n"}),c){var f=d(c,this.ownerDocument);b.insertBefore(f,b.firstChild)}}},findNodes:function(a,b){var c=this.querySelectorAll(a).array(),d=this.templateContent();if(d){var e=d.querySelectorAll(a).array();c=c.concat(e)}return b?c.filter(b):c},installGlobalStyles:function(){var a=this.styleForScope(m);c(a,document.head)},cssTextForScope:function(a){var b="",c="["+n+"="+a+"]",d=function(a){return f(a,c)},g=this.sheets.filter(d);g.forEach(function(a){b+=e(a)+"\n\n"});var h=this.styles.filter(d);return h.forEach(function(a){b+=a.textContent+"\n\n"}),b},styleForScope:function(a){var b=this.cssTextForScope(a);return this.cssTextToScopeStyle(b,a)},cssTextToScopeStyle:function(a,b){if(a){var c=d(a);return c.setAttribute(h,this.getAttribute("name")+"-"+b),c}}},p=HTMLElement.prototype,q=p.matches||p.matchesSelector||p.webkitMatchesSelector||p.mozMatchesSelector;a.api.declaration.styles=o,a.applyStyleToScope=c}(Polymer),function(a){var b=(window.WebComponents?WebComponents.flags.log:{},a.api.instance.events),c=b.EVENT_PREFIX,d={};["webkitAnimationStart","webkitAnimationEnd","webkitTransitionEnd","DOMFocusOut","DOMFocusIn","DOMMouseScroll"].forEach(function(a){d[a.toLowerCase()]=a});var e={parseHostEvents:function(){var a=this.prototype.eventDelegates;this.addAttributeDelegates(a)},addAttributeDelegates:function(a){for(var b,c=0;b=this.attributes[c];c++)this.hasEventPrefix(b.name)&&(a[this.removeEventPrefix(b.name)]=b.value.replace("{{","").replace("}}","").trim())},hasEventPrefix:function(a){return a&&"o"===a[0]&&"n"===a[1]&&"-"===a[2]},removeEventPrefix:function(a){return a.slice(f)},findController:function(a){for(;a.parentNode;){if(a.eventController)return a.eventController;a=a.parentNode}return a.host},getEventHandler:function(a,b,c){var d=this;return function(e){a&&a.PolymerBase||(a=d.findController(b));var f=[e,e.detail,e.currentTarget];a.dispatchMethod(a,c,f)}},prepareEventBinding:function(a,b){if(this.hasEventPrefix(b)){var c=this.removeEventPrefix(b);c=d[c]||c;var e=this;return function(b,d,f){function g(){return"{{ "+a+" }}"}var h=e.getEventHandler(void 0,d,a);return PolymerGestures.addEventListener(d,c,h),f?void 0:{open:g,discardChanges:g,close:function(){PolymerGestures.removeEventListener(d,c,h)}}}}}},f=c.length;a.api.declaration.events=e}(Polymer),function(a){var b=["attribute"],c={inferObservers:function(a){var b,c=a.observe;for(var d in a)"Changed"===d.slice(-7)&&(b=d.slice(0,-7),this.canObserveProperty(b)&&(c||(c=a.observe={}),c[b]=c[b]||d))},canObserveProperty:function(a){return b.indexOf(a)<0},explodeObservers:function(a){var b=a.observe;if(b){var c={};for(var d in b)for(var e,f=d.split(" "),g=0;e=f[g];g++)c[e]=b[d];a.observe=c}},optimizePropertyMaps:function(a){if(a.observe){var b=a._observeNames=[];for(var c in a.observe)for(var d,e=c.split(" "),f=0;d=e[f];f++)b.push(d)}if(a.publish){var b=a._publishNames=[];for(var c in a.publish)b.push(c)}if(a.computed){var b=a._computedNames=[];for(var c in a.computed)b.push(c)}},publishProperties:function(a,b){var c=a.publish;c&&(this.requireProperties(c,a,b),this.filterInvalidAccessorNames(c),a._publishLC=this.lowerCaseMap(c));var d=a.computed;d&&this.filterInvalidAccessorNames(d)},filterInvalidAccessorNames:function(a){for(var b in a)this.propertyNameBlacklist[b]&&(console.warn('Cannot define property "'+b+'" for element "'+this.name+'" because it has the same name as an HTMLElement property, and not all browsers support overriding that. Consider giving it a different name.'),delete a[b])},requireProperties:function(a,b){b.reflect=b.reflect||{};for(var c in a){var d=a[c];d&&void 0!==d.reflect&&(b.reflect[c]=Boolean(d.reflect),d=d.value),void 0!==d&&(b[c]=d)}},lowerCaseMap:function(a){var b={};for(var c in a)b[c.toLowerCase()]=c;return b},createPropertyAccessor:function(a,b){var c=this.prototype,d=a+"_",e=a+"Observable_";c[d]=c[a],Object.defineProperty(c,a,{get:function(){var a=this[e];return a&&a.deliver(),this[d]},set:function(c){if(b)return this[d];var f=this[e];if(f)return void f.setValue(c);var g=this[d];return this[d]=c,this.emitPropertyChangeRecord(a,c,g),c},configurable:!0})},createPropertyAccessors:function(a){var b=a._computedNames;if(b&&b.length)for(var c,d=0,e=b.length;e>d&&(c=b[d]);d++)this.createPropertyAccessor(c,!0);var b=a._publishNames;if(b&&b.length)for(var c,d=0,e=b.length;e>d&&(c=b[d]);d++)a.computed&&a.computed[c]||this.createPropertyAccessor(c)},propertyNameBlacklist:{children:1,"class":1,id:1,hidden:1,style:1,title:1}};a.api.declaration.properties=c}(Polymer),function(a){var b="attributes",c=/\s|,/,d={inheritAttributesObjects:function(a){this.inheritObject(a,"publishLC"),this.inheritObject(a,"_instanceAttributes")},publishAttributes:function(a){var d=this.getAttribute(b);if(d)for(var e,f=a.publish||(a.publish={}),g=d.split(c),h=0,i=g.length;i>h;h++)e=g[h].trim(),e&&void 0===f[e]&&(f[e]=void 0)},accumulateInstanceAttributes:function(){for(var a,b=this.prototype._instanceAttributes,c=this.attributes,d=0,e=c.length;e>d&&(a=c[d]);d++)this.isInstanceAttribute(a.name)&&(b[a.name]=a.value)},isInstanceAttribute:function(a){return!this.blackList[a]&&"on-"!==a.slice(0,3)},blackList:{name:1,"extends":1,constructor:1,noscript:1,assetpath:1,"cache-csstext":1}};d.blackList[b]=1,a.api.declaration.attributes=d}(Polymer),function(a){var b=a.api.declaration.events,c=new PolymerExpressions,d=c.prepareBinding;c.prepareBinding=function(a,e,f){return b.prepareEventBinding(a,e,f)||d.call(c,a,e,f)};var e={syntax:c,fetchTemplate:function(){return this.querySelector("template")},templateContent:function(){var a=this.fetchTemplate();return a&&a.content},installBindingDelegate:function(a){a&&(a.bindingDelegate=this.syntax)}};a.api.declaration.mdv=e}(Polymer),function(a){function b(a){if(!Object.__proto__){var b=Object.getPrototypeOf(a);a.__proto__=b,d(b)&&(b.__proto__=Object.getPrototypeOf(b))}}var c=a.api,d=a.isBase,e=a.extend,f=window.ShadowDOMPolyfill,g={register:function(a,b){this.buildPrototype(a,b),this.registerPrototype(a,b),this.publishConstructor()},buildPrototype:function(b,c){var d=a.getRegisteredPrototype(b),e=this.generateBasePrototype(c);this.desugarBeforeChaining(d,e),this.prototype=this.chainPrototypes(d,e),this.desugarAfterChaining(b,c)},desugarBeforeChaining:function(a,b){a.element=this,this.publishAttributes(a,b),this.publishProperties(a,b),this.inferObservers(a),this.explodeObservers(a)},chainPrototypes:function(a,c){this.inheritMetaData(a,c);var d=this.chainObject(a,c);return b(d),d},inheritMetaData:function(a,b){this.inheritObject("observe",a,b),this.inheritObject("publish",a,b),this.inheritObject("reflect",a,b),this.inheritObject("_publishLC",a,b),this.inheritObject("_instanceAttributes",a,b),this.inheritObject("eventDelegates",a,b)},desugarAfterChaining:function(a,b){this.optimizePropertyMaps(this.prototype),this.createPropertyAccessors(this.prototype),this.installBindingDelegate(this.fetchTemplate()),this.installSheets(),this.resolveElementPaths(this),this.accumulateInstanceAttributes(),this.parseHostEvents(),this.addResolvePathApi(),f&&WebComponents.ShadowCSS.shimStyling(this.templateContent(),a,b),this.prototype.registerCallback&&this.prototype.registerCallback(this)},publishConstructor:function(){var a=this.getAttribute("constructor");a&&(window[a]=this.ctor)},generateBasePrototype:function(a){var b=this.findBasePrototype(a);if(!b){var b=HTMLElement.getPrototypeForTag(a);b=this.ensureBaseApi(b),h[a]=b}return b},findBasePrototype:function(a){return h[a]},ensureBaseApi:function(a){if(a.PolymerBase)return a;var b=Object.create(a);return c.publish(c.instance,b),this.mixinMethod(b,a,c.instance.mdv,"bind"),b},mixinMethod:function(a,b,c,d){var e=function(a){return b[d].apply(this,a)};a[d]=function(){return this.mixinSuper=e,c[d].apply(this,arguments)}},inheritObject:function(a,b,c){var d=b[a]||{};b[a]=this.chainObject(d,c[a])},registerPrototype:function(a,b){var c={prototype:this.prototype},d=this.findTypeExtension(b);d&&(c.extends=d),HTMLElement.register(a,this.prototype),this.ctor=document.registerElement(a,c)},findTypeExtension:function(a){if(a&&a.indexOf("-")<0)return a;var b=this.findBasePrototype(a);return b.element?this.findTypeExtension(b.element.extends):void 0}},h={};g.chainObject=Object.__proto__?function(a,b){return a&&b&&a!==b&&(a.__proto__=b),a}:function(a,b){if(a&&b&&a!==b){var c=Object.create(b);a=e(c,a)}return a},c.declaration.prototype=g}(Polymer),function(a){function b(a){return document.contains(a)?j:i}function c(){return i.length?i[0]:j[0]}function d(a){f.waitToReady=!0,Polymer.endOfMicrotask(function(){HTMLImports.whenReady(function(){f.addReadyCallback(a),f.waitToReady=!1,f.check()})})}function e(a){if(void 0===a)return void f.ready();var b=setTimeout(function(){f.ready()},a);Polymer.whenReady(function(){clearTimeout(b)})}var f={wait:function(a){a.__queue||(a.__queue={},g.push(a))},enqueue:function(a,c,d){var e=a.__queue&&!a.__queue.check;return e&&(b(a).push(a),a.__queue.check=c,a.__queue.go=d),0!==this.indexOf(a)},indexOf:function(a){var c=b(a).indexOf(a);return c>=0&&document.contains(a)&&(c+=HTMLImports.useNative||HTMLImports.ready?i.length:1e9),c},go:function(a){var b=this.remove(a);b&&(a.__queue.flushable=!0,this.addToFlushQueue(b),this.check())},remove:function(a){var c=this.indexOf(a);if(0===c)return b(a).shift()},check:function(){var a=this.nextElement();return a&&a.__queue.check.call(a),this.canReady()?(this.ready(),!0):void 0},nextElement:function(){return c()},canReady:function(){return!this.waitToReady&&this.isEmpty()},isEmpty:function(){for(var a,b=0,c=g.length;c>b&&(a=g[b]);b++)if(a.__queue&&!a.__queue.flushable)return;return!0},addToFlushQueue:function(a){h.push(a)},flush:function(){if(!this.flushing){this.flushing=!0;for(var a;h.length;)a=h.shift(),a.__queue.go.call(a),a.__queue=null;this.flushing=!1}},ready:function(){var a=CustomElements.ready;CustomElements.ready=!1,this.flush(),CustomElements.useNative||CustomElements.upgradeDocumentTree(document),CustomElements.ready=a,Polymer.flush(),requestAnimationFrame(this.flushReadyCallbacks)},addReadyCallback:function(a){a&&k.push(a)},flushReadyCallbacks:function(){if(k)for(var a;k.length;)(a=k.shift())()},waitingFor:function(){for(var a,b=[],c=0,d=g.length;d>c&&(a=g[c]);c++)a.__queue&&!a.__queue.flushable&&b.push(a);return b},waitToReady:!0},g=[],h=[],i=[],j=[],k=[];a.elements=g,a.waitingFor=f.waitingFor.bind(f),a.forceReady=e,a.queue=f,a.whenReady=a.whenPolymerReady=d}(Polymer),function(a){function b(a){return Boolean(HTMLElement.getPrototypeForTag(a))}function c(a){return a&&a.indexOf("-")>=0}var d=a.extend,e=a.api,f=a.queue,g=a.whenReady,h=a.getRegisteredPrototype,i=a.waitingForPrototype,j=d(Object.create(HTMLElement.prototype),{createdCallback:function(){this.getAttribute("name")&&this.init()},init:function(){this.name=this.getAttribute("name"),this.extends=this.getAttribute("extends"),f.wait(this),this.loadResources(),this.registerWhenReady()},registerWhenReady:function(){this.registered||this.waitingForPrototype(this.name)||this.waitingForQueue()||this.waitingForResources()||f.go(this)},_register:function(){c(this.extends)&&!b(this.extends)&&console.warn("%s is attempting to extend %s, an unregistered element or one that was not registered with Polymer.",this.name,this.extends),this.register(this.name,this.extends),this.registered=!0},waitingForPrototype:function(a){return h(a)?void 0:(i(a,this),this.handleNoScript(a),!0)},handleNoScript:function(a){this.hasAttribute("noscript")&&!this.noscript&&(this.noscript=!0,Polymer(a))},waitingForResources:function(){return this._needsResources},waitingForQueue:function(){return f.enqueue(this,this.registerWhenReady,this._register)},loadResources:function(){this._needsResources=!0,this.loadStyles(function(){this._needsResources=!1,this.registerWhenReady()}.bind(this))}});e.publish(e.declaration,j),g(function(){document.body.removeAttribute("unresolved"),document.dispatchEvent(new CustomEvent("polymer-ready",{bubbles:!0}))}),document.registerElement("polymer-element",{prototype:j})}(Polymer),function(a){function b(a,b){a?(document.head.appendChild(a),d(b)):b&&b()}function c(a,c){if(a&&a.length){for(var d,e,f=document.createDocumentFragment(),g=0,h=a.length;h>g&&(d=a[g]);g++)e=document.createElement("link"),e.rel="import",e.href=d,f.appendChild(e);b(f,c)}else c&&c()}var d=a.whenReady;a.import=c,a.importElements=b}(Polymer),function(){var a=document.createElement("polymer-element");a.setAttribute("name","auto-binding"),a.setAttribute("extends","template"),a.init(),Polymer("auto-binding",{createdCallback:function(){this.syntax=this.bindingDelegate=this.makeSyntax(),Polymer.whenPolymerReady(function(){this.model=this,this.setAttribute("bind",""),this.async(function(){this.marshalNodeReferences(this.parentNode),this.fire("template-bound")})}.bind(this))},makeSyntax:function(){var a=Object.create(Polymer.api.declaration.events),b=this;a.findController=function(){return b.model};var c=new PolymerExpressions,d=c.prepareBinding;return c.prepareBinding=function(b,e,f){return a.prepareEventBinding(b,e,f)||d.call(c,b,e,f)},c}})}();