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/example.py
!config/custom_components/hello_world.py
# Hide sublime text stuff
*.sublime-project

View File

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

View File

@ -1,23 +1,6 @@
# Adding support for a new device
You've probably came here beacuse you noticed that your favorite device is not supported and want to add it.
First step is to decide under which component the device has to reside. Each component is responsible for a specific domain within Home Assistant. An example is the switch component, which is responsible for interaction with different types of switches. The switch component consists of the following files:
**homeassistant/components/switch/\_\_init\_\_.py**<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).
For help on building your component, please see the See the [developer documentation on home-assistant.io](https://home-assistant.io/developers/).
After you finish adding support for your device:

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)
This is the source for Home Assistant. For installation instructions, tutorials and the docs, please see [https://home-assistant.io](https://home-assistant.io). For a functioning demo frontend of Home Assistant, [click here](https://home-assistant.io/demo/).
Home Assistant is a home automation platform running on Python 3. The goal of Home Assistant is to be able to track and control all devices at home and offer a platform for automating control.
It offers the following functionality through built-in components:
@ -23,7 +25,7 @@ Home Assistant also includes functionality for controlling HTPCs:
* Download files
* Open URLs in the default browser
![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).
@ -55,6 +57,8 @@ After you got the demo mode running it is time to enable some real components an
*Note:* you can append `?api_password=YOUR_PASSWORD` to the url of the web interface to log in automatically.
*Note:* for the light and switch component, you can specify multiple platforms by using sequential sections: [switch], [switch 2], [switch 3] etc
### Philips Hue
To get Philips Hue working you will have to connect Home Assistant to the Hue bridge.
@ -68,7 +72,7 @@ After that add the following lines to your `home-assistant.conf`:
```
[light]
type=hue
platform=hue
```
### Wireless router
@ -77,7 +81,7 @@ Your wireless router is used to track which devices are connected. Three differe
```
[device_tracker]
type=netgear
platform=netgear
host=192.168.1.1
username=admin
password=MY_PASSWORD
@ -87,336 +91,12 @@ password=MY_PASSWORD
*Note on luci:* before the Luci scanner can be used you have to install the luci RPC package on OpenWRT: `opkg install luci-mod-rpc`.
Once tracking the `device_tracker` component will maintain a file in your config dir called `known_devices.csv`. Edit this file to adjust which devices have to be tracked.
Once tracking, the `device_tracker` component will maintain a file in your config dir called `known_devices.csv`. Edit this file to adjust which devices have to be tracked.
<a name='customizing'></a>
## 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:
As an alternative to the router-based device tracking, it is possible to directly scan the network for devices by using nmap. The IP addresses to scan can be specified in any format that nmap understands, including the network-prefix notation (`192.168.1.1/24`) and the range notation (`192.168.1.1-255`).
```
[example]
host=paulusschoutsen.nl
[device_tracker]
platform=nmap_tracker
hosts=192.168.1.1/24
```
Then in the setup-method of your component you will be able to refer to `config[example][host]` to get the value `paulusschoutsen.nl`.
If you want to get your component included with the Home Assistant distribution, please take a look at the [contributing page](https://github.com/balloob/home-assistant/blob/master/CONTRIBUTING.md).
<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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~
Bare minimum what is needed for a component to be valid.
Example component to target an entity_id to:
- turn it on at 7AM in the morning
- turn it on if anyone comes home and it is off
- turn it off if all lights are turned off
- turn it off if all people leave the house
- offer a service to turn it on for 10 seconds
"""
import time
import logging
from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_ON, STATE_OFF
import homeassistant.loader as loader
from homeassistant.helpers import validate_config
import homeassistant.components as core
# The domain of your component. Should be equal to the name of your component
DOMAIN = "example"
# List of component names (string) your component depends upon
# If you are setting up a group but not using a group for anything,
# don't depend on group
DEPENDENCIES = []
# We depend on group because group will be loaded after all the components that
# initalize devices have been setup.
DEPENDENCIES = ['group']
# Configuration key for the entity id we are targetting
CONF_TARGET = 'target'
# Name of the service that we expose
SERVICE_FLASH = 'flash'
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def setup(hass, config):
""" Register services or listen for events that your component needs. """
""" Setup example component. """
# Example of a service that prints the service call to the command-line.
hass.services.register(DOMAIN, "example_service_name", print)
# Validate that all required config options are given
if not validate_config(config, {DOMAIN: [CONF_TARGET]}, _LOGGER):
return False
# This prints a time change event to the command-line twice a minute.
hass.track_time_change(print, second=[0, 30])
target_id = config[DOMAIN][CONF_TARGET]
# See also (defined in homeassistant/__init__.py):
# hass.track_state_change
# hass.track_point_in_time
# Validate that the target entity id exists
if hass.states.get(target_id) is None:
_LOGGER.error("Target entity id %s does not exist", target_id)
# Tell the bootstrapper that we failed to initialize
return False
# We will use the component helper methods to check the states.
device_tracker = loader.get_component('device_tracker')
light = loader.get_component('light')
def track_devices(entity_id, old_state, new_state):
""" Called when the group.all devices change state. """
# If anyone comes home and the core is not on, turn it on.
if new_state.state == STATE_HOME and not core.is_on(hass, target_id):
core.turn_on(hass, target_id)
# If all people leave the house and the core is on, turn it off
elif new_state.state == STATE_NOT_HOME and core.is_on(hass, target_id):
core.turn_off(hass, target_id)
# Register our track_devices method to receive state changes of the
# all tracked devices group.
hass.states.track_change(
device_tracker.ENTITY_ID_ALL_DEVICES, track_devices)
def wake_up(now):
""" Turn it on in the morning if there are people home and
it is not already on. """
if device_tracker.is_on(hass) and not core.is_on(hass, target_id):
_LOGGER.info('People home at 7AM, turning it on')
core.turn_on(hass, target_id)
# Register our wake_up service to be called at 7AM in the morning
hass.track_time_change(wake_up, hour=7, minute=0, second=0)
def all_lights_off(entity_id, old_state, new_state):
""" If all lights turn off, turn off. """
if core.is_on(hass, target_id):
_LOGGER.info('All lights have been turned off, turning it off')
core.turn_off(hass, target_id)
# Register our all_lights_off method to be called when all lights turn off
hass.states.track_change(
light.ENTITY_ID_ALL_LIGHTS, all_lights_off, STATE_ON, STATE_OFF)
def flash_service(call):
""" Service that will turn the target off for 10 seconds
if on and vice versa. """
if core.is_on(hass, target_id):
core.turn_off(hass, target_id)
time.sleep(10)
core.turn_on(hass, target_id)
else:
core.turn_on(hass, target_id)
time.sleep(10)
core.turn_off(hass, target_id)
# Register our service with HASS.
hass.services.register(DOMAIN, SERVICE_FLASH, flash_service)
# Tells the bootstrapper that the component was succesfully initialized
return True

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
[light]
type=hue
platform=hue
[device_tracker]
# The following types are available: netgear, tomato, luci
type=netgear
# The following types are available: netgear, tomato, luci, nmap_tracker
platform=netgear
host=192.168.1.1
username=admin
password=PASSWORD
# http_id is needed for Tomato routers only
# http_id=ABCDEFGHH
# For nmap_tracker, only the IP addresses to scan are needed:
# hosts=192.168.1.1/24 # netmask prefix notation or
# hosts=192.168.1.1-255 # address range
[chromecast]
# Optional: hard code the hosts (comma seperated) to find chromecasts
@ -26,7 +29,7 @@ password=PASSWORD
# hosts=192.168.1.9,192.168.1.12
[switch]
type=wemo
platform=wemo
# Optional: hard code the hosts (comma seperated) to avoid scanning the network
# hosts=192.168.1.9,192.168.1.12
@ -53,6 +56,12 @@ xbmc=XBMC.App
[example]
[simple_alarm]
# Which light/light group has to flash when a known device comes home
known_light=light.Bowl
# Which light/light group has to flash red when light turns on while no one home
unknown_light=group.living_room
[browser]
[keyboard]

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

View File

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

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

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.
"""
@ -52,30 +52,50 @@ def setUpModule(): # pylint: disable=invalid-name
def tearDownModule(): # pylint: disable=invalid-name
""" Stops the Home Assistant server. """
global hass
hass.stop()
class TestHTTP(unittest.TestCase):
""" Test the HTTP debug interface and API. """
def test_get_frontend(self):
def test_setup(self):
""" Test http.setup. """
self.assertFalse(http.setup(hass, {}))
self.assertFalse(http.setup(hass, {http.DOMAIN: {}}))
def test_frontend_and_static(self):
""" Tests if we can get the frontend. """
req = requests.get(_url(""))
self.assertEqual(200, req.status_code)
# Test we can retrieve frontend.js
frontendjs = re.search(
r'(?P<app>\/static\/frontend-[A-Za-z0-9]{32}.html)',
req.text).groups(0)[0]
req.text)
self.assertIsNotNone(frontendjs)
req = requests.get(_url(frontendjs))
req = requests.head(_url(frontendjs.groups(0)[0]))
self.assertEqual(200, req.status_code)
# Test auto filling in api password
req = requests.get(
_url("?{}={}".format(http.DATA_API_PASSWORD, API_PASSWORD)))
self.assertEqual(200, req.status_code)
auth_text = re.search(r"auth='{}'".format(API_PASSWORD), req.text)
self.assertIsNotNone(auth_text)
# Test 404
self.assertEqual(404, requests.get(_url("/not-existing")).status_code)
# Test we cannot POST to /
self.assertEqual(405, requests.post(_url("")).status_code)
def test_api_password(self):
""" Test if we get access denied if we omit or provide
a wrong api password. """
@ -127,8 +147,8 @@ class TestHTTP(unittest.TestCase):
hass.states.set("test.test", "not_to_be_set")
requests.post(_url(remote.URL_API_STATES_ENTITY.format("test.test")),
data=json.dumps({"state": "debug_state_change2",
"api_password": API_PASSWORD}))
data=json.dumps({"state": "debug_state_change2"}),
headers=HA_HEADERS)
self.assertEqual("debug_state_change2",
hass.states.get("test.test").state)
@ -143,8 +163,8 @@ class TestHTTP(unittest.TestCase):
req = requests.post(
_url(remote.URL_API_STATES_ENTITY.format(
"test_entity.that_does_not_exist")),
data=json.dumps({"state": new_state,
"api_password": API_PASSWORD}))
data=json.dumps({'state': new_state}),
headers=HA_HEADERS)
cur_state = (hass.states.
get("test_entity.that_does_not_exist").state)
@ -152,6 +172,18 @@ class TestHTTP(unittest.TestCase):
self.assertEqual(201, req.status_code)
self.assertEqual(cur_state, new_state)
# pylint: disable=invalid-name
def test_api_state_change_with_bad_data(self):
""" Test if API sends appropriate error if we omit state. """
req = requests.post(
_url(remote.URL_API_STATES_ENTITY.format(
"test_entity.that_does_not_exist")),
data=json.dumps({}),
headers=HA_HEADERS)
self.assertEqual(400, req.status_code)
# pylint: disable=invalid-name
def test_api_fire_event_with_no_data(self):
""" Test if the API allows us to fire an event. """
@ -161,13 +193,13 @@ class TestHTTP(unittest.TestCase):
""" Helper method that will verify our event got called. """
test_value.append(1)
hass.listen_once_event("test.event_no_data", listener)
hass.bus.listen_once("test.event_no_data", listener)
requests.post(
_url(remote.URL_API_EVENTS_EVENT.format("test.event_no_data")),
headers=HA_HEADERS)
hass._pool.block_till_done()
hass.pool.block_till_done()
self.assertEqual(1, len(test_value))
@ -182,14 +214,14 @@ class TestHTTP(unittest.TestCase):
if "test" in event.data:
test_value.append(1)
hass.listen_once_event("test_event_with_data", listener)
hass.bus.listen_once("test_event_with_data", listener)
requests.post(
_url(remote.URL_API_EVENTS_EVENT.format("test_event_with_data")),
data=json.dumps({"test": 1}),
headers=HA_HEADERS)
hass._pool.block_till_done()
hass.pool.block_till_done()
self.assertEqual(1, len(test_value))
@ -202,14 +234,25 @@ class TestHTTP(unittest.TestCase):
""" Helper method that will verify our event got called. """
test_value.append(1)
hass.listen_once_event("test_event_bad_data", listener)
hass.bus.listen_once("test_event_bad_data", listener)
req = requests.post(
_url(remote.URL_API_EVENTS_EVENT.format("test_event_bad_data")),
data=json.dumps('not an object'),
headers=HA_HEADERS)
hass._pool.block_till_done()
hass.pool.block_till_done()
self.assertEqual(422, req.status_code)
self.assertEqual(0, len(test_value))
# Try now with valid but unusable JSON
req = requests.post(
_url(remote.URL_API_EVENTS_EVENT.format("test_event_bad_data")),
data=json.dumps([1, 2, 3]),
headers=HA_HEADERS)
hass.pool.block_till_done()
self.assertEqual(422, req.status_code)
self.assertEqual(0, len(test_value))
@ -254,7 +297,7 @@ class TestHTTP(unittest.TestCase):
"test_domain", "test_service")),
headers=HA_HEADERS)
hass._pool.block_till_done()
hass.pool.block_till_done()
self.assertEqual(1, len(test_value))
@ -276,6 +319,82 @@ class TestHTTP(unittest.TestCase):
data=json.dumps({"test": 1}),
headers=HA_HEADERS)
hass._pool.block_till_done()
hass.pool.block_till_done()
self.assertEqual(1, len(test_value))
def test_api_event_forward(self):
""" Test setting up event forwarding. """
req = requests.post(
_url(remote.URL_API_EVENT_FORWARD),
headers=HA_HEADERS)
self.assertEqual(400, req.status_code)
req = requests.post(
_url(remote.URL_API_EVENT_FORWARD),
data=json.dumps({'host': '127.0.0.1'}),
headers=HA_HEADERS)
self.assertEqual(400, req.status_code)
req = requests.post(
_url(remote.URL_API_EVENT_FORWARD),
data=json.dumps({'api_password': 'bla-di-bla'}),
headers=HA_HEADERS)
self.assertEqual(400, req.status_code)
req = requests.post(
_url(remote.URL_API_EVENT_FORWARD),
data=json.dumps({
'api_password': 'bla-di-bla',
'host': '127.0.0.1',
'port': 'abcd'
}),
headers=HA_HEADERS)
self.assertEqual(422, req.status_code)
req = requests.post(
_url(remote.URL_API_EVENT_FORWARD),
data=json.dumps({
'api_password': 'bla-di-bla',
'host': '127.0.0.1',
'port': '8125'
}),
headers=HA_HEADERS)
self.assertEqual(422, req.status_code)
# Setup a real one
req = requests.post(
_url(remote.URL_API_EVENT_FORWARD),
data=json.dumps({
'api_password': API_PASSWORD,
'host': '127.0.0.1',
'port': SERVER_PORT
}),
headers=HA_HEADERS)
self.assertEqual(200, req.status_code)
# Delete it again..
req = requests.delete(
_url(remote.URL_API_EVENT_FORWARD),
data=json.dumps({}),
headers=HA_HEADERS)
self.assertEqual(400, req.status_code)
req = requests.delete(
_url(remote.URL_API_EVENT_FORWARD),
data=json.dumps({
'host': '127.0.0.1',
'port': 'abcd'
}),
headers=HA_HEADERS)
self.assertEqual(422, req.status_code)
req = requests.delete(
_url(remote.URL_API_EVENT_FORWARD),
data=json.dumps({
'host': '127.0.0.1',
'port': SERVER_PORT
}),
headers=HA_HEADERS)
self.assertEqual(200, req.status_code)

View File

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

View File

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

View File

@ -1,6 +1,6 @@
"""
test.test_component_switch
~~~~~~~~~~~~~~~~~~~~~~~~~~
ha_test.test_component_switch
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests switch component.
"""
@ -9,38 +9,39 @@ import unittest
import homeassistant as ha
import homeassistant.loader as loader
import homeassistant.components as components
from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM
import homeassistant.components.switch as switch
import mock_toggledevice_platform
from helpers import get_test_home_assistant
class TestSwitch(unittest.TestCase):
""" Test the switch module. """
def setUp(self): # pylint: disable=invalid-name
self.hass = ha.HomeAssistant()
self.hass = get_test_home_assistant()
loader.prepare(self.hass)
loader.set_component('switch.test', mock_toggledevice_platform)
mock_toggledevice_platform.init()
platform = loader.get_component('switch.test')
platform.init()
self.assertTrue(switch.setup(
self.hass, {switch.DOMAIN: {ha.CONF_TYPE: 'test'}}
self.hass, {switch.DOMAIN: {CONF_PLATFORM: 'test'}}
))
# Switch 1 is ON, switch 2 is OFF
self.switch_1, self.switch_2, self.switch_3 = \
mock_toggledevice_platform.get_switches(None, None)
platform.get_switches(None, None)
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
self.hass._pool.stop()
self.hass.stop()
def test_methods(self):
""" Test is_on, turn_on, turn_off methods. """
self.assertTrue(switch.is_on(self.hass))
self.assertEqual(
components.STATE_ON,
STATE_ON,
self.hass.states.get(switch.ENTITY_ID_ALL_SWITCHES).state)
self.assertTrue(switch.is_on(self.hass, self.switch_1.entity_id))
self.assertFalse(switch.is_on(self.hass, self.switch_2.entity_id))
@ -49,7 +50,7 @@ class TestSwitch(unittest.TestCase):
switch.turn_off(self.hass, self.switch_1.entity_id)
switch.turn_on(self.hass, self.switch_2.entity_id)
self.hass._pool.block_till_done()
self.hass.pool.block_till_done()
self.assertTrue(switch.is_on(self.hass))
self.assertFalse(switch.is_on(self.hass, self.switch_1.entity_id))
@ -58,11 +59,11 @@ class TestSwitch(unittest.TestCase):
# Turn all off
switch.turn_off(self.hass)
self.hass._pool.block_till_done()
self.hass.pool.block_till_done()
self.assertFalse(switch.is_on(self.hass))
self.assertEqual(
components.STATE_OFF,
STATE_OFF,
self.hass.states.get(switch.ENTITY_ID_ALL_SWITCHES).state)
self.assertFalse(switch.is_on(self.hass, self.switch_1.entity_id))
self.assertFalse(switch.is_on(self.hass, self.switch_2.entity_id))
@ -71,11 +72,11 @@ class TestSwitch(unittest.TestCase):
# Turn all on
switch.turn_on(self.hass)
self.hass._pool.block_till_done()
self.hass.pool.block_till_done()
self.assertTrue(switch.is_on(self.hass))
self.assertEqual(
components.STATE_ON,
STATE_ON,
self.hass.states.get(switch.ENTITY_ID_ALL_SWITCHES).state)
self.assertTrue(switch.is_on(self.hass, self.switch_1.entity_id))
self.assertTrue(switch.is_on(self.hass, self.switch_2.entity_id))
@ -89,15 +90,27 @@ class TestSwitch(unittest.TestCase):
# Test with non-existing component
self.assertFalse(switch.setup(
self.hass, {switch.DOMAIN: {ha.CONF_TYPE: 'nonexisting'}}
self.hass, {switch.DOMAIN: {CONF_PLATFORM: 'nonexisting'}}
))
# Test if switch component returns 0 switches
mock_toggledevice_platform.init(True)
test_platform = loader.get_component('switch.test')
test_platform.init(True)
self.assertEqual(
[], mock_toggledevice_platform.get_switches(None, None))
[], test_platform.get_switches(None, None))
self.assertFalse(switch.setup(
self.hass, {switch.DOMAIN: {ha.CONF_TYPE: 'test'}}
self.hass, {switch.DOMAIN: {CONF_PLATFORM: 'test'}}
))
# Test if we can load 2 platforms
loader.set_component('switch.test2', test_platform)
test_platform.init(False)
self.assertTrue(switch.setup(
self.hass, {
switch.DOMAIN: {CONF_PLATFORM: 'test'},
'{} 2'.format(switch.DOMAIN): {CONF_PLATFORM: 'test2'},
}
))

View File

@ -1,6 +1,6 @@
"""
test.test_core
~~~~~~~~~~~~~~
ha_test.test_core
~~~~~~~~~~~~~~~~~
Provides tests to verify that Home Assistant core works.
"""
@ -30,7 +30,7 @@ class TestHomeAssistant(unittest.TestCase):
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
self.hass._pool.stop()
self.hass.stop()
def test_get_config_path(self):
""" Test get_config_path method. """
@ -52,81 +52,18 @@ class TestHomeAssistant(unittest.TestCase):
self.assertTrue(blocking_thread.is_alive())
self.hass.call_service(ha.DOMAIN, ha.SERVICE_HOMEASSISTANT_STOP)
self.hass._pool.block_till_done()
self.hass.services.call(ha.DOMAIN, ha.SERVICE_HOMEASSISTANT_STOP)
self.hass.pool.block_till_done()
# hass.block_till_stopped checks every second if it should quit
# we have to wait worst case 1 second
wait_loops = 0
while blocking_thread.is_alive() and wait_loops < 10:
while blocking_thread.is_alive() and wait_loops < 50:
wait_loops += 1
time.sleep(0.1)
self.assertFalse(blocking_thread.is_alive())
def test_get_entity_ids(self):
""" Test get_entity_ids method. """
ent_ids = self.hass.get_entity_ids()
self.assertEqual(2, len(ent_ids))
self.assertTrue('light.Bowl' in ent_ids)
self.assertTrue('switch.AC' in ent_ids)
ent_ids = self.hass.get_entity_ids('light')
self.assertEqual(1, len(ent_ids))
self.assertTrue('light.Bowl' in ent_ids)
def test_track_state_change(self):
""" Test track_state_change. """
# 2 lists to track how often our callbacks got called
specific_runs = []
wildcard_runs = []
self.hass.track_state_change(
'light.Bowl', lambda a, b, c: specific_runs.append(1), 'on', 'off')
self.hass.track_state_change(
'light.Bowl', lambda a, b, c: wildcard_runs.append(1),
ha.MATCH_ALL, ha.MATCH_ALL)
# Set same state should not trigger a state change/listener
self.hass.states.set('light.Bowl', 'on')
self.hass._pool.block_till_done()
self.assertEqual(0, len(specific_runs))
self.assertEqual(0, len(wildcard_runs))
# State change off -> on
self.hass.states.set('light.Bowl', 'off')
self.hass._pool.block_till_done()
self.assertEqual(1, len(specific_runs))
self.assertEqual(1, len(wildcard_runs))
# State change off -> off
self.hass.states.set('light.Bowl', 'off', {"some_attr": 1})
self.hass._pool.block_till_done()
self.assertEqual(1, len(specific_runs))
self.assertEqual(2, len(wildcard_runs))
# State change off -> on
self.hass.states.set('light.Bowl', 'on')
self.hass._pool.block_till_done()
self.assertEqual(1, len(specific_runs))
self.assertEqual(3, len(wildcard_runs))
def test_listen_once_event(self):
""" Test listen_once_event method. """
runs = []
self.hass.listen_once_event('test_event', lambda x: runs.append(1))
self.hass.bus.fire('test_event')
self.hass._pool.block_till_done()
self.assertEqual(1, len(runs))
# Second time it should not increase runs
self.hass.bus.fire('test_event')
self.hass._pool.block_till_done()
self.assertEqual(1, len(runs))
def test_track_point_in_time(self):
""" Test track point in time. """
before_birthday = datetime(1985, 7, 9, 12, 0, 0)
@ -139,23 +76,23 @@ class TestHomeAssistant(unittest.TestCase):
lambda x: runs.append(1), birthday_paulus)
self._send_time_changed(before_birthday)
self.hass._pool.block_till_done()
self.hass.pool.block_till_done()
self.assertEqual(0, len(runs))
self._send_time_changed(birthday_paulus)
self.hass._pool.block_till_done()
self.hass.pool.block_till_done()
self.assertEqual(1, len(runs))
# A point in time tracker will only fire once, this should do nothing
self._send_time_changed(birthday_paulus)
self.hass._pool.block_till_done()
self.hass.pool.block_till_done()
self.assertEqual(1, len(runs))
self.hass.track_point_in_time(
lambda x: runs.append(1), birthday_paulus)
self._send_time_changed(after_birthday)
self.hass._pool.block_till_done()
self.hass.pool.block_till_done()
self.assertEqual(2, len(runs))
def test_track_time_change(self):
@ -168,17 +105,17 @@ class TestHomeAssistant(unittest.TestCase):
lambda x: specific_runs.append(1), second=[0, 30])
self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0))
self.hass._pool.block_till_done()
self.hass.pool.block_till_done()
self.assertEqual(1, len(specific_runs))
self.assertEqual(1, len(wildcard_runs))
self._send_time_changed(datetime(2014, 5, 24, 12, 0, 15))
self.hass._pool.block_till_done()
self.hass.pool.block_till_done()
self.assertEqual(1, len(specific_runs))
self.assertEqual(2, len(wildcard_runs))
self._send_time_changed(datetime(2014, 5, 24, 12, 0, 30))
self.hass._pool.block_till_done()
self.hass.pool.block_till_done()
self.assertEqual(2, len(specific_runs))
self.assertEqual(3, len(wildcard_runs))
@ -234,6 +171,21 @@ class TestEventBus(unittest.TestCase):
# Try deleting listener while category doesn't exist either
self.bus.remove_listener('test', listener)
def test_listen_once_event(self):
""" Test listen_once_event method. """
runs = []
self.bus.listen_once('test_event', lambda x: runs.append(1))
self.bus.fire('test_event')
self.bus._pool.block_till_done()
self.assertEqual(1, len(runs))
# Second time it should not increase runs
self.bus.fire('test_event')
self.bus._pool.block_till_done()
self.assertEqual(1, len(runs))
class TestState(unittest.TestCase):
""" Test EventBus methods. """
@ -276,15 +228,76 @@ class TestStateMachine(unittest.TestCase):
self.assertFalse(self.states.is_state('light.Bowl', 'off'))
self.assertFalse(self.states.is_state('light.Non_existing', 'on'))
def test_entity_ids(self):
""" Test get_entity_ids method. """
ent_ids = self.states.entity_ids()
self.assertEqual(2, len(ent_ids))
self.assertTrue('light.Bowl' in ent_ids)
self.assertTrue('switch.AC' in ent_ids)
ent_ids = self.states.entity_ids('light')
self.assertEqual(1, len(ent_ids))
self.assertTrue('light.Bowl' in ent_ids)
def test_remove(self):
""" Test remove method. """
self.assertTrue('light.Bowl' in self.states.entity_ids)
self.assertTrue('light.Bowl' in self.states.entity_ids())
self.assertTrue(self.states.remove('light.Bowl'))
self.assertFalse('light.Bowl' in self.states.entity_ids)
self.assertFalse('light.Bowl' in self.states.entity_ids())
# If it does not exist, we should get False
self.assertFalse(self.states.remove('light.Bowl'))
def test_track_change(self):
""" Test states.track_change. """
# 2 lists to track how often our callbacks got called
specific_runs = []
wildcard_runs = []
self.states.track_change(
'light.Bowl', lambda a, b, c: specific_runs.append(1), 'on', 'off')
self.states.track_change(
'light.Bowl', lambda a, b, c: wildcard_runs.append(1),
ha.MATCH_ALL, ha.MATCH_ALL)
# Set same state should not trigger a state change/listener
self.states.set('light.Bowl', 'on')
self.bus._pool.block_till_done()
self.assertEqual(0, len(specific_runs))
self.assertEqual(0, len(wildcard_runs))
# State change off -> on
self.states.set('light.Bowl', 'off')
self.bus._pool.block_till_done()
self.assertEqual(1, len(specific_runs))
self.assertEqual(1, len(wildcard_runs))
# State change off -> off
self.states.set('light.Bowl', 'off', {"some_attr": 1})
self.bus._pool.block_till_done()
self.assertEqual(1, len(specific_runs))
self.assertEqual(2, len(wildcard_runs))
# State change off -> on
self.states.set('light.Bowl', 'on')
self.bus._pool.block_till_done()
self.assertEqual(1, len(specific_runs))
self.assertEqual(3, len(wildcard_runs))
def test_case_insensitivty(self):
runs = []
self.states.track_change(
'light.BoWl', lambda a, b, c: runs.append(1),
ha.MATCH_ALL, ha.MATCH_ALL)
self.states.set('light.BOWL', 'off')
self.bus._pool.block_till_done()
self.assertTrue(self.states.is_state('light.bowl', 'off'))
self.assertEqual(1, len(runs))
class TestServiceCall(unittest.TestCase):
""" Test ServiceCall class. """

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

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 functools as ft
from requests.structures import CaseInsensitiveDict
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
SERVICE_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED,
EVENT_CALL_SERVICE, ATTR_NOW, ATTR_DOMAIN, ATTR_SERVICE, MATCH_ALL,
EVENT_SERVICE_EXECUTED, ATTR_SERVICE_CALL_ID)
import homeassistant.util as util
MATCH_ALL = '*'
DOMAIN = "homeassistant"
SERVICE_HOMEASSISTANT_STOP = "stop"
EVENT_HOMEASSISTANT_START = "homeassistant_start"
EVENT_HOMEASSISTANT_STOP = "homeassistant_stop"
EVENT_STATE_CHANGED = "state_changed"
EVENT_TIME_CHANGED = "time_changed"
EVENT_CALL_SERVICE = "call_service"
ATTR_NOW = "now"
ATTR_DOMAIN = "domain"
ATTR_SERVICE = "service"
CONF_LATITUDE = "latitude"
CONF_LONGITUDE = "longitude"
CONF_TYPE = "type"
CONF_HOST = "host"
CONF_HOSTS = "hosts"
CONF_USERNAME = "username"
CONF_PASSWORD = "password"
# How often time_changed event should fire
TIMER_INTERVAL = 10 # seconds
# Number of worker threads
POOL_NUM_THREAD = 4
# How long we wait for the result of a service call
SERVICE_CALL_LIMIT = 10 # seconds
# Define number of MINIMUM worker threads.
# During bootstrap of HA (see bootstrap.from_config_dict()) worker threads
# will be added for each component that polls devices.
MIN_WORKER_THREAD = 2
# Pattern for validating entity IDs (format: <domain>.<entity>)
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. """
def __init__(self):
self._pool = pool = create_worker_pool()
self.pool = pool = create_worker_pool()
self.bus = EventBus(pool)
self.services = ServiceRegistry(self.bus, pool)
self.states = StateMachine(self.bus)
@ -71,6 +60,9 @@ class HomeAssistant(object):
def start(self):
""" Start home assistant. """
_LOGGER.info(
"Starting Home Assistant (%d threads)", self.pool.worker_count)
Timer(self)
self.bus.fire(EVENT_HOMEASSISTANT_START)
@ -92,50 +84,6 @@ class HomeAssistant(object):
self.stop()
def call_service(self, domain, service, service_data=None):
""" Fires event to call specified service. """
event_data = service_data or {}
event_data[ATTR_DOMAIN] = domain
event_data[ATTR_SERVICE] = service
self.bus.fire(EVENT_CALL_SERVICE, event_data)
def get_entity_ids(self, domain_filter=None):
""" Returns known entity ids. """
if domain_filter:
return [entity_id for entity_id in self.states.entity_ids
if entity_id.startswith(domain_filter)]
else:
return self.states.entity_ids
def track_state_change(self, entity_ids, action,
from_state=None, to_state=None):
"""
Track specific state changes.
entity_ids, from_state and to_state can be string or list.
Use list to match multiple.
"""
from_state = _process_match_param(from_state)
to_state = _process_match_param(to_state)
# Ensure it is a list with entity ids we want to match on
if isinstance(entity_ids, str):
entity_ids = [entity_ids]
@ft.wraps(action)
def state_listener(event):
""" The listener that listens for specific state changes. """
if event.data['entity_id'] in entity_ids and \
'old_state' in event.data and \
_matcher(event.data['old_state'].state, from_state) and \
_matcher(event.data['new_state'].state, to_state):
action(event.data['entity_id'],
event.data['old_state'],
event.data['new_state'])
self.bus.listen(EVENT_STATE_CHANGED, state_listener)
def track_point_in_time(self, action, point_in_time):
"""
Adds a listener that fires once at or after a spefic point in time.
@ -202,31 +150,6 @@ class HomeAssistant(object):
self.bus.listen(EVENT_TIME_CHANGED, time_listener)
def listen_once_event(self, event_type, listener):
""" Listen once for event of a specific type.
To listen to all events specify the constant ``MATCH_ALL``
as event_type.
Note: at the moment it is impossible to remove a one time listener.
"""
@ft.wraps(listener)
def onetime_listener(event):
""" Removes listener from eventbus and then fires listener. """
if not hasattr(onetime_listener, 'run'):
# Set variable so that we will never run twice.
# Because the event bus might have to wait till a thread comes
# available to execute this listener it might occur that the
# listener gets lined up twice to be executed.
# This will make sure the second time it does nothing.
onetime_listener.run = True
self.bus.remove_listener(event_type, onetime_listener)
listener(event)
self.bus.listen(event_type, onetime_listener)
def stop(self):
""" Stops Home Assistant and shuts down all threads. """
_LOGGER.info("Stopping")
@ -234,14 +157,67 @@ class HomeAssistant(object):
self.bus.fire(EVENT_HOMEASSISTANT_STOP)
# Wait till all responses to homeassistant_stop are done
self._pool.block_till_done()
self.pool.block_till_done()
self._pool.stop()
self.pool.stop()
def get_entity_ids(self, domain_filter=None):
"""
Returns known entity ids.
THIS METHOD IS DEPRECATED. Use hass.states.entity_ids
"""
_LOGGER.warning(
"hass.get_entiy_ids is deprecated. Use hass.states.entity_ids")
return self.states.entity_ids(domain_filter)
def listen_once_event(self, event_type, listener):
""" Listen once for event of a specific type.
To listen to all events specify the constant ``MATCH_ALL``
as event_type.
Note: at the moment it is impossible to remove a one time listener.
THIS METHOD IS DEPRECATED. Please use hass.events.listen_once.
"""
_LOGGER.warning(
"hass.listen_once_event is deprecated. Use hass.bus.listen_once")
self.bus.listen_once(event_type, listener)
def track_state_change(self, entity_ids, action,
from_state=None, to_state=None):
"""
Track specific state changes.
entity_ids, from_state and to_state can be string or list.
Use list to match multiple.
THIS METHOD IS DEPRECATED. Use hass.states.track_change
"""
_LOGGER.warning((
"hass.track_state_change is deprecated. "
"Use hass.states.track_change"))
self.states.track_change(entity_ids, action, from_state, to_state)
def call_service(self, domain, service, service_data=None):
"""
Fires event to call specified service.
THIS METHOD IS DEPRECATED. Use hass.services.call
"""
_LOGGER.warning((
"hass.services.call is deprecated. "
"Use hass.services.call"))
self.services.call(domain, service, service_data)
def _process_match_param(parameter):
""" Wraps parameter in a list if it is not one and returns it. """
if not parameter or parameter == MATCH_ALL:
if parameter is None or parameter == MATCH_ALL:
return MATCH_ALL
elif isinstance(parameter, list):
return parameter
@ -261,6 +237,7 @@ class JobPriority(util.OrderedEnum):
""" Provides priorities for bus events. """
# pylint: disable=no-init,too-few-public-methods
EVENT_CALLBACK = 0
EVENT_SERVICE = 1
EVENT_STATE = 2
EVENT_TIME = 3
@ -275,11 +252,13 @@ class JobPriority(util.OrderedEnum):
return JobPriority.EVENT_STATE
elif event_type == EVENT_CALL_SERVICE:
return JobPriority.EVENT_SERVICE
elif event_type == EVENT_SERVICE_EXECUTED:
return JobPriority.EVENT_CALLBACK
else:
return JobPriority.EVENT_DEFAULT
def create_worker_pool(thread_count=POOL_NUM_THREAD):
def create_worker_pool():
""" Creates a worker pool to be used. """
def job_handler(job):
@ -292,18 +271,18 @@ def create_worker_pool(thread_count=POOL_NUM_THREAD):
# We do not want to crash our ThreadPool
_LOGGER.exception("BusHandler:Exception doing job")
def busy_callback(current_jobs, pending_jobs_count):
def busy_callback(worker_count, current_jobs, pending_jobs_count):
""" Callback to be called when the pool queue gets too big. """
_LOGGER.error(
_LOGGER.warning(
"WorkerPool:All %d threads are busy and %d jobs pending",
thread_count, pending_jobs_count)
worker_count, pending_jobs_count)
for start, job in current_jobs:
_LOGGER.error("WorkerPool:Current job from %s: %s",
util.datetime_to_str(start), job)
_LOGGER.warning("WorkerPool:Current job from %s: %s",
util.datetime_to_str(start), job)
return util.ThreadPool(thread_count, job_handler, busy_callback)
return util.ThreadPool(job_handler, MIN_WORKER_THREAD, busy_callback)
class EventOrigin(enum.Enum):
@ -374,9 +353,10 @@ class EventBus(object):
if not listeners:
return
job_priority = JobPriority.from_event_type(event_type)
for func in listeners:
self._pool.add_job(JobPriority.from_event_type(event_type),
(func, event))
self._pool.add_job(job_priority, (func, event))
def listen(self, event_type, listener):
""" Listen for all events or events of a specific type.
@ -390,6 +370,31 @@ class EventBus(object):
else:
self._listeners[event_type] = [listener]
def listen_once(self, event_type, listener):
""" Listen once for event of a specific type.
To listen to all events specify the constant ``MATCH_ALL``
as event_type.
Note: at the moment it is impossible to remove a one time listener.
"""
@ft.wraps(listener)
def onetime_listener(event):
""" Removes listener from eventbus and then fires listener. """
if not hasattr(onetime_listener, 'run'):
# Set variable so that we will never run twice.
# Because the event bus might have to wait till a thread comes
# available to execute this listener it might occur that the
# listener gets lined up twice to be executed.
# This will make sure the second time it does nothing.
onetime_listener.run = True
self.remove_listener(event_type, onetime_listener)
listener(event)
self.listen(event_type, onetime_listener)
def remove_listener(self, event_type, listener):
""" Removes a listener of a specific event_type. """
with self._lock:
@ -420,17 +425,13 @@ class State(object):
self.entity_id = entity_id
self.state = state
self.attributes = attributes or {}
last_changed = last_changed or dt.datetime.now()
# Strip microsecond from last_changed else we cannot guarantee
# state == State.from_dict(state.as_dict())
# This behavior occurs because to_dict uses datetime_to_str
# which strips microseconds
if last_changed.microsecond:
self.last_changed = last_changed - dt.timedelta(
microseconds=last_changed.microsecond)
else:
self.last_changed = last_changed
# which does not preserve microseconds
self.last_changed = util.strip_microseconds(
last_changed or dt.datetime.now())
def copy(self):
""" Creates a copy of itself. """
@ -483,14 +484,20 @@ class StateMachine(object):
""" Helper class that tracks the state of different entities. """
def __init__(self, bus):
self._states = {}
self._states = CaseInsensitiveDict()
self._bus = bus
self._lock = threading.Lock()
@property
def entity_ids(self):
def entity_ids(self, domain_filter=None):
""" List of entity ids that are being tracked. """
return list(self._states.keys())
if domain_filter is not None:
domain_filter = domain_filter.lower()
return [state.entity_id for key, state
in self._states.lower_items()
if util.split_entity_id(key)[0] == domain_filter]
else:
return list(self._states.keys())
def all(self):
""" Returns a list of all states. """
@ -503,15 +510,28 @@ class StateMachine(object):
# Make a copy so people won't mutate the state
return state.copy() if state else None
def get_since(self, point_in_time):
"""
Returns all states that have been changed since point_in_time.
Note: States keep track of last_changed -without- microseconds.
Therefore your point_in_time will also be stripped of microseconds.
"""
point_in_time = util.strip_microseconds(point_in_time)
with self._lock:
return [state for state in self._states.values()
if state.last_changed >= point_in_time]
def is_state(self, entity_id, state):
""" Returns True if entity exists and is specified state. """
return (entity_id in self._states and
self._states[entity_id].state == state)
def remove(self, entity_id):
""" Removes a entity from the state machine.
""" Removes an entity from the state machine.
Returns boolean to indicate if a entity was removed. """
Returns boolean to indicate if an entity was removed. """
with self._lock:
return self._states.pop(entity_id, None) is not None
@ -540,6 +560,40 @@ class StateMachine(object):
self._bus.fire(EVENT_STATE_CHANGED, event_data)
def track_change(self, entity_ids, action, from_state=None, to_state=None):
"""
Track specific state changes.
entity_ids, from_state and to_state can be string or list.
Use list to match multiple.
Returns the listener that listens on the bus for EVENT_STATE_CHANGED.
Pass the return value into hass.bus.remove_listener to remove it.
"""
from_state = _process_match_param(from_state)
to_state = _process_match_param(to_state)
# Ensure it is a lowercase list with entity ids we want to match on
if isinstance(entity_ids, str):
entity_ids = [entity_ids.lower()]
else:
entity_ids = [entity_id.lower() for entity_id in entity_ids]
@ft.wraps(action)
def state_listener(event):
""" The listener that listens for specific state changes. """
if event.data['entity_id'].lower() in entity_ids and \
'old_state' in event.data and \
_matcher(event.data['old_state'].state, from_state) and \
_matcher(event.data['new_state'].state, to_state):
action(event.data['entity_id'],
event.data['old_state'],
event.data['new_state'])
self._bus.listen(EVENT_STATE_CHANGED, state_listener)
return state_listener
# pylint: disable=too-few-public-methods
class ServiceCall(object):
@ -567,6 +621,8 @@ class ServiceRegistry(object):
self._services = {}
self._lock = threading.Lock()
self._pool = pool or create_worker_pool()
self._bus = bus
self._cur_id = 0
bus.listen(EVENT_CALL_SERVICE, self._event_to_service_call)
@property
@ -588,6 +644,57 @@ class ServiceRegistry(object):
else:
self._services[domain] = {service: service_func}
def call(self, domain, service, service_data=None, blocking=False):
"""
Calls specified service.
Specify blocking=True to wait till service is executed.
Waits a maximum of SERVICE_CALL_LIMIT.
If blocking = True, will return boolean if service executed
succesfully within SERVICE_CALL_LIMIT.
This method will fire an event to call the service.
This event will be picked up by this ServiceRegistry and any
other ServiceRegistry that is listening on the EventBus.
Because the service is sent as an event you are not allowed to use
the keys ATTR_DOMAIN and ATTR_SERVICE in your service_data.
"""
call_id = self._generate_unique_id()
event_data = service_data or {}
event_data[ATTR_DOMAIN] = domain
event_data[ATTR_SERVICE] = service
event_data[ATTR_SERVICE_CALL_ID] = call_id
if blocking:
executed_event = threading.Event()
def service_executed(call):
"""
Called when a service is executed.
Will set the event if matches our service call.
"""
if call.data[ATTR_SERVICE_CALL_ID] == call_id:
executed_event.set()
self._bus.remove_listener(
EVENT_SERVICE_EXECUTED, service_executed)
self._bus.listen(EVENT_SERVICE_EXECUTED, service_executed)
self._bus.fire(EVENT_CALL_SERVICE, event_data)
if blocking:
# wait will return False if event not set after our limit has
# passed. If not set, clean up the listener
if not executed_event.wait(SERVICE_CALL_LIMIT):
self._bus.remove_listener(
EVENT_SERVICE_EXECUTED, service_executed)
return False
return True
def _event_to_service_call(self, event):
""" Calls a service from an event. """
service_data = dict(event.data)
@ -598,9 +705,27 @@ class ServiceRegistry(object):
if domain in self._services and service in self._services[domain]:
service_call = ServiceCall(domain, service, service_data)
# Add a job to the pool that calls _execute_service
self._pool.add_job(JobPriority.EVENT_SERVICE,
(self._services[domain][service],
service_call))
(self._execute_service,
(self._services[domain][service],
service_call)))
def _execute_service(self, service_and_call):
""" Executes a service and fires a SERVICE_EXECUTED event. """
service, call = service_and_call
service(call)
self._bus.fire(
EVENT_SERVICE_EXECUTED, {
ATTR_SERVICE_CALL_ID: call.data[ATTR_SERVICE_CALL_ID]
})
def _generate_unique_id(self):
""" Generates a unique service call id. """
self._cur_id += 1
return "{}-{}".format(id(self), self._cur_id)
class Timer(threading.Thread):
@ -610,7 +735,7 @@ class Timer(threading.Thread):
threading.Thread.__init__(self)
self.daemon = True
self._bus = hass.bus
self.hass = hass
self.interval = interval or TIMER_INTERVAL
self._stop = threading.Event()
@ -619,15 +744,15 @@ class Timer(threading.Thread):
# every minute.
assert 60 % self.interval == 0, "60 % TIMER_INTERVAL should be 0!"
hass.listen_once_event(EVENT_HOMEASSISTANT_START,
lambda event: self.start())
hass.listen_once_event(EVENT_HOMEASSISTANT_STOP,
lambda event: self._stop.set())
hass.bus.listen_once(EVENT_HOMEASSISTANT_START,
lambda event: self.start())
def run(self):
""" Start the timer. """
self.hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
lambda event: self._stop.set())
_LOGGER.info("Timer:starting")
last_fired_on_second = -1
@ -658,7 +783,7 @@ class Timer(threading.Thread):
last_fired_on_second = now.second
self._bus.fire(EVENT_TIME_CHANGED, {ATTR_NOW: now})
self.hass.bus.fire(EVENT_TIME_CHANGED, {ATTR_NOW: now})
class HomeAssistantError(Exception):

View File

@ -13,12 +13,10 @@ import os
import configparser
import logging
from collections import defaultdict
from itertools import chain
import homeassistant
import homeassistant.loader as loader
import homeassistant.components as core_components
import homeassistant.components.group as group
# pylint: disable=too-many-branches, too-many-statements
@ -33,123 +31,49 @@ def from_config_dict(config, hass=None):
logger = logging.getLogger(__name__)
loader.prepare(hass)
# Make a copy because we are mutating it.
# Convert it to defaultdict so components can always have config dict
config = defaultdict(dict, config)
# List of loaded components
components = {}
# Filter out the repeating and common config section [homeassistant]
components = (key for key in config.keys()
if ' ' not in key and key != homeassistant.DOMAIN)
# List of components to validate
to_validate = []
# List of validated components
validated = []
# List of components we are going to load
to_load = [key for key in config.keys() if key != homeassistant.DOMAIN]
loader.prepare(hass)
# Load required components
while to_load:
domain = to_load.pop()
component = loader.get_component(domain)
# if None it does not exist, error already thrown by get_component
if component is not None:
components[domain] = component
# Special treatment for GROUP, we want to load it as late as
# possible. We do this by loading it if all other to be loaded
# modules depend on it.
if component.DOMAIN == group.DOMAIN:
pass
# Components with no dependencies are valid
elif not component.DEPENDENCIES:
validated.append(domain)
# If dependencies we'll validate it later
else:
to_validate.append(domain)
# Make sure to load all dependencies that are not being loaded
for dependency in component.DEPENDENCIES:
if dependency not in chain(components.keys(), to_load):
to_load.append(dependency)
# Validate dependencies
group_added = False
while to_validate:
newly_validated = []
for domain in to_validate:
if all(domain in validated for domain
in components[domain].DEPENDENCIES):
newly_validated.append(domain)
# We validated new domains this iteration, add them to validated
if newly_validated:
# Add newly validated domains to validated
validated.extend(newly_validated)
# remove domains from to_validate
for domain in newly_validated:
to_validate.remove(domain)
newly_validated.clear()
# Nothing validated this iteration. Add group dependency and try again.
elif not group_added:
group_added = True
validated.append(group.DOMAIN)
# Group has already been added and we still can't validate all.
# Report missing deps as error and skip loading of these domains
else:
for domain in to_validate:
missing_deps = [dep for dep in components[domain].DEPENDENCIES
if dep not in validated]
logger.error(
"Could not validate all dependencies for %s: %s",
domain, ", ".join(missing_deps))
break
# Make sure we load groups if not in list yet.
if not group_added:
validated.append(group.DOMAIN)
if group.DOMAIN not in components:
components[group.DOMAIN] = \
loader.get_component(group.DOMAIN)
# Setup the components
if core_components.setup(hass, config):
logger.info("Home Assistant core initialized")
for domain in validated:
component = components[domain]
try:
if component.setup(hass, config):
logger.info("component %s initialized", domain)
else:
logger.error("component %s failed to initialize", domain)
except Exception: # pylint: disable=broad-except
logger.exception("Error during setup of component %s", domain)
else:
if not core_components.setup(hass, config):
logger.error(("Home Assistant core failed to initialize. "
"Further initialization aborted."))
return hass
logger.info("Home Assistant core initialized")
# Setup the components
# We assume that all components that load before the group component loads
# are components that poll devices. As their tasks are IO based, we will
# add an extra worker for each of them.
add_worker = True
for domain in loader.load_order_components(components):
component = loader.get_component(domain)
try:
if component.setup(hass, config):
logger.info("component %s initialized", domain)
add_worker = add_worker and domain != "group"
if add_worker:
hass.pool.add_worker()
else:
logger.error("component %s failed to initialize", domain)
except Exception: # pylint: disable=broad-except
logger.exception("Error during setup of component %s", domain)
return hass
@ -181,7 +105,7 @@ def from_config_file(config_path, hass=None, enable_logging=True):
err_handler = logging.FileHandler(
err_log_path, mode='w', delay=True)
err_handler.setLevel(logging.ERROR)
err_handler.setLevel(logging.WARNING)
err_handler.setFormatter(
logging.Formatter('%(asctime)s %(name)s: %(message)s',
datefmt='%H:%M %d-%m-%y'))

View File

@ -19,36 +19,10 @@ import logging
import homeassistant as ha
import homeassistant.util as util
from homeassistant.helpers import extract_entity_ids
from homeassistant.loader import get_component
# Contains one string or a list of strings, each being an entity id
ATTR_ENTITY_ID = 'entity_id'
# String with a friendly name for the entity
ATTR_FRIENDLY_NAME = "friendly_name"
# A picture to represent entity
ATTR_ENTITY_PICTURE = "entity_picture"
# The unit of measurement if applicable
ATTR_UNIT_OF_MEASUREMENT = "unit_of_measurement"
STATE_ON = 'on'
STATE_OFF = 'off'
STATE_HOME = 'home'
STATE_NOT_HOME = 'not_home'
SERVICE_TURN_ON = 'turn_on'
SERVICE_TURN_OFF = 'turn_off'
SERVICE_VOLUME_UP = "volume_up"
SERVICE_VOLUME_DOWN = "volume_down"
SERVICE_VOLUME_MUTE = "volume_mute"
SERVICE_MEDIA_PLAY_PAUSE = "media_play_pause"
SERVICE_MEDIA_PLAY = "media_play"
SERVICE_MEDIA_PAUSE = "media_pause"
SERVICE_MEDIA_NEXT_TRACK = "media_next_track"
SERVICE_MEDIA_PREV_TRACK = "media_prev_track"
from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
_LOGGER = logging.getLogger(__name__)
@ -61,7 +35,7 @@ def is_on(hass, entity_id=None):
entity_ids = group.expand_entity_ids(hass, [entity_id])
else:
entity_ids = hass.states.entity_ids
entity_ids = hass.states.entity_ids()
for entity_id in entity_ids:
domain = util.split_entity_id(entity_id)[0]
@ -85,7 +59,7 @@ def turn_on(hass, entity_id=None, **service_data):
if entity_id is not None:
service_data[ATTR_ENTITY_ID] = entity_id
hass.call_service(ha.DOMAIN, SERVICE_TURN_ON, service_data)
hass.services.call(ha.DOMAIN, SERVICE_TURN_ON, service_data)
def turn_off(hass, entity_id=None, **service_data):
@ -93,80 +67,7 @@ def turn_off(hass, entity_id=None, **service_data):
if entity_id is not None:
service_data[ATTR_ENTITY_ID] = entity_id
hass.call_service(ha.DOMAIN, SERVICE_TURN_OFF, service_data)
def extract_entity_ids(hass, service):
"""
Helper method to extract a list of entity ids from a service call.
Will convert group entity ids to the entity ids it represents.
"""
entity_ids = []
if service.data and ATTR_ENTITY_ID in service.data:
group = get_component('group')
# Entity ID attr can be a list or a string
service_ent_id = service.data[ATTR_ENTITY_ID]
if isinstance(service_ent_id, list):
ent_ids = service_ent_id
else:
ent_ids = [service_ent_id]
entity_ids.extend(
ent_id for ent_id
in group.expand_entity_ids(hass, ent_ids)
if ent_id not in entity_ids)
return entity_ids
class ToggleDevice(object):
""" ABC for devices that can be turned on and off. """
# pylint: disable=no-self-use
entity_id = None
def get_name(self):
""" Returns the name of the device if any. """
return None
def turn_on(self, **kwargs):
""" Turn the device on. """
pass
def turn_off(self, **kwargs):
""" Turn the device off. """
pass
def is_on(self):
""" True if device is on. """
return False
def get_state_attributes(self):
""" Returns optional state attributes. """
return {}
def update(self):
""" Retrieve latest state from the real device. """
pass
def update_ha_state(self, hass, force_refresh=False):
"""
Updates Home Assistant with current state of device.
If force_refresh == True will update device before setting state.
"""
if self.entity_id is None:
raise ha.NoEntitySpecifiedError(
"No entity specified for device {}".format(self.get_name()))
if force_refresh:
self.update()
state = STATE_ON if self.is_on() else STATE_OFF
return hass.states.set(self.entity_id, state,
self.get_state_attributes())
hass.services.call(ha.DOMAIN, SERVICE_TURN_OFF, service_data)
# pylint: disable=unused-argument
@ -195,7 +96,7 @@ def setup(hass, config):
# ent_ids is a generator, convert it to a list.
data[ATTR_ENTITY_ID] = list(ent_ids)
hass.call_service(domain, service.service, data)
hass.services.call(domain, service.service, data, True)
hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service)
hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
"""
homeassistant.components.tracker
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to keep track of devices.
"""
@ -10,11 +10,13 @@ import os
import csv
from datetime import datetime, timedelta
import homeassistant as ha
from homeassistant.loader import get_component
from homeassistant.helpers import validate_config
import homeassistant.util as util
import homeassistant.components as components
from homeassistant.const import (
STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME,
CONF_PLATFORM, CONF_TYPE)
from homeassistant.components import group
DOMAIN = "device_tracker"
@ -30,7 +32,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
# After how much time do we consider a device not home if
# it does not show up on scans
TIME_SPAN_FOR_ERROR_IN_SCANNING = timedelta(minutes=3)
TIME_DEVICE_NOT_FOUND = timedelta(minutes=3)
# Filename to save known devices to
KNOWN_DEVICES_FILE = "known_devices.csv"
@ -43,16 +45,26 @@ def is_on(hass, entity_id=None):
""" Returns if any or specified device is home. """
entity = entity_id or ENTITY_ID_ALL_DEVICES
return hass.states.is_state(entity, components.STATE_HOME)
return hass.states.is_state(entity, STATE_HOME)
def setup(hass, config):
""" Sets up the device tracker. """
if not util.validate_config(config, {DOMAIN: [ha.CONF_TYPE]}, _LOGGER):
# CONF_TYPE is deprecated for CONF_PLATOFRM. We keep supporting it for now.
if not (validate_config(config, {DOMAIN: [CONF_PLATFORM]}, _LOGGER)
or validate_config(config, {DOMAIN: [CONF_TYPE]}, _LOGGER)):
return False
tracker_type = config[DOMAIN][ha.CONF_TYPE]
tracker_type = config[DOMAIN].get(CONF_PLATFORM)
if tracker_type is None:
tracker_type = config[DOMAIN][CONF_TYPE]
_LOGGER.warning((
"Please update your config for %s to use 'platform' "
"instead of 'type'"), tracker_type)
tracker_implementation = get_component(
'device_tracker.{}'.format(tracker_type))
@ -70,105 +82,109 @@ def setup(hass, config):
return False
DeviceTracker(hass, device_scanner)
tracker = DeviceTracker(hass, device_scanner)
return True
# We only succeeded if we got to parse the known devices file
return not tracker.invalid_known_devices_file
# pylint: disable=too-many-instance-attributes
class DeviceTracker(object):
""" Class that tracks which devices are home and which are not. """
def __init__(self, hass, device_scanner):
self.states = hass.states
self.hass = hass
self.device_scanner = device_scanner
self.error_scanning = TIME_SPAN_FOR_ERROR_IN_SCANNING
self.lock = threading.Lock()
self.path_known_devices_file = hass.get_config_path(KNOWN_DEVICES_FILE)
# Dictionary to keep track of known devices and devices we track
self.known_devices = {}
self.tracked = {}
self.untracked_devices = set()
# Did we encounter an invalid known devices file
self.invalid_known_devices_file = False
self._read_known_devices_file()
# Wrap it in a func instead of lambda so it can be identified in
# the bus by its __name__ attribute.
def update_device_state(time): # pylint: disable=unused-argument
def update_device_state(now):
""" Triggers update of the device states. """
self.update_devices()
self.update_devices(now)
# pylint: disable=unused-argument
def reload_known_devices_service(service):
""" Reload known devices file. """
group.remove_group(self.hass, GROUP_NAME_ALL_DEVICES)
self._read_known_devices_file()
self.update_devices(datetime.now())
if self.tracked:
group.setup_group(
self.hass, GROUP_NAME_ALL_DEVICES,
self.device_entity_ids, False)
reload_known_devices_service(None)
if self.invalid_known_devices_file:
return
hass.track_time_change(update_device_state)
hass.services.register(DOMAIN,
SERVICE_DEVICE_TRACKER_RELOAD,
lambda service: self._read_known_devices_file())
self.update_devices()
group.setup_group(
hass, GROUP_NAME_ALL_DEVICES, self.device_entity_ids, False)
reload_known_devices_service)
@property
def device_entity_ids(self):
""" Returns a set containing all device entity ids
that are being tracked. """
return set([self.known_devices[device]['entity_id'] for device
in self.known_devices
if self.known_devices[device]['track']])
return set(device['entity_id'] for device in self.tracked.values())
def update_devices(self, found_devices=None):
def _update_state(self, now, device, is_home):
""" Update the state of a device. """
dev_info = self.tracked[device]
if is_home:
# Update last seen if at home
dev_info['last_seen'] = now
else:
# State remains at home if it has been seen in the last
# TIME_DEVICE_NOT_FOUND
is_home = now - dev_info['last_seen'] < TIME_DEVICE_NOT_FOUND
state = STATE_HOME if is_home else STATE_NOT_HOME
self.hass.states.set(
dev_info['entity_id'], state,
dev_info['state_attr'])
def update_devices(self, now):
""" Update device states based on the found devices. """
self.lock.acquire()
found_devices = found_devices or self.device_scanner.scan_devices()
found_devices = set(self.device_scanner.scan_devices())
now = datetime.now()
for device in self.tracked:
is_home = device in found_devices
known_dev = self.known_devices
self._update_state(now, device, is_home)
temp_tracking_devices = [device for device in known_dev
if known_dev[device]['track']]
if is_home:
found_devices.remove(device)
for device in found_devices:
# Are we tracking this device?
if device in temp_tracking_devices:
temp_tracking_devices.remove(device)
# Did we find any devices that we didn't know about yet?
new_devices = found_devices - self.untracked_devices
known_dev[device]['last_seen'] = now
if new_devices:
self.untracked_devices.update(new_devices)
self.states.set(
known_dev[device]['entity_id'], components.STATE_HOME,
known_dev[device]['default_state_attr'])
# Write new devices to known devices file
if not self.invalid_known_devices_file:
# For all devices we did not find, set state to NH
# But only if they have been gone for longer then the error time span
# Because we do not want to have stuff happening when the device does
# not show up for 1 scan beacuse of reboot etc
for device in temp_tracking_devices:
if now - known_dev[device]['last_seen'] > self.error_scanning:
known_dev_path = self.hass.get_config_path(KNOWN_DEVICES_FILE)
self.states.set(known_dev[device]['entity_id'],
components.STATE_NOT_HOME,
known_dev[device]['default_state_attr'])
# If we come along any unknown devices we will write them to the
# known devices file but only if we did not encounter an invalid
# known devices file
if not self.invalid_known_devices_file:
known_dev_path = self.path_known_devices_file
unknown_devices = [device for device in found_devices
if device not in known_dev]
if unknown_devices:
try:
# If file does not exist we will write the header too
is_new_file = not os.path.isfile(known_dev_path)
@ -176,7 +192,7 @@ class DeviceTracker(object):
with open(known_dev_path, 'a') as outp:
_LOGGER.info(
"Found %d new devices, updating %s",
len(unknown_devices), known_dev_path)
len(new_devices), known_dev_path)
writer = csv.writer(outp)
@ -184,109 +200,114 @@ class DeviceTracker(object):
writer.writerow((
"device", "name", "track", "picture"))
for device in unknown_devices:
for device in new_devices:
# See if the device scanner knows the name
# else defaults to unknown device
name = (self.device_scanner.get_device_name(device)
or "unknown_device")
writer.writerow((device, name, 0, ""))
known_dev[device] = {'name': name,
'track': False,
'picture': ""}
except IOError:
_LOGGER.exception(
"Error updating %s with %d new devices",
known_dev_path, len(unknown_devices))
known_dev_path, len(new_devices))
self.lock.release()
# pylint: disable=too-many-branches
def _read_known_devices_file(self):
""" Parse and process the known devices file. """
known_dev_path = self.hass.get_config_path(KNOWN_DEVICES_FILE)
# Read known devices if file exists
if os.path.isfile(self.path_known_devices_file):
self.lock.acquire()
# Return if no known devices file exists
if not os.path.isfile(known_dev_path):
return
known_devices = {}
self.lock.acquire()
with open(self.path_known_devices_file) as inp:
default_last_seen = datetime(1990, 1, 1)
self.untracked_devices.clear()
# Temp variable to keep track of which entity ids we use
# so we can ensure we have unique entity ids.
used_entity_ids = []
with open(known_dev_path) as inp:
default_last_seen = datetime(1990, 1, 1)
try:
for row in csv.DictReader(inp):
device = row['device']
# To track which devices need an entity_id assigned
need_entity_id = []
row['track'] = True if row['track'] == '1' else False
# All devices that are still in this set after we read the CSV file
# have been removed from the file and thus need to be cleaned up.
removed_devices = set(self.tracked.keys())
try:
for row in csv.DictReader(inp):
device = row['device']
if row['track'] == '1':
if device in self.tracked:
# Device exists
removed_devices.remove(device)
else:
# We found a new device
need_entity_id.append(device)
self.tracked[device] = {
'name': row['name'],
'last_seen': default_last_seen
}
# Update state_attr with latest from file
state_attr = {
ATTR_FRIENDLY_NAME: row['name']
}
if row['picture']:
row['default_state_attr'] = {
components.ATTR_ENTITY_PICTURE: row['picture']}
state_attr[ATTR_ENTITY_PICTURE] = row['picture']
else:
row['default_state_attr'] = None
self.tracked[device]['state_attr'] = state_attr
# If we track this device setup tracking variables
if row['track']:
row['last_seen'] = default_last_seen
else:
self.untracked_devices.add(device)
# Make sure that each device is mapped
# to a unique entity_id name
name = util.slugify(row['name']) if row['name'] \
else "unnamed_device"
# Remove existing devices that we no longer track
for device in removed_devices:
entity_id = self.tracked[device]['entity_id']
entity_id = ENTITY_ID_FORMAT.format(name)
tries = 1
_LOGGER.info("Removing entity %s", entity_id)
while entity_id in used_entity_ids:
tries += 1
self.hass.states.remove(entity_id)
suffix = "_{}".format(tries)
self.tracked.pop(device)
entity_id = ENTITY_ID_FORMAT.format(
name + suffix)
# Setup entity_ids for the new devices
used_entity_ids = [info['entity_id'] for device, info
in self.tracked.items()
if device not in need_entity_id]
row['entity_id'] = entity_id
used_entity_ids.append(entity_id)
for device in need_entity_id:
name = self.tracked[device]['name']
row['picture'] = row['picture']
entity_id = util.ensure_unique_string(
ENTITY_ID_FORMAT.format(util.slugify(name)),
used_entity_ids)
known_devices[device] = row
used_entity_ids.append(entity_id)
if not known_devices:
_LOGGER.warning(
"No devices to track. Please update %s.",
self.path_known_devices_file)
self.tracked[device]['entity_id'] = entity_id
# Remove entities that are no longer maintained
new_entity_ids = set([known_devices[dev]['entity_id']
for dev in known_devices
if known_devices[dev]['track']])
for entity_id in \
self.device_entity_ids - new_entity_ids:
_LOGGER.info("Removing entity %s", entity_id)
self.states.remove(entity_id)
# File parsed, warnings given if necessary
# entities cleaned up, make it available
self.known_devices = known_devices
_LOGGER.info("Loaded devices from %s",
self.path_known_devices_file)
except KeyError:
self.invalid_known_devices_file = True
if not self.tracked:
_LOGGER.warning(
("Invalid known devices file: %s. "
"We won't update it with new found devices."),
self.path_known_devices_file)
"No devices to track. Please update %s.",
known_dev_path)
finally:
self.lock.release()
_LOGGER.info("Loaded devices from %s", known_dev_path)
except KeyError:
self.invalid_known_devices_file = True
_LOGGER.warning(
("Invalid known devices file: %s. "
"We won't update it with new found devices."),
known_dev_path)
finally:
self.lock.release()

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@ -1,2 +1,2 @@
""" 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"
],
"dependencies": {
"webcomponentsjs": "Polymer/webcomponentsjs#~0.5.1",
"font-roboto": "Polymer/font-roboto#~0.5.1",
"core-header-panel": "Polymer/core-header-panel#~0.5.1",
"core-toolbar": "Polymer/core-toolbar#~0.5.1",
"core-tooltip": "Polymer/core-tooltip#~0.5.1",
"core-menu": "Polymer/core-menu#~0.5.1",
"core-item": "Polymer/core-item#~0.5.1",
"core-input": "Polymer/core-input#~0.5.1",
"core-icons": "polymer/core-icons#~0.5.1",
"core-image": "polymer/core-image#~0.5.1",
"paper-toast": "Polymer/paper-toast#~0.5.1",
"paper-dialog": "Polymer/paper-dialog#~0.5.1",
"paper-spinner": "Polymer/paper-spinner#~0.5.1",
"paper-button": "Polymer/paper-button#~0.5.1",
"paper-input": "Polymer/paper-input#~0.5.1",
"paper-toggle-button": "polymer/paper-toggle-button#~0.5.1",
"paper-tabs": "polymer/paper-tabs#~0.5.1",
"paper-icon-button": "polymer/paper-icon-button#~0.5.1",
"paper-menu-button": "polymer/paper-menu-button#~0.5.1",
"paper-dropdown": "polymer/paper-dropdown#~0.5.1",
"paper-item": "polymer/paper-item#~0.5.1",
"moment": "~2.8.3"
"webcomponentsjs": "Polymer/webcomponentsjs#~0.5.2",
"font-roboto": "Polymer/font-roboto#~0.5.2",
"core-header-panel": "Polymer/core-header-panel#~0.5.2",
"core-toolbar": "Polymer/core-toolbar#~0.5.2",
"core-tooltip": "Polymer/core-tooltip#~0.5.2",
"core-menu": "Polymer/core-menu#~0.5.2",
"core-item": "Polymer/core-item#~0.5.2",
"core-input": "Polymer/core-input#~0.5.2",
"core-icons": "polymer/core-icons#~0.5.2",
"core-image": "polymer/core-image#~0.5.2",
"paper-toast": "Polymer/paper-toast#~0.5.2",
"paper-dialog": "Polymer/paper-dialog#~0.5.2",
"paper-spinner": "Polymer/paper-spinner#~0.5.2",
"paper-button": "Polymer/paper-button#~0.5.2",
"paper-input": "Polymer/paper-input#~0.5.2",
"paper-toggle-button": "polymer/paper-toggle-button#~0.5.2",
"paper-tabs": "polymer/paper-tabs#~0.5.2",
"paper-icon-button": "polymer/paper-icon-button#~0.5.2",
"paper-menu-button": "polymer/paper-menu-button#~0.5.2",
"paper-dropdown": "polymer/paper-dropdown#~0.5.2",
"paper-item": "polymer/paper-item#~0.5.2",
"moment": "~2.8.4",
"core-style": "polymer/core-style#~0.5.2"
}
}

View File

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

View File

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

View File

@ -31,6 +31,7 @@
/* Color the icon if light or sun is 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] {
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="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">
<template>
@ -9,45 +11,68 @@
width: 100%;
}
state-card {
display: inline-block;
}
@media all and (min-width: 764px) {
:host {
padding-bottom: 8px;
}
state-card {
.state-card {
width: calc(50% - 44px);
margin: 8px 0 0 8px;
}
}
@media all and (min-width: 1100px) {
state-card {
.state-card {
width: calc(33% - 38px);
}
}
@media all and (min-width: 1450px) {
state-card {
.state-card {
width: calc(25% - 42px);
}
}
</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>
<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}}"
cb_turn_on="{{api.turn_on}}"
cb_turn_off="{{api.turn_off}}"
cb_edit={{editCallback}}>
</state-card>
</state-card-display>
</template>
<template repeat="{{state in getStates(api.states, filter)}}">
<template bind ref="{{state.cardType}}"></template>
</template>
</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.
"""
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"
DEPENDENCIES = []
@ -14,32 +18,32 @@ DEPENDENCIES = []
def volume_up(hass):
""" 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):
""" 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):
""" 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):
""" 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):
""" 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):
""" 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
@ -56,27 +60,27 @@ def setup(hass, config):
keyboard = pykeyboard.PyKeyboard()
keyboard.special_key_assignment()
hass.services.register(DOMAIN, components.SERVICE_VOLUME_UP,
hass.services.register(DOMAIN, SERVICE_VOLUME_UP,
lambda service:
keyboard.tap_key(keyboard.volume_up_key))
hass.services.register(DOMAIN, components.SERVICE_VOLUME_DOWN,
hass.services.register(DOMAIN, SERVICE_VOLUME_DOWN,
lambda service:
keyboard.tap_key(keyboard.volume_down_key))
hass.services.register(DOMAIN, components.SERVICE_VOLUME_MUTE,
hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE,
lambda service:
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:
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:
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:
keyboard.tap_key(keyboard.media_prev_track_key))

View File

@ -52,12 +52,12 @@ import logging
import os
import csv
import homeassistant as ha
from homeassistant.loader import get_component
import homeassistant.util as util
from homeassistant.components import (
group, extract_entity_ids, STATE_ON,
SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
from homeassistant.const import (
STATE_ON, 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"
@ -82,6 +82,12 @@ ATTR_BRIGHTNESS = "brightness"
# String representing a profile (built-in ones or external defined)
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"
_LOGGER = logging.getLogger(__name__)
@ -96,51 +102,39 @@ def is_on(hass, entity_id=None):
# pylint: disable=too-many-arguments
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. """
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:
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)
hass.services.call(DOMAIN, SERVICE_TURN_ON, data)
def turn_off(hass, entity_id=None, transition=None):
""" 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:
data[ATTR_ENTITY_ID] = entity_id
if transition is not None:
data[ATTR_TRANSITION] = transition
hass.call_service(DOMAIN, SERVICE_TURN_OFF, data)
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
# pylint: disable=too-many-branches, too-many-locals
def setup(hass, config):
""" 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
profile_paths = [os.path.join(os.path.dirname(__file__),
LIGHT_PROFILES_FILE),
@ -169,20 +163,9 @@ def setup(hass, config):
return False
# Load platform
light_type = config[DOMAIN][ha.CONF_TYPE]
lights = platform_devices_from_config(config, DOMAIN, hass, _LOGGER)
light_init = get_component('light.{}'.format(light_type))
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")
if not lights:
return False
ent_to_light = {}
@ -198,7 +181,7 @@ def setup(hass, config):
entity_id = util.ensure_unique_string(
ENTITY_ID_FORMAT.format(util.slugify(name)),
list(ent_to_light.keys()))
ent_to_light.keys())
light.entity_id = entity_id
ent_to_light[entity_id] = light
@ -249,7 +232,6 @@ def setup(hass, config):
profile = profiles.get(dat.get(ATTR_PROFILE))
if profile:
# *color, bright = profile
*params[ATTR_XY_COLOR], params[ATTR_BRIGHTNESS] = profile
if ATTR_BRIGHTNESS in dat:
@ -288,6 +270,13 @@ def setup(hass, config):
# ValueError if not all values can be converted to int
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:
# pylint: disable=star-args
light.turn_on(**params)

View File

@ -1,19 +1,22 @@
""" Support for Hue lights. """
import logging
import socket
from datetime import datetime, timedelta
from datetime import timedelta
import homeassistant as ha
from homeassistant.components import ToggleDevice, ATTR_FRIENDLY_NAME
import homeassistant.util as util
from homeassistant.helpers import ToggleDevice
from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_HOST
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_FORCED_SCANS = timedelta(seconds=1)
PHUE_CONFIG_FILE = "phue.conf"
def get_lights(hass, config):
def get_devices(hass, config):
""" Gets the Hue lights. """
logger = logging.getLogger(__name__)
try:
@ -23,7 +26,7 @@ def get_lights(hass, config):
return []
host = config.get(ha.CONF_HOST, None)
host = config.get(CONF_HOST, None)
try:
bridge = phue.Bridge(
@ -37,25 +40,9 @@ def get_lights(hass, config):
lights = {}
def update_lights(force_reload=False):
""" Updates the light states. """
now = datetime.now()
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
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update_lights():
""" Updates the Hue light objects with latest info from the bridge. """
try:
api = bridge.get_api()
except socket.error:
@ -109,6 +96,15 @@ class HueLight(ToggleDevice):
if ATTR_XY_COLOR in kwargs:
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)
def turn_off(self, **kwargs):
@ -142,4 +138,4 @@ class HueLight(ToggleDevice):
def update(self):
""" 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
from homeassistant.components import STATE_ON, STATE_OFF
from homeassistant.const import STATE_ON, STATE_OFF
import homeassistant.util as util
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
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 = []
DOMAIN = "sun"
@ -35,7 +37,7 @@ def next_setting(hass, entity_id=None):
state = hass.states.get(ENTITY_ID)
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):
# AttributeError if state is None
# 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)
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):
# AttributeError if state is None
# KeyError if STATE_ATTR_NEXT_RISING does not exist
@ -60,10 +62,9 @@ def setup(hass, config):
""" Tracks the state of the sun. """
logger = logging.getLogger(__name__)
if not util.validate_config(config,
{ha.DOMAIN: [ha.CONF_LATITUDE,
ha.CONF_LONGITUDE]},
logger):
if not validate_config(config,
{ha.DOMAIN: [CONF_LATITUDE, CONF_LONGITUDE]},
logger):
return False
try:
@ -74,8 +75,8 @@ def setup(hass, config):
sun = ephem.Sun() # pylint: disable=no-member
latitude = config[ha.DOMAIN][ha.CONF_LATITUDE]
longitude = config[ha.DOMAIN][ha.CONF_LONGITUDE]
latitude = config[ha.DOMAIN][CONF_LATITUDE]
longitude = config[ha.DOMAIN][CONF_LONGITUDE]
# Validate latitude and longitude
observer = ephem.Observer()
@ -123,8 +124,8 @@ def setup(hass, config):
new_state, next_change.strftime("%H:%M"))
state_attributes = {
STATE_ATTR_NEXT_RISING: util.datetime_to_str(next_rising_dt),
STATE_ATTR_NEXT_SETTING: util.datetime_to_str(next_setting_dt)
STATE_ATTR_NEXT_RISING: datetime_to_str(next_rising_dt),
STATE_ATTR_NEXT_SETTING: datetime_to_str(next_setting_dt)
}
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.
"""
import logging
from datetime import datetime, timedelta
from datetime import timedelta
import homeassistant as ha
import homeassistant.util as util
from homeassistant.loader import get_component
from homeassistant.components import (
group, extract_entity_ids, STATE_ON,
SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
from homeassistant.const import (
STATE_ON, 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 = 'switch'
DEPENDENCIES = []
@ -22,10 +22,8 @@ ENTITY_ID_ALL_SWITCHES = group.ENTITY_ID_FORMAT.format(
ENTITY_ID_FORMAT = DOMAIN + '.{}'
ATTR_TODAY_KWH = "today_kwh"
ATTR_CURRENT_POWER = "current_power"
ATTR_TODAY_ON_TIME = "today_on_time"
ATTR_TODAY_STANDBY_TIME = "today_standby_time"
ATTR_TODAY_MWH = "today_mwh"
ATTR_CURRENT_POWER_MWH = "current_power_mwh"
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
@ -43,37 +41,23 @@ def turn_on(hass, entity_id=None):
""" Turns all or specified switch on. """
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):
""" Turns all or specified switch off. """
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):
""" Track states and offer events for switches. """
logger = logging.getLogger(__name__)
if not util.validate_config(config, {DOMAIN: [ha.CONF_TYPE]}, logger):
return False
switches = platform_devices_from_config(config, DOMAIN, hass, logger)
switch_type = config[DOMAIN][ha.CONF_TYPE]
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")
if not switches:
return False
# Setup a dict mapping entity IDs to devices
@ -90,27 +74,22 @@ def setup(hass, config):
entity_id = util.ensure_unique_string(
ENTITY_ID_FORMAT.format(util.slugify(name)),
list(ent_to_switch.keys()))
ent_to_switch.keys())
switch.entity_id = entity_id
ent_to_switch[entity_id] = switch
# 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. """
# First time this method gets called, force_reload should be True
if force_reload or \
datetime.now() - update_states.last_updated > \
MIN_TIME_BETWEEN_SCANS:
logger.info("Updating switch states")
logger.info("Updating switch states")
update_states.last_updated = datetime.now()
for switch in switches:
switch.update_ha_state(hass)
for switch in switches:
switch.update_ha_state(hass)
update_states(None, True)
update_states(None)
def handle_switch_service(service):
""" Handles calls to the switch services. """

View File

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

View File

@ -1,12 +1,14 @@
""" Support for WeMo switchces. """
import logging
import homeassistant as ha
from homeassistant.components import ToggleDevice, ATTR_FRIENDLY_NAME
from homeassistant.helpers import ToggleDevice
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
def get_switches(hass, config):
def get_devices(hass, config):
""" Find and return WeMo switches. """
try:
@ -21,9 +23,9 @@ def get_switches(hass, config):
return []
if ha.CONF_HOSTS in config:
if CONF_HOSTS in config:
switches = (pywemo.device_from_host(host) for host
in config[ha.CONF_HOSTS].split(","))
in config[CONF_HOSTS].split(","))
else:
logging.getLogger(__name__).info("Scanning for WeMo devices")
@ -38,7 +40,6 @@ class WemoSwitch(ToggleDevice):
""" represents a WeMo switch within home assistant. """
def __init__(self, wemo):
self.wemo = wemo
self.state_attr = {ATTR_FRIENDLY_NAME: wemo.name}
def get_name(self):
""" Returns the name of the switch if any. """
@ -58,4 +59,13 @@ class WemoSwitch(ToggleDevice):
def get_state_attributes(self):
""" 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
import homeassistant.util as util
from homeassistant.components import (ATTR_FRIENDLY_NAME,
ATTR_UNIT_OF_MEASUREMENT)
from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT
# The domain of your component. Should be equal to the name of your component
DOMAIN = "tellstick_sensor"
@ -88,7 +87,7 @@ def setup(hass, config):
}
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_descriptions[sensor_value.datatype]
sensor_value_name = '{} {}'.format(
@ -117,7 +116,7 @@ def setup(hass, config):
]
def update_sensor_state(sensor):
"Updates all the sensor values from the sensor"
""" Updates all the sensor values from the sensor """
try:
sensor_name = config[DOMAIN][str(sensor.id)]
except KeyError:
@ -132,7 +131,7 @@ def setup(hass, config):
# pylint: disable=unused-argument
def update_sensors_state(time):
"Update the state of all sensors"
""" Update the state of all sensors """
for sensor in sensors:
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 logging
from homeassistant.util import OrderedSet
PREPARED = False
# List of available components
AVAILABLE_COMPONENTS = []
@ -30,6 +34,8 @@ _LOGGER = logging.getLogger(__name__)
def prepare(hass):
""" Prepares the loading of components. """
global PREPARED # pylint: disable=global-statement
# Load the built-in components
import homeassistant.components as components
@ -56,15 +62,22 @@ def prepare(hass):
# just might output more errors.
for fil in os.listdir(custom_path):
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:
# For files we will strip out .py extension
AVAILABLE_COMPONENTS.append(
'custom_components.{}'.format(fil[0:-3]))
PREPARED = True
def set_component(comp_name, component):
""" Sets a component in the cache. """
_check_prepared()
_COMPONENT_CACHE[comp_name] = component
@ -76,6 +89,8 @@ def get_component(comp_name):
if comp_name in _COMPONENT_CACHE:
return _COMPONENT_CACHE[comp_name]
_check_prepared()
# If we ie. try to load custom_components.switch.wemo but the parent
# custom_components.switch does not exist, importing it will trigger
# 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)
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
SERVER_PORT = 8123
AUTH_HEADER = "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"
from homeassistant.const import (
SERVER_PORT, AUTH_HEADER, URL_API, URL_API_STATES, URL_API_STATES_ENTITY,
URL_API_EVENTS, URL_API_EVENTS_EVENT, URL_API_SERVICES,
URL_API_SERVICES_SERVICE, URL_API_EVENT_FORWARD)
METHOD_GET = "get"
METHOD_POST = "post"
METHOD_DELETE = "delete"
_LOGGER = logging.getLogger(__name__)
@ -94,6 +87,10 @@ class API(object):
_LOGGER.exception(error)
raise ha.HomeAssistantError(error)
def __repr__(self):
return "API({}, {}, {})".format(
self.host, self.api_password, self.port)
class HomeAssistant(ha.HomeAssistant):
""" Home Assistant that forwards work. """
@ -122,18 +119,23 @@ class HomeAssistant(ha.HomeAssistant):
import random
# 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)
# 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,
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):
""" Stops Home Assistant and shuts down all threads. """
_LOGGER.info("Stopping")
@ -141,6 +143,9 @@ class HomeAssistant(ha.HomeAssistant):
self.bus.fire(ha.EVENT_HOMEASSISTANT_STOP,
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
self._pool.block_till_done()
@ -285,30 +290,51 @@ def validate_api(api):
def connect_remote_events(from_api, to_api):
""" Sets up from_api to forward all events to to_api. """
data = {'host': to_api.host, 'api_password': to_api.api_password}
if to_api.port is not None:
data['port'] = to_api.port
data = {
'host': to_api.host,
'api_password': to_api.api_password,
'port': to_api.port
}
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:
pass
_LOGGER.exception("Error setting up event forwarding")
return False
def disconnect_remote_events(from_api, to_api):
""" Disconnects forwarding events from from_api to to_api. """
data = {'host': to_api.host, '_METHOD': 'DELETE'}
if to_api.port is not None:
data['port'] = to_api.port
data = {
'host': to_api.host,
'port': to_api.port
}
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:
pass
_LOGGER.exception("Error removing an event forwarder")
return False
def get_event_listeners(api):
@ -336,7 +362,7 @@ def fire_event(api, event_type, data=None):
req.status_code, req.text)
except ha.HomeAssistantError:
pass
_LOGGER.exception("Error firing event")
def get_state(api, entity_id):
@ -372,7 +398,7 @@ def get_states(api):
# ValueError if req.json() can't parse the json
_LOGGER.exception("Error fetching states")
return {}
return []
def set_state(api, entity_id, new_state, attributes=None):

View File

@ -4,12 +4,15 @@ homeassistant.util
Helper methods for various modules.
"""
import collections
from itertools import chain
import threading
import queue
import datetime
from datetime import datetime, timedelta
import re
import enum
import socket
from functools import wraps
RE_SANITIZE_FILENAME = re.compile(r'(~|\.\.|/|\\)')
RE_SANITIZE_PATH = re.compile(r'(~|\.(\.)+)')
@ -49,11 +52,19 @@ def str_to_datetime(dt_str):
@rtype: datetime
"""
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
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):
""" Splits a state entity_id into domain, object_id. """
return entity_id.split(".", 1)
@ -65,7 +76,7 @@ def repr_helper(inp):
return ", ".join(
repr_helper(key)+"="+repr_helper(item) for key, item
in inp.items())
elif isinstance(inp, datetime.datetime):
elif isinstance(inp, datetime):
return datetime_to_str(inp)
else:
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.
If preferred string exists will append _2, _3, .. """
string = preferred_string
current_strings = list(current_strings)
tries = 1
@ -176,93 +188,199 @@ class OrderedEnum(enum.Enum):
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.
items is a dict with per domain which attributes we require.
logger is the logger from the caller to log the errors to.
Calling a method a second time during the interval will return None.
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
for domain in items.keys():
config.setdefault(domain, {})
# pylint: disable=too-few-public-methods
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:
logger.error(
"Missing required configuration items in {}: {}".format(
domain, ", ".join(errors)))
def __call__(self, method):
lock = threading.Lock()
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):
""" A simple queue-based thread pool.
Will initiate it's workers using worker(queue).start() """
""" A priority queue-based thread pool. """
# 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
worker_count: number of threads to run that handle jobs
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.current_jobs = current_jobs = []
self.worker_count = worker_count
self.busy_callback = busy_callback
self.busy_warning_limit = worker_count**2
self._job_handler = job_handler
self._busy_callback = busy_callback
self.worker_count = 0
self.busy_warning_limit = 0
self._work_queue = queue.PriorityQueue()
self.current_jobs = []
self._lock = threading.RLock()
self._quit_task = object()
self.running = True
for _ in range(worker_count):
worker = threading.Thread(target=_threadpool_worker,
args=(work_queue, current_jobs,
job_handler, self._quit_task))
self.add_worker()
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.start()
self.running = True
self.worker_count += 1
self.busy_warning_limit = self.worker_count * 3
def add_job(self, priority, job):
""" Add a job to be sent to the workers. """
def remove_worker(self):
""" Removes a worker from the thread pool. Resets warning limit. """
with self._lock:
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
if self.work_queue.qsize() > self.busy_warning_limit \
and self.busy_callback is not None:
if self._work_queue.qsize() > self.busy_warning_limit \
and self._busy_callback is not None:
# Increase limit we will issue next warning
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):
""" Blocks till all work is done. """
self.work_queue.join()
self._work_queue.join()
def stop(self):
""" Stops all the threads. """
@ -270,19 +388,41 @@ class ThreadPool(object):
if not self.running:
return
# Clear the queue
while self.work_queue.qsize() > 0:
self.work_queue.get()
self.work_queue.task_done()
# Ensure all current jobs finish
self.block_till_done()
# Tell the workers to quit
for _ in range(self.worker_count):
self.add_job(1000, self._quit_task)
self.remove_worker()
self.running = False
# Wait till all workers have quit
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):
""" Holds a priority and a value. Used within PriorityQueue. """
@ -294,27 +434,3 @@ class PriorityQueueItem(object):
def __lt__(self, other):
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
tellcore-py>=1.0.4
# namp_tracker plugin
python-libnmap

View File

@ -2,4 +2,4 @@
pylint homeassistant
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"]))