Merge remote-tracking branch 'upstream/master' into scheduler

* upstream/master: (60 commits)
  StateMachine is now case insensitive for entity ids
  Added an example component that does the bare minimum
  State card rendering now way more flexible
  Update README.md
  Update documentation for example component
  Add link to demo in README
  Add code to mock API for demo on home-assistant.io
  Moved documentation from GitHub source to home-assistant.io
  Make nmap mac regex more flexible to play nice with OS X
  Frontend: color switch icons yellow if on
  New strategy for defining number of used threads
  WeMo component exposes Insight info if available
  Only turn off the specified lights
  Fix default light and device group IDs
  Add nmap_tracker documentation
  Fix typo and default groups
  Specify devices for trigger
  nmap-based device tracking plugin
  Make block_till_stopped test more flexible
  Fix PyLint
  ...
pull/11/head
Gustav Ahlberg 2014-12-28 14:49:28 +01:00
commit 99b1cbf9b5
74 changed files with 3588 additions and 2224 deletions

1
.gitignore vendored
View File

@ -6,6 +6,7 @@ homeassistant/components/http/www_static/polymer/bower_components/*
!config/custom_components !config/custom_components
config/custom_components/* config/custom_components/*
!config/custom_components/example.py !config/custom_components/example.py
!config/custom_components/hello_world.py
# Hide sublime text stuff # Hide sublime text stuff
*.sublime-project *.sublime-project

View File

@ -7,6 +7,6 @@ install:
script: script:
- flake8 homeassistant --exclude bower_components,external - flake8 homeassistant --exclude bower_components,external
- pylint homeassistant - pylint homeassistant
- coverage run --source=homeassistant -m unittest discover test - coverage run --source=homeassistant -m unittest discover ha_test
after_success: after_success:
- coveralls - coveralls

View File

@ -1,23 +1,6 @@
# Adding support for a new device # 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. For help on building your component, please see the See the [developer documentation on home-assistant.io](https://home-assistant.io/developers/).
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**<br />
Contains the Switch component code.
**homeassistant/components/switch/wemo.py**<br />
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).
After you finish adding support for your device: After you finish adding support for your device:

344
README.md
View File

@ -1,5 +1,7 @@
# Home Assistant [![Build Status](https://travis-ci.org/balloob/home-assistant.svg?branch=master)](https://travis-ci.org/balloob/home-assistant) [![Coverage Status](https://img.shields.io/coveralls/balloob/home-assistant.svg)](https://coveralls.io/r/balloob/home-assistant?branch=master) # Home Assistant [![Build Status](https://travis-ci.org/balloob/home-assistant.svg?branch=master)](https://travis-ci.org/balloob/home-assistant) [![Coverage Status](https://img.shields.io/coveralls/balloob/home-assistant.svg)](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. 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: It offers the following functionality through built-in components:
@ -23,7 +25,7 @@ Home Assistant also includes functionality for controlling HTPCs:
* Download files * Download files
* Open URLs in the default browser * Open URLs in the default browser
![screenshot-states](https://raw.github.com/balloob/home-assistant/master/docs/screenshots.png) [![screenshot-states](https://raw.github.com/balloob/home-assistant/master/docs/screenshots.png)](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). 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:* 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 ### Philips Hue
To get Philips Hue working you will have to connect Home Assistant to the Hue bridge. 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] [light]
type=hue platform=hue
``` ```
### Wireless router ### Wireless router
@ -77,7 +81,7 @@ Your wireless router is used to track which devices are connected. Three differe
``` ```
[device_tracker] [device_tracker]
type=netgear platform=netgear
host=192.168.1.1 host=192.168.1.1
username=admin username=admin
password=MY_PASSWORD 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`. *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.
<a name='customizing'></a> 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`).
## 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:
* &lt;config file directory>/custom_components/&lt;component name>.py
* homeassistant/components/&lt;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**<br>
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**<br>
If your configuration file containes the following lines:
``` ```
[example] [device_tracker]
host=paulusschoutsen.nl 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).
<a name="architecture"></a>
## 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.
![home assistant architecture](https://raw.github.com/balloob/home-assistant/master/docs/architecture.png)
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.
<a name='components'></a>
### 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.
<a name='API'></a>
## 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**<br>
Returns message if API is up and running.
```json
{
"message": "API running."
}
```
**/api/events - GET**<br>
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**<br>
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**<br>
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/&lt;entity_id>** - GET<br>
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/&lt;entity_id>** - POST<br>
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.<br>
parameter: new_state - string<br>
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/&lt;event_type>** - POST<br>
Fires an event with event_type<br>
optional body: JSON encoded object that represents event_data
```json
{
"message": "Event download_file fired."
}
```
**/api/services/&lt;domain>/&lt;service>** - POST<br>
Calls a service within a specific domain.<br>
optional body: JSON encoded object that represents service_data
```json
{
"message": "Service keyboard/volume_up called."
}
```
**/api/event_forwarding** - POST<br>
Setup event forwarding to another Home Assistant instance.<br>
parameter: host - string<br>
parameter: api_password - string<br>
optional parameter: port - int<br>
```json
{
"message": "Event forwarding setup."
}
```
**/api/event_forwarding** - DELETE<br>
Cancel event forwarding to another Home Assistant instance.<br>
parameter: host - string<br>
optional parameter: port - int<br>
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."
}
```
<a name='connected_instances'></a>
## 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.
![home assistant master-slave architecture](https://raw.github.com/balloob/home-assistant/master/docs/architecture-remote.png)
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()
```
<a name="related_projects"></a>
## Related projects
[Home Assistant API client in Ruby](https://github.com/balloob/home-assistant-ruby)<br>
[Home Assistant API client for Tasker for Android](https://github.com/balloob/home-assistant-android-tasker)

View File

@ -1,32 +1,120 @@
""" """
custom_components.example 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 # The domain of your component. Should be equal to the name of your component
DOMAIN = "example" DOMAIN = "example"
# List of component names (string) your component depends upon # List of component names (string) your component depends upon
# If you are setting up a group but not using a group for anything, # We depend on group because group will be loaded after all the components that
# don't depend on group # initalize devices have been setup.
DEPENDENCIES = [] 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 # pylint: disable=unused-argument
def setup(hass, config): 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. # Validate that all required config options are given
hass.services.register(DOMAIN, "example_service_name", print) if not validate_config(config, {DOMAIN: [CONF_TARGET]}, _LOGGER):
return False
# This prints a time change event to the command-line twice a minute. target_id = config[DOMAIN][CONF_TARGET]
hass.track_time_change(print, second=[0, 30])
# See also (defined in homeassistant/__init__.py): # Validate that the target entity id exists
# hass.track_state_change if hass.states.get(target_id) is None:
# hass.track_point_in_time _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 # Tells the bootstrapper that the component was succesfully initialized
return True return True

View File

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

View File

@ -9,16 +9,19 @@ api_password=mypass
# development=1 # development=1
[light] [light]
type=hue platform=hue
[device_tracker] [device_tracker]
# The following types are available: netgear, tomato, luci # The following types are available: netgear, tomato, luci, nmap_tracker
type=netgear platform=netgear
host=192.168.1.1 host=192.168.1.1
username=admin username=admin
password=PASSWORD password=PASSWORD
# http_id is needed for Tomato routers only # http_id is needed for Tomato routers only
# http_id=ABCDEFGHH # 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] [chromecast]
# Optional: hard code the hosts (comma seperated) to find chromecasts # 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 # hosts=192.168.1.9,192.168.1.12
[switch] [switch]
type=wemo platform=wemo
# Optional: hard code the hosts (comma seperated) to avoid scanning the network # Optional: hard code the hosts (comma seperated) to avoid scanning the network
# hosts=192.168.1.9,192.168.1.12 # hosts=192.168.1.9,192.168.1.12
@ -53,6 +56,12 @@ xbmc=XBMC.App
[example] [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] [browser]
[keyboard] [keyboard]

View File

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

View File

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

View File

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

77
ha_test/helpers.py Normal file
View File

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

View File

@ -1,6 +1,6 @@
""" """
test.test_component_chromecast ha_test.test_component_chromecast
~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests Chromecast component. Tests Chromecast component.
""" """
@ -9,9 +9,13 @@ import logging
import unittest import unittest
import homeassistant as ha 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 import homeassistant.components.chromecast as chromecast
from helper import mock_service from helpers import mock_service
def setUpModule(): # pylint: disable=invalid-name def setUpModule(): # pylint: disable=invalid-name
@ -33,7 +37,7 @@ class TestChromecast(unittest.TestCase):
def tearDown(self): # pylint: disable=invalid-name def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """ """ Stop down stuff we started. """
self.hass._pool.stop() self.hass.stop()
def test_is_on(self): def test_is_on(self):
""" Test is_on method. """ """ Test is_on method. """
@ -45,37 +49,36 @@ class TestChromecast(unittest.TestCase):
Test if the call service methods conver to correct service calls. Test if the call service methods conver to correct service calls.
""" """
services = { services = {
components.SERVICE_TURN_OFF: chromecast.turn_off, SERVICE_TURN_OFF: chromecast.turn_off,
components.SERVICE_VOLUME_UP: chromecast.volume_up, SERVICE_VOLUME_UP: chromecast.volume_up,
components.SERVICE_VOLUME_DOWN: chromecast.volume_down, SERVICE_VOLUME_DOWN: chromecast.volume_down,
components.SERVICE_MEDIA_PLAY_PAUSE: chromecast.media_play_pause, SERVICE_MEDIA_PLAY_PAUSE: chromecast.media_play_pause,
components.SERVICE_MEDIA_PLAY: chromecast.media_play, SERVICE_MEDIA_PLAY: chromecast.media_play,
components.SERVICE_MEDIA_PAUSE: chromecast.media_pause, SERVICE_MEDIA_PAUSE: chromecast.media_pause,
components.SERVICE_MEDIA_NEXT_TRACK: chromecast.media_next_track, SERVICE_MEDIA_NEXT_TRACK: chromecast.media_next_track,
components.SERVICE_MEDIA_PREV_TRACK: chromecast.media_prev_track SERVICE_MEDIA_PREV_TRACK: chromecast.media_prev_track
} }
for service_name, service_method in services.items(): for service_name, service_method in services.items():
calls = mock_service(self.hass, chromecast.DOMAIN, service_name) calls = mock_service(self.hass, chromecast.DOMAIN, service_name)
service_method(self.hass) service_method(self.hass)
self.hass._pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(1, len(calls)) self.assertEqual(1, len(calls))
call = calls[-1] call = calls[-1]
self.assertEqual(call.domain, chromecast.DOMAIN) self.assertEqual(chromecast.DOMAIN, call.domain)
self.assertEqual(call.service, service_name) self.assertEqual(service_name, call.service)
self.assertEqual(call.data, {})
service_method(self.hass, self.test_entity) service_method(self.hass, self.test_entity)
self.hass._pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(2, len(calls)) self.assertEqual(2, len(calls))
call = calls[-1] call = calls[-1]
self.assertEqual(call.domain, chromecast.DOMAIN) self.assertEqual(chromecast.DOMAIN, call.domain)
self.assertEqual(call.service, service_name) self.assertEqual(service_name, call.service)
self.assertEqual(call.data, self.assertEqual(self.test_entity,
{components.ATTR_ENTITY_ID: self.test_entity}) call.data.get(ATTR_ENTITY_ID))
def test_setup(self): def test_setup(self):
""" """
@ -84,4 +87,4 @@ class TestChromecast(unittest.TestCase):
In an ideal world we would create a mock pychromecast API.. In an ideal world we would create a mock pychromecast API..
""" """
self.assertFalse(chromecast.setup( 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'}}))

View File

@ -1,6 +1,6 @@
""" """
test.test_component_core ha_test.test_component_core
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests core compoments. Tests core compoments.
""" """
@ -9,6 +9,8 @@ import unittest
import homeassistant as ha import homeassistant as ha
import homeassistant.loader as loader import homeassistant.loader as loader
from homeassistant.const import (
STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF)
import homeassistant.components as comps import homeassistant.components as comps
@ -21,12 +23,12 @@ class TestComponentsCore(unittest.TestCase):
loader.prepare(self.hass) loader.prepare(self.hass)
self.assertTrue(comps.setup(self.hass, {})) self.assertTrue(comps.setup(self.hass, {}))
self.hass.states.set('light.Bowl', comps.STATE_ON) self.hass.states.set('light.Bowl', STATE_ON)
self.hass.states.set('light.Ceiling', comps.STATE_OFF) self.hass.states.set('light.Ceiling', STATE_OFF)
def tearDown(self): # pylint: disable=invalid-name def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """ """ Stop down stuff we started. """
self.hass._pool.stop() self.hass.stop()
def test_is_on(self): def test_is_on(self):
""" Test is_on method. """ """ Test is_on method. """
@ -38,11 +40,11 @@ class TestComponentsCore(unittest.TestCase):
""" Test turn_on method. """ """ Test turn_on method. """
runs = [] runs = []
self.hass.services.register( 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') comps.turn_on(self.hass, 'light.Ceiling')
self.hass._pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(1, len(runs)) self.assertEqual(1, len(runs))
@ -50,24 +52,10 @@ class TestComponentsCore(unittest.TestCase):
""" Test turn_off method. """ """ Test turn_off method. """
runs = [] runs = []
self.hass.services.register( 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') comps.turn_off(self.hass, 'light.Bowl')
self.hass._pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(1, len(runs)) 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))

View File

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

View File

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

View File

@ -1,6 +1,6 @@
""" """
test.test_component_group ha_test.test_component_group
~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests the group compoments. Tests the group compoments.
""" """
@ -9,7 +9,7 @@ import unittest
import logging import logging
import homeassistant as ha 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 import homeassistant.components.group as group
@ -25,9 +25,9 @@ class TestComponentsGroup(unittest.TestCase):
""" Init needed objects. """ """ Init needed objects. """
self.hass = ha.HomeAssistant() self.hass = ha.HomeAssistant()
self.hass.states.set('light.Bowl', comps.STATE_ON) self.hass.states.set('light.Bowl', STATE_ON)
self.hass.states.set('light.Ceiling', comps.STATE_OFF) self.hass.states.set('light.Ceiling', STATE_OFF)
self.hass.states.set('switch.AC', comps.STATE_OFF) self.hass.states.set('switch.AC', STATE_OFF)
group.setup_group(self.hass, 'init_group', group.setup_group(self.hass, 'init_group',
['light.Bowl', 'light.Ceiling'], False) ['light.Bowl', 'light.Ceiling'], False)
group.setup_group(self.hass, 'mixed_group', group.setup_group(self.hass, 'mixed_group',
@ -40,43 +40,27 @@ class TestComponentsGroup(unittest.TestCase):
""" Stop down stuff we started. """ """ Stop down stuff we started. """
self.hass.stop() self.hass.stop()
def test_setup_and_monitor_group(self): def test_setup_group(self):
""" Test setup_group method. """ """ 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 # Try to setup a group with mixed groupable states
self.hass.states.set('device_tracker.Paulus', comps.STATE_HOME) self.hass.states.set('device_tracker.Paulus', STATE_HOME)
self.assertFalse(group.setup_group( self.assertTrue(group.setup_group(
self.hass, 'person_and_light', self.hass, 'person_and_light',
['light.Bowl', 'device_tracker.Paulus'])) ['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 # Try to setup a group with a non existing state
self.assertNotIn('non.existing', self.hass.states.entity_ids) self.assertNotIn('non.existing', self.hass.states.entity_ids())
self.assertFalse(group.setup_group( self.assertTrue(group.setup_group(
self.hass, 'light_and_nothing', self.hass, 'light_and_nothing',
['light.Bowl', 'non.existing'])) ['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 # Try to setup a group with non groupable states
self.hass.states.set('cast.living_room', "Plex") self.hass.states.set('cast.living_room', "Plex")
@ -89,23 +73,37 @@ class TestComponentsGroup(unittest.TestCase):
# Try to setup an empty group # Try to setup an empty group
self.assertFalse(group.setup_group(self.hass, 'nothing', [])) self.assertFalse(group.setup_group(self.hass, 'nothing', []))
def test__get_group_type(self): def test_monitor_group(self):
""" Test _get_group_type method. """ """ Test if the group keeps track of states. """
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))
# Unsupported state # Test if group setup in our init mode is ok
self.assertIsNone(group._get_group_type('unsupported_state')) 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): def test_is_on(self):
""" Test is_on method. """ """ Test is_on method. """
self.assertTrue(group.is_on(self.hass, self.group_name)) self.assertTrue(group.is_on(self.hass, self.group_name))
self.hass.states.set('light.Bowl', comps.STATE_OFF) self.hass.states.set('light.Bowl', STATE_OFF)
self.hass._pool.block_till_done() self.hass.pool.block_till_done()
self.assertFalse(group.is_on(self.hass, self.group_name)) self.assertFalse(group.is_on(self.hass, self.group_name))
# Try on non existing state # Try on non existing state
@ -159,5 +157,5 @@ class TestComponentsGroup(unittest.TestCase):
group_state = self.hass.states.get( group_state = self.hass.states.get(
group.ENTITY_ID_FORMAT.format('second_group')) 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]) self.assertFalse(group_state.attributes[group.ATTR_AUTO])

View File

@ -1,6 +1,6 @@
""" """
test.test_component_http ha_test.test_component_http
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests Home Assistant HTTP component does what it should do. 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 def tearDownModule(): # pylint: disable=invalid-name
""" Stops the Home Assistant server. """ """ Stops the Home Assistant server. """
global hass
hass.stop() hass.stop()
class TestHTTP(unittest.TestCase): class TestHTTP(unittest.TestCase):
""" Test the HTTP debug interface and API. """ """ 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. """ """ Tests if we can get the frontend. """
req = requests.get(_url("")) req = requests.get(_url(""))
self.assertEqual(200, req.status_code) self.assertEqual(200, req.status_code)
# Test we can retrieve frontend.js
frontendjs = re.search( frontendjs = re.search(
r'(?P<app>\/static\/frontend-[A-Za-z0-9]{32}.html)', r'(?P<app>\/static\/frontend-[A-Za-z0-9]{32}.html)',
req.text).groups(0)[0] req.text)
self.assertIsNotNone(frontendjs) self.assertIsNotNone(frontendjs)
req = requests.get(_url(frontendjs)) req = requests.head(_url(frontendjs.groups(0)[0]))
self.assertEqual(200, req.status_code) 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): def test_api_password(self):
""" Test if we get access denied if we omit or provide """ Test if we get access denied if we omit or provide
a wrong api password. """ a wrong api password. """
@ -127,8 +147,8 @@ class TestHTTP(unittest.TestCase):
hass.states.set("test.test", "not_to_be_set") hass.states.set("test.test", "not_to_be_set")
requests.post(_url(remote.URL_API_STATES_ENTITY.format("test.test")), requests.post(_url(remote.URL_API_STATES_ENTITY.format("test.test")),
data=json.dumps({"state": "debug_state_change2", data=json.dumps({"state": "debug_state_change2"}),
"api_password": API_PASSWORD})) headers=HA_HEADERS)
self.assertEqual("debug_state_change2", self.assertEqual("debug_state_change2",
hass.states.get("test.test").state) hass.states.get("test.test").state)
@ -143,8 +163,8 @@ class TestHTTP(unittest.TestCase):
req = requests.post( req = requests.post(
_url(remote.URL_API_STATES_ENTITY.format( _url(remote.URL_API_STATES_ENTITY.format(
"test_entity.that_does_not_exist")), "test_entity.that_does_not_exist")),
data=json.dumps({"state": new_state, data=json.dumps({'state': new_state}),
"api_password": API_PASSWORD})) headers=HA_HEADERS)
cur_state = (hass.states. cur_state = (hass.states.
get("test_entity.that_does_not_exist").state) get("test_entity.that_does_not_exist").state)
@ -152,6 +172,18 @@ class TestHTTP(unittest.TestCase):
self.assertEqual(201, req.status_code) self.assertEqual(201, req.status_code)
self.assertEqual(cur_state, new_state) 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 # pylint: disable=invalid-name
def test_api_fire_event_with_no_data(self): def test_api_fire_event_with_no_data(self):
""" Test if the API allows us to fire an event. """ """ 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. """ """ Helper method that will verify our event got called. """
test_value.append(1) test_value.append(1)
hass.listen_once_event("test.event_no_data", listener) hass.bus.listen_once("test.event_no_data", listener)
requests.post( requests.post(
_url(remote.URL_API_EVENTS_EVENT.format("test.event_no_data")), _url(remote.URL_API_EVENTS_EVENT.format("test.event_no_data")),
headers=HA_HEADERS) headers=HA_HEADERS)
hass._pool.block_till_done() hass.pool.block_till_done()
self.assertEqual(1, len(test_value)) self.assertEqual(1, len(test_value))
@ -182,14 +214,14 @@ class TestHTTP(unittest.TestCase):
if "test" in event.data: if "test" in event.data:
test_value.append(1) test_value.append(1)
hass.listen_once_event("test_event_with_data", listener) hass.bus.listen_once("test_event_with_data", listener)
requests.post( requests.post(
_url(remote.URL_API_EVENTS_EVENT.format("test_event_with_data")), _url(remote.URL_API_EVENTS_EVENT.format("test_event_with_data")),
data=json.dumps({"test": 1}), data=json.dumps({"test": 1}),
headers=HA_HEADERS) headers=HA_HEADERS)
hass._pool.block_till_done() hass.pool.block_till_done()
self.assertEqual(1, len(test_value)) self.assertEqual(1, len(test_value))
@ -202,14 +234,25 @@ class TestHTTP(unittest.TestCase):
""" Helper method that will verify our event got called. """ """ Helper method that will verify our event got called. """
test_value.append(1) 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( req = requests.post(
_url(remote.URL_API_EVENTS_EVENT.format("test_event_bad_data")), _url(remote.URL_API_EVENTS_EVENT.format("test_event_bad_data")),
data=json.dumps('not an object'), data=json.dumps('not an object'),
headers=HA_HEADERS) 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(422, req.status_code)
self.assertEqual(0, len(test_value)) self.assertEqual(0, len(test_value))
@ -254,7 +297,7 @@ class TestHTTP(unittest.TestCase):
"test_domain", "test_service")), "test_domain", "test_service")),
headers=HA_HEADERS) headers=HA_HEADERS)
hass._pool.block_till_done() hass.pool.block_till_done()
self.assertEqual(1, len(test_value)) self.assertEqual(1, len(test_value))
@ -276,6 +319,82 @@ class TestHTTP(unittest.TestCase):
data=json.dumps({"test": 1}), data=json.dumps({"test": 1}),
headers=HA_HEADERS) headers=HA_HEADERS)
hass._pool.block_till_done() hass.pool.block_till_done()
self.assertEqual(1, len(test_value)) 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)

View File

@ -1,6 +1,6 @@
""" """
test.test_component_switch ha_test.test_component_switch
~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests switch component. Tests switch component.
""" """
@ -11,12 +11,12 @@ import os
import homeassistant as ha import homeassistant as ha
import homeassistant.loader as loader import homeassistant.loader as loader
import homeassistant.util as util 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 homeassistant.components.light as light
import mock_toggledevice_platform from helpers import mock_service, get_test_home_assistant
from helper import mock_service, get_test_home_assistant
class TestLight(unittest.TestCase): class TestLight(unittest.TestCase):
@ -25,11 +25,10 @@ class TestLight(unittest.TestCase):
def setUp(self): # pylint: disable=invalid-name def setUp(self): # pylint: disable=invalid-name
self.hass = get_test_home_assistant() self.hass = get_test_home_assistant()
loader.prepare(self.hass) loader.prepare(self.hass)
loader.set_component('light.test', mock_toggledevice_platform)
def tearDown(self): # pylint: disable=invalid-name def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """ """ Stop down stuff we started. """
self.hass._pool.stop() self.hass.stop()
user_light_file = self.hass.get_config_path(light.LIGHT_PROFILES_FILE) user_light_file = self.hass.get_config_path(light.LIGHT_PROFILES_FILE)
@ -39,21 +38,21 @@ class TestLight(unittest.TestCase):
def test_methods(self): def test_methods(self):
""" Test if methods call the services as expected. """ """ Test if methods call the services as expected. """
# Test is_on # 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.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.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.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)) self.assertFalse(light.is_on(self.hass))
# Test turn_on # Test turn_on
turn_on_calls = mock_service( turn_on_calls = mock_service(
self.hass, light.DOMAIN, components.SERVICE_TURN_ON) self.hass, light.DOMAIN, SERVICE_TURN_ON)
light.turn_on( light.turn_on(
self.hass, self.hass,
@ -64,44 +63,48 @@ class TestLight(unittest.TestCase):
xy_color='xy_color_val', xy_color='xy_color_val',
profile='profile_val') profile='profile_val')
self.hass._pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(1, len(turn_on_calls)) self.assertEqual(1, len(turn_on_calls))
call = turn_on_calls[-1] call = turn_on_calls[-1]
self.assertEqual(light.DOMAIN, call.domain) self.assertEqual(light.DOMAIN, call.domain)
self.assertEqual(components.SERVICE_TURN_ON, call.service) self.assertEqual(SERVICE_TURN_ON, call.service)
self.assertEqual('entity_id_val', call.data[components.ATTR_ENTITY_ID]) self.assertEqual('entity_id_val', call.data.get(ATTR_ENTITY_ID))
self.assertEqual('transition_val', call.data[light.ATTR_TRANSITION]) self.assertEqual(
self.assertEqual('brightness_val', call.data[light.ATTR_BRIGHTNESS]) 'transition_val', call.data.get(light.ATTR_TRANSITION))
self.assertEqual('rgb_color_val', call.data[light.ATTR_RGB_COLOR]) self.assertEqual(
self.assertEqual('xy_color_val', call.data[light.ATTR_XY_COLOR]) 'brightness_val', call.data.get(light.ATTR_BRIGHTNESS))
self.assertEqual('profile_val', call.data[light.ATTR_PROFILE]) 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 # Test turn_off
turn_off_calls = mock_service( turn_off_calls = mock_service(
self.hass, light.DOMAIN, components.SERVICE_TURN_OFF) self.hass, light.DOMAIN, SERVICE_TURN_OFF)
light.turn_off( light.turn_off(
self.hass, entity_id='entity_id_val', transition='transition_val') 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)) self.assertEqual(1, len(turn_off_calls))
call = turn_off_calls[-1] call = turn_off_calls[-1]
self.assertEqual(light.DOMAIN, call.domain) self.assertEqual(light.DOMAIN, call.domain)
self.assertEqual(components.SERVICE_TURN_OFF, call.service) self.assertEqual(SERVICE_TURN_OFF, call.service)
self.assertEqual('entity_id_val', call.data[components.ATTR_ENTITY_ID]) self.assertEqual('entity_id_val', call.data[ATTR_ENTITY_ID])
self.assertEqual('transition_val', call.data[light.ATTR_TRANSITION]) self.assertEqual('transition_val', call.data[light.ATTR_TRANSITION])
def test_services(self): def test_services(self):
""" Test the provided services. """ """ Test the provided services. """
mock_toggledevice_platform.init() platform = loader.get_component('light.test')
self.assertTrue(
light.setup(self.hass, {light.DOMAIN: {ha.CONF_TYPE: '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 # Test init
self.assertTrue(light.is_on(self.hass, dev1.entity_id)) 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_off(self.hass, entity_id=dev1.entity_id)
light.turn_on(self.hass, entity_id=dev2.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.assertFalse(light.is_on(self.hass, dev1.entity_id))
self.assertTrue(light.is_on(self.hass, dev2.entity_id)) self.assertTrue(light.is_on(self.hass, dev2.entity_id))
@ -120,7 +123,7 @@ class TestLight(unittest.TestCase):
# turn on all lights # turn on all lights
light.turn_on(self.hass) 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, dev1.entity_id))
self.assertTrue(light.is_on(self.hass, dev2.entity_id)) self.assertTrue(light.is_on(self.hass, dev2.entity_id))
@ -129,7 +132,7 @@ class TestLight(unittest.TestCase):
# turn off all lights # turn off all lights
light.turn_off(self.hass) 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, dev1.entity_id))
self.assertFalse(light.is_on(self.hass, dev2.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]) self.hass, dev2.entity_id, rgb_color=[255, 255, 255])
light.turn_on(self.hass, dev3.entity_id, xy_color=[.4, .6]) 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') method, data = dev1.last_call('turn_on')
self.assertEqual( self.assertEqual(
@ -168,7 +171,7 @@ class TestLight(unittest.TestCase):
self.hass, dev2.entity_id, self.hass, dev2.entity_id,
profile=prof_name, brightness=100, xy_color=[.4, .6]) 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') method, data = dev1.last_call('turn_on')
self.assertEqual( 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, dev2.entity_id, xy_color=["bla-di-bla", 5])
light.turn_on(self.hass, dev3.entity_id, rgb_color=[255, None, 2]) 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') method, data = dev1.last_call('turn_on')
self.assertEqual({}, data) self.assertEqual({}, data)
@ -203,7 +206,7 @@ class TestLight(unittest.TestCase):
self.hass, dev1.entity_id, self.hass, dev1.entity_id,
profile=prof_name, brightness='bright', rgb_color='yellowish') 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') method, data = dev1.last_call('turn_on')
self.assertEqual( self.assertEqual(
@ -220,22 +223,23 @@ class TestLight(unittest.TestCase):
# Test with non-existing component # Test with non-existing component
self.assertFalse(light.setup( 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 # Test if light component returns 0 lightes
mock_toggledevice_platform.init(True) platform = loader.get_component('light.test')
platform.init(True)
self.assertEqual( self.assertEqual([], platform.get_lights(None, None))
[], mock_toggledevice_platform.get_lights(None, None))
self.assertFalse(light.setup( self.assertFalse(light.setup(
self.hass, {light.DOMAIN: {ha.CONF_TYPE: 'test'}} self.hass, {light.DOMAIN: {CONF_TYPE: 'test'}}
)) ))
def test_light_profiles(self): def test_light_profiles(self):
""" Test light profiles. """ """ 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) 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') user_file.write('I,WILL,NOT,WORK\n')
self.assertFalse(light.setup( self.assertFalse(light.setup(
self.hass, {light.DOMAIN: {ha.CONF_TYPE: 'test'}} self.hass, {light.DOMAIN: {CONF_TYPE: 'test'}}
)) ))
# Clean up broken file # Clean up broken file
@ -256,14 +260,14 @@ class TestLight(unittest.TestCase):
user_file.write('test,.4,.6,100\n') user_file.write('test,.4,.6,100\n')
self.assertTrue(light.setup( 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') 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') method, data = dev1.last_call('turn_on')

View File

@ -1,6 +1,6 @@
""" """
test.test_component_sun ha_test.test_component_sun
~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests Sun component. Tests Sun component.
""" """
@ -11,6 +11,7 @@ import datetime as dt
import ephem import ephem
import homeassistant as ha import homeassistant as ha
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
import homeassistant.components.sun as sun import homeassistant.components.sun as sun
@ -22,7 +23,7 @@ class TestSun(unittest.TestCase):
def tearDown(self): # pylint: disable=invalid-name def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """ """ Stop down stuff we started. """
self.hass._pool.stop() self.hass.stop()
def test_is_on(self): def test_is_on(self):
""" Test is_on method. """ """ Test is_on method. """
@ -37,8 +38,8 @@ class TestSun(unittest.TestCase):
self.assertTrue(sun.setup( self.assertTrue(sun.setup(
self.hass, self.hass,
{ha.DOMAIN: { {ha.DOMAIN: {
ha.CONF_LATITUDE: '32.87336', CONF_LATITUDE: '32.87336',
ha.CONF_LONGITUDE: '117.22743' CONF_LONGITUDE: '117.22743'
}})) }}))
observer = ephem.Observer() observer = ephem.Observer()
@ -76,8 +77,8 @@ class TestSun(unittest.TestCase):
self.assertTrue(sun.setup( self.assertTrue(sun.setup(
self.hass, self.hass,
{ha.DOMAIN: { {ha.DOMAIN: {
ha.CONF_LATITUDE: '32.87336', CONF_LATITUDE: '32.87336',
ha.CONF_LONGITUDE: '117.22743' CONF_LONGITUDE: '117.22743'
}})) }}))
if sun.is_on(self.hass): if sun.is_on(self.hass):
@ -92,7 +93,7 @@ class TestSun(unittest.TestCase):
self.hass.bus.fire(ha.EVENT_TIME_CHANGED, self.hass.bus.fire(ha.EVENT_TIME_CHANGED,
{ha.ATTR_NOW: test_time + dt.timedelta(seconds=5)}) {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) 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, {}))
self.assertFalse(sun.setup(self.hass, {sun.DOMAIN: {}})) self.assertFalse(sun.setup(self.hass, {sun.DOMAIN: {}}))
self.assertFalse(sun.setup( 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.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.assertFalse(sun.setup(
self.hass, {ha.DOMAIN: {ha.CONF_LATITUDE: 'hello'}})) self.hass, {ha.DOMAIN: {CONF_LATITUDE: 'hello'}}))
self.assertFalse(sun.setup( 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.assertFalse(sun.setup(
self.hass, {ha.DOMAIN: { 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.assertFalse(sun.setup(
self.hass, {ha.DOMAIN: { self.hass, {ha.DOMAIN: {
ha.CONF_LATITUDE: '32.87336', ha.CONF_LONGITUDE: 'wrong' CONF_LATITUDE: '32.87336', CONF_LONGITUDE: 'wrong'
}})) }}))
# Test with correct config # Test with correct config
self.assertTrue(sun.setup( self.assertTrue(sun.setup(
self.hass, {ha.DOMAIN: { self.hass, {ha.DOMAIN: {
ha.CONF_LATITUDE: '32.87336', ha.CONF_LONGITUDE: '117.22743' CONF_LATITUDE: '32.87336', CONF_LONGITUDE: '117.22743'
}})) }}))

View File

@ -1,6 +1,6 @@
""" """
test.test_component_switch ha_test.test_component_switch
~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests switch component. Tests switch component.
""" """
@ -9,38 +9,39 @@ import unittest
import homeassistant as ha import homeassistant as ha
import homeassistant.loader as loader 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 homeassistant.components.switch as switch
import mock_toggledevice_platform from helpers import get_test_home_assistant
class TestSwitch(unittest.TestCase): class TestSwitch(unittest.TestCase):
""" Test the switch module. """ """ Test the switch module. """
def setUp(self): # pylint: disable=invalid-name def setUp(self): # pylint: disable=invalid-name
self.hass = ha.HomeAssistant() self.hass = get_test_home_assistant()
loader.prepare(self.hass) 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.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 # Switch 1 is ON, switch 2 is OFF
self.switch_1, self.switch_2, self.switch_3 = \ 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 def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """ """ Stop down stuff we started. """
self.hass._pool.stop() self.hass.stop()
def test_methods(self): def test_methods(self):
""" Test is_on, turn_on, turn_off methods. """ """ Test is_on, turn_on, turn_off methods. """
self.assertTrue(switch.is_on(self.hass)) self.assertTrue(switch.is_on(self.hass))
self.assertEqual( self.assertEqual(
components.STATE_ON, STATE_ON,
self.hass.states.get(switch.ENTITY_ID_ALL_SWITCHES).state) 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_1.entity_id))
self.assertFalse(switch.is_on(self.hass, self.switch_2.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_off(self.hass, self.switch_1.entity_id)
switch.turn_on(self.hass, self.switch_2.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.assertTrue(switch.is_on(self.hass))
self.assertFalse(switch.is_on(self.hass, self.switch_1.entity_id)) self.assertFalse(switch.is_on(self.hass, self.switch_1.entity_id))
@ -58,11 +59,11 @@ class TestSwitch(unittest.TestCase):
# Turn all off # Turn all off
switch.turn_off(self.hass) 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.assertFalse(switch.is_on(self.hass))
self.assertEqual( self.assertEqual(
components.STATE_OFF, STATE_OFF,
self.hass.states.get(switch.ENTITY_ID_ALL_SWITCHES).state) 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_1.entity_id))
self.assertFalse(switch.is_on(self.hass, self.switch_2.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 # Turn all on
switch.turn_on(self.hass) 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.assertTrue(switch.is_on(self.hass))
self.assertEqual( self.assertEqual(
components.STATE_ON, STATE_ON,
self.hass.states.get(switch.ENTITY_ID_ALL_SWITCHES).state) 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_1.entity_id))
self.assertTrue(switch.is_on(self.hass, self.switch_2.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 # Test with non-existing component
self.assertFalse(switch.setup( 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 # 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( self.assertEqual(
[], mock_toggledevice_platform.get_switches(None, None)) [], test_platform.get_switches(None, None))
self.assertFalse(switch.setup( 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'},
}
)) ))

View File

@ -1,6 +1,6 @@
""" """
test.test_core ha_test.test_core
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
Provides tests to verify that Home Assistant core works. Provides tests to verify that Home Assistant core works.
""" """
@ -30,7 +30,7 @@ class TestHomeAssistant(unittest.TestCase):
def tearDown(self): # pylint: disable=invalid-name def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """ """ Stop down stuff we started. """
self.hass._pool.stop() self.hass.stop()
def test_get_config_path(self): def test_get_config_path(self):
""" Test get_config_path method. """ """ Test get_config_path method. """
@ -52,81 +52,18 @@ class TestHomeAssistant(unittest.TestCase):
self.assertTrue(blocking_thread.is_alive()) self.assertTrue(blocking_thread.is_alive())
self.hass.call_service(ha.DOMAIN, ha.SERVICE_HOMEASSISTANT_STOP) self.hass.services.call(ha.DOMAIN, ha.SERVICE_HOMEASSISTANT_STOP)
self.hass._pool.block_till_done() self.hass.pool.block_till_done()
# hass.block_till_stopped checks every second if it should quit # hass.block_till_stopped checks every second if it should quit
# we have to wait worst case 1 second # we have to wait worst case 1 second
wait_loops = 0 wait_loops = 0
while blocking_thread.is_alive() and wait_loops < 10: while blocking_thread.is_alive() and wait_loops < 50:
wait_loops += 1 wait_loops += 1
time.sleep(0.1) time.sleep(0.1)
self.assertFalse(blocking_thread.is_alive()) 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): def test_track_point_in_time(self):
""" Test track point in time. """ """ Test track point in time. """
before_birthday = datetime(1985, 7, 9, 12, 0, 0) before_birthday = datetime(1985, 7, 9, 12, 0, 0)
@ -139,23 +76,23 @@ class TestHomeAssistant(unittest.TestCase):
lambda x: runs.append(1), birthday_paulus) lambda x: runs.append(1), birthday_paulus)
self._send_time_changed(before_birthday) self._send_time_changed(before_birthday)
self.hass._pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(0, len(runs)) self.assertEqual(0, len(runs))
self._send_time_changed(birthday_paulus) self._send_time_changed(birthday_paulus)
self.hass._pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(1, len(runs)) self.assertEqual(1, len(runs))
# A point in time tracker will only fire once, this should do nothing # A point in time tracker will only fire once, this should do nothing
self._send_time_changed(birthday_paulus) self._send_time_changed(birthday_paulus)
self.hass._pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(1, len(runs)) self.assertEqual(1, len(runs))
self.hass.track_point_in_time( self.hass.track_point_in_time(
lambda x: runs.append(1), birthday_paulus) lambda x: runs.append(1), birthday_paulus)
self._send_time_changed(after_birthday) self._send_time_changed(after_birthday)
self.hass._pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(2, len(runs)) self.assertEqual(2, len(runs))
def test_track_time_change(self): def test_track_time_change(self):
@ -168,17 +105,17 @@ class TestHomeAssistant(unittest.TestCase):
lambda x: specific_runs.append(1), second=[0, 30]) lambda x: specific_runs.append(1), second=[0, 30])
self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0)) 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(specific_runs))
self.assertEqual(1, len(wildcard_runs)) self.assertEqual(1, len(wildcard_runs))
self._send_time_changed(datetime(2014, 5, 24, 12, 0, 15)) 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(1, len(specific_runs))
self.assertEqual(2, len(wildcard_runs)) self.assertEqual(2, len(wildcard_runs))
self._send_time_changed(datetime(2014, 5, 24, 12, 0, 30)) 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(2, len(specific_runs))
self.assertEqual(3, len(wildcard_runs)) self.assertEqual(3, len(wildcard_runs))
@ -234,6 +171,21 @@ class TestEventBus(unittest.TestCase):
# Try deleting listener while category doesn't exist either # Try deleting listener while category doesn't exist either
self.bus.remove_listener('test', listener) 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): class TestState(unittest.TestCase):
""" Test EventBus methods. """ """ 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.Bowl', 'off'))
self.assertFalse(self.states.is_state('light.Non_existing', 'on')) 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): def test_remove(self):
""" Test remove method. """ """ 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.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 # If it does not exist, we should get False
self.assertFalse(self.states.remove('light.Bowl')) 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): class TestServiceCall(unittest.TestCase):
""" Test ServiceCall class. """ """ Test ServiceCall class. """

49
ha_test/test_helpers.py Normal file
View File

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

87
ha_test/test_loader.py Normal file
View File

@ -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']))

View File

@ -1,8 +1,10 @@
""" """
test.remote ha_test.remote
~~~~~~~~~~~ ~~~~~~~~~~~~~~
Tests Home Assistant remote methods and classes. 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 # pylint: disable=protected-access,too-many-public-methods
import unittest import unittest
@ -13,11 +15,11 @@ import homeassistant.components.http as http
API_PASSWORD = "test1234" 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} 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=""): def _url(path=""):
@ -27,7 +29,7 @@ def _url(path=""):
def setUpModule(): # pylint: disable=invalid-name def setUpModule(): # pylint: disable=invalid-name
""" Initalizes a Home Assistant server and Slave instance. """ """ Initalizes a Home Assistant server and Slave instance. """
global hass, slave, master_api global hass, slave, master_api, broken_api
hass = ha.HomeAssistant() hass = ha.HomeAssistant()
@ -35,29 +37,28 @@ def setUpModule(): # pylint: disable=invalid-name
hass.states.set('test.test', 'a_state') hass.states.set('test.test', 'a_state')
http.setup(hass, 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() 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 # Start slave
local_api = remote.API("127.0.0.1", API_PASSWORD, 8124) slave = remote.HomeAssistant(master_api)
slave = remote.HomeAssistant(master_api, local_api)
http.setup(slave,
{http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD,
http.CONF_SERVER_PORT: 8124}})
slave.start() slave.start()
# Setup API pointing at nothing
broken_api = remote.API("127.0.0.1", "", 8125)
def tearDownModule(): # pylint: disable=invalid-name def tearDownModule(): # pylint: disable=invalid-name
""" Stops the Home Assistant server and slave. """ """ Stops the Home Assistant server and slave. """
global hass, slave global hass, slave
hass.stop()
slave.stop() slave.stop()
hass.stop()
class TestRemoteMethods(unittest.TestCase): class TestRemoteMethods(unittest.TestCase):
@ -71,6 +72,9 @@ class TestRemoteMethods(unittest.TestCase):
remote.validate_api( remote.validate_api(
remote.API("127.0.0.1", API_PASSWORD + "A"))) 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): def test_get_event_listeners(self):
""" Test Python API get_event_listeners. """ """ Test Python API get_event_listeners. """
local_data = hass.bus.listeners local_data = hass.bus.listeners
@ -82,6 +86,8 @@ class TestRemoteMethods(unittest.TestCase):
self.assertEqual(len(local_data), 0) self.assertEqual(len(local_data), 0)
self.assertEqual({}, remote.get_event_listeners(broken_api))
def test_fire_event(self): def test_fire_event(self):
""" Test Python API fire_event. """ """ Test Python API fire_event. """
test_value = [] test_value = []
@ -90,14 +96,17 @@ class TestRemoteMethods(unittest.TestCase):
""" Helper method that will verify our event got called. """ """ Helper method that will verify our event got called. """
test_value.append(1) 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") 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)) 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): def test_get_state(self):
""" Test Python API get_state. """ """ Test Python API get_state. """
@ -105,11 +114,13 @@ class TestRemoteMethods(unittest.TestCase):
hass.states.get('test.test'), hass.states.get('test.test'),
remote.get_state(master_api, 'test.test')) remote.get_state(master_api, 'test.test'))
self.assertEqual(None, remote.get_state(broken_api, 'test.test'))
def test_get_states(self): def test_get_states(self):
""" Test Python API get_state_entity_ids. """ """ Test Python API get_state_entity_ids. """
self.assertEqual( self.assertEqual(hass.states.all(), remote.get_states(master_api))
remote.get_states(master_api), hass.states.all()) self.assertEqual([], remote.get_states(broken_api))
def test_set_state(self): def test_set_state(self):
""" Test Python API set_state. """ """ Test Python API set_state. """
@ -117,6 +128,8 @@ class TestRemoteMethods(unittest.TestCase):
self.assertEqual('set_test', hass.states.get('test.test').state) 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): def test_is_state(self):
""" Test Python API is_state. """ """ Test Python API is_state. """
@ -124,6 +137,10 @@ class TestRemoteMethods(unittest.TestCase):
remote.is_state(master_api, 'test.test', remote.is_state(master_api, 'test.test',
hass.states.get('test.test').state)) 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): def test_get_services(self):
""" Test Python API get_services. """ """ Test Python API get_services. """
@ -134,8 +151,10 @@ class TestRemoteMethods(unittest.TestCase):
self.assertEqual(local, serv_domain["services"]) self.assertEqual(local, serv_domain["services"])
self.assertEqual({}, remote.get_services(broken_api))
def test_call_service(self): def test_call_service(self):
""" Test Python API call_service. """ """ Test Python API services.call. """
test_value = [] test_value = []
def listener(service_call): # pylint: disable=unused-argument 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") 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)) self.assertEqual(1, len(test_value))
# Should not raise an exception
remote.call_service(broken_api, "test_domain", "test_service")
class TestRemoteClasses(unittest.TestCase): class TestRemoteClasses(unittest.TestCase):
""" Test the homeassistant.remote module. """ """ Test the homeassistant.remote module. """
def test_home_assistant_init(self): def test_home_assistant_init(self):
""" Test HomeAssistant init. """ """ Test HomeAssistant init. """
# Wrong password
self.assertRaises( self.assertRaises(
ha.HomeAssistantError, remote.HomeAssistant, ha.HomeAssistantError, remote.HomeAssistant,
remote.API('127.0.0.1', API_PASSWORD + 'A', 8124)) 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): def test_statemachine_init(self):
""" Tests if remote.StateMachine copies all states on init. """ """ Tests if remote.StateMachine copies all states on init. """
self.assertEqual(len(hass.states.all()), self.assertEqual(len(hass.states.all()),
@ -176,7 +204,7 @@ class TestRemoteClasses(unittest.TestCase):
# Wait till slave tells master # Wait till slave tells master
slave._pool.block_till_done() slave._pool.block_till_done()
# Wait till master gives updated state # Wait till master gives updated state
hass._pool.block_till_done() hass.pool.block_till_done()
self.assertEqual("remote.statemachine test", self.assertEqual("remote.statemachine test",
slave.states.get("remote.test").state) slave.states.get("remote.test").state)
@ -189,13 +217,23 @@ class TestRemoteClasses(unittest.TestCase):
""" Helper method that will verify our event got called. """ """ Helper method that will verify our event got called. """
test_value.append(1) 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") slave.bus.fire("test.event_no_data")
# Wait till slave tells master # Wait till slave tells master
slave._pool.block_till_done() slave._pool.block_till_done()
# Wait till master gives updated event # Wait till master gives updated event
hass._pool.block_till_done() hass.pool.block_till_done()
self.assertEqual(1, len(test_value)) 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)

256
ha_test/test_util.py Normal file
View File

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

View File

@ -15,37 +15,27 @@ import re
import datetime as dt import datetime as dt
import functools as ft 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 import homeassistant.util as util
MATCH_ALL = '*'
DOMAIN = "homeassistant" 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 # How often time_changed event should fire
TIMER_INTERVAL = 10 # seconds TIMER_INTERVAL = 10 # seconds
# Number of worker threads # How long we wait for the result of a service call
POOL_NUM_THREAD = 4 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: <domain>.<entity>) # Pattern for validating entity IDs (format: <domain>.<entity>)
ENTITY_ID_PATTERN = re.compile(r"^(?P<domain>\w+)\.(?P<entity>\w+)$") ENTITY_ID_PATTERN = re.compile(r"^(?P<domain>\w+)\.(?P<entity>\w+)$")
@ -57,8 +47,7 @@ class HomeAssistant(object):
""" Core class to route all communication to right components. """ """ Core class to route all communication to right components. """
def __init__(self): def __init__(self):
self._pool = pool = create_worker_pool() self.pool = pool = create_worker_pool()
self.bus = EventBus(pool) self.bus = EventBus(pool)
self.services = ServiceRegistry(self.bus, pool) self.services = ServiceRegistry(self.bus, pool)
self.states = StateMachine(self.bus) self.states = StateMachine(self.bus)
@ -71,6 +60,9 @@ class HomeAssistant(object):
def start(self): def start(self):
""" Start home assistant. """ """ Start home assistant. """
_LOGGER.info(
"Starting Home Assistant (%d threads)", self.pool.worker_count)
Timer(self) Timer(self)
self.bus.fire(EVENT_HOMEASSISTANT_START) self.bus.fire(EVENT_HOMEASSISTANT_START)
@ -92,50 +84,6 @@ class HomeAssistant(object):
self.stop() 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): def track_point_in_time(self, action, point_in_time):
""" """
Adds a listener that fires once at or after a spefic 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) 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): def stop(self):
""" Stops Home Assistant and shuts down all threads. """ """ Stops Home Assistant and shuts down all threads. """
_LOGGER.info("Stopping") _LOGGER.info("Stopping")
@ -234,14 +157,67 @@ class HomeAssistant(object):
self.bus.fire(EVENT_HOMEASSISTANT_STOP) self.bus.fire(EVENT_HOMEASSISTANT_STOP)
# Wait till all responses to homeassistant_stop are done # 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): def _process_match_param(parameter):
""" Wraps parameter in a list if it is not one and returns it. """ """ 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 return MATCH_ALL
elif isinstance(parameter, list): elif isinstance(parameter, list):
return parameter return parameter
@ -261,6 +237,7 @@ class JobPriority(util.OrderedEnum):
""" Provides priorities for bus events. """ """ Provides priorities for bus events. """
# pylint: disable=no-init,too-few-public-methods # pylint: disable=no-init,too-few-public-methods
EVENT_CALLBACK = 0
EVENT_SERVICE = 1 EVENT_SERVICE = 1
EVENT_STATE = 2 EVENT_STATE = 2
EVENT_TIME = 3 EVENT_TIME = 3
@ -275,11 +252,13 @@ class JobPriority(util.OrderedEnum):
return JobPriority.EVENT_STATE return JobPriority.EVENT_STATE
elif event_type == EVENT_CALL_SERVICE: elif event_type == EVENT_CALL_SERVICE:
return JobPriority.EVENT_SERVICE return JobPriority.EVENT_SERVICE
elif event_type == EVENT_SERVICE_EXECUTED:
return JobPriority.EVENT_CALLBACK
else: else:
return JobPriority.EVENT_DEFAULT return JobPriority.EVENT_DEFAULT
def create_worker_pool(thread_count=POOL_NUM_THREAD): def create_worker_pool():
""" Creates a worker pool to be used. """ """ Creates a worker pool to be used. """
def job_handler(job): 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 # We do not want to crash our ThreadPool
_LOGGER.exception("BusHandler:Exception doing job") _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. """ """ 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", "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: for start, job in current_jobs:
_LOGGER.error("WorkerPool:Current job from %s: %s", _LOGGER.warning("WorkerPool:Current job from %s: %s",
util.datetime_to_str(start), job) 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): class EventOrigin(enum.Enum):
@ -374,9 +353,10 @@ class EventBus(object):
if not listeners: if not listeners:
return return
job_priority = JobPriority.from_event_type(event_type)
for func in listeners: for func in listeners:
self._pool.add_job(JobPriority.from_event_type(event_type), self._pool.add_job(job_priority, (func, event))
(func, event))
def listen(self, event_type, listener): def listen(self, event_type, listener):
""" Listen for all events or events of a specific type. """ Listen for all events or events of a specific type.
@ -390,6 +370,31 @@ class EventBus(object):
else: else:
self._listeners[event_type] = [listener] 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): def remove_listener(self, event_type, listener):
""" Removes a listener of a specific event_type. """ """ Removes a listener of a specific event_type. """
with self._lock: with self._lock:
@ -420,17 +425,13 @@ class State(object):
self.entity_id = entity_id self.entity_id = entity_id
self.state = state self.state = state
self.attributes = attributes or {} self.attributes = attributes or {}
last_changed = last_changed or dt.datetime.now()
# Strip microsecond from last_changed else we cannot guarantee # Strip microsecond from last_changed else we cannot guarantee
# state == State.from_dict(state.as_dict()) # state == State.from_dict(state.as_dict())
# This behavior occurs because to_dict uses datetime_to_str # This behavior occurs because to_dict uses datetime_to_str
# which strips microseconds # which does not preserve microseconds
if last_changed.microsecond: self.last_changed = util.strip_microseconds(
self.last_changed = last_changed - dt.timedelta( last_changed or dt.datetime.now())
microseconds=last_changed.microsecond)
else:
self.last_changed = last_changed
def copy(self): def copy(self):
""" Creates a copy of itself. """ """ Creates a copy of itself. """
@ -483,14 +484,20 @@ class StateMachine(object):
""" Helper class that tracks the state of different entities. """ """ Helper class that tracks the state of different entities. """
def __init__(self, bus): def __init__(self, bus):
self._states = {} self._states = CaseInsensitiveDict()
self._bus = bus self._bus = bus
self._lock = threading.Lock() self._lock = threading.Lock()
@property def entity_ids(self, domain_filter=None):
def entity_ids(self):
""" List of entity ids that are being tracked. """ """ 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): def all(self):
""" Returns a list of all states. """ """ Returns a list of all states. """
@ -503,15 +510,28 @@ class StateMachine(object):
# Make a copy so people won't mutate the state # Make a copy so people won't mutate the state
return state.copy() if state else None 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): def is_state(self, entity_id, state):
""" Returns True if entity exists and is specified state. """ """ Returns True if entity exists and is specified state. """
return (entity_id in self._states and return (entity_id in self._states and
self._states[entity_id].state == state) self._states[entity_id].state == state)
def remove(self, entity_id): 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: with self._lock:
return self._states.pop(entity_id, None) is not None 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) 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 # pylint: disable=too-few-public-methods
class ServiceCall(object): class ServiceCall(object):
@ -567,6 +621,8 @@ class ServiceRegistry(object):
self._services = {} self._services = {}
self._lock = threading.Lock() self._lock = threading.Lock()
self._pool = pool or create_worker_pool() self._pool = pool or create_worker_pool()
self._bus = bus
self._cur_id = 0
bus.listen(EVENT_CALL_SERVICE, self._event_to_service_call) bus.listen(EVENT_CALL_SERVICE, self._event_to_service_call)
@property @property
@ -588,6 +644,57 @@ class ServiceRegistry(object):
else: else:
self._services[domain] = {service: service_func} 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): def _event_to_service_call(self, event):
""" Calls a service from an event. """ """ Calls a service from an event. """
service_data = dict(event.data) service_data = dict(event.data)
@ -598,9 +705,27 @@ class ServiceRegistry(object):
if domain in self._services and service in self._services[domain]: if domain in self._services and service in self._services[domain]:
service_call = ServiceCall(domain, service, service_data) 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._pool.add_job(JobPriority.EVENT_SERVICE,
(self._services[domain][service], (self._execute_service,
service_call)) (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): class Timer(threading.Thread):
@ -610,7 +735,7 @@ class Timer(threading.Thread):
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.daemon = True self.daemon = True
self._bus = hass.bus self.hass = hass
self.interval = interval or TIMER_INTERVAL self.interval = interval or TIMER_INTERVAL
self._stop = threading.Event() self._stop = threading.Event()
@ -619,15 +744,15 @@ class Timer(threading.Thread):
# every minute. # every minute.
assert 60 % self.interval == 0, "60 % TIMER_INTERVAL should be 0!" assert 60 % self.interval == 0, "60 % TIMER_INTERVAL should be 0!"
hass.listen_once_event(EVENT_HOMEASSISTANT_START, hass.bus.listen_once(EVENT_HOMEASSISTANT_START,
lambda event: self.start()) lambda event: self.start())
hass.listen_once_event(EVENT_HOMEASSISTANT_STOP,
lambda event: self._stop.set())
def run(self): def run(self):
""" Start the timer. """ """ Start the timer. """
self.hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
lambda event: self._stop.set())
_LOGGER.info("Timer:starting") _LOGGER.info("Timer:starting")
last_fired_on_second = -1 last_fired_on_second = -1
@ -658,7 +783,7 @@ class Timer(threading.Thread):
last_fired_on_second = now.second 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): class HomeAssistantError(Exception):

View File

@ -13,12 +13,10 @@ import os
import configparser import configparser
import logging import logging
from collections import defaultdict from collections import defaultdict
from itertools import chain
import homeassistant import homeassistant
import homeassistant.loader as loader import homeassistant.loader as loader
import homeassistant.components as core_components import homeassistant.components as core_components
import homeassistant.components.group as group
# pylint: disable=too-many-branches, too-many-statements # pylint: disable=too-many-branches, too-many-statements
@ -33,123 +31,49 @@ def from_config_dict(config, hass=None):
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
loader.prepare(hass)
# Make a copy because we are mutating it. # Make a copy because we are mutating it.
# Convert it to defaultdict so components can always have config dict # Convert it to defaultdict so components can always have config dict
config = defaultdict(dict, config) config = defaultdict(dict, config)
# List of loaded components # Filter out the repeating and common config section [homeassistant]
components = {} components = (key for key in config.keys()
if ' ' not in key and key != homeassistant.DOMAIN)
# List of components to validate if not core_components.setup(hass, config):
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:
logger.error(("Home Assistant core failed to initialize. " logger.error(("Home Assistant core failed to initialize. "
"Further initialization aborted.")) "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 return hass
@ -181,7 +105,7 @@ def from_config_file(config_path, hass=None, enable_logging=True):
err_handler = logging.FileHandler( err_handler = logging.FileHandler(
err_log_path, mode='w', delay=True) err_log_path, mode='w', delay=True)
err_handler.setLevel(logging.ERROR) err_handler.setLevel(logging.WARNING)
err_handler.setFormatter( err_handler.setFormatter(
logging.Formatter('%(asctime)s %(name)s: %(message)s', logging.Formatter('%(asctime)s %(name)s: %(message)s',
datefmt='%H:%M %d-%m-%y')) datefmt='%H:%M %d-%m-%y'))

View File

@ -19,36 +19,10 @@ import logging
import homeassistant as ha import homeassistant as ha
import homeassistant.util as util import homeassistant.util as util
from homeassistant.helpers import extract_entity_ids
from homeassistant.loader import get_component from homeassistant.loader import get_component
from homeassistant.const import (
# Contains one string or a list of strings, each being an entity id ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
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"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -61,7 +35,7 @@ def is_on(hass, entity_id=None):
entity_ids = group.expand_entity_ids(hass, [entity_id]) entity_ids = group.expand_entity_ids(hass, [entity_id])
else: else:
entity_ids = hass.states.entity_ids entity_ids = hass.states.entity_ids()
for entity_id in entity_ids: for entity_id in entity_ids:
domain = util.split_entity_id(entity_id)[0] 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: if entity_id is not None:
service_data[ATTR_ENTITY_ID] = entity_id 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): 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: if entity_id is not None:
service_data[ATTR_ENTITY_ID] = entity_id service_data[ATTR_ENTITY_ID] = entity_id
hass.call_service(ha.DOMAIN, SERVICE_TURN_OFF, service_data) hass.services.call(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())
# pylint: disable=unused-argument # pylint: disable=unused-argument
@ -195,7 +96,7 @@ def setup(hass, config):
# ent_ids is a generator, convert it to a list. # ent_ids is a generator, convert it to a list.
data[ATTR_ENTITY_ID] = list(ent_ids) 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_OFF, handle_turn_service)
hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service) hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)

View File

@ -6,9 +6,14 @@ Provides functionality to interact with Chromecasts.
""" """
import logging import logging
import homeassistant as ha
import homeassistant.util as util 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' DOMAIN = 'chromecast'
DEPENDENCIES = [] DEPENDENCIES = []
@ -38,7 +43,7 @@ def is_on(hass, entity_id=None):
""" Returns true if specified ChromeCast entity_id is on. """ Returns true if specified ChromeCast entity_id is on.
Will check all chromecasts if no entity_id specified. """ 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) return any(not hass.states.is_state(entity_id, STATE_NO_APP)
for entity_id in entity_ids) for entity_id in entity_ids)
@ -46,58 +51,58 @@ def is_on(hass, entity_id=None):
def turn_off(hass, entity_id=None): def turn_off(hass, entity_id=None):
""" Will turn off specified Chromecast or all. """ """ 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): def volume_up(hass, entity_id=None):
""" Send the chromecast the command for volume up. """ """ 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): def volume_down(hass, entity_id=None):
""" Send the chromecast the command for volume down. """ """ 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): def media_play_pause(hass, entity_id=None):
""" Send the chromecast the command for play/pause. """ """ 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): def media_play(hass, entity_id=None):
""" Send the chromecast the command for play/pause. """ """ 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): def media_pause(hass, entity_id=None):
""" Send the chromecast the command for play/pause. """ """ 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): def media_next_track(hass, entity_id=None):
""" Send the chromecast the command for next track. """ """ 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): def media_prev_track(hass, entity_id=None):
""" Send the chromecast the command for prev track. """ """ 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 # pylint: disable=too-many-locals, too-many-branches
@ -114,8 +119,8 @@ def setup(hass, config):
return False return False
if ha.CONF_HOSTS in config[DOMAIN]: if CONF_HOSTS in config[DOMAIN]:
hosts = config[DOMAIN][ha.CONF_HOSTS].split(",") hosts = config[DOMAIN][CONF_HOSTS].split(",")
# If no hosts given, scan for chromecasts # If no hosts given, scan for chromecasts
else: else:
@ -131,7 +136,7 @@ def setup(hass, config):
entity_id = util.ensure_unique_string( entity_id = util.ensure_unique_string(
ENTITY_ID_FORMAT.format( ENTITY_ID_FORMAT.format(
util.slugify(cast.device.friendly_name)), util.slugify(cast.device.friendly_name)),
list(casts.keys())) casts.keys())
casts[entity_id] = cast casts[entity_id] = cast
@ -148,7 +153,7 @@ def setup(hass, config):
status = chromecast.app status = chromecast.app
state_attr = {components.ATTR_FRIENDLY_NAME: state_attr = {ATTR_FRIENDLY_NAME:
chromecast.device.friendly_name} chromecast.device.friendly_name}
if status and status.app_id != pychromecast.APP_ID['HOME']: if status and status.app_id != pychromecast.APP_ID['HOME']:
@ -196,7 +201,7 @@ def setup(hass, config):
def _service_to_entities(service): def _service_to_entities(service):
""" Helper method to get entities from 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: if entity_ids:
for entity_id in entity_ids: for entity_id in entity_ids:
@ -274,25 +279,25 @@ def setup(hass, config):
hass.track_time_change(update_chromecast_states) 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) turn_off_service)
hass.services.register(DOMAIN, components.SERVICE_VOLUME_UP, hass.services.register(DOMAIN, SERVICE_VOLUME_UP,
volume_up_service) volume_up_service)
hass.services.register(DOMAIN, components.SERVICE_VOLUME_DOWN, hass.services.register(DOMAIN, SERVICE_VOLUME_DOWN,
volume_down_service) volume_down_service)
hass.services.register(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE, hass.services.register(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE,
media_play_pause_service) media_play_pause_service)
hass.services.register(DOMAIN, components.SERVICE_MEDIA_PLAY, hass.services.register(DOMAIN, SERVICE_MEDIA_PLAY,
media_play_service) media_play_service)
hass.services.register(DOMAIN, components.SERVICE_MEDIA_PAUSE, hass.services.register(DOMAIN, SERVICE_MEDIA_PAUSE,
media_pause_service) media_pause_service)
hass.services.register(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK, hass.services.register(DOMAIN, SERVICE_MEDIA_NEXT_TRACK,
media_next_track_service) media_next_track_service)
hass.services.register(DOMAIN, "start_fireplace", hass.services.register(DOMAIN, "start_fireplace",

View File

@ -8,11 +8,12 @@ import random
import homeassistant as ha import homeassistant as ha
import homeassistant.loader as loader import homeassistant.loader as loader
from homeassistant.components import (SERVICE_TURN_ON, SERVICE_TURN_OFF, from homeassistant.helpers import extract_entity_ids
STATE_ON, STATE_OFF, ATTR_ENTITY_PICTURE, from homeassistant.const import (
extract_entity_ids) SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON, STATE_OFF,
from homeassistant.components.light import (ATTR_XY_COLOR, ATTR_BRIGHTNESS, ATTR_ENTITY_PICTURE, ATTR_ENTITY_ID, CONF_LATITUDE, CONF_LONGITUDE)
GROUP_NAME_ALL_LIGHTS) from homeassistant.components.light import (
ATTR_XY_COLOR, ATTR_BRIGHTNESS, GROUP_NAME_ALL_LIGHTS)
from homeassistant.util import split_entity_id from homeassistant.util import split_entity_id
DOMAIN = "demo" DOMAIN = "demo"
@ -24,6 +25,9 @@ def setup(hass, config):
""" Setup a demo environment. """ """ Setup a demo environment. """
group = loader.get_component('group') group = loader.get_component('group')
config.setdefault(ha.DOMAIN, {})
config.setdefault(DOMAIN, {})
if config[DOMAIN].get('hide_demo_state') != '1': if config[DOMAIN].get('hide_demo_state') != '1':
hass.states.set('a.Demo_Mode', 'Enabled') hass.states.set('a.Demo_Mode', 'Enabled')
@ -35,7 +39,12 @@ def setup(hass, config):
def mock_turn_on(service): def mock_turn_on(service):
""" Will fake the component has been turned on. """ """ 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) domain, _ = split_entity_id(entity_id)
if domain == "light": if domain == "light":
@ -48,15 +57,20 @@ def setup(hass, config):
def mock_turn_off(service): def mock_turn_off(service):
""" Will fake the component has been turned off. """ """ 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) hass.states.set(entity_id, STATE_OFF)
# Setup sun # Setup sun
if ha.CONF_LATITUDE not in config[ha.DOMAIN]: if CONF_LATITUDE not in config[ha.DOMAIN]:
config[ha.DOMAIN][ha.CONF_LATITUDE] = '32.87336' config[ha.DOMAIN][CONF_LATITUDE] = '32.87336'
if ha.CONF_LONGITUDE not in config[ha.DOMAIN]: if CONF_LONGITUDE not in config[ha.DOMAIN]:
config[ha.DOMAIN][ha.CONF_LONGITUDE] = '-117.22743' config[ha.DOMAIN][CONF_LONGITUDE] = '-117.22743'
loader.get_component('sun').setup(hass, config) loader.get_component('sun').setup(hass, config)

View File

@ -8,7 +8,7 @@ the state of the sun and devices.
import logging import logging
from datetime import datetime, timedelta 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 from . import light, sun, device_tracker, group
DOMAIN = "device_sun_light_trigger" DOMAIN = "device_sun_light_trigger"
@ -21,6 +21,7 @@ LIGHT_PROFILE = 'relax'
CONF_LIGHT_PROFILE = 'light_profile' CONF_LIGHT_PROFILE = 'light_profile'
CONF_LIGHT_GROUP = 'light_group' CONF_LIGHT_GROUP = 'light_group'
CONF_DEVICE_GROUP = 'device_group'
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
@ -30,13 +31,17 @@ def setup(hass, config):
disable_turn_off = 'disable_turn_off' in config[DOMAIN] disable_turn_off = 'disable_turn_off' in config[DOMAIN]
light_group = config[DOMAIN].get(CONF_LIGHT_GROUP, 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) 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__) 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: if not device_entity_ids:
logger.error("No devices found to track") 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 # Track every time sun rises so we can schedule a time-based
# pre-sun set event # pre-sun set event
hass.track_state_change(sun.ENTITY_ID, schedule_light_on_sun_rise, hass.states.track_change(sun.ENTITY_ID, schedule_light_on_sun_rise,
sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON) sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON)
# If the sun is already above horizon # If the sun is already above horizon
# schedule the time-based pre-sun set event # schedule the time-based pre-sun set event
@ -108,7 +113,7 @@ def setup(hass, config):
# Specific device came home ? # Specific device came home ?
if entity != device_tracker.ENTITY_ID_ALL_DEVICES and \ 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 # These variables are needed for the elif check
now = datetime.now() now = datetime.now()
@ -142,8 +147,8 @@ def setup(hass, config):
break break
# Did all devices leave the house? # Did all devices leave the house?
elif (entity == device_tracker.ENTITY_ID_ALL_DEVICES and elif (entity == device_group and
new_state.state == components.STATE_NOT_HOME and lights_are_on new_state.state == STATE_NOT_HOME and lights_are_on
and not disable_turn_off): and not disable_turn_off):
logger.info( logger.info(
@ -152,12 +157,13 @@ def setup(hass, config):
light.turn_off(hass) light.turn_off(hass)
# Track home coming of each device # Track home coming of each device
hass.track_state_change(device_entity_ids, check_light_on_dev_state_change, hass.states.track_change(
components.STATE_NOT_HOME, components.STATE_HOME) device_entity_ids, check_light_on_dev_state_change,
STATE_NOT_HOME, STATE_HOME)
# Track when all devices are gone to shut down lights # Track when all devices are gone to shut down lights
hass.track_state_change(device_tracker.ENTITY_ID_ALL_DEVICES, hass.states.track_change(
check_light_on_dev_state_change, device_group, check_light_on_dev_state_change,
components.STATE_HOME, components.STATE_NOT_HOME) STATE_HOME, STATE_NOT_HOME)
return True return True

View File

@ -1,6 +1,6 @@
""" """
homeassistant.components.tracker homeassistant.components.tracker
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to keep track of devices. Provides functionality to keep track of devices.
""" """
@ -10,11 +10,13 @@ import os
import csv import csv
from datetime import datetime, timedelta from datetime import datetime, timedelta
import homeassistant as ha
from homeassistant.loader import get_component from homeassistant.loader import get_component
from homeassistant.helpers import validate_config
import homeassistant.util as util 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 from homeassistant.components import group
DOMAIN = "device_tracker" DOMAIN = "device_tracker"
@ -30,7 +32,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
# After how much time do we consider a device not home if # After how much time do we consider a device not home if
# it does not show up on scans # 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 # Filename to save known devices to
KNOWN_DEVICES_FILE = "known_devices.csv" 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. """ """ Returns if any or specified device is home. """
entity = entity_id or ENTITY_ID_ALL_DEVICES 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): def setup(hass, config):
""" Sets up the device tracker. """ """ 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 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( tracker_implementation = get_component(
'device_tracker.{}'.format(tracker_type)) 'device_tracker.{}'.format(tracker_type))
@ -70,105 +82,109 @@ def setup(hass, config):
return False 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 DeviceTracker(object):
""" Class that tracks which devices are home and which are not. """ """ Class that tracks which devices are home and which are not. """
def __init__(self, hass, device_scanner): def __init__(self, hass, device_scanner):
self.states = hass.states self.hass = hass
self.device_scanner = device_scanner self.device_scanner = device_scanner
self.error_scanning = TIME_SPAN_FOR_ERROR_IN_SCANNING
self.lock = threading.Lock() 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 # 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 # Did we encounter an invalid known devices file
self.invalid_known_devices_file = False 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 # Wrap it in a func instead of lambda so it can be identified in
# the bus by its __name__ attribute. # 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. """ """ 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.track_time_change(update_device_state)
hass.services.register(DOMAIN, hass.services.register(DOMAIN,
SERVICE_DEVICE_TRACKER_RELOAD, SERVICE_DEVICE_TRACKER_RELOAD,
lambda service: self._read_known_devices_file()) reload_known_devices_service)
self.update_devices()
group.setup_group(
hass, GROUP_NAME_ALL_DEVICES, self.device_entity_ids, False)
@property @property
def device_entity_ids(self): def device_entity_ids(self):
""" Returns a set containing all device entity ids """ Returns a set containing all device entity ids
that are being tracked. """ that are being tracked. """
return set([self.known_devices[device]['entity_id'] for device return set(device['entity_id'] for device in self.tracked.values())
in self.known_devices
if self.known_devices[device]['track']])
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. """ """ Update device states based on the found devices. """
self.lock.acquire() 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 is_home:
if known_dev[device]['track']] found_devices.remove(device)
for device in found_devices: # Did we find any devices that we didn't know about yet?
# Are we tracking this device? new_devices = found_devices - self.untracked_devices
if device in temp_tracking_devices:
temp_tracking_devices.remove(device)
known_dev[device]['last_seen'] = now if new_devices:
self.untracked_devices.update(new_devices)
self.states.set( # Write new devices to known devices file
known_dev[device]['entity_id'], components.STATE_HOME, if not self.invalid_known_devices_file:
known_dev[device]['default_state_attr'])
# For all devices we did not find, set state to NH known_dev_path = self.hass.get_config_path(KNOWN_DEVICES_FILE)
# 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:
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: try:
# If file does not exist we will write the header too # If file does not exist we will write the header too
is_new_file = not os.path.isfile(known_dev_path) 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: with open(known_dev_path, 'a') as outp:
_LOGGER.info( _LOGGER.info(
"Found %d new devices, updating %s", "Found %d new devices, updating %s",
len(unknown_devices), known_dev_path) len(new_devices), known_dev_path)
writer = csv.writer(outp) writer = csv.writer(outp)
@ -184,109 +200,114 @@ class DeviceTracker(object):
writer.writerow(( writer.writerow((
"device", "name", "track", "picture")) "device", "name", "track", "picture"))
for device in unknown_devices: for device in new_devices:
# See if the device scanner knows the name # See if the device scanner knows the name
# else defaults to unknown device # else defaults to unknown device
name = (self.device_scanner.get_device_name(device) name = (self.device_scanner.get_device_name(device)
or "unknown_device") or "unknown_device")
writer.writerow((device, name, 0, "")) writer.writerow((device, name, 0, ""))
known_dev[device] = {'name': name,
'track': False,
'picture': ""}
except IOError: except IOError:
_LOGGER.exception( _LOGGER.exception(
"Error updating %s with %d new devices", "Error updating %s with %d new devices",
known_dev_path, len(unknown_devices)) known_dev_path, len(new_devices))
self.lock.release() self.lock.release()
# pylint: disable=too-many-branches
def _read_known_devices_file(self): def _read_known_devices_file(self):
""" Parse and process the known devices file. """ """ Parse and process the known devices file. """
known_dev_path = self.hass.get_config_path(KNOWN_DEVICES_FILE)
# Read known devices if file exists # Return if no known devices file exists
if os.path.isfile(self.path_known_devices_file): if not os.path.isfile(known_dev_path):
self.lock.acquire() return
known_devices = {} self.lock.acquire()
with open(self.path_known_devices_file) as inp: self.untracked_devices.clear()
default_last_seen = datetime(1990, 1, 1)
# Temp variable to keep track of which entity ids we use with open(known_dev_path) as inp:
# so we can ensure we have unique entity ids. default_last_seen = datetime(1990, 1, 1)
used_entity_ids = []
try: # To track which devices need an entity_id assigned
for row in csv.DictReader(inp): need_entity_id = []
device = row['device']
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']: if row['picture']:
row['default_state_attr'] = { state_attr[ATTR_ENTITY_PICTURE] = row['picture']
components.ATTR_ENTITY_PICTURE: row['picture']}
else: self.tracked[device]['state_attr'] = state_attr
row['default_state_attr'] = None
# If we track this device setup tracking variables else:
if row['track']: self.untracked_devices.add(device)
row['last_seen'] = default_last_seen
# Make sure that each device is mapped # Remove existing devices that we no longer track
# to a unique entity_id name for device in removed_devices:
name = util.slugify(row['name']) if row['name'] \ entity_id = self.tracked[device]['entity_id']
else "unnamed_device"
entity_id = ENTITY_ID_FORMAT.format(name) _LOGGER.info("Removing entity %s", entity_id)
tries = 1
while entity_id in used_entity_ids: self.hass.states.remove(entity_id)
tries += 1
suffix = "_{}".format(tries) self.tracked.pop(device)
entity_id = ENTITY_ID_FORMAT.format( # Setup entity_ids for the new devices
name + suffix) 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 for device in need_entity_id:
used_entity_ids.append(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: self.tracked[device]['entity_id'] = entity_id
_LOGGER.warning(
"No devices to track. Please update %s.",
self.path_known_devices_file)
# Remove entities that are no longer maintained if not self.tracked:
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
_LOGGER.warning( _LOGGER.warning(
("Invalid known devices file: %s. " "No devices to track. Please update %s.",
"We won't update it with new found devices."), known_dev_path)
self.path_known_devices_file)
finally: _LOGGER.info("Loaded devices from %s", known_dev_path)
self.lock.release()
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()

View File

@ -1,13 +1,14 @@
""" Supports scanning a OpenWRT router. """ """ Supports scanning a OpenWRT router. """
import logging import logging
import json import json
from datetime import datetime, timedelta from datetime import timedelta
import re import re
import threading import threading
import requests import requests
import homeassistant as ha from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
import homeassistant.util as util from homeassistant.helpers import validate_config
from homeassistant.util import Throttle
from homeassistant.components.device_tracker import DOMAIN from homeassistant.components.device_tracker import DOMAIN
# Return cached results if last scan was less then this time ago # Return cached results if last scan was less then this time ago
@ -19,10 +20,9 @@ _LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def get_scanner(hass, config): def get_scanner(hass, config):
""" Validates config and returns a Luci scanner. """ """ Validates config and returns a Luci scanner. """
if not util.validate_config(config, if not validate_config(config,
{DOMAIN: [ha.CONF_HOST, ha.CONF_USERNAME, {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
ha.CONF_PASSWORD]}, _LOGGER):
_LOGGER):
return None return None
scanner = LuciDeviceScanner(config[DOMAIN]) scanner = LuciDeviceScanner(config[DOMAIN])
@ -45,14 +45,13 @@ class LuciDeviceScanner(object):
""" """
def __init__(self, config): def __init__(self, config):
host = config[ha.CONF_HOST] host = config[CONF_HOST]
username, password = config[ha.CONF_USERNAME], config[ha.CONF_PASSWORD] username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);") self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
self.lock = threading.Lock() self.lock = threading.Lock()
self.date_updated = None
self.last_results = {} self.last_results = {}
self.token = _get_token(host, username, password) self.token = _get_token(host, username, password)
@ -88,29 +87,25 @@ class LuciDeviceScanner(object):
return return
return self.mac2name.get(device, None) return self.mac2name.get(device, None)
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self): def _update_info(self):
""" Ensures the information from the Luci router is up to date. """ Ensures the information from the Luci router is up to date.
Returns boolean if scanning successful. """ Returns boolean if scanning successful. """
if not self.success_init: if not self.success_init:
return False return False
with self.lock: with self.lock:
# if date_updated is None or the date is too old we scan _LOGGER.info("Checking ARP")
# for new data
if not self.date_updated or \
datetime.now() - self.date_updated > MIN_TIME_BETWEEN_SCANS:
_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) return True
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 False
def _req_json_rpc(url, method, *args, **kwargs): def _req_json_rpc(url, method, *args, **kwargs):

View File

@ -1,10 +1,11 @@
""" Supports scanning a Netgear router. """ """ Supports scanning a Netgear router. """
import logging import logging
from datetime import datetime, timedelta from datetime import timedelta
import threading import threading
import homeassistant as ha from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
import homeassistant.util as util from homeassistant.helpers import validate_config
from homeassistant.util import Throttle
from homeassistant.components.device_tracker import DOMAIN from homeassistant.components.device_tracker import DOMAIN
# Return cached results if last scan was less then this time ago # Return cached results if last scan was less then this time ago
@ -16,10 +17,9 @@ _LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def get_scanner(hass, config): def get_scanner(hass, config):
""" Validates config and returns a Netgear scanner. """ """ Validates config and returns a Netgear scanner. """
if not util.validate_config(config, if not validate_config(config,
{DOMAIN: [ha.CONF_HOST, ha.CONF_USERNAME, {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
ha.CONF_PASSWORD]}, _LOGGER):
_LOGGER):
return None return None
scanner = NetgearDeviceScanner(config[DOMAIN]) scanner = NetgearDeviceScanner(config[DOMAIN])
@ -31,10 +31,9 @@ class NetgearDeviceScanner(object):
""" This class queries a Netgear wireless router using the SOAP-api. """ """ This class queries a Netgear wireless router using the SOAP-api. """
def __init__(self, config): def __init__(self, config):
host = config[ha.CONF_HOST] host = config[CONF_HOST]
username, password = config[ha.CONF_USERNAME], config[ha.CONF_PASSWORD] username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
self.date_updated = None
self.last_results = [] self.last_results = []
try: try:
@ -75,10 +74,6 @@ class NetgearDeviceScanner(object):
def get_device_name(self, mac): def get_device_name(self, mac):
""" Returns the name of the given device or None if we don't know. """ """ 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 filter_named = [device.name for device in self.last_results
if device.mac == mac] if device.mac == mac]
@ -87,6 +82,7 @@ class NetgearDeviceScanner(object):
else: else:
return None return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self): def _update_info(self):
""" Retrieves latest information from the Netgear router. """ Retrieves latest information from the Netgear router.
Returns boolean if scanning successful. """ Returns boolean if scanning successful. """
@ -94,18 +90,6 @@ class NetgearDeviceScanner(object):
return return
with self.lock: with self.lock:
# if date_updated is None or the date is too old we scan for _LOGGER.info("Scanning")
# new data
if not self.date_updated or \
datetime.now() - self.date_updated > MIN_TIME_BETWEEN_SCANS:
_LOGGER.info("Scanning") self.last_results = self._api.get_attached_devices()
self.last_results = self._api.get_attached_devices()
self.date_updated = datetime.now()
return
else:
return

View File

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

View File

@ -1,14 +1,15 @@
""" Supports scanning a Tomato router. """ """ Supports scanning a Tomato router. """
import logging import logging
import json import json
from datetime import datetime, timedelta from datetime import timedelta
import re import re
import threading import threading
import requests import requests
import homeassistant as ha from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
import homeassistant.util as util from homeassistant.helpers import validate_config
from homeassistant.util import Throttle
from homeassistant.components.device_tracker import DOMAIN from homeassistant.components.device_tracker import DOMAIN
# Return cached results if last scan was less then this time ago # Return cached results if last scan was less then this time ago
@ -22,10 +23,10 @@ _LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def get_scanner(hass, config): def get_scanner(hass, config):
""" Validates config and returns a Tomato scanner. """ """ Validates config and returns a Tomato scanner. """
if not util.validate_config(config, if not validate_config(config,
{DOMAIN: [ha.CONF_HOST, ha.CONF_USERNAME, {DOMAIN: [CONF_HOST, CONF_USERNAME,
ha.CONF_PASSWORD, CONF_HTTP_ID]}, CONF_PASSWORD, CONF_HTTP_ID]},
_LOGGER): _LOGGER):
return None return None
return TomatoDeviceScanner(config[DOMAIN]) return TomatoDeviceScanner(config[DOMAIN])
@ -40,8 +41,8 @@ class TomatoDeviceScanner(object):
""" """
def __init__(self, config): def __init__(self, config):
host, http_id = config[ha.CONF_HOST], config[CONF_HTTP_ID] host, http_id = config[CONF_HOST], config[CONF_HTTP_ID]
username, password = config[ha.CONF_USERNAME], config[ha.CONF_PASSWORD] username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
self.req = requests.Request('POST', self.req = requests.Request('POST',
'http://{}/update.cgi'.format(host), 'http://{}/update.cgi'.format(host),
@ -55,7 +56,6 @@ class TomatoDeviceScanner(object):
self.logger = logging.getLogger("{}.{}".format(__name__, "Tomato")) self.logger = logging.getLogger("{}.{}".format(__name__, "Tomato"))
self.lock = threading.Lock() self.lock = threading.Lock()
self.date_updated = None
self.last_results = {"wldev": [], "dhcpd_lease": []} self.last_results = {"wldev": [], "dhcpd_lease": []}
self.success_init = self._update_tomato_info() self.success_init = self._update_tomato_info()
@ -71,10 +71,6 @@ class TomatoDeviceScanner(object):
def get_device_name(self, device): def get_device_name(self, device):
""" Returns the name of the given device or None if we don't know. """ """ 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'] filter_named = [item[0] for item in self.last_results['dhcpd_lease']
if item[2] == device] if item[2] == device]
@ -83,16 +79,12 @@ class TomatoDeviceScanner(object):
else: else:
return filter_named[0] return filter_named[0]
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_tomato_info(self): def _update_tomato_info(self):
""" Ensures the information from the Tomato router is up to date. """ Ensures the information from the Tomato router is up to date.
Returns boolean if scanning successful. """ Returns boolean if scanning successful. """
self.lock.acquire() 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:
self.logger.info("Scanning") self.logger.info("Scanning")
try: try:
@ -111,8 +103,6 @@ class TomatoDeviceScanner(object):
self.last_results[param] = \ self.last_results[param] = \
json.loads(value.replace("'", '"')) json.loads(value.replace("'", '"'))
self.date_updated = datetime.now()
return True return True
elif response.status_code == 401: elif response.status_code == 401:
@ -146,13 +136,3 @@ class TomatoDeviceScanner(object):
"Failed to parse response from router") "Failed to parse response from router")
return False 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

View File

@ -9,7 +9,8 @@ import logging
import re import re
import threading import threading
import homeassistant.util as util from homeassistant.helpers import validate_config
from homeassistant.util import sanitize_filename
DOMAIN = "downloader" DOMAIN = "downloader"
DEPENDENCIES = [] DEPENDENCIES = []
@ -36,7 +37,7 @@ def setup(hass, config):
return False 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 return False
download_path = config[DOMAIN][CONF_DOWNLOAD_DIR] download_path = config[DOMAIN][CONF_DOWNLOAD_DIR]
@ -64,7 +65,7 @@ def setup(hass, config):
subdir = service.data.get(ATTR_SUBDIR) subdir = service.data.get(ATTR_SUBDIR)
if subdir: if subdir:
subdir = util.sanitize_filename(subdir) subdir = sanitize_filename(subdir)
final_path = None final_path = None
@ -88,7 +89,7 @@ def setup(hass, config):
filename = "ha_download" filename = "ha_download"
# Remove stuff to ruin paths # Remove stuff to ruin paths
filename = util.sanitize_filename(filename) filename = sanitize_filename(filename)
# Do we want to download to subdir, create if needed # Do we want to download to subdir, create if needed
if subdir: if subdir:

View File

@ -7,10 +7,10 @@ Provides functionality to group devices that can be turned on or off.
import logging import logging
import homeassistant as ha
import homeassistant.util as util import homeassistant.util as util
from homeassistant.components import (STATE_ON, STATE_OFF, from homeassistant.const import (
STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID, STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME)
ATTR_ENTITY_ID)
DOMAIN = "group" DOMAIN = "group"
DEPENDENCIES = [] DEPENDENCIES = []
@ -19,19 +19,19 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}"
ATTR_AUTO = "auto" ATTR_AUTO = "auto"
_GROUP_TYPES = { # List of ON/OFF state tuples for groupable states
"on_off": (STATE_ON, STATE_OFF), _GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME)]
"home_not_home": (STATE_HOME, STATE_NOT_HOME)
} _GROUPS = {}
def _get_group_type(state): def _get_group_on_off(state):
""" Determine the group type based on the given group type. """ """ Determine the group on/off states based on a state. """
for group_type, states in _GROUP_TYPES.items(): for states in _GROUP_TYPES:
if state in states: if state in states:
return group_type return states
return None return None, None
def is_on(hass, entity_id): def is_on(hass, entity_id):
@ -39,10 +39,10 @@ def is_on(hass, entity_id):
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
if state: 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 # 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 return False
@ -101,93 +101,114 @@ def setup(hass, config):
return True return True
# pylint: disable=too-many-branches
def setup_group(hass, name, entity_ids, user_defined=True): def setup_group(hass, name, entity_ids, user_defined=True):
""" Sets up a group state that is the combined state of """ Sets up a group state that is the combined state of
several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """ several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """
logger = logging.getLogger(__name__)
# In case an iterable is passed in # In case an iterable is passed in
entity_ids = list(entity_ids) 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: # Loop over the given entities to:
# - determine which group type this is (on_off, device_home) # - determine which group type this is (on_off, device_home)
# - if all states exist and have valid states # - determine which states exist and have groupable states
# - retrieve the current state of the group # - determine the current state of the group
errors = [] warnings = []
group_type, group_on, group_off, group_state = None, None, None, None group_ids = []
group_on, group_off = None, None
group_state = False
for entity_id in entity_ids: for entity_id in entity_ids:
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
# Try to determine group type if we didn't yet # Try to determine group type if we didn't yet
if not group_type and state: if group_on is None and state:
group_type = _get_group_type(state.state) group_on, group_off = _get_group_on_off(state.state)
if group_type: if group_on is None:
group_on, group_off = _GROUP_TYPES[group_type]
group_state = group_off
else:
# We did not find a matching group_type # We did not find a matching group_type
errors.append( warnings.append(
"Entity {} has ungroupable state '{}'".format( "Entity {} has ungroupable state '{}'".format(
name, state.state)) name, state.state))
# Stop check all other entity IDs and report as error continue
break
# Check if entity exists # Check if entity exists
if not state: 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: 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)) entity_id, state.state, group_off, group_on))
# Keep track of the group state to init later on # We have a valid group state
elif state.state == group_on: else:
group_state = group_on group_ids.append(entity_id)
if group_type is None and not errors: # Keep track of the group state to init later on
errors.append('Unable to determine group type for {}'.format(name)) group_state = group_state or state.state == group_on
if errors: # If none of the entities could be found during setup
logging.getLogger(__name__).error( if not group_ids:
"Error setting up group %s: %s", name, ", ".join(errors)) logger.error('Unable to find any entities to track for group %s', name)
return False return False
else: elif warnings:
group_entity_id = ENTITY_ID_FORMAT.format(name) logger.warning(
state_attr = {ATTR_ENTITY_ID: entity_ids, ATTR_AUTO: not user_defined} 'Warnings during setting up group %s: %s',
name, ", ".join(warnings))
# pylint: disable=unused-argument group_entity_id = ENTITY_ID_FORMAT.format(util.slugify(name))
def update_group_state(entity_id, old_state, new_state): state = group_on if group_state else group_off
""" Updates the group state based on a state change by state_attr = {ATTR_ENTITY_ID: group_ids, ATTR_AUTO: not user_defined}
a tracked entity. """
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 cur_gr_state = hass.states.get(group_entity_id).state
# if cur_gr_state = ON and new_state = OFF: research
# else: ignore
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 elif cur_gr_state == group_on and new_state.state == group_off:
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)
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))

View File

@ -83,6 +83,10 @@ from socketserver import ThreadingMixIn
from urllib.parse import urlparse, parse_qs from urllib.parse import urlparse, parse_qs
import homeassistant as ha 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.remote as rem
import homeassistant.util as util import homeassistant.util as util
from . import frontend from . import frontend
@ -108,22 +112,23 @@ CONF_SERVER_HOST = "server_host"
CONF_SERVER_PORT = "server_port" CONF_SERVER_PORT = "server_port"
CONF_DEVELOPMENT = "development" CONF_DEVELOPMENT = "development"
DATA_API_PASSWORD = 'api_password'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def setup(hass, config): def setup(hass, config):
""" Sets up the HTTP API and debug interface. """ """ Sets up the HTTP API and debug interface. """
if not util.validate_config(config, {DOMAIN: [CONF_API_PASSWORD]}, if not validate_config(config, {DOMAIN: [CONF_API_PASSWORD]}, _LOGGER):
_LOGGER):
return False 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 # If no server host is given, accept all incoming requests
server_host = config[DOMAIN].get(CONF_SERVER_HOST, '0.0.0.0') 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" development = config[DOMAIN].get(CONF_DEVELOPMENT, "") == "1"
@ -131,15 +136,11 @@ def setup(hass, config):
RequestHandler, hass, api_password, RequestHandler, hass, api_password,
development) development)
hass.listen_once_event( hass.bus.listen_once(
ha.EVENT_HOMEASSISTANT_START, ha.EVENT_HOMEASSISTANT_START,
lambda event: lambda event:
threading.Thread(target=server.start, daemon=True).start()) 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 no local api set, set one with known information
if isinstance(hass, rem.HomeAssistant) and hass.local_api is None: if isinstance(hass, rem.HomeAssistant) and hass.local_api is None:
hass.local_api = \ hass.local_api = \
@ -156,9 +157,9 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
daemon_threads = True daemon_threads = True
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
def __init__(self, server_address, RequestHandlerClass, def __init__(self, server_address, request_handler_class,
hass, api_password, development=False): hass, api_password, development=False):
super().__init__(server_address, RequestHandlerClass) super().__init__(server_address, request_handler_class)
self.server_address = server_address self.server_address = server_address
self.hass = hass self.hass = hass
@ -173,6 +174,10 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
def start(self): def start(self):
""" Starts the server. """ """ Starts the server. """
self.hass.bus.listen_once(
ha.EVENT_HOMEASSISTANT_STOP,
lambda event: self.shutdown())
_LOGGER.info( _LOGGER.info(
"Starting web interface at http://%s:%d", *self.server_address) "Starting web interface at http://%s:%d", *self.server_address)
@ -192,13 +197,12 @@ class RequestHandler(SimpleHTTPRequestHandler):
PATHS = [ # debug interface PATHS = [ # debug interface
('GET', URL_ROOT, '_handle_get_root'), ('GET', URL_ROOT, '_handle_get_root'),
('POST', URL_ROOT, '_handle_get_root'),
# /api - for validation purposes # /api - for validation purposes
('GET', rem.URL_API, '_handle_get_api'), ('GET', URL_API, '_handle_get_api'),
# /states # /states
('GET', rem.URL_API_STATES, '_handle_get_api_states'), ('GET', URL_API_STATES, '_handle_get_api_states'),
('GET', ('GET',
re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'), re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
'_handle_get_api_states_entity'), '_handle_get_api_states_entity'),
@ -210,13 +214,13 @@ class RequestHandler(SimpleHTTPRequestHandler):
'_handle_post_state_entity'), '_handle_post_state_entity'),
# /events # /events
('GET', rem.URL_API_EVENTS, '_handle_get_api_events'), ('GET', URL_API_EVENTS, '_handle_get_api_events'),
('POST', ('POST',
re.compile(r'/api/events/(?P<event_type>[a-zA-Z\._0-9]+)'), re.compile(r'/api/events/(?P<event_type>[a-zA-Z\._0-9]+)'),
'_handle_api_post_events_event'), '_handle_api_post_events_event'),
# /services # /services
('GET', rem.URL_API_SERVICES, '_handle_get_api_services'), ('GET', URL_API_SERVICES, '_handle_get_api_services'),
('POST', ('POST',
re.compile((r'/api/services/' re.compile((r'/api/services/'
r'(?P<domain>[a-zA-Z\._0-9]+)/' r'(?P<domain>[a-zA-Z\._0-9]+)/'
@ -224,12 +228,14 @@ class RequestHandler(SimpleHTTPRequestHandler):
'_handle_post_api_services_domain_service'), '_handle_post_api_services_domain_service'),
# /event_forwarding # /event_forwarding
('POST', rem.URL_API_EVENT_FORWARD, '_handle_post_api_event_forward'), ('POST', URL_API_EVENT_FORWARD, '_handle_post_api_event_forward'),
('DELETE', rem.URL_API_EVENT_FORWARD, ('DELETE', URL_API_EVENT_FORWARD,
'_handle_delete_api_event_forward'), '_handle_delete_api_event_forward'),
# Statis files # Static files
('GET', re.compile(r'/static/(?P<file>[a-zA-Z\._\-0-9/]+)'), ('GET', re.compile(r'/static/(?P<file>[a-zA-Z\._\-0-9/]+)'),
'_handle_get_static'),
('HEAD', re.compile(r'/static/(?P<file>[a-zA-Z\._\-0-9/]+)'),
'_handle_get_static') '_handle_get_static')
] ]
@ -255,24 +261,22 @@ class RequestHandler(SimpleHTTPRequestHandler):
if content_length: if content_length:
body_content = self.rfile.read(content_length).decode("UTF-8") body_content = self.rfile.read(content_length).decode("UTF-8")
if self.use_json: try:
try: data.update(json.loads(body_content))
data.update(json.loads(body_content)) except (TypeError, ValueError):
except ValueError: # TypeError is JSON object is not a dict
_LOGGER.exception("Exception parsing JSON: %s", # ValueError if we could not parse JSON
body_content) _LOGGER.exception("Exception parsing JSON: %s",
body_content)
self._message( self._json_message(
"Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY) "Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY)
return return
else:
data.update({key: value[-1] for key, value in
parse_qs(body_content).items()})
api_password = self.headers.get(rem.AUTH_HEADER) api_password = self.headers.get(AUTH_HEADER)
if not api_password and 'api_password' in data: if not api_password and DATA_API_PASSWORD in data:
api_password = data['api_password'] api_password = data[DATA_API_PASSWORD]
if '_METHOD' in data: if '_METHOD' in data:
method = data.pop('_METHOD') method = data.pop('_METHOD')
@ -307,7 +311,7 @@ class RequestHandler(SimpleHTTPRequestHandler):
# For API calls we need a valid password # For API calls we need a valid password
if self.use_json and api_password != self.server.api_password: if self.use_json and api_password != self.server.api_password:
self._message( self._json_message(
"API password missing or incorrect.", HTTP_UNAUTHORIZED) "API password missing or incorrect.", HTTP_UNAUTHORIZED)
else: else:
@ -315,9 +319,11 @@ class RequestHandler(SimpleHTTPRequestHandler):
elif path_matched_but_not_method: elif path_matched_but_not_method:
self.send_response(HTTP_METHOD_NOT_ALLOWED) self.send_response(HTTP_METHOD_NOT_ALLOWED)
self.end_headers()
else: else:
self.send_response(HTTP_NOT_FOUND) self.send_response(HTTP_NOT_FOUND)
self.end_headers()
def do_HEAD(self): # pylint: disable=invalid-name def do_HEAD(self): # pylint: disable=invalid-name
""" HEAD request handler. """ """ HEAD request handler. """
@ -377,7 +383,7 @@ class RequestHandler(SimpleHTTPRequestHandler):
# pylint: disable=unused-argument # pylint: disable=unused-argument
def _handle_get_api(self, path_match, data): def _handle_get_api(self, path_match, data):
""" Renders the debug interface. """ """ Renders the debug interface. """
self._message("API running.") self._json_message("API running.")
# pylint: disable=unused-argument # pylint: disable=unused-argument
def _handle_get_api_states(self, path_match, data): def _handle_get_api_states(self, path_match, data):
@ -394,7 +400,7 @@ class RequestHandler(SimpleHTTPRequestHandler):
if state: if state:
self._write_json(state) self._write_json(state)
else: 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): def _handle_post_state_entity(self, path_match, data):
""" Handles updating the state of an entity. """ Handles updating the state of an entity.
@ -407,7 +413,7 @@ class RequestHandler(SimpleHTTPRequestHandler):
try: try:
new_state = data['state'] new_state = data['state']
except KeyError: except KeyError:
self._message("state not specified", HTTP_BAD_REQUEST) self._json_message("state not specified", HTTP_BAD_REQUEST)
return return
attributes = data['attributes'] if 'attributes' in data else None attributes = data['attributes'] if 'attributes' in data else None
@ -417,19 +423,14 @@ class RequestHandler(SimpleHTTPRequestHandler):
# Write state # Write state
self.server.hass.states.set(entity_id, new_state, attributes) self.server.hass.states.set(entity_id, new_state, attributes)
# Return state if json, else redirect to main page state = self.server.hass.states.get(entity_id)
if self.use_json:
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( self._write_json(
state.as_dict(), state.as_dict(),
status_code=status_code, status_code=status_code,
location=rem.URL_API_STATES_ENTITY.format(entity_id)) location=URL_API_STATES_ENTITY.format(entity_id))
else:
self._message(
"State of {} changed to {}".format(entity_id, new_state))
def _handle_get_api_events(self, path_match, data): def _handle_get_api_events(self, path_match, data):
""" Handles getting overview of event listeners. """ """ Handles getting overview of event listeners. """
@ -448,8 +449,8 @@ class RequestHandler(SimpleHTTPRequestHandler):
event_type = path_match.group('event_type') event_type = path_match.group('event_type')
if event_data is not None and not isinstance(event_data, dict): if event_data is not None and not isinstance(event_data, dict):
self._message("event_data should be an object", self._json_message("event_data should be an object",
HTTP_UNPROCESSABLE_ENTITY) HTTP_UNPROCESSABLE_ENTITY)
event_origin = ha.EventOrigin.remote event_origin = ha.EventOrigin.remote
@ -464,7 +465,7 @@ class RequestHandler(SimpleHTTPRequestHandler):
self.server.hass.bus.fire(event_type, event_data, event_origin) 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): def _handle_get_api_services(self, path_match, data):
""" Handles getting overview of services. """ """ Handles getting overview of services. """
@ -483,9 +484,10 @@ class RequestHandler(SimpleHTTPRequestHandler):
domain = path_match.group('domain') domain = path_match.group('domain')
service = path_match.group('service') 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 # pylint: disable=invalid-name
def _handle_post_api_event_forward(self, path_match, data): def _handle_post_api_event_forward(self, path_match, data):
@ -495,26 +497,31 @@ class RequestHandler(SimpleHTTPRequestHandler):
host = data['host'] host = data['host']
api_password = data['api_password'] api_password = data['api_password']
except KeyError: except KeyError:
self._message("No host or api_password received.", self._json_message("No host or api_password received.",
HTTP_BAD_REQUEST) HTTP_BAD_REQUEST)
return return
try: try:
port = int(data['port']) if 'port' in data else None port = int(data['port']) if 'port' in data else None
except ValueError: except ValueError:
self._message( self._json_message(
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY) "Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
return 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: if self.server.event_forwarder is None:
self.server.event_forwarder = \ self.server.event_forwarder = \
rem.EventForwarder(self.server.hass) rem.EventForwarder(self.server.hass)
api = rem.API(host, api_password, port)
self.server.event_forwarder.connect(api) 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): def _handle_delete_api_event_forward(self, path_match, data):
""" Handles deleting an event forwarding target. """ """ Handles deleting an event forwarding target. """
@ -522,14 +529,14 @@ class RequestHandler(SimpleHTTPRequestHandler):
try: try:
host = data['host'] host = data['host']
except KeyError: except KeyError:
self._message("No host received.", self._json_message("No host received.",
HTTP_BAD_REQUEST) HTTP_BAD_REQUEST)
return return
try: try:
port = int(data['port']) if 'port' in data else None port = int(data['port']) if 'port' in data else None
except ValueError: except ValueError:
self._message( self._json_message(
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY) "Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
return return
@ -538,7 +545,7 @@ class RequestHandler(SimpleHTTPRequestHandler):
self.server.event_forwarder.disconnect(api) 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): def _handle_get_static(self, path_match, data):
""" Returns a static file. """ """ Returns a static file. """
@ -585,7 +592,10 @@ class RequestHandler(SimpleHTTPRequestHandler):
self.end_headers() self.end_headers()
if do_gzip: if self.command == 'HEAD':
return
elif do_gzip:
self.wfile.write(gzip_data) self.wfile.write(gzip_data)
else: else:
@ -599,22 +609,9 @@ class RequestHandler(SimpleHTTPRequestHandler):
if inp: if inp:
inp.close() 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. """ """ Helper method to return a message to the caller. """
if self.use_json: self._write_json({'message': message}, status_code=status_code)
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()
def _write_json(self, data=None, status_code=HTTP_OK, location=None): def _write_json(self, data=None, status_code=HTTP_OK, location=None):
""" Helper method to return JSON to the caller. """ """ Helper method to return JSON to the caller. """

View File

@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """ """ DO NOT MODIFY. Auto-generated by build_frontend script """
VERSION = "12ba7bca8ad0c196cb04ada4fe85a76b" VERSION = "78343829ea70bf07a9e939b321587122"

File diff suppressed because one or more lines are too long

View File

@ -11,27 +11,28 @@
"bower_components" "bower_components"
], ],
"dependencies": { "dependencies": {
"webcomponentsjs": "Polymer/webcomponentsjs#~0.5.1", "webcomponentsjs": "Polymer/webcomponentsjs#~0.5.2",
"font-roboto": "Polymer/font-roboto#~0.5.1", "font-roboto": "Polymer/font-roboto#~0.5.2",
"core-header-panel": "Polymer/core-header-panel#~0.5.1", "core-header-panel": "Polymer/core-header-panel#~0.5.2",
"core-toolbar": "Polymer/core-toolbar#~0.5.1", "core-toolbar": "Polymer/core-toolbar#~0.5.2",
"core-tooltip": "Polymer/core-tooltip#~0.5.1", "core-tooltip": "Polymer/core-tooltip#~0.5.2",
"core-menu": "Polymer/core-menu#~0.5.1", "core-menu": "Polymer/core-menu#~0.5.2",
"core-item": "Polymer/core-item#~0.5.1", "core-item": "Polymer/core-item#~0.5.2",
"core-input": "Polymer/core-input#~0.5.1", "core-input": "Polymer/core-input#~0.5.2",
"core-icons": "polymer/core-icons#~0.5.1", "core-icons": "polymer/core-icons#~0.5.2",
"core-image": "polymer/core-image#~0.5.1", "core-image": "polymer/core-image#~0.5.2",
"paper-toast": "Polymer/paper-toast#~0.5.1", "paper-toast": "Polymer/paper-toast#~0.5.2",
"paper-dialog": "Polymer/paper-dialog#~0.5.1", "paper-dialog": "Polymer/paper-dialog#~0.5.2",
"paper-spinner": "Polymer/paper-spinner#~0.5.1", "paper-spinner": "Polymer/paper-spinner#~0.5.2",
"paper-button": "Polymer/paper-button#~0.5.1", "paper-button": "Polymer/paper-button#~0.5.2",
"paper-input": "Polymer/paper-input#~0.5.1", "paper-input": "Polymer/paper-input#~0.5.2",
"paper-toggle-button": "polymer/paper-toggle-button#~0.5.1", "paper-toggle-button": "polymer/paper-toggle-button#~0.5.2",
"paper-tabs": "polymer/paper-tabs#~0.5.1", "paper-tabs": "polymer/paper-tabs#~0.5.2",
"paper-icon-button": "polymer/paper-icon-button#~0.5.1", "paper-icon-button": "polymer/paper-icon-button#~0.5.2",
"paper-menu-button": "polymer/paper-menu-button#~0.5.1", "paper-menu-button": "polymer/paper-menu-button#~0.5.2",
"paper-dropdown": "polymer/paper-dropdown#~0.5.1", "paper-dropdown": "polymer/paper-dropdown#~0.5.2",
"paper-item": "polymer/paper-item#~0.5.1", "paper-item": "polymer/paper-item#~0.5.2",
"moment": "~2.8.3" "moment": "~2.8.4",
"core-style": "polymer/core-style#~0.5.2"
} }
} }

View File

@ -28,23 +28,28 @@
return "image:flash-on"; return "image:flash-on";
case "chromecast": case "chromecast":
if(state && state != "idle") { var icon = "hardware:cast";
return "hardware:cast-connected";
} else { if (state !== "idle") {
return "hardware:cast"; icon += "-connected";
} }
return icon;
case "process": case "process":
return "hardware:memory" return "hardware:memory";
case "sun": case "sun":
return "image:wb-sunny" return "image:wb-sunny";
case "light": case "light":
return "image:wb-incandescent" return "image:wb-incandescent";
case "tellstick_sensor": case "tellstick_sensor":
return "trending-up"; return "trending-up";
case "simple_alarm":
return "social:notifications";
default: default:
return "bookmark-outline"; return "bookmark-outline";

View File

@ -35,7 +35,7 @@
}; };
Object.defineProperties(State.prototype, { Object.defineProperties(State.prototype, {
"stateDisplay": { stateDisplay: {
get: function() { get: function() {
var state = this.state.replace(/_/g, " "); var state = this.state.replace(/_/g, " ");
if(this.attributes.unit_of_measurement) { if(this.attributes.unit_of_measurement) {
@ -46,19 +46,30 @@
} }
}, },
"isCustomGroup": { isCustomGroup: {
get: function() { get: function() {
return this.domain == "group" && !this.attributes.auto; return this.domain == "group" && !this.attributes.auto;
} }
}, },
"canToggle": { canToggle: {
get: function() { get: function() {
// groups that have the on/off state or if there is a turn_on service // groups that have the on/off state or if there is a turn_on service
return ((this.domain == 'group' && return ((this.domain == 'group' &&
(this.state == 'on' || this.state == 'off')) || (this.state == 'on' || this.state == 'off')) ||
this.api.hasService(this.domain, 'turn_on')); this.api.hasService(this.domain, 'turn_on'));
} }
},
// how to render the card for this state
cardType: {
get: function() {
if(this.canToggle) {
return "toggle";
} else {
return "display";
}
}
} }
}); });
@ -69,10 +80,6 @@
events: [], events: [],
stateUpdateTimeout: null, stateUpdateTimeout: null,
computed: {
ha_headers: '{"HA-access": auth}'
},
created: function() { created: function() {
this.api = this; this.api = this;
@ -116,7 +123,7 @@
} else { } else {
return 0; return 0;
} }
}) });
}, },
_pushNewState: function(new_state) { _pushNewState: function(new_state) {
@ -140,7 +147,13 @@
this._sortStates(this.states); this._sortStates(this.states);
} }
this.fire('states-updated') this.fire('states-updated');
},
_pushNewStates: function(new_states) {
new_states.map(function(state) {
this._pushNewState(state);
}.bind(this));
}, },
// call api methods // call api methods
@ -153,7 +166,7 @@
fetchState: function(entityId) { fetchState: function(entityId) {
var successStateUpdate = function(new_state) { var successStateUpdate = function(new_state) {
this._pushNewState(new_state); this._pushNewState(new_state);
} };
this.call_api("GET", "states/" + entityId, null, successStateUpdate.bind(this)); this.call_api("GET", "states/" + entityId, null, successStateUpdate.bind(this));
}, },
@ -166,14 +179,14 @@
return new State(json, this); return new State(json, this);
}.bind(this)); }.bind(this));
this.fire('states-updated') this.fire('states-updated');
this._laterFetchStates(); this._laterFetchStates();
if(onSuccess) { if(onSuccess) {
onSuccess(this.states); onSuccess(this.states);
} }
} };
this.call_api( this.call_api(
"GET", "states", null, successStatesUpdate.bind(this), onError); "GET", "states", null, successStatesUpdate.bind(this), onError);
@ -183,12 +196,12 @@
var successEventsUpdated = function(events) { var successEventsUpdated = function(events) {
this.events = events; this.events = events;
this.fire('events-updated') this.fire('events-updated');
if(onSuccess) { if(onSuccess) {
onSuccess(events); onSuccess(events);
} }
} };
this.call_api( this.call_api(
"GET", "events", null, successEventsUpdated.bind(this), onError); "GET", "events", null, successEventsUpdated.bind(this), onError);
@ -198,27 +211,29 @@
var successServicesUpdated = function(services) { var successServicesUpdated = function(services) {
this.services = services; this.services = services;
this.fire('services-updated') this.fire('services-updated');
if(onSuccess) { if(onSuccess) {
onSuccess(this.services); onSuccess(this.services);
} }
} };
this.call_api( this.call_api(
"GET", "services", null, successServicesUpdated.bind(this), onError); "GET", "services", null, successServicesUpdated.bind(this), onError);
}, },
turn_on: function(entity_id) { turn_on: function(entity_id, options) {
this.call_service("homeassistant", "turn_on", {entity_id: entity_id}); this.call_service(
"homeassistant", "turn_on", {entity_id: entity_id}, options);
}, },
turn_off: function(entity_id) { turn_off: function(entity_id, options) {
this.call_service("homeassistant", "turn_off", {entity_id: entity_id}) this.call_service(
"homeassistant", "turn_off", {entity_id: entity_id}, options);
}, },
set_state: function(entity_id, state, attributes) { set_state: function(entity_id, state, attributes) {
var payload = {state: state} var payload = {state: state};
if(attributes) { if(attributes) {
payload.attributes = attributes; payload.attributes = attributes;
@ -227,16 +242,17 @@
var successToast = function(new_state) { var successToast = function(new_state) {
this.showToast("State of "+entity_id+" set to "+state+"."); this.showToast("State of "+entity_id+" set to "+state+".");
this._pushNewState(new_state); this._pushNewState(new_state);
} };
this.call_api("POST", "states/" + entity_id, this.call_api("POST", "states/" + entity_id,
payload, successToast.bind(this)); payload, successToast.bind(this));
}, },
call_service: function(domain, service, parameters) { call_service: function(domain, service, parameters, options) {
parameters = parameters || {}; parameters = parameters || {};
options = options || {};
var successToast = function() { var successHandler = function(changed_states) {
if(service == "turn_on" && parameters.entity_id) { if(service == "turn_on" && parameters.entity_id) {
this.showToast("Turned on " + parameters.entity_id + '.'); this.showToast("Turned on " + parameters.entity_id + '.');
} else if(service == "turn_off" && parameters.entity_id) { } else if(service == "turn_off" && parameters.entity_id) {
@ -245,30 +261,21 @@
this.showToast("Service "+domain+"/"+service+" called."); this.showToast("Service "+domain+"/"+service+" called.");
} }
// if we call a service on an entity_id, update the state this._pushNewStates(changed_states);
if(parameters && parameters.entity_id) {
var update_func;
// if entity_id is a string, update 1 state, else all. if(options.success) {
if(typeof(parameters.entity_id === "string")) { options.success();
// if it is a group, fetch all
if(parameters.entity_id.slice(0,6) == "group.") {
update_func = this.fetchStates
} else {
update_func = function() {
this.fetchState(parameters.entity_id);
}
}
} else {
update_func = this.fetchStates
}
setTimeout(update_func.bind(this), 1000);
} }
} };
var errorHandler = function(error_data) {
if(options.error) {
options.error(error_data);
}
};
this.call_api("POST", "services/" + domain + "/" + service, this.call_api("POST", "services/" + domain + "/" + service,
parameters, successToast.bind(this)); parameters, successHandler.bind(this), errorHandler);
}, },
fire_event: function(eventType, eventData) { fire_event: function(eventType, eventData) {
@ -276,16 +283,26 @@
var successToast = function() { var successToast = function() {
this.showToast("Event "+eventType+" fired."); this.showToast("Event "+eventType+" fired.");
} };
this.call_api("POST", "events/" + eventType, this.call_api("POST", "events/" + eventType,
eventData, successToast.bind(this)); eventData, successToast.bind(this));
}, },
call_api: function(method, path, parameters, onSuccess, onError) { call_api: function(method, path, parameters, onSuccess, onError) {
var url = "/api/" + path;
// set to true to generate a frontend to be used as demo on the website
if (false) {
if (path === "states" || path === "services" || path === "events") {
url = "/demo/" + path + ".json";
} else {
return;
}
}
var req = new XMLHttpRequest(); var req = new XMLHttpRequest();
req.open(method, "/api/" + path, true) req.open(method, url, true);
req.setRequestHeader("HA-access", this.auth); req.setRequestHeader("X-HA-access", this.auth);
req.onreadystatechange = function() { req.onreadystatechange = function() {
@ -303,7 +320,7 @@
} }
}.bind(this) }.bind(this);
if(parameters) { if(parameters) {
req.send(JSON.stringify(parameters)); req.send(JSON.stringify(parameters));
@ -316,7 +333,7 @@
showEditStateDialog: function(entityId) { showEditStateDialog: function(entityId) {
var state = this.getState(entityId); var state = this.getState(entityId);
this.showSetStateDialog(entityId, state.state, state.attributes) this.showSetStateDialog(entityId, state.state, state.attributes);
}, },
showSetStateDialog: function(entityId, state, stateAttributes) { showSetStateDialog: function(entityId, state, stateAttributes) {

View File

@ -31,6 +31,7 @@
/* Color the icon if light or sun is on */ /* Color the icon if light or sun is on */
domain-icon[data-domain=light][data-state=on], domain-icon[data-domain=light][data-state=on],
domain-icon[data-domain=switch][data-state=on],
domain-icon[data-domain=sun][data-state=above_horizon] { domain-icon[data-domain=sun][data-state=above_horizon] {
color: #fff176; color: #fff176;
} }

View File

@ -0,0 +1,30 @@
<script src="bower_components/moment/moment.js"></script>
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/core-style/core-style.html">
<link rel="import" href="state-info.html">
<polymer-element name="state-card-display"
attributes="stateObj cb_edit"
noscript>
<template>
<core-style ref='state-card'></core-style>
<style>
.state {
text-transform: capitalize;
font-weight: 300;
font-size: 1.3rem;
text-align: right;
}
</style>
<div horizontal justified layout>
<state-info
stateObj="{{stateObj}}"
cb_edit="{{cb_edit}}">
</state-info>
<div class='state'>{{stateObj.stateDisplay}}</div>
</div>
</template>
</polymer-element>

View File

@ -0,0 +1,117 @@
<script src="bower_components/moment/moment.js"></script>
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/paper-toggle-button/paper-toggle-button.html">
<link rel="import" href="bower_components/core-style/core-style.html">
<link rel="import" href="state-badge.html">
<polymer-element name="state-card-toggle"
attributes="stateObj cb_turn_on, cb_turn_off cb_edit">
<template>
<core-style ref='state-card'></core-style>
<style>
.state {
text-align: right;
}
/* the splash while enabling */
paper-toggle-button::shadow paper-radio-button::shadow #ink[checked] {
color: #0091ea;
}
/* filling of circle when checked */
paper-toggle-button::shadow paper-radio-button::shadow #onRadio {
background-color: #039be5;
}
/* line when checked */
paper-toggle-button::shadow #toggleBar[checked] {
background-color: #039be5;
}
</style>
<div horizontal justified layout>
<state-info
stateObj="{{stateObj}}"
cb_edit="{{cb_edit}}">
</state-info>
<div class='state toggle' self-center flex>
<paper-toggle-button checked="{{toggleChecked}}">
</paper-toggle-button>
</div>
</div>
</template>
<script>
Polymer({
stateObj: {},
cb_turn_on: null,
cb_turn_off: null,
cb_edit: null,
toggleChecked: -1,
observe: {
'stateObj.state': 'stateChanged'
},
lastChangedFromNow: function(lastChanged) {
return moment(lastChanged, "HH:mm:ss DD-MM-YYYY").fromNow();
},
toggleCheckedChanged: function(oldVal, newVal) {
// to filter out init
if(oldVal === -1) {
return;
}
if(newVal && this.stateObj.state == "off") {
this.turn_on();
} else if(!newVal && this.stateObj.state == "on") {
this.turn_off();
}
},
stateChanged: function(oldVal, newVal) {
this.toggleChecked = newVal === "on";
},
turn_on: function() {
// We call stateChanged after a successful call to re-sync the toggle
// with the state. It will be out of sync if our service call did not
// result in the entity to be turned on. Since the state is not changing,
// the resync is not called automatic.
if(this.cb_turn_on) {
this.cb_turn_on(this.stateObj.entity_id, {
success: function() {
this.stateChanged(this.stateObj.state, this.stateObj.state);
}.bind(this)
});
}
},
turn_off: function() {
// We call stateChanged after a successful call to re-sync the toggle
// with the state. It will be out of sync if our service call did not
// result in the entity to be turned on. Since the state is not changing,
// the resync is not called automatic.
if(this.cb_turn_off) {
this.cb_turn_off(this.stateObj.entity_id, {
success: function() {
this.stateChanged(this.stateObj.state, this.stateObj.state);
}.bind(this)
});
}
},
editClicked: function() {
if(this.cb_edit) {
this.cb_edit(this.stateObj.entity_id);
}
},
});
</script>
</polymer-element>

View File

@ -1,178 +0,0 @@
<script src="bower_components/moment/moment.js"></script>
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/core-tooltip/core-tooltip.html">
<link rel="import" href="bower_components/paper-button/paper-button.html">
<link rel="import" href="bower_components/paper-toggle-button/paper-toggle-button.html">
<link rel="import" href="state-badge.html">
<polymer-element name="state-card"
attributes="stateObj cb_turn_on, cb_turn_off cb_edit">
<template>
<style>
:host {
background-color: #fff;
border-radius: 2px;
box-shadow: rgba(0, 0, 0, 0.098) 0px 2px 4px, rgba(0, 0, 0, 0.098) 0px 0px 3px;
transition: all 0.30s ease-out;
position: relative;
background-color: white;
padding: 16px;
width: 100%;
}
state-badge {
float: left;
cursor: pointer;
}
state-badge:hover {
background-color: #039be5;
}
.name, .state.text {
text-transform: capitalize;
font-weight: 300;
font-size: 1.3rem;
}
.state {
text-align: right;
}
.info {
margin-left: 60px;
}
.time-ago {
color: darkgrey;
margin-top: -2px;
}
/* the splash while enabling */
paper-toggle-button::shadow paper-radio-button::shadow #ink[checked] {
color: #0091ea;
}
/* filling of circle when checked */
paper-toggle-button::shadow paper-radio-button::shadow #onRadio {
background-color: #039be5;
}
/* line when checked */
paper-toggle-button::shadow #toggleBar[checked] {
background-color: #039be5;
}
</style>
<div horizontal justified layout>
<div class="entity">
<state-badge
stateObj="{{stateObj}}"
on-click="{{editClicked}}">
</state-badge>
<div class='info'>
<div class='name'>
{{stateObj.entityDisplay}}
</div>
<div class="time-ago">
<core-tooltip label="{{stateObj.last_changed}}" position="bottom">
{{lastChangedFromNow(stateObj.last_changed)}}
</core-tooltip>
</div>
</div>
</div>
<template if="{{!stateUnknown}}">
<template if="{{stateObj.canToggle}}">
<div class='state toggle' self-center flex>
<paper-toggle-button checked="{{toggleChecked}}">
</paper-toggle-button>
</div>
</template>
<template if="{{!stateObj.canToggle}}">
<div class='state text'>{{stateObj.stateDisplay}}</div>
</template>
</template>
<template if="{{stateUnknown}}">
<div class="state" self-center flex>Updating..</div>
</template>
</div>
</template>
<script>
Polymer({
stateObj: {},
cb_turn_on: null,
cb_turn_off: null,
cb_edit: null,
stateUnknown: false,
toggleChecked: -1,
observe: {
'stateObj.state': 'stateChanged'
},
lastChangedFromNow: function(lastChanged) {
return moment(lastChanged, "HH:mm:ss DD-MM-YYYY").fromNow();
},
toggleCheckedChanged: function(oldVal, newVal) {
// to filter out init
if(oldVal === -1) {
return;
}
if(newVal && this.stateObj.state == "off") {
this.turn_on();
} else if(!newVal && this.stateObj.state == "on") {
this.turn_off();
}
},
stateChanged: function(oldVal, newVal) {
this.stateUnknown = newVal === null;
this.toggleChecked = newVal === "on";
},
turn_on: function() {
if(this.cb_turn_on) {
this.cb_turn_on(this.stateObj.entity_id);
// unset state while we wait for an update
var delayUnsetSate = function() {
this.stateObj.state = null;
};
setTimeout(delayUnsetSate.bind(this), 500);
}
},
turn_off: function() {
if(this.cb_turn_off) {
this.cb_turn_off(this.stateObj.entity_id);
// unset state while we wait for an update
var delayUnsetSate = function() {
this.stateObj.state = null;
};
setTimeout(delayUnsetSate.bind(this), 500);
}
},
editClicked: function() {
if(this.cb_edit) {
this.cb_edit(this.stateObj.entity_id);
}
},
});
</script>
</polymer-element>

View File

@ -0,0 +1,74 @@
<script src="bower_components/moment/moment.js"></script>
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/core-tooltip/core-tooltip.html">
<link rel="import" href="bower_components/core-style/core-style.html">
<link rel="import" href="state-badge.html">
<polymer-element name="state-info"
attributes="stateObj cb_edit">
<template>
<style>
state-badge {
float: left;
cursor: pointer;
}
state-badge:hover {
background-color: #039be5;
}
.name {
text-transform: capitalize;
font-weight: 300;
font-size: 1.3rem;
}
.info {
margin-left: 60px;
}
.time-ago {
color: darkgrey;
margin-top: -2px;
}
</style>
<div>
<state-badge
stateObj="{{stateObj}}"
on-click="{{editClicked}}">
</state-badge>
<div class='info'>
<div class='name'>
{{stateObj.entityDisplay}}
</div>
<div class="time-ago">
<core-tooltip label="{{stateObj.last_changed}}" position="bottom">
{{lastChangedFromNow(stateObj.last_changed)}}
</core-tooltip>
</div>
</div>
</div>
</template>
<script>
Polymer({
stateObj: {},
lastChangedFromNow: function(lastChanged) {
return moment(lastChanged, "HH:mm:ss DD-MM-YYYY").fromNow();
},
editClicked: function() {
if(this.cb_edit) {
this.cb_edit(this.stateObj.entity_id);
}
},
});
</script>
</polymer-element>

View File

@ -1,5 +1,7 @@
<link rel="import" href="bower_components/polymer/polymer.html"> <link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="state-card.html"> <link rel="import" href="bower_components/core-style/core-style.html">
<link rel="import" href="state-card-display.html">
<link rel="import" href="state-card-toggle.html">
<polymer-element name="states-cards" attributes="api filter"> <polymer-element name="states-cards" attributes="api filter">
<template> <template>
@ -9,45 +11,68 @@
width: 100%; width: 100%;
} }
state-card {
display: inline-block;
}
@media all and (min-width: 764px) { @media all and (min-width: 764px) {
:host { :host {
padding-bottom: 8px; padding-bottom: 8px;
} }
state-card { .state-card {
width: calc(50% - 44px); width: calc(50% - 44px);
margin: 8px 0 0 8px; margin: 8px 0 0 8px;
} }
} }
@media all and (min-width: 1100px) { @media all and (min-width: 1100px) {
state-card { .state-card {
width: calc(33% - 38px); width: calc(33% - 38px);
} }
} }
@media all and (min-width: 1450px) { @media all and (min-width: 1450px) {
state-card { .state-card {
width: calc(25% - 42px); width: calc(25% - 42px);
} }
} }
</style> </style>
<core-style id="state-card">
<!-- generic state card CSS -->
:host {
background-color: #fff;
border-radius: 2px;
box-shadow: rgba(0, 0, 0, 0.098) 0px 2px 4px, rgba(0, 0, 0, 0.098) 0px 0px 3px;
transition: all 0.30s ease-out;
position: relative;
background-color: white;
padding: 16px;
width: 100%;
}
</core-style>
<div horizontal layout wrap> <div horizontal layout wrap>
<template repeat="{{state in getStates(api.states, filter)}}">
<state-card <template id="display">
<state-card-display
class='state-card'
stateObj="{{state}}"
cb_edit={{editCallback}}>
</state-card-display>
</template>
<template id="toggle">
<state-card-toggle
class='state-card'
stateObj="{{state}}" stateObj="{{state}}"
cb_turn_on="{{api.turn_on}}" cb_turn_on="{{api.turn_on}}"
cb_turn_off="{{api.turn_off}}" cb_turn_off="{{api.turn_off}}"
cb_edit={{editCallback}}> cb_edit={{editCallback}}>
</state-card> </state-card-display>
</template>
<template repeat="{{state in getStates(api.states, filter)}}">
<template bind ref="{{state.cardType}}"></template>
</template> </template>
</div> </div>

File diff suppressed because one or more lines are too long

View File

@ -1,12 +1,16 @@
""" """
homeassistant.components.keyboard homeassistant.keyboard
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to emulate keyboard presses on host machine. Provides functionality to emulate keyboard presses on host machine.
""" """
import logging import logging
import homeassistant.components as components from homeassistant.const import (
SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK,
SERVICE_MEDIA_PLAY_PAUSE)
DOMAIN = "keyboard" DOMAIN = "keyboard"
DEPENDENCIES = [] DEPENDENCIES = []
@ -14,32 +18,32 @@ DEPENDENCIES = []
def volume_up(hass): def volume_up(hass):
""" Press the keyboard button for volume up. """ """ Press the keyboard button for volume up. """
hass.call_service(DOMAIN, components.SERVICE_VOLUME_UP) hass.services.call(DOMAIN, SERVICE_VOLUME_UP)
def volume_down(hass): def volume_down(hass):
""" Press the keyboard button for volume down. """ """ Press the keyboard button for volume down. """
hass.call_service(DOMAIN, components.SERVICE_VOLUME_DOWN) hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN)
def volume_mute(hass): def volume_mute(hass):
""" Press the keyboard button for muting volume. """ """ Press the keyboard button for muting volume. """
hass.call_service(DOMAIN, components.SERVICE_VOLUME_MUTE) hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE)
def media_play_pause(hass): def media_play_pause(hass):
""" Press the keyboard button for play/pause. """ """ Press the keyboard button for play/pause. """
hass.call_service(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE) hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE)
def media_next_track(hass): def media_next_track(hass):
""" Press the keyboard button for next track. """ """ Press the keyboard button for next track. """
hass.call_service(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK) hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK)
def media_prev_track(hass): def media_prev_track(hass):
""" Press the keyboard button for prev track. """ """ Press the keyboard button for prev track. """
hass.call_service(DOMAIN, components.SERVICE_MEDIA_PREV_TRACK) hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK)
# pylint: disable=unused-argument # pylint: disable=unused-argument
@ -56,27 +60,27 @@ def setup(hass, config):
keyboard = pykeyboard.PyKeyboard() keyboard = pykeyboard.PyKeyboard()
keyboard.special_key_assignment() keyboard.special_key_assignment()
hass.services.register(DOMAIN, components.SERVICE_VOLUME_UP, hass.services.register(DOMAIN, SERVICE_VOLUME_UP,
lambda service: lambda service:
keyboard.tap_key(keyboard.volume_up_key)) keyboard.tap_key(keyboard.volume_up_key))
hass.services.register(DOMAIN, components.SERVICE_VOLUME_DOWN, hass.services.register(DOMAIN, SERVICE_VOLUME_DOWN,
lambda service: lambda service:
keyboard.tap_key(keyboard.volume_down_key)) keyboard.tap_key(keyboard.volume_down_key))
hass.services.register(DOMAIN, components.SERVICE_VOLUME_MUTE, hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE,
lambda service: lambda service:
keyboard.tap_key(keyboard.volume_mute_key)) keyboard.tap_key(keyboard.volume_mute_key))
hass.services.register(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE, hass.services.register(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE,
lambda service: lambda service:
keyboard.tap_key(keyboard.media_play_pause_key)) keyboard.tap_key(keyboard.media_play_pause_key))
hass.services.register(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK, hass.services.register(DOMAIN, SERVICE_MEDIA_NEXT_TRACK,
lambda service: lambda service:
keyboard.tap_key(keyboard.media_next_track_key)) keyboard.tap_key(keyboard.media_next_track_key))
hass.services.register(DOMAIN, components.SERVICE_MEDIA_PREV_TRACK, hass.services.register(DOMAIN, SERVICE_MEDIA_PREV_TRACK,
lambda service: lambda service:
keyboard.tap_key(keyboard.media_prev_track_key)) keyboard.tap_key(keyboard.media_prev_track_key))

View File

@ -52,12 +52,12 @@ import logging
import os import os
import csv import csv
import homeassistant as ha
from homeassistant.loader import get_component
import homeassistant.util as util import homeassistant.util as util
from homeassistant.components import ( from homeassistant.const import (
group, extract_entity_ids, STATE_ON, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) from homeassistant.helpers import (
extract_entity_ids, platform_devices_from_config)
from homeassistant.components import group
DOMAIN = "light" DOMAIN = "light"
@ -82,6 +82,12 @@ ATTR_BRIGHTNESS = "brightness"
# String representing a profile (built-in ones or external defined) # String representing a profile (built-in ones or external defined)
ATTR_PROFILE = "profile" ATTR_PROFILE = "profile"
# If the light should flash, can be FLASH_SHORT or FLASH_LONG
ATTR_FLASH = "flash"
FLASH_SHORT = "short"
FLASH_LONG = "long"
LIGHT_PROFILES_FILE = "light_profiles.csv" LIGHT_PROFILES_FILE = "light_profiles.csv"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -96,51 +102,39 @@ def is_on(hass, entity_id=None):
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
def turn_on(hass, entity_id=None, transition=None, brightness=None, def turn_on(hass, entity_id=None, transition=None, brightness=None,
rgb_color=None, xy_color=None, profile=None): rgb_color=None, xy_color=None, profile=None, flash=None):
""" Turns all or specified light on. """ """ Turns all or specified light on. """
data = {} data = {
key: value for key, value in [
(ATTR_ENTITY_ID, entity_id),
(ATTR_PROFILE, profile),
(ATTR_TRANSITION, transition),
(ATTR_BRIGHTNESS, brightness),
(ATTR_RGB_COLOR, rgb_color),
(ATTR_XY_COLOR, xy_color),
(ATTR_FLASH, flash),
] if value is not None
}
if entity_id: hass.services.call(DOMAIN, SERVICE_TURN_ON, data)
data[ATTR_ENTITY_ID] = entity_id
if profile:
data[ATTR_PROFILE] = profile
if transition is not None:
data[ATTR_TRANSITION] = transition
if brightness is not None:
data[ATTR_BRIGHTNESS] = brightness
if rgb_color:
data[ATTR_RGB_COLOR] = rgb_color
if xy_color:
data[ATTR_XY_COLOR] = xy_color
hass.call_service(DOMAIN, SERVICE_TURN_ON, data)
def turn_off(hass, entity_id=None, transition=None): def turn_off(hass, entity_id=None, transition=None):
""" Turns all or specified light off. """ """ Turns all or specified light off. """
data = {} data = {
key: value for key, value in [
(ATTR_ENTITY_ID, entity_id),
(ATTR_TRANSITION, transition),
] if value is not None
}
if entity_id: hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
data[ATTR_ENTITY_ID] = entity_id
if transition is not None:
data[ATTR_TRANSITION] = transition
hass.call_service(DOMAIN, SERVICE_TURN_OFF, data)
# pylint: disable=too-many-branches, too-many-locals # pylint: disable=too-many-branches, too-many-locals
def setup(hass, config): def setup(hass, config):
""" Exposes light control via statemachine and services. """ """ Exposes light control via statemachine and services. """
if not util.validate_config(config, {DOMAIN: [ha.CONF_TYPE]}, _LOGGER):
return False
# Load built-in profiles and custom profiles # Load built-in profiles and custom profiles
profile_paths = [os.path.join(os.path.dirname(__file__), profile_paths = [os.path.join(os.path.dirname(__file__),
LIGHT_PROFILES_FILE), LIGHT_PROFILES_FILE),
@ -169,20 +163,9 @@ def setup(hass, config):
return False return False
# Load platform lights = platform_devices_from_config(config, DOMAIN, hass, _LOGGER)
light_type = config[DOMAIN][ha.CONF_TYPE]
light_init = get_component('light.{}'.format(light_type)) if not lights:
if light_init is None:
_LOGGER.error("Unknown light type specified: %s", light_type)
return False
lights = light_init.get_lights(hass, config[DOMAIN])
if len(lights) == 0:
_LOGGER.error("No lights found")
return False return False
ent_to_light = {} ent_to_light = {}
@ -198,7 +181,7 @@ def setup(hass, config):
entity_id = util.ensure_unique_string( entity_id = util.ensure_unique_string(
ENTITY_ID_FORMAT.format(util.slugify(name)), ENTITY_ID_FORMAT.format(util.slugify(name)),
list(ent_to_light.keys())) ent_to_light.keys())
light.entity_id = entity_id light.entity_id = entity_id
ent_to_light[entity_id] = light ent_to_light[entity_id] = light
@ -249,7 +232,6 @@ def setup(hass, config):
profile = profiles.get(dat.get(ATTR_PROFILE)) profile = profiles.get(dat.get(ATTR_PROFILE))
if profile: if profile:
# *color, bright = profile
*params[ATTR_XY_COLOR], params[ATTR_BRIGHTNESS] = profile *params[ATTR_XY_COLOR], params[ATTR_BRIGHTNESS] = profile
if ATTR_BRIGHTNESS in dat: if ATTR_BRIGHTNESS in dat:
@ -288,6 +270,13 @@ def setup(hass, config):
# ValueError if not all values can be converted to int # ValueError if not all values can be converted to int
pass pass
if ATTR_FLASH in dat:
if dat[ATTR_FLASH] == FLASH_SHORT:
params[ATTR_FLASH] = FLASH_SHORT
elif dat[ATTR_FLASH] == FLASH_LONG:
params[ATTR_FLASH] = FLASH_LONG
for light in lights: for light in lights:
# pylint: disable=star-args # pylint: disable=star-args
light.turn_on(**params) light.turn_on(**params)

View File

@ -1,19 +1,22 @@
""" Support for Hue lights. """ """ Support for Hue lights. """
import logging import logging
import socket import socket
from datetime import datetime, timedelta from datetime import timedelta
import homeassistant as ha import homeassistant.util as util
from homeassistant.components import ToggleDevice, ATTR_FRIENDLY_NAME from homeassistant.helpers import ToggleDevice
from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_HOST
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_TRANSITION) ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_TRANSITION,
ATTR_FLASH, FLASH_LONG, FLASH_SHORT)
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
PHUE_CONFIG_FILE = "phue.conf" PHUE_CONFIG_FILE = "phue.conf"
def get_lights(hass, config): def get_devices(hass, config):
""" Gets the Hue lights. """ """ Gets the Hue lights. """
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
try: try:
@ -23,7 +26,7 @@ def get_lights(hass, config):
return [] return []
host = config.get(ha.CONF_HOST, None) host = config.get(CONF_HOST, None)
try: try:
bridge = phue.Bridge( bridge = phue.Bridge(
@ -37,25 +40,9 @@ def get_lights(hass, config):
lights = {} lights = {}
def update_lights(force_reload=False): @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
""" Updates the light states. """ def update_lights():
now = datetime.now() """ Updates the Hue light objects with latest info from the bridge. """
try:
time_scans = now - update_lights.last_updated
# force_reload == True, return if updated in last second
# force_reload == False, return if last update was less then
# MIN_TIME_BETWEEN_SCANS ago
if force_reload and time_scans.seconds < 1 or \
not force_reload and time_scans < MIN_TIME_BETWEEN_SCANS:
return
except AttributeError:
# First time we run last_updated is not set, continue as usual
pass
update_lights.last_updated = now
try: try:
api = bridge.get_api() api = bridge.get_api()
except socket.error: except socket.error:
@ -109,6 +96,15 @@ class HueLight(ToggleDevice):
if ATTR_XY_COLOR in kwargs: if ATTR_XY_COLOR in kwargs:
command['xy'] = kwargs[ATTR_XY_COLOR] command['xy'] = kwargs[ATTR_XY_COLOR]
flash = kwargs.get(ATTR_FLASH)
if flash == FLASH_LONG:
command['alert'] = 'lselect'
elif flash == FLASH_SHORT:
command['alert'] = 'select'
else:
command['alert'] = 'none'
self.bridge.set_light(self.light_id, command) self.bridge.set_light(self.light_id, command)
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
@ -142,4 +138,4 @@ class HueLight(ToggleDevice):
def update(self): def update(self):
""" Synchronize state with bridge. """ """ Synchronize state with bridge. """
self.update_lights(True) self.update_lights(no_throttle=True)

View File

@ -10,7 +10,7 @@ Author: Markus Stenberg <fingon@iki.fi>
import os import os
from homeassistant.components import STATE_ON, STATE_OFF from homeassistant.const import STATE_ON, STATE_OFF
import homeassistant.util as util import homeassistant.util as util
DOMAIN = 'process' DOMAIN = 'process'

View File

@ -0,0 +1,97 @@
"""
homeassistant.components.simple_alarm
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides a simple alarm feature:
- flash a light when a known device comes home
- flash a light red if a light turns on while there is no one home.
"""
import logging
import homeassistant.loader as loader
from homeassistant.const import STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME
DOMAIN = "simple_alarm"
DEPENDENCIES = ['group', 'device_tracker', 'light']
# Attribute to tell which light has to flash whem a known person comes home
# If ommitted will flash all.
CONF_KNOWN_LIGHT = "known_light"
# Attribute to tell which light has to flash whem an unknown person comes home
# If ommitted will flash all.
CONF_UNKNOWN_LIGHT = "unknown_light"
# Services to test the alarms
SERVICE_TEST_KNOWN_ALARM = "test_known"
SERVICE_TEST_UNKNOWN_ALARM = "test_unknown"
def setup(hass, config):
""" Sets up the simple alarms. """
logger = logging.getLogger(__name__)
device_tracker = loader.get_component('device_tracker')
light = loader.get_component('light')
light_ids = []
for conf_key in (CONF_KNOWN_LIGHT, CONF_UNKNOWN_LIGHT):
light_id = config[DOMAIN].get(conf_key) or light.ENTITY_ID_ALL_LIGHTS
if hass.states.get(light_id) is None:
logger.error(
'Light id %s could not be found in state machine', light_id)
return False
else:
light_ids.append(light_id)
# pylint: disable=unbalanced-tuple-unpacking
known_light_id, unknown_light_id = light_ids
if hass.states.get(device_tracker.ENTITY_ID_ALL_DEVICES) is None:
logger.error('No devices are being tracked, cannot setup alarm')
return False
def known_alarm():
""" Fire an alarm if a known person arrives home. """
light.turn_on(hass, known_light_id, flash=light.FLASH_SHORT)
def unknown_alarm():
""" Fire an alarm if the light turns on while no one is home. """
light.turn_on(
hass, unknown_light_id,
flash=light.FLASH_LONG, rgb_color=[255, 0, 0])
# Setup services to test the effect
hass.services.register(
DOMAIN, SERVICE_TEST_KNOWN_ALARM, lambda call: known_alarm())
hass.services.register(
DOMAIN, SERVICE_TEST_UNKNOWN_ALARM, lambda call: unknown_alarm())
# pylint: disable=unused-argument
def unknown_alarm_if_lights_on(entity_id, old_state, new_state):
""" Called when a light has been turned on. """
if not device_tracker.is_on(hass):
unknown_alarm()
hass.states.track_change(
light.ENTITY_ID_ALL_LIGHTS,
unknown_alarm_if_lights_on, STATE_OFF, STATE_ON)
# pylint: disable=unused-argument
def ring_known_alarm(entity_id, old_state, new_state):
""" Called when a known person comes home. """
if light.is_on(hass, known_light_id):
known_alarm()
# Track home coming of each device
hass.states.track_change(
hass.states.entity_ids(device_tracker.DOMAIN),
ring_known_alarm, STATE_NOT_HOME, STATE_HOME)
return True

View File

@ -8,7 +8,9 @@ import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
import homeassistant as ha import homeassistant as ha
import homeassistant.util as util from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.helpers import validate_config
from homeassistant.util import str_to_datetime, datetime_to_str
DEPENDENCIES = [] DEPENDENCIES = []
DOMAIN = "sun" DOMAIN = "sun"
@ -35,7 +37,7 @@ def next_setting(hass, entity_id=None):
state = hass.states.get(ENTITY_ID) state = hass.states.get(ENTITY_ID)
try: try:
return util.str_to_datetime(state.attributes[STATE_ATTR_NEXT_SETTING]) return str_to_datetime(state.attributes[STATE_ATTR_NEXT_SETTING])
except (AttributeError, KeyError): except (AttributeError, KeyError):
# AttributeError if state is None # AttributeError if state is None
# KeyError if STATE_ATTR_NEXT_SETTING does not exist # KeyError if STATE_ATTR_NEXT_SETTING does not exist
@ -49,7 +51,7 @@ def next_rising(hass, entity_id=None):
state = hass.states.get(ENTITY_ID) state = hass.states.get(ENTITY_ID)
try: try:
return util.str_to_datetime(state.attributes[STATE_ATTR_NEXT_RISING]) return str_to_datetime(state.attributes[STATE_ATTR_NEXT_RISING])
except (AttributeError, KeyError): except (AttributeError, KeyError):
# AttributeError if state is None # AttributeError if state is None
# KeyError if STATE_ATTR_NEXT_RISING does not exist # KeyError if STATE_ATTR_NEXT_RISING does not exist
@ -60,10 +62,9 @@ def setup(hass, config):
""" Tracks the state of the sun. """ """ Tracks the state of the sun. """
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
if not util.validate_config(config, if not validate_config(config,
{ha.DOMAIN: [ha.CONF_LATITUDE, {ha.DOMAIN: [CONF_LATITUDE, CONF_LONGITUDE]},
ha.CONF_LONGITUDE]}, logger):
logger):
return False return False
try: try:
@ -74,8 +75,8 @@ def setup(hass, config):
sun = ephem.Sun() # pylint: disable=no-member sun = ephem.Sun() # pylint: disable=no-member
latitude = config[ha.DOMAIN][ha.CONF_LATITUDE] latitude = config[ha.DOMAIN][CONF_LATITUDE]
longitude = config[ha.DOMAIN][ha.CONF_LONGITUDE] longitude = config[ha.DOMAIN][CONF_LONGITUDE]
# Validate latitude and longitude # Validate latitude and longitude
observer = ephem.Observer() observer = ephem.Observer()
@ -123,8 +124,8 @@ def setup(hass, config):
new_state, next_change.strftime("%H:%M")) new_state, next_change.strftime("%H:%M"))
state_attributes = { state_attributes = {
STATE_ATTR_NEXT_RISING: util.datetime_to_str(next_rising_dt), STATE_ATTR_NEXT_RISING: datetime_to_str(next_rising_dt),
STATE_ATTR_NEXT_SETTING: util.datetime_to_str(next_setting_dt) STATE_ATTR_NEXT_SETTING: datetime_to_str(next_setting_dt)
} }
hass.states.set(ENTITY_ID, new_state, state_attributes) hass.states.set(ENTITY_ID, new_state, state_attributes)

View File

@ -4,14 +4,14 @@ homeassistant.components.switch
Component to interface with various switches that can be controlled remotely. Component to interface with various switches that can be controlled remotely.
""" """
import logging import logging
from datetime import datetime, timedelta from datetime import timedelta
import homeassistant as ha
import homeassistant.util as util import homeassistant.util as util
from homeassistant.loader import get_component from homeassistant.const import (
from homeassistant.components import ( STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
group, extract_entity_ids, STATE_ON, from homeassistant.helpers import (
SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) extract_entity_ids, platform_devices_from_config)
from homeassistant.components import group
DOMAIN = 'switch' DOMAIN = 'switch'
DEPENDENCIES = [] DEPENDENCIES = []
@ -22,10 +22,8 @@ ENTITY_ID_ALL_SWITCHES = group.ENTITY_ID_FORMAT.format(
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
ATTR_TODAY_KWH = "today_kwh" ATTR_TODAY_MWH = "today_mwh"
ATTR_CURRENT_POWER = "current_power" ATTR_CURRENT_POWER_MWH = "current_power_mwh"
ATTR_TODAY_ON_TIME = "today_on_time"
ATTR_TODAY_STANDBY_TIME = "today_standby_time"
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
@ -43,37 +41,23 @@ def turn_on(hass, entity_id=None):
""" Turns all or specified switch on. """ """ Turns all or specified switch on. """
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.call_service(DOMAIN, SERVICE_TURN_ON, data) hass.services.call(DOMAIN, SERVICE_TURN_ON, data)
def turn_off(hass, entity_id=None): def turn_off(hass, entity_id=None):
""" Turns all or specified switch off. """ """ Turns all or specified switch off. """
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.call_service(DOMAIN, SERVICE_TURN_OFF, data) hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
# pylint: disable=too-many-branches
def setup(hass, config): def setup(hass, config):
""" Track states and offer events for switches. """ """ Track states and offer events for switches. """
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
if not util.validate_config(config, {DOMAIN: [ha.CONF_TYPE]}, logger): switches = platform_devices_from_config(config, DOMAIN, hass, logger)
return False
switch_type = config[DOMAIN][ha.CONF_TYPE] if not switches:
switch_init = get_component('switch.{}'.format(switch_type))
if switch_init is None:
logger.error("Error loading switch component %s", switch_type)
return False
switches = switch_init.get_switches(hass, config[DOMAIN])
if len(switches) == 0:
logger.error("No switches found")
return False return False
# Setup a dict mapping entity IDs to devices # Setup a dict mapping entity IDs to devices
@ -90,27 +74,22 @@ def setup(hass, config):
entity_id = util.ensure_unique_string( entity_id = util.ensure_unique_string(
ENTITY_ID_FORMAT.format(util.slugify(name)), ENTITY_ID_FORMAT.format(util.slugify(name)),
list(ent_to_switch.keys())) ent_to_switch.keys())
switch.entity_id = entity_id switch.entity_id = entity_id
ent_to_switch[entity_id] = switch ent_to_switch[entity_id] = switch
# pylint: disable=unused-argument # pylint: disable=unused-argument
def update_states(time, force_reload=False): @util.Throttle(MIN_TIME_BETWEEN_SCANS)
def update_states(now):
""" Update states of all switches. """ """ Update states of all switches. """
# First time this method gets called, force_reload should be True logger.info("Updating switch states")
if force_reload or \
datetime.now() - update_states.last_updated > \
MIN_TIME_BETWEEN_SCANS:
logger.info("Updating switch states") for switch in switches:
update_states.last_updated = datetime.now() switch.update_ha_state(hass)
for switch in switches: update_states(None)
switch.update_ha_state(hass)
update_states(None, True)
def handle_switch_service(service): def handle_switch_service(service):
""" Handles calls to the switch services. """ """ Handles calls to the switch services. """

View File

@ -1,7 +1,8 @@
""" Support for Tellstick switches. """ """ Support for Tellstick switches. """
import logging import logging
from homeassistant.components import ToggleDevice, ATTR_FRIENDLY_NAME from homeassistant.helpers import ToggleDevice
from homeassistant.const import ATTR_FRIENDLY_NAME
try: try:
import tellcore.constants as tc_constants import tellcore.constants as tc_constants
@ -11,7 +12,7 @@ except ImportError:
# pylint: disable=unused-argument # pylint: disable=unused-argument
def get_switches(hass, config): def get_devices(hass, config):
""" Find and return Tellstick switches. """ """ Find and return Tellstick switches. """
try: try:
import tellcore.telldus as telldus import tellcore.telldus as telldus

View File

@ -1,12 +1,14 @@
""" Support for WeMo switchces. """ """ Support for WeMo switchces. """
import logging import logging
import homeassistant as ha from homeassistant.helpers import ToggleDevice
from homeassistant.components import ToggleDevice, ATTR_FRIENDLY_NAME from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_HOSTS
from homeassistant.components.switch import (
ATTR_TODAY_MWH, ATTR_CURRENT_POWER_MWH)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def get_switches(hass, config): def get_devices(hass, config):
""" Find and return WeMo switches. """ """ Find and return WeMo switches. """
try: try:
@ -21,9 +23,9 @@ def get_switches(hass, config):
return [] return []
if ha.CONF_HOSTS in config: if CONF_HOSTS in config:
switches = (pywemo.device_from_host(host) for host switches = (pywemo.device_from_host(host) for host
in config[ha.CONF_HOSTS].split(",")) in config[CONF_HOSTS].split(","))
else: else:
logging.getLogger(__name__).info("Scanning for WeMo devices") logging.getLogger(__name__).info("Scanning for WeMo devices")
@ -38,7 +40,6 @@ class WemoSwitch(ToggleDevice):
""" represents a WeMo switch within home assistant. """ """ represents a WeMo switch within home assistant. """
def __init__(self, wemo): def __init__(self, wemo):
self.wemo = wemo self.wemo = wemo
self.state_attr = {ATTR_FRIENDLY_NAME: wemo.name}
def get_name(self): def get_name(self):
""" Returns the name of the switch if any. """ """ Returns the name of the switch if any. """
@ -58,4 +59,13 @@ class WemoSwitch(ToggleDevice):
def get_state_attributes(self): def get_state_attributes(self):
""" Returns optional state attributes. """ """ Returns optional state attributes. """
return self.state_attr if self.wemo.model.startswith('Belkin Insight'):
cur_info = self.wemo.insight_params
return {
ATTR_FRIENDLY_NAME: self.wemo.name,
ATTR_CURRENT_POWER_MWH: cur_info['currentpower'],
ATTR_TODAY_MWH: cur_info['todaymw']
}
else:
return {ATTR_FRIENDLY_NAME: self.wemo.name}

View File

@ -26,8 +26,7 @@ import logging
from collections import namedtuple from collections import namedtuple
import homeassistant.util as util import homeassistant.util as util
from homeassistant.components import (ATTR_FRIENDLY_NAME, from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT
ATTR_UNIT_OF_MEASUREMENT)
# The domain of your component. Should be equal to the name of your component # The domain of your component. Should be equal to the name of your component
DOMAIN = "tellstick_sensor" DOMAIN = "tellstick_sensor"
@ -88,7 +87,7 @@ def setup(hass, config):
} }
def update_sensor_value_state(sensor_name, sensor_value): def update_sensor_value_state(sensor_name, sensor_value):
"Update the state of a sensor value" """ Update the state of a sensor value """
sensor_value_description = \ sensor_value_description = \
sensor_value_descriptions[sensor_value.datatype] sensor_value_descriptions[sensor_value.datatype]
sensor_value_name = '{} {}'.format( sensor_value_name = '{} {}'.format(
@ -117,7 +116,7 @@ def setup(hass, config):
] ]
def update_sensor_state(sensor): def update_sensor_state(sensor):
"Updates all the sensor values from the sensor" """ Updates all the sensor values from the sensor """
try: try:
sensor_name = config[DOMAIN][str(sensor.id)] sensor_name = config[DOMAIN][str(sensor.id)]
except KeyError: except KeyError:
@ -132,7 +131,7 @@ def setup(hass, config):
# pylint: disable=unused-argument # pylint: disable=unused-argument
def update_sensors_state(time): def update_sensors_state(time):
"Update the state of all sensors" """ Update the state of all sensors """
for sensor in sensors: for sensor in sensors:
update_sensor_state(sensor) update_sensor_state(sensor)

82
homeassistant/const.py Normal file
View File

@ -0,0 +1,82 @@
""" Constants used by Home Assistant components. """
# Can be used to specify a catch all when registering state or event listeners.
MATCH_ALL = '*'
# #### CONFIG ####
CONF_LATITUDE = "latitude"
CONF_LONGITUDE = "longitude"
# This one is deprecated. Use platform instead.
CONF_TYPE = "type"
CONF_PLATFORM = "platform"
CONF_HOST = "host"
CONF_HOSTS = "hosts"
CONF_USERNAME = "username"
CONF_PASSWORD = "password"
# #### EVENTS ####
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"
EVENT_SERVICE_EXECUTED = "service_executed"
# #### STATES ####
STATE_ON = 'on'
STATE_OFF = 'off'
STATE_HOME = 'home'
STATE_NOT_HOME = 'not_home'
# #### STATE AND EVENT ATTRIBUTES ####
# Contains current time for a TIME_CHANGED event
ATTR_NOW = "now"
# Contains domain, service for a SERVICE_CALL event
ATTR_DOMAIN = "domain"
ATTR_SERVICE = "service"
# Data for a SERVICE_EXECUTED event
ATTR_SERVICE_CALL_ID = "service_call_id"
# 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"
# #### SERVICES ####
SERVICE_HOMEASSISTANT_STOP = "stop"
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"
# #### API / REMOTE ####
SERVER_PORT = 8123
AUTH_HEADER = "X-HA-access"
URL_API = "/api/"
URL_API_STATES = "/api/states"
URL_API_STATES_ENTITY = "/api/states/{}"
URL_API_EVENTS = "/api/events"
URL_API_EVENTS_EVENT = "/api/events/{}"
URL_API_SERVICES = "/api/services"
URL_API_SERVICES_SERVICE = "/api/services/{}/{}"
URL_API_EVENT_FORWARD = "/api/event_forwarding"

@ -1 +1 @@
Subproject commit 10e7d7ba12b2326d69e3afe335d663b236790d3d Subproject commit b86d410cd67ea1e3a60355aa23d17fe6761cb8c5

197
homeassistant/helpers.py Normal file
View File

@ -0,0 +1,197 @@
"""
Helper methods for components within Home Assistant.
"""
from datetime import datetime
from homeassistant import NoEntitySpecifiedError
from homeassistant.loader import get_component
from homeassistant.const import (
ATTR_ENTITY_ID, STATE_ON, STATE_OFF, CONF_PLATFORM, CONF_TYPE)
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
# pylint: disable=too-few-public-methods, attribute-defined-outside-init
class TrackStates(object):
"""
Records the time when the with-block is entered. Will add all states
that have changed since the start time to the return list when with-block
is exited.
"""
def __init__(self, hass):
self.hass = hass
self.states = []
def __enter__(self):
self.now = datetime.now()
return self.states
def __exit__(self, exc_type, exc_value, traceback):
self.states.extend(self.hass.states.get_since(self.now))
def validate_config(config, items, logger):
"""
Validates if all items are available in the configuration.
config is the general dictionary with all the configurations.
items is a dict with per domain which attributes we require.
logger is the logger from the caller to log the errors to.
Returns True if all required items were found.
"""
errors_found = False
for domain in items.keys():
config.setdefault(domain, {})
errors = [item for item in items[domain] if item not in config[domain]]
if errors:
logger.error(
"Missing required configuration items in {}: {}".format(
domain, ", ".join(errors)))
errors_found = True
return not errors_found
def config_per_platform(config, domain, logger):
"""
Generator to break a component config into different platforms.
For example, will find 'switch', 'switch 2', 'switch 3', .. etc
"""
config_key = domain
found = 1
while config_key in config:
platform_config = config[config_key]
platform_type = platform_config.get(CONF_PLATFORM)
# DEPRECATED, still supported for now.
if platform_type is None:
platform_type = platform_config.get(CONF_TYPE)
if platform_type is not None:
logger.warning((
'Please update your config for {}.{} to use "platform" '
'instead of "type"').format(domain, platform_type))
if platform_type is None:
logger.warning('No platform specified for %s', config_key)
break
yield platform_type, platform_config
found += 1
config_key = "{} {}".format(domain, found)
def platform_devices_from_config(config, domain, hass, logger):
""" Parses the config for specified domain.
Loads different platforms and retrieve domains. """
devices = []
for p_type, p_config in config_per_platform(config, domain, logger):
platform = get_component('{}.{}'.format(domain, p_type))
if platform is None:
logger.error("Unknown %s type specified: %s", domain, p_type)
else:
try:
p_devices = platform.get_devices(hass, p_config)
except AttributeError:
# DEPRECATED, still supported for now
logger.warning(
'Platform %s should migrate to use the method get_devices',
p_type)
if domain == 'light':
p_devices = platform.get_lights(hass, p_config)
elif domain == 'switch':
p_devices = platform.get_switches(hass, p_config)
else:
raise
logger.info("Found %d %s %ss", len(p_devices), p_type, domain)
devices.extend(p_devices)
if len(devices) == 0:
logger.error("No devices found for %s", domain)
return devices
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 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())

View File

@ -19,6 +19,10 @@ import pkgutil
import importlib import importlib
import logging import logging
from homeassistant.util import OrderedSet
PREPARED = False
# List of available components # List of available components
AVAILABLE_COMPONENTS = [] AVAILABLE_COMPONENTS = []
@ -30,6 +34,8 @@ _LOGGER = logging.getLogger(__name__)
def prepare(hass): def prepare(hass):
""" Prepares the loading of components. """ """ Prepares the loading of components. """
global PREPARED # pylint: disable=global-statement
# Load the built-in components # Load the built-in components
import homeassistant.components as components import homeassistant.components as components
@ -56,15 +62,22 @@ def prepare(hass):
# just might output more errors. # just might output more errors.
for fil in os.listdir(custom_path): for fil in os.listdir(custom_path):
if os.path.isdir(os.path.join(custom_path, fil)): if os.path.isdir(os.path.join(custom_path, fil)):
AVAILABLE_COMPONENTS.append('custom_components.{}'.format(fil)) if fil != '__pycache__':
AVAILABLE_COMPONENTS.append(
'custom_components.{}'.format(fil))
else: else:
# For files we will strip out .py extension
AVAILABLE_COMPONENTS.append( AVAILABLE_COMPONENTS.append(
'custom_components.{}'.format(fil[0:-3])) 'custom_components.{}'.format(fil[0:-3]))
PREPARED = True
def set_component(comp_name, component): def set_component(comp_name, component):
""" Sets a component in the cache. """ """ Sets a component in the cache. """
_check_prepared()
_COMPONENT_CACHE[comp_name] = component _COMPONENT_CACHE[comp_name] = component
@ -76,6 +89,8 @@ def get_component(comp_name):
if comp_name in _COMPONENT_CACHE: if comp_name in _COMPONENT_CACHE:
return _COMPONENT_CACHE[comp_name] return _COMPONENT_CACHE[comp_name]
_check_prepared()
# If we ie. try to load custom_components.switch.wemo but the parent # If we ie. try to load custom_components.switch.wemo but the parent
# custom_components.switch does not exist, importing it will trigger # custom_components.switch does not exist, importing it will trigger
# an exception because it will try to import the parent. # an exception because it will try to import the parent.
@ -125,3 +140,89 @@ def get_component(comp_name):
_LOGGER.error("Unable to find component %s", comp_name) _LOGGER.error("Unable to find component %s", comp_name)
return None return None
def load_order_components(components):
"""
Takes in a list of components we want to load:
- filters out components we cannot load
- filters out components that have invalid/circular dependencies
- Will ensure that all components that do not directly depend on
the group component will be loaded before the group component.
- returns an OrderedSet load order.
"""
_check_prepared()
group = get_component('group')
load_order = OrderedSet()
# Sort the list of modules on if they depend on group component or not.
# We do this because the components that do not depend on the group
# component usually set up states that the group component requires to be
# created before it can group them.
# This does not matter in the future if we can setup groups without the
# states existing yet.
for comp_load_order in sorted((load_order_component(component)
for component in components),
# Test if group component exists in case
# above get_component call had an error.
key=lambda order:
group and group.DOMAIN in order):
load_order.update(comp_load_order)
return load_order
def load_order_component(comp_name):
"""
Returns an OrderedSet of components in the correct order of loading.
Raises HomeAssistantError if a circular dependency is detected.
Returns an empty list if component could not be loaded.
"""
return _load_order_component(comp_name, OrderedSet(), set())
def _load_order_component(comp_name, load_order, loading):
""" Recursive function to get load order of components. """
component = get_component(comp_name)
# if None it does not exist, error already thrown by get_component
if component is None:
return OrderedSet()
loading.add(comp_name)
for dependency in component.DEPENDENCIES:
# Check not already loaded
if dependency not in load_order:
# If we are already loading it, we have a circular dependency
if dependency in loading:
_LOGGER.error('Circular dependency detected: %s -> %s',
comp_name, dependency)
return OrderedSet()
dep_load_order = _load_order_component(
dependency, load_order, loading)
# length == 0 means error loading dependency or children
if len(dep_load_order) == 0:
_LOGGER.error('Error loading %s dependency: %s',
comp_name, dependency)
return OrderedSet()
load_order.update(dep_load_order)
load_order.add(comp_name)
loading.remove(comp_name)
return load_order
def _check_prepared():
""" Issues a warning if loader.prepare() has never been called. """
if not PREPARED:
_LOGGER.warning((
"You did not call loader.prepare() yet. "
"Certain functionality might not be working."))

View File

@ -19,21 +19,14 @@ import requests
import homeassistant as ha import homeassistant as ha
SERVER_PORT = 8123 from homeassistant.const import (
SERVER_PORT, AUTH_HEADER, URL_API, URL_API_STATES, URL_API_STATES_ENTITY,
AUTH_HEADER = "HA-access" URL_API_EVENTS, URL_API_EVENTS_EVENT, URL_API_SERVICES,
URL_API_SERVICES_SERVICE, URL_API_EVENT_FORWARD)
URL_API = "/api/"
URL_API_STATES = "/api/states"
URL_API_STATES_ENTITY = "/api/states/{}"
URL_API_EVENTS = "/api/events"
URL_API_EVENTS_EVENT = "/api/events/{}"
URL_API_SERVICES = "/api/services"
URL_API_SERVICES_SERVICE = "/api/services/{}/{}"
URL_API_EVENT_FORWARD = "/api/event_forwarding"
METHOD_GET = "get" METHOD_GET = "get"
METHOD_POST = "post" METHOD_POST = "post"
METHOD_DELETE = "delete"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -94,6 +87,10 @@ class API(object):
_LOGGER.exception(error) _LOGGER.exception(error)
raise ha.HomeAssistantError(error) raise ha.HomeAssistantError(error)
def __repr__(self):
return "API({}, {}, {})".format(
self.host, self.api_password, self.port)
class HomeAssistant(ha.HomeAssistant): class HomeAssistant(ha.HomeAssistant):
""" Home Assistant that forwards work. """ """ Home Assistant that forwards work. """
@ -122,18 +119,23 @@ class HomeAssistant(ha.HomeAssistant):
import random import random
# pylint: disable=too-many-format-args # pylint: disable=too-many-format-args
random_password = '%030x'.format(random.randrange(16**30)) random_password = '{:30}'.format(random.randrange(16**30))
http.setup(self, random_password) http.setup(
self, {http.DOMAIN: {http.CONF_API_PASSWORD: random_password}})
ha.Timer(self) ha.Timer(self)
# Setup that events from remote_api get forwarded to local_api
connect_remote_events(self.remote_api, self.local_api)
self.bus.fire(ha.EVENT_HOMEASSISTANT_START, self.bus.fire(ha.EVENT_HOMEASSISTANT_START,
origin=ha.EventOrigin.remote) origin=ha.EventOrigin.remote)
# Setup that events from remote_api get forwarded to local_api
# Do this after we fire START, otherwise HTTP is not started
if not connect_remote_events(self.remote_api, self.local_api):
raise ha.HomeAssistantError((
'Could not setup event forwarding from api {} to '
'local api {}').format(self.remote_api, self.local_api))
def stop(self): def stop(self):
""" Stops Home Assistant and shuts down all threads. """ """ Stops Home Assistant and shuts down all threads. """
_LOGGER.info("Stopping") _LOGGER.info("Stopping")
@ -141,6 +143,9 @@ class HomeAssistant(ha.HomeAssistant):
self.bus.fire(ha.EVENT_HOMEASSISTANT_STOP, self.bus.fire(ha.EVENT_HOMEASSISTANT_STOP,
origin=ha.EventOrigin.remote) origin=ha.EventOrigin.remote)
# Disconnect master event forwarding
disconnect_remote_events(self.remote_api, self.local_api)
# Wait till all responses to homeassistant_stop are done # Wait till all responses to homeassistant_stop are done
self._pool.block_till_done() self._pool.block_till_done()
@ -285,30 +290,51 @@ def validate_api(api):
def connect_remote_events(from_api, to_api): def connect_remote_events(from_api, to_api):
""" Sets up from_api to forward all events to to_api. """ """ Sets up from_api to forward all events to to_api. """
data = {'host': to_api.host, 'api_password': to_api.api_password} data = {
'host': to_api.host,
if to_api.port is not None: 'api_password': to_api.api_password,
data['port'] = to_api.port 'port': to_api.port
}
try: try:
from_api(METHOD_POST, URL_API_EVENT_FORWARD, data) req = from_api(METHOD_POST, URL_API_EVENT_FORWARD, data)
if req.status_code == 200:
return True
else:
_LOGGER.error(
"Error settign up event forwarding: %s - %s",
req.status_code, req.text)
return False
except ha.HomeAssistantError: except ha.HomeAssistantError:
pass _LOGGER.exception("Error setting up event forwarding")
return False
def disconnect_remote_events(from_api, to_api): def disconnect_remote_events(from_api, to_api):
""" Disconnects forwarding events from from_api to to_api. """ """ Disconnects forwarding events from from_api to to_api. """
data = {'host': to_api.host, '_METHOD': 'DELETE'} data = {
'host': to_api.host,
if to_api.port is not None: 'port': to_api.port
data['port'] = to_api.port }
try: try:
from_api(METHOD_POST, URL_API_EVENT_FORWARD, data) req = from_api(METHOD_DELETE, URL_API_EVENT_FORWARD, data)
if req.status_code == 200:
return True
else:
_LOGGER.error(
"Error removing event forwarding: %s - %s",
req.status_code, req.text)
return False
except ha.HomeAssistantError: except ha.HomeAssistantError:
pass _LOGGER.exception("Error removing an event forwarder")
return False
def get_event_listeners(api): def get_event_listeners(api):
@ -336,7 +362,7 @@ def fire_event(api, event_type, data=None):
req.status_code, req.text) req.status_code, req.text)
except ha.HomeAssistantError: except ha.HomeAssistantError:
pass _LOGGER.exception("Error firing event")
def get_state(api, entity_id): def get_state(api, entity_id):
@ -372,7 +398,7 @@ def get_states(api):
# ValueError if req.json() can't parse the json # ValueError if req.json() can't parse the json
_LOGGER.exception("Error fetching states") _LOGGER.exception("Error fetching states")
return {} return []
def set_state(api, entity_id, new_state, attributes=None): def set_state(api, entity_id, new_state, attributes=None):

View File

@ -4,12 +4,15 @@ homeassistant.util
Helper methods for various modules. Helper methods for various modules.
""" """
import collections
from itertools import chain
import threading import threading
import queue import queue
import datetime from datetime import datetime, timedelta
import re import re
import enum import enum
import socket import socket
from functools import wraps
RE_SANITIZE_FILENAME = re.compile(r'(~|\.\.|/|\\)') RE_SANITIZE_FILENAME = re.compile(r'(~|\.\.|/|\\)')
RE_SANITIZE_PATH = re.compile(r'(~|\.(\.)+)') RE_SANITIZE_PATH = re.compile(r'(~|\.(\.)+)')
@ -49,11 +52,19 @@ def str_to_datetime(dt_str):
@rtype: datetime @rtype: datetime
""" """
try: try:
return datetime.datetime.strptime(dt_str, DATE_STR_FORMAT) return datetime.strptime(dt_str, DATE_STR_FORMAT)
except ValueError: # If dt_str did not match our format except ValueError: # If dt_str did not match our format
return None return None
def strip_microseconds(dattim):
""" Returns a copy of dattime object but with microsecond set to 0. """
if dattim.microsecond:
return dattim - timedelta(microseconds=dattim.microsecond)
else:
return dattim
def split_entity_id(entity_id): def split_entity_id(entity_id):
""" Splits a state entity_id into domain, object_id. """ """ Splits a state entity_id into domain, object_id. """
return entity_id.split(".", 1) return entity_id.split(".", 1)
@ -65,7 +76,7 @@ def repr_helper(inp):
return ", ".join( return ", ".join(
repr_helper(key)+"="+repr_helper(item) for key, item repr_helper(key)+"="+repr_helper(item) for key, item
in inp.items()) in inp.items())
elif isinstance(inp, datetime.datetime): elif isinstance(inp, datetime):
return datetime_to_str(inp) return datetime_to_str(inp)
else: else:
return str(inp) return str(inp)
@ -124,6 +135,7 @@ def ensure_unique_string(preferred_string, current_strings):
""" Returns a string that is not present in current_strings. """ Returns a string that is not present in current_strings.
If preferred string exists will append _2, _3, .. """ If preferred string exists will append _2, _3, .. """
string = preferred_string string = preferred_string
current_strings = list(current_strings)
tries = 1 tries = 1
@ -176,93 +188,199 @@ class OrderedEnum(enum.Enum):
return NotImplemented return NotImplemented
def validate_config(config, items, logger): class OrderedSet(collections.MutableSet):
""" Ordered set taken from http://code.activestate.com/recipes/576694/ """
def __init__(self, iterable=None):
self.end = end = []
end += [None, end, end] # sentinel node for doubly linked list
self.map = {} # key --> [key, prev, next]
if iterable is not None:
self |= iterable
def __len__(self):
return len(self.map)
def __contains__(self, key):
return key in self.map
def add(self, key):
""" Add an element to the set. """
if key not in self.map:
end = self.end
curr = end[1]
curr[2] = end[1] = self.map[key] = [key, curr, end]
def discard(self, key):
""" Discard an element from the set. """
if key in self.map:
key, prev_item, next_item = self.map.pop(key)
prev_item[2] = next_item
next_item[1] = prev_item
def __iter__(self):
end = self.end
curr = end[2]
while curr is not end:
yield curr[0]
curr = curr[2]
def __reversed__(self):
end = self.end
curr = end[1]
while curr is not end:
yield curr[0]
curr = curr[1]
def pop(self, last=True): # pylint: disable=arguments-differ
""" Pops element of the end of the set.
Set last=False to pop from the beginning. """
if not self:
raise KeyError('set is empty')
key = self.end[1][0] if last else self.end[2][0]
self.discard(key)
return key
def update(self, *args):
""" Add elements from args to the set. """
for item in chain(*args):
self.add(item)
def __repr__(self):
if not self:
return '%s()' % (self.__class__.__name__,)
return '%s(%r)' % (self.__class__.__name__, list(self))
def __eq__(self, other):
if isinstance(other, OrderedSet):
return len(self) == len(other) and list(self) == list(other)
return set(self) == set(other)
class Throttle(object):
""" """
Validates if all items are available in the configuration. A method decorator to add a cooldown to a method to prevent it from being
called more then 1 time within the timedelta interval `min_time` after it
returned its result.
config is the general dictionary with all the configurations. Calling a method a second time during the interval will return None.
items is a dict with per domain which attributes we require.
logger is the logger from the caller to log the errors to.
Returns True if all required items were found. Pass keyword argument `no_throttle=True` to the wrapped method to make
the call not throttled.
Decorator takes in an optional second timedelta interval to throttle the
'no_throttle' calls.
Adds a datetime attribute `last_call` to the method.
""" """
errors_found = False # pylint: disable=too-few-public-methods
for domain in items.keys():
config.setdefault(domain, {})
errors = [item for item in items[domain] if item not in config[domain]] def __init__(self, min_time, limit_no_throttle=None):
self.min_time = min_time
self.limit_no_throttle = limit_no_throttle
if errors: def __call__(self, method):
logger.error( lock = threading.Lock()
"Missing required configuration items in {}: {}".format(
domain, ", ".join(errors)))
errors_found = True if self.limit_no_throttle is not None:
method = Throttle(self.limit_no_throttle)(method)
return not errors_found @wraps(method)
def wrapper(*args, **kwargs):
"""
Wrapper that allows wrapped to be called only once per min_time.
"""
with lock:
last_call = wrapper.last_call
# Check if method is never called or no_throttle is given
force = last_call is None or kwargs.pop('no_throttle', False)
if force or datetime.now() - last_call > self.min_time:
result = method(*args, **kwargs)
wrapper.last_call = datetime.now()
return result
else:
return None
wrapper.last_call = None
return wrapper
# Reason why I decided to roll my own ThreadPool instead of using
# multiprocessing.dummy.pool or even better, use multiprocessing.pool and
# not be hurt by the GIL in the cpython interpreter:
# 1. The built in threadpool does not allow me to create custom workers and so
# I would have to wrap every listener that I passed into it with code to log
# the exceptions. Saving a reference to the logger in the worker seemed
# like a more sane thing to do.
# 2. Most event listeners are simple checks if attributes match. If the method
# that they will call takes a long time to complete it might be better to
# put that request in a seperate thread. This is for every component to
# decide on its own instead of enforcing it for everyone.
class ThreadPool(object): class ThreadPool(object):
""" A simple queue-based thread pool. """ A priority queue-based thread pool. """
Will initiate it's workers using worker(queue).start() """
# pylint: disable=too-many-instance-attributes # pylint: disable=too-many-instance-attributes
def __init__(self, worker_count, job_handler, busy_callback=None): def __init__(self, job_handler, worker_count=0, busy_callback=None):
""" """
worker_count: number of threads to run that handle jobs
job_handler: method to be called from worker thread to handle job job_handler: method to be called from worker thread to handle job
worker_count: number of threads to run that handle jobs
busy_callback: method to be called when queue gets too big. busy_callback: method to be called when queue gets too big.
Parameters: list_of_current_jobs, number_pending_jobs Parameters: worker_count, list of current_jobs,
pending_jobs_count
""" """
self.work_queue = work_queue = queue.PriorityQueue() self._job_handler = job_handler
self.current_jobs = current_jobs = [] self._busy_callback = busy_callback
self.worker_count = worker_count
self.busy_callback = busy_callback self.worker_count = 0
self.busy_warning_limit = worker_count**2 self.busy_warning_limit = 0
self._work_queue = queue.PriorityQueue()
self.current_jobs = []
self._lock = threading.RLock() self._lock = threading.RLock()
self._quit_task = object() self._quit_task = object()
self.running = True
for _ in range(worker_count): for _ in range(worker_count):
worker = threading.Thread(target=_threadpool_worker, self.add_worker()
args=(work_queue, current_jobs,
job_handler, self._quit_task)) def add_worker(self):
""" Adds a worker to the thread pool. Resets warning limit. """
with self._lock:
if not self.running:
raise RuntimeError("ThreadPool not running")
worker = threading.Thread(target=self._worker)
worker.daemon = True worker.daemon = True
worker.start() worker.start()
self.running = True self.worker_count += 1
self.busy_warning_limit = self.worker_count * 3
def add_job(self, priority, job): def remove_worker(self):
""" Add a job to be sent to the workers. """ """ Removes a worker from the thread pool. Resets warning limit. """
with self._lock: with self._lock:
if not self.running: if not self.running:
raise Exception("We are shutting down the ") raise RuntimeError("ThreadPool not running")
self.work_queue.put(PriorityQueueItem(priority, job)) self._work_queue.put(PriorityQueueItem(0, self._quit_task))
self.worker_count -= 1
self.busy_warning_limit = self.worker_count * 3
def add_job(self, priority, job):
""" Add a job to the queue. """
with self._lock:
if not self.running:
raise RuntimeError("ThreadPool not running")
self._work_queue.put(PriorityQueueItem(priority, job))
# check if our queue is getting too big # check if our queue is getting too big
if self.work_queue.qsize() > self.busy_warning_limit \ if self._work_queue.qsize() > self.busy_warning_limit \
and self.busy_callback is not None: and self._busy_callback is not None:
# Increase limit we will issue next warning # Increase limit we will issue next warning
self.busy_warning_limit *= 2 self.busy_warning_limit *= 2
self.busy_callback(self.current_jobs, self.work_queue.qsize()) self._busy_callback(
self.worker_count, self.current_jobs,
self._work_queue.qsize())
def block_till_done(self): def block_till_done(self):
""" Blocks till all work is done. """ """ Blocks till all work is done. """
self.work_queue.join() self._work_queue.join()
def stop(self): def stop(self):
""" Stops all the threads. """ """ Stops all the threads. """
@ -270,19 +388,41 @@ class ThreadPool(object):
if not self.running: if not self.running:
return return
# Clear the queue # Ensure all current jobs finish
while self.work_queue.qsize() > 0: self.block_till_done()
self.work_queue.get()
self.work_queue.task_done()
# Tell the workers to quit # Tell the workers to quit
for _ in range(self.worker_count): for _ in range(self.worker_count):
self.add_job(1000, self._quit_task) self.remove_worker()
self.running = False self.running = False
# Wait till all workers have quit
self.block_till_done() self.block_till_done()
def _worker(self):
""" Handles jobs for the thread pool. """
while True:
# Get new item from work_queue
job = self._work_queue.get().item
if job == self._quit_task:
self._work_queue.task_done()
return
# Add to current running jobs
job_log = (datetime.now(), job)
self.current_jobs.append(job_log)
# Do the job
self._job_handler(job)
# Remove from current running job
self.current_jobs.remove(job_log)
# Tell work_queue the task is done
self._work_queue.task_done()
class PriorityQueueItem(object): class PriorityQueueItem(object):
""" Holds a priority and a value. Used within PriorityQueue. """ """ Holds a priority and a value. Used within PriorityQueue. """
@ -294,27 +434,3 @@ class PriorityQueueItem(object):
def __lt__(self, other): def __lt__(self, other):
return self.priority < other.priority return self.priority < other.priority
def _threadpool_worker(work_queue, current_jobs, job_handler, quit_task):
""" Provides the base functionality of a worker for the thread pool. """
while True:
# Get new item from work_queue
job = work_queue.get().item
if job == quit_task:
work_queue.task_done()
return
# Add to current running jobs
job_log = (datetime.datetime.now(), job)
current_jobs.append(job_log)
# Do the job
job_handler(job)
# Remove from current running job
current_jobs.remove(job_log)
# Tell work_queue a task is done
work_queue.task_done()

View File

@ -17,3 +17,6 @@ pyuserinput>=0.1.9
# switch.tellstick, tellstick_sensor # switch.tellstick, tellstick_sensor
tellcore-py>=1.0.4 tellcore-py>=1.0.4
# namp_tracker plugin
python-libnmap

View File

@ -2,4 +2,4 @@
pylint homeassistant pylint homeassistant
flake8 homeassistant --exclude bower_components,external flake8 homeassistant --exclude bower_components,external
python3 -m unittest discover test python3 -m unittest discover ha_test

View File

@ -1,3 +0,0 @@
"""
Module to be loaded by the Loader test.
"""

View File

@ -1,30 +0,0 @@
"""
test.helper
~~~~~~~~~~~
Helper method for writing tests.
"""
import os
import homeassistant as ha
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

View File

@ -1,64 +0,0 @@
"""
test.mock.switch_platform
~~~~~~~~~~~~~~~~~~~~~~~~~
Provides a mock switch platform.
Call init before using it in your tests to ensure clean test data.
"""
import homeassistant.components as components
class MockToggleDevice(components.ToggleDevice):
""" Fake switch. """
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 = components.STATE_ON
def turn_off(self, **kwargs):
""" Turn the device off. """
self.calls.append(('turn_off', kwargs))
self.state = components.STATE_OFF
def is_on(self):
""" True if device is on. """
self.calls.append(('is_on', {}))
return self.state == components.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)
DEVICES = []
def init(empty=False):
""" (re-)initalizes the platform with devices. """
global DEVICES
DEVICES = [] if empty else [
MockToggleDevice('AC', components.STATE_ON),
MockToggleDevice('AC', components.STATE_OFF),
MockToggleDevice(None, components.STATE_OFF)
]
def get_switches(hass, config):
""" Returns mock devices. """
return DEVICES
get_lights = get_switches

View File

@ -1,39 +0,0 @@
"""
test.test_loader
~~~~~~~~~~~~~~~~~~
Provides tests to verify that we can load components.
"""
# pylint: disable=too-many-public-methods,protected-access
import unittest
import homeassistant as ha
import homeassistant.loader as loader
import homeassistant.components.http as http
import mock_toggledevice_platform
from helper import get_test_home_assistant
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._pool.stop()
def test_set_component(self):
""" Test if set_component works. """
loader.set_component('switch.test', mock_toggledevice_platform)
self.assertEqual(
mock_toggledevice_platform, 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('custom_one'))

View File

@ -1,89 +0,0 @@
"""
test.test_util
~~~~~~~~~~~~~~
Tests Home Assistant util methods.
"""
# pylint: disable=too-many-public-methods
import unittest
from datetime import datetime
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"))
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"]))