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/*
|
config/custom_components/*
|
||||||
!config/custom_components/example.py
|
!config/custom_components/example.py
|
||||||
|
!config/custom_components/hello_world.py
|
||||||
|
|
||||||
# Hide sublime text stuff
|
# Hide sublime text stuff
|
||||||
*.sublime-project
|
*.sublime-project
|
||||||
|
|
|
@ -7,6 +7,6 @@ install:
|
||||||
script:
|
script:
|
||||||
- flake8 homeassistant --exclude bower_components,external
|
- flake8 homeassistant --exclude bower_components,external
|
||||||
- pylint homeassistant
|
- pylint homeassistant
|
||||||
- coverage run --source=homeassistant -m unittest discover test
|
- coverage run --source=homeassistant -m unittest discover ha_test
|
||||||
after_success:
|
after_success:
|
||||||
- coveralls
|
- coveralls
|
||||||
|
|
|
@ -1,23 +1,6 @@
|
||||||
# Adding support for a new device
|
# Adding support for a new device
|
||||||
|
|
||||||
You've probably came here beacuse you noticed that your favorite device is not supported and want to add it.
|
For help on building your component, please see the See the [developer documentation on home-assistant.io](https://home-assistant.io/developers/).
|
||||||
|
|
||||||
First step is to decide under which component the device has to reside. Each component is responsible for a specific domain within Home Assistant. An example is the switch component, which is responsible for interaction with different types of switches. The switch component consists of the following files:
|
|
||||||
|
|
||||||
**homeassistant/components/switch/\_\_init\_\_.py**<br />
|
|
||||||
Contains the Switch component code.
|
|
||||||
|
|
||||||
**homeassistant/components/switch/wemo.py**<br />
|
|
||||||
Contains the code to interact with WeMo switches. Called if type=wemo in switch config.
|
|
||||||
|
|
||||||
**homeassistant/components/switch/tellstick.py**
|
|
||||||
Contains the code to interact with Tellstick switches. Called if type=tellstick in switch config.
|
|
||||||
|
|
||||||
If a component exists, your job is easy. Have a look at how the component works with other platforms and create a similar file for the platform that you would like to add. If you cannot find a suitable component, you'll have to add it yourself. When writing a component try to structure it after the Switch component to maximize reusability.
|
|
||||||
|
|
||||||
Communication between Home Assistant and devices should happen via third-party libraries that implement the device API. This will make sure the platform support code stays as small as possible.
|
|
||||||
|
|
||||||
For help on building your component, please see the See the documentation on [further customizing Home Assistant](https://github.com/balloob/home-assistant#further-customizing-home-assistant).
|
|
||||||
|
|
||||||
After you finish adding support for your device:
|
After you finish adding support for your device:
|
||||||
|
|
||||||
|
|
344
README.md
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)
|
# Home Assistant [![Build Status](https://travis-ci.org/balloob/home-assistant.svg?branch=master)](https://travis-ci.org/balloob/home-assistant) [![Coverage Status](https://img.shields.io/coveralls/balloob/home-assistant.svg)](https://coveralls.io/r/balloob/home-assistant?branch=master)
|
||||||
|
|
||||||
|
This is the source for Home Assistant. For installation instructions, tutorials and the docs, please see [https://home-assistant.io](https://home-assistant.io). For a functioning demo frontend of Home Assistant, [click here](https://home-assistant.io/demo/).
|
||||||
|
|
||||||
Home Assistant is a home automation platform running on Python 3. The goal of Home Assistant is to be able to track and control all devices at home and offer a platform for automating control.
|
Home Assistant is a home automation platform running on Python 3. The goal of Home Assistant is to be able to track and control all devices at home and offer a platform for automating control.
|
||||||
|
|
||||||
It offers the following functionality through built-in components:
|
It offers the following functionality through built-in components:
|
||||||
|
@ -23,7 +25,7 @@ Home Assistant also includes functionality for controlling HTPCs:
|
||||||
* Download files
|
* Download files
|
||||||
* Open URLs in the default browser
|
* Open URLs in the default browser
|
||||||
|
|
||||||
![screenshot-states](https://raw.github.com/balloob/home-assistant/master/docs/screenshots.png)
|
[![screenshot-states](https://raw.github.com/balloob/home-assistant/master/docs/screenshots.png)](https://home-assistant.io/demo/)
|
||||||
|
|
||||||
The system is built modular so support for other devices or actions can be implemented easily. See also the [section on architecture](#architecture) and the [section on customizing](#customizing).
|
The system is built modular so support for other devices or actions can be implemented easily. See also the [section on architecture](#architecture) and the [section on customizing](#customizing).
|
||||||
|
|
||||||
|
@ -55,6 +57,8 @@ After you got the demo mode running it is time to enable some real components an
|
||||||
|
|
||||||
*Note:* you can append `?api_password=YOUR_PASSWORD` to the url of the web interface to log in automatically.
|
*Note:* you can append `?api_password=YOUR_PASSWORD` to the url of the web interface to log in automatically.
|
||||||
|
|
||||||
|
*Note:* for the light and switch component, you can specify multiple platforms by using sequential sections: [switch], [switch 2], [switch 3] etc
|
||||||
|
|
||||||
### Philips Hue
|
### Philips Hue
|
||||||
To get Philips Hue working you will have to connect Home Assistant to the Hue bridge.
|
To get Philips Hue working you will have to connect Home Assistant to the Hue bridge.
|
||||||
|
|
||||||
|
@ -68,7 +72,7 @@ After that add the following lines to your `home-assistant.conf`:
|
||||||
|
|
||||||
```
|
```
|
||||||
[light]
|
[light]
|
||||||
type=hue
|
platform=hue
|
||||||
```
|
```
|
||||||
|
|
||||||
### Wireless router
|
### Wireless router
|
||||||
|
@ -77,7 +81,7 @@ Your wireless router is used to track which devices are connected. Three differe
|
||||||
|
|
||||||
```
|
```
|
||||||
[device_tracker]
|
[device_tracker]
|
||||||
type=netgear
|
platform=netgear
|
||||||
host=192.168.1.1
|
host=192.168.1.1
|
||||||
username=admin
|
username=admin
|
||||||
password=MY_PASSWORD
|
password=MY_PASSWORD
|
||||||
|
@ -87,336 +91,12 @@ password=MY_PASSWORD
|
||||||
|
|
||||||
*Note on luci:* before the Luci scanner can be used you have to install the luci RPC package on OpenWRT: `opkg install luci-mod-rpc`.
|
*Note on luci:* before the Luci scanner can be used you have to install the luci RPC package on OpenWRT: `opkg install luci-mod-rpc`.
|
||||||
|
|
||||||
Once tracking the `device_tracker` component will maintain a file in your config dir called `known_devices.csv`. Edit this file to adjust which devices have to be tracked.
|
Once tracking, the `device_tracker` component will maintain a file in your config dir called `known_devices.csv`. Edit this file to adjust which devices have to be tracked.
|
||||||
|
|
||||||
<a name='customizing'></a>
|
As an alternative to the router-based device tracking, it is possible to directly scan the network for devices by using nmap. The IP addresses to scan can be specified in any format that nmap understands, including the network-prefix notation (`192.168.1.1/24`) and the range notation (`192.168.1.1-255`).
|
||||||
## Further customizing Home Assistant
|
|
||||||
|
|
||||||
Home Assistant can be extended by components. Components can listen for- or trigger events and offer services. Components are written in Python and can do all the goodness that Python has to offer.
|
|
||||||
|
|
||||||
Home Assistant offers [built-in components](#components) but it is easy to built your own. An example component can be found in [`/config/custom_components/example.py`](https://github.com/balloob/home-assistant/blob/master/config/custom_components/example.py).
|
|
||||||
|
|
||||||
*Note:* Home Assistant will use the directory that contains your config file as the directory that holds your customizations. By default this is the `./config` folder but this can be pointed anywhere on the filesystem by using the `--config /YOUR/CONFIG/PATH/` argument.
|
|
||||||
|
|
||||||
A component will be loaded on start if a section (ie. `[light]`) for it exists in the config file or a module that depends on the component is loaded. When loading a component Home Assistant will check the following paths:
|
|
||||||
|
|
||||||
* <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:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
[example]
|
[device_tracker]
|
||||||
host=paulusschoutsen.nl
|
platform=nmap_tracker
|
||||||
|
hosts=192.168.1.1/24
|
||||||
```
|
```
|
||||||
|
|
||||||
Then in the setup-method of your component you will be able to refer to `config[example][host]` to get the value `paulusschoutsen.nl`.
|
|
||||||
|
|
||||||
If you want to get your component included with the Home Assistant distribution, please take a look at the [contributing page](https://github.com/balloob/home-assistant/blob/master/CONTRIBUTING.md).
|
|
||||||
|
|
||||||
<a name="architecture"></a>
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
The core of Home Assistant exists of three parts; an Event Bus for firing events, a State Machine that keeps track of the state of things and a Service Registry to manage services.
|
|
||||||
|
|
||||||
![home assistant architecture](https://raw.github.com/balloob/home-assistant/master/docs/architecture.png)
|
|
||||||
|
|
||||||
For example to control the lights there are two components. One is the device_tracker that polls the wireless router for connected devices and updates the state of the tracked devices in the State Machine to be either 'Home' or 'Not Home'.
|
|
||||||
|
|
||||||
When a state is changed a state_changed event is fired for which the device_sun_light_trigger component is listening. Based on the new state of the device combined with the state of the sun it will decide if it should turn the lights on or off:
|
|
||||||
|
|
||||||
In the event that the state of device 'Paulus Nexus 5' changes to the 'Home' state:
|
|
||||||
If the sun has set and the lights are not on:
|
|
||||||
Turn on the lights
|
|
||||||
|
|
||||||
In the event that the combined state of all tracked devices changes to 'Not Home':
|
|
||||||
If the lights are on:
|
|
||||||
Turn off the lights
|
|
||||||
|
|
||||||
In the event of the sun setting:
|
|
||||||
If the lights are off and the combined state of all tracked device equals 'Home':
|
|
||||||
Turn on the lights
|
|
||||||
|
|
||||||
By using the Bus as a central communication hub between components it is easy to replace components or add functionality. For example if you would want to change the way devices are detected you only have to write a component that updates the device states in the State Machine.
|
|
||||||
|
|
||||||
<a name='components'></a>
|
|
||||||
### Components
|
|
||||||
|
|
||||||
**sun**
|
|
||||||
Tracks the state of the sun and when the next sun rising and setting will occur.
|
|
||||||
Depends on: config variables common/latitude and common/longitude
|
|
||||||
Action: maintains state of `weather.sun` including attributes `next_rising` and `next_setting`
|
|
||||||
|
|
||||||
**device_tracker**
|
|
||||||
Keeps track of which devices are currently home.
|
|
||||||
Action: sets the state per device and maintains a combined state called `all_devices`. Keeps track of known devices in the file `config/known_devices.csv`.
|
|
||||||
|
|
||||||
**light**
|
|
||||||
Keeps track which lights are turned on and can control the lights. It has [4 built-in light profiles](https://github.com/balloob/home-assistant/blob/master/homeassistant/components/light/light_profiles.csv) which you're able to extend by putting a light_profiles.csv file in your config dir.
|
|
||||||
|
|
||||||
Registers services `light/turn_on` and `light/turn_off` to turn a or all lights on or off.
|
|
||||||
|
|
||||||
Optional service data:
|
|
||||||
- `entity_id` - only act on specific light. Else targets all.
|
|
||||||
- `transition_seconds` - seconds to take to swithc to new state.
|
|
||||||
- `profile` - which light profile to use.
|
|
||||||
- `xy_color` - two comma seperated floats that represent the color in XY
|
|
||||||
- `rgb_color` - three comma seperated integers that represent the color in RGB
|
|
||||||
- `brightness` - integer between 0 and 255 for how bright the color should be
|
|
||||||
|
|
||||||
**switch**
|
|
||||||
Keeps track which switches are in the network, their state and allows you to control them.
|
|
||||||
|
|
||||||
Registers services `switch/turn_on` and `switch/turn_off` to turn a or all switches on or off.
|
|
||||||
|
|
||||||
Optional service data:
|
|
||||||
- `entity_id` - only act on specific switch. Else targets all.
|
|
||||||
|
|
||||||
**device_sun_light_trigger**
|
|
||||||
Turns lights on or off using a light control component based on state of the sun and devices that are home.
|
|
||||||
Depends on: light control, track_sun, device_tracker
|
|
||||||
Action:
|
|
||||||
|
|
||||||
* Turns lights off when all devices leave home.
|
|
||||||
* Turns lights on when a device is home while sun is setting.
|
|
||||||
* Turns lights on when a device gets home after sun set.
|
|
||||||
|
|
||||||
**chromecast**
|
|
||||||
Registers 7 services to control playback on a Chromecast: `turn_off`, `volume_up`, `volume_down`, `media_play_pause`, `media_play`, `media_pause`, `media_next_track`.
|
|
||||||
|
|
||||||
Registers three services to start playing YouTube video's on the ChromeCast.
|
|
||||||
|
|
||||||
Service `chromecast/play_youtube_video` starts playing the specified video on the YouTube app on the ChromeCast. Specify video using `video` in service_data.
|
|
||||||
|
|
||||||
Service `chromecast/start_fireplace` will start a YouTube movie simulating a fireplace and the `chromecast/start_epic_sax` service will start playing Epic Sax Guy 10h version.
|
|
||||||
|
|
||||||
**keyboard**
|
|
||||||
Registers services that will simulate key presses on the keyboard. It currently offers the following Buttons as a Service (BaaS): `keyboard/volume_up`, `keyboard/volume_down` and `keyboard/media_play_pause`
|
|
||||||
This actor depends on: PyUserInput
|
|
||||||
|
|
||||||
**downloader**
|
|
||||||
Registers service `downloader/download_file` that will download files. File to download is specified in the `url` field in the service data.
|
|
||||||
|
|
||||||
**browser**
|
|
||||||
Registers service `browser/browse_url` that opens `url` as specified in event_data in the system default browser.
|
|
||||||
|
|
||||||
**tellstick_sensor**
|
|
||||||
Shows the values of that sensors that is connected to your Tellstick.
|
|
||||||
|
|
||||||
<a name='API'></a>
|
|
||||||
## Rest API
|
|
||||||
|
|
||||||
Home Assistent runs a webserver accessible on port 8123.
|
|
||||||
|
|
||||||
* At http://127.0.0.1:8123/ it will provide an interface allowing you to control Home Assistant.
|
|
||||||
* At http://localhost:8123/api/ it provides a password protected API.
|
|
||||||
|
|
||||||
In the package `homeassistant.remote` a Python API on top of the HTTP API can be found.
|
|
||||||
|
|
||||||
All API calls have to be accompanied by the header "HA-Access" with as value the api password (as specified in `home-assistant.conf`). The API returns only JSON encoded objects. Successful calls will return status code 200 or 201.
|
|
||||||
|
|
||||||
Other status codes that can occur are:
|
|
||||||
- 400 (Bad Request)
|
|
||||||
- 401 (Unauthorized)
|
|
||||||
- 404 (Not Found)
|
|
||||||
- 405 (Method not allowed)
|
|
||||||
|
|
||||||
The api supports the following actions:
|
|
||||||
|
|
||||||
**/api - GET**<br>
|
|
||||||
Returns message if API is up and running.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "API running."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**/api/events - GET**<br>
|
|
||||||
Returns a dict with as keys the events and as value the number of listeners.
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"event": "state_changed",
|
|
||||||
"listener_count": 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"event": "time_changed",
|
|
||||||
"listener_count": 2
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
**/api/services - GET**<br>
|
|
||||||
Returns a dict with as keys the domain and as value a list of published services.
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"domain": "browser",
|
|
||||||
"services": [
|
|
||||||
"browse_url"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"domain": "keyboard",
|
|
||||||
"services": [
|
|
||||||
"volume_up",
|
|
||||||
"volume_down"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
**/api/states - GET**<br>
|
|
||||||
Returns a dict with as keys the entity_ids and as value the state.
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"attributes": {
|
|
||||||
"next_rising": "07:04:15 29-10-2013",
|
|
||||||
"next_setting": "18:00:31 29-10-2013"
|
|
||||||
},
|
|
||||||
"entity_id": "sun.sun",
|
|
||||||
"last_changed": "23:24:33 28-10-2013",
|
|
||||||
"state": "below_horizon"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"attributes": {},
|
|
||||||
"entity_id": "process.Dropbox",
|
|
||||||
"last_changed": "23:24:33 28-10-2013",
|
|
||||||
"state": "on"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
**/api/states/<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
|
custom_components.example
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Bare minimum what is needed for a component to be valid.
|
Example component to target an entity_id to:
|
||||||
|
- turn it on at 7AM in the morning
|
||||||
|
- turn it on if anyone comes home and it is off
|
||||||
|
- turn it off if all lights are turned off
|
||||||
|
- turn it off if all people leave the house
|
||||||
|
- offer a service to turn it on for 10 seconds
|
||||||
"""
|
"""
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_ON, STATE_OFF
|
||||||
|
import homeassistant.loader as loader
|
||||||
|
from homeassistant.helpers import validate_config
|
||||||
|
import homeassistant.components as core
|
||||||
|
|
||||||
# The domain of your component. Should be equal to the name of your component
|
# The domain of your component. Should be equal to the name of your component
|
||||||
DOMAIN = "example"
|
DOMAIN = "example"
|
||||||
|
|
||||||
# List of component names (string) your component depends upon
|
# List of component names (string) your component depends upon
|
||||||
# If you are setting up a group but not using a group for anything,
|
# We depend on group because group will be loaded after all the components that
|
||||||
# don't depend on group
|
# initalize devices have been setup.
|
||||||
DEPENDENCIES = []
|
DEPENDENCIES = ['group']
|
||||||
|
|
||||||
|
# Configuration key for the entity id we are targetting
|
||||||
|
CONF_TARGET = 'target'
|
||||||
|
|
||||||
|
# Name of the service that we expose
|
||||||
|
SERVICE_FLASH = 'flash'
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
""" Register services or listen for events that your component needs. """
|
""" Setup example component. """
|
||||||
|
|
||||||
# Example of a service that prints the service call to the command-line.
|
# Validate that all required config options are given
|
||||||
hass.services.register(DOMAIN, "example_service_name", print)
|
if not validate_config(config, {DOMAIN: [CONF_TARGET]}, _LOGGER):
|
||||||
|
return False
|
||||||
|
|
||||||
# This prints a time change event to the command-line twice a minute.
|
target_id = config[DOMAIN][CONF_TARGET]
|
||||||
hass.track_time_change(print, second=[0, 30])
|
|
||||||
|
|
||||||
# See also (defined in homeassistant/__init__.py):
|
# Validate that the target entity id exists
|
||||||
# hass.track_state_change
|
if hass.states.get(target_id) is None:
|
||||||
# hass.track_point_in_time
|
_LOGGER.error("Target entity id %s does not exist", target_id)
|
||||||
|
|
||||||
|
# Tell the bootstrapper that we failed to initialize
|
||||||
|
return False
|
||||||
|
|
||||||
|
# We will use the component helper methods to check the states.
|
||||||
|
device_tracker = loader.get_component('device_tracker')
|
||||||
|
light = loader.get_component('light')
|
||||||
|
|
||||||
|
def track_devices(entity_id, old_state, new_state):
|
||||||
|
""" Called when the group.all devices change state. """
|
||||||
|
|
||||||
|
# If anyone comes home and the core is not on, turn it on.
|
||||||
|
if new_state.state == STATE_HOME and not core.is_on(hass, target_id):
|
||||||
|
|
||||||
|
core.turn_on(hass, target_id)
|
||||||
|
|
||||||
|
# If all people leave the house and the core is on, turn it off
|
||||||
|
elif new_state.state == STATE_NOT_HOME and core.is_on(hass, target_id):
|
||||||
|
|
||||||
|
core.turn_off(hass, target_id)
|
||||||
|
|
||||||
|
# Register our track_devices method to receive state changes of the
|
||||||
|
# all tracked devices group.
|
||||||
|
hass.states.track_change(
|
||||||
|
device_tracker.ENTITY_ID_ALL_DEVICES, track_devices)
|
||||||
|
|
||||||
|
def wake_up(now):
|
||||||
|
""" Turn it on in the morning if there are people home and
|
||||||
|
it is not already on. """
|
||||||
|
|
||||||
|
if device_tracker.is_on(hass) and not core.is_on(hass, target_id):
|
||||||
|
_LOGGER.info('People home at 7AM, turning it on')
|
||||||
|
core.turn_on(hass, target_id)
|
||||||
|
|
||||||
|
# Register our wake_up service to be called at 7AM in the morning
|
||||||
|
hass.track_time_change(wake_up, hour=7, minute=0, second=0)
|
||||||
|
|
||||||
|
def all_lights_off(entity_id, old_state, new_state):
|
||||||
|
""" If all lights turn off, turn off. """
|
||||||
|
|
||||||
|
if core.is_on(hass, target_id):
|
||||||
|
_LOGGER.info('All lights have been turned off, turning it off')
|
||||||
|
core.turn_off(hass, target_id)
|
||||||
|
|
||||||
|
# Register our all_lights_off method to be called when all lights turn off
|
||||||
|
hass.states.track_change(
|
||||||
|
light.ENTITY_ID_ALL_LIGHTS, all_lights_off, STATE_ON, STATE_OFF)
|
||||||
|
|
||||||
|
def flash_service(call):
|
||||||
|
""" Service that will turn the target off for 10 seconds
|
||||||
|
if on and vice versa. """
|
||||||
|
|
||||||
|
if core.is_on(hass, target_id):
|
||||||
|
core.turn_off(hass, target_id)
|
||||||
|
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
core.turn_on(hass, target_id)
|
||||||
|
|
||||||
|
else:
|
||||||
|
core.turn_on(hass, target_id)
|
||||||
|
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
core.turn_off(hass, target_id)
|
||||||
|
|
||||||
|
# Register our service with HASS.
|
||||||
|
hass.services.register(DOMAIN, SERVICE_FLASH, flash_service)
|
||||||
|
|
||||||
# Tells the bootstrapper that the component was succesfully initialized
|
# Tells the bootstrapper that the component was succesfully initialized
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -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
|
# development=1
|
||||||
|
|
||||||
[light]
|
[light]
|
||||||
type=hue
|
platform=hue
|
||||||
|
|
||||||
[device_tracker]
|
[device_tracker]
|
||||||
# The following types are available: netgear, tomato, luci
|
# The following types are available: netgear, tomato, luci, nmap_tracker
|
||||||
type=netgear
|
platform=netgear
|
||||||
host=192.168.1.1
|
host=192.168.1.1
|
||||||
username=admin
|
username=admin
|
||||||
password=PASSWORD
|
password=PASSWORD
|
||||||
# http_id is needed for Tomato routers only
|
# http_id is needed for Tomato routers only
|
||||||
# http_id=ABCDEFGHH
|
# http_id=ABCDEFGHH
|
||||||
|
# For nmap_tracker, only the IP addresses to scan are needed:
|
||||||
|
# hosts=192.168.1.1/24 # netmask prefix notation or
|
||||||
|
# hosts=192.168.1.1-255 # address range
|
||||||
|
|
||||||
[chromecast]
|
[chromecast]
|
||||||
# Optional: hard code the hosts (comma seperated) to find chromecasts
|
# Optional: hard code the hosts (comma seperated) to find chromecasts
|
||||||
|
@ -26,7 +29,7 @@ password=PASSWORD
|
||||||
# hosts=192.168.1.9,192.168.1.12
|
# hosts=192.168.1.9,192.168.1.12
|
||||||
|
|
||||||
[switch]
|
[switch]
|
||||||
type=wemo
|
platform=wemo
|
||||||
# Optional: hard code the hosts (comma seperated) to avoid scanning the network
|
# Optional: hard code the hosts (comma seperated) to avoid scanning the network
|
||||||
# hosts=192.168.1.9,192.168.1.12
|
# hosts=192.168.1.9,192.168.1.12
|
||||||
|
|
||||||
|
@ -53,6 +56,12 @@ xbmc=XBMC.App
|
||||||
|
|
||||||
[example]
|
[example]
|
||||||
|
|
||||||
|
[simple_alarm]
|
||||||
|
# Which light/light group has to flash when a known device comes home
|
||||||
|
known_light=light.Bowl
|
||||||
|
# Which light/light group has to flash red when light turns on while no one home
|
||||||
|
unknown_light=group.living_room
|
||||||
|
|
||||||
[browser]
|
[browser]
|
||||||
|
|
||||||
[keyboard]
|
[keyboard]
|
||||||
|
|
|
@ -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.
|
Tests Chromecast component.
|
||||||
"""
|
"""
|
||||||
|
@ -9,9 +9,13 @@ import logging
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import homeassistant as ha
|
import homeassistant as ha
|
||||||
import homeassistant.components as components
|
from homeassistant.const import (
|
||||||
|
SERVICE_TURN_OFF, SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN,
|
||||||
|
SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE,
|
||||||
|
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK, ATTR_ENTITY_ID,
|
||||||
|
CONF_HOSTS)
|
||||||
import homeassistant.components.chromecast as chromecast
|
import homeassistant.components.chromecast as chromecast
|
||||||
from helper import mock_service
|
from helpers import mock_service
|
||||||
|
|
||||||
|
|
||||||
def setUpModule(): # pylint: disable=invalid-name
|
def setUpModule(): # pylint: disable=invalid-name
|
||||||
|
@ -33,7 +37,7 @@ class TestChromecast(unittest.TestCase):
|
||||||
|
|
||||||
def tearDown(self): # pylint: disable=invalid-name
|
def tearDown(self): # pylint: disable=invalid-name
|
||||||
""" Stop down stuff we started. """
|
""" Stop down stuff we started. """
|
||||||
self.hass._pool.stop()
|
self.hass.stop()
|
||||||
|
|
||||||
def test_is_on(self):
|
def test_is_on(self):
|
||||||
""" Test is_on method. """
|
""" Test is_on method. """
|
||||||
|
@ -45,37 +49,36 @@ class TestChromecast(unittest.TestCase):
|
||||||
Test if the call service methods conver to correct service calls.
|
Test if the call service methods conver to correct service calls.
|
||||||
"""
|
"""
|
||||||
services = {
|
services = {
|
||||||
components.SERVICE_TURN_OFF: chromecast.turn_off,
|
SERVICE_TURN_OFF: chromecast.turn_off,
|
||||||
components.SERVICE_VOLUME_UP: chromecast.volume_up,
|
SERVICE_VOLUME_UP: chromecast.volume_up,
|
||||||
components.SERVICE_VOLUME_DOWN: chromecast.volume_down,
|
SERVICE_VOLUME_DOWN: chromecast.volume_down,
|
||||||
components.SERVICE_MEDIA_PLAY_PAUSE: chromecast.media_play_pause,
|
SERVICE_MEDIA_PLAY_PAUSE: chromecast.media_play_pause,
|
||||||
components.SERVICE_MEDIA_PLAY: chromecast.media_play,
|
SERVICE_MEDIA_PLAY: chromecast.media_play,
|
||||||
components.SERVICE_MEDIA_PAUSE: chromecast.media_pause,
|
SERVICE_MEDIA_PAUSE: chromecast.media_pause,
|
||||||
components.SERVICE_MEDIA_NEXT_TRACK: chromecast.media_next_track,
|
SERVICE_MEDIA_NEXT_TRACK: chromecast.media_next_track,
|
||||||
components.SERVICE_MEDIA_PREV_TRACK: chromecast.media_prev_track
|
SERVICE_MEDIA_PREV_TRACK: chromecast.media_prev_track
|
||||||
}
|
}
|
||||||
|
|
||||||
for service_name, service_method in services.items():
|
for service_name, service_method in services.items():
|
||||||
calls = mock_service(self.hass, chromecast.DOMAIN, service_name)
|
calls = mock_service(self.hass, chromecast.DOMAIN, service_name)
|
||||||
|
|
||||||
service_method(self.hass)
|
service_method(self.hass)
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
|
|
||||||
self.assertEqual(1, len(calls))
|
self.assertEqual(1, len(calls))
|
||||||
call = calls[-1]
|
call = calls[-1]
|
||||||
self.assertEqual(call.domain, chromecast.DOMAIN)
|
self.assertEqual(chromecast.DOMAIN, call.domain)
|
||||||
self.assertEqual(call.service, service_name)
|
self.assertEqual(service_name, call.service)
|
||||||
self.assertEqual(call.data, {})
|
|
||||||
|
|
||||||
service_method(self.hass, self.test_entity)
|
service_method(self.hass, self.test_entity)
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
|
|
||||||
self.assertEqual(2, len(calls))
|
self.assertEqual(2, len(calls))
|
||||||
call = calls[-1]
|
call = calls[-1]
|
||||||
self.assertEqual(call.domain, chromecast.DOMAIN)
|
self.assertEqual(chromecast.DOMAIN, call.domain)
|
||||||
self.assertEqual(call.service, service_name)
|
self.assertEqual(service_name, call.service)
|
||||||
self.assertEqual(call.data,
|
self.assertEqual(self.test_entity,
|
||||||
{components.ATTR_ENTITY_ID: self.test_entity})
|
call.data.get(ATTR_ENTITY_ID))
|
||||||
|
|
||||||
def test_setup(self):
|
def test_setup(self):
|
||||||
"""
|
"""
|
||||||
|
@ -84,4 +87,4 @@ class TestChromecast(unittest.TestCase):
|
||||||
In an ideal world we would create a mock pychromecast API..
|
In an ideal world we would create a mock pychromecast API..
|
||||||
"""
|
"""
|
||||||
self.assertFalse(chromecast.setup(
|
self.assertFalse(chromecast.setup(
|
||||||
self.hass, {chromecast.DOMAIN: {ha.CONF_HOSTS: '127.0.0.1'}}))
|
self.hass, {chromecast.DOMAIN: {CONF_HOSTS: '127.0.0.1'}}))
|
|
@ -1,6 +1,6 @@
|
||||||
"""
|
"""
|
||||||
test.test_component_core
|
ha_test.test_component_core
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Tests core compoments.
|
Tests core compoments.
|
||||||
"""
|
"""
|
||||||
|
@ -9,6 +9,8 @@ import unittest
|
||||||
|
|
||||||
import homeassistant as ha
|
import homeassistant as ha
|
||||||
import homeassistant.loader as loader
|
import homeassistant.loader as loader
|
||||||
|
from homeassistant.const import (
|
||||||
|
STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF)
|
||||||
import homeassistant.components as comps
|
import homeassistant.components as comps
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,12 +23,12 @@ class TestComponentsCore(unittest.TestCase):
|
||||||
loader.prepare(self.hass)
|
loader.prepare(self.hass)
|
||||||
self.assertTrue(comps.setup(self.hass, {}))
|
self.assertTrue(comps.setup(self.hass, {}))
|
||||||
|
|
||||||
self.hass.states.set('light.Bowl', comps.STATE_ON)
|
self.hass.states.set('light.Bowl', STATE_ON)
|
||||||
self.hass.states.set('light.Ceiling', comps.STATE_OFF)
|
self.hass.states.set('light.Ceiling', STATE_OFF)
|
||||||
|
|
||||||
def tearDown(self): # pylint: disable=invalid-name
|
def tearDown(self): # pylint: disable=invalid-name
|
||||||
""" Stop down stuff we started. """
|
""" Stop down stuff we started. """
|
||||||
self.hass._pool.stop()
|
self.hass.stop()
|
||||||
|
|
||||||
def test_is_on(self):
|
def test_is_on(self):
|
||||||
""" Test is_on method. """
|
""" Test is_on method. """
|
||||||
|
@ -38,11 +40,11 @@ class TestComponentsCore(unittest.TestCase):
|
||||||
""" Test turn_on method. """
|
""" Test turn_on method. """
|
||||||
runs = []
|
runs = []
|
||||||
self.hass.services.register(
|
self.hass.services.register(
|
||||||
'light', comps.SERVICE_TURN_ON, lambda x: runs.append(1))
|
'light', SERVICE_TURN_ON, lambda x: runs.append(1))
|
||||||
|
|
||||||
comps.turn_on(self.hass, 'light.Ceiling')
|
comps.turn_on(self.hass, 'light.Ceiling')
|
||||||
|
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
|
|
||||||
self.assertEqual(1, len(runs))
|
self.assertEqual(1, len(runs))
|
||||||
|
|
||||||
|
@ -50,24 +52,10 @@ class TestComponentsCore(unittest.TestCase):
|
||||||
""" Test turn_off method. """
|
""" Test turn_off method. """
|
||||||
runs = []
|
runs = []
|
||||||
self.hass.services.register(
|
self.hass.services.register(
|
||||||
'light', comps.SERVICE_TURN_OFF, lambda x: runs.append(1))
|
'light', SERVICE_TURN_OFF, lambda x: runs.append(1))
|
||||||
|
|
||||||
comps.turn_off(self.hass, 'light.Bowl')
|
comps.turn_off(self.hass, 'light.Bowl')
|
||||||
|
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
|
|
||||||
self.assertEqual(1, len(runs))
|
self.assertEqual(1, len(runs))
|
||||||
|
|
||||||
def test_extract_entity_ids(self):
|
|
||||||
""" Test extract_entity_ids method. """
|
|
||||||
call = ha.ServiceCall('light', 'turn_on',
|
|
||||||
{comps.ATTR_ENTITY_ID: 'light.Bowl'})
|
|
||||||
|
|
||||||
self.assertEqual(['light.Bowl'],
|
|
||||||
comps.extract_entity_ids(self.hass, call))
|
|
||||||
|
|
||||||
call = ha.ServiceCall('light', 'turn_on',
|
|
||||||
{comps.ATTR_ENTITY_ID: ['light.Bowl']})
|
|
||||||
|
|
||||||
self.assertEqual(['light.Bowl'],
|
|
||||||
comps.extract_entity_ids(self.hass, call))
|
|
|
@ -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.
|
Tests the group compoments.
|
||||||
"""
|
"""
|
||||||
|
@ -9,7 +9,7 @@ import unittest
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import homeassistant as ha
|
import homeassistant as ha
|
||||||
import homeassistant.components as comps
|
from homeassistant.const import STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME
|
||||||
import homeassistant.components.group as group
|
import homeassistant.components.group as group
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,9 +25,9 @@ class TestComponentsGroup(unittest.TestCase):
|
||||||
""" Init needed objects. """
|
""" Init needed objects. """
|
||||||
self.hass = ha.HomeAssistant()
|
self.hass = ha.HomeAssistant()
|
||||||
|
|
||||||
self.hass.states.set('light.Bowl', comps.STATE_ON)
|
self.hass.states.set('light.Bowl', STATE_ON)
|
||||||
self.hass.states.set('light.Ceiling', comps.STATE_OFF)
|
self.hass.states.set('light.Ceiling', STATE_OFF)
|
||||||
self.hass.states.set('switch.AC', comps.STATE_OFF)
|
self.hass.states.set('switch.AC', STATE_OFF)
|
||||||
group.setup_group(self.hass, 'init_group',
|
group.setup_group(self.hass, 'init_group',
|
||||||
['light.Bowl', 'light.Ceiling'], False)
|
['light.Bowl', 'light.Ceiling'], False)
|
||||||
group.setup_group(self.hass, 'mixed_group',
|
group.setup_group(self.hass, 'mixed_group',
|
||||||
|
@ -40,43 +40,27 @@ class TestComponentsGroup(unittest.TestCase):
|
||||||
""" Stop down stuff we started. """
|
""" Stop down stuff we started. """
|
||||||
self.hass.stop()
|
self.hass.stop()
|
||||||
|
|
||||||
def test_setup_and_monitor_group(self):
|
def test_setup_group(self):
|
||||||
""" Test setup_group method. """
|
""" Test setup_group method. """
|
||||||
|
|
||||||
# Test if group setup in our init mode is ok
|
|
||||||
self.assertIn(self.group_name, self.hass.states.entity_ids)
|
|
||||||
|
|
||||||
group_state = self.hass.states.get(self.group_name)
|
|
||||||
self.assertEqual(comps.STATE_ON, group_state.state)
|
|
||||||
self.assertTrue(group_state.attributes[group.ATTR_AUTO])
|
|
||||||
|
|
||||||
# Turn the Bowl off and see if group turns off
|
|
||||||
self.hass.states.set('light.Bowl', comps.STATE_OFF)
|
|
||||||
|
|
||||||
self.hass._pool.block_till_done()
|
|
||||||
|
|
||||||
group_state = self.hass.states.get(self.group_name)
|
|
||||||
self.assertEqual(comps.STATE_OFF, group_state.state)
|
|
||||||
|
|
||||||
# Turn the Ceiling on and see if group turns on
|
|
||||||
self.hass.states.set('light.Ceiling', comps.STATE_ON)
|
|
||||||
|
|
||||||
self.hass._pool.block_till_done()
|
|
||||||
|
|
||||||
group_state = self.hass.states.get(self.group_name)
|
|
||||||
self.assertEqual(comps.STATE_ON, group_state.state)
|
|
||||||
|
|
||||||
# Try to setup a group with mixed groupable states
|
# Try to setup a group with mixed groupable states
|
||||||
self.hass.states.set('device_tracker.Paulus', comps.STATE_HOME)
|
self.hass.states.set('device_tracker.Paulus', STATE_HOME)
|
||||||
self.assertFalse(group.setup_group(
|
self.assertTrue(group.setup_group(
|
||||||
self.hass, 'person_and_light',
|
self.hass, 'person_and_light',
|
||||||
['light.Bowl', 'device_tracker.Paulus']))
|
['light.Bowl', 'device_tracker.Paulus']))
|
||||||
|
self.assertEqual(
|
||||||
|
STATE_ON,
|
||||||
|
self.hass.states.get(
|
||||||
|
group.ENTITY_ID_FORMAT.format('person_and_light')).state)
|
||||||
|
|
||||||
# Try to setup a group with a non existing state
|
# Try to setup a group with a non existing state
|
||||||
self.assertNotIn('non.existing', self.hass.states.entity_ids)
|
self.assertNotIn('non.existing', self.hass.states.entity_ids())
|
||||||
self.assertFalse(group.setup_group(
|
self.assertTrue(group.setup_group(
|
||||||
self.hass, 'light_and_nothing',
|
self.hass, 'light_and_nothing',
|
||||||
['light.Bowl', 'non.existing']))
|
['light.Bowl', 'non.existing']))
|
||||||
|
self.assertEqual(
|
||||||
|
STATE_ON,
|
||||||
|
self.hass.states.get(
|
||||||
|
group.ENTITY_ID_FORMAT.format('light_and_nothing')).state)
|
||||||
|
|
||||||
# Try to setup a group with non groupable states
|
# Try to setup a group with non groupable states
|
||||||
self.hass.states.set('cast.living_room', "Plex")
|
self.hass.states.set('cast.living_room', "Plex")
|
||||||
|
@ -89,23 +73,37 @@ class TestComponentsGroup(unittest.TestCase):
|
||||||
# Try to setup an empty group
|
# Try to setup an empty group
|
||||||
self.assertFalse(group.setup_group(self.hass, 'nothing', []))
|
self.assertFalse(group.setup_group(self.hass, 'nothing', []))
|
||||||
|
|
||||||
def test__get_group_type(self):
|
def test_monitor_group(self):
|
||||||
""" Test _get_group_type method. """
|
""" Test if the group keeps track of states. """
|
||||||
self.assertEqual('on_off', group._get_group_type(comps.STATE_ON))
|
|
||||||
self.assertEqual('on_off', group._get_group_type(comps.STATE_OFF))
|
|
||||||
self.assertEqual('home_not_home',
|
|
||||||
group._get_group_type(comps.STATE_HOME))
|
|
||||||
self.assertEqual('home_not_home',
|
|
||||||
group._get_group_type(comps.STATE_NOT_HOME))
|
|
||||||
|
|
||||||
# Unsupported state
|
# Test if group setup in our init mode is ok
|
||||||
self.assertIsNone(group._get_group_type('unsupported_state'))
|
self.assertIn(self.group_name, self.hass.states.entity_ids())
|
||||||
|
|
||||||
|
group_state = self.hass.states.get(self.group_name)
|
||||||
|
self.assertEqual(STATE_ON, group_state.state)
|
||||||
|
self.assertTrue(group_state.attributes[group.ATTR_AUTO])
|
||||||
|
|
||||||
|
# Turn the Bowl off and see if group turns off
|
||||||
|
self.hass.states.set('light.Bowl', STATE_OFF)
|
||||||
|
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
|
||||||
|
group_state = self.hass.states.get(self.group_name)
|
||||||
|
self.assertEqual(STATE_OFF, group_state.state)
|
||||||
|
|
||||||
|
# Turn the Ceiling on and see if group turns on
|
||||||
|
self.hass.states.set('light.Ceiling', STATE_ON)
|
||||||
|
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
|
||||||
|
group_state = self.hass.states.get(self.group_name)
|
||||||
|
self.assertEqual(STATE_ON, group_state.state)
|
||||||
|
|
||||||
def test_is_on(self):
|
def test_is_on(self):
|
||||||
""" Test is_on method. """
|
""" Test is_on method. """
|
||||||
self.assertTrue(group.is_on(self.hass, self.group_name))
|
self.assertTrue(group.is_on(self.hass, self.group_name))
|
||||||
self.hass.states.set('light.Bowl', comps.STATE_OFF)
|
self.hass.states.set('light.Bowl', STATE_OFF)
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
self.assertFalse(group.is_on(self.hass, self.group_name))
|
self.assertFalse(group.is_on(self.hass, self.group_name))
|
||||||
|
|
||||||
# Try on non existing state
|
# Try on non existing state
|
||||||
|
@ -159,5 +157,5 @@ class TestComponentsGroup(unittest.TestCase):
|
||||||
group_state = self.hass.states.get(
|
group_state = self.hass.states.get(
|
||||||
group.ENTITY_ID_FORMAT.format('second_group'))
|
group.ENTITY_ID_FORMAT.format('second_group'))
|
||||||
|
|
||||||
self.assertEqual(comps.STATE_ON, group_state.state)
|
self.assertEqual(STATE_ON, group_state.state)
|
||||||
self.assertFalse(group_state.attributes[group.ATTR_AUTO])
|
self.assertFalse(group_state.attributes[group.ATTR_AUTO])
|
|
@ -1,6 +1,6 @@
|
||||||
"""
|
"""
|
||||||
test.test_component_http
|
ha_test.test_component_http
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Tests Home Assistant HTTP component does what it should do.
|
Tests Home Assistant HTTP component does what it should do.
|
||||||
"""
|
"""
|
||||||
|
@ -52,30 +52,50 @@ def setUpModule(): # pylint: disable=invalid-name
|
||||||
|
|
||||||
def tearDownModule(): # pylint: disable=invalid-name
|
def tearDownModule(): # pylint: disable=invalid-name
|
||||||
""" Stops the Home Assistant server. """
|
""" Stops the Home Assistant server. """
|
||||||
global hass
|
|
||||||
|
|
||||||
hass.stop()
|
hass.stop()
|
||||||
|
|
||||||
|
|
||||||
class TestHTTP(unittest.TestCase):
|
class TestHTTP(unittest.TestCase):
|
||||||
""" Test the HTTP debug interface and API. """
|
""" Test the HTTP debug interface and API. """
|
||||||
|
|
||||||
def test_get_frontend(self):
|
def test_setup(self):
|
||||||
|
""" Test http.setup. """
|
||||||
|
self.assertFalse(http.setup(hass, {}))
|
||||||
|
self.assertFalse(http.setup(hass, {http.DOMAIN: {}}))
|
||||||
|
|
||||||
|
def test_frontend_and_static(self):
|
||||||
""" Tests if we can get the frontend. """
|
""" Tests if we can get the frontend. """
|
||||||
req = requests.get(_url(""))
|
req = requests.get(_url(""))
|
||||||
|
|
||||||
self.assertEqual(200, req.status_code)
|
self.assertEqual(200, req.status_code)
|
||||||
|
|
||||||
|
# Test we can retrieve frontend.js
|
||||||
frontendjs = re.search(
|
frontendjs = re.search(
|
||||||
r'(?P<app>\/static\/frontend-[A-Za-z0-9]{32}.html)',
|
r'(?P<app>\/static\/frontend-[A-Za-z0-9]{32}.html)',
|
||||||
req.text).groups(0)[0]
|
req.text)
|
||||||
|
|
||||||
self.assertIsNotNone(frontendjs)
|
self.assertIsNotNone(frontendjs)
|
||||||
|
|
||||||
req = requests.get(_url(frontendjs))
|
req = requests.head(_url(frontendjs.groups(0)[0]))
|
||||||
|
|
||||||
self.assertEqual(200, req.status_code)
|
self.assertEqual(200, req.status_code)
|
||||||
|
|
||||||
|
# Test auto filling in api password
|
||||||
|
req = requests.get(
|
||||||
|
_url("?{}={}".format(http.DATA_API_PASSWORD, API_PASSWORD)))
|
||||||
|
|
||||||
|
self.assertEqual(200, req.status_code)
|
||||||
|
|
||||||
|
auth_text = re.search(r"auth='{}'".format(API_PASSWORD), req.text)
|
||||||
|
|
||||||
|
self.assertIsNotNone(auth_text)
|
||||||
|
|
||||||
|
# Test 404
|
||||||
|
self.assertEqual(404, requests.get(_url("/not-existing")).status_code)
|
||||||
|
|
||||||
|
# Test we cannot POST to /
|
||||||
|
self.assertEqual(405, requests.post(_url("")).status_code)
|
||||||
|
|
||||||
def test_api_password(self):
|
def test_api_password(self):
|
||||||
""" Test if we get access denied if we omit or provide
|
""" Test if we get access denied if we omit or provide
|
||||||
a wrong api password. """
|
a wrong api password. """
|
||||||
|
@ -127,8 +147,8 @@ class TestHTTP(unittest.TestCase):
|
||||||
hass.states.set("test.test", "not_to_be_set")
|
hass.states.set("test.test", "not_to_be_set")
|
||||||
|
|
||||||
requests.post(_url(remote.URL_API_STATES_ENTITY.format("test.test")),
|
requests.post(_url(remote.URL_API_STATES_ENTITY.format("test.test")),
|
||||||
data=json.dumps({"state": "debug_state_change2",
|
data=json.dumps({"state": "debug_state_change2"}),
|
||||||
"api_password": API_PASSWORD}))
|
headers=HA_HEADERS)
|
||||||
|
|
||||||
self.assertEqual("debug_state_change2",
|
self.assertEqual("debug_state_change2",
|
||||||
hass.states.get("test.test").state)
|
hass.states.get("test.test").state)
|
||||||
|
@ -143,8 +163,8 @@ class TestHTTP(unittest.TestCase):
|
||||||
req = requests.post(
|
req = requests.post(
|
||||||
_url(remote.URL_API_STATES_ENTITY.format(
|
_url(remote.URL_API_STATES_ENTITY.format(
|
||||||
"test_entity.that_does_not_exist")),
|
"test_entity.that_does_not_exist")),
|
||||||
data=json.dumps({"state": new_state,
|
data=json.dumps({'state': new_state}),
|
||||||
"api_password": API_PASSWORD}))
|
headers=HA_HEADERS)
|
||||||
|
|
||||||
cur_state = (hass.states.
|
cur_state = (hass.states.
|
||||||
get("test_entity.that_does_not_exist").state)
|
get("test_entity.that_does_not_exist").state)
|
||||||
|
@ -152,6 +172,18 @@ class TestHTTP(unittest.TestCase):
|
||||||
self.assertEqual(201, req.status_code)
|
self.assertEqual(201, req.status_code)
|
||||||
self.assertEqual(cur_state, new_state)
|
self.assertEqual(cur_state, new_state)
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
def test_api_state_change_with_bad_data(self):
|
||||||
|
""" Test if API sends appropriate error if we omit state. """
|
||||||
|
|
||||||
|
req = requests.post(
|
||||||
|
_url(remote.URL_API_STATES_ENTITY.format(
|
||||||
|
"test_entity.that_does_not_exist")),
|
||||||
|
data=json.dumps({}),
|
||||||
|
headers=HA_HEADERS)
|
||||||
|
|
||||||
|
self.assertEqual(400, req.status_code)
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
def test_api_fire_event_with_no_data(self):
|
def test_api_fire_event_with_no_data(self):
|
||||||
""" Test if the API allows us to fire an event. """
|
""" Test if the API allows us to fire an event. """
|
||||||
|
@ -161,13 +193,13 @@ class TestHTTP(unittest.TestCase):
|
||||||
""" Helper method that will verify our event got called. """
|
""" Helper method that will verify our event got called. """
|
||||||
test_value.append(1)
|
test_value.append(1)
|
||||||
|
|
||||||
hass.listen_once_event("test.event_no_data", listener)
|
hass.bus.listen_once("test.event_no_data", listener)
|
||||||
|
|
||||||
requests.post(
|
requests.post(
|
||||||
_url(remote.URL_API_EVENTS_EVENT.format("test.event_no_data")),
|
_url(remote.URL_API_EVENTS_EVENT.format("test.event_no_data")),
|
||||||
headers=HA_HEADERS)
|
headers=HA_HEADERS)
|
||||||
|
|
||||||
hass._pool.block_till_done()
|
hass.pool.block_till_done()
|
||||||
|
|
||||||
self.assertEqual(1, len(test_value))
|
self.assertEqual(1, len(test_value))
|
||||||
|
|
||||||
|
@ -182,14 +214,14 @@ class TestHTTP(unittest.TestCase):
|
||||||
if "test" in event.data:
|
if "test" in event.data:
|
||||||
test_value.append(1)
|
test_value.append(1)
|
||||||
|
|
||||||
hass.listen_once_event("test_event_with_data", listener)
|
hass.bus.listen_once("test_event_with_data", listener)
|
||||||
|
|
||||||
requests.post(
|
requests.post(
|
||||||
_url(remote.URL_API_EVENTS_EVENT.format("test_event_with_data")),
|
_url(remote.URL_API_EVENTS_EVENT.format("test_event_with_data")),
|
||||||
data=json.dumps({"test": 1}),
|
data=json.dumps({"test": 1}),
|
||||||
headers=HA_HEADERS)
|
headers=HA_HEADERS)
|
||||||
|
|
||||||
hass._pool.block_till_done()
|
hass.pool.block_till_done()
|
||||||
|
|
||||||
self.assertEqual(1, len(test_value))
|
self.assertEqual(1, len(test_value))
|
||||||
|
|
||||||
|
@ -202,14 +234,25 @@ class TestHTTP(unittest.TestCase):
|
||||||
""" Helper method that will verify our event got called. """
|
""" Helper method that will verify our event got called. """
|
||||||
test_value.append(1)
|
test_value.append(1)
|
||||||
|
|
||||||
hass.listen_once_event("test_event_bad_data", listener)
|
hass.bus.listen_once("test_event_bad_data", listener)
|
||||||
|
|
||||||
req = requests.post(
|
req = requests.post(
|
||||||
_url(remote.URL_API_EVENTS_EVENT.format("test_event_bad_data")),
|
_url(remote.URL_API_EVENTS_EVENT.format("test_event_bad_data")),
|
||||||
data=json.dumps('not an object'),
|
data=json.dumps('not an object'),
|
||||||
headers=HA_HEADERS)
|
headers=HA_HEADERS)
|
||||||
|
|
||||||
hass._pool.block_till_done()
|
hass.pool.block_till_done()
|
||||||
|
|
||||||
|
self.assertEqual(422, req.status_code)
|
||||||
|
self.assertEqual(0, len(test_value))
|
||||||
|
|
||||||
|
# Try now with valid but unusable JSON
|
||||||
|
req = requests.post(
|
||||||
|
_url(remote.URL_API_EVENTS_EVENT.format("test_event_bad_data")),
|
||||||
|
data=json.dumps([1, 2, 3]),
|
||||||
|
headers=HA_HEADERS)
|
||||||
|
|
||||||
|
hass.pool.block_till_done()
|
||||||
|
|
||||||
self.assertEqual(422, req.status_code)
|
self.assertEqual(422, req.status_code)
|
||||||
self.assertEqual(0, len(test_value))
|
self.assertEqual(0, len(test_value))
|
||||||
|
@ -254,7 +297,7 @@ class TestHTTP(unittest.TestCase):
|
||||||
"test_domain", "test_service")),
|
"test_domain", "test_service")),
|
||||||
headers=HA_HEADERS)
|
headers=HA_HEADERS)
|
||||||
|
|
||||||
hass._pool.block_till_done()
|
hass.pool.block_till_done()
|
||||||
|
|
||||||
self.assertEqual(1, len(test_value))
|
self.assertEqual(1, len(test_value))
|
||||||
|
|
||||||
|
@ -276,6 +319,82 @@ class TestHTTP(unittest.TestCase):
|
||||||
data=json.dumps({"test": 1}),
|
data=json.dumps({"test": 1}),
|
||||||
headers=HA_HEADERS)
|
headers=HA_HEADERS)
|
||||||
|
|
||||||
hass._pool.block_till_done()
|
hass.pool.block_till_done()
|
||||||
|
|
||||||
self.assertEqual(1, len(test_value))
|
self.assertEqual(1, len(test_value))
|
||||||
|
|
||||||
|
def test_api_event_forward(self):
|
||||||
|
""" Test setting up event forwarding. """
|
||||||
|
|
||||||
|
req = requests.post(
|
||||||
|
_url(remote.URL_API_EVENT_FORWARD),
|
||||||
|
headers=HA_HEADERS)
|
||||||
|
self.assertEqual(400, req.status_code)
|
||||||
|
|
||||||
|
req = requests.post(
|
||||||
|
_url(remote.URL_API_EVENT_FORWARD),
|
||||||
|
data=json.dumps({'host': '127.0.0.1'}),
|
||||||
|
headers=HA_HEADERS)
|
||||||
|
self.assertEqual(400, req.status_code)
|
||||||
|
|
||||||
|
req = requests.post(
|
||||||
|
_url(remote.URL_API_EVENT_FORWARD),
|
||||||
|
data=json.dumps({'api_password': 'bla-di-bla'}),
|
||||||
|
headers=HA_HEADERS)
|
||||||
|
self.assertEqual(400, req.status_code)
|
||||||
|
|
||||||
|
req = requests.post(
|
||||||
|
_url(remote.URL_API_EVENT_FORWARD),
|
||||||
|
data=json.dumps({
|
||||||
|
'api_password': 'bla-di-bla',
|
||||||
|
'host': '127.0.0.1',
|
||||||
|
'port': 'abcd'
|
||||||
|
}),
|
||||||
|
headers=HA_HEADERS)
|
||||||
|
self.assertEqual(422, req.status_code)
|
||||||
|
|
||||||
|
req = requests.post(
|
||||||
|
_url(remote.URL_API_EVENT_FORWARD),
|
||||||
|
data=json.dumps({
|
||||||
|
'api_password': 'bla-di-bla',
|
||||||
|
'host': '127.0.0.1',
|
||||||
|
'port': '8125'
|
||||||
|
}),
|
||||||
|
headers=HA_HEADERS)
|
||||||
|
self.assertEqual(422, req.status_code)
|
||||||
|
|
||||||
|
# Setup a real one
|
||||||
|
req = requests.post(
|
||||||
|
_url(remote.URL_API_EVENT_FORWARD),
|
||||||
|
data=json.dumps({
|
||||||
|
'api_password': API_PASSWORD,
|
||||||
|
'host': '127.0.0.1',
|
||||||
|
'port': SERVER_PORT
|
||||||
|
}),
|
||||||
|
headers=HA_HEADERS)
|
||||||
|
self.assertEqual(200, req.status_code)
|
||||||
|
|
||||||
|
# Delete it again..
|
||||||
|
req = requests.delete(
|
||||||
|
_url(remote.URL_API_EVENT_FORWARD),
|
||||||
|
data=json.dumps({}),
|
||||||
|
headers=HA_HEADERS)
|
||||||
|
self.assertEqual(400, req.status_code)
|
||||||
|
|
||||||
|
req = requests.delete(
|
||||||
|
_url(remote.URL_API_EVENT_FORWARD),
|
||||||
|
data=json.dumps({
|
||||||
|
'host': '127.0.0.1',
|
||||||
|
'port': 'abcd'
|
||||||
|
}),
|
||||||
|
headers=HA_HEADERS)
|
||||||
|
self.assertEqual(422, req.status_code)
|
||||||
|
|
||||||
|
req = requests.delete(
|
||||||
|
_url(remote.URL_API_EVENT_FORWARD),
|
||||||
|
data=json.dumps({
|
||||||
|
'host': '127.0.0.1',
|
||||||
|
'port': SERVER_PORT
|
||||||
|
}),
|
||||||
|
headers=HA_HEADERS)
|
||||||
|
self.assertEqual(200, req.status_code)
|
|
@ -1,6 +1,6 @@
|
||||||
"""
|
"""
|
||||||
test.test_component_switch
|
ha_test.test_component_switch
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Tests switch component.
|
Tests switch component.
|
||||||
"""
|
"""
|
||||||
|
@ -11,12 +11,12 @@ import os
|
||||||
import homeassistant as ha
|
import homeassistant as ha
|
||||||
import homeassistant.loader as loader
|
import homeassistant.loader as loader
|
||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
import homeassistant.components as components
|
from homeassistant.const import (
|
||||||
|
ATTR_ENTITY_ID, STATE_ON, STATE_OFF, CONF_TYPE,
|
||||||
|
SERVICE_TURN_ON, SERVICE_TURN_OFF)
|
||||||
import homeassistant.components.light as light
|
import homeassistant.components.light as light
|
||||||
|
|
||||||
import mock_toggledevice_platform
|
from helpers import mock_service, get_test_home_assistant
|
||||||
|
|
||||||
from helper import mock_service, get_test_home_assistant
|
|
||||||
|
|
||||||
|
|
||||||
class TestLight(unittest.TestCase):
|
class TestLight(unittest.TestCase):
|
||||||
|
@ -25,11 +25,10 @@ class TestLight(unittest.TestCase):
|
||||||
def setUp(self): # pylint: disable=invalid-name
|
def setUp(self): # pylint: disable=invalid-name
|
||||||
self.hass = get_test_home_assistant()
|
self.hass = get_test_home_assistant()
|
||||||
loader.prepare(self.hass)
|
loader.prepare(self.hass)
|
||||||
loader.set_component('light.test', mock_toggledevice_platform)
|
|
||||||
|
|
||||||
def tearDown(self): # pylint: disable=invalid-name
|
def tearDown(self): # pylint: disable=invalid-name
|
||||||
""" Stop down stuff we started. """
|
""" Stop down stuff we started. """
|
||||||
self.hass._pool.stop()
|
self.hass.stop()
|
||||||
|
|
||||||
user_light_file = self.hass.get_config_path(light.LIGHT_PROFILES_FILE)
|
user_light_file = self.hass.get_config_path(light.LIGHT_PROFILES_FILE)
|
||||||
|
|
||||||
|
@ -39,21 +38,21 @@ class TestLight(unittest.TestCase):
|
||||||
def test_methods(self):
|
def test_methods(self):
|
||||||
""" Test if methods call the services as expected. """
|
""" Test if methods call the services as expected. """
|
||||||
# Test is_on
|
# Test is_on
|
||||||
self.hass.states.set('light.test', components.STATE_ON)
|
self.hass.states.set('light.test', STATE_ON)
|
||||||
self.assertTrue(light.is_on(self.hass, 'light.test'))
|
self.assertTrue(light.is_on(self.hass, 'light.test'))
|
||||||
|
|
||||||
self.hass.states.set('light.test', components.STATE_OFF)
|
self.hass.states.set('light.test', STATE_OFF)
|
||||||
self.assertFalse(light.is_on(self.hass, 'light.test'))
|
self.assertFalse(light.is_on(self.hass, 'light.test'))
|
||||||
|
|
||||||
self.hass.states.set(light.ENTITY_ID_ALL_LIGHTS, components.STATE_ON)
|
self.hass.states.set(light.ENTITY_ID_ALL_LIGHTS, STATE_ON)
|
||||||
self.assertTrue(light.is_on(self.hass))
|
self.assertTrue(light.is_on(self.hass))
|
||||||
|
|
||||||
self.hass.states.set(light.ENTITY_ID_ALL_LIGHTS, components.STATE_OFF)
|
self.hass.states.set(light.ENTITY_ID_ALL_LIGHTS, STATE_OFF)
|
||||||
self.assertFalse(light.is_on(self.hass))
|
self.assertFalse(light.is_on(self.hass))
|
||||||
|
|
||||||
# Test turn_on
|
# Test turn_on
|
||||||
turn_on_calls = mock_service(
|
turn_on_calls = mock_service(
|
||||||
self.hass, light.DOMAIN, components.SERVICE_TURN_ON)
|
self.hass, light.DOMAIN, SERVICE_TURN_ON)
|
||||||
|
|
||||||
light.turn_on(
|
light.turn_on(
|
||||||
self.hass,
|
self.hass,
|
||||||
|
@ -64,44 +63,48 @@ class TestLight(unittest.TestCase):
|
||||||
xy_color='xy_color_val',
|
xy_color='xy_color_val',
|
||||||
profile='profile_val')
|
profile='profile_val')
|
||||||
|
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
|
|
||||||
self.assertEqual(1, len(turn_on_calls))
|
self.assertEqual(1, len(turn_on_calls))
|
||||||
call = turn_on_calls[-1]
|
call = turn_on_calls[-1]
|
||||||
|
|
||||||
self.assertEqual(light.DOMAIN, call.domain)
|
self.assertEqual(light.DOMAIN, call.domain)
|
||||||
self.assertEqual(components.SERVICE_TURN_ON, call.service)
|
self.assertEqual(SERVICE_TURN_ON, call.service)
|
||||||
self.assertEqual('entity_id_val', call.data[components.ATTR_ENTITY_ID])
|
self.assertEqual('entity_id_val', call.data.get(ATTR_ENTITY_ID))
|
||||||
self.assertEqual('transition_val', call.data[light.ATTR_TRANSITION])
|
self.assertEqual(
|
||||||
self.assertEqual('brightness_val', call.data[light.ATTR_BRIGHTNESS])
|
'transition_val', call.data.get(light.ATTR_TRANSITION))
|
||||||
self.assertEqual('rgb_color_val', call.data[light.ATTR_RGB_COLOR])
|
self.assertEqual(
|
||||||
self.assertEqual('xy_color_val', call.data[light.ATTR_XY_COLOR])
|
'brightness_val', call.data.get(light.ATTR_BRIGHTNESS))
|
||||||
self.assertEqual('profile_val', call.data[light.ATTR_PROFILE])
|
self.assertEqual('rgb_color_val', call.data.get(light.ATTR_RGB_COLOR))
|
||||||
|
self.assertEqual('xy_color_val', call.data.get(light.ATTR_XY_COLOR))
|
||||||
|
self.assertEqual('profile_val', call.data.get(light.ATTR_PROFILE))
|
||||||
|
|
||||||
# Test turn_off
|
# Test turn_off
|
||||||
turn_off_calls = mock_service(
|
turn_off_calls = mock_service(
|
||||||
self.hass, light.DOMAIN, components.SERVICE_TURN_OFF)
|
self.hass, light.DOMAIN, SERVICE_TURN_OFF)
|
||||||
|
|
||||||
light.turn_off(
|
light.turn_off(
|
||||||
self.hass, entity_id='entity_id_val', transition='transition_val')
|
self.hass, entity_id='entity_id_val', transition='transition_val')
|
||||||
|
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
|
|
||||||
self.assertEqual(1, len(turn_off_calls))
|
self.assertEqual(1, len(turn_off_calls))
|
||||||
call = turn_off_calls[-1]
|
call = turn_off_calls[-1]
|
||||||
|
|
||||||
self.assertEqual(light.DOMAIN, call.domain)
|
self.assertEqual(light.DOMAIN, call.domain)
|
||||||
self.assertEqual(components.SERVICE_TURN_OFF, call.service)
|
self.assertEqual(SERVICE_TURN_OFF, call.service)
|
||||||
self.assertEqual('entity_id_val', call.data[components.ATTR_ENTITY_ID])
|
self.assertEqual('entity_id_val', call.data[ATTR_ENTITY_ID])
|
||||||
self.assertEqual('transition_val', call.data[light.ATTR_TRANSITION])
|
self.assertEqual('transition_val', call.data[light.ATTR_TRANSITION])
|
||||||
|
|
||||||
def test_services(self):
|
def test_services(self):
|
||||||
""" Test the provided services. """
|
""" Test the provided services. """
|
||||||
mock_toggledevice_platform.init()
|
platform = loader.get_component('light.test')
|
||||||
self.assertTrue(
|
|
||||||
light.setup(self.hass, {light.DOMAIN: {ha.CONF_TYPE: 'test'}}))
|
|
||||||
|
|
||||||
dev1, dev2, dev3 = mock_toggledevice_platform.get_lights(None, None)
|
platform.init()
|
||||||
|
self.assertTrue(
|
||||||
|
light.setup(self.hass, {light.DOMAIN: {CONF_TYPE: 'test'}}))
|
||||||
|
|
||||||
|
dev1, dev2, dev3 = platform.get_lights(None, None)
|
||||||
|
|
||||||
# Test init
|
# Test init
|
||||||
self.assertTrue(light.is_on(self.hass, dev1.entity_id))
|
self.assertTrue(light.is_on(self.hass, dev1.entity_id))
|
||||||
|
@ -112,7 +115,7 @@ class TestLight(unittest.TestCase):
|
||||||
light.turn_off(self.hass, entity_id=dev1.entity_id)
|
light.turn_off(self.hass, entity_id=dev1.entity_id)
|
||||||
light.turn_on(self.hass, entity_id=dev2.entity_id)
|
light.turn_on(self.hass, entity_id=dev2.entity_id)
|
||||||
|
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
|
|
||||||
self.assertFalse(light.is_on(self.hass, dev1.entity_id))
|
self.assertFalse(light.is_on(self.hass, dev1.entity_id))
|
||||||
self.assertTrue(light.is_on(self.hass, dev2.entity_id))
|
self.assertTrue(light.is_on(self.hass, dev2.entity_id))
|
||||||
|
@ -120,7 +123,7 @@ class TestLight(unittest.TestCase):
|
||||||
# turn on all lights
|
# turn on all lights
|
||||||
light.turn_on(self.hass)
|
light.turn_on(self.hass)
|
||||||
|
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
|
|
||||||
self.assertTrue(light.is_on(self.hass, dev1.entity_id))
|
self.assertTrue(light.is_on(self.hass, dev1.entity_id))
|
||||||
self.assertTrue(light.is_on(self.hass, dev2.entity_id))
|
self.assertTrue(light.is_on(self.hass, dev2.entity_id))
|
||||||
|
@ -129,7 +132,7 @@ class TestLight(unittest.TestCase):
|
||||||
# turn off all lights
|
# turn off all lights
|
||||||
light.turn_off(self.hass)
|
light.turn_off(self.hass)
|
||||||
|
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
|
|
||||||
self.assertFalse(light.is_on(self.hass, dev1.entity_id))
|
self.assertFalse(light.is_on(self.hass, dev1.entity_id))
|
||||||
self.assertFalse(light.is_on(self.hass, dev2.entity_id))
|
self.assertFalse(light.is_on(self.hass, dev2.entity_id))
|
||||||
|
@ -142,7 +145,7 @@ class TestLight(unittest.TestCase):
|
||||||
self.hass, dev2.entity_id, rgb_color=[255, 255, 255])
|
self.hass, dev2.entity_id, rgb_color=[255, 255, 255])
|
||||||
light.turn_on(self.hass, dev3.entity_id, xy_color=[.4, .6])
|
light.turn_on(self.hass, dev3.entity_id, xy_color=[.4, .6])
|
||||||
|
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
|
|
||||||
method, data = dev1.last_call('turn_on')
|
method, data = dev1.last_call('turn_on')
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
@ -168,7 +171,7 @@ class TestLight(unittest.TestCase):
|
||||||
self.hass, dev2.entity_id,
|
self.hass, dev2.entity_id,
|
||||||
profile=prof_name, brightness=100, xy_color=[.4, .6])
|
profile=prof_name, brightness=100, xy_color=[.4, .6])
|
||||||
|
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
|
|
||||||
method, data = dev1.last_call('turn_on')
|
method, data = dev1.last_call('turn_on')
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
@ -187,7 +190,7 @@ class TestLight(unittest.TestCase):
|
||||||
light.turn_on(self.hass, dev2.entity_id, xy_color=["bla-di-bla", 5])
|
light.turn_on(self.hass, dev2.entity_id, xy_color=["bla-di-bla", 5])
|
||||||
light.turn_on(self.hass, dev3.entity_id, rgb_color=[255, None, 2])
|
light.turn_on(self.hass, dev3.entity_id, rgb_color=[255, None, 2])
|
||||||
|
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
|
|
||||||
method, data = dev1.last_call('turn_on')
|
method, data = dev1.last_call('turn_on')
|
||||||
self.assertEqual({}, data)
|
self.assertEqual({}, data)
|
||||||
|
@ -203,7 +206,7 @@ class TestLight(unittest.TestCase):
|
||||||
self.hass, dev1.entity_id,
|
self.hass, dev1.entity_id,
|
||||||
profile=prof_name, brightness='bright', rgb_color='yellowish')
|
profile=prof_name, brightness='bright', rgb_color='yellowish')
|
||||||
|
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
|
|
||||||
method, data = dev1.last_call('turn_on')
|
method, data = dev1.last_call('turn_on')
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
@ -220,22 +223,23 @@ class TestLight(unittest.TestCase):
|
||||||
|
|
||||||
# Test with non-existing component
|
# Test with non-existing component
|
||||||
self.assertFalse(light.setup(
|
self.assertFalse(light.setup(
|
||||||
self.hass, {light.DOMAIN: {ha.CONF_TYPE: 'nonexisting'}}
|
self.hass, {light.DOMAIN: {CONF_TYPE: 'nonexisting'}}
|
||||||
))
|
))
|
||||||
|
|
||||||
# Test if light component returns 0 lightes
|
# Test if light component returns 0 lightes
|
||||||
mock_toggledevice_platform.init(True)
|
platform = loader.get_component('light.test')
|
||||||
|
platform.init(True)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual([], platform.get_lights(None, None))
|
||||||
[], mock_toggledevice_platform.get_lights(None, None))
|
|
||||||
|
|
||||||
self.assertFalse(light.setup(
|
self.assertFalse(light.setup(
|
||||||
self.hass, {light.DOMAIN: {ha.CONF_TYPE: 'test'}}
|
self.hass, {light.DOMAIN: {CONF_TYPE: 'test'}}
|
||||||
))
|
))
|
||||||
|
|
||||||
def test_light_profiles(self):
|
def test_light_profiles(self):
|
||||||
""" Test light profiles. """
|
""" Test light profiles. """
|
||||||
mock_toggledevice_platform.init()
|
platform = loader.get_component('light.test')
|
||||||
|
platform.init()
|
||||||
|
|
||||||
user_light_file = self.hass.get_config_path(light.LIGHT_PROFILES_FILE)
|
user_light_file = self.hass.get_config_path(light.LIGHT_PROFILES_FILE)
|
||||||
|
|
||||||
|
@ -245,7 +249,7 @@ class TestLight(unittest.TestCase):
|
||||||
user_file.write('I,WILL,NOT,WORK\n')
|
user_file.write('I,WILL,NOT,WORK\n')
|
||||||
|
|
||||||
self.assertFalse(light.setup(
|
self.assertFalse(light.setup(
|
||||||
self.hass, {light.DOMAIN: {ha.CONF_TYPE: 'test'}}
|
self.hass, {light.DOMAIN: {CONF_TYPE: 'test'}}
|
||||||
))
|
))
|
||||||
|
|
||||||
# Clean up broken file
|
# Clean up broken file
|
||||||
|
@ -256,14 +260,14 @@ class TestLight(unittest.TestCase):
|
||||||
user_file.write('test,.4,.6,100\n')
|
user_file.write('test,.4,.6,100\n')
|
||||||
|
|
||||||
self.assertTrue(light.setup(
|
self.assertTrue(light.setup(
|
||||||
self.hass, {light.DOMAIN: {ha.CONF_TYPE: 'test'}}
|
self.hass, {light.DOMAIN: {CONF_TYPE: 'test'}}
|
||||||
))
|
))
|
||||||
|
|
||||||
dev1, dev2, dev3 = mock_toggledevice_platform.get_lights(None, None)
|
dev1, dev2, dev3 = platform.get_lights(None, None)
|
||||||
|
|
||||||
light.turn_on(self.hass, dev1.entity_id, profile='test')
|
light.turn_on(self.hass, dev1.entity_id, profile='test')
|
||||||
|
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
|
|
||||||
method, data = dev1.last_call('turn_on')
|
method, data = dev1.last_call('turn_on')
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"""
|
"""
|
||||||
test.test_component_sun
|
ha_test.test_component_sun
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Tests Sun component.
|
Tests Sun component.
|
||||||
"""
|
"""
|
||||||
|
@ -11,6 +11,7 @@ import datetime as dt
|
||||||
import ephem
|
import ephem
|
||||||
|
|
||||||
import homeassistant as ha
|
import homeassistant as ha
|
||||||
|
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
|
||||||
import homeassistant.components.sun as sun
|
import homeassistant.components.sun as sun
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,7 +23,7 @@ class TestSun(unittest.TestCase):
|
||||||
|
|
||||||
def tearDown(self): # pylint: disable=invalid-name
|
def tearDown(self): # pylint: disable=invalid-name
|
||||||
""" Stop down stuff we started. """
|
""" Stop down stuff we started. """
|
||||||
self.hass._pool.stop()
|
self.hass.stop()
|
||||||
|
|
||||||
def test_is_on(self):
|
def test_is_on(self):
|
||||||
""" Test is_on method. """
|
""" Test is_on method. """
|
||||||
|
@ -37,8 +38,8 @@ class TestSun(unittest.TestCase):
|
||||||
self.assertTrue(sun.setup(
|
self.assertTrue(sun.setup(
|
||||||
self.hass,
|
self.hass,
|
||||||
{ha.DOMAIN: {
|
{ha.DOMAIN: {
|
||||||
ha.CONF_LATITUDE: '32.87336',
|
CONF_LATITUDE: '32.87336',
|
||||||
ha.CONF_LONGITUDE: '117.22743'
|
CONF_LONGITUDE: '117.22743'
|
||||||
}}))
|
}}))
|
||||||
|
|
||||||
observer = ephem.Observer()
|
observer = ephem.Observer()
|
||||||
|
@ -76,8 +77,8 @@ class TestSun(unittest.TestCase):
|
||||||
self.assertTrue(sun.setup(
|
self.assertTrue(sun.setup(
|
||||||
self.hass,
|
self.hass,
|
||||||
{ha.DOMAIN: {
|
{ha.DOMAIN: {
|
||||||
ha.CONF_LATITUDE: '32.87336',
|
CONF_LATITUDE: '32.87336',
|
||||||
ha.CONF_LONGITUDE: '117.22743'
|
CONF_LONGITUDE: '117.22743'
|
||||||
}}))
|
}}))
|
||||||
|
|
||||||
if sun.is_on(self.hass):
|
if sun.is_on(self.hass):
|
||||||
|
@ -92,7 +93,7 @@ class TestSun(unittest.TestCase):
|
||||||
self.hass.bus.fire(ha.EVENT_TIME_CHANGED,
|
self.hass.bus.fire(ha.EVENT_TIME_CHANGED,
|
||||||
{ha.ATTR_NOW: test_time + dt.timedelta(seconds=5)})
|
{ha.ATTR_NOW: test_time + dt.timedelta(seconds=5)})
|
||||||
|
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
|
|
||||||
self.assertEqual(test_state, self.hass.states.get(sun.ENTITY_ID).state)
|
self.assertEqual(test_state, self.hass.states.get(sun.ENTITY_ID).state)
|
||||||
|
|
||||||
|
@ -101,24 +102,24 @@ class TestSun(unittest.TestCase):
|
||||||
self.assertFalse(sun.setup(self.hass, {}))
|
self.assertFalse(sun.setup(self.hass, {}))
|
||||||
self.assertFalse(sun.setup(self.hass, {sun.DOMAIN: {}}))
|
self.assertFalse(sun.setup(self.hass, {sun.DOMAIN: {}}))
|
||||||
self.assertFalse(sun.setup(
|
self.assertFalse(sun.setup(
|
||||||
self.hass, {ha.DOMAIN: {ha.CONF_LATITUDE: '32.87336'}}))
|
self.hass, {ha.DOMAIN: {CONF_LATITUDE: '32.87336'}}))
|
||||||
self.assertFalse(sun.setup(
|
self.assertFalse(sun.setup(
|
||||||
self.hass, {ha.DOMAIN: {ha.CONF_LONGITUDE: '117.22743'}}))
|
self.hass, {ha.DOMAIN: {CONF_LONGITUDE: '117.22743'}}))
|
||||||
self.assertFalse(sun.setup(
|
self.assertFalse(sun.setup(
|
||||||
self.hass, {ha.DOMAIN: {ha.CONF_LATITUDE: 'hello'}}))
|
self.hass, {ha.DOMAIN: {CONF_LATITUDE: 'hello'}}))
|
||||||
self.assertFalse(sun.setup(
|
self.assertFalse(sun.setup(
|
||||||
self.hass, {ha.DOMAIN: {ha.CONF_LONGITUDE: 'how are you'}}))
|
self.hass, {ha.DOMAIN: {CONF_LONGITUDE: 'how are you'}}))
|
||||||
self.assertFalse(sun.setup(
|
self.assertFalse(sun.setup(
|
||||||
self.hass, {ha.DOMAIN: {
|
self.hass, {ha.DOMAIN: {
|
||||||
ha.CONF_LATITUDE: 'wrong', ha.CONF_LONGITUDE: '117.22743'
|
CONF_LATITUDE: 'wrong', CONF_LONGITUDE: '117.22743'
|
||||||
}}))
|
}}))
|
||||||
self.assertFalse(sun.setup(
|
self.assertFalse(sun.setup(
|
||||||
self.hass, {ha.DOMAIN: {
|
self.hass, {ha.DOMAIN: {
|
||||||
ha.CONF_LATITUDE: '32.87336', ha.CONF_LONGITUDE: 'wrong'
|
CONF_LATITUDE: '32.87336', CONF_LONGITUDE: 'wrong'
|
||||||
}}))
|
}}))
|
||||||
|
|
||||||
# Test with correct config
|
# Test with correct config
|
||||||
self.assertTrue(sun.setup(
|
self.assertTrue(sun.setup(
|
||||||
self.hass, {ha.DOMAIN: {
|
self.hass, {ha.DOMAIN: {
|
||||||
ha.CONF_LATITUDE: '32.87336', ha.CONF_LONGITUDE: '117.22743'
|
CONF_LATITUDE: '32.87336', CONF_LONGITUDE: '117.22743'
|
||||||
}}))
|
}}))
|
|
@ -1,6 +1,6 @@
|
||||||
"""
|
"""
|
||||||
test.test_component_switch
|
ha_test.test_component_switch
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Tests switch component.
|
Tests switch component.
|
||||||
"""
|
"""
|
||||||
|
@ -9,38 +9,39 @@ import unittest
|
||||||
|
|
||||||
import homeassistant as ha
|
import homeassistant as ha
|
||||||
import homeassistant.loader as loader
|
import homeassistant.loader as loader
|
||||||
import homeassistant.components as components
|
from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM
|
||||||
import homeassistant.components.switch as switch
|
import homeassistant.components.switch as switch
|
||||||
|
|
||||||
import mock_toggledevice_platform
|
from helpers import get_test_home_assistant
|
||||||
|
|
||||||
|
|
||||||
class TestSwitch(unittest.TestCase):
|
class TestSwitch(unittest.TestCase):
|
||||||
""" Test the switch module. """
|
""" Test the switch module. """
|
||||||
|
|
||||||
def setUp(self): # pylint: disable=invalid-name
|
def setUp(self): # pylint: disable=invalid-name
|
||||||
self.hass = ha.HomeAssistant()
|
self.hass = get_test_home_assistant()
|
||||||
loader.prepare(self.hass)
|
loader.prepare(self.hass)
|
||||||
loader.set_component('switch.test', mock_toggledevice_platform)
|
|
||||||
|
|
||||||
mock_toggledevice_platform.init()
|
platform = loader.get_component('switch.test')
|
||||||
|
|
||||||
|
platform.init()
|
||||||
self.assertTrue(switch.setup(
|
self.assertTrue(switch.setup(
|
||||||
self.hass, {switch.DOMAIN: {ha.CONF_TYPE: 'test'}}
|
self.hass, {switch.DOMAIN: {CONF_PLATFORM: 'test'}}
|
||||||
))
|
))
|
||||||
|
|
||||||
# Switch 1 is ON, switch 2 is OFF
|
# Switch 1 is ON, switch 2 is OFF
|
||||||
self.switch_1, self.switch_2, self.switch_3 = \
|
self.switch_1, self.switch_2, self.switch_3 = \
|
||||||
mock_toggledevice_platform.get_switches(None, None)
|
platform.get_switches(None, None)
|
||||||
|
|
||||||
def tearDown(self): # pylint: disable=invalid-name
|
def tearDown(self): # pylint: disable=invalid-name
|
||||||
""" Stop down stuff we started. """
|
""" Stop down stuff we started. """
|
||||||
self.hass._pool.stop()
|
self.hass.stop()
|
||||||
|
|
||||||
def test_methods(self):
|
def test_methods(self):
|
||||||
""" Test is_on, turn_on, turn_off methods. """
|
""" Test is_on, turn_on, turn_off methods. """
|
||||||
self.assertTrue(switch.is_on(self.hass))
|
self.assertTrue(switch.is_on(self.hass))
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
components.STATE_ON,
|
STATE_ON,
|
||||||
self.hass.states.get(switch.ENTITY_ID_ALL_SWITCHES).state)
|
self.hass.states.get(switch.ENTITY_ID_ALL_SWITCHES).state)
|
||||||
self.assertTrue(switch.is_on(self.hass, self.switch_1.entity_id))
|
self.assertTrue(switch.is_on(self.hass, self.switch_1.entity_id))
|
||||||
self.assertFalse(switch.is_on(self.hass, self.switch_2.entity_id))
|
self.assertFalse(switch.is_on(self.hass, self.switch_2.entity_id))
|
||||||
|
@ -49,7 +50,7 @@ class TestSwitch(unittest.TestCase):
|
||||||
switch.turn_off(self.hass, self.switch_1.entity_id)
|
switch.turn_off(self.hass, self.switch_1.entity_id)
|
||||||
switch.turn_on(self.hass, self.switch_2.entity_id)
|
switch.turn_on(self.hass, self.switch_2.entity_id)
|
||||||
|
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
|
|
||||||
self.assertTrue(switch.is_on(self.hass))
|
self.assertTrue(switch.is_on(self.hass))
|
||||||
self.assertFalse(switch.is_on(self.hass, self.switch_1.entity_id))
|
self.assertFalse(switch.is_on(self.hass, self.switch_1.entity_id))
|
||||||
|
@ -58,11 +59,11 @@ class TestSwitch(unittest.TestCase):
|
||||||
# Turn all off
|
# Turn all off
|
||||||
switch.turn_off(self.hass)
|
switch.turn_off(self.hass)
|
||||||
|
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
|
|
||||||
self.assertFalse(switch.is_on(self.hass))
|
self.assertFalse(switch.is_on(self.hass))
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
components.STATE_OFF,
|
STATE_OFF,
|
||||||
self.hass.states.get(switch.ENTITY_ID_ALL_SWITCHES).state)
|
self.hass.states.get(switch.ENTITY_ID_ALL_SWITCHES).state)
|
||||||
self.assertFalse(switch.is_on(self.hass, self.switch_1.entity_id))
|
self.assertFalse(switch.is_on(self.hass, self.switch_1.entity_id))
|
||||||
self.assertFalse(switch.is_on(self.hass, self.switch_2.entity_id))
|
self.assertFalse(switch.is_on(self.hass, self.switch_2.entity_id))
|
||||||
|
@ -71,11 +72,11 @@ class TestSwitch(unittest.TestCase):
|
||||||
# Turn all on
|
# Turn all on
|
||||||
switch.turn_on(self.hass)
|
switch.turn_on(self.hass)
|
||||||
|
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
|
|
||||||
self.assertTrue(switch.is_on(self.hass))
|
self.assertTrue(switch.is_on(self.hass))
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
components.STATE_ON,
|
STATE_ON,
|
||||||
self.hass.states.get(switch.ENTITY_ID_ALL_SWITCHES).state)
|
self.hass.states.get(switch.ENTITY_ID_ALL_SWITCHES).state)
|
||||||
self.assertTrue(switch.is_on(self.hass, self.switch_1.entity_id))
|
self.assertTrue(switch.is_on(self.hass, self.switch_1.entity_id))
|
||||||
self.assertTrue(switch.is_on(self.hass, self.switch_2.entity_id))
|
self.assertTrue(switch.is_on(self.hass, self.switch_2.entity_id))
|
||||||
|
@ -89,15 +90,27 @@ class TestSwitch(unittest.TestCase):
|
||||||
|
|
||||||
# Test with non-existing component
|
# Test with non-existing component
|
||||||
self.assertFalse(switch.setup(
|
self.assertFalse(switch.setup(
|
||||||
self.hass, {switch.DOMAIN: {ha.CONF_TYPE: 'nonexisting'}}
|
self.hass, {switch.DOMAIN: {CONF_PLATFORM: 'nonexisting'}}
|
||||||
))
|
))
|
||||||
|
|
||||||
# Test if switch component returns 0 switches
|
# Test if switch component returns 0 switches
|
||||||
mock_toggledevice_platform.init(True)
|
test_platform = loader.get_component('switch.test')
|
||||||
|
test_platform.init(True)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
[], mock_toggledevice_platform.get_switches(None, None))
|
[], test_platform.get_switches(None, None))
|
||||||
|
|
||||||
self.assertFalse(switch.setup(
|
self.assertFalse(switch.setup(
|
||||||
self.hass, {switch.DOMAIN: {ha.CONF_TYPE: 'test'}}
|
self.hass, {switch.DOMAIN: {CONF_PLATFORM: 'test'}}
|
||||||
|
))
|
||||||
|
|
||||||
|
# Test if we can load 2 platforms
|
||||||
|
loader.set_component('switch.test2', test_platform)
|
||||||
|
test_platform.init(False)
|
||||||
|
|
||||||
|
self.assertTrue(switch.setup(
|
||||||
|
self.hass, {
|
||||||
|
switch.DOMAIN: {CONF_PLATFORM: 'test'},
|
||||||
|
'{} 2'.format(switch.DOMAIN): {CONF_PLATFORM: 'test2'},
|
||||||
|
}
|
||||||
))
|
))
|
|
@ -1,6 +1,6 @@
|
||||||
"""
|
"""
|
||||||
test.test_core
|
ha_test.test_core
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Provides tests to verify that Home Assistant core works.
|
Provides tests to verify that Home Assistant core works.
|
||||||
"""
|
"""
|
||||||
|
@ -30,7 +30,7 @@ class TestHomeAssistant(unittest.TestCase):
|
||||||
|
|
||||||
def tearDown(self): # pylint: disable=invalid-name
|
def tearDown(self): # pylint: disable=invalid-name
|
||||||
""" Stop down stuff we started. """
|
""" Stop down stuff we started. """
|
||||||
self.hass._pool.stop()
|
self.hass.stop()
|
||||||
|
|
||||||
def test_get_config_path(self):
|
def test_get_config_path(self):
|
||||||
""" Test get_config_path method. """
|
""" Test get_config_path method. """
|
||||||
|
@ -52,81 +52,18 @@ class TestHomeAssistant(unittest.TestCase):
|
||||||
|
|
||||||
self.assertTrue(blocking_thread.is_alive())
|
self.assertTrue(blocking_thread.is_alive())
|
||||||
|
|
||||||
self.hass.call_service(ha.DOMAIN, ha.SERVICE_HOMEASSISTANT_STOP)
|
self.hass.services.call(ha.DOMAIN, ha.SERVICE_HOMEASSISTANT_STOP)
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
|
|
||||||
# hass.block_till_stopped checks every second if it should quit
|
# hass.block_till_stopped checks every second if it should quit
|
||||||
# we have to wait worst case 1 second
|
# we have to wait worst case 1 second
|
||||||
wait_loops = 0
|
wait_loops = 0
|
||||||
while blocking_thread.is_alive() and wait_loops < 10:
|
while blocking_thread.is_alive() and wait_loops < 50:
|
||||||
wait_loops += 1
|
wait_loops += 1
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
self.assertFalse(blocking_thread.is_alive())
|
self.assertFalse(blocking_thread.is_alive())
|
||||||
|
|
||||||
def test_get_entity_ids(self):
|
|
||||||
""" Test get_entity_ids method. """
|
|
||||||
ent_ids = self.hass.get_entity_ids()
|
|
||||||
self.assertEqual(2, len(ent_ids))
|
|
||||||
self.assertTrue('light.Bowl' in ent_ids)
|
|
||||||
self.assertTrue('switch.AC' in ent_ids)
|
|
||||||
|
|
||||||
ent_ids = self.hass.get_entity_ids('light')
|
|
||||||
self.assertEqual(1, len(ent_ids))
|
|
||||||
self.assertTrue('light.Bowl' in ent_ids)
|
|
||||||
|
|
||||||
def test_track_state_change(self):
|
|
||||||
""" Test track_state_change. """
|
|
||||||
# 2 lists to track how often our callbacks got called
|
|
||||||
specific_runs = []
|
|
||||||
wildcard_runs = []
|
|
||||||
|
|
||||||
self.hass.track_state_change(
|
|
||||||
'light.Bowl', lambda a, b, c: specific_runs.append(1), 'on', 'off')
|
|
||||||
|
|
||||||
self.hass.track_state_change(
|
|
||||||
'light.Bowl', lambda a, b, c: wildcard_runs.append(1),
|
|
||||||
ha.MATCH_ALL, ha.MATCH_ALL)
|
|
||||||
|
|
||||||
# Set same state should not trigger a state change/listener
|
|
||||||
self.hass.states.set('light.Bowl', 'on')
|
|
||||||
self.hass._pool.block_till_done()
|
|
||||||
self.assertEqual(0, len(specific_runs))
|
|
||||||
self.assertEqual(0, len(wildcard_runs))
|
|
||||||
|
|
||||||
# State change off -> on
|
|
||||||
self.hass.states.set('light.Bowl', 'off')
|
|
||||||
self.hass._pool.block_till_done()
|
|
||||||
self.assertEqual(1, len(specific_runs))
|
|
||||||
self.assertEqual(1, len(wildcard_runs))
|
|
||||||
|
|
||||||
# State change off -> off
|
|
||||||
self.hass.states.set('light.Bowl', 'off', {"some_attr": 1})
|
|
||||||
self.hass._pool.block_till_done()
|
|
||||||
self.assertEqual(1, len(specific_runs))
|
|
||||||
self.assertEqual(2, len(wildcard_runs))
|
|
||||||
|
|
||||||
# State change off -> on
|
|
||||||
self.hass.states.set('light.Bowl', 'on')
|
|
||||||
self.hass._pool.block_till_done()
|
|
||||||
self.assertEqual(1, len(specific_runs))
|
|
||||||
self.assertEqual(3, len(wildcard_runs))
|
|
||||||
|
|
||||||
def test_listen_once_event(self):
|
|
||||||
""" Test listen_once_event method. """
|
|
||||||
runs = []
|
|
||||||
|
|
||||||
self.hass.listen_once_event('test_event', lambda x: runs.append(1))
|
|
||||||
|
|
||||||
self.hass.bus.fire('test_event')
|
|
||||||
self.hass._pool.block_till_done()
|
|
||||||
self.assertEqual(1, len(runs))
|
|
||||||
|
|
||||||
# Second time it should not increase runs
|
|
||||||
self.hass.bus.fire('test_event')
|
|
||||||
self.hass._pool.block_till_done()
|
|
||||||
self.assertEqual(1, len(runs))
|
|
||||||
|
|
||||||
def test_track_point_in_time(self):
|
def test_track_point_in_time(self):
|
||||||
""" Test track point in time. """
|
""" Test track point in time. """
|
||||||
before_birthday = datetime(1985, 7, 9, 12, 0, 0)
|
before_birthday = datetime(1985, 7, 9, 12, 0, 0)
|
||||||
|
@ -139,23 +76,23 @@ class TestHomeAssistant(unittest.TestCase):
|
||||||
lambda x: runs.append(1), birthday_paulus)
|
lambda x: runs.append(1), birthday_paulus)
|
||||||
|
|
||||||
self._send_time_changed(before_birthday)
|
self._send_time_changed(before_birthday)
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
self.assertEqual(0, len(runs))
|
self.assertEqual(0, len(runs))
|
||||||
|
|
||||||
self._send_time_changed(birthday_paulus)
|
self._send_time_changed(birthday_paulus)
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
self.assertEqual(1, len(runs))
|
self.assertEqual(1, len(runs))
|
||||||
|
|
||||||
# A point in time tracker will only fire once, this should do nothing
|
# A point in time tracker will only fire once, this should do nothing
|
||||||
self._send_time_changed(birthday_paulus)
|
self._send_time_changed(birthday_paulus)
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
self.assertEqual(1, len(runs))
|
self.assertEqual(1, len(runs))
|
||||||
|
|
||||||
self.hass.track_point_in_time(
|
self.hass.track_point_in_time(
|
||||||
lambda x: runs.append(1), birthday_paulus)
|
lambda x: runs.append(1), birthday_paulus)
|
||||||
|
|
||||||
self._send_time_changed(after_birthday)
|
self._send_time_changed(after_birthday)
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
self.assertEqual(2, len(runs))
|
self.assertEqual(2, len(runs))
|
||||||
|
|
||||||
def test_track_time_change(self):
|
def test_track_time_change(self):
|
||||||
|
@ -168,17 +105,17 @@ class TestHomeAssistant(unittest.TestCase):
|
||||||
lambda x: specific_runs.append(1), second=[0, 30])
|
lambda x: specific_runs.append(1), second=[0, 30])
|
||||||
|
|
||||||
self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0))
|
self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0))
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
self.assertEqual(1, len(specific_runs))
|
self.assertEqual(1, len(specific_runs))
|
||||||
self.assertEqual(1, len(wildcard_runs))
|
self.assertEqual(1, len(wildcard_runs))
|
||||||
|
|
||||||
self._send_time_changed(datetime(2014, 5, 24, 12, 0, 15))
|
self._send_time_changed(datetime(2014, 5, 24, 12, 0, 15))
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
self.assertEqual(1, len(specific_runs))
|
self.assertEqual(1, len(specific_runs))
|
||||||
self.assertEqual(2, len(wildcard_runs))
|
self.assertEqual(2, len(wildcard_runs))
|
||||||
|
|
||||||
self._send_time_changed(datetime(2014, 5, 24, 12, 0, 30))
|
self._send_time_changed(datetime(2014, 5, 24, 12, 0, 30))
|
||||||
self.hass._pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
self.assertEqual(2, len(specific_runs))
|
self.assertEqual(2, len(specific_runs))
|
||||||
self.assertEqual(3, len(wildcard_runs))
|
self.assertEqual(3, len(wildcard_runs))
|
||||||
|
|
||||||
|
@ -234,6 +171,21 @@ class TestEventBus(unittest.TestCase):
|
||||||
# Try deleting listener while category doesn't exist either
|
# Try deleting listener while category doesn't exist either
|
||||||
self.bus.remove_listener('test', listener)
|
self.bus.remove_listener('test', listener)
|
||||||
|
|
||||||
|
def test_listen_once_event(self):
|
||||||
|
""" Test listen_once_event method. """
|
||||||
|
runs = []
|
||||||
|
|
||||||
|
self.bus.listen_once('test_event', lambda x: runs.append(1))
|
||||||
|
|
||||||
|
self.bus.fire('test_event')
|
||||||
|
self.bus._pool.block_till_done()
|
||||||
|
self.assertEqual(1, len(runs))
|
||||||
|
|
||||||
|
# Second time it should not increase runs
|
||||||
|
self.bus.fire('test_event')
|
||||||
|
self.bus._pool.block_till_done()
|
||||||
|
self.assertEqual(1, len(runs))
|
||||||
|
|
||||||
|
|
||||||
class TestState(unittest.TestCase):
|
class TestState(unittest.TestCase):
|
||||||
""" Test EventBus methods. """
|
""" Test EventBus methods. """
|
||||||
|
@ -276,15 +228,76 @@ class TestStateMachine(unittest.TestCase):
|
||||||
self.assertFalse(self.states.is_state('light.Bowl', 'off'))
|
self.assertFalse(self.states.is_state('light.Bowl', 'off'))
|
||||||
self.assertFalse(self.states.is_state('light.Non_existing', 'on'))
|
self.assertFalse(self.states.is_state('light.Non_existing', 'on'))
|
||||||
|
|
||||||
|
def test_entity_ids(self):
|
||||||
|
""" Test get_entity_ids method. """
|
||||||
|
ent_ids = self.states.entity_ids()
|
||||||
|
self.assertEqual(2, len(ent_ids))
|
||||||
|
self.assertTrue('light.Bowl' in ent_ids)
|
||||||
|
self.assertTrue('switch.AC' in ent_ids)
|
||||||
|
|
||||||
|
ent_ids = self.states.entity_ids('light')
|
||||||
|
self.assertEqual(1, len(ent_ids))
|
||||||
|
self.assertTrue('light.Bowl' in ent_ids)
|
||||||
|
|
||||||
def test_remove(self):
|
def test_remove(self):
|
||||||
""" Test remove method. """
|
""" Test remove method. """
|
||||||
self.assertTrue('light.Bowl' in self.states.entity_ids)
|
self.assertTrue('light.Bowl' in self.states.entity_ids())
|
||||||
self.assertTrue(self.states.remove('light.Bowl'))
|
self.assertTrue(self.states.remove('light.Bowl'))
|
||||||
self.assertFalse('light.Bowl' in self.states.entity_ids)
|
self.assertFalse('light.Bowl' in self.states.entity_ids())
|
||||||
|
|
||||||
# If it does not exist, we should get False
|
# If it does not exist, we should get False
|
||||||
self.assertFalse(self.states.remove('light.Bowl'))
|
self.assertFalse(self.states.remove('light.Bowl'))
|
||||||
|
|
||||||
|
def test_track_change(self):
|
||||||
|
""" Test states.track_change. """
|
||||||
|
# 2 lists to track how often our callbacks got called
|
||||||
|
specific_runs = []
|
||||||
|
wildcard_runs = []
|
||||||
|
|
||||||
|
self.states.track_change(
|
||||||
|
'light.Bowl', lambda a, b, c: specific_runs.append(1), 'on', 'off')
|
||||||
|
|
||||||
|
self.states.track_change(
|
||||||
|
'light.Bowl', lambda a, b, c: wildcard_runs.append(1),
|
||||||
|
ha.MATCH_ALL, ha.MATCH_ALL)
|
||||||
|
|
||||||
|
# Set same state should not trigger a state change/listener
|
||||||
|
self.states.set('light.Bowl', 'on')
|
||||||
|
self.bus._pool.block_till_done()
|
||||||
|
self.assertEqual(0, len(specific_runs))
|
||||||
|
self.assertEqual(0, len(wildcard_runs))
|
||||||
|
|
||||||
|
# State change off -> on
|
||||||
|
self.states.set('light.Bowl', 'off')
|
||||||
|
self.bus._pool.block_till_done()
|
||||||
|
self.assertEqual(1, len(specific_runs))
|
||||||
|
self.assertEqual(1, len(wildcard_runs))
|
||||||
|
|
||||||
|
# State change off -> off
|
||||||
|
self.states.set('light.Bowl', 'off', {"some_attr": 1})
|
||||||
|
self.bus._pool.block_till_done()
|
||||||
|
self.assertEqual(1, len(specific_runs))
|
||||||
|
self.assertEqual(2, len(wildcard_runs))
|
||||||
|
|
||||||
|
# State change off -> on
|
||||||
|
self.states.set('light.Bowl', 'on')
|
||||||
|
self.bus._pool.block_till_done()
|
||||||
|
self.assertEqual(1, len(specific_runs))
|
||||||
|
self.assertEqual(3, len(wildcard_runs))
|
||||||
|
|
||||||
|
def test_case_insensitivty(self):
|
||||||
|
runs = []
|
||||||
|
|
||||||
|
self.states.track_change(
|
||||||
|
'light.BoWl', lambda a, b, c: runs.append(1),
|
||||||
|
ha.MATCH_ALL, ha.MATCH_ALL)
|
||||||
|
|
||||||
|
self.states.set('light.BOWL', 'off')
|
||||||
|
self.bus._pool.block_till_done()
|
||||||
|
|
||||||
|
self.assertTrue(self.states.is_state('light.bowl', 'off'))
|
||||||
|
self.assertEqual(1, len(runs))
|
||||||
|
|
||||||
|
|
||||||
class TestServiceCall(unittest.TestCase):
|
class TestServiceCall(unittest.TestCase):
|
||||||
""" Test ServiceCall class. """
|
""" Test ServiceCall class. """
|
|
@ -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.
|
Tests Home Assistant remote methods and classes.
|
||||||
|
Uses port 8122 for master, 8123 for slave
|
||||||
|
Uses port 8125 as a port that nothing runs on
|
||||||
"""
|
"""
|
||||||
# pylint: disable=protected-access,too-many-public-methods
|
# pylint: disable=protected-access,too-many-public-methods
|
||||||
import unittest
|
import unittest
|
||||||
|
@ -13,11 +15,11 @@ import homeassistant.components.http as http
|
||||||
|
|
||||||
API_PASSWORD = "test1234"
|
API_PASSWORD = "test1234"
|
||||||
|
|
||||||
HTTP_BASE_URL = "http://127.0.0.1:{}".format(remote.SERVER_PORT)
|
HTTP_BASE_URL = "http://127.0.0.1:8122"
|
||||||
|
|
||||||
HA_HEADERS = {remote.AUTH_HEADER: API_PASSWORD}
|
HA_HEADERS = {remote.AUTH_HEADER: API_PASSWORD}
|
||||||
|
|
||||||
hass, slave, master_api = None, None, None
|
hass, slave, master_api, broken_api = None, None, None, None
|
||||||
|
|
||||||
|
|
||||||
def _url(path=""):
|
def _url(path=""):
|
||||||
|
@ -27,7 +29,7 @@ def _url(path=""):
|
||||||
|
|
||||||
def setUpModule(): # pylint: disable=invalid-name
|
def setUpModule(): # pylint: disable=invalid-name
|
||||||
""" Initalizes a Home Assistant server and Slave instance. """
|
""" Initalizes a Home Assistant server and Slave instance. """
|
||||||
global hass, slave, master_api
|
global hass, slave, master_api, broken_api
|
||||||
|
|
||||||
hass = ha.HomeAssistant()
|
hass = ha.HomeAssistant()
|
||||||
|
|
||||||
|
@ -35,29 +37,28 @@ def setUpModule(): # pylint: disable=invalid-name
|
||||||
hass.states.set('test.test', 'a_state')
|
hass.states.set('test.test', 'a_state')
|
||||||
|
|
||||||
http.setup(hass,
|
http.setup(hass,
|
||||||
{http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD}})
|
{http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD,
|
||||||
|
http.CONF_SERVER_PORT: 8122}})
|
||||||
|
|
||||||
hass.start()
|
hass.start()
|
||||||
|
|
||||||
master_api = remote.API("127.0.0.1", API_PASSWORD)
|
master_api = remote.API("127.0.0.1", API_PASSWORD, 8122)
|
||||||
|
|
||||||
# Start slave
|
# Start slave
|
||||||
local_api = remote.API("127.0.0.1", API_PASSWORD, 8124)
|
slave = remote.HomeAssistant(master_api)
|
||||||
slave = remote.HomeAssistant(master_api, local_api)
|
|
||||||
|
|
||||||
http.setup(slave,
|
|
||||||
{http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD,
|
|
||||||
http.CONF_SERVER_PORT: 8124}})
|
|
||||||
|
|
||||||
slave.start()
|
slave.start()
|
||||||
|
|
||||||
|
# Setup API pointing at nothing
|
||||||
|
broken_api = remote.API("127.0.0.1", "", 8125)
|
||||||
|
|
||||||
|
|
||||||
def tearDownModule(): # pylint: disable=invalid-name
|
def tearDownModule(): # pylint: disable=invalid-name
|
||||||
""" Stops the Home Assistant server and slave. """
|
""" Stops the Home Assistant server and slave. """
|
||||||
global hass, slave
|
global hass, slave
|
||||||
|
|
||||||
hass.stop()
|
|
||||||
slave.stop()
|
slave.stop()
|
||||||
|
hass.stop()
|
||||||
|
|
||||||
|
|
||||||
class TestRemoteMethods(unittest.TestCase):
|
class TestRemoteMethods(unittest.TestCase):
|
||||||
|
@ -71,6 +72,9 @@ class TestRemoteMethods(unittest.TestCase):
|
||||||
remote.validate_api(
|
remote.validate_api(
|
||||||
remote.API("127.0.0.1", API_PASSWORD + "A")))
|
remote.API("127.0.0.1", API_PASSWORD + "A")))
|
||||||
|
|
||||||
|
self.assertEqual(remote.APIStatus.CANNOT_CONNECT,
|
||||||
|
remote.validate_api(broken_api))
|
||||||
|
|
||||||
def test_get_event_listeners(self):
|
def test_get_event_listeners(self):
|
||||||
""" Test Python API get_event_listeners. """
|
""" Test Python API get_event_listeners. """
|
||||||
local_data = hass.bus.listeners
|
local_data = hass.bus.listeners
|
||||||
|
@ -82,6 +86,8 @@ class TestRemoteMethods(unittest.TestCase):
|
||||||
|
|
||||||
self.assertEqual(len(local_data), 0)
|
self.assertEqual(len(local_data), 0)
|
||||||
|
|
||||||
|
self.assertEqual({}, remote.get_event_listeners(broken_api))
|
||||||
|
|
||||||
def test_fire_event(self):
|
def test_fire_event(self):
|
||||||
""" Test Python API fire_event. """
|
""" Test Python API fire_event. """
|
||||||
test_value = []
|
test_value = []
|
||||||
|
@ -90,14 +96,17 @@ class TestRemoteMethods(unittest.TestCase):
|
||||||
""" Helper method that will verify our event got called. """
|
""" Helper method that will verify our event got called. """
|
||||||
test_value.append(1)
|
test_value.append(1)
|
||||||
|
|
||||||
hass.listen_once_event("test.event_no_data", listener)
|
hass.bus.listen_once("test.event_no_data", listener)
|
||||||
|
|
||||||
remote.fire_event(master_api, "test.event_no_data")
|
remote.fire_event(master_api, "test.event_no_data")
|
||||||
|
|
||||||
hass._pool.block_till_done()
|
hass.pool.block_till_done()
|
||||||
|
|
||||||
self.assertEqual(1, len(test_value))
|
self.assertEqual(1, len(test_value))
|
||||||
|
|
||||||
|
# Should not trigger any exception
|
||||||
|
remote.fire_event(broken_api, "test.event_no_data")
|
||||||
|
|
||||||
def test_get_state(self):
|
def test_get_state(self):
|
||||||
""" Test Python API get_state. """
|
""" Test Python API get_state. """
|
||||||
|
|
||||||
|
@ -105,11 +114,13 @@ class TestRemoteMethods(unittest.TestCase):
|
||||||
hass.states.get('test.test'),
|
hass.states.get('test.test'),
|
||||||
remote.get_state(master_api, 'test.test'))
|
remote.get_state(master_api, 'test.test'))
|
||||||
|
|
||||||
|
self.assertEqual(None, remote.get_state(broken_api, 'test.test'))
|
||||||
|
|
||||||
def test_get_states(self):
|
def test_get_states(self):
|
||||||
""" Test Python API get_state_entity_ids. """
|
""" Test Python API get_state_entity_ids. """
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(hass.states.all(), remote.get_states(master_api))
|
||||||
remote.get_states(master_api), hass.states.all())
|
self.assertEqual([], remote.get_states(broken_api))
|
||||||
|
|
||||||
def test_set_state(self):
|
def test_set_state(self):
|
||||||
""" Test Python API set_state. """
|
""" Test Python API set_state. """
|
||||||
|
@ -117,6 +128,8 @@ class TestRemoteMethods(unittest.TestCase):
|
||||||
|
|
||||||
self.assertEqual('set_test', hass.states.get('test.test').state)
|
self.assertEqual('set_test', hass.states.get('test.test').state)
|
||||||
|
|
||||||
|
self.assertFalse(remote.set_state(broken_api, 'test.test', 'set_test'))
|
||||||
|
|
||||||
def test_is_state(self):
|
def test_is_state(self):
|
||||||
""" Test Python API is_state. """
|
""" Test Python API is_state. """
|
||||||
|
|
||||||
|
@ -124,6 +137,10 @@ class TestRemoteMethods(unittest.TestCase):
|
||||||
remote.is_state(master_api, 'test.test',
|
remote.is_state(master_api, 'test.test',
|
||||||
hass.states.get('test.test').state))
|
hass.states.get('test.test').state))
|
||||||
|
|
||||||
|
self.assertFalse(
|
||||||
|
remote.is_state(broken_api, 'test.test',
|
||||||
|
hass.states.get('test.test').state))
|
||||||
|
|
||||||
def test_get_services(self):
|
def test_get_services(self):
|
||||||
""" Test Python API get_services. """
|
""" Test Python API get_services. """
|
||||||
|
|
||||||
|
@ -134,8 +151,10 @@ class TestRemoteMethods(unittest.TestCase):
|
||||||
|
|
||||||
self.assertEqual(local, serv_domain["services"])
|
self.assertEqual(local, serv_domain["services"])
|
||||||
|
|
||||||
|
self.assertEqual({}, remote.get_services(broken_api))
|
||||||
|
|
||||||
def test_call_service(self):
|
def test_call_service(self):
|
||||||
""" Test Python API call_service. """
|
""" Test Python API services.call. """
|
||||||
test_value = []
|
test_value = []
|
||||||
|
|
||||||
def listener(service_call): # pylint: disable=unused-argument
|
def listener(service_call): # pylint: disable=unused-argument
|
||||||
|
@ -146,20 +165,29 @@ class TestRemoteMethods(unittest.TestCase):
|
||||||
|
|
||||||
remote.call_service(master_api, "test_domain", "test_service")
|
remote.call_service(master_api, "test_domain", "test_service")
|
||||||
|
|
||||||
hass._pool.block_till_done()
|
hass.pool.block_till_done()
|
||||||
|
|
||||||
self.assertEqual(1, len(test_value))
|
self.assertEqual(1, len(test_value))
|
||||||
|
|
||||||
|
# Should not raise an exception
|
||||||
|
remote.call_service(broken_api, "test_domain", "test_service")
|
||||||
|
|
||||||
|
|
||||||
class TestRemoteClasses(unittest.TestCase):
|
class TestRemoteClasses(unittest.TestCase):
|
||||||
""" Test the homeassistant.remote module. """
|
""" Test the homeassistant.remote module. """
|
||||||
|
|
||||||
def test_home_assistant_init(self):
|
def test_home_assistant_init(self):
|
||||||
""" Test HomeAssistant init. """
|
""" Test HomeAssistant init. """
|
||||||
|
# Wrong password
|
||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
ha.HomeAssistantError, remote.HomeAssistant,
|
ha.HomeAssistantError, remote.HomeAssistant,
|
||||||
remote.API('127.0.0.1', API_PASSWORD + 'A', 8124))
|
remote.API('127.0.0.1', API_PASSWORD + 'A', 8124))
|
||||||
|
|
||||||
|
# Wrong port
|
||||||
|
self.assertRaises(
|
||||||
|
ha.HomeAssistantError, remote.HomeAssistant,
|
||||||
|
remote.API('127.0.0.1', API_PASSWORD, 8125))
|
||||||
|
|
||||||
def test_statemachine_init(self):
|
def test_statemachine_init(self):
|
||||||
""" Tests if remote.StateMachine copies all states on init. """
|
""" Tests if remote.StateMachine copies all states on init. """
|
||||||
self.assertEqual(len(hass.states.all()),
|
self.assertEqual(len(hass.states.all()),
|
||||||
|
@ -176,7 +204,7 @@ class TestRemoteClasses(unittest.TestCase):
|
||||||
# Wait till slave tells master
|
# Wait till slave tells master
|
||||||
slave._pool.block_till_done()
|
slave._pool.block_till_done()
|
||||||
# Wait till master gives updated state
|
# Wait till master gives updated state
|
||||||
hass._pool.block_till_done()
|
hass.pool.block_till_done()
|
||||||
|
|
||||||
self.assertEqual("remote.statemachine test",
|
self.assertEqual("remote.statemachine test",
|
||||||
slave.states.get("remote.test").state)
|
slave.states.get("remote.test").state)
|
||||||
|
@ -189,13 +217,23 @@ class TestRemoteClasses(unittest.TestCase):
|
||||||
""" Helper method that will verify our event got called. """
|
""" Helper method that will verify our event got called. """
|
||||||
test_value.append(1)
|
test_value.append(1)
|
||||||
|
|
||||||
slave.listen_once_event("test.event_no_data", listener)
|
slave.bus.listen_once("test.event_no_data", listener)
|
||||||
|
|
||||||
slave.bus.fire("test.event_no_data")
|
slave.bus.fire("test.event_no_data")
|
||||||
|
|
||||||
# Wait till slave tells master
|
# Wait till slave tells master
|
||||||
slave._pool.block_till_done()
|
slave._pool.block_till_done()
|
||||||
# Wait till master gives updated event
|
# Wait till master gives updated event
|
||||||
hass._pool.block_till_done()
|
hass.pool.block_till_done()
|
||||||
|
|
||||||
self.assertEqual(1, len(test_value))
|
self.assertEqual(1, len(test_value))
|
||||||
|
|
||||||
|
def test_json_encoder(self):
|
||||||
|
""" Test the JSON Encoder. """
|
||||||
|
ha_json_enc = remote.JSONEncoder()
|
||||||
|
state = hass.states.get('test.test')
|
||||||
|
|
||||||
|
self.assertEqual(state.as_dict(), ha_json_enc.default(state))
|
||||||
|
|
||||||
|
# Default method raises TypeError if non HA object
|
||||||
|
self.assertRaises(TypeError, ha_json_enc.default, 1)
|
|
@ -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 datetime as dt
|
||||||
import functools as ft
|
import functools as ft
|
||||||
|
|
||||||
|
from requests.structures import CaseInsensitiveDict
|
||||||
|
|
||||||
|
from homeassistant.const import (
|
||||||
|
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
||||||
|
SERVICE_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED,
|
||||||
|
EVENT_CALL_SERVICE, ATTR_NOW, ATTR_DOMAIN, ATTR_SERVICE, MATCH_ALL,
|
||||||
|
EVENT_SERVICE_EXECUTED, ATTR_SERVICE_CALL_ID)
|
||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
|
|
||||||
MATCH_ALL = '*'
|
|
||||||
|
|
||||||
DOMAIN = "homeassistant"
|
DOMAIN = "homeassistant"
|
||||||
|
|
||||||
SERVICE_HOMEASSISTANT_STOP = "stop"
|
|
||||||
|
|
||||||
EVENT_HOMEASSISTANT_START = "homeassistant_start"
|
|
||||||
EVENT_HOMEASSISTANT_STOP = "homeassistant_stop"
|
|
||||||
EVENT_STATE_CHANGED = "state_changed"
|
|
||||||
EVENT_TIME_CHANGED = "time_changed"
|
|
||||||
EVENT_CALL_SERVICE = "call_service"
|
|
||||||
|
|
||||||
ATTR_NOW = "now"
|
|
||||||
ATTR_DOMAIN = "domain"
|
|
||||||
ATTR_SERVICE = "service"
|
|
||||||
|
|
||||||
CONF_LATITUDE = "latitude"
|
|
||||||
CONF_LONGITUDE = "longitude"
|
|
||||||
CONF_TYPE = "type"
|
|
||||||
CONF_HOST = "host"
|
|
||||||
CONF_HOSTS = "hosts"
|
|
||||||
CONF_USERNAME = "username"
|
|
||||||
CONF_PASSWORD = "password"
|
|
||||||
|
|
||||||
# How often time_changed event should fire
|
# How often time_changed event should fire
|
||||||
TIMER_INTERVAL = 10 # seconds
|
TIMER_INTERVAL = 10 # seconds
|
||||||
|
|
||||||
# Number of worker threads
|
# How long we wait for the result of a service call
|
||||||
POOL_NUM_THREAD = 4
|
SERVICE_CALL_LIMIT = 10 # seconds
|
||||||
|
|
||||||
|
# Define number of MINIMUM worker threads.
|
||||||
|
# During bootstrap of HA (see bootstrap.from_config_dict()) worker threads
|
||||||
|
# will be added for each component that polls devices.
|
||||||
|
MIN_WORKER_THREAD = 2
|
||||||
|
|
||||||
# Pattern for validating entity IDs (format: <domain>.<entity>)
|
# Pattern for validating entity IDs (format: <domain>.<entity>)
|
||||||
ENTITY_ID_PATTERN = re.compile(r"^(?P<domain>\w+)\.(?P<entity>\w+)$")
|
ENTITY_ID_PATTERN = re.compile(r"^(?P<domain>\w+)\.(?P<entity>\w+)$")
|
||||||
|
@ -57,8 +47,7 @@ class HomeAssistant(object):
|
||||||
""" Core class to route all communication to right components. """
|
""" Core class to route all communication to right components. """
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._pool = pool = create_worker_pool()
|
self.pool = pool = create_worker_pool()
|
||||||
|
|
||||||
self.bus = EventBus(pool)
|
self.bus = EventBus(pool)
|
||||||
self.services = ServiceRegistry(self.bus, pool)
|
self.services = ServiceRegistry(self.bus, pool)
|
||||||
self.states = StateMachine(self.bus)
|
self.states = StateMachine(self.bus)
|
||||||
|
@ -71,6 +60,9 @@ class HomeAssistant(object):
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
""" Start home assistant. """
|
""" Start home assistant. """
|
||||||
|
_LOGGER.info(
|
||||||
|
"Starting Home Assistant (%d threads)", self.pool.worker_count)
|
||||||
|
|
||||||
Timer(self)
|
Timer(self)
|
||||||
|
|
||||||
self.bus.fire(EVENT_HOMEASSISTANT_START)
|
self.bus.fire(EVENT_HOMEASSISTANT_START)
|
||||||
|
@ -92,50 +84,6 @@ class HomeAssistant(object):
|
||||||
|
|
||||||
self.stop()
|
self.stop()
|
||||||
|
|
||||||
def call_service(self, domain, service, service_data=None):
|
|
||||||
""" Fires event to call specified service. """
|
|
||||||
event_data = service_data or {}
|
|
||||||
event_data[ATTR_DOMAIN] = domain
|
|
||||||
event_data[ATTR_SERVICE] = service
|
|
||||||
|
|
||||||
self.bus.fire(EVENT_CALL_SERVICE, event_data)
|
|
||||||
|
|
||||||
def get_entity_ids(self, domain_filter=None):
|
|
||||||
""" Returns known entity ids. """
|
|
||||||
if domain_filter:
|
|
||||||
return [entity_id for entity_id in self.states.entity_ids
|
|
||||||
if entity_id.startswith(domain_filter)]
|
|
||||||
else:
|
|
||||||
return self.states.entity_ids
|
|
||||||
|
|
||||||
def track_state_change(self, entity_ids, action,
|
|
||||||
from_state=None, to_state=None):
|
|
||||||
"""
|
|
||||||
Track specific state changes.
|
|
||||||
entity_ids, from_state and to_state can be string or list.
|
|
||||||
Use list to match multiple.
|
|
||||||
"""
|
|
||||||
from_state = _process_match_param(from_state)
|
|
||||||
to_state = _process_match_param(to_state)
|
|
||||||
|
|
||||||
# Ensure it is a list with entity ids we want to match on
|
|
||||||
if isinstance(entity_ids, str):
|
|
||||||
entity_ids = [entity_ids]
|
|
||||||
|
|
||||||
@ft.wraps(action)
|
|
||||||
def state_listener(event):
|
|
||||||
""" The listener that listens for specific state changes. """
|
|
||||||
if event.data['entity_id'] in entity_ids and \
|
|
||||||
'old_state' in event.data and \
|
|
||||||
_matcher(event.data['old_state'].state, from_state) and \
|
|
||||||
_matcher(event.data['new_state'].state, to_state):
|
|
||||||
|
|
||||||
action(event.data['entity_id'],
|
|
||||||
event.data['old_state'],
|
|
||||||
event.data['new_state'])
|
|
||||||
|
|
||||||
self.bus.listen(EVENT_STATE_CHANGED, state_listener)
|
|
||||||
|
|
||||||
def track_point_in_time(self, action, point_in_time):
|
def track_point_in_time(self, action, point_in_time):
|
||||||
"""
|
"""
|
||||||
Adds a listener that fires once at or after a spefic point in time.
|
Adds a listener that fires once at or after a spefic point in time.
|
||||||
|
@ -202,31 +150,6 @@ class HomeAssistant(object):
|
||||||
|
|
||||||
self.bus.listen(EVENT_TIME_CHANGED, time_listener)
|
self.bus.listen(EVENT_TIME_CHANGED, time_listener)
|
||||||
|
|
||||||
def listen_once_event(self, event_type, listener):
|
|
||||||
""" Listen once for event of a specific type.
|
|
||||||
|
|
||||||
To listen to all events specify the constant ``MATCH_ALL``
|
|
||||||
as event_type.
|
|
||||||
|
|
||||||
Note: at the moment it is impossible to remove a one time listener.
|
|
||||||
"""
|
|
||||||
@ft.wraps(listener)
|
|
||||||
def onetime_listener(event):
|
|
||||||
""" Removes listener from eventbus and then fires listener. """
|
|
||||||
if not hasattr(onetime_listener, 'run'):
|
|
||||||
# Set variable so that we will never run twice.
|
|
||||||
# Because the event bus might have to wait till a thread comes
|
|
||||||
# available to execute this listener it might occur that the
|
|
||||||
# listener gets lined up twice to be executed.
|
|
||||||
# This will make sure the second time it does nothing.
|
|
||||||
onetime_listener.run = True
|
|
||||||
|
|
||||||
self.bus.remove_listener(event_type, onetime_listener)
|
|
||||||
|
|
||||||
listener(event)
|
|
||||||
|
|
||||||
self.bus.listen(event_type, onetime_listener)
|
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
""" Stops Home Assistant and shuts down all threads. """
|
""" Stops Home Assistant and shuts down all threads. """
|
||||||
_LOGGER.info("Stopping")
|
_LOGGER.info("Stopping")
|
||||||
|
@ -234,14 +157,67 @@ class HomeAssistant(object):
|
||||||
self.bus.fire(EVENT_HOMEASSISTANT_STOP)
|
self.bus.fire(EVENT_HOMEASSISTANT_STOP)
|
||||||
|
|
||||||
# Wait till all responses to homeassistant_stop are done
|
# Wait till all responses to homeassistant_stop are done
|
||||||
self._pool.block_till_done()
|
self.pool.block_till_done()
|
||||||
|
|
||||||
self._pool.stop()
|
self.pool.stop()
|
||||||
|
|
||||||
|
def get_entity_ids(self, domain_filter=None):
|
||||||
|
"""
|
||||||
|
Returns known entity ids.
|
||||||
|
|
||||||
|
THIS METHOD IS DEPRECATED. Use hass.states.entity_ids
|
||||||
|
"""
|
||||||
|
_LOGGER.warning(
|
||||||
|
"hass.get_entiy_ids is deprecated. Use hass.states.entity_ids")
|
||||||
|
|
||||||
|
return self.states.entity_ids(domain_filter)
|
||||||
|
|
||||||
|
def listen_once_event(self, event_type, listener):
|
||||||
|
""" Listen once for event of a specific type.
|
||||||
|
|
||||||
|
To listen to all events specify the constant ``MATCH_ALL``
|
||||||
|
as event_type.
|
||||||
|
|
||||||
|
Note: at the moment it is impossible to remove a one time listener.
|
||||||
|
|
||||||
|
THIS METHOD IS DEPRECATED. Please use hass.events.listen_once.
|
||||||
|
"""
|
||||||
|
_LOGGER.warning(
|
||||||
|
"hass.listen_once_event is deprecated. Use hass.bus.listen_once")
|
||||||
|
|
||||||
|
self.bus.listen_once(event_type, listener)
|
||||||
|
|
||||||
|
def track_state_change(self, entity_ids, action,
|
||||||
|
from_state=None, to_state=None):
|
||||||
|
"""
|
||||||
|
Track specific state changes.
|
||||||
|
entity_ids, from_state and to_state can be string or list.
|
||||||
|
Use list to match multiple.
|
||||||
|
|
||||||
|
THIS METHOD IS DEPRECATED. Use hass.states.track_change
|
||||||
|
"""
|
||||||
|
_LOGGER.warning((
|
||||||
|
"hass.track_state_change is deprecated. "
|
||||||
|
"Use hass.states.track_change"))
|
||||||
|
|
||||||
|
self.states.track_change(entity_ids, action, from_state, to_state)
|
||||||
|
|
||||||
|
def call_service(self, domain, service, service_data=None):
|
||||||
|
"""
|
||||||
|
Fires event to call specified service.
|
||||||
|
|
||||||
|
THIS METHOD IS DEPRECATED. Use hass.services.call
|
||||||
|
"""
|
||||||
|
_LOGGER.warning((
|
||||||
|
"hass.services.call is deprecated. "
|
||||||
|
"Use hass.services.call"))
|
||||||
|
|
||||||
|
self.services.call(domain, service, service_data)
|
||||||
|
|
||||||
|
|
||||||
def _process_match_param(parameter):
|
def _process_match_param(parameter):
|
||||||
""" Wraps parameter in a list if it is not one and returns it. """
|
""" Wraps parameter in a list if it is not one and returns it. """
|
||||||
if not parameter or parameter == MATCH_ALL:
|
if parameter is None or parameter == MATCH_ALL:
|
||||||
return MATCH_ALL
|
return MATCH_ALL
|
||||||
elif isinstance(parameter, list):
|
elif isinstance(parameter, list):
|
||||||
return parameter
|
return parameter
|
||||||
|
@ -261,6 +237,7 @@ class JobPriority(util.OrderedEnum):
|
||||||
""" Provides priorities for bus events. """
|
""" Provides priorities for bus events. """
|
||||||
# pylint: disable=no-init,too-few-public-methods
|
# pylint: disable=no-init,too-few-public-methods
|
||||||
|
|
||||||
|
EVENT_CALLBACK = 0
|
||||||
EVENT_SERVICE = 1
|
EVENT_SERVICE = 1
|
||||||
EVENT_STATE = 2
|
EVENT_STATE = 2
|
||||||
EVENT_TIME = 3
|
EVENT_TIME = 3
|
||||||
|
@ -275,11 +252,13 @@ class JobPriority(util.OrderedEnum):
|
||||||
return JobPriority.EVENT_STATE
|
return JobPriority.EVENT_STATE
|
||||||
elif event_type == EVENT_CALL_SERVICE:
|
elif event_type == EVENT_CALL_SERVICE:
|
||||||
return JobPriority.EVENT_SERVICE
|
return JobPriority.EVENT_SERVICE
|
||||||
|
elif event_type == EVENT_SERVICE_EXECUTED:
|
||||||
|
return JobPriority.EVENT_CALLBACK
|
||||||
else:
|
else:
|
||||||
return JobPriority.EVENT_DEFAULT
|
return JobPriority.EVENT_DEFAULT
|
||||||
|
|
||||||
|
|
||||||
def create_worker_pool(thread_count=POOL_NUM_THREAD):
|
def create_worker_pool():
|
||||||
""" Creates a worker pool to be used. """
|
""" Creates a worker pool to be used. """
|
||||||
|
|
||||||
def job_handler(job):
|
def job_handler(job):
|
||||||
|
@ -292,18 +271,18 @@ def create_worker_pool(thread_count=POOL_NUM_THREAD):
|
||||||
# We do not want to crash our ThreadPool
|
# We do not want to crash our ThreadPool
|
||||||
_LOGGER.exception("BusHandler:Exception doing job")
|
_LOGGER.exception("BusHandler:Exception doing job")
|
||||||
|
|
||||||
def busy_callback(current_jobs, pending_jobs_count):
|
def busy_callback(worker_count, current_jobs, pending_jobs_count):
|
||||||
""" Callback to be called when the pool queue gets too big. """
|
""" Callback to be called when the pool queue gets too big. """
|
||||||
|
|
||||||
_LOGGER.error(
|
_LOGGER.warning(
|
||||||
"WorkerPool:All %d threads are busy and %d jobs pending",
|
"WorkerPool:All %d threads are busy and %d jobs pending",
|
||||||
thread_count, pending_jobs_count)
|
worker_count, pending_jobs_count)
|
||||||
|
|
||||||
for start, job in current_jobs:
|
for start, job in current_jobs:
|
||||||
_LOGGER.error("WorkerPool:Current job from %s: %s",
|
_LOGGER.warning("WorkerPool:Current job from %s: %s",
|
||||||
util.datetime_to_str(start), job)
|
util.datetime_to_str(start), job)
|
||||||
|
|
||||||
return util.ThreadPool(thread_count, job_handler, busy_callback)
|
return util.ThreadPool(job_handler, MIN_WORKER_THREAD, busy_callback)
|
||||||
|
|
||||||
|
|
||||||
class EventOrigin(enum.Enum):
|
class EventOrigin(enum.Enum):
|
||||||
|
@ -374,9 +353,10 @@ class EventBus(object):
|
||||||
if not listeners:
|
if not listeners:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
job_priority = JobPriority.from_event_type(event_type)
|
||||||
|
|
||||||
for func in listeners:
|
for func in listeners:
|
||||||
self._pool.add_job(JobPriority.from_event_type(event_type),
|
self._pool.add_job(job_priority, (func, event))
|
||||||
(func, event))
|
|
||||||
|
|
||||||
def listen(self, event_type, listener):
|
def listen(self, event_type, listener):
|
||||||
""" Listen for all events or events of a specific type.
|
""" Listen for all events or events of a specific type.
|
||||||
|
@ -390,6 +370,31 @@ class EventBus(object):
|
||||||
else:
|
else:
|
||||||
self._listeners[event_type] = [listener]
|
self._listeners[event_type] = [listener]
|
||||||
|
|
||||||
|
def listen_once(self, event_type, listener):
|
||||||
|
""" Listen once for event of a specific type.
|
||||||
|
|
||||||
|
To listen to all events specify the constant ``MATCH_ALL``
|
||||||
|
as event_type.
|
||||||
|
|
||||||
|
Note: at the moment it is impossible to remove a one time listener.
|
||||||
|
"""
|
||||||
|
@ft.wraps(listener)
|
||||||
|
def onetime_listener(event):
|
||||||
|
""" Removes listener from eventbus and then fires listener. """
|
||||||
|
if not hasattr(onetime_listener, 'run'):
|
||||||
|
# Set variable so that we will never run twice.
|
||||||
|
# Because the event bus might have to wait till a thread comes
|
||||||
|
# available to execute this listener it might occur that the
|
||||||
|
# listener gets lined up twice to be executed.
|
||||||
|
# This will make sure the second time it does nothing.
|
||||||
|
onetime_listener.run = True
|
||||||
|
|
||||||
|
self.remove_listener(event_type, onetime_listener)
|
||||||
|
|
||||||
|
listener(event)
|
||||||
|
|
||||||
|
self.listen(event_type, onetime_listener)
|
||||||
|
|
||||||
def remove_listener(self, event_type, listener):
|
def remove_listener(self, event_type, listener):
|
||||||
""" Removes a listener of a specific event_type. """
|
""" Removes a listener of a specific event_type. """
|
||||||
with self._lock:
|
with self._lock:
|
||||||
|
@ -420,17 +425,13 @@ class State(object):
|
||||||
self.entity_id = entity_id
|
self.entity_id = entity_id
|
||||||
self.state = state
|
self.state = state
|
||||||
self.attributes = attributes or {}
|
self.attributes = attributes or {}
|
||||||
last_changed = last_changed or dt.datetime.now()
|
|
||||||
|
|
||||||
# Strip microsecond from last_changed else we cannot guarantee
|
# Strip microsecond from last_changed else we cannot guarantee
|
||||||
# state == State.from_dict(state.as_dict())
|
# state == State.from_dict(state.as_dict())
|
||||||
# This behavior occurs because to_dict uses datetime_to_str
|
# This behavior occurs because to_dict uses datetime_to_str
|
||||||
# which strips microseconds
|
# which does not preserve microseconds
|
||||||
if last_changed.microsecond:
|
self.last_changed = util.strip_microseconds(
|
||||||
self.last_changed = last_changed - dt.timedelta(
|
last_changed or dt.datetime.now())
|
||||||
microseconds=last_changed.microsecond)
|
|
||||||
else:
|
|
||||||
self.last_changed = last_changed
|
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
""" Creates a copy of itself. """
|
""" Creates a copy of itself. """
|
||||||
|
@ -483,14 +484,20 @@ class StateMachine(object):
|
||||||
""" Helper class that tracks the state of different entities. """
|
""" Helper class that tracks the state of different entities. """
|
||||||
|
|
||||||
def __init__(self, bus):
|
def __init__(self, bus):
|
||||||
self._states = {}
|
self._states = CaseInsensitiveDict()
|
||||||
self._bus = bus
|
self._bus = bus
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
@property
|
def entity_ids(self, domain_filter=None):
|
||||||
def entity_ids(self):
|
|
||||||
""" List of entity ids that are being tracked. """
|
""" List of entity ids that are being tracked. """
|
||||||
return list(self._states.keys())
|
if domain_filter is not None:
|
||||||
|
domain_filter = domain_filter.lower()
|
||||||
|
|
||||||
|
return [state.entity_id for key, state
|
||||||
|
in self._states.lower_items()
|
||||||
|
if util.split_entity_id(key)[0] == domain_filter]
|
||||||
|
else:
|
||||||
|
return list(self._states.keys())
|
||||||
|
|
||||||
def all(self):
|
def all(self):
|
||||||
""" Returns a list of all states. """
|
""" Returns a list of all states. """
|
||||||
|
@ -503,15 +510,28 @@ class StateMachine(object):
|
||||||
# Make a copy so people won't mutate the state
|
# Make a copy so people won't mutate the state
|
||||||
return state.copy() if state else None
|
return state.copy() if state else None
|
||||||
|
|
||||||
|
def get_since(self, point_in_time):
|
||||||
|
"""
|
||||||
|
Returns all states that have been changed since point_in_time.
|
||||||
|
|
||||||
|
Note: States keep track of last_changed -without- microseconds.
|
||||||
|
Therefore your point_in_time will also be stripped of microseconds.
|
||||||
|
"""
|
||||||
|
point_in_time = util.strip_microseconds(point_in_time)
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
return [state for state in self._states.values()
|
||||||
|
if state.last_changed >= point_in_time]
|
||||||
|
|
||||||
def is_state(self, entity_id, state):
|
def is_state(self, entity_id, state):
|
||||||
""" Returns True if entity exists and is specified state. """
|
""" Returns True if entity exists and is specified state. """
|
||||||
return (entity_id in self._states and
|
return (entity_id in self._states and
|
||||||
self._states[entity_id].state == state)
|
self._states[entity_id].state == state)
|
||||||
|
|
||||||
def remove(self, entity_id):
|
def remove(self, entity_id):
|
||||||
""" Removes a entity from the state machine.
|
""" Removes an entity from the state machine.
|
||||||
|
|
||||||
Returns boolean to indicate if a entity was removed. """
|
Returns boolean to indicate if an entity was removed. """
|
||||||
with self._lock:
|
with self._lock:
|
||||||
return self._states.pop(entity_id, None) is not None
|
return self._states.pop(entity_id, None) is not None
|
||||||
|
|
||||||
|
@ -540,6 +560,40 @@ class StateMachine(object):
|
||||||
|
|
||||||
self._bus.fire(EVENT_STATE_CHANGED, event_data)
|
self._bus.fire(EVENT_STATE_CHANGED, event_data)
|
||||||
|
|
||||||
|
def track_change(self, entity_ids, action, from_state=None, to_state=None):
|
||||||
|
"""
|
||||||
|
Track specific state changes.
|
||||||
|
entity_ids, from_state and to_state can be string or list.
|
||||||
|
Use list to match multiple.
|
||||||
|
|
||||||
|
Returns the listener that listens on the bus for EVENT_STATE_CHANGED.
|
||||||
|
Pass the return value into hass.bus.remove_listener to remove it.
|
||||||
|
"""
|
||||||
|
from_state = _process_match_param(from_state)
|
||||||
|
to_state = _process_match_param(to_state)
|
||||||
|
|
||||||
|
# Ensure it is a lowercase list with entity ids we want to match on
|
||||||
|
if isinstance(entity_ids, str):
|
||||||
|
entity_ids = [entity_ids.lower()]
|
||||||
|
else:
|
||||||
|
entity_ids = [entity_id.lower() for entity_id in entity_ids]
|
||||||
|
|
||||||
|
@ft.wraps(action)
|
||||||
|
def state_listener(event):
|
||||||
|
""" The listener that listens for specific state changes. """
|
||||||
|
if event.data['entity_id'].lower() in entity_ids and \
|
||||||
|
'old_state' in event.data and \
|
||||||
|
_matcher(event.data['old_state'].state, from_state) and \
|
||||||
|
_matcher(event.data['new_state'].state, to_state):
|
||||||
|
|
||||||
|
action(event.data['entity_id'],
|
||||||
|
event.data['old_state'],
|
||||||
|
event.data['new_state'])
|
||||||
|
|
||||||
|
self._bus.listen(EVENT_STATE_CHANGED, state_listener)
|
||||||
|
|
||||||
|
return state_listener
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
class ServiceCall(object):
|
class ServiceCall(object):
|
||||||
|
@ -567,6 +621,8 @@ class ServiceRegistry(object):
|
||||||
self._services = {}
|
self._services = {}
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
self._pool = pool or create_worker_pool()
|
self._pool = pool or create_worker_pool()
|
||||||
|
self._bus = bus
|
||||||
|
self._cur_id = 0
|
||||||
bus.listen(EVENT_CALL_SERVICE, self._event_to_service_call)
|
bus.listen(EVENT_CALL_SERVICE, self._event_to_service_call)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -588,6 +644,57 @@ class ServiceRegistry(object):
|
||||||
else:
|
else:
|
||||||
self._services[domain] = {service: service_func}
|
self._services[domain] = {service: service_func}
|
||||||
|
|
||||||
|
def call(self, domain, service, service_data=None, blocking=False):
|
||||||
|
"""
|
||||||
|
Calls specified service.
|
||||||
|
Specify blocking=True to wait till service is executed.
|
||||||
|
Waits a maximum of SERVICE_CALL_LIMIT.
|
||||||
|
|
||||||
|
If blocking = True, will return boolean if service executed
|
||||||
|
succesfully within SERVICE_CALL_LIMIT.
|
||||||
|
|
||||||
|
This method will fire an event to call the service.
|
||||||
|
This event will be picked up by this ServiceRegistry and any
|
||||||
|
other ServiceRegistry that is listening on the EventBus.
|
||||||
|
|
||||||
|
Because the service is sent as an event you are not allowed to use
|
||||||
|
the keys ATTR_DOMAIN and ATTR_SERVICE in your service_data.
|
||||||
|
"""
|
||||||
|
call_id = self._generate_unique_id()
|
||||||
|
event_data = service_data or {}
|
||||||
|
event_data[ATTR_DOMAIN] = domain
|
||||||
|
event_data[ATTR_SERVICE] = service
|
||||||
|
event_data[ATTR_SERVICE_CALL_ID] = call_id
|
||||||
|
|
||||||
|
if blocking:
|
||||||
|
executed_event = threading.Event()
|
||||||
|
|
||||||
|
def service_executed(call):
|
||||||
|
"""
|
||||||
|
Called when a service is executed.
|
||||||
|
Will set the event if matches our service call.
|
||||||
|
"""
|
||||||
|
if call.data[ATTR_SERVICE_CALL_ID] == call_id:
|
||||||
|
executed_event.set()
|
||||||
|
|
||||||
|
self._bus.remove_listener(
|
||||||
|
EVENT_SERVICE_EXECUTED, service_executed)
|
||||||
|
|
||||||
|
self._bus.listen(EVENT_SERVICE_EXECUTED, service_executed)
|
||||||
|
|
||||||
|
self._bus.fire(EVENT_CALL_SERVICE, event_data)
|
||||||
|
|
||||||
|
if blocking:
|
||||||
|
# wait will return False if event not set after our limit has
|
||||||
|
# passed. If not set, clean up the listener
|
||||||
|
if not executed_event.wait(SERVICE_CALL_LIMIT):
|
||||||
|
self._bus.remove_listener(
|
||||||
|
EVENT_SERVICE_EXECUTED, service_executed)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def _event_to_service_call(self, event):
|
def _event_to_service_call(self, event):
|
||||||
""" Calls a service from an event. """
|
""" Calls a service from an event. """
|
||||||
service_data = dict(event.data)
|
service_data = dict(event.data)
|
||||||
|
@ -598,9 +705,27 @@ class ServiceRegistry(object):
|
||||||
if domain in self._services and service in self._services[domain]:
|
if domain in self._services and service in self._services[domain]:
|
||||||
service_call = ServiceCall(domain, service, service_data)
|
service_call = ServiceCall(domain, service, service_data)
|
||||||
|
|
||||||
|
# Add a job to the pool that calls _execute_service
|
||||||
self._pool.add_job(JobPriority.EVENT_SERVICE,
|
self._pool.add_job(JobPriority.EVENT_SERVICE,
|
||||||
(self._services[domain][service],
|
(self._execute_service,
|
||||||
service_call))
|
(self._services[domain][service],
|
||||||
|
service_call)))
|
||||||
|
|
||||||
|
def _execute_service(self, service_and_call):
|
||||||
|
""" Executes a service and fires a SERVICE_EXECUTED event. """
|
||||||
|
service, call = service_and_call
|
||||||
|
|
||||||
|
service(call)
|
||||||
|
|
||||||
|
self._bus.fire(
|
||||||
|
EVENT_SERVICE_EXECUTED, {
|
||||||
|
ATTR_SERVICE_CALL_ID: call.data[ATTR_SERVICE_CALL_ID]
|
||||||
|
})
|
||||||
|
|
||||||
|
def _generate_unique_id(self):
|
||||||
|
""" Generates a unique service call id. """
|
||||||
|
self._cur_id += 1
|
||||||
|
return "{}-{}".format(id(self), self._cur_id)
|
||||||
|
|
||||||
|
|
||||||
class Timer(threading.Thread):
|
class Timer(threading.Thread):
|
||||||
|
@ -610,7 +735,7 @@ class Timer(threading.Thread):
|
||||||
threading.Thread.__init__(self)
|
threading.Thread.__init__(self)
|
||||||
|
|
||||||
self.daemon = True
|
self.daemon = True
|
||||||
self._bus = hass.bus
|
self.hass = hass
|
||||||
self.interval = interval or TIMER_INTERVAL
|
self.interval = interval or TIMER_INTERVAL
|
||||||
self._stop = threading.Event()
|
self._stop = threading.Event()
|
||||||
|
|
||||||
|
@ -619,15 +744,15 @@ class Timer(threading.Thread):
|
||||||
# every minute.
|
# every minute.
|
||||||
assert 60 % self.interval == 0, "60 % TIMER_INTERVAL should be 0!"
|
assert 60 % self.interval == 0, "60 % TIMER_INTERVAL should be 0!"
|
||||||
|
|
||||||
hass.listen_once_event(EVENT_HOMEASSISTANT_START,
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_START,
|
||||||
lambda event: self.start())
|
lambda event: self.start())
|
||||||
|
|
||||||
hass.listen_once_event(EVENT_HOMEASSISTANT_STOP,
|
|
||||||
lambda event: self._stop.set())
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
""" Start the timer. """
|
""" Start the timer. """
|
||||||
|
|
||||||
|
self.hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||||
|
lambda event: self._stop.set())
|
||||||
|
|
||||||
_LOGGER.info("Timer:starting")
|
_LOGGER.info("Timer:starting")
|
||||||
|
|
||||||
last_fired_on_second = -1
|
last_fired_on_second = -1
|
||||||
|
@ -658,7 +783,7 @@ class Timer(threading.Thread):
|
||||||
|
|
||||||
last_fired_on_second = now.second
|
last_fired_on_second = now.second
|
||||||
|
|
||||||
self._bus.fire(EVENT_TIME_CHANGED, {ATTR_NOW: now})
|
self.hass.bus.fire(EVENT_TIME_CHANGED, {ATTR_NOW: now})
|
||||||
|
|
||||||
|
|
||||||
class HomeAssistantError(Exception):
|
class HomeAssistantError(Exception):
|
||||||
|
|
|
@ -13,12 +13,10 @@ import os
|
||||||
import configparser
|
import configparser
|
||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from itertools import chain
|
|
||||||
|
|
||||||
import homeassistant
|
import homeassistant
|
||||||
import homeassistant.loader as loader
|
import homeassistant.loader as loader
|
||||||
import homeassistant.components as core_components
|
import homeassistant.components as core_components
|
||||||
import homeassistant.components.group as group
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-branches, too-many-statements
|
# pylint: disable=too-many-branches, too-many-statements
|
||||||
|
@ -33,123 +31,49 @@ def from_config_dict(config, hass=None):
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
loader.prepare(hass)
|
||||||
|
|
||||||
# Make a copy because we are mutating it.
|
# Make a copy because we are mutating it.
|
||||||
# Convert it to defaultdict so components can always have config dict
|
# Convert it to defaultdict so components can always have config dict
|
||||||
config = defaultdict(dict, config)
|
config = defaultdict(dict, config)
|
||||||
|
|
||||||
# List of loaded components
|
# Filter out the repeating and common config section [homeassistant]
|
||||||
components = {}
|
components = (key for key in config.keys()
|
||||||
|
if ' ' not in key and key != homeassistant.DOMAIN)
|
||||||
|
|
||||||
# List of components to validate
|
if not core_components.setup(hass, config):
|
||||||
to_validate = []
|
|
||||||
|
|
||||||
# List of validated components
|
|
||||||
validated = []
|
|
||||||
|
|
||||||
# List of components we are going to load
|
|
||||||
to_load = [key for key in config.keys() if key != homeassistant.DOMAIN]
|
|
||||||
|
|
||||||
loader.prepare(hass)
|
|
||||||
|
|
||||||
# Load required components
|
|
||||||
while to_load:
|
|
||||||
domain = to_load.pop()
|
|
||||||
|
|
||||||
component = loader.get_component(domain)
|
|
||||||
|
|
||||||
# if None it does not exist, error already thrown by get_component
|
|
||||||
if component is not None:
|
|
||||||
components[domain] = component
|
|
||||||
|
|
||||||
# Special treatment for GROUP, we want to load it as late as
|
|
||||||
# possible. We do this by loading it if all other to be loaded
|
|
||||||
# modules depend on it.
|
|
||||||
if component.DOMAIN == group.DOMAIN:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Components with no dependencies are valid
|
|
||||||
elif not component.DEPENDENCIES:
|
|
||||||
validated.append(domain)
|
|
||||||
|
|
||||||
# If dependencies we'll validate it later
|
|
||||||
else:
|
|
||||||
to_validate.append(domain)
|
|
||||||
|
|
||||||
# Make sure to load all dependencies that are not being loaded
|
|
||||||
for dependency in component.DEPENDENCIES:
|
|
||||||
if dependency not in chain(components.keys(), to_load):
|
|
||||||
to_load.append(dependency)
|
|
||||||
|
|
||||||
# Validate dependencies
|
|
||||||
group_added = False
|
|
||||||
|
|
||||||
while to_validate:
|
|
||||||
newly_validated = []
|
|
||||||
|
|
||||||
for domain in to_validate:
|
|
||||||
if all(domain in validated for domain
|
|
||||||
in components[domain].DEPENDENCIES):
|
|
||||||
|
|
||||||
newly_validated.append(domain)
|
|
||||||
|
|
||||||
# We validated new domains this iteration, add them to validated
|
|
||||||
if newly_validated:
|
|
||||||
|
|
||||||
# Add newly validated domains to validated
|
|
||||||
validated.extend(newly_validated)
|
|
||||||
|
|
||||||
# remove domains from to_validate
|
|
||||||
for domain in newly_validated:
|
|
||||||
to_validate.remove(domain)
|
|
||||||
|
|
||||||
newly_validated.clear()
|
|
||||||
|
|
||||||
# Nothing validated this iteration. Add group dependency and try again.
|
|
||||||
elif not group_added:
|
|
||||||
group_added = True
|
|
||||||
validated.append(group.DOMAIN)
|
|
||||||
|
|
||||||
# Group has already been added and we still can't validate all.
|
|
||||||
# Report missing deps as error and skip loading of these domains
|
|
||||||
else:
|
|
||||||
for domain in to_validate:
|
|
||||||
missing_deps = [dep for dep in components[domain].DEPENDENCIES
|
|
||||||
if dep not in validated]
|
|
||||||
|
|
||||||
logger.error(
|
|
||||||
"Could not validate all dependencies for %s: %s",
|
|
||||||
domain, ", ".join(missing_deps))
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
# Make sure we load groups if not in list yet.
|
|
||||||
if not group_added:
|
|
||||||
validated.append(group.DOMAIN)
|
|
||||||
|
|
||||||
if group.DOMAIN not in components:
|
|
||||||
components[group.DOMAIN] = \
|
|
||||||
loader.get_component(group.DOMAIN)
|
|
||||||
|
|
||||||
# Setup the components
|
|
||||||
if core_components.setup(hass, config):
|
|
||||||
logger.info("Home Assistant core initialized")
|
|
||||||
|
|
||||||
for domain in validated:
|
|
||||||
component = components[domain]
|
|
||||||
|
|
||||||
try:
|
|
||||||
if component.setup(hass, config):
|
|
||||||
logger.info("component %s initialized", domain)
|
|
||||||
else:
|
|
||||||
logger.error("component %s failed to initialize", domain)
|
|
||||||
|
|
||||||
except Exception: # pylint: disable=broad-except
|
|
||||||
logger.exception("Error during setup of component %s", domain)
|
|
||||||
|
|
||||||
else:
|
|
||||||
logger.error(("Home Assistant core failed to initialize. "
|
logger.error(("Home Assistant core failed to initialize. "
|
||||||
"Further initialization aborted."))
|
"Further initialization aborted."))
|
||||||
|
|
||||||
|
return hass
|
||||||
|
|
||||||
|
logger.info("Home Assistant core initialized")
|
||||||
|
|
||||||
|
# Setup the components
|
||||||
|
|
||||||
|
# We assume that all components that load before the group component loads
|
||||||
|
# are components that poll devices. As their tasks are IO based, we will
|
||||||
|
# add an extra worker for each of them.
|
||||||
|
add_worker = True
|
||||||
|
|
||||||
|
for domain in loader.load_order_components(components):
|
||||||
|
component = loader.get_component(domain)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if component.setup(hass, config):
|
||||||
|
logger.info("component %s initialized", domain)
|
||||||
|
|
||||||
|
add_worker = add_worker and domain != "group"
|
||||||
|
|
||||||
|
if add_worker:
|
||||||
|
hass.pool.add_worker()
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.error("component %s failed to initialize", domain)
|
||||||
|
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
logger.exception("Error during setup of component %s", domain)
|
||||||
|
|
||||||
return hass
|
return hass
|
||||||
|
|
||||||
|
|
||||||
|
@ -181,7 +105,7 @@ def from_config_file(config_path, hass=None, enable_logging=True):
|
||||||
err_handler = logging.FileHandler(
|
err_handler = logging.FileHandler(
|
||||||
err_log_path, mode='w', delay=True)
|
err_log_path, mode='w', delay=True)
|
||||||
|
|
||||||
err_handler.setLevel(logging.ERROR)
|
err_handler.setLevel(logging.WARNING)
|
||||||
err_handler.setFormatter(
|
err_handler.setFormatter(
|
||||||
logging.Formatter('%(asctime)s %(name)s: %(message)s',
|
logging.Formatter('%(asctime)s %(name)s: %(message)s',
|
||||||
datefmt='%H:%M %d-%m-%y'))
|
datefmt='%H:%M %d-%m-%y'))
|
||||||
|
|
|
@ -19,36 +19,10 @@ import logging
|
||||||
|
|
||||||
import homeassistant as ha
|
import homeassistant as ha
|
||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
|
from homeassistant.helpers import extract_entity_ids
|
||||||
from homeassistant.loader import get_component
|
from homeassistant.loader import get_component
|
||||||
|
from homeassistant.const import (
|
||||||
# Contains one string or a list of strings, each being an entity id
|
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
|
||||||
ATTR_ENTITY_ID = 'entity_id'
|
|
||||||
|
|
||||||
# String with a friendly name for the entity
|
|
||||||
ATTR_FRIENDLY_NAME = "friendly_name"
|
|
||||||
|
|
||||||
# A picture to represent entity
|
|
||||||
ATTR_ENTITY_PICTURE = "entity_picture"
|
|
||||||
|
|
||||||
# The unit of measurement if applicable
|
|
||||||
ATTR_UNIT_OF_MEASUREMENT = "unit_of_measurement"
|
|
||||||
|
|
||||||
STATE_ON = 'on'
|
|
||||||
STATE_OFF = 'off'
|
|
||||||
STATE_HOME = 'home'
|
|
||||||
STATE_NOT_HOME = 'not_home'
|
|
||||||
|
|
||||||
SERVICE_TURN_ON = 'turn_on'
|
|
||||||
SERVICE_TURN_OFF = 'turn_off'
|
|
||||||
|
|
||||||
SERVICE_VOLUME_UP = "volume_up"
|
|
||||||
SERVICE_VOLUME_DOWN = "volume_down"
|
|
||||||
SERVICE_VOLUME_MUTE = "volume_mute"
|
|
||||||
SERVICE_MEDIA_PLAY_PAUSE = "media_play_pause"
|
|
||||||
SERVICE_MEDIA_PLAY = "media_play"
|
|
||||||
SERVICE_MEDIA_PAUSE = "media_pause"
|
|
||||||
SERVICE_MEDIA_NEXT_TRACK = "media_next_track"
|
|
||||||
SERVICE_MEDIA_PREV_TRACK = "media_prev_track"
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -61,7 +35,7 @@ def is_on(hass, entity_id=None):
|
||||||
|
|
||||||
entity_ids = group.expand_entity_ids(hass, [entity_id])
|
entity_ids = group.expand_entity_ids(hass, [entity_id])
|
||||||
else:
|
else:
|
||||||
entity_ids = hass.states.entity_ids
|
entity_ids = hass.states.entity_ids()
|
||||||
|
|
||||||
for entity_id in entity_ids:
|
for entity_id in entity_ids:
|
||||||
domain = util.split_entity_id(entity_id)[0]
|
domain = util.split_entity_id(entity_id)[0]
|
||||||
|
@ -85,7 +59,7 @@ def turn_on(hass, entity_id=None, **service_data):
|
||||||
if entity_id is not None:
|
if entity_id is not None:
|
||||||
service_data[ATTR_ENTITY_ID] = entity_id
|
service_data[ATTR_ENTITY_ID] = entity_id
|
||||||
|
|
||||||
hass.call_service(ha.DOMAIN, SERVICE_TURN_ON, service_data)
|
hass.services.call(ha.DOMAIN, SERVICE_TURN_ON, service_data)
|
||||||
|
|
||||||
|
|
||||||
def turn_off(hass, entity_id=None, **service_data):
|
def turn_off(hass, entity_id=None, **service_data):
|
||||||
|
@ -93,80 +67,7 @@ def turn_off(hass, entity_id=None, **service_data):
|
||||||
if entity_id is not None:
|
if entity_id is not None:
|
||||||
service_data[ATTR_ENTITY_ID] = entity_id
|
service_data[ATTR_ENTITY_ID] = entity_id
|
||||||
|
|
||||||
hass.call_service(ha.DOMAIN, SERVICE_TURN_OFF, service_data)
|
hass.services.call(ha.DOMAIN, SERVICE_TURN_OFF, service_data)
|
||||||
|
|
||||||
|
|
||||||
def extract_entity_ids(hass, service):
|
|
||||||
"""
|
|
||||||
Helper method to extract a list of entity ids from a service call.
|
|
||||||
Will convert group entity ids to the entity ids it represents.
|
|
||||||
"""
|
|
||||||
entity_ids = []
|
|
||||||
|
|
||||||
if service.data and ATTR_ENTITY_ID in service.data:
|
|
||||||
group = get_component('group')
|
|
||||||
|
|
||||||
# Entity ID attr can be a list or a string
|
|
||||||
service_ent_id = service.data[ATTR_ENTITY_ID]
|
|
||||||
if isinstance(service_ent_id, list):
|
|
||||||
ent_ids = service_ent_id
|
|
||||||
else:
|
|
||||||
ent_ids = [service_ent_id]
|
|
||||||
|
|
||||||
entity_ids.extend(
|
|
||||||
ent_id for ent_id
|
|
||||||
in group.expand_entity_ids(hass, ent_ids)
|
|
||||||
if ent_id not in entity_ids)
|
|
||||||
|
|
||||||
return entity_ids
|
|
||||||
|
|
||||||
|
|
||||||
class ToggleDevice(object):
|
|
||||||
""" ABC for devices that can be turned on and off. """
|
|
||||||
# pylint: disable=no-self-use
|
|
||||||
|
|
||||||
entity_id = None
|
|
||||||
|
|
||||||
def get_name(self):
|
|
||||||
""" Returns the name of the device if any. """
|
|
||||||
return None
|
|
||||||
|
|
||||||
def turn_on(self, **kwargs):
|
|
||||||
""" Turn the device on. """
|
|
||||||
pass
|
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
|
||||||
""" Turn the device off. """
|
|
||||||
pass
|
|
||||||
|
|
||||||
def is_on(self):
|
|
||||||
""" True if device is on. """
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_state_attributes(self):
|
|
||||||
""" Returns optional state attributes. """
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
""" Retrieve latest state from the real device. """
|
|
||||||
pass
|
|
||||||
|
|
||||||
def update_ha_state(self, hass, force_refresh=False):
|
|
||||||
"""
|
|
||||||
Updates Home Assistant with current state of device.
|
|
||||||
If force_refresh == True will update device before setting state.
|
|
||||||
"""
|
|
||||||
if self.entity_id is None:
|
|
||||||
raise ha.NoEntitySpecifiedError(
|
|
||||||
"No entity specified for device {}".format(self.get_name()))
|
|
||||||
|
|
||||||
if force_refresh:
|
|
||||||
self.update()
|
|
||||||
|
|
||||||
state = STATE_ON if self.is_on() else STATE_OFF
|
|
||||||
|
|
||||||
return hass.states.set(self.entity_id, state,
|
|
||||||
self.get_state_attributes())
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
|
@ -195,7 +96,7 @@ def setup(hass, config):
|
||||||
# ent_ids is a generator, convert it to a list.
|
# ent_ids is a generator, convert it to a list.
|
||||||
data[ATTR_ENTITY_ID] = list(ent_ids)
|
data[ATTR_ENTITY_ID] = list(ent_ids)
|
||||||
|
|
||||||
hass.call_service(domain, service.service, data)
|
hass.services.call(domain, service.service, data, True)
|
||||||
|
|
||||||
hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service)
|
hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service)
|
||||||
hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)
|
hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)
|
||||||
|
|
|
@ -6,9 +6,14 @@ Provides functionality to interact with Chromecasts.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import homeassistant as ha
|
|
||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
import homeassistant.components as components
|
from homeassistant.helpers import extract_entity_ids
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_VOLUME_UP,
|
||||||
|
SERVICE_VOLUME_DOWN, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY,
|
||||||
|
SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK,
|
||||||
|
CONF_HOSTS)
|
||||||
|
|
||||||
|
|
||||||
DOMAIN = 'chromecast'
|
DOMAIN = 'chromecast'
|
||||||
DEPENDENCIES = []
|
DEPENDENCIES = []
|
||||||
|
@ -38,7 +43,7 @@ def is_on(hass, entity_id=None):
|
||||||
""" Returns true if specified ChromeCast entity_id is on.
|
""" Returns true if specified ChromeCast entity_id is on.
|
||||||
Will check all chromecasts if no entity_id specified. """
|
Will check all chromecasts if no entity_id specified. """
|
||||||
|
|
||||||
entity_ids = [entity_id] if entity_id else hass.get_entity_ids(DOMAIN)
|
entity_ids = [entity_id] if entity_id else hass.states.entity_ids(DOMAIN)
|
||||||
|
|
||||||
return any(not hass.states.is_state(entity_id, STATE_NO_APP)
|
return any(not hass.states.is_state(entity_id, STATE_NO_APP)
|
||||||
for entity_id in entity_ids)
|
for entity_id in entity_ids)
|
||||||
|
@ -46,58 +51,58 @@ def is_on(hass, entity_id=None):
|
||||||
|
|
||||||
def turn_off(hass, entity_id=None):
|
def turn_off(hass, entity_id=None):
|
||||||
""" Will turn off specified Chromecast or all. """
|
""" Will turn off specified Chromecast or all. """
|
||||||
data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||||
|
|
||||||
hass.call_service(DOMAIN, components.SERVICE_TURN_OFF, data)
|
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
|
||||||
|
|
||||||
|
|
||||||
def volume_up(hass, entity_id=None):
|
def volume_up(hass, entity_id=None):
|
||||||
""" Send the chromecast the command for volume up. """
|
""" Send the chromecast the command for volume up. """
|
||||||
data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||||
|
|
||||||
hass.call_service(DOMAIN, components.SERVICE_VOLUME_UP, data)
|
hass.services.call(DOMAIN, SERVICE_VOLUME_UP, data)
|
||||||
|
|
||||||
|
|
||||||
def volume_down(hass, entity_id=None):
|
def volume_down(hass, entity_id=None):
|
||||||
""" Send the chromecast the command for volume down. """
|
""" Send the chromecast the command for volume down. """
|
||||||
data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||||
|
|
||||||
hass.call_service(DOMAIN, components.SERVICE_VOLUME_DOWN, data)
|
hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN, data)
|
||||||
|
|
||||||
|
|
||||||
def media_play_pause(hass, entity_id=None):
|
def media_play_pause(hass, entity_id=None):
|
||||||
""" Send the chromecast the command for play/pause. """
|
""" Send the chromecast the command for play/pause. """
|
||||||
data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||||
|
|
||||||
hass.call_service(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE, data)
|
hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, data)
|
||||||
|
|
||||||
|
|
||||||
def media_play(hass, entity_id=None):
|
def media_play(hass, entity_id=None):
|
||||||
""" Send the chromecast the command for play/pause. """
|
""" Send the chromecast the command for play/pause. """
|
||||||
data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||||
|
|
||||||
hass.call_service(DOMAIN, components.SERVICE_MEDIA_PLAY, data)
|
hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY, data)
|
||||||
|
|
||||||
|
|
||||||
def media_pause(hass, entity_id=None):
|
def media_pause(hass, entity_id=None):
|
||||||
""" Send the chromecast the command for play/pause. """
|
""" Send the chromecast the command for play/pause. """
|
||||||
data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||||
|
|
||||||
hass.call_service(DOMAIN, components.SERVICE_MEDIA_PAUSE, data)
|
hass.services.call(DOMAIN, SERVICE_MEDIA_PAUSE, data)
|
||||||
|
|
||||||
|
|
||||||
def media_next_track(hass, entity_id=None):
|
def media_next_track(hass, entity_id=None):
|
||||||
""" Send the chromecast the command for next track. """
|
""" Send the chromecast the command for next track. """
|
||||||
data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||||
|
|
||||||
hass.call_service(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK, data)
|
hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, data)
|
||||||
|
|
||||||
|
|
||||||
def media_prev_track(hass, entity_id=None):
|
def media_prev_track(hass, entity_id=None):
|
||||||
""" Send the chromecast the command for prev track. """
|
""" Send the chromecast the command for prev track. """
|
||||||
data = {components.ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||||
|
|
||||||
hass.call_service(DOMAIN, components.SERVICE_MEDIA_PREV_TRACK, data)
|
hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK, data)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-locals, too-many-branches
|
# pylint: disable=too-many-locals, too-many-branches
|
||||||
|
@ -114,8 +119,8 @@ def setup(hass, config):
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if ha.CONF_HOSTS in config[DOMAIN]:
|
if CONF_HOSTS in config[DOMAIN]:
|
||||||
hosts = config[DOMAIN][ha.CONF_HOSTS].split(",")
|
hosts = config[DOMAIN][CONF_HOSTS].split(",")
|
||||||
|
|
||||||
# If no hosts given, scan for chromecasts
|
# If no hosts given, scan for chromecasts
|
||||||
else:
|
else:
|
||||||
|
@ -131,7 +136,7 @@ def setup(hass, config):
|
||||||
entity_id = util.ensure_unique_string(
|
entity_id = util.ensure_unique_string(
|
||||||
ENTITY_ID_FORMAT.format(
|
ENTITY_ID_FORMAT.format(
|
||||||
util.slugify(cast.device.friendly_name)),
|
util.slugify(cast.device.friendly_name)),
|
||||||
list(casts.keys()))
|
casts.keys())
|
||||||
|
|
||||||
casts[entity_id] = cast
|
casts[entity_id] = cast
|
||||||
|
|
||||||
|
@ -148,7 +153,7 @@ def setup(hass, config):
|
||||||
|
|
||||||
status = chromecast.app
|
status = chromecast.app
|
||||||
|
|
||||||
state_attr = {components.ATTR_FRIENDLY_NAME:
|
state_attr = {ATTR_FRIENDLY_NAME:
|
||||||
chromecast.device.friendly_name}
|
chromecast.device.friendly_name}
|
||||||
|
|
||||||
if status and status.app_id != pychromecast.APP_ID['HOME']:
|
if status and status.app_id != pychromecast.APP_ID['HOME']:
|
||||||
|
@ -196,7 +201,7 @@ def setup(hass, config):
|
||||||
|
|
||||||
def _service_to_entities(service):
|
def _service_to_entities(service):
|
||||||
""" Helper method to get entities from service. """
|
""" Helper method to get entities from service. """
|
||||||
entity_ids = components.extract_entity_ids(hass, service)
|
entity_ids = extract_entity_ids(hass, service)
|
||||||
|
|
||||||
if entity_ids:
|
if entity_ids:
|
||||||
for entity_id in entity_ids:
|
for entity_id in entity_ids:
|
||||||
|
@ -274,25 +279,25 @@ def setup(hass, config):
|
||||||
|
|
||||||
hass.track_time_change(update_chromecast_states)
|
hass.track_time_change(update_chromecast_states)
|
||||||
|
|
||||||
hass.services.register(DOMAIN, components.SERVICE_TURN_OFF,
|
hass.services.register(DOMAIN, SERVICE_TURN_OFF,
|
||||||
turn_off_service)
|
turn_off_service)
|
||||||
|
|
||||||
hass.services.register(DOMAIN, components.SERVICE_VOLUME_UP,
|
hass.services.register(DOMAIN, SERVICE_VOLUME_UP,
|
||||||
volume_up_service)
|
volume_up_service)
|
||||||
|
|
||||||
hass.services.register(DOMAIN, components.SERVICE_VOLUME_DOWN,
|
hass.services.register(DOMAIN, SERVICE_VOLUME_DOWN,
|
||||||
volume_down_service)
|
volume_down_service)
|
||||||
|
|
||||||
hass.services.register(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE,
|
hass.services.register(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE,
|
||||||
media_play_pause_service)
|
media_play_pause_service)
|
||||||
|
|
||||||
hass.services.register(DOMAIN, components.SERVICE_MEDIA_PLAY,
|
hass.services.register(DOMAIN, SERVICE_MEDIA_PLAY,
|
||||||
media_play_service)
|
media_play_service)
|
||||||
|
|
||||||
hass.services.register(DOMAIN, components.SERVICE_MEDIA_PAUSE,
|
hass.services.register(DOMAIN, SERVICE_MEDIA_PAUSE,
|
||||||
media_pause_service)
|
media_pause_service)
|
||||||
|
|
||||||
hass.services.register(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK,
|
hass.services.register(DOMAIN, SERVICE_MEDIA_NEXT_TRACK,
|
||||||
media_next_track_service)
|
media_next_track_service)
|
||||||
|
|
||||||
hass.services.register(DOMAIN, "start_fireplace",
|
hass.services.register(DOMAIN, "start_fireplace",
|
||||||
|
|
|
@ -8,11 +8,12 @@ import random
|
||||||
|
|
||||||
import homeassistant as ha
|
import homeassistant as ha
|
||||||
import homeassistant.loader as loader
|
import homeassistant.loader as loader
|
||||||
from homeassistant.components import (SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
from homeassistant.helpers import extract_entity_ids
|
||||||
STATE_ON, STATE_OFF, ATTR_ENTITY_PICTURE,
|
from homeassistant.const import (
|
||||||
extract_entity_ids)
|
SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON, STATE_OFF,
|
||||||
from homeassistant.components.light import (ATTR_XY_COLOR, ATTR_BRIGHTNESS,
|
ATTR_ENTITY_PICTURE, ATTR_ENTITY_ID, CONF_LATITUDE, CONF_LONGITUDE)
|
||||||
GROUP_NAME_ALL_LIGHTS)
|
from homeassistant.components.light import (
|
||||||
|
ATTR_XY_COLOR, ATTR_BRIGHTNESS, GROUP_NAME_ALL_LIGHTS)
|
||||||
from homeassistant.util import split_entity_id
|
from homeassistant.util import split_entity_id
|
||||||
|
|
||||||
DOMAIN = "demo"
|
DOMAIN = "demo"
|
||||||
|
@ -24,6 +25,9 @@ def setup(hass, config):
|
||||||
""" Setup a demo environment. """
|
""" Setup a demo environment. """
|
||||||
group = loader.get_component('group')
|
group = loader.get_component('group')
|
||||||
|
|
||||||
|
config.setdefault(ha.DOMAIN, {})
|
||||||
|
config.setdefault(DOMAIN, {})
|
||||||
|
|
||||||
if config[DOMAIN].get('hide_demo_state') != '1':
|
if config[DOMAIN].get('hide_demo_state') != '1':
|
||||||
hass.states.set('a.Demo_Mode', 'Enabled')
|
hass.states.set('a.Demo_Mode', 'Enabled')
|
||||||
|
|
||||||
|
@ -35,7 +39,12 @@ def setup(hass, config):
|
||||||
|
|
||||||
def mock_turn_on(service):
|
def mock_turn_on(service):
|
||||||
""" Will fake the component has been turned on. """
|
""" Will fake the component has been turned on. """
|
||||||
for entity_id in extract_entity_ids(hass, service):
|
if service.data and ATTR_ENTITY_ID in service.data:
|
||||||
|
entity_ids = extract_entity_ids(hass, service)
|
||||||
|
else:
|
||||||
|
entity_ids = hass.states.entity_ids(service.domain)
|
||||||
|
|
||||||
|
for entity_id in entity_ids:
|
||||||
domain, _ = split_entity_id(entity_id)
|
domain, _ = split_entity_id(entity_id)
|
||||||
|
|
||||||
if domain == "light":
|
if domain == "light":
|
||||||
|
@ -48,15 +57,20 @@ def setup(hass, config):
|
||||||
|
|
||||||
def mock_turn_off(service):
|
def mock_turn_off(service):
|
||||||
""" Will fake the component has been turned off. """
|
""" Will fake the component has been turned off. """
|
||||||
for entity_id in extract_entity_ids(hass, service):
|
if service.data and ATTR_ENTITY_ID in service.data:
|
||||||
|
entity_ids = extract_entity_ids(hass, service)
|
||||||
|
else:
|
||||||
|
entity_ids = hass.states.entity_ids(service.domain)
|
||||||
|
|
||||||
|
for entity_id in entity_ids:
|
||||||
hass.states.set(entity_id, STATE_OFF)
|
hass.states.set(entity_id, STATE_OFF)
|
||||||
|
|
||||||
# Setup sun
|
# Setup sun
|
||||||
if ha.CONF_LATITUDE not in config[ha.DOMAIN]:
|
if CONF_LATITUDE not in config[ha.DOMAIN]:
|
||||||
config[ha.DOMAIN][ha.CONF_LATITUDE] = '32.87336'
|
config[ha.DOMAIN][CONF_LATITUDE] = '32.87336'
|
||||||
|
|
||||||
if ha.CONF_LONGITUDE not in config[ha.DOMAIN]:
|
if CONF_LONGITUDE not in config[ha.DOMAIN]:
|
||||||
config[ha.DOMAIN][ha.CONF_LONGITUDE] = '-117.22743'
|
config[ha.DOMAIN][CONF_LONGITUDE] = '-117.22743'
|
||||||
|
|
||||||
loader.get_component('sun').setup(hass, config)
|
loader.get_component('sun').setup(hass, config)
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ the state of the sun and devices.
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import homeassistant.components as components
|
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
|
||||||
from . import light, sun, device_tracker, group
|
from . import light, sun, device_tracker, group
|
||||||
|
|
||||||
DOMAIN = "device_sun_light_trigger"
|
DOMAIN = "device_sun_light_trigger"
|
||||||
|
@ -21,6 +21,7 @@ LIGHT_PROFILE = 'relax'
|
||||||
|
|
||||||
CONF_LIGHT_PROFILE = 'light_profile'
|
CONF_LIGHT_PROFILE = 'light_profile'
|
||||||
CONF_LIGHT_GROUP = 'light_group'
|
CONF_LIGHT_GROUP = 'light_group'
|
||||||
|
CONF_DEVICE_GROUP = 'device_group'
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-branches
|
# pylint: disable=too-many-branches
|
||||||
|
@ -30,13 +31,17 @@ def setup(hass, config):
|
||||||
disable_turn_off = 'disable_turn_off' in config[DOMAIN]
|
disable_turn_off = 'disable_turn_off' in config[DOMAIN]
|
||||||
|
|
||||||
light_group = config[DOMAIN].get(CONF_LIGHT_GROUP,
|
light_group = config[DOMAIN].get(CONF_LIGHT_GROUP,
|
||||||
light.GROUP_NAME_ALL_LIGHTS)
|
light.ENTITY_ID_ALL_LIGHTS)
|
||||||
|
|
||||||
light_profile = config[DOMAIN].get(CONF_LIGHT_PROFILE, LIGHT_PROFILE)
|
light_profile = config[DOMAIN].get(CONF_LIGHT_PROFILE, LIGHT_PROFILE)
|
||||||
|
|
||||||
|
device_group = config[DOMAIN].get(CONF_DEVICE_GROUP,
|
||||||
|
device_tracker.ENTITY_ID_ALL_DEVICES)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
device_entity_ids = hass.get_entity_ids(device_tracker.DOMAIN)
|
device_entity_ids = group.get_entity_ids(hass, device_group,
|
||||||
|
device_tracker.DOMAIN)
|
||||||
|
|
||||||
if not device_entity_ids:
|
if not device_entity_ids:
|
||||||
logger.error("No devices found to track")
|
logger.error("No devices found to track")
|
||||||
|
@ -92,8 +97,8 @@ def setup(hass, config):
|
||||||
|
|
||||||
# Track every time sun rises so we can schedule a time-based
|
# Track every time sun rises so we can schedule a time-based
|
||||||
# pre-sun set event
|
# pre-sun set event
|
||||||
hass.track_state_change(sun.ENTITY_ID, schedule_light_on_sun_rise,
|
hass.states.track_change(sun.ENTITY_ID, schedule_light_on_sun_rise,
|
||||||
sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON)
|
sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON)
|
||||||
|
|
||||||
# If the sun is already above horizon
|
# If the sun is already above horizon
|
||||||
# schedule the time-based pre-sun set event
|
# schedule the time-based pre-sun set event
|
||||||
|
@ -108,7 +113,7 @@ def setup(hass, config):
|
||||||
|
|
||||||
# Specific device came home ?
|
# Specific device came home ?
|
||||||
if entity != device_tracker.ENTITY_ID_ALL_DEVICES and \
|
if entity != device_tracker.ENTITY_ID_ALL_DEVICES and \
|
||||||
new_state.state == components.STATE_HOME:
|
new_state.state == STATE_HOME:
|
||||||
|
|
||||||
# These variables are needed for the elif check
|
# These variables are needed for the elif check
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
@ -142,8 +147,8 @@ def setup(hass, config):
|
||||||
break
|
break
|
||||||
|
|
||||||
# Did all devices leave the house?
|
# Did all devices leave the house?
|
||||||
elif (entity == device_tracker.ENTITY_ID_ALL_DEVICES and
|
elif (entity == device_group and
|
||||||
new_state.state == components.STATE_NOT_HOME and lights_are_on
|
new_state.state == STATE_NOT_HOME and lights_are_on
|
||||||
and not disable_turn_off):
|
and not disable_turn_off):
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
@ -152,12 +157,13 @@ def setup(hass, config):
|
||||||
light.turn_off(hass)
|
light.turn_off(hass)
|
||||||
|
|
||||||
# Track home coming of each device
|
# Track home coming of each device
|
||||||
hass.track_state_change(device_entity_ids, check_light_on_dev_state_change,
|
hass.states.track_change(
|
||||||
components.STATE_NOT_HOME, components.STATE_HOME)
|
device_entity_ids, check_light_on_dev_state_change,
|
||||||
|
STATE_NOT_HOME, STATE_HOME)
|
||||||
|
|
||||||
# Track when all devices are gone to shut down lights
|
# Track when all devices are gone to shut down lights
|
||||||
hass.track_state_change(device_tracker.ENTITY_ID_ALL_DEVICES,
|
hass.states.track_change(
|
||||||
check_light_on_dev_state_change,
|
device_group, check_light_on_dev_state_change,
|
||||||
components.STATE_HOME, components.STATE_NOT_HOME)
|
STATE_HOME, STATE_NOT_HOME)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"""
|
"""
|
||||||
homeassistant.components.tracker
|
homeassistant.components.tracker
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Provides functionality to keep track of devices.
|
Provides functionality to keep track of devices.
|
||||||
"""
|
"""
|
||||||
|
@ -10,11 +10,13 @@ import os
|
||||||
import csv
|
import csv
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import homeassistant as ha
|
|
||||||
from homeassistant.loader import get_component
|
from homeassistant.loader import get_component
|
||||||
|
from homeassistant.helpers import validate_config
|
||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
import homeassistant.components as components
|
|
||||||
|
|
||||||
|
from homeassistant.const import (
|
||||||
|
STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME,
|
||||||
|
CONF_PLATFORM, CONF_TYPE)
|
||||||
from homeassistant.components import group
|
from homeassistant.components import group
|
||||||
|
|
||||||
DOMAIN = "device_tracker"
|
DOMAIN = "device_tracker"
|
||||||
|
@ -30,7 +32,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||||
|
|
||||||
# After how much time do we consider a device not home if
|
# After how much time do we consider a device not home if
|
||||||
# it does not show up on scans
|
# it does not show up on scans
|
||||||
TIME_SPAN_FOR_ERROR_IN_SCANNING = timedelta(minutes=3)
|
TIME_DEVICE_NOT_FOUND = timedelta(minutes=3)
|
||||||
|
|
||||||
# Filename to save known devices to
|
# Filename to save known devices to
|
||||||
KNOWN_DEVICES_FILE = "known_devices.csv"
|
KNOWN_DEVICES_FILE = "known_devices.csv"
|
||||||
|
@ -43,16 +45,26 @@ def is_on(hass, entity_id=None):
|
||||||
""" Returns if any or specified device is home. """
|
""" Returns if any or specified device is home. """
|
||||||
entity = entity_id or ENTITY_ID_ALL_DEVICES
|
entity = entity_id or ENTITY_ID_ALL_DEVICES
|
||||||
|
|
||||||
return hass.states.is_state(entity, components.STATE_HOME)
|
return hass.states.is_state(entity, STATE_HOME)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
""" Sets up the device tracker. """
|
""" Sets up the device tracker. """
|
||||||
|
|
||||||
if not util.validate_config(config, {DOMAIN: [ha.CONF_TYPE]}, _LOGGER):
|
# CONF_TYPE is deprecated for CONF_PLATOFRM. We keep supporting it for now.
|
||||||
|
if not (validate_config(config, {DOMAIN: [CONF_PLATFORM]}, _LOGGER)
|
||||||
|
or validate_config(config, {DOMAIN: [CONF_TYPE]}, _LOGGER)):
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
tracker_type = config[DOMAIN][ha.CONF_TYPE]
|
tracker_type = config[DOMAIN].get(CONF_PLATFORM)
|
||||||
|
|
||||||
|
if tracker_type is None:
|
||||||
|
tracker_type = config[DOMAIN][CONF_TYPE]
|
||||||
|
|
||||||
|
_LOGGER.warning((
|
||||||
|
"Please update your config for %s to use 'platform' "
|
||||||
|
"instead of 'type'"), tracker_type)
|
||||||
|
|
||||||
tracker_implementation = get_component(
|
tracker_implementation = get_component(
|
||||||
'device_tracker.{}'.format(tracker_type))
|
'device_tracker.{}'.format(tracker_type))
|
||||||
|
@ -70,105 +82,109 @@ def setup(hass, config):
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
DeviceTracker(hass, device_scanner)
|
tracker = DeviceTracker(hass, device_scanner)
|
||||||
|
|
||||||
return True
|
# We only succeeded if we got to parse the known devices file
|
||||||
|
return not tracker.invalid_known_devices_file
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes
|
|
||||||
class DeviceTracker(object):
|
class DeviceTracker(object):
|
||||||
""" Class that tracks which devices are home and which are not. """
|
""" Class that tracks which devices are home and which are not. """
|
||||||
|
|
||||||
def __init__(self, hass, device_scanner):
|
def __init__(self, hass, device_scanner):
|
||||||
self.states = hass.states
|
self.hass = hass
|
||||||
|
|
||||||
self.device_scanner = device_scanner
|
self.device_scanner = device_scanner
|
||||||
|
|
||||||
self.error_scanning = TIME_SPAN_FOR_ERROR_IN_SCANNING
|
|
||||||
|
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
self.path_known_devices_file = hass.get_config_path(KNOWN_DEVICES_FILE)
|
|
||||||
|
|
||||||
# Dictionary to keep track of known devices and devices we track
|
# Dictionary to keep track of known devices and devices we track
|
||||||
self.known_devices = {}
|
self.tracked = {}
|
||||||
|
self.untracked_devices = set()
|
||||||
|
|
||||||
# Did we encounter an invalid known devices file
|
# Did we encounter an invalid known devices file
|
||||||
self.invalid_known_devices_file = False
|
self.invalid_known_devices_file = False
|
||||||
|
|
||||||
self._read_known_devices_file()
|
|
||||||
|
|
||||||
# Wrap it in a func instead of lambda so it can be identified in
|
# Wrap it in a func instead of lambda so it can be identified in
|
||||||
# the bus by its __name__ attribute.
|
# the bus by its __name__ attribute.
|
||||||
def update_device_state(time): # pylint: disable=unused-argument
|
def update_device_state(now):
|
||||||
""" Triggers update of the device states. """
|
""" Triggers update of the device states. """
|
||||||
self.update_devices()
|
self.update_devices(now)
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def reload_known_devices_service(service):
|
||||||
|
""" Reload known devices file. """
|
||||||
|
group.remove_group(self.hass, GROUP_NAME_ALL_DEVICES)
|
||||||
|
|
||||||
|
self._read_known_devices_file()
|
||||||
|
|
||||||
|
self.update_devices(datetime.now())
|
||||||
|
|
||||||
|
if self.tracked:
|
||||||
|
group.setup_group(
|
||||||
|
self.hass, GROUP_NAME_ALL_DEVICES,
|
||||||
|
self.device_entity_ids, False)
|
||||||
|
|
||||||
|
reload_known_devices_service(None)
|
||||||
|
|
||||||
|
if self.invalid_known_devices_file:
|
||||||
|
return
|
||||||
|
|
||||||
hass.track_time_change(update_device_state)
|
hass.track_time_change(update_device_state)
|
||||||
|
|
||||||
hass.services.register(DOMAIN,
|
hass.services.register(DOMAIN,
|
||||||
SERVICE_DEVICE_TRACKER_RELOAD,
|
SERVICE_DEVICE_TRACKER_RELOAD,
|
||||||
lambda service: self._read_known_devices_file())
|
reload_known_devices_service)
|
||||||
|
|
||||||
self.update_devices()
|
|
||||||
|
|
||||||
group.setup_group(
|
|
||||||
hass, GROUP_NAME_ALL_DEVICES, self.device_entity_ids, False)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_entity_ids(self):
|
def device_entity_ids(self):
|
||||||
""" Returns a set containing all device entity ids
|
""" Returns a set containing all device entity ids
|
||||||
that are being tracked. """
|
that are being tracked. """
|
||||||
return set([self.known_devices[device]['entity_id'] for device
|
return set(device['entity_id'] for device in self.tracked.values())
|
||||||
in self.known_devices
|
|
||||||
if self.known_devices[device]['track']])
|
|
||||||
|
|
||||||
def update_devices(self, found_devices=None):
|
def _update_state(self, now, device, is_home):
|
||||||
|
""" Update the state of a device. """
|
||||||
|
dev_info = self.tracked[device]
|
||||||
|
|
||||||
|
if is_home:
|
||||||
|
# Update last seen if at home
|
||||||
|
dev_info['last_seen'] = now
|
||||||
|
else:
|
||||||
|
# State remains at home if it has been seen in the last
|
||||||
|
# TIME_DEVICE_NOT_FOUND
|
||||||
|
is_home = now - dev_info['last_seen'] < TIME_DEVICE_NOT_FOUND
|
||||||
|
|
||||||
|
state = STATE_HOME if is_home else STATE_NOT_HOME
|
||||||
|
|
||||||
|
self.hass.states.set(
|
||||||
|
dev_info['entity_id'], state,
|
||||||
|
dev_info['state_attr'])
|
||||||
|
|
||||||
|
def update_devices(self, now):
|
||||||
""" Update device states based on the found devices. """
|
""" Update device states based on the found devices. """
|
||||||
self.lock.acquire()
|
self.lock.acquire()
|
||||||
|
|
||||||
found_devices = found_devices or self.device_scanner.scan_devices()
|
found_devices = set(self.device_scanner.scan_devices())
|
||||||
|
|
||||||
now = datetime.now()
|
for device in self.tracked:
|
||||||
|
is_home = device in found_devices
|
||||||
|
|
||||||
known_dev = self.known_devices
|
self._update_state(now, device, is_home)
|
||||||
|
|
||||||
temp_tracking_devices = [device for device in known_dev
|
if is_home:
|
||||||
if known_dev[device]['track']]
|
found_devices.remove(device)
|
||||||
|
|
||||||
for device in found_devices:
|
# Did we find any devices that we didn't know about yet?
|
||||||
# Are we tracking this device?
|
new_devices = found_devices - self.untracked_devices
|
||||||
if device in temp_tracking_devices:
|
|
||||||
temp_tracking_devices.remove(device)
|
|
||||||
|
|
||||||
known_dev[device]['last_seen'] = now
|
if new_devices:
|
||||||
|
self.untracked_devices.update(new_devices)
|
||||||
|
|
||||||
self.states.set(
|
# Write new devices to known devices file
|
||||||
known_dev[device]['entity_id'], components.STATE_HOME,
|
if not self.invalid_known_devices_file:
|
||||||
known_dev[device]['default_state_attr'])
|
|
||||||
|
|
||||||
# For all devices we did not find, set state to NH
|
known_dev_path = self.hass.get_config_path(KNOWN_DEVICES_FILE)
|
||||||
# But only if they have been gone for longer then the error time span
|
|
||||||
# Because we do not want to have stuff happening when the device does
|
|
||||||
# not show up for 1 scan beacuse of reboot etc
|
|
||||||
for device in temp_tracking_devices:
|
|
||||||
if now - known_dev[device]['last_seen'] > self.error_scanning:
|
|
||||||
|
|
||||||
self.states.set(known_dev[device]['entity_id'],
|
|
||||||
components.STATE_NOT_HOME,
|
|
||||||
known_dev[device]['default_state_attr'])
|
|
||||||
|
|
||||||
# If we come along any unknown devices we will write them to the
|
|
||||||
# known devices file but only if we did not encounter an invalid
|
|
||||||
# known devices file
|
|
||||||
if not self.invalid_known_devices_file:
|
|
||||||
|
|
||||||
known_dev_path = self.path_known_devices_file
|
|
||||||
|
|
||||||
unknown_devices = [device for device in found_devices
|
|
||||||
if device not in known_dev]
|
|
||||||
|
|
||||||
if unknown_devices:
|
|
||||||
try:
|
try:
|
||||||
# If file does not exist we will write the header too
|
# If file does not exist we will write the header too
|
||||||
is_new_file = not os.path.isfile(known_dev_path)
|
is_new_file = not os.path.isfile(known_dev_path)
|
||||||
|
@ -176,7 +192,7 @@ class DeviceTracker(object):
|
||||||
with open(known_dev_path, 'a') as outp:
|
with open(known_dev_path, 'a') as outp:
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Found %d new devices, updating %s",
|
"Found %d new devices, updating %s",
|
||||||
len(unknown_devices), known_dev_path)
|
len(new_devices), known_dev_path)
|
||||||
|
|
||||||
writer = csv.writer(outp)
|
writer = csv.writer(outp)
|
||||||
|
|
||||||
|
@ -184,109 +200,114 @@ class DeviceTracker(object):
|
||||||
writer.writerow((
|
writer.writerow((
|
||||||
"device", "name", "track", "picture"))
|
"device", "name", "track", "picture"))
|
||||||
|
|
||||||
for device in unknown_devices:
|
for device in new_devices:
|
||||||
# See if the device scanner knows the name
|
# See if the device scanner knows the name
|
||||||
# else defaults to unknown device
|
# else defaults to unknown device
|
||||||
name = (self.device_scanner.get_device_name(device)
|
name = (self.device_scanner.get_device_name(device)
|
||||||
or "unknown_device")
|
or "unknown_device")
|
||||||
|
|
||||||
writer.writerow((device, name, 0, ""))
|
writer.writerow((device, name, 0, ""))
|
||||||
known_dev[device] = {'name': name,
|
|
||||||
'track': False,
|
|
||||||
'picture': ""}
|
|
||||||
|
|
||||||
except IOError:
|
except IOError:
|
||||||
_LOGGER.exception(
|
_LOGGER.exception(
|
||||||
"Error updating %s with %d new devices",
|
"Error updating %s with %d new devices",
|
||||||
known_dev_path, len(unknown_devices))
|
known_dev_path, len(new_devices))
|
||||||
|
|
||||||
self.lock.release()
|
self.lock.release()
|
||||||
|
|
||||||
|
# pylint: disable=too-many-branches
|
||||||
def _read_known_devices_file(self):
|
def _read_known_devices_file(self):
|
||||||
""" Parse and process the known devices file. """
|
""" Parse and process the known devices file. """
|
||||||
|
known_dev_path = self.hass.get_config_path(KNOWN_DEVICES_FILE)
|
||||||
|
|
||||||
# Read known devices if file exists
|
# Return if no known devices file exists
|
||||||
if os.path.isfile(self.path_known_devices_file):
|
if not os.path.isfile(known_dev_path):
|
||||||
self.lock.acquire()
|
return
|
||||||
|
|
||||||
known_devices = {}
|
self.lock.acquire()
|
||||||
|
|
||||||
with open(self.path_known_devices_file) as inp:
|
self.untracked_devices.clear()
|
||||||
default_last_seen = datetime(1990, 1, 1)
|
|
||||||
|
|
||||||
# Temp variable to keep track of which entity ids we use
|
with open(known_dev_path) as inp:
|
||||||
# so we can ensure we have unique entity ids.
|
default_last_seen = datetime(1990, 1, 1)
|
||||||
used_entity_ids = []
|
|
||||||
|
|
||||||
try:
|
# To track which devices need an entity_id assigned
|
||||||
for row in csv.DictReader(inp):
|
need_entity_id = []
|
||||||
device = row['device']
|
|
||||||
|
|
||||||
row['track'] = True if row['track'] == '1' else False
|
# All devices that are still in this set after we read the CSV file
|
||||||
|
# have been removed from the file and thus need to be cleaned up.
|
||||||
|
removed_devices = set(self.tracked.keys())
|
||||||
|
|
||||||
|
try:
|
||||||
|
for row in csv.DictReader(inp):
|
||||||
|
device = row['device']
|
||||||
|
|
||||||
|
if row['track'] == '1':
|
||||||
|
if device in self.tracked:
|
||||||
|
# Device exists
|
||||||
|
removed_devices.remove(device)
|
||||||
|
else:
|
||||||
|
# We found a new device
|
||||||
|
need_entity_id.append(device)
|
||||||
|
|
||||||
|
self.tracked[device] = {
|
||||||
|
'name': row['name'],
|
||||||
|
'last_seen': default_last_seen
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update state_attr with latest from file
|
||||||
|
state_attr = {
|
||||||
|
ATTR_FRIENDLY_NAME: row['name']
|
||||||
|
}
|
||||||
|
|
||||||
if row['picture']:
|
if row['picture']:
|
||||||
row['default_state_attr'] = {
|
state_attr[ATTR_ENTITY_PICTURE] = row['picture']
|
||||||
components.ATTR_ENTITY_PICTURE: row['picture']}
|
|
||||||
|
|
||||||
else:
|
self.tracked[device]['state_attr'] = state_attr
|
||||||
row['default_state_attr'] = None
|
|
||||||
|
|
||||||
# If we track this device setup tracking variables
|
else:
|
||||||
if row['track']:
|
self.untracked_devices.add(device)
|
||||||
row['last_seen'] = default_last_seen
|
|
||||||
|
|
||||||
# Make sure that each device is mapped
|
# Remove existing devices that we no longer track
|
||||||
# to a unique entity_id name
|
for device in removed_devices:
|
||||||
name = util.slugify(row['name']) if row['name'] \
|
entity_id = self.tracked[device]['entity_id']
|
||||||
else "unnamed_device"
|
|
||||||
|
|
||||||
entity_id = ENTITY_ID_FORMAT.format(name)
|
_LOGGER.info("Removing entity %s", entity_id)
|
||||||
tries = 1
|
|
||||||
|
|
||||||
while entity_id in used_entity_ids:
|
self.hass.states.remove(entity_id)
|
||||||
tries += 1
|
|
||||||
|
|
||||||
suffix = "_{}".format(tries)
|
self.tracked.pop(device)
|
||||||
|
|
||||||
entity_id = ENTITY_ID_FORMAT.format(
|
# Setup entity_ids for the new devices
|
||||||
name + suffix)
|
used_entity_ids = [info['entity_id'] for device, info
|
||||||
|
in self.tracked.items()
|
||||||
|
if device not in need_entity_id]
|
||||||
|
|
||||||
row['entity_id'] = entity_id
|
for device in need_entity_id:
|
||||||
used_entity_ids.append(entity_id)
|
name = self.tracked[device]['name']
|
||||||
|
|
||||||
row['picture'] = row['picture']
|
entity_id = util.ensure_unique_string(
|
||||||
|
ENTITY_ID_FORMAT.format(util.slugify(name)),
|
||||||
|
used_entity_ids)
|
||||||
|
|
||||||
known_devices[device] = row
|
used_entity_ids.append(entity_id)
|
||||||
|
|
||||||
if not known_devices:
|
self.tracked[device]['entity_id'] = entity_id
|
||||||
_LOGGER.warning(
|
|
||||||
"No devices to track. Please update %s.",
|
|
||||||
self.path_known_devices_file)
|
|
||||||
|
|
||||||
# Remove entities that are no longer maintained
|
if not self.tracked:
|
||||||
new_entity_ids = set([known_devices[dev]['entity_id']
|
|
||||||
for dev in known_devices
|
|
||||||
if known_devices[dev]['track']])
|
|
||||||
|
|
||||||
for entity_id in \
|
|
||||||
self.device_entity_ids - new_entity_ids:
|
|
||||||
|
|
||||||
_LOGGER.info("Removing entity %s", entity_id)
|
|
||||||
self.states.remove(entity_id)
|
|
||||||
|
|
||||||
# File parsed, warnings given if necessary
|
|
||||||
# entities cleaned up, make it available
|
|
||||||
self.known_devices = known_devices
|
|
||||||
|
|
||||||
_LOGGER.info("Loaded devices from %s",
|
|
||||||
self.path_known_devices_file)
|
|
||||||
|
|
||||||
except KeyError:
|
|
||||||
self.invalid_known_devices_file = True
|
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
("Invalid known devices file: %s. "
|
"No devices to track. Please update %s.",
|
||||||
"We won't update it with new found devices."),
|
known_dev_path)
|
||||||
self.path_known_devices_file)
|
|
||||||
|
|
||||||
finally:
|
_LOGGER.info("Loaded devices from %s", known_dev_path)
|
||||||
self.lock.release()
|
|
||||||
|
except KeyError:
|
||||||
|
self.invalid_known_devices_file = True
|
||||||
|
|
||||||
|
_LOGGER.warning(
|
||||||
|
("Invalid known devices file: %s. "
|
||||||
|
"We won't update it with new found devices."),
|
||||||
|
known_dev_path)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self.lock.release()
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
""" Supports scanning a OpenWRT router. """
|
""" Supports scanning a OpenWRT router. """
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
from datetime import datetime, timedelta
|
from datetime import timedelta
|
||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
import homeassistant as ha
|
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||||
import homeassistant.util as util
|
from homeassistant.helpers import validate_config
|
||||||
|
from homeassistant.util import Throttle
|
||||||
from homeassistant.components.device_tracker import DOMAIN
|
from homeassistant.components.device_tracker import DOMAIN
|
||||||
|
|
||||||
# Return cached results if last scan was less then this time ago
|
# Return cached results if last scan was less then this time ago
|
||||||
|
@ -19,10 +20,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def get_scanner(hass, config):
|
def get_scanner(hass, config):
|
||||||
""" Validates config and returns a Luci scanner. """
|
""" Validates config and returns a Luci scanner. """
|
||||||
if not util.validate_config(config,
|
if not validate_config(config,
|
||||||
{DOMAIN: [ha.CONF_HOST, ha.CONF_USERNAME,
|
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
|
||||||
ha.CONF_PASSWORD]},
|
_LOGGER):
|
||||||
_LOGGER):
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
scanner = LuciDeviceScanner(config[DOMAIN])
|
scanner = LuciDeviceScanner(config[DOMAIN])
|
||||||
|
@ -45,14 +45,13 @@ class LuciDeviceScanner(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
host = config[ha.CONF_HOST]
|
host = config[CONF_HOST]
|
||||||
username, password = config[ha.CONF_USERNAME], config[ha.CONF_PASSWORD]
|
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
|
||||||
|
|
||||||
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
|
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
|
||||||
|
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
self.date_updated = None
|
|
||||||
self.last_results = {}
|
self.last_results = {}
|
||||||
|
|
||||||
self.token = _get_token(host, username, password)
|
self.token = _get_token(host, username, password)
|
||||||
|
@ -88,29 +87,25 @@ class LuciDeviceScanner(object):
|
||||||
return
|
return
|
||||||
return self.mac2name.get(device, None)
|
return self.mac2name.get(device, None)
|
||||||
|
|
||||||
|
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
""" Ensures the information from the Luci router is up to date.
|
""" Ensures the information from the Luci router is up to date.
|
||||||
Returns boolean if scanning successful. """
|
Returns boolean if scanning successful. """
|
||||||
if not self.success_init:
|
if not self.success_init:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
# if date_updated is None or the date is too old we scan
|
_LOGGER.info("Checking ARP")
|
||||||
# for new data
|
|
||||||
if not self.date_updated or \
|
|
||||||
datetime.now() - self.date_updated > MIN_TIME_BETWEEN_SCANS:
|
|
||||||
|
|
||||||
_LOGGER.info("Checking ARP")
|
url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host)
|
||||||
|
result = _req_json_rpc(url, 'net.arptable',
|
||||||
|
params={'auth': self.token})
|
||||||
|
if result:
|
||||||
|
self.last_results = [x['HW address'] for x in result]
|
||||||
|
|
||||||
url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host)
|
return True
|
||||||
result = _req_json_rpc(url, 'net.arptable',
|
|
||||||
params={'auth': self.token})
|
|
||||||
if result:
|
|
||||||
self.last_results = [x['HW address'] for x in result]
|
|
||||||
self.date_updated = datetime.now()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _req_json_rpc(url, method, *args, **kwargs):
|
def _req_json_rpc(url, method, *args, **kwargs):
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
""" Supports scanning a Netgear router. """
|
""" Supports scanning a Netgear router. """
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import timedelta
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
import homeassistant as ha
|
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||||
import homeassistant.util as util
|
from homeassistant.helpers import validate_config
|
||||||
|
from homeassistant.util import Throttle
|
||||||
from homeassistant.components.device_tracker import DOMAIN
|
from homeassistant.components.device_tracker import DOMAIN
|
||||||
|
|
||||||
# Return cached results if last scan was less then this time ago
|
# Return cached results if last scan was less then this time ago
|
||||||
|
@ -16,10 +17,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def get_scanner(hass, config):
|
def get_scanner(hass, config):
|
||||||
""" Validates config and returns a Netgear scanner. """
|
""" Validates config and returns a Netgear scanner. """
|
||||||
if not util.validate_config(config,
|
if not validate_config(config,
|
||||||
{DOMAIN: [ha.CONF_HOST, ha.CONF_USERNAME,
|
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
|
||||||
ha.CONF_PASSWORD]},
|
_LOGGER):
|
||||||
_LOGGER):
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
scanner = NetgearDeviceScanner(config[DOMAIN])
|
scanner = NetgearDeviceScanner(config[DOMAIN])
|
||||||
|
@ -31,10 +31,9 @@ class NetgearDeviceScanner(object):
|
||||||
""" This class queries a Netgear wireless router using the SOAP-api. """
|
""" This class queries a Netgear wireless router using the SOAP-api. """
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
host = config[ha.CONF_HOST]
|
host = config[CONF_HOST]
|
||||||
username, password = config[ha.CONF_USERNAME], config[ha.CONF_PASSWORD]
|
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
|
||||||
|
|
||||||
self.date_updated = None
|
|
||||||
self.last_results = []
|
self.last_results = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -75,10 +74,6 @@ class NetgearDeviceScanner(object):
|
||||||
def get_device_name(self, mac):
|
def get_device_name(self, mac):
|
||||||
""" Returns the name of the given device or None if we don't know. """
|
""" Returns the name of the given device or None if we don't know. """
|
||||||
|
|
||||||
# Make sure there are results
|
|
||||||
if not self.date_updated:
|
|
||||||
self._update_info()
|
|
||||||
|
|
||||||
filter_named = [device.name for device in self.last_results
|
filter_named = [device.name for device in self.last_results
|
||||||
if device.mac == mac]
|
if device.mac == mac]
|
||||||
|
|
||||||
|
@ -87,6 +82,7 @@ class NetgearDeviceScanner(object):
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||||
def _update_info(self):
|
def _update_info(self):
|
||||||
""" Retrieves latest information from the Netgear router.
|
""" Retrieves latest information from the Netgear router.
|
||||||
Returns boolean if scanning successful. """
|
Returns boolean if scanning successful. """
|
||||||
|
@ -94,18 +90,6 @@ class NetgearDeviceScanner(object):
|
||||||
return
|
return
|
||||||
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
# if date_updated is None or the date is too old we scan for
|
_LOGGER.info("Scanning")
|
||||||
# new data
|
|
||||||
if not self.date_updated or \
|
|
||||||
datetime.now() - self.date_updated > MIN_TIME_BETWEEN_SCANS:
|
|
||||||
|
|
||||||
_LOGGER.info("Scanning")
|
self.last_results = self._api.get_attached_devices()
|
||||||
|
|
||||||
self.last_results = self._api.get_attached_devices()
|
|
||||||
|
|
||||||
self.date_updated = datetime.now()
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
|
@ -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. """
|
""" Supports scanning a Tomato router. """
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
from datetime import datetime, timedelta
|
from datetime import timedelta
|
||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
import homeassistant as ha
|
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||||
import homeassistant.util as util
|
from homeassistant.helpers import validate_config
|
||||||
|
from homeassistant.util import Throttle
|
||||||
from homeassistant.components.device_tracker import DOMAIN
|
from homeassistant.components.device_tracker import DOMAIN
|
||||||
|
|
||||||
# Return cached results if last scan was less then this time ago
|
# Return cached results if last scan was less then this time ago
|
||||||
|
@ -22,10 +23,10 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def get_scanner(hass, config):
|
def get_scanner(hass, config):
|
||||||
""" Validates config and returns a Tomato scanner. """
|
""" Validates config and returns a Tomato scanner. """
|
||||||
if not util.validate_config(config,
|
if not validate_config(config,
|
||||||
{DOMAIN: [ha.CONF_HOST, ha.CONF_USERNAME,
|
{DOMAIN: [CONF_HOST, CONF_USERNAME,
|
||||||
ha.CONF_PASSWORD, CONF_HTTP_ID]},
|
CONF_PASSWORD, CONF_HTTP_ID]},
|
||||||
_LOGGER):
|
_LOGGER):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return TomatoDeviceScanner(config[DOMAIN])
|
return TomatoDeviceScanner(config[DOMAIN])
|
||||||
|
@ -40,8 +41,8 @@ class TomatoDeviceScanner(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
host, http_id = config[ha.CONF_HOST], config[CONF_HTTP_ID]
|
host, http_id = config[CONF_HOST], config[CONF_HTTP_ID]
|
||||||
username, password = config[ha.CONF_USERNAME], config[ha.CONF_PASSWORD]
|
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
|
||||||
|
|
||||||
self.req = requests.Request('POST',
|
self.req = requests.Request('POST',
|
||||||
'http://{}/update.cgi'.format(host),
|
'http://{}/update.cgi'.format(host),
|
||||||
|
@ -55,7 +56,6 @@ class TomatoDeviceScanner(object):
|
||||||
self.logger = logging.getLogger("{}.{}".format(__name__, "Tomato"))
|
self.logger = logging.getLogger("{}.{}".format(__name__, "Tomato"))
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
self.date_updated = None
|
|
||||||
self.last_results = {"wldev": [], "dhcpd_lease": []}
|
self.last_results = {"wldev": [], "dhcpd_lease": []}
|
||||||
|
|
||||||
self.success_init = self._update_tomato_info()
|
self.success_init = self._update_tomato_info()
|
||||||
|
@ -71,10 +71,6 @@ class TomatoDeviceScanner(object):
|
||||||
def get_device_name(self, device):
|
def get_device_name(self, device):
|
||||||
""" Returns the name of the given device or None if we don't know. """
|
""" Returns the name of the given device or None if we don't know. """
|
||||||
|
|
||||||
# Make sure there are results
|
|
||||||
if not self.date_updated:
|
|
||||||
self._update_tomato_info()
|
|
||||||
|
|
||||||
filter_named = [item[0] for item in self.last_results['dhcpd_lease']
|
filter_named = [item[0] for item in self.last_results['dhcpd_lease']
|
||||||
if item[2] == device]
|
if item[2] == device]
|
||||||
|
|
||||||
|
@ -83,16 +79,12 @@ class TomatoDeviceScanner(object):
|
||||||
else:
|
else:
|
||||||
return filter_named[0]
|
return filter_named[0]
|
||||||
|
|
||||||
|
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||||
def _update_tomato_info(self):
|
def _update_tomato_info(self):
|
||||||
""" Ensures the information from the Tomato router is up to date.
|
""" Ensures the information from the Tomato router is up to date.
|
||||||
Returns boolean if scanning successful. """
|
Returns boolean if scanning successful. """
|
||||||
|
|
||||||
self.lock.acquire()
|
with self.lock:
|
||||||
|
|
||||||
# if date_updated is None or the date is too old we scan for new data
|
|
||||||
if not self.date_updated or \
|
|
||||||
datetime.now() - self.date_updated > MIN_TIME_BETWEEN_SCANS:
|
|
||||||
|
|
||||||
self.logger.info("Scanning")
|
self.logger.info("Scanning")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -111,8 +103,6 @@ class TomatoDeviceScanner(object):
|
||||||
self.last_results[param] = \
|
self.last_results[param] = \
|
||||||
json.loads(value.replace("'", '"'))
|
json.loads(value.replace("'", '"'))
|
||||||
|
|
||||||
self.date_updated = datetime.now()
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
elif response.status_code == 401:
|
elif response.status_code == 401:
|
||||||
|
@ -146,13 +136,3 @@ class TomatoDeviceScanner(object):
|
||||||
"Failed to parse response from router")
|
"Failed to parse response from router")
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
finally:
|
|
||||||
self.lock.release()
|
|
||||||
|
|
||||||
else:
|
|
||||||
# We acquired the lock before the IF check,
|
|
||||||
# release it before we return True
|
|
||||||
self.lock.release()
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
|
@ -9,7 +9,8 @@ import logging
|
||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
import homeassistant.util as util
|
from homeassistant.helpers import validate_config
|
||||||
|
from homeassistant.util import sanitize_filename
|
||||||
|
|
||||||
DOMAIN = "downloader"
|
DOMAIN = "downloader"
|
||||||
DEPENDENCIES = []
|
DEPENDENCIES = []
|
||||||
|
@ -36,7 +37,7 @@ def setup(hass, config):
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not util.validate_config(config, {DOMAIN: [CONF_DOWNLOAD_DIR]}, logger):
|
if not validate_config(config, {DOMAIN: [CONF_DOWNLOAD_DIR]}, logger):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
download_path = config[DOMAIN][CONF_DOWNLOAD_DIR]
|
download_path = config[DOMAIN][CONF_DOWNLOAD_DIR]
|
||||||
|
@ -64,7 +65,7 @@ def setup(hass, config):
|
||||||
subdir = service.data.get(ATTR_SUBDIR)
|
subdir = service.data.get(ATTR_SUBDIR)
|
||||||
|
|
||||||
if subdir:
|
if subdir:
|
||||||
subdir = util.sanitize_filename(subdir)
|
subdir = sanitize_filename(subdir)
|
||||||
|
|
||||||
final_path = None
|
final_path = None
|
||||||
|
|
||||||
|
@ -88,7 +89,7 @@ def setup(hass, config):
|
||||||
filename = "ha_download"
|
filename = "ha_download"
|
||||||
|
|
||||||
# Remove stuff to ruin paths
|
# Remove stuff to ruin paths
|
||||||
filename = util.sanitize_filename(filename)
|
filename = sanitize_filename(filename)
|
||||||
|
|
||||||
# Do we want to download to subdir, create if needed
|
# Do we want to download to subdir, create if needed
|
||||||
if subdir:
|
if subdir:
|
||||||
|
|
|
@ -7,10 +7,10 @@ Provides functionality to group devices that can be turned on or off.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import homeassistant as ha
|
||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
from homeassistant.components import (STATE_ON, STATE_OFF,
|
from homeassistant.const import (
|
||||||
STATE_HOME, STATE_NOT_HOME,
|
ATTR_ENTITY_ID, STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME)
|
||||||
ATTR_ENTITY_ID)
|
|
||||||
|
|
||||||
DOMAIN = "group"
|
DOMAIN = "group"
|
||||||
DEPENDENCIES = []
|
DEPENDENCIES = []
|
||||||
|
@ -19,19 +19,19 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||||
|
|
||||||
ATTR_AUTO = "auto"
|
ATTR_AUTO = "auto"
|
||||||
|
|
||||||
_GROUP_TYPES = {
|
# List of ON/OFF state tuples for groupable states
|
||||||
"on_off": (STATE_ON, STATE_OFF),
|
_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME)]
|
||||||
"home_not_home": (STATE_HOME, STATE_NOT_HOME)
|
|
||||||
}
|
_GROUPS = {}
|
||||||
|
|
||||||
|
|
||||||
def _get_group_type(state):
|
def _get_group_on_off(state):
|
||||||
""" Determine the group type based on the given group type. """
|
""" Determine the group on/off states based on a state. """
|
||||||
for group_type, states in _GROUP_TYPES.items():
|
for states in _GROUP_TYPES:
|
||||||
if state in states:
|
if state in states:
|
||||||
return group_type
|
return states
|
||||||
|
|
||||||
return None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
def is_on(hass, entity_id):
|
def is_on(hass, entity_id):
|
||||||
|
@ -39,10 +39,10 @@ def is_on(hass, entity_id):
|
||||||
state = hass.states.get(entity_id)
|
state = hass.states.get(entity_id)
|
||||||
|
|
||||||
if state:
|
if state:
|
||||||
group_type = _get_group_type(state.state)
|
group_on, _ = _get_group_on_off(state.state)
|
||||||
|
|
||||||
# If we found a group_type, compare to ON-state
|
# If we found a group_type, compare to ON-state
|
||||||
return group_type and state.state == _GROUP_TYPES[group_type][0]
|
return group_on is not None and state.state == group_on
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -101,93 +101,114 @@ def setup(hass, config):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-branches
|
|
||||||
def setup_group(hass, name, entity_ids, user_defined=True):
|
def setup_group(hass, name, entity_ids, user_defined=True):
|
||||||
""" Sets up a group state that is the combined state of
|
""" Sets up a group state that is the combined state of
|
||||||
several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """
|
several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# In case an iterable is passed in
|
# In case an iterable is passed in
|
||||||
entity_ids = list(entity_ids)
|
entity_ids = list(entity_ids)
|
||||||
|
|
||||||
|
if not entity_ids:
|
||||||
|
logger.error(
|
||||||
|
'Error setting up group %s: no entities passed in to track', name)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
# Loop over the given entities to:
|
# Loop over the given entities to:
|
||||||
# - determine which group type this is (on_off, device_home)
|
# - determine which group type this is (on_off, device_home)
|
||||||
# - if all states exist and have valid states
|
# - determine which states exist and have groupable states
|
||||||
# - retrieve the current state of the group
|
# - determine the current state of the group
|
||||||
errors = []
|
warnings = []
|
||||||
group_type, group_on, group_off, group_state = None, None, None, None
|
group_ids = []
|
||||||
|
group_on, group_off = None, None
|
||||||
|
group_state = False
|
||||||
|
|
||||||
for entity_id in entity_ids:
|
for entity_id in entity_ids:
|
||||||
state = hass.states.get(entity_id)
|
state = hass.states.get(entity_id)
|
||||||
|
|
||||||
# Try to determine group type if we didn't yet
|
# Try to determine group type if we didn't yet
|
||||||
if not group_type and state:
|
if group_on is None and state:
|
||||||
group_type = _get_group_type(state.state)
|
group_on, group_off = _get_group_on_off(state.state)
|
||||||
|
|
||||||
if group_type:
|
if group_on is None:
|
||||||
group_on, group_off = _GROUP_TYPES[group_type]
|
|
||||||
group_state = group_off
|
|
||||||
|
|
||||||
else:
|
|
||||||
# We did not find a matching group_type
|
# We did not find a matching group_type
|
||||||
errors.append(
|
warnings.append(
|
||||||
"Entity {} has ungroupable state '{}'".format(
|
"Entity {} has ungroupable state '{}'".format(
|
||||||
name, state.state))
|
name, state.state))
|
||||||
|
|
||||||
# Stop check all other entity IDs and report as error
|
continue
|
||||||
break
|
|
||||||
|
|
||||||
# Check if entity exists
|
# Check if entity exists
|
||||||
if not state:
|
if not state:
|
||||||
errors.append("Entity {} does not exist".format(entity_id))
|
warnings.append("Entity {} does not exist".format(entity_id))
|
||||||
|
|
||||||
# Check if entity is valid state
|
# Check if entity is invalid state
|
||||||
elif state.state != group_off and state.state != group_on:
|
elif state.state != group_off and state.state != group_on:
|
||||||
|
|
||||||
errors.append("State of {} is {} (expected: {} or {})".format(
|
warnings.append("State of {} is {} (expected: {} or {})".format(
|
||||||
entity_id, state.state, group_off, group_on))
|
entity_id, state.state, group_off, group_on))
|
||||||
|
|
||||||
# Keep track of the group state to init later on
|
# We have a valid group state
|
||||||
elif state.state == group_on:
|
else:
|
||||||
group_state = group_on
|
group_ids.append(entity_id)
|
||||||
|
|
||||||
if group_type is None and not errors:
|
# Keep track of the group state to init later on
|
||||||
errors.append('Unable to determine group type for {}'.format(name))
|
group_state = group_state or state.state == group_on
|
||||||
|
|
||||||
if errors:
|
# If none of the entities could be found during setup
|
||||||
logging.getLogger(__name__).error(
|
if not group_ids:
|
||||||
"Error setting up group %s: %s", name, ", ".join(errors))
|
logger.error('Unable to find any entities to track for group %s', name)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
else:
|
elif warnings:
|
||||||
group_entity_id = ENTITY_ID_FORMAT.format(name)
|
logger.warning(
|
||||||
state_attr = {ATTR_ENTITY_ID: entity_ids, ATTR_AUTO: not user_defined}
|
'Warnings during setting up group %s: %s',
|
||||||
|
name, ", ".join(warnings))
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
group_entity_id = ENTITY_ID_FORMAT.format(util.slugify(name))
|
||||||
def update_group_state(entity_id, old_state, new_state):
|
state = group_on if group_state else group_off
|
||||||
""" Updates the group state based on a state change by
|
state_attr = {ATTR_ENTITY_ID: group_ids, ATTR_AUTO: not user_defined}
|
||||||
a tracked entity. """
|
|
||||||
|
|
||||||
cur_gr_state = hass.states.get(group_entity_id).state
|
# pylint: disable=unused-argument
|
||||||
|
def update_group_state(entity_id, old_state, new_state):
|
||||||
|
""" Updates the group state based on a state change by
|
||||||
|
a tracked entity. """
|
||||||
|
|
||||||
# if cur_gr_state = OFF and new_state = ON: set ON
|
cur_gr_state = hass.states.get(group_entity_id).state
|
||||||
# if cur_gr_state = ON and new_state = OFF: research
|
|
||||||
# else: ignore
|
|
||||||
|
|
||||||
if cur_gr_state == group_off and new_state.state == group_on:
|
# if cur_gr_state = OFF and new_state = ON: set ON
|
||||||
|
# if cur_gr_state = ON and new_state = OFF: research
|
||||||
|
# else: ignore
|
||||||
|
|
||||||
hass.states.set(group_entity_id, group_on, state_attr)
|
if cur_gr_state == group_off and new_state.state == group_on:
|
||||||
|
|
||||||
elif cur_gr_state == group_on and new_state.state == group_off:
|
hass.states.set(group_entity_id, group_on, state_attr)
|
||||||
|
|
||||||
# Check if any of the other states is still on
|
elif cur_gr_state == group_on and new_state.state == group_off:
|
||||||
if not any([hass.states.is_state(ent_id, group_on)
|
|
||||||
for ent_id in entity_ids
|
|
||||||
if entity_id != ent_id]):
|
|
||||||
hass.states.set(group_entity_id, group_off, state_attr)
|
|
||||||
|
|
||||||
hass.track_state_change(entity_ids, update_group_state)
|
# Check if any of the other states is still on
|
||||||
|
if not any([hass.states.is_state(ent_id, group_on)
|
||||||
|
for ent_id in group_ids
|
||||||
|
if entity_id != ent_id]):
|
||||||
|
hass.states.set(group_entity_id, group_off, state_attr)
|
||||||
|
|
||||||
hass.states.set(group_entity_id, group_state, state_attr)
|
_GROUPS[group_entity_id] = hass.states.track_change(
|
||||||
|
group_ids, update_group_state)
|
||||||
|
|
||||||
return True
|
hass.states.set(group_entity_id, state, state_attr)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def remove_group(hass, name):
|
||||||
|
""" Remove a group and its state listener from Home Assistant. """
|
||||||
|
group_entity_id = ENTITY_ID_FORMAT.format(util.slugify(name))
|
||||||
|
|
||||||
|
if hass.states.get(group_entity_id) is not None:
|
||||||
|
hass.states.remove(group_entity_id)
|
||||||
|
|
||||||
|
if group_entity_id in _GROUPS:
|
||||||
|
hass.bus.remove_listener(
|
||||||
|
ha.EVENT_STATE_CHANGED, _GROUPS.pop(group_entity_id))
|
||||||
|
|
|
@ -83,6 +83,10 @@ from socketserver import ThreadingMixIn
|
||||||
from urllib.parse import urlparse, parse_qs
|
from urllib.parse import urlparse, parse_qs
|
||||||
|
|
||||||
import homeassistant as ha
|
import homeassistant as ha
|
||||||
|
from homeassistant.const import (
|
||||||
|
SERVER_PORT, URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES,
|
||||||
|
URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, AUTH_HEADER)
|
||||||
|
from homeassistant.helpers import validate_config, TrackStates
|
||||||
import homeassistant.remote as rem
|
import homeassistant.remote as rem
|
||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
from . import frontend
|
from . import frontend
|
||||||
|
@ -108,22 +112,23 @@ CONF_SERVER_HOST = "server_host"
|
||||||
CONF_SERVER_PORT = "server_port"
|
CONF_SERVER_PORT = "server_port"
|
||||||
CONF_DEVELOPMENT = "development"
|
CONF_DEVELOPMENT = "development"
|
||||||
|
|
||||||
|
DATA_API_PASSWORD = 'api_password'
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
""" Sets up the HTTP API and debug interface. """
|
""" Sets up the HTTP API and debug interface. """
|
||||||
|
|
||||||
if not util.validate_config(config, {DOMAIN: [CONF_API_PASSWORD]},
|
if not validate_config(config, {DOMAIN: [CONF_API_PASSWORD]}, _LOGGER):
|
||||||
_LOGGER):
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
api_password = config[DOMAIN]['api_password']
|
api_password = config[DOMAIN][CONF_API_PASSWORD]
|
||||||
|
|
||||||
# If no server host is given, accept all incoming requests
|
# If no server host is given, accept all incoming requests
|
||||||
server_host = config[DOMAIN].get(CONF_SERVER_HOST, '0.0.0.0')
|
server_host = config[DOMAIN].get(CONF_SERVER_HOST, '0.0.0.0')
|
||||||
|
|
||||||
server_port = config[DOMAIN].get(CONF_SERVER_PORT, rem.SERVER_PORT)
|
server_port = config[DOMAIN].get(CONF_SERVER_PORT, SERVER_PORT)
|
||||||
|
|
||||||
development = config[DOMAIN].get(CONF_DEVELOPMENT, "") == "1"
|
development = config[DOMAIN].get(CONF_DEVELOPMENT, "") == "1"
|
||||||
|
|
||||||
|
@ -131,15 +136,11 @@ def setup(hass, config):
|
||||||
RequestHandler, hass, api_password,
|
RequestHandler, hass, api_password,
|
||||||
development)
|
development)
|
||||||
|
|
||||||
hass.listen_once_event(
|
hass.bus.listen_once(
|
||||||
ha.EVENT_HOMEASSISTANT_START,
|
ha.EVENT_HOMEASSISTANT_START,
|
||||||
lambda event:
|
lambda event:
|
||||||
threading.Thread(target=server.start, daemon=True).start())
|
threading.Thread(target=server.start, daemon=True).start())
|
||||||
|
|
||||||
hass.listen_once_event(
|
|
||||||
ha.EVENT_HOMEASSISTANT_STOP,
|
|
||||||
lambda event: server.shutdown())
|
|
||||||
|
|
||||||
# If no local api set, set one with known information
|
# If no local api set, set one with known information
|
||||||
if isinstance(hass, rem.HomeAssistant) and hass.local_api is None:
|
if isinstance(hass, rem.HomeAssistant) and hass.local_api is None:
|
||||||
hass.local_api = \
|
hass.local_api = \
|
||||||
|
@ -156,9 +157,9 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
|
||||||
daemon_threads = True
|
daemon_threads = True
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
# pylint: disable=too-many-arguments
|
||||||
def __init__(self, server_address, RequestHandlerClass,
|
def __init__(self, server_address, request_handler_class,
|
||||||
hass, api_password, development=False):
|
hass, api_password, development=False):
|
||||||
super().__init__(server_address, RequestHandlerClass)
|
super().__init__(server_address, request_handler_class)
|
||||||
|
|
||||||
self.server_address = server_address
|
self.server_address = server_address
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
|
@ -173,6 +174,10 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
""" Starts the server. """
|
""" Starts the server. """
|
||||||
|
self.hass.bus.listen_once(
|
||||||
|
ha.EVENT_HOMEASSISTANT_STOP,
|
||||||
|
lambda event: self.shutdown())
|
||||||
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Starting web interface at http://%s:%d", *self.server_address)
|
"Starting web interface at http://%s:%d", *self.server_address)
|
||||||
|
|
||||||
|
@ -192,13 +197,12 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||||
|
|
||||||
PATHS = [ # debug interface
|
PATHS = [ # debug interface
|
||||||
('GET', URL_ROOT, '_handle_get_root'),
|
('GET', URL_ROOT, '_handle_get_root'),
|
||||||
('POST', URL_ROOT, '_handle_get_root'),
|
|
||||||
|
|
||||||
# /api - for validation purposes
|
# /api - for validation purposes
|
||||||
('GET', rem.URL_API, '_handle_get_api'),
|
('GET', URL_API, '_handle_get_api'),
|
||||||
|
|
||||||
# /states
|
# /states
|
||||||
('GET', rem.URL_API_STATES, '_handle_get_api_states'),
|
('GET', URL_API_STATES, '_handle_get_api_states'),
|
||||||
('GET',
|
('GET',
|
||||||
re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||||
'_handle_get_api_states_entity'),
|
'_handle_get_api_states_entity'),
|
||||||
|
@ -210,13 +214,13 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||||
'_handle_post_state_entity'),
|
'_handle_post_state_entity'),
|
||||||
|
|
||||||
# /events
|
# /events
|
||||||
('GET', rem.URL_API_EVENTS, '_handle_get_api_events'),
|
('GET', URL_API_EVENTS, '_handle_get_api_events'),
|
||||||
('POST',
|
('POST',
|
||||||
re.compile(r'/api/events/(?P<event_type>[a-zA-Z\._0-9]+)'),
|
re.compile(r'/api/events/(?P<event_type>[a-zA-Z\._0-9]+)'),
|
||||||
'_handle_api_post_events_event'),
|
'_handle_api_post_events_event'),
|
||||||
|
|
||||||
# /services
|
# /services
|
||||||
('GET', rem.URL_API_SERVICES, '_handle_get_api_services'),
|
('GET', URL_API_SERVICES, '_handle_get_api_services'),
|
||||||
('POST',
|
('POST',
|
||||||
re.compile((r'/api/services/'
|
re.compile((r'/api/services/'
|
||||||
r'(?P<domain>[a-zA-Z\._0-9]+)/'
|
r'(?P<domain>[a-zA-Z\._0-9]+)/'
|
||||||
|
@ -224,12 +228,14 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||||
'_handle_post_api_services_domain_service'),
|
'_handle_post_api_services_domain_service'),
|
||||||
|
|
||||||
# /event_forwarding
|
# /event_forwarding
|
||||||
('POST', rem.URL_API_EVENT_FORWARD, '_handle_post_api_event_forward'),
|
('POST', URL_API_EVENT_FORWARD, '_handle_post_api_event_forward'),
|
||||||
('DELETE', rem.URL_API_EVENT_FORWARD,
|
('DELETE', URL_API_EVENT_FORWARD,
|
||||||
'_handle_delete_api_event_forward'),
|
'_handle_delete_api_event_forward'),
|
||||||
|
|
||||||
# Statis files
|
# Static files
|
||||||
('GET', re.compile(r'/static/(?P<file>[a-zA-Z\._\-0-9/]+)'),
|
('GET', re.compile(r'/static/(?P<file>[a-zA-Z\._\-0-9/]+)'),
|
||||||
|
'_handle_get_static'),
|
||||||
|
('HEAD', re.compile(r'/static/(?P<file>[a-zA-Z\._\-0-9/]+)'),
|
||||||
'_handle_get_static')
|
'_handle_get_static')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -255,24 +261,22 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||||
if content_length:
|
if content_length:
|
||||||
body_content = self.rfile.read(content_length).decode("UTF-8")
|
body_content = self.rfile.read(content_length).decode("UTF-8")
|
||||||
|
|
||||||
if self.use_json:
|
try:
|
||||||
try:
|
data.update(json.loads(body_content))
|
||||||
data.update(json.loads(body_content))
|
except (TypeError, ValueError):
|
||||||
except ValueError:
|
# TypeError is JSON object is not a dict
|
||||||
_LOGGER.exception("Exception parsing JSON: %s",
|
# ValueError if we could not parse JSON
|
||||||
body_content)
|
_LOGGER.exception("Exception parsing JSON: %s",
|
||||||
|
body_content)
|
||||||
|
|
||||||
self._message(
|
self._json_message(
|
||||||
"Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY)
|
"Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY)
|
||||||
return
|
return
|
||||||
else:
|
|
||||||
data.update({key: value[-1] for key, value in
|
|
||||||
parse_qs(body_content).items()})
|
|
||||||
|
|
||||||
api_password = self.headers.get(rem.AUTH_HEADER)
|
api_password = self.headers.get(AUTH_HEADER)
|
||||||
|
|
||||||
if not api_password and 'api_password' in data:
|
if not api_password and DATA_API_PASSWORD in data:
|
||||||
api_password = data['api_password']
|
api_password = data[DATA_API_PASSWORD]
|
||||||
|
|
||||||
if '_METHOD' in data:
|
if '_METHOD' in data:
|
||||||
method = data.pop('_METHOD')
|
method = data.pop('_METHOD')
|
||||||
|
@ -307,7 +311,7 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||||
|
|
||||||
# For API calls we need a valid password
|
# For API calls we need a valid password
|
||||||
if self.use_json and api_password != self.server.api_password:
|
if self.use_json and api_password != self.server.api_password:
|
||||||
self._message(
|
self._json_message(
|
||||||
"API password missing or incorrect.", HTTP_UNAUTHORIZED)
|
"API password missing or incorrect.", HTTP_UNAUTHORIZED)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
@ -315,9 +319,11 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||||
|
|
||||||
elif path_matched_but_not_method:
|
elif path_matched_but_not_method:
|
||||||
self.send_response(HTTP_METHOD_NOT_ALLOWED)
|
self.send_response(HTTP_METHOD_NOT_ALLOWED)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.send_response(HTTP_NOT_FOUND)
|
self.send_response(HTTP_NOT_FOUND)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
def do_HEAD(self): # pylint: disable=invalid-name
|
def do_HEAD(self): # pylint: disable=invalid-name
|
||||||
""" HEAD request handler. """
|
""" HEAD request handler. """
|
||||||
|
@ -377,7 +383,7 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def _handle_get_api(self, path_match, data):
|
def _handle_get_api(self, path_match, data):
|
||||||
""" Renders the debug interface. """
|
""" Renders the debug interface. """
|
||||||
self._message("API running.")
|
self._json_message("API running.")
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def _handle_get_api_states(self, path_match, data):
|
def _handle_get_api_states(self, path_match, data):
|
||||||
|
@ -394,7 +400,7 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||||
if state:
|
if state:
|
||||||
self._write_json(state)
|
self._write_json(state)
|
||||||
else:
|
else:
|
||||||
self._message("State does not exist.", HTTP_NOT_FOUND)
|
self._json_message("State does not exist.", HTTP_NOT_FOUND)
|
||||||
|
|
||||||
def _handle_post_state_entity(self, path_match, data):
|
def _handle_post_state_entity(self, path_match, data):
|
||||||
""" Handles updating the state of an entity.
|
""" Handles updating the state of an entity.
|
||||||
|
@ -407,7 +413,7 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||||
try:
|
try:
|
||||||
new_state = data['state']
|
new_state = data['state']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self._message("state not specified", HTTP_BAD_REQUEST)
|
self._json_message("state not specified", HTTP_BAD_REQUEST)
|
||||||
return
|
return
|
||||||
|
|
||||||
attributes = data['attributes'] if 'attributes' in data else None
|
attributes = data['attributes'] if 'attributes' in data else None
|
||||||
|
@ -417,19 +423,14 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||||
# Write state
|
# Write state
|
||||||
self.server.hass.states.set(entity_id, new_state, attributes)
|
self.server.hass.states.set(entity_id, new_state, attributes)
|
||||||
|
|
||||||
# Return state if json, else redirect to main page
|
state = self.server.hass.states.get(entity_id)
|
||||||
if self.use_json:
|
|
||||||
state = self.server.hass.states.get(entity_id)
|
|
||||||
|
|
||||||
status_code = HTTP_CREATED if is_new_state else HTTP_OK
|
status_code = HTTP_CREATED if is_new_state else HTTP_OK
|
||||||
|
|
||||||
self._write_json(
|
self._write_json(
|
||||||
state.as_dict(),
|
state.as_dict(),
|
||||||
status_code=status_code,
|
status_code=status_code,
|
||||||
location=rem.URL_API_STATES_ENTITY.format(entity_id))
|
location=URL_API_STATES_ENTITY.format(entity_id))
|
||||||
else:
|
|
||||||
self._message(
|
|
||||||
"State of {} changed to {}".format(entity_id, new_state))
|
|
||||||
|
|
||||||
def _handle_get_api_events(self, path_match, data):
|
def _handle_get_api_events(self, path_match, data):
|
||||||
""" Handles getting overview of event listeners. """
|
""" Handles getting overview of event listeners. """
|
||||||
|
@ -448,8 +449,8 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||||
event_type = path_match.group('event_type')
|
event_type = path_match.group('event_type')
|
||||||
|
|
||||||
if event_data is not None and not isinstance(event_data, dict):
|
if event_data is not None and not isinstance(event_data, dict):
|
||||||
self._message("event_data should be an object",
|
self._json_message("event_data should be an object",
|
||||||
HTTP_UNPROCESSABLE_ENTITY)
|
HTTP_UNPROCESSABLE_ENTITY)
|
||||||
|
|
||||||
event_origin = ha.EventOrigin.remote
|
event_origin = ha.EventOrigin.remote
|
||||||
|
|
||||||
|
@ -464,7 +465,7 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||||
|
|
||||||
self.server.hass.bus.fire(event_type, event_data, event_origin)
|
self.server.hass.bus.fire(event_type, event_data, event_origin)
|
||||||
|
|
||||||
self._message("Event {} fired.".format(event_type))
|
self._json_message("Event {} fired.".format(event_type))
|
||||||
|
|
||||||
def _handle_get_api_services(self, path_match, data):
|
def _handle_get_api_services(self, path_match, data):
|
||||||
""" Handles getting overview of services. """
|
""" Handles getting overview of services. """
|
||||||
|
@ -483,9 +484,10 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||||
domain = path_match.group('domain')
|
domain = path_match.group('domain')
|
||||||
service = path_match.group('service')
|
service = path_match.group('service')
|
||||||
|
|
||||||
self.server.hass.call_service(domain, service, data)
|
with TrackStates(self.server.hass) as changed_states:
|
||||||
|
self.server.hass.services.call(domain, service, data, True)
|
||||||
|
|
||||||
self._message("Service {}/{} called.".format(domain, service))
|
self._write_json(changed_states)
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
def _handle_post_api_event_forward(self, path_match, data):
|
def _handle_post_api_event_forward(self, path_match, data):
|
||||||
|
@ -495,26 +497,31 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||||
host = data['host']
|
host = data['host']
|
||||||
api_password = data['api_password']
|
api_password = data['api_password']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self._message("No host or api_password received.",
|
self._json_message("No host or api_password received.",
|
||||||
HTTP_BAD_REQUEST)
|
HTTP_BAD_REQUEST)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
port = int(data['port']) if 'port' in data else None
|
port = int(data['port']) if 'port' in data else None
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self._message(
|
self._json_message(
|
||||||
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
|
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
api = rem.API(host, api_password, port)
|
||||||
|
|
||||||
|
if not api.validate_api():
|
||||||
|
self._json_message(
|
||||||
|
"Unable to validate API", HTTP_UNPROCESSABLE_ENTITY)
|
||||||
|
return
|
||||||
|
|
||||||
if self.server.event_forwarder is None:
|
if self.server.event_forwarder is None:
|
||||||
self.server.event_forwarder = \
|
self.server.event_forwarder = \
|
||||||
rem.EventForwarder(self.server.hass)
|
rem.EventForwarder(self.server.hass)
|
||||||
|
|
||||||
api = rem.API(host, api_password, port)
|
|
||||||
|
|
||||||
self.server.event_forwarder.connect(api)
|
self.server.event_forwarder.connect(api)
|
||||||
|
|
||||||
self._message("Event forwarding setup.")
|
self._json_message("Event forwarding setup.")
|
||||||
|
|
||||||
def _handle_delete_api_event_forward(self, path_match, data):
|
def _handle_delete_api_event_forward(self, path_match, data):
|
||||||
""" Handles deleting an event forwarding target. """
|
""" Handles deleting an event forwarding target. """
|
||||||
|
@ -522,14 +529,14 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||||
try:
|
try:
|
||||||
host = data['host']
|
host = data['host']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self._message("No host received.",
|
self._json_message("No host received.",
|
||||||
HTTP_BAD_REQUEST)
|
HTTP_BAD_REQUEST)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
port = int(data['port']) if 'port' in data else None
|
port = int(data['port']) if 'port' in data else None
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self._message(
|
self._json_message(
|
||||||
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
|
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -538,7 +545,7 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||||
|
|
||||||
self.server.event_forwarder.disconnect(api)
|
self.server.event_forwarder.disconnect(api)
|
||||||
|
|
||||||
self._message("Event forwarding cancelled.")
|
self._json_message("Event forwarding cancelled.")
|
||||||
|
|
||||||
def _handle_get_static(self, path_match, data):
|
def _handle_get_static(self, path_match, data):
|
||||||
""" Returns a static file. """
|
""" Returns a static file. """
|
||||||
|
@ -585,7 +592,10 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||||
|
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|
||||||
if do_gzip:
|
if self.command == 'HEAD':
|
||||||
|
return
|
||||||
|
|
||||||
|
elif do_gzip:
|
||||||
self.wfile.write(gzip_data)
|
self.wfile.write(gzip_data)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
@ -599,22 +609,9 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||||
if inp:
|
if inp:
|
||||||
inp.close()
|
inp.close()
|
||||||
|
|
||||||
def _message(self, message, status_code=HTTP_OK):
|
def _json_message(self, message, status_code=HTTP_OK):
|
||||||
""" Helper method to return a message to the caller. """
|
""" Helper method to return a message to the caller. """
|
||||||
if self.use_json:
|
self._write_json({'message': message}, status_code=status_code)
|
||||||
self._write_json({'message': message}, status_code=status_code)
|
|
||||||
else:
|
|
||||||
self.send_error(status_code, message)
|
|
||||||
|
|
||||||
def _redirect(self, location):
|
|
||||||
""" Helper method to redirect caller. """
|
|
||||||
self.send_response(HTTP_MOVED_PERMANENTLY)
|
|
||||||
|
|
||||||
self.send_header(
|
|
||||||
"Location", "{}?api_password={}".format(
|
|
||||||
location, self.server.api_password))
|
|
||||||
|
|
||||||
self.end_headers()
|
|
||||||
|
|
||||||
def _write_json(self, data=None, status_code=HTTP_OK, location=None):
|
def _write_json(self, data=None, status_code=HTTP_OK, location=None):
|
||||||
""" Helper method to return JSON to the caller. """
|
""" Helper method to return JSON to the caller. """
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
""" DO NOT MODIFY. Auto-generated by build_frontend script """
|
""" DO NOT MODIFY. Auto-generated by build_frontend script """
|
||||||
VERSION = "12ba7bca8ad0c196cb04ada4fe85a76b"
|
VERSION = "78343829ea70bf07a9e939b321587122"
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -11,27 +11,28 @@
|
||||||
"bower_components"
|
"bower_components"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"webcomponentsjs": "Polymer/webcomponentsjs#~0.5.1",
|
"webcomponentsjs": "Polymer/webcomponentsjs#~0.5.2",
|
||||||
"font-roboto": "Polymer/font-roboto#~0.5.1",
|
"font-roboto": "Polymer/font-roboto#~0.5.2",
|
||||||
"core-header-panel": "Polymer/core-header-panel#~0.5.1",
|
"core-header-panel": "Polymer/core-header-panel#~0.5.2",
|
||||||
"core-toolbar": "Polymer/core-toolbar#~0.5.1",
|
"core-toolbar": "Polymer/core-toolbar#~0.5.2",
|
||||||
"core-tooltip": "Polymer/core-tooltip#~0.5.1",
|
"core-tooltip": "Polymer/core-tooltip#~0.5.2",
|
||||||
"core-menu": "Polymer/core-menu#~0.5.1",
|
"core-menu": "Polymer/core-menu#~0.5.2",
|
||||||
"core-item": "Polymer/core-item#~0.5.1",
|
"core-item": "Polymer/core-item#~0.5.2",
|
||||||
"core-input": "Polymer/core-input#~0.5.1",
|
"core-input": "Polymer/core-input#~0.5.2",
|
||||||
"core-icons": "polymer/core-icons#~0.5.1",
|
"core-icons": "polymer/core-icons#~0.5.2",
|
||||||
"core-image": "polymer/core-image#~0.5.1",
|
"core-image": "polymer/core-image#~0.5.2",
|
||||||
"paper-toast": "Polymer/paper-toast#~0.5.1",
|
"paper-toast": "Polymer/paper-toast#~0.5.2",
|
||||||
"paper-dialog": "Polymer/paper-dialog#~0.5.1",
|
"paper-dialog": "Polymer/paper-dialog#~0.5.2",
|
||||||
"paper-spinner": "Polymer/paper-spinner#~0.5.1",
|
"paper-spinner": "Polymer/paper-spinner#~0.5.2",
|
||||||
"paper-button": "Polymer/paper-button#~0.5.1",
|
"paper-button": "Polymer/paper-button#~0.5.2",
|
||||||
"paper-input": "Polymer/paper-input#~0.5.1",
|
"paper-input": "Polymer/paper-input#~0.5.2",
|
||||||
"paper-toggle-button": "polymer/paper-toggle-button#~0.5.1",
|
"paper-toggle-button": "polymer/paper-toggle-button#~0.5.2",
|
||||||
"paper-tabs": "polymer/paper-tabs#~0.5.1",
|
"paper-tabs": "polymer/paper-tabs#~0.5.2",
|
||||||
"paper-icon-button": "polymer/paper-icon-button#~0.5.1",
|
"paper-icon-button": "polymer/paper-icon-button#~0.5.2",
|
||||||
"paper-menu-button": "polymer/paper-menu-button#~0.5.1",
|
"paper-menu-button": "polymer/paper-menu-button#~0.5.2",
|
||||||
"paper-dropdown": "polymer/paper-dropdown#~0.5.1",
|
"paper-dropdown": "polymer/paper-dropdown#~0.5.2",
|
||||||
"paper-item": "polymer/paper-item#~0.5.1",
|
"paper-item": "polymer/paper-item#~0.5.2",
|
||||||
"moment": "~2.8.3"
|
"moment": "~2.8.4",
|
||||||
|
"core-style": "polymer/core-style#~0.5.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,23 +28,28 @@
|
||||||
return "image:flash-on";
|
return "image:flash-on";
|
||||||
|
|
||||||
case "chromecast":
|
case "chromecast":
|
||||||
if(state && state != "idle") {
|
var icon = "hardware:cast";
|
||||||
return "hardware:cast-connected";
|
|
||||||
} else {
|
if (state !== "idle") {
|
||||||
return "hardware:cast";
|
icon += "-connected";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return icon;
|
||||||
|
|
||||||
case "process":
|
case "process":
|
||||||
return "hardware:memory"
|
return "hardware:memory";
|
||||||
|
|
||||||
case "sun":
|
case "sun":
|
||||||
return "image:wb-sunny"
|
return "image:wb-sunny";
|
||||||
|
|
||||||
case "light":
|
case "light":
|
||||||
return "image:wb-incandescent"
|
return "image:wb-incandescent";
|
||||||
|
|
||||||
case "tellstick_sensor":
|
case "tellstick_sensor":
|
||||||
return "trending-up";
|
return "trending-up";
|
||||||
|
|
||||||
|
case "simple_alarm":
|
||||||
|
return "social:notifications";
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return "bookmark-outline";
|
return "bookmark-outline";
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.defineProperties(State.prototype, {
|
Object.defineProperties(State.prototype, {
|
||||||
"stateDisplay": {
|
stateDisplay: {
|
||||||
get: function() {
|
get: function() {
|
||||||
var state = this.state.replace(/_/g, " ");
|
var state = this.state.replace(/_/g, " ");
|
||||||
if(this.attributes.unit_of_measurement) {
|
if(this.attributes.unit_of_measurement) {
|
||||||
|
@ -46,19 +46,30 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"isCustomGroup": {
|
isCustomGroup: {
|
||||||
get: function() {
|
get: function() {
|
||||||
return this.domain == "group" && !this.attributes.auto;
|
return this.domain == "group" && !this.attributes.auto;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"canToggle": {
|
canToggle: {
|
||||||
get: function() {
|
get: function() {
|
||||||
// groups that have the on/off state or if there is a turn_on service
|
// groups that have the on/off state or if there is a turn_on service
|
||||||
return ((this.domain == 'group' &&
|
return ((this.domain == 'group' &&
|
||||||
(this.state == 'on' || this.state == 'off')) ||
|
(this.state == 'on' || this.state == 'off')) ||
|
||||||
this.api.hasService(this.domain, 'turn_on'));
|
this.api.hasService(this.domain, 'turn_on'));
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// how to render the card for this state
|
||||||
|
cardType: {
|
||||||
|
get: function() {
|
||||||
|
if(this.canToggle) {
|
||||||
|
return "toggle";
|
||||||
|
} else {
|
||||||
|
return "display";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -69,10 +80,6 @@
|
||||||
events: [],
|
events: [],
|
||||||
stateUpdateTimeout: null,
|
stateUpdateTimeout: null,
|
||||||
|
|
||||||
computed: {
|
|
||||||
ha_headers: '{"HA-access": auth}'
|
|
||||||
},
|
|
||||||
|
|
||||||
created: function() {
|
created: function() {
|
||||||
this.api = this;
|
this.api = this;
|
||||||
|
|
||||||
|
@ -116,7 +123,7 @@
|
||||||
} else {
|
} else {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_pushNewState: function(new_state) {
|
_pushNewState: function(new_state) {
|
||||||
|
@ -140,7 +147,13 @@
|
||||||
this._sortStates(this.states);
|
this._sortStates(this.states);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fire('states-updated')
|
this.fire('states-updated');
|
||||||
|
},
|
||||||
|
|
||||||
|
_pushNewStates: function(new_states) {
|
||||||
|
new_states.map(function(state) {
|
||||||
|
this._pushNewState(state);
|
||||||
|
}.bind(this));
|
||||||
},
|
},
|
||||||
|
|
||||||
// call api methods
|
// call api methods
|
||||||
|
@ -153,7 +166,7 @@
|
||||||
fetchState: function(entityId) {
|
fetchState: function(entityId) {
|
||||||
var successStateUpdate = function(new_state) {
|
var successStateUpdate = function(new_state) {
|
||||||
this._pushNewState(new_state);
|
this._pushNewState(new_state);
|
||||||
}
|
};
|
||||||
|
|
||||||
this.call_api("GET", "states/" + entityId, null, successStateUpdate.bind(this));
|
this.call_api("GET", "states/" + entityId, null, successStateUpdate.bind(this));
|
||||||
},
|
},
|
||||||
|
@ -166,14 +179,14 @@
|
||||||
return new State(json, this);
|
return new State(json, this);
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
|
|
||||||
this.fire('states-updated')
|
this.fire('states-updated');
|
||||||
|
|
||||||
this._laterFetchStates();
|
this._laterFetchStates();
|
||||||
|
|
||||||
if(onSuccess) {
|
if(onSuccess) {
|
||||||
onSuccess(this.states);
|
onSuccess(this.states);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
this.call_api(
|
this.call_api(
|
||||||
"GET", "states", null, successStatesUpdate.bind(this), onError);
|
"GET", "states", null, successStatesUpdate.bind(this), onError);
|
||||||
|
@ -183,12 +196,12 @@
|
||||||
var successEventsUpdated = function(events) {
|
var successEventsUpdated = function(events) {
|
||||||
this.events = events;
|
this.events = events;
|
||||||
|
|
||||||
this.fire('events-updated')
|
this.fire('events-updated');
|
||||||
|
|
||||||
if(onSuccess) {
|
if(onSuccess) {
|
||||||
onSuccess(events);
|
onSuccess(events);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
this.call_api(
|
this.call_api(
|
||||||
"GET", "events", null, successEventsUpdated.bind(this), onError);
|
"GET", "events", null, successEventsUpdated.bind(this), onError);
|
||||||
|
@ -198,27 +211,29 @@
|
||||||
var successServicesUpdated = function(services) {
|
var successServicesUpdated = function(services) {
|
||||||
this.services = services;
|
this.services = services;
|
||||||
|
|
||||||
this.fire('services-updated')
|
this.fire('services-updated');
|
||||||
|
|
||||||
if(onSuccess) {
|
if(onSuccess) {
|
||||||
onSuccess(this.services);
|
onSuccess(this.services);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
this.call_api(
|
this.call_api(
|
||||||
"GET", "services", null, successServicesUpdated.bind(this), onError);
|
"GET", "services", null, successServicesUpdated.bind(this), onError);
|
||||||
},
|
},
|
||||||
|
|
||||||
turn_on: function(entity_id) {
|
turn_on: function(entity_id, options) {
|
||||||
this.call_service("homeassistant", "turn_on", {entity_id: entity_id});
|
this.call_service(
|
||||||
|
"homeassistant", "turn_on", {entity_id: entity_id}, options);
|
||||||
},
|
},
|
||||||
|
|
||||||
turn_off: function(entity_id) {
|
turn_off: function(entity_id, options) {
|
||||||
this.call_service("homeassistant", "turn_off", {entity_id: entity_id})
|
this.call_service(
|
||||||
|
"homeassistant", "turn_off", {entity_id: entity_id}, options);
|
||||||
},
|
},
|
||||||
|
|
||||||
set_state: function(entity_id, state, attributes) {
|
set_state: function(entity_id, state, attributes) {
|
||||||
var payload = {state: state}
|
var payload = {state: state};
|
||||||
|
|
||||||
if(attributes) {
|
if(attributes) {
|
||||||
payload.attributes = attributes;
|
payload.attributes = attributes;
|
||||||
|
@ -227,16 +242,17 @@
|
||||||
var successToast = function(new_state) {
|
var successToast = function(new_state) {
|
||||||
this.showToast("State of "+entity_id+" set to "+state+".");
|
this.showToast("State of "+entity_id+" set to "+state+".");
|
||||||
this._pushNewState(new_state);
|
this._pushNewState(new_state);
|
||||||
}
|
};
|
||||||
|
|
||||||
this.call_api("POST", "states/" + entity_id,
|
this.call_api("POST", "states/" + entity_id,
|
||||||
payload, successToast.bind(this));
|
payload, successToast.bind(this));
|
||||||
},
|
},
|
||||||
|
|
||||||
call_service: function(domain, service, parameters) {
|
call_service: function(domain, service, parameters, options) {
|
||||||
parameters = parameters || {};
|
parameters = parameters || {};
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
var successToast = function() {
|
var successHandler = function(changed_states) {
|
||||||
if(service == "turn_on" && parameters.entity_id) {
|
if(service == "turn_on" && parameters.entity_id) {
|
||||||
this.showToast("Turned on " + parameters.entity_id + '.');
|
this.showToast("Turned on " + parameters.entity_id + '.');
|
||||||
} else if(service == "turn_off" && parameters.entity_id) {
|
} else if(service == "turn_off" && parameters.entity_id) {
|
||||||
|
@ -245,30 +261,21 @@
|
||||||
this.showToast("Service "+domain+"/"+service+" called.");
|
this.showToast("Service "+domain+"/"+service+" called.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we call a service on an entity_id, update the state
|
this._pushNewStates(changed_states);
|
||||||
if(parameters && parameters.entity_id) {
|
|
||||||
var update_func;
|
|
||||||
|
|
||||||
// if entity_id is a string, update 1 state, else all.
|
if(options.success) {
|
||||||
if(typeof(parameters.entity_id === "string")) {
|
options.success();
|
||||||
// if it is a group, fetch all
|
|
||||||
if(parameters.entity_id.slice(0,6) == "group.") {
|
|
||||||
update_func = this.fetchStates
|
|
||||||
} else {
|
|
||||||
update_func = function() {
|
|
||||||
this.fetchState(parameters.entity_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
update_func = this.fetchStates
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(update_func.bind(this), 1000);
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
var errorHandler = function(error_data) {
|
||||||
|
if(options.error) {
|
||||||
|
options.error(error_data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
this.call_api("POST", "services/" + domain + "/" + service,
|
this.call_api("POST", "services/" + domain + "/" + service,
|
||||||
parameters, successToast.bind(this));
|
parameters, successHandler.bind(this), errorHandler);
|
||||||
},
|
},
|
||||||
|
|
||||||
fire_event: function(eventType, eventData) {
|
fire_event: function(eventType, eventData) {
|
||||||
|
@ -276,16 +283,26 @@
|
||||||
|
|
||||||
var successToast = function() {
|
var successToast = function() {
|
||||||
this.showToast("Event "+eventType+" fired.");
|
this.showToast("Event "+eventType+" fired.");
|
||||||
}
|
};
|
||||||
|
|
||||||
this.call_api("POST", "events/" + eventType,
|
this.call_api("POST", "events/" + eventType,
|
||||||
eventData, successToast.bind(this));
|
eventData, successToast.bind(this));
|
||||||
},
|
},
|
||||||
|
|
||||||
call_api: function(method, path, parameters, onSuccess, onError) {
|
call_api: function(method, path, parameters, onSuccess, onError) {
|
||||||
|
var url = "/api/" + path;
|
||||||
|
|
||||||
|
// set to true to generate a frontend to be used as demo on the website
|
||||||
|
if (false) {
|
||||||
|
if (path === "states" || path === "services" || path === "events") {
|
||||||
|
url = "/demo/" + path + ".json";
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
var req = new XMLHttpRequest();
|
var req = new XMLHttpRequest();
|
||||||
req.open(method, "/api/" + path, true)
|
req.open(method, url, true);
|
||||||
req.setRequestHeader("HA-access", this.auth);
|
req.setRequestHeader("X-HA-access", this.auth);
|
||||||
|
|
||||||
req.onreadystatechange = function() {
|
req.onreadystatechange = function() {
|
||||||
|
|
||||||
|
@ -303,7 +320,7 @@
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}.bind(this)
|
}.bind(this);
|
||||||
|
|
||||||
if(parameters) {
|
if(parameters) {
|
||||||
req.send(JSON.stringify(parameters));
|
req.send(JSON.stringify(parameters));
|
||||||
|
@ -316,7 +333,7 @@
|
||||||
showEditStateDialog: function(entityId) {
|
showEditStateDialog: function(entityId) {
|
||||||
var state = this.getState(entityId);
|
var state = this.getState(entityId);
|
||||||
|
|
||||||
this.showSetStateDialog(entityId, state.state, state.attributes)
|
this.showSetStateDialog(entityId, state.state, state.attributes);
|
||||||
},
|
},
|
||||||
|
|
||||||
showSetStateDialog: function(entityId, state, stateAttributes) {
|
showSetStateDialog: function(entityId, state, stateAttributes) {
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
|
|
||||||
/* Color the icon if light or sun is on */
|
/* Color the icon if light or sun is on */
|
||||||
domain-icon[data-domain=light][data-state=on],
|
domain-icon[data-domain=light][data-state=on],
|
||||||
|
domain-icon[data-domain=switch][data-state=on],
|
||||||
domain-icon[data-domain=sun][data-state=above_horizon] {
|
domain-icon[data-domain=sun][data-state=above_horizon] {
|
||||||
color: #fff176;
|
color: #fff176;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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="bower_components/polymer/polymer.html">
|
||||||
<link rel="import" href="state-card.html">
|
<link rel="import" href="bower_components/core-style/core-style.html">
|
||||||
|
<link rel="import" href="state-card-display.html">
|
||||||
|
<link rel="import" href="state-card-toggle.html">
|
||||||
|
|
||||||
<polymer-element name="states-cards" attributes="api filter">
|
<polymer-element name="states-cards" attributes="api filter">
|
||||||
<template>
|
<template>
|
||||||
|
@ -9,45 +11,68 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
state-card {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media all and (min-width: 764px) {
|
@media all and (min-width: 764px) {
|
||||||
:host {
|
:host {
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
state-card {
|
.state-card {
|
||||||
width: calc(50% - 44px);
|
width: calc(50% - 44px);
|
||||||
margin: 8px 0 0 8px;
|
margin: 8px 0 0 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and (min-width: 1100px) {
|
@media all and (min-width: 1100px) {
|
||||||
state-card {
|
.state-card {
|
||||||
width: calc(33% - 38px);
|
width: calc(33% - 38px);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and (min-width: 1450px) {
|
@media all and (min-width: 1450px) {
|
||||||
state-card {
|
.state-card {
|
||||||
width: calc(25% - 42px);
|
width: calc(25% - 42px);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<core-style id="state-card">
|
||||||
|
<!-- generic state card CSS -->
|
||||||
|
:host {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.098) 0px 2px 4px, rgba(0, 0, 0, 0.098) 0px 0px 3px;
|
||||||
|
transition: all 0.30s ease-out;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
background-color: white;
|
||||||
|
padding: 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</core-style>
|
||||||
|
|
||||||
<div horizontal layout wrap>
|
<div horizontal layout wrap>
|
||||||
<template repeat="{{state in getStates(api.states, filter)}}">
|
|
||||||
<state-card
|
<template id="display">
|
||||||
|
<state-card-display
|
||||||
|
class='state-card'
|
||||||
|
stateObj="{{state}}"
|
||||||
|
cb_edit={{editCallback}}>
|
||||||
|
</state-card-display>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="toggle">
|
||||||
|
<state-card-toggle
|
||||||
|
class='state-card'
|
||||||
stateObj="{{state}}"
|
stateObj="{{state}}"
|
||||||
cb_turn_on="{{api.turn_on}}"
|
cb_turn_on="{{api.turn_on}}"
|
||||||
cb_turn_off="{{api.turn_off}}"
|
cb_turn_off="{{api.turn_off}}"
|
||||||
cb_edit={{editCallback}}>
|
cb_edit={{editCallback}}>
|
||||||
</state-card>
|
</state-card-display>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template repeat="{{state in getStates(api.states, filter)}}">
|
||||||
|
<template bind ref="{{state.cardType}}"></template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,12 +1,16 @@
|
||||||
"""
|
"""
|
||||||
homeassistant.components.keyboard
|
homeassistant.keyboard
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Provides functionality to emulate keyboard presses on host machine.
|
Provides functionality to emulate keyboard presses on host machine.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import homeassistant.components as components
|
from homeassistant.const import (
|
||||||
|
SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE,
|
||||||
|
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK,
|
||||||
|
SERVICE_MEDIA_PLAY_PAUSE)
|
||||||
|
|
||||||
|
|
||||||
DOMAIN = "keyboard"
|
DOMAIN = "keyboard"
|
||||||
DEPENDENCIES = []
|
DEPENDENCIES = []
|
||||||
|
@ -14,32 +18,32 @@ DEPENDENCIES = []
|
||||||
|
|
||||||
def volume_up(hass):
|
def volume_up(hass):
|
||||||
""" Press the keyboard button for volume up. """
|
""" Press the keyboard button for volume up. """
|
||||||
hass.call_service(DOMAIN, components.SERVICE_VOLUME_UP)
|
hass.services.call(DOMAIN, SERVICE_VOLUME_UP)
|
||||||
|
|
||||||
|
|
||||||
def volume_down(hass):
|
def volume_down(hass):
|
||||||
""" Press the keyboard button for volume down. """
|
""" Press the keyboard button for volume down. """
|
||||||
hass.call_service(DOMAIN, components.SERVICE_VOLUME_DOWN)
|
hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN)
|
||||||
|
|
||||||
|
|
||||||
def volume_mute(hass):
|
def volume_mute(hass):
|
||||||
""" Press the keyboard button for muting volume. """
|
""" Press the keyboard button for muting volume. """
|
||||||
hass.call_service(DOMAIN, components.SERVICE_VOLUME_MUTE)
|
hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE)
|
||||||
|
|
||||||
|
|
||||||
def media_play_pause(hass):
|
def media_play_pause(hass):
|
||||||
""" Press the keyboard button for play/pause. """
|
""" Press the keyboard button for play/pause. """
|
||||||
hass.call_service(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE)
|
hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE)
|
||||||
|
|
||||||
|
|
||||||
def media_next_track(hass):
|
def media_next_track(hass):
|
||||||
""" Press the keyboard button for next track. """
|
""" Press the keyboard button for next track. """
|
||||||
hass.call_service(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK)
|
hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK)
|
||||||
|
|
||||||
|
|
||||||
def media_prev_track(hass):
|
def media_prev_track(hass):
|
||||||
""" Press the keyboard button for prev track. """
|
""" Press the keyboard button for prev track. """
|
||||||
hass.call_service(DOMAIN, components.SERVICE_MEDIA_PREV_TRACK)
|
hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
|
@ -56,27 +60,27 @@ def setup(hass, config):
|
||||||
keyboard = pykeyboard.PyKeyboard()
|
keyboard = pykeyboard.PyKeyboard()
|
||||||
keyboard.special_key_assignment()
|
keyboard.special_key_assignment()
|
||||||
|
|
||||||
hass.services.register(DOMAIN, components.SERVICE_VOLUME_UP,
|
hass.services.register(DOMAIN, SERVICE_VOLUME_UP,
|
||||||
lambda service:
|
lambda service:
|
||||||
keyboard.tap_key(keyboard.volume_up_key))
|
keyboard.tap_key(keyboard.volume_up_key))
|
||||||
|
|
||||||
hass.services.register(DOMAIN, components.SERVICE_VOLUME_DOWN,
|
hass.services.register(DOMAIN, SERVICE_VOLUME_DOWN,
|
||||||
lambda service:
|
lambda service:
|
||||||
keyboard.tap_key(keyboard.volume_down_key))
|
keyboard.tap_key(keyboard.volume_down_key))
|
||||||
|
|
||||||
hass.services.register(DOMAIN, components.SERVICE_VOLUME_MUTE,
|
hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE,
|
||||||
lambda service:
|
lambda service:
|
||||||
keyboard.tap_key(keyboard.volume_mute_key))
|
keyboard.tap_key(keyboard.volume_mute_key))
|
||||||
|
|
||||||
hass.services.register(DOMAIN, components.SERVICE_MEDIA_PLAY_PAUSE,
|
hass.services.register(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE,
|
||||||
lambda service:
|
lambda service:
|
||||||
keyboard.tap_key(keyboard.media_play_pause_key))
|
keyboard.tap_key(keyboard.media_play_pause_key))
|
||||||
|
|
||||||
hass.services.register(DOMAIN, components.SERVICE_MEDIA_NEXT_TRACK,
|
hass.services.register(DOMAIN, SERVICE_MEDIA_NEXT_TRACK,
|
||||||
lambda service:
|
lambda service:
|
||||||
keyboard.tap_key(keyboard.media_next_track_key))
|
keyboard.tap_key(keyboard.media_next_track_key))
|
||||||
|
|
||||||
hass.services.register(DOMAIN, components.SERVICE_MEDIA_PREV_TRACK,
|
hass.services.register(DOMAIN, SERVICE_MEDIA_PREV_TRACK,
|
||||||
lambda service:
|
lambda service:
|
||||||
keyboard.tap_key(keyboard.media_prev_track_key))
|
keyboard.tap_key(keyboard.media_prev_track_key))
|
||||||
|
|
||||||
|
|
|
@ -52,12 +52,12 @@ import logging
|
||||||
import os
|
import os
|
||||||
import csv
|
import csv
|
||||||
|
|
||||||
import homeassistant as ha
|
|
||||||
from homeassistant.loader import get_component
|
|
||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
from homeassistant.components import (
|
from homeassistant.const import (
|
||||||
group, extract_entity_ids, STATE_ON,
|
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
|
||||||
SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
|
from homeassistant.helpers import (
|
||||||
|
extract_entity_ids, platform_devices_from_config)
|
||||||
|
from homeassistant.components import group
|
||||||
|
|
||||||
|
|
||||||
DOMAIN = "light"
|
DOMAIN = "light"
|
||||||
|
@ -82,6 +82,12 @@ ATTR_BRIGHTNESS = "brightness"
|
||||||
# String representing a profile (built-in ones or external defined)
|
# String representing a profile (built-in ones or external defined)
|
||||||
ATTR_PROFILE = "profile"
|
ATTR_PROFILE = "profile"
|
||||||
|
|
||||||
|
# If the light should flash, can be FLASH_SHORT or FLASH_LONG
|
||||||
|
ATTR_FLASH = "flash"
|
||||||
|
FLASH_SHORT = "short"
|
||||||
|
FLASH_LONG = "long"
|
||||||
|
|
||||||
|
|
||||||
LIGHT_PROFILES_FILE = "light_profiles.csv"
|
LIGHT_PROFILES_FILE = "light_profiles.csv"
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -96,51 +102,39 @@ def is_on(hass, entity_id=None):
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
# pylint: disable=too-many-arguments
|
||||||
def turn_on(hass, entity_id=None, transition=None, brightness=None,
|
def turn_on(hass, entity_id=None, transition=None, brightness=None,
|
||||||
rgb_color=None, xy_color=None, profile=None):
|
rgb_color=None, xy_color=None, profile=None, flash=None):
|
||||||
""" Turns all or specified light on. """
|
""" Turns all or specified light on. """
|
||||||
data = {}
|
data = {
|
||||||
|
key: value for key, value in [
|
||||||
|
(ATTR_ENTITY_ID, entity_id),
|
||||||
|
(ATTR_PROFILE, profile),
|
||||||
|
(ATTR_TRANSITION, transition),
|
||||||
|
(ATTR_BRIGHTNESS, brightness),
|
||||||
|
(ATTR_RGB_COLOR, rgb_color),
|
||||||
|
(ATTR_XY_COLOR, xy_color),
|
||||||
|
(ATTR_FLASH, flash),
|
||||||
|
] if value is not None
|
||||||
|
}
|
||||||
|
|
||||||
if entity_id:
|
hass.services.call(DOMAIN, SERVICE_TURN_ON, data)
|
||||||
data[ATTR_ENTITY_ID] = entity_id
|
|
||||||
|
|
||||||
if profile:
|
|
||||||
data[ATTR_PROFILE] = profile
|
|
||||||
|
|
||||||
if transition is not None:
|
|
||||||
data[ATTR_TRANSITION] = transition
|
|
||||||
|
|
||||||
if brightness is not None:
|
|
||||||
data[ATTR_BRIGHTNESS] = brightness
|
|
||||||
|
|
||||||
if rgb_color:
|
|
||||||
data[ATTR_RGB_COLOR] = rgb_color
|
|
||||||
|
|
||||||
if xy_color:
|
|
||||||
data[ATTR_XY_COLOR] = xy_color
|
|
||||||
|
|
||||||
hass.call_service(DOMAIN, SERVICE_TURN_ON, data)
|
|
||||||
|
|
||||||
|
|
||||||
def turn_off(hass, entity_id=None, transition=None):
|
def turn_off(hass, entity_id=None, transition=None):
|
||||||
""" Turns all or specified light off. """
|
""" Turns all or specified light off. """
|
||||||
data = {}
|
data = {
|
||||||
|
key: value for key, value in [
|
||||||
|
(ATTR_ENTITY_ID, entity_id),
|
||||||
|
(ATTR_TRANSITION, transition),
|
||||||
|
] if value is not None
|
||||||
|
}
|
||||||
|
|
||||||
if entity_id:
|
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
|
||||||
data[ATTR_ENTITY_ID] = entity_id
|
|
||||||
|
|
||||||
if transition is not None:
|
|
||||||
data[ATTR_TRANSITION] = transition
|
|
||||||
|
|
||||||
hass.call_service(DOMAIN, SERVICE_TURN_OFF, data)
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-branches, too-many-locals
|
# pylint: disable=too-many-branches, too-many-locals
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
""" Exposes light control via statemachine and services. """
|
""" Exposes light control via statemachine and services. """
|
||||||
|
|
||||||
if not util.validate_config(config, {DOMAIN: [ha.CONF_TYPE]}, _LOGGER):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Load built-in profiles and custom profiles
|
# Load built-in profiles and custom profiles
|
||||||
profile_paths = [os.path.join(os.path.dirname(__file__),
|
profile_paths = [os.path.join(os.path.dirname(__file__),
|
||||||
LIGHT_PROFILES_FILE),
|
LIGHT_PROFILES_FILE),
|
||||||
|
@ -169,20 +163,9 @@ def setup(hass, config):
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Load platform
|
lights = platform_devices_from_config(config, DOMAIN, hass, _LOGGER)
|
||||||
light_type = config[DOMAIN][ha.CONF_TYPE]
|
|
||||||
|
|
||||||
light_init = get_component('light.{}'.format(light_type))
|
if not lights:
|
||||||
|
|
||||||
if light_init is None:
|
|
||||||
_LOGGER.error("Unknown light type specified: %s", light_type)
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
lights = light_init.get_lights(hass, config[DOMAIN])
|
|
||||||
|
|
||||||
if len(lights) == 0:
|
|
||||||
_LOGGER.error("No lights found")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
ent_to_light = {}
|
ent_to_light = {}
|
||||||
|
@ -198,7 +181,7 @@ def setup(hass, config):
|
||||||
|
|
||||||
entity_id = util.ensure_unique_string(
|
entity_id = util.ensure_unique_string(
|
||||||
ENTITY_ID_FORMAT.format(util.slugify(name)),
|
ENTITY_ID_FORMAT.format(util.slugify(name)),
|
||||||
list(ent_to_light.keys()))
|
ent_to_light.keys())
|
||||||
|
|
||||||
light.entity_id = entity_id
|
light.entity_id = entity_id
|
||||||
ent_to_light[entity_id] = light
|
ent_to_light[entity_id] = light
|
||||||
|
@ -249,7 +232,6 @@ def setup(hass, config):
|
||||||
profile = profiles.get(dat.get(ATTR_PROFILE))
|
profile = profiles.get(dat.get(ATTR_PROFILE))
|
||||||
|
|
||||||
if profile:
|
if profile:
|
||||||
# *color, bright = profile
|
|
||||||
*params[ATTR_XY_COLOR], params[ATTR_BRIGHTNESS] = profile
|
*params[ATTR_XY_COLOR], params[ATTR_BRIGHTNESS] = profile
|
||||||
|
|
||||||
if ATTR_BRIGHTNESS in dat:
|
if ATTR_BRIGHTNESS in dat:
|
||||||
|
@ -288,6 +270,13 @@ def setup(hass, config):
|
||||||
# ValueError if not all values can be converted to int
|
# ValueError if not all values can be converted to int
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
if ATTR_FLASH in dat:
|
||||||
|
if dat[ATTR_FLASH] == FLASH_SHORT:
|
||||||
|
params[ATTR_FLASH] = FLASH_SHORT
|
||||||
|
|
||||||
|
elif dat[ATTR_FLASH] == FLASH_LONG:
|
||||||
|
params[ATTR_FLASH] = FLASH_LONG
|
||||||
|
|
||||||
for light in lights:
|
for light in lights:
|
||||||
# pylint: disable=star-args
|
# pylint: disable=star-args
|
||||||
light.turn_on(**params)
|
light.turn_on(**params)
|
||||||
|
|
|
@ -1,19 +1,22 @@
|
||||||
""" Support for Hue lights. """
|
""" Support for Hue lights. """
|
||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
from datetime import datetime, timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
import homeassistant as ha
|
import homeassistant.util as util
|
||||||
from homeassistant.components import ToggleDevice, ATTR_FRIENDLY_NAME
|
from homeassistant.helpers import ToggleDevice
|
||||||
|
from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_HOST
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_TRANSITION)
|
ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_TRANSITION,
|
||||||
|
ATTR_FLASH, FLASH_LONG, FLASH_SHORT)
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||||
|
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
|
||||||
|
|
||||||
PHUE_CONFIG_FILE = "phue.conf"
|
PHUE_CONFIG_FILE = "phue.conf"
|
||||||
|
|
||||||
|
|
||||||
def get_lights(hass, config):
|
def get_devices(hass, config):
|
||||||
""" Gets the Hue lights. """
|
""" Gets the Hue lights. """
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
try:
|
try:
|
||||||
|
@ -23,7 +26,7 @@ def get_lights(hass, config):
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
host = config.get(ha.CONF_HOST, None)
|
host = config.get(CONF_HOST, None)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
bridge = phue.Bridge(
|
bridge = phue.Bridge(
|
||||||
|
@ -37,25 +40,9 @@ def get_lights(hass, config):
|
||||||
|
|
||||||
lights = {}
|
lights = {}
|
||||||
|
|
||||||
def update_lights(force_reload=False):
|
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
||||||
""" Updates the light states. """
|
def update_lights():
|
||||||
now = datetime.now()
|
""" Updates the Hue light objects with latest info from the bridge. """
|
||||||
|
|
||||||
try:
|
|
||||||
time_scans = now - update_lights.last_updated
|
|
||||||
|
|
||||||
# force_reload == True, return if updated in last second
|
|
||||||
# force_reload == False, return if last update was less then
|
|
||||||
# MIN_TIME_BETWEEN_SCANS ago
|
|
||||||
if force_reload and time_scans.seconds < 1 or \
|
|
||||||
not force_reload and time_scans < MIN_TIME_BETWEEN_SCANS:
|
|
||||||
return
|
|
||||||
except AttributeError:
|
|
||||||
# First time we run last_updated is not set, continue as usual
|
|
||||||
pass
|
|
||||||
|
|
||||||
update_lights.last_updated = now
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
api = bridge.get_api()
|
api = bridge.get_api()
|
||||||
except socket.error:
|
except socket.error:
|
||||||
|
@ -109,6 +96,15 @@ class HueLight(ToggleDevice):
|
||||||
if ATTR_XY_COLOR in kwargs:
|
if ATTR_XY_COLOR in kwargs:
|
||||||
command['xy'] = kwargs[ATTR_XY_COLOR]
|
command['xy'] = kwargs[ATTR_XY_COLOR]
|
||||||
|
|
||||||
|
flash = kwargs.get(ATTR_FLASH)
|
||||||
|
|
||||||
|
if flash == FLASH_LONG:
|
||||||
|
command['alert'] = 'lselect'
|
||||||
|
elif flash == FLASH_SHORT:
|
||||||
|
command['alert'] = 'select'
|
||||||
|
else:
|
||||||
|
command['alert'] = 'none'
|
||||||
|
|
||||||
self.bridge.set_light(self.light_id, command)
|
self.bridge.set_light(self.light_id, command)
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
|
@ -142,4 +138,4 @@ class HueLight(ToggleDevice):
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
""" Synchronize state with bridge. """
|
""" Synchronize state with bridge. """
|
||||||
self.update_lights(True)
|
self.update_lights(no_throttle=True)
|
||||||
|
|
|
@ -10,7 +10,7 @@ Author: Markus Stenberg <fingon@iki.fi>
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from homeassistant.components import STATE_ON, STATE_OFF
|
from homeassistant.const import STATE_ON, STATE_OFF
|
||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
|
|
||||||
DOMAIN = 'process'
|
DOMAIN = 'process'
|
||||||
|
|
|
@ -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
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import homeassistant as ha
|
import homeassistant as ha
|
||||||
import homeassistant.util as util
|
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
|
||||||
|
from homeassistant.helpers import validate_config
|
||||||
|
from homeassistant.util import str_to_datetime, datetime_to_str
|
||||||
|
|
||||||
DEPENDENCIES = []
|
DEPENDENCIES = []
|
||||||
DOMAIN = "sun"
|
DOMAIN = "sun"
|
||||||
|
@ -35,7 +37,7 @@ def next_setting(hass, entity_id=None):
|
||||||
state = hass.states.get(ENTITY_ID)
|
state = hass.states.get(ENTITY_ID)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return util.str_to_datetime(state.attributes[STATE_ATTR_NEXT_SETTING])
|
return str_to_datetime(state.attributes[STATE_ATTR_NEXT_SETTING])
|
||||||
except (AttributeError, KeyError):
|
except (AttributeError, KeyError):
|
||||||
# AttributeError if state is None
|
# AttributeError if state is None
|
||||||
# KeyError if STATE_ATTR_NEXT_SETTING does not exist
|
# KeyError if STATE_ATTR_NEXT_SETTING does not exist
|
||||||
|
@ -49,7 +51,7 @@ def next_rising(hass, entity_id=None):
|
||||||
state = hass.states.get(ENTITY_ID)
|
state = hass.states.get(ENTITY_ID)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return util.str_to_datetime(state.attributes[STATE_ATTR_NEXT_RISING])
|
return str_to_datetime(state.attributes[STATE_ATTR_NEXT_RISING])
|
||||||
except (AttributeError, KeyError):
|
except (AttributeError, KeyError):
|
||||||
# AttributeError if state is None
|
# AttributeError if state is None
|
||||||
# KeyError if STATE_ATTR_NEXT_RISING does not exist
|
# KeyError if STATE_ATTR_NEXT_RISING does not exist
|
||||||
|
@ -60,10 +62,9 @@ def setup(hass, config):
|
||||||
""" Tracks the state of the sun. """
|
""" Tracks the state of the sun. """
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
if not util.validate_config(config,
|
if not validate_config(config,
|
||||||
{ha.DOMAIN: [ha.CONF_LATITUDE,
|
{ha.DOMAIN: [CONF_LATITUDE, CONF_LONGITUDE]},
|
||||||
ha.CONF_LONGITUDE]},
|
logger):
|
||||||
logger):
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -74,8 +75,8 @@ def setup(hass, config):
|
||||||
|
|
||||||
sun = ephem.Sun() # pylint: disable=no-member
|
sun = ephem.Sun() # pylint: disable=no-member
|
||||||
|
|
||||||
latitude = config[ha.DOMAIN][ha.CONF_LATITUDE]
|
latitude = config[ha.DOMAIN][CONF_LATITUDE]
|
||||||
longitude = config[ha.DOMAIN][ha.CONF_LONGITUDE]
|
longitude = config[ha.DOMAIN][CONF_LONGITUDE]
|
||||||
|
|
||||||
# Validate latitude and longitude
|
# Validate latitude and longitude
|
||||||
observer = ephem.Observer()
|
observer = ephem.Observer()
|
||||||
|
@ -123,8 +124,8 @@ def setup(hass, config):
|
||||||
new_state, next_change.strftime("%H:%M"))
|
new_state, next_change.strftime("%H:%M"))
|
||||||
|
|
||||||
state_attributes = {
|
state_attributes = {
|
||||||
STATE_ATTR_NEXT_RISING: util.datetime_to_str(next_rising_dt),
|
STATE_ATTR_NEXT_RISING: datetime_to_str(next_rising_dt),
|
||||||
STATE_ATTR_NEXT_SETTING: util.datetime_to_str(next_setting_dt)
|
STATE_ATTR_NEXT_SETTING: datetime_to_str(next_setting_dt)
|
||||||
}
|
}
|
||||||
|
|
||||||
hass.states.set(ENTITY_ID, new_state, state_attributes)
|
hass.states.set(ENTITY_ID, new_state, state_attributes)
|
||||||
|
|
|
@ -4,14 +4,14 @@ homeassistant.components.switch
|
||||||
Component to interface with various switches that can be controlled remotely.
|
Component to interface with various switches that can be controlled remotely.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
import homeassistant as ha
|
|
||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
from homeassistant.loader import get_component
|
from homeassistant.const import (
|
||||||
from homeassistant.components import (
|
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
|
||||||
group, extract_entity_ids, STATE_ON,
|
from homeassistant.helpers import (
|
||||||
SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
|
extract_entity_ids, platform_devices_from_config)
|
||||||
|
from homeassistant.components import group
|
||||||
|
|
||||||
DOMAIN = 'switch'
|
DOMAIN = 'switch'
|
||||||
DEPENDENCIES = []
|
DEPENDENCIES = []
|
||||||
|
@ -22,10 +22,8 @@ ENTITY_ID_ALL_SWITCHES = group.ENTITY_ID_FORMAT.format(
|
||||||
|
|
||||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||||
|
|
||||||
ATTR_TODAY_KWH = "today_kwh"
|
ATTR_TODAY_MWH = "today_mwh"
|
||||||
ATTR_CURRENT_POWER = "current_power"
|
ATTR_CURRENT_POWER_MWH = "current_power_mwh"
|
||||||
ATTR_TODAY_ON_TIME = "today_on_time"
|
|
||||||
ATTR_TODAY_STANDBY_TIME = "today_standby_time"
|
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||||
|
|
||||||
|
@ -43,37 +41,23 @@ def turn_on(hass, entity_id=None):
|
||||||
""" Turns all or specified switch on. """
|
""" Turns all or specified switch on. """
|
||||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||||
|
|
||||||
hass.call_service(DOMAIN, SERVICE_TURN_ON, data)
|
hass.services.call(DOMAIN, SERVICE_TURN_ON, data)
|
||||||
|
|
||||||
|
|
||||||
def turn_off(hass, entity_id=None):
|
def turn_off(hass, entity_id=None):
|
||||||
""" Turns all or specified switch off. """
|
""" Turns all or specified switch off. """
|
||||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
|
||||||
|
|
||||||
hass.call_service(DOMAIN, SERVICE_TURN_OFF, data)
|
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-branches
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
""" Track states and offer events for switches. """
|
""" Track states and offer events for switches. """
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
if not util.validate_config(config, {DOMAIN: [ha.CONF_TYPE]}, logger):
|
switches = platform_devices_from_config(config, DOMAIN, hass, logger)
|
||||||
return False
|
|
||||||
|
|
||||||
switch_type = config[DOMAIN][ha.CONF_TYPE]
|
if not switches:
|
||||||
|
|
||||||
switch_init = get_component('switch.{}'.format(switch_type))
|
|
||||||
|
|
||||||
if switch_init is None:
|
|
||||||
logger.error("Error loading switch component %s", switch_type)
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
switches = switch_init.get_switches(hass, config[DOMAIN])
|
|
||||||
|
|
||||||
if len(switches) == 0:
|
|
||||||
logger.error("No switches found")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Setup a dict mapping entity IDs to devices
|
# Setup a dict mapping entity IDs to devices
|
||||||
|
@ -90,27 +74,22 @@ def setup(hass, config):
|
||||||
|
|
||||||
entity_id = util.ensure_unique_string(
|
entity_id = util.ensure_unique_string(
|
||||||
ENTITY_ID_FORMAT.format(util.slugify(name)),
|
ENTITY_ID_FORMAT.format(util.slugify(name)),
|
||||||
list(ent_to_switch.keys()))
|
ent_to_switch.keys())
|
||||||
|
|
||||||
switch.entity_id = entity_id
|
switch.entity_id = entity_id
|
||||||
ent_to_switch[entity_id] = switch
|
ent_to_switch[entity_id] = switch
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def update_states(time, force_reload=False):
|
@util.Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||||
|
def update_states(now):
|
||||||
""" Update states of all switches. """
|
""" Update states of all switches. """
|
||||||
|
|
||||||
# First time this method gets called, force_reload should be True
|
logger.info("Updating switch states")
|
||||||
if force_reload or \
|
|
||||||
datetime.now() - update_states.last_updated > \
|
|
||||||
MIN_TIME_BETWEEN_SCANS:
|
|
||||||
|
|
||||||
logger.info("Updating switch states")
|
for switch in switches:
|
||||||
update_states.last_updated = datetime.now()
|
switch.update_ha_state(hass)
|
||||||
|
|
||||||
for switch in switches:
|
update_states(None)
|
||||||
switch.update_ha_state(hass)
|
|
||||||
|
|
||||||
update_states(None, True)
|
|
||||||
|
|
||||||
def handle_switch_service(service):
|
def handle_switch_service(service):
|
||||||
""" Handles calls to the switch services. """
|
""" Handles calls to the switch services. """
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
""" Support for Tellstick switches. """
|
""" Support for Tellstick switches. """
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components import ToggleDevice, ATTR_FRIENDLY_NAME
|
from homeassistant.helpers import ToggleDevice
|
||||||
|
from homeassistant.const import ATTR_FRIENDLY_NAME
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import tellcore.constants as tc_constants
|
import tellcore.constants as tc_constants
|
||||||
|
@ -11,7 +12,7 @@ except ImportError:
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def get_switches(hass, config):
|
def get_devices(hass, config):
|
||||||
""" Find and return Tellstick switches. """
|
""" Find and return Tellstick switches. """
|
||||||
try:
|
try:
|
||||||
import tellcore.telldus as telldus
|
import tellcore.telldus as telldus
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
""" Support for WeMo switchces. """
|
""" Support for WeMo switchces. """
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import homeassistant as ha
|
from homeassistant.helpers import ToggleDevice
|
||||||
from homeassistant.components import ToggleDevice, ATTR_FRIENDLY_NAME
|
from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_HOSTS
|
||||||
|
from homeassistant.components.switch import (
|
||||||
|
ATTR_TODAY_MWH, ATTR_CURRENT_POWER_MWH)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def get_switches(hass, config):
|
def get_devices(hass, config):
|
||||||
""" Find and return WeMo switches. """
|
""" Find and return WeMo switches. """
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -21,9 +23,9 @@ def get_switches(hass, config):
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
if ha.CONF_HOSTS in config:
|
if CONF_HOSTS in config:
|
||||||
switches = (pywemo.device_from_host(host) for host
|
switches = (pywemo.device_from_host(host) for host
|
||||||
in config[ha.CONF_HOSTS].split(","))
|
in config[CONF_HOSTS].split(","))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logging.getLogger(__name__).info("Scanning for WeMo devices")
|
logging.getLogger(__name__).info("Scanning for WeMo devices")
|
||||||
|
@ -38,7 +40,6 @@ class WemoSwitch(ToggleDevice):
|
||||||
""" represents a WeMo switch within home assistant. """
|
""" represents a WeMo switch within home assistant. """
|
||||||
def __init__(self, wemo):
|
def __init__(self, wemo):
|
||||||
self.wemo = wemo
|
self.wemo = wemo
|
||||||
self.state_attr = {ATTR_FRIENDLY_NAME: wemo.name}
|
|
||||||
|
|
||||||
def get_name(self):
|
def get_name(self):
|
||||||
""" Returns the name of the switch if any. """
|
""" Returns the name of the switch if any. """
|
||||||
|
@ -58,4 +59,13 @@ class WemoSwitch(ToggleDevice):
|
||||||
|
|
||||||
def get_state_attributes(self):
|
def get_state_attributes(self):
|
||||||
""" Returns optional state attributes. """
|
""" Returns optional state attributes. """
|
||||||
return self.state_attr
|
if self.wemo.model.startswith('Belkin Insight'):
|
||||||
|
cur_info = self.wemo.insight_params
|
||||||
|
|
||||||
|
return {
|
||||||
|
ATTR_FRIENDLY_NAME: self.wemo.name,
|
||||||
|
ATTR_CURRENT_POWER_MWH: cur_info['currentpower'],
|
||||||
|
ATTR_TODAY_MWH: cur_info['todaymw']
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {ATTR_FRIENDLY_NAME: self.wemo.name}
|
||||||
|
|
|
@ -26,8 +26,7 @@ import logging
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
from homeassistant.components import (ATTR_FRIENDLY_NAME,
|
from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT
|
||||||
ATTR_UNIT_OF_MEASUREMENT)
|
|
||||||
|
|
||||||
# The domain of your component. Should be equal to the name of your component
|
# The domain of your component. Should be equal to the name of your component
|
||||||
DOMAIN = "tellstick_sensor"
|
DOMAIN = "tellstick_sensor"
|
||||||
|
@ -88,7 +87,7 @@ def setup(hass, config):
|
||||||
}
|
}
|
||||||
|
|
||||||
def update_sensor_value_state(sensor_name, sensor_value):
|
def update_sensor_value_state(sensor_name, sensor_value):
|
||||||
"Update the state of a sensor value"
|
""" Update the state of a sensor value """
|
||||||
sensor_value_description = \
|
sensor_value_description = \
|
||||||
sensor_value_descriptions[sensor_value.datatype]
|
sensor_value_descriptions[sensor_value.datatype]
|
||||||
sensor_value_name = '{} {}'.format(
|
sensor_value_name = '{} {}'.format(
|
||||||
|
@ -117,7 +116,7 @@ def setup(hass, config):
|
||||||
]
|
]
|
||||||
|
|
||||||
def update_sensor_state(sensor):
|
def update_sensor_state(sensor):
|
||||||
"Updates all the sensor values from the sensor"
|
""" Updates all the sensor values from the sensor """
|
||||||
try:
|
try:
|
||||||
sensor_name = config[DOMAIN][str(sensor.id)]
|
sensor_name = config[DOMAIN][str(sensor.id)]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -132,7 +131,7 @@ def setup(hass, config):
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def update_sensors_state(time):
|
def update_sensors_state(time):
|
||||||
"Update the state of all sensors"
|
""" Update the state of all sensors """
|
||||||
for sensor in sensors:
|
for sensor in sensors:
|
||||||
update_sensor_state(sensor)
|
update_sensor_state(sensor)
|
||||||
|
|
||||||
|
|
|
@ -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 importlib
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.util import OrderedSet
|
||||||
|
|
||||||
|
PREPARED = False
|
||||||
|
|
||||||
# List of available components
|
# List of available components
|
||||||
AVAILABLE_COMPONENTS = []
|
AVAILABLE_COMPONENTS = []
|
||||||
|
|
||||||
|
@ -30,6 +34,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
def prepare(hass):
|
def prepare(hass):
|
||||||
""" Prepares the loading of components. """
|
""" Prepares the loading of components. """
|
||||||
|
global PREPARED # pylint: disable=global-statement
|
||||||
|
|
||||||
# Load the built-in components
|
# Load the built-in components
|
||||||
import homeassistant.components as components
|
import homeassistant.components as components
|
||||||
|
|
||||||
|
@ -56,15 +62,22 @@ def prepare(hass):
|
||||||
# just might output more errors.
|
# just might output more errors.
|
||||||
for fil in os.listdir(custom_path):
|
for fil in os.listdir(custom_path):
|
||||||
if os.path.isdir(os.path.join(custom_path, fil)):
|
if os.path.isdir(os.path.join(custom_path, fil)):
|
||||||
AVAILABLE_COMPONENTS.append('custom_components.{}'.format(fil))
|
if fil != '__pycache__':
|
||||||
|
AVAILABLE_COMPONENTS.append(
|
||||||
|
'custom_components.{}'.format(fil))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
# For files we will strip out .py extension
|
||||||
AVAILABLE_COMPONENTS.append(
|
AVAILABLE_COMPONENTS.append(
|
||||||
'custom_components.{}'.format(fil[0:-3]))
|
'custom_components.{}'.format(fil[0:-3]))
|
||||||
|
|
||||||
|
PREPARED = True
|
||||||
|
|
||||||
|
|
||||||
def set_component(comp_name, component):
|
def set_component(comp_name, component):
|
||||||
""" Sets a component in the cache. """
|
""" Sets a component in the cache. """
|
||||||
|
_check_prepared()
|
||||||
|
|
||||||
_COMPONENT_CACHE[comp_name] = component
|
_COMPONENT_CACHE[comp_name] = component
|
||||||
|
|
||||||
|
|
||||||
|
@ -76,6 +89,8 @@ def get_component(comp_name):
|
||||||
if comp_name in _COMPONENT_CACHE:
|
if comp_name in _COMPONENT_CACHE:
|
||||||
return _COMPONENT_CACHE[comp_name]
|
return _COMPONENT_CACHE[comp_name]
|
||||||
|
|
||||||
|
_check_prepared()
|
||||||
|
|
||||||
# If we ie. try to load custom_components.switch.wemo but the parent
|
# If we ie. try to load custom_components.switch.wemo but the parent
|
||||||
# custom_components.switch does not exist, importing it will trigger
|
# custom_components.switch does not exist, importing it will trigger
|
||||||
# an exception because it will try to import the parent.
|
# an exception because it will try to import the parent.
|
||||||
|
@ -125,3 +140,89 @@ def get_component(comp_name):
|
||||||
_LOGGER.error("Unable to find component %s", comp_name)
|
_LOGGER.error("Unable to find component %s", comp_name)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def load_order_components(components):
|
||||||
|
"""
|
||||||
|
Takes in a list of components we want to load:
|
||||||
|
- filters out components we cannot load
|
||||||
|
- filters out components that have invalid/circular dependencies
|
||||||
|
- Will ensure that all components that do not directly depend on
|
||||||
|
the group component will be loaded before the group component.
|
||||||
|
- returns an OrderedSet load order.
|
||||||
|
"""
|
||||||
|
_check_prepared()
|
||||||
|
|
||||||
|
group = get_component('group')
|
||||||
|
|
||||||
|
load_order = OrderedSet()
|
||||||
|
|
||||||
|
# Sort the list of modules on if they depend on group component or not.
|
||||||
|
# We do this because the components that do not depend on the group
|
||||||
|
# component usually set up states that the group component requires to be
|
||||||
|
# created before it can group them.
|
||||||
|
# This does not matter in the future if we can setup groups without the
|
||||||
|
# states existing yet.
|
||||||
|
for comp_load_order in sorted((load_order_component(component)
|
||||||
|
for component in components),
|
||||||
|
# Test if group component exists in case
|
||||||
|
# above get_component call had an error.
|
||||||
|
key=lambda order:
|
||||||
|
group and group.DOMAIN in order):
|
||||||
|
load_order.update(comp_load_order)
|
||||||
|
|
||||||
|
return load_order
|
||||||
|
|
||||||
|
|
||||||
|
def load_order_component(comp_name):
|
||||||
|
"""
|
||||||
|
Returns an OrderedSet of components in the correct order of loading.
|
||||||
|
Raises HomeAssistantError if a circular dependency is detected.
|
||||||
|
Returns an empty list if component could not be loaded.
|
||||||
|
"""
|
||||||
|
return _load_order_component(comp_name, OrderedSet(), set())
|
||||||
|
|
||||||
|
|
||||||
|
def _load_order_component(comp_name, load_order, loading):
|
||||||
|
""" Recursive function to get load order of components. """
|
||||||
|
component = get_component(comp_name)
|
||||||
|
|
||||||
|
# if None it does not exist, error already thrown by get_component
|
||||||
|
if component is None:
|
||||||
|
return OrderedSet()
|
||||||
|
|
||||||
|
loading.add(comp_name)
|
||||||
|
|
||||||
|
for dependency in component.DEPENDENCIES:
|
||||||
|
# Check not already loaded
|
||||||
|
if dependency not in load_order:
|
||||||
|
# If we are already loading it, we have a circular dependency
|
||||||
|
if dependency in loading:
|
||||||
|
_LOGGER.error('Circular dependency detected: %s -> %s',
|
||||||
|
comp_name, dependency)
|
||||||
|
|
||||||
|
return OrderedSet()
|
||||||
|
|
||||||
|
dep_load_order = _load_order_component(
|
||||||
|
dependency, load_order, loading)
|
||||||
|
|
||||||
|
# length == 0 means error loading dependency or children
|
||||||
|
if len(dep_load_order) == 0:
|
||||||
|
_LOGGER.error('Error loading %s dependency: %s',
|
||||||
|
comp_name, dependency)
|
||||||
|
return OrderedSet()
|
||||||
|
|
||||||
|
load_order.update(dep_load_order)
|
||||||
|
|
||||||
|
load_order.add(comp_name)
|
||||||
|
loading.remove(comp_name)
|
||||||
|
|
||||||
|
return load_order
|
||||||
|
|
||||||
|
|
||||||
|
def _check_prepared():
|
||||||
|
""" Issues a warning if loader.prepare() has never been called. """
|
||||||
|
if not PREPARED:
|
||||||
|
_LOGGER.warning((
|
||||||
|
"You did not call loader.prepare() yet. "
|
||||||
|
"Certain functionality might not be working."))
|
||||||
|
|
|
@ -19,21 +19,14 @@ import requests
|
||||||
|
|
||||||
import homeassistant as ha
|
import homeassistant as ha
|
||||||
|
|
||||||
SERVER_PORT = 8123
|
from homeassistant.const import (
|
||||||
|
SERVER_PORT, AUTH_HEADER, URL_API, URL_API_STATES, URL_API_STATES_ENTITY,
|
||||||
AUTH_HEADER = "HA-access"
|
URL_API_EVENTS, URL_API_EVENTS_EVENT, URL_API_SERVICES,
|
||||||
|
URL_API_SERVICES_SERVICE, URL_API_EVENT_FORWARD)
|
||||||
URL_API = "/api/"
|
|
||||||
URL_API_STATES = "/api/states"
|
|
||||||
URL_API_STATES_ENTITY = "/api/states/{}"
|
|
||||||
URL_API_EVENTS = "/api/events"
|
|
||||||
URL_API_EVENTS_EVENT = "/api/events/{}"
|
|
||||||
URL_API_SERVICES = "/api/services"
|
|
||||||
URL_API_SERVICES_SERVICE = "/api/services/{}/{}"
|
|
||||||
URL_API_EVENT_FORWARD = "/api/event_forwarding"
|
|
||||||
|
|
||||||
METHOD_GET = "get"
|
METHOD_GET = "get"
|
||||||
METHOD_POST = "post"
|
METHOD_POST = "post"
|
||||||
|
METHOD_DELETE = "delete"
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -94,6 +87,10 @@ class API(object):
|
||||||
_LOGGER.exception(error)
|
_LOGGER.exception(error)
|
||||||
raise ha.HomeAssistantError(error)
|
raise ha.HomeAssistantError(error)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "API({}, {}, {})".format(
|
||||||
|
self.host, self.api_password, self.port)
|
||||||
|
|
||||||
|
|
||||||
class HomeAssistant(ha.HomeAssistant):
|
class HomeAssistant(ha.HomeAssistant):
|
||||||
""" Home Assistant that forwards work. """
|
""" Home Assistant that forwards work. """
|
||||||
|
@ -122,18 +119,23 @@ class HomeAssistant(ha.HomeAssistant):
|
||||||
import random
|
import random
|
||||||
|
|
||||||
# pylint: disable=too-many-format-args
|
# pylint: disable=too-many-format-args
|
||||||
random_password = '%030x'.format(random.randrange(16**30))
|
random_password = '{:30}'.format(random.randrange(16**30))
|
||||||
|
|
||||||
http.setup(self, random_password)
|
http.setup(
|
||||||
|
self, {http.DOMAIN: {http.CONF_API_PASSWORD: random_password}})
|
||||||
|
|
||||||
ha.Timer(self)
|
ha.Timer(self)
|
||||||
|
|
||||||
# Setup that events from remote_api get forwarded to local_api
|
|
||||||
connect_remote_events(self.remote_api, self.local_api)
|
|
||||||
|
|
||||||
self.bus.fire(ha.EVENT_HOMEASSISTANT_START,
|
self.bus.fire(ha.EVENT_HOMEASSISTANT_START,
|
||||||
origin=ha.EventOrigin.remote)
|
origin=ha.EventOrigin.remote)
|
||||||
|
|
||||||
|
# Setup that events from remote_api get forwarded to local_api
|
||||||
|
# Do this after we fire START, otherwise HTTP is not started
|
||||||
|
if not connect_remote_events(self.remote_api, self.local_api):
|
||||||
|
raise ha.HomeAssistantError((
|
||||||
|
'Could not setup event forwarding from api {} to '
|
||||||
|
'local api {}').format(self.remote_api, self.local_api))
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
""" Stops Home Assistant and shuts down all threads. """
|
""" Stops Home Assistant and shuts down all threads. """
|
||||||
_LOGGER.info("Stopping")
|
_LOGGER.info("Stopping")
|
||||||
|
@ -141,6 +143,9 @@ class HomeAssistant(ha.HomeAssistant):
|
||||||
self.bus.fire(ha.EVENT_HOMEASSISTANT_STOP,
|
self.bus.fire(ha.EVENT_HOMEASSISTANT_STOP,
|
||||||
origin=ha.EventOrigin.remote)
|
origin=ha.EventOrigin.remote)
|
||||||
|
|
||||||
|
# Disconnect master event forwarding
|
||||||
|
disconnect_remote_events(self.remote_api, self.local_api)
|
||||||
|
|
||||||
# Wait till all responses to homeassistant_stop are done
|
# Wait till all responses to homeassistant_stop are done
|
||||||
self._pool.block_till_done()
|
self._pool.block_till_done()
|
||||||
|
|
||||||
|
@ -285,30 +290,51 @@ def validate_api(api):
|
||||||
def connect_remote_events(from_api, to_api):
|
def connect_remote_events(from_api, to_api):
|
||||||
""" Sets up from_api to forward all events to to_api. """
|
""" Sets up from_api to forward all events to to_api. """
|
||||||
|
|
||||||
data = {'host': to_api.host, 'api_password': to_api.api_password}
|
data = {
|
||||||
|
'host': to_api.host,
|
||||||
if to_api.port is not None:
|
'api_password': to_api.api_password,
|
||||||
data['port'] = to_api.port
|
'port': to_api.port
|
||||||
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from_api(METHOD_POST, URL_API_EVENT_FORWARD, data)
|
req = from_api(METHOD_POST, URL_API_EVENT_FORWARD, data)
|
||||||
|
|
||||||
|
if req.status_code == 200:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Error settign up event forwarding: %s - %s",
|
||||||
|
req.status_code, req.text)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
except ha.HomeAssistantError:
|
except ha.HomeAssistantError:
|
||||||
pass
|
_LOGGER.exception("Error setting up event forwarding")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def disconnect_remote_events(from_api, to_api):
|
def disconnect_remote_events(from_api, to_api):
|
||||||
""" Disconnects forwarding events from from_api to to_api. """
|
""" Disconnects forwarding events from from_api to to_api. """
|
||||||
data = {'host': to_api.host, '_METHOD': 'DELETE'}
|
data = {
|
||||||
|
'host': to_api.host,
|
||||||
if to_api.port is not None:
|
'port': to_api.port
|
||||||
data['port'] = to_api.port
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from_api(METHOD_POST, URL_API_EVENT_FORWARD, data)
|
req = from_api(METHOD_DELETE, URL_API_EVENT_FORWARD, data)
|
||||||
|
|
||||||
|
if req.status_code == 200:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Error removing event forwarding: %s - %s",
|
||||||
|
req.status_code, req.text)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
except ha.HomeAssistantError:
|
except ha.HomeAssistantError:
|
||||||
pass
|
_LOGGER.exception("Error removing an event forwarder")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_event_listeners(api):
|
def get_event_listeners(api):
|
||||||
|
@ -336,7 +362,7 @@ def fire_event(api, event_type, data=None):
|
||||||
req.status_code, req.text)
|
req.status_code, req.text)
|
||||||
|
|
||||||
except ha.HomeAssistantError:
|
except ha.HomeAssistantError:
|
||||||
pass
|
_LOGGER.exception("Error firing event")
|
||||||
|
|
||||||
|
|
||||||
def get_state(api, entity_id):
|
def get_state(api, entity_id):
|
||||||
|
@ -372,7 +398,7 @@ def get_states(api):
|
||||||
# ValueError if req.json() can't parse the json
|
# ValueError if req.json() can't parse the json
|
||||||
_LOGGER.exception("Error fetching states")
|
_LOGGER.exception("Error fetching states")
|
||||||
|
|
||||||
return {}
|
return []
|
||||||
|
|
||||||
|
|
||||||
def set_state(api, entity_id, new_state, attributes=None):
|
def set_state(api, entity_id, new_state, attributes=None):
|
||||||
|
|
|
@ -4,12 +4,15 @@ homeassistant.util
|
||||||
|
|
||||||
Helper methods for various modules.
|
Helper methods for various modules.
|
||||||
"""
|
"""
|
||||||
|
import collections
|
||||||
|
from itertools import chain
|
||||||
import threading
|
import threading
|
||||||
import queue
|
import queue
|
||||||
import datetime
|
from datetime import datetime, timedelta
|
||||||
import re
|
import re
|
||||||
import enum
|
import enum
|
||||||
import socket
|
import socket
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
RE_SANITIZE_FILENAME = re.compile(r'(~|\.\.|/|\\)')
|
RE_SANITIZE_FILENAME = re.compile(r'(~|\.\.|/|\\)')
|
||||||
RE_SANITIZE_PATH = re.compile(r'(~|\.(\.)+)')
|
RE_SANITIZE_PATH = re.compile(r'(~|\.(\.)+)')
|
||||||
|
@ -49,11 +52,19 @@ def str_to_datetime(dt_str):
|
||||||
@rtype: datetime
|
@rtype: datetime
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return datetime.datetime.strptime(dt_str, DATE_STR_FORMAT)
|
return datetime.strptime(dt_str, DATE_STR_FORMAT)
|
||||||
except ValueError: # If dt_str did not match our format
|
except ValueError: # If dt_str did not match our format
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def strip_microseconds(dattim):
|
||||||
|
""" Returns a copy of dattime object but with microsecond set to 0. """
|
||||||
|
if dattim.microsecond:
|
||||||
|
return dattim - timedelta(microseconds=dattim.microsecond)
|
||||||
|
else:
|
||||||
|
return dattim
|
||||||
|
|
||||||
|
|
||||||
def split_entity_id(entity_id):
|
def split_entity_id(entity_id):
|
||||||
""" Splits a state entity_id into domain, object_id. """
|
""" Splits a state entity_id into domain, object_id. """
|
||||||
return entity_id.split(".", 1)
|
return entity_id.split(".", 1)
|
||||||
|
@ -65,7 +76,7 @@ def repr_helper(inp):
|
||||||
return ", ".join(
|
return ", ".join(
|
||||||
repr_helper(key)+"="+repr_helper(item) for key, item
|
repr_helper(key)+"="+repr_helper(item) for key, item
|
||||||
in inp.items())
|
in inp.items())
|
||||||
elif isinstance(inp, datetime.datetime):
|
elif isinstance(inp, datetime):
|
||||||
return datetime_to_str(inp)
|
return datetime_to_str(inp)
|
||||||
else:
|
else:
|
||||||
return str(inp)
|
return str(inp)
|
||||||
|
@ -124,6 +135,7 @@ def ensure_unique_string(preferred_string, current_strings):
|
||||||
""" Returns a string that is not present in current_strings.
|
""" Returns a string that is not present in current_strings.
|
||||||
If preferred string exists will append _2, _3, .. """
|
If preferred string exists will append _2, _3, .. """
|
||||||
string = preferred_string
|
string = preferred_string
|
||||||
|
current_strings = list(current_strings)
|
||||||
|
|
||||||
tries = 1
|
tries = 1
|
||||||
|
|
||||||
|
@ -176,93 +188,199 @@ class OrderedEnum(enum.Enum):
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|
||||||
|
|
||||||
def validate_config(config, items, logger):
|
class OrderedSet(collections.MutableSet):
|
||||||
|
""" Ordered set taken from http://code.activestate.com/recipes/576694/ """
|
||||||
|
|
||||||
|
def __init__(self, iterable=None):
|
||||||
|
self.end = end = []
|
||||||
|
end += [None, end, end] # sentinel node for doubly linked list
|
||||||
|
self.map = {} # key --> [key, prev, next]
|
||||||
|
if iterable is not None:
|
||||||
|
self |= iterable
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.map)
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
return key in self.map
|
||||||
|
|
||||||
|
def add(self, key):
|
||||||
|
""" Add an element to the set. """
|
||||||
|
if key not in self.map:
|
||||||
|
end = self.end
|
||||||
|
curr = end[1]
|
||||||
|
curr[2] = end[1] = self.map[key] = [key, curr, end]
|
||||||
|
|
||||||
|
def discard(self, key):
|
||||||
|
""" Discard an element from the set. """
|
||||||
|
if key in self.map:
|
||||||
|
key, prev_item, next_item = self.map.pop(key)
|
||||||
|
prev_item[2] = next_item
|
||||||
|
next_item[1] = prev_item
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
end = self.end
|
||||||
|
curr = end[2]
|
||||||
|
while curr is not end:
|
||||||
|
yield curr[0]
|
||||||
|
curr = curr[2]
|
||||||
|
|
||||||
|
def __reversed__(self):
|
||||||
|
end = self.end
|
||||||
|
curr = end[1]
|
||||||
|
while curr is not end:
|
||||||
|
yield curr[0]
|
||||||
|
curr = curr[1]
|
||||||
|
|
||||||
|
def pop(self, last=True): # pylint: disable=arguments-differ
|
||||||
|
""" Pops element of the end of the set.
|
||||||
|
Set last=False to pop from the beginning. """
|
||||||
|
if not self:
|
||||||
|
raise KeyError('set is empty')
|
||||||
|
key = self.end[1][0] if last else self.end[2][0]
|
||||||
|
self.discard(key)
|
||||||
|
return key
|
||||||
|
|
||||||
|
def update(self, *args):
|
||||||
|
""" Add elements from args to the set. """
|
||||||
|
for item in chain(*args):
|
||||||
|
self.add(item)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if not self:
|
||||||
|
return '%s()' % (self.__class__.__name__,)
|
||||||
|
return '%s(%r)' % (self.__class__.__name__, list(self))
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, OrderedSet):
|
||||||
|
return len(self) == len(other) and list(self) == list(other)
|
||||||
|
return set(self) == set(other)
|
||||||
|
|
||||||
|
|
||||||
|
class Throttle(object):
|
||||||
"""
|
"""
|
||||||
Validates if all items are available in the configuration.
|
A method decorator to add a cooldown to a method to prevent it from being
|
||||||
|
called more then 1 time within the timedelta interval `min_time` after it
|
||||||
|
returned its result.
|
||||||
|
|
||||||
config is the general dictionary with all the configurations.
|
Calling a method a second time during the interval will return None.
|
||||||
items is a dict with per domain which attributes we require.
|
|
||||||
logger is the logger from the caller to log the errors to.
|
|
||||||
|
|
||||||
Returns True if all required items were found.
|
Pass keyword argument `no_throttle=True` to the wrapped method to make
|
||||||
|
the call not throttled.
|
||||||
|
|
||||||
|
Decorator takes in an optional second timedelta interval to throttle the
|
||||||
|
'no_throttle' calls.
|
||||||
|
|
||||||
|
Adds a datetime attribute `last_call` to the method.
|
||||||
"""
|
"""
|
||||||
errors_found = False
|
# pylint: disable=too-few-public-methods
|
||||||
for domain in items.keys():
|
|
||||||
config.setdefault(domain, {})
|
|
||||||
|
|
||||||
errors = [item for item in items[domain] if item not in config[domain]]
|
def __init__(self, min_time, limit_no_throttle=None):
|
||||||
|
self.min_time = min_time
|
||||||
|
self.limit_no_throttle = limit_no_throttle
|
||||||
|
|
||||||
if errors:
|
def __call__(self, method):
|
||||||
logger.error(
|
lock = threading.Lock()
|
||||||
"Missing required configuration items in {}: {}".format(
|
|
||||||
domain, ", ".join(errors)))
|
|
||||||
|
|
||||||
errors_found = True
|
if self.limit_no_throttle is not None:
|
||||||
|
method = Throttle(self.limit_no_throttle)(method)
|
||||||
|
|
||||||
return not errors_found
|
@wraps(method)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
Wrapper that allows wrapped to be called only once per min_time.
|
||||||
|
"""
|
||||||
|
with lock:
|
||||||
|
last_call = wrapper.last_call
|
||||||
|
# Check if method is never called or no_throttle is given
|
||||||
|
force = last_call is None or kwargs.pop('no_throttle', False)
|
||||||
|
|
||||||
|
if force or datetime.now() - last_call > self.min_time:
|
||||||
|
|
||||||
|
result = method(*args, **kwargs)
|
||||||
|
wrapper.last_call = datetime.now()
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
wrapper.last_call = None
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
# Reason why I decided to roll my own ThreadPool instead of using
|
|
||||||
# multiprocessing.dummy.pool or even better, use multiprocessing.pool and
|
|
||||||
# not be hurt by the GIL in the cpython interpreter:
|
|
||||||
# 1. The built in threadpool does not allow me to create custom workers and so
|
|
||||||
# I would have to wrap every listener that I passed into it with code to log
|
|
||||||
# the exceptions. Saving a reference to the logger in the worker seemed
|
|
||||||
# like a more sane thing to do.
|
|
||||||
# 2. Most event listeners are simple checks if attributes match. If the method
|
|
||||||
# that they will call takes a long time to complete it might be better to
|
|
||||||
# put that request in a seperate thread. This is for every component to
|
|
||||||
# decide on its own instead of enforcing it for everyone.
|
|
||||||
class ThreadPool(object):
|
class ThreadPool(object):
|
||||||
""" A simple queue-based thread pool.
|
""" A priority queue-based thread pool. """
|
||||||
|
|
||||||
Will initiate it's workers using worker(queue).start() """
|
|
||||||
# pylint: disable=too-many-instance-attributes
|
# pylint: disable=too-many-instance-attributes
|
||||||
|
|
||||||
def __init__(self, worker_count, job_handler, busy_callback=None):
|
def __init__(self, job_handler, worker_count=0, busy_callback=None):
|
||||||
"""
|
"""
|
||||||
worker_count: number of threads to run that handle jobs
|
|
||||||
job_handler: method to be called from worker thread to handle job
|
job_handler: method to be called from worker thread to handle job
|
||||||
|
worker_count: number of threads to run that handle jobs
|
||||||
busy_callback: method to be called when queue gets too big.
|
busy_callback: method to be called when queue gets too big.
|
||||||
Parameters: list_of_current_jobs, number_pending_jobs
|
Parameters: worker_count, list of current_jobs,
|
||||||
|
pending_jobs_count
|
||||||
"""
|
"""
|
||||||
self.work_queue = work_queue = queue.PriorityQueue()
|
self._job_handler = job_handler
|
||||||
self.current_jobs = current_jobs = []
|
self._busy_callback = busy_callback
|
||||||
self.worker_count = worker_count
|
|
||||||
self.busy_callback = busy_callback
|
self.worker_count = 0
|
||||||
self.busy_warning_limit = worker_count**2
|
self.busy_warning_limit = 0
|
||||||
|
self._work_queue = queue.PriorityQueue()
|
||||||
|
self.current_jobs = []
|
||||||
self._lock = threading.RLock()
|
self._lock = threading.RLock()
|
||||||
self._quit_task = object()
|
self._quit_task = object()
|
||||||
|
|
||||||
|
self.running = True
|
||||||
|
|
||||||
for _ in range(worker_count):
|
for _ in range(worker_count):
|
||||||
worker = threading.Thread(target=_threadpool_worker,
|
self.add_worker()
|
||||||
args=(work_queue, current_jobs,
|
|
||||||
job_handler, self._quit_task))
|
def add_worker(self):
|
||||||
|
""" Adds a worker to the thread pool. Resets warning limit. """
|
||||||
|
with self._lock:
|
||||||
|
if not self.running:
|
||||||
|
raise RuntimeError("ThreadPool not running")
|
||||||
|
|
||||||
|
worker = threading.Thread(target=self._worker)
|
||||||
worker.daemon = True
|
worker.daemon = True
|
||||||
worker.start()
|
worker.start()
|
||||||
|
|
||||||
self.running = True
|
self.worker_count += 1
|
||||||
|
self.busy_warning_limit = self.worker_count * 3
|
||||||
|
|
||||||
def add_job(self, priority, job):
|
def remove_worker(self):
|
||||||
""" Add a job to be sent to the workers. """
|
""" Removes a worker from the thread pool. Resets warning limit. """
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if not self.running:
|
if not self.running:
|
||||||
raise Exception("We are shutting down the ")
|
raise RuntimeError("ThreadPool not running")
|
||||||
|
|
||||||
self.work_queue.put(PriorityQueueItem(priority, job))
|
self._work_queue.put(PriorityQueueItem(0, self._quit_task))
|
||||||
|
|
||||||
|
self.worker_count -= 1
|
||||||
|
self.busy_warning_limit = self.worker_count * 3
|
||||||
|
|
||||||
|
def add_job(self, priority, job):
|
||||||
|
""" Add a job to the queue. """
|
||||||
|
with self._lock:
|
||||||
|
if not self.running:
|
||||||
|
raise RuntimeError("ThreadPool not running")
|
||||||
|
|
||||||
|
self._work_queue.put(PriorityQueueItem(priority, job))
|
||||||
|
|
||||||
# check if our queue is getting too big
|
# check if our queue is getting too big
|
||||||
if self.work_queue.qsize() > self.busy_warning_limit \
|
if self._work_queue.qsize() > self.busy_warning_limit \
|
||||||
and self.busy_callback is not None:
|
and self._busy_callback is not None:
|
||||||
|
|
||||||
# Increase limit we will issue next warning
|
# Increase limit we will issue next warning
|
||||||
self.busy_warning_limit *= 2
|
self.busy_warning_limit *= 2
|
||||||
|
|
||||||
self.busy_callback(self.current_jobs, self.work_queue.qsize())
|
self._busy_callback(
|
||||||
|
self.worker_count, self.current_jobs,
|
||||||
|
self._work_queue.qsize())
|
||||||
|
|
||||||
def block_till_done(self):
|
def block_till_done(self):
|
||||||
""" Blocks till all work is done. """
|
""" Blocks till all work is done. """
|
||||||
self.work_queue.join()
|
self._work_queue.join()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
""" Stops all the threads. """
|
""" Stops all the threads. """
|
||||||
|
@ -270,19 +388,41 @@ class ThreadPool(object):
|
||||||
if not self.running:
|
if not self.running:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Clear the queue
|
# Ensure all current jobs finish
|
||||||
while self.work_queue.qsize() > 0:
|
self.block_till_done()
|
||||||
self.work_queue.get()
|
|
||||||
self.work_queue.task_done()
|
|
||||||
|
|
||||||
# Tell the workers to quit
|
# Tell the workers to quit
|
||||||
for _ in range(self.worker_count):
|
for _ in range(self.worker_count):
|
||||||
self.add_job(1000, self._quit_task)
|
self.remove_worker()
|
||||||
|
|
||||||
self.running = False
|
self.running = False
|
||||||
|
|
||||||
|
# Wait till all workers have quit
|
||||||
self.block_till_done()
|
self.block_till_done()
|
||||||
|
|
||||||
|
def _worker(self):
|
||||||
|
""" Handles jobs for the thread pool. """
|
||||||
|
while True:
|
||||||
|
# Get new item from work_queue
|
||||||
|
job = self._work_queue.get().item
|
||||||
|
|
||||||
|
if job == self._quit_task:
|
||||||
|
self._work_queue.task_done()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Add to current running jobs
|
||||||
|
job_log = (datetime.now(), job)
|
||||||
|
self.current_jobs.append(job_log)
|
||||||
|
|
||||||
|
# Do the job
|
||||||
|
self._job_handler(job)
|
||||||
|
|
||||||
|
# Remove from current running job
|
||||||
|
self.current_jobs.remove(job_log)
|
||||||
|
|
||||||
|
# Tell work_queue the task is done
|
||||||
|
self._work_queue.task_done()
|
||||||
|
|
||||||
|
|
||||||
class PriorityQueueItem(object):
|
class PriorityQueueItem(object):
|
||||||
""" Holds a priority and a value. Used within PriorityQueue. """
|
""" Holds a priority and a value. Used within PriorityQueue. """
|
||||||
|
@ -294,27 +434,3 @@ class PriorityQueueItem(object):
|
||||||
|
|
||||||
def __lt__(self, other):
|
def __lt__(self, other):
|
||||||
return self.priority < other.priority
|
return self.priority < other.priority
|
||||||
|
|
||||||
|
|
||||||
def _threadpool_worker(work_queue, current_jobs, job_handler, quit_task):
|
|
||||||
""" Provides the base functionality of a worker for the thread pool. """
|
|
||||||
while True:
|
|
||||||
# Get new item from work_queue
|
|
||||||
job = work_queue.get().item
|
|
||||||
|
|
||||||
if job == quit_task:
|
|
||||||
work_queue.task_done()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Add to current running jobs
|
|
||||||
job_log = (datetime.datetime.now(), job)
|
|
||||||
current_jobs.append(job_log)
|
|
||||||
|
|
||||||
# Do the job
|
|
||||||
job_handler(job)
|
|
||||||
|
|
||||||
# Remove from current running job
|
|
||||||
current_jobs.remove(job_log)
|
|
||||||
|
|
||||||
# Tell work_queue a task is done
|
|
||||||
work_queue.task_done()
|
|
||||||
|
|
|
@ -17,3 +17,6 @@ pyuserinput>=0.1.9
|
||||||
|
|
||||||
# switch.tellstick, tellstick_sensor
|
# switch.tellstick, tellstick_sensor
|
||||||
tellcore-py>=1.0.4
|
tellcore-py>=1.0.4
|
||||||
|
|
||||||
|
# namp_tracker plugin
|
||||||
|
python-libnmap
|
||||||
|
|
|
@ -2,4 +2,4 @@
|
||||||
|
|
||||||
pylint homeassistant
|
pylint homeassistant
|
||||||
flake8 homeassistant --exclude bower_components,external
|
flake8 homeassistant --exclude bower_components,external
|
||||||
python3 -m unittest discover test
|
python3 -m unittest discover ha_test
|
||||||
|
|
|
@ -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