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
commit
99b1cbf9b5
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
344
README.md
|
@ -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:
|
||||
|
||||
* <config file directory>/custom_components/<component name>.py
|
||||
* homeassistant/components/<component name>.py (built-in components)
|
||||
|
||||
Once loaded, a component will only be setup if all dependencies can be loaded and are able to setup. Keep an eye on the logs to see if loading and setup of your component went well.
|
||||
|
||||
*Warning:* You can override a built-in component by offering a component with the same name in your custom_components folder. This is not recommended and may lead to unexpected behavior!
|
||||
|
||||
After a component is loaded the bootstrapper will call its setup method `setup(hass, config)`:
|
||||
|
||||
| Parameter | Description |
|
||||
| --------- | ----------- |
|
||||
| hass | The Home Assistant object. Call its methods to track time, register services or listen for events. [Overview of available methods.](https://github.com/balloob/home-assistant/blob/master/homeassistant/__init__.py#L54) |
|
||||
| config | A dict containing the configuration. The keys of the config-dict are component names and the value is another dict with configuration attributes. |
|
||||
|
||||
**Tips on using the Home Assistant object parameter**<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/<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/<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/<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/<domain>/<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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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]
|
||||
|
|
|
@ -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()
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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'}}))
|
|
@ -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))
|
|
@ -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'))
|
|
@ -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))
|
|
@ -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])
|
|
@ -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)
|
|
@ -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')
|
||||
|
|
@ -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'
|
||||
}}))
|
|
@ -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'},
|
||||
}
|
||||
))
|
|
@ -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. """
|
|
@ -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))
|
|
@ -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']))
|
|
@ -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)
|
|
@ -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))
|
|
@ -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):
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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. """
|
||||
|
|
|
@ -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
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
@ -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))
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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. """
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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())
|
|
@ -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."))
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -17,3 +17,6 @@ pyuserinput>=0.1.9
|
|||
|
||||
# switch.tellstick, tellstick_sensor
|
||||
tellcore-py>=1.0.4
|
||||
|
||||
# namp_tracker plugin
|
||||
python-libnmap
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
|
||||
pylint homeassistant
|
||||
flake8 homeassistant --exclude bower_components,external
|
||||
python3 -m unittest discover test
|
||||
python3 -m unittest discover ha_test
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
"""
|
||||
Module to be loaded by the Loader test.
|
||||
"""
|
|
@ -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
|
|
@ -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
|
|
@ -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'))
|
|
@ -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"]))
|
Loading…
Reference in New Issue