Merge branch 'dev' of github.com:balloob/home-assistant into dev

Conflicts:
	requirements.txt
This commit is contained in:
Wolfgang Ettlinger 2015-06-17 13:45:59 +02:00
commit 61638e8b72
61 changed files with 6631 additions and 3291 deletions

View file

@ -36,11 +36,14 @@ omit =
homeassistant/components/notify/nma.py
homeassistant/components/notify/pushbullet.py
homeassistant/components/notify/pushover.py
homeassistant/components/notify/smtp.py
homeassistant/components/notify/syslog.py
homeassistant/components/notify/xmpp.py
homeassistant/components/sensor/bitcoin.py
homeassistant/components/sensor/mysensors.py
homeassistant/components/sensor/openweathermap.py
homeassistant/components/sensor/sabnzbd.py
homeassistant/components/sensor/swiss_public_transport.py
homeassistant/components/sensor/systemmonitor.py
homeassistant/components/sensor/time_date.py
homeassistant/components/sensor/transmission.py

View file

@ -1,24 +1,41 @@
# Adding support for a new device
# Contributing to Home Assistant
For help on building your component, please see the See the [developer documentation on home-assistant.io](https://home-assistant.io/developers/).
Everybody is invited and welcome to contribute to Home Assistant. There is a lot to do...if you are not a developer perhaps you would like to help with the documentation on [home-assistant.io](https://home-assistant.io/)? If you are a developer and have devices in your home which aren't working with Home Assistant yet, why not spent a couple of hours and help to integrate them?
The process is straight-forward.
- Fork the Home Assistant [git repository](https://github.com/balloob/home-assistant).
- Write the code for your device, notification service, sensor, or IoT thing.
- Check it with ``pylint`` and ``flake8``.
- Create a Pull Request against the [**dev**](https://github.com/balloob/home-assistant/tree/dev) branch of Home Assistant.
Still interested? Then you should read the next sections and get more details.
## Adding support for a new device
For help on building your component, please see the [developer documentation](https://home-assistant.io/developers/) on [home-assistant.io](https://home-assistant.io/).
After you finish adding support for your device:
- update the supported devices in README.md.
- add any new dependencies to requirements.txt.
- Make sure all your code passes Pylint, flake8 (PEP8 and some more) validation. To generate reports, run `pylint homeassistant > pylint.txt` and `flake8 homeassistant --exclude bower_components,external > flake8.txt`.
- Update the supported devices in the `README.md` file.
- Add any new dependencies to `requirements.txt`.
- Update the `.coveragerc` file.
- Provide some documentation for [home-assistant.io](https://home-assistant.io/). The documentation is handled in a separate [git repository](https://github.com/balloob/home-assistant.io).
- Make sure all your code passes Pylint and flake8 (PEP8 and some more) validation. To generate reports, run `pylint homeassistant > pylint.txt` and `flake8 homeassistant --exclude bower_components,external > flake8.txt`.
- Create a Pull Request against the [**dev**](https://github.com/balloob/home-assistant/tree/dev) branch of Home Assistant.
- Check for comments and suggestions on your Pull Request and keep an eye on the [Travis output](https://travis-ci.org/balloob/home-assistant/).
If you've added a component:
- update the file [`domain-icon.html`](https://github.com/balloob/home-assistant/blob/master/homeassistant/components/http/www_static/polymer/domain-icon.html) with an icon for your domain ([pick from this list](https://www.polymer-project.org/0.5/components/core-elements/demo.html#core-icon))
- update the demo component with two states that it provides
- Update the file [`home-assistant-icons.html`](https://github.com/balloob/home-assistant/blob/master/homeassistant/components/frontend/www_static/polymer/resources/home-assistant-icons.html) with an icon for your domain ([pick one from this list](https://www.polymer-project.org/1.0/components/core-elements/demo.html#core-icon)).
- Update the demo component with two states that it provides
- Add your component to home-assistant.conf.example
Since you've updated domain-icon.html, you've made changes to the frontend:
Since you've updated `home-assistant-icons.html`, you've made changes to the frontend:
- run `build_frontend`. This will build a new version of the frontend. Make sure you add the changed files `frontend.py` and `frontend.html` to the commit.
- Run `build_frontend`. This will build a new version of the frontend. Make sure you add the changed files `frontend.py` and `frontend.html` to the commit.
## Setting states
### Setting states
It is the responsibility of the component to maintain the states of the devices in your domain. Each device should be a single state and, if possible, a group should be provided that tracks the combined state of the devices.
@ -31,9 +48,9 @@ A state can have several attributes that will help the frontend in displaying yo
These attributes are defined in [homeassistant.components](https://github.com/balloob/home-assistant/blob/master/homeassistant/components/__init__.py#L25).
## Proper Visibility Handling ##
### Proper Visibility Handling
Generally, when creating a new entity for Home Assistant you will want it to be a class that inherits the [homeassistant.helpers.entity.Entity](https://github.com/balloob/home-assistant/blob/master/homeassistant/helpers/entity.py) Class. If this is done, visibility will be handled for you.
Generally, when creating a new entity for Home Assistant you will want it to be a class that inherits the [homeassistant.helpers.entity.Entity](https://github.com/balloob/home-assistant/blob/master/homeassistant/helpers/entity.py) class. If this is done, visibility will be handled for you.
You can set a suggestion for your entity's visibility by setting the hidden property by doing something similar to the following.
```python
@ -44,12 +61,12 @@ This will SUGGEST that the active frontend hides the entity. This requires that
Remember: The suggestion set by your component's code will always be overwritten by user settings in the configuration.yaml file. This is why you may set hidden to be False, but the property may remain True (or vice-versa).
## Working on the frontend
### Working on the frontend
The frontend is composed of Polymer web-components and compiled into the file `frontend.html`. During development you do not want to work with the compiled version but with the seperate files. To have Home Assistant serve the seperate files, set `development=1` for the http-component in your config.
The frontend is composed of [Polymer](https://www.polymer-project.org) web-components and compiled into the file `frontend.html`. During development you do not want to work with the compiled version but with the seperate files. To have Home Assistant serve the seperate files, set `development=1` for the *http-component* in your config.
When you are done with development and ready to commit your changes, run `build_frontend`, set `development=0` in your config and validate that everything still works.
## Notes on PyLint and PEP8 validation
### Notes on PyLint and PEP8 validation
In case a PyLint warning cannot be avoided, add a comment to disable the PyLint check for that line. This can be done using the format `# pylint: disable=YOUR-ERROR-NAME`. Example of an unavoidable PyLint warning is if you do not use the passed in datetime if you're listening for time change.

View file

@ -6,12 +6,11 @@ Home Assistant is a home automation platform running on Python 3. The goal of Ho
It offers the following functionality through built-in components:
* Track if devices are home by monitoring connected devices to a wireless router (supporting [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index))
* Track and control [Philips Hue](http://meethue.com) lights
* Track and control [WeMo switches](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/)
* Track and control [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast)
* Track running services by monitoring `ps` output
* Track and control [Tellstick devices and sensors](http://www.telldus.se/products/tellstick)
* Track if devices are home by monitoring connected devices to a wireless router (supporting [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), and [DD-WRT](http://www.dd-wrt.com/site/index))
* Track and control [Philips Hue](http://meethue.com) lights, [WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) switches, and [Tellstick](http://www.telldus.se/products/tellstick) devices and sensors
* Track and control [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast) and [Music Player Daemon](http://www.musicpd.org/)
* Support for [ISY994](https://www.universal-devices.com/residential/isy994i-series/) (Insteon and X10 devices), [Z-Wave](http://www.z-wave.com/), [Nest Thermostats](https://nest.com/), and [Modbus](http://www.modbus.org/)
* Track running system services and monitoring your system stats (Memory, disk usage, and more)
* Control low-cost 433 MHz remote control wall-socket devices (https://github.com/r10r/rcswitch-pi) and other switches that can be turned on/off with shell commands
* Turn on the lights when people get home after sun set
* Turn on lights slowly during sun set to compensate for light loss
@ -19,6 +18,8 @@ It offers the following functionality through built-in components:
* Offers web interface to monitor and control Home Assistant
* Offers a [REST API](https://home-assistant.io/developers/api.html) for easy integration with other projects
* [Ability to have multiple instances of Home Assistant work together](https://home-assistant.io/developers/architecture.html)
* Allow sending notifications using [Instapush](https://instapush.im), [Notify My Android (NMA)](http://www.notifymyandroid.com/), [PushBullet](https://www.pushbullet.com/), [PushOver](https://pushover.net/), and [Jabber (XMPP)](http://xmpp.org)
* Allow to display details about a running [Transmission](http://www.transmissionbt.com/) client, the [Bitcoin](https://bitcoin.org) network, local meteorological data from [OpenWeatherMap](http://openweathermap.org/), the time, the date, and the downloads from [SABnzbd](http://sabnzbd.org)
Home Assistant also includes functionality for controlling HTPCs:
@ -34,16 +35,16 @@ If you run into issues while using Home Assistant or during development of a com
## Installation instructions / Quick-start guide
Running Home Assistant requires that python 3.4 and the package requests are installed. Run the following code to install and start Home Assistant:
Running Home Assistant requires that [Python](https://www.python.org/) 3.4 and the package [requests](http://docs.python-requests.org/en/latest/) are installed. Run the following code to install and start Home Assistant:
```python
git clone --recursive https://github.com/balloob/home-assistant.git
cd home-assistant
pip3 install -r requirements.txt
python3 -m pip install --user -r requirements.txt
python3 -m homeassistant --open-ui
```
The last command will start the Home Assistant server and launch its webinterface. By default Home Assistant looks for the configuration file `config/home-assistant.conf`. A standard configuration file will be written if none exists.
The last command will start the Home Assistant server and launch its web interface. By default Home Assistant looks for the configuration file `config/home-assistant.conf`. A standard configuration file will be written if none exists.
If you are still exploring if you want to use Home Assistant in the first place, you can enable the demo mode by adding the `--demo-mode` argument to the last command.

View file

@ -8,6 +8,22 @@ Example component to target an entity_id to:
- 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
Configuration:
To use the Example custom component you will need to add the following to
your config/configuration.yaml
example:
target: TARGET_ENTITY
Variable:
target
*Required
TARGET_ENTITY should be one of your devices that can be turned on and off,
ie a light or a switch. Example value could be light.Ceiling or switch.AC
(if you have these devices with those names).
"""
import time
import logging
@ -31,6 +47,7 @@ CONF_TARGET = 'target'
# Name of the service that we expose
SERVICE_FLASH = 'flash'
# Shortcut for the logger
_LOGGER = logging.getLogger(__name__)

View file

@ -3,6 +3,14 @@ custom_components.hello_world
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Implements the bare minimum that a component should implement.
Configuration:
To use the hello_word component you will need to add the following to your
config/configuration.yaml
hello_world:
"""
# The domain of your component. Should be equal to the name of your component

View file

@ -186,6 +186,24 @@ def from_config_file(config_path, hass=None):
def enable_logging(hass):
""" Setup the logging for home assistant. """
logging.basicConfig(level=logging.INFO)
fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) "
"[%(name)s] %(message)s%(reset)s")
try:
from colorlog import ColoredFormatter
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
fmt,
datefmt='%y-%m-%d %H:%M:%S',
reset=True,
log_colors={
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red',
}
))
except ImportError:
_LOGGER.warn("Colorlog package not found, console coloring disabled")
# Log errors to a file if we have write access to file or config dir
err_log_path = hass.config.path('home-assistant.log')
@ -202,7 +220,7 @@ def enable_logging(hass):
err_handler.setLevel(logging.WARNING)
err_handler.setFormatter(
logging.Formatter('%(asctime)s %(name)s: %(message)s',
datefmt='%H:%M %d-%m-%y'))
datefmt='%y-%m-%d %H:%M:%S'))
logging.getLogger('').addHandler(err_handler)
else:
@ -235,8 +253,13 @@ def process_ha_core_config(hass, config):
set_time_zone(config.get(CONF_TIME_ZONE))
for entity_id, attrs in config.get(CONF_CUSTOMIZE, {}).items():
Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values())
customize = config.get(CONF_CUSTOMIZE)
if isinstance(customize, dict):
for entity_id, attrs in config.get(CONF_CUSTOMIZE, {}).items():
if not isinstance(attrs, dict):
continue
Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values())
if CONF_TEMPERATURE_UNIT in config:
unit = config[CONF_TEMPERATURE_UNIT]

View file

@ -93,10 +93,10 @@ def setup(hass, config):
# Setup fake device tracker
hass.states.set("device_tracker.paulus", "home",
{ATTR_ENTITY_PICTURE:
"http://graph.facebook.com/schoutsen/picture"})
"http://graph.facebook.com/297400035/picture"})
hass.states.set("device_tracker.anne_therese", "not_home",
{ATTR_ENTITY_PICTURE:
"http://graph.facebook.com/anne.t.frederiksen/picture"})
"http://graph.facebook.com/621994601/picture"})
hass.states.set("group.all_devices", "home",
{

View file

@ -17,7 +17,7 @@ import homeassistant.util.dt as dt_util
from homeassistant.const import (
STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME,
CONF_PLATFORM)
CONF_PLATFORM, DEVICE_DEFAULT_NAME)
from homeassistant.components import group
DOMAIN = "device_tracker"
@ -169,66 +169,28 @@ class DeviceTracker(object):
if not self.lock.acquire(False):
return
found_devices = set(dev.upper() for dev in
self.device_scanner.scan_devices())
try:
found_devices = set(dev.upper() for dev in
self.device_scanner.scan_devices())
for device in self.tracked:
is_home = device in found_devices
for device in self.tracked:
is_home = device in found_devices
self._update_state(now, device, is_home)
self._update_state(now, device, is_home)
if is_home:
found_devices.remove(device)
if is_home:
found_devices.remove(device)
# Did we find any devices that we didn't know about yet?
new_devices = found_devices - self.untracked_devices
# Did we find any devices that we didn't know about yet?
new_devices = found_devices - self.untracked_devices
if new_devices:
if not self.track_new_devices:
self.untracked_devices.update(new_devices)
if new_devices:
if not self.track_new_devices:
self.untracked_devices.update(new_devices)
# Write new devices to known devices file
if not self.invalid_known_devices_file:
known_dev_path = self.hass.config.path(KNOWN_DEVICES_FILE)
try:
# If file does not exist we will write the header too
is_new_file = not os.path.isfile(known_dev_path)
with open(known_dev_path, 'a') as outp:
_LOGGER.info(
"Found %d new devices, updating %s",
len(new_devices), known_dev_path)
writer = csv.writer(outp)
if is_new_file:
writer.writerow((
"device", "name", "track", "picture"))
for device in new_devices:
# See if the device scanner knows the name
# else defaults to unknown device
dname = self.device_scanner.get_device_name(device)
name = dname or "unknown device"
track = 0
if self.track_new_devices:
self._track_device(device, name)
track = 1
writer.writerow((device, name, track, ""))
if self.track_new_devices:
self._generate_entity_ids(new_devices)
except IOError:
_LOGGER.exception(
"Error updating %s with %d new devices",
known_dev_path, len(new_devices))
self.lock.release()
self._update_known_devices_file(new_devices)
finally:
self.lock.release()
# pylint: disable=too-many-branches
def _read_known_devices_file(self):
@ -309,6 +271,44 @@ class DeviceTracker(object):
finally:
self.lock.release()
def _update_known_devices_file(self, new_devices):
""" Add new devices to known devices file. """
if not self.invalid_known_devices_file:
known_dev_path = self.hass.config.path(KNOWN_DEVICES_FILE)
try:
# If file does not exist we will write the header too
is_new_file = not os.path.isfile(known_dev_path)
with open(known_dev_path, 'a') as outp:
_LOGGER.info("Found %d new devices, updating %s",
len(new_devices), known_dev_path)
writer = csv.writer(outp)
if is_new_file:
writer.writerow(("device", "name", "track", "picture"))
for device in new_devices:
# See if the device scanner knows the name
# else defaults to unknown device
name = self.device_scanner.get_device_name(device) or \
DEVICE_DEFAULT_NAME
track = 0
if self.track_new_devices:
self._track_device(device, name)
track = 1
writer.writerow((device, name, track, ""))
if self.track_new_devices:
self._generate_entity_ids(new_devices)
except IOError:
_LOGGER.exception("Error updating %s with %d new devices",
known_dev_path, len(new_devices))
def _track_device(self, device, name):
"""
Add a device to the list of tracked devices.

View file

@ -15,8 +15,8 @@
<link rel='shortcut icon' href='/static/favicon.ico' />
<link rel='icon' type='image/png'
href='/static/favicon-192x192.png' sizes='192x192'>
<link rel='apple-touch-icon' sizes='192x192'
href='/static/favicon-192x192.png'>
<link rel='apple-touch-icon' sizes='180x180'
href='/static/favicon-apple-180x180.png'>
<meta name='theme-color' content='#03a9f4'>
</head>
<body fullbleed>

View file

@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """
VERSION = "2d15135e9bfd0ee5b023d9abb79be62d"
VERSION = "010d9683fa9d210abd199b3cde4edbc0"

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

View file

@ -31,12 +31,13 @@
"paper-slider": "PolymerElements/paper-slider#^1.0.0",
"paper-checkbox": "PolymerElements/paper-checkbox#^1.0.0",
"paper-drawer-panel": "PolymerElements/paper-drawer-panel#^1.0.0",
"paper-scroll-header-panel": "polymerelements/paper-scroll-header-panel#~1.0",
"paper-scroll-header-panel": "polymerelements/paper-scroll-header-panel#^1.0.0",
"google-apis": "GoogleWebComponents/google-apis#0.8-preview",
"moment": "^2.10.3",
"layout": "Polymer/layout",
"color-picker-element": "~0.0.3",
"paper-styles": "polymerelements/paper-styles#~1.0"
"paper-styles": "polymerelements/paper-styles#^1.0.0",
"paper-date-picker": "vsimonian/paper-date-picker#master"
},
"resolutions": {
"polymer": "^1.0.0",

View file

@ -7,14 +7,6 @@
<link rel="import" href="state-card-scene.html">
<link rel="import" href="state-card-media_player.html">
<dom-module id="state-card-content">
<style>
:host {
display: block;
}
</style>
</dom-module>
<script>
(function() {
var uiUtil = window.hass.uiUtil;

View file

@ -11,6 +11,7 @@
.state {
margin-left: 16px;
text-align: right;
overflow-x: hidden;
}
.main-text {
@ -32,8 +33,8 @@
<div class='horizontal justified layout'>
<state-info state-obj="[[stateObj]]"></state-info>
<div class='state'>
<div class='main-text'>[[computePrimaryText(stateObj)]]</div>
<div class='secondary-text'>[[computeSecondaryText(stateObj)]]</div>
<div class='main-text'>[[computePrimaryText(stateObj, isPlaying)]]</div>
<div class='secondary-text'>[[computeSecondaryText(stateObj, isPlaying)]]</div>
</div>
</div>
</template>
@ -41,6 +42,7 @@
<script>
(function() {
var PLAYING_STATES = ['playing', 'paused'];
Polymer({
is: 'state-card-media_player',
@ -48,14 +50,41 @@
stateObj: {
type: Object,
},
isPlaying: {
type: Boolean,
computed: 'computeIsPlaying(stateObj)',
},
},
computePrimaryText: function(stateObj) {
return stateObj.attributes.media_title || stateObj.stateDisplay;
computeIsPlaying: function(stateObj) {
return PLAYING_STATES.indexOf(stateObj.state) !== -1;
},
computeSecondaryText: function(stateObj) {
return stateObj.attributes.media_title ? stateObj.stateDisplay : '';
computePrimaryText: function(stateObj, isPlaying) {
return isPlaying ? stateObj.attributes.media_title : stateObj.stateDisplay;
},
computeSecondaryText: function(stateObj, isPlaying) {
var text;
if (stateObj.attributes.media_content_type == 'music') {
return stateObj.attributes.media_artist;
} else if (stateObj.attributes.media_content_type == 'tvshow') {
text = stateObj.attributes.media_series_title;
if (stateObj.attributes.media_season && stateObj.attributes.media_episode) {
text += ' S' + stateObj.attributes.media_season + 'E' + stateObj.attributes.media_episode;
}
return text;
} else if (stateObj.attributes.app_name) {
return stateObj.attributes.app_name;
} else {
return '';
}
},
});
})();

View file

@ -5,10 +5,10 @@
<dom-module id="state-card-scene">
<template>
<template is='dom-if' if=[[allowToggle]]>
<template is='dom-if' if='[[allowToggle]]'>
<state-card-toggle state-obj="[[stateObj]]"></state-card-toggle>
</template>
<template is='dom-if' if=[[!allowToggle]]>
<template is='dom-if' if='[[!allowToggle]]'>
<state-card-display state-obj="[[stateObj]]"></state-card-display>
</template>
</template>

View file

@ -16,7 +16,7 @@
<paper-toggle-button class='self-center'
checked="[[toggleChecked]]"
on-change="toggleChanged"
on-click="toggleClicked">
on-tap="toggleTapped">
</paper-toggle-button>
</div>
@ -47,6 +47,10 @@
this.forceStateChange();
},
toggleTapped: function(ev) {
ev.stopPropagation();
},
toggleChanged: function(ev) {
var newVal = ev.target.checked;

View file

@ -15,6 +15,7 @@
width: 100%;
cursor: pointer;
overflow: hidden;
}
</style>
@ -37,11 +38,14 @@
},
listeners: {
'click': 'cardClicked',
'tap': 'cardTapped',
},
cardClicked: function() {
uiActions.showMoreInfoDialog(this.stateObj.entityId);
cardTapped: function(ev) {
ev.stopPropagation();
this.debounce('show-more-info-dialog', function() {
uiActions.showMoreInfoDialog(this.stateObj.entityId);
}.bind(this), 100);
},
});
})();

View file

@ -10,8 +10,13 @@
}
</style>
<template>
<template is='dom-repeat' items="[[entries]]">
<logbook-entry entry-obj="[[item]]"></logbook-entry>
<template is='dom-if' if='[[!entries]]'>
No logbook entries found.
</template>
<template is='dom-if' if='[[entries]]'>
<template is='dom-repeat' items="[[entries]]">
<logbook-entry entry-obj="[[item]]"></logbook-entry>
</template>
</template>
</template>
</dom-module>

View file

@ -32,8 +32,8 @@
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;
padding: 16px;
margin: 16px auto;
padding: 0 16px 8px;
margin: 16px;
}
</style>
@ -44,9 +44,15 @@
<state-card class="state-card" state-obj="[[item]]"></state-card>
</template>
<template if="[[computeEmptyStates(states)]]">
<template is='dom-if' if="[[computeEmptyStates(states)]]">
<div class='no-states-content'>
<content></content>
<h3>Hi there!</h3>
<p>
It looks like we have nothing to show you right now. It could be that we have not yet discovered all your devices but it is more likely that you have not configured Home Assistant yet.
</p>
<p>
Please see the <a href='https://home-assistant.io/getting-started/' target='_blank'>Getting Started</a> section on how to setup your devices.
</p>
</div>
</template>

View file

@ -1,5 +1,14 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<dom-module is='state-history-chart-timeline'>
<style>
:host {
display: block;
}
</style>
<template></template>
</dom-module>
<script>
(function() {
Polymer({
@ -18,10 +27,6 @@
},
},
created: function() {
this.style.display = 'block';
},
attached: function() {
this.isAttached = true;
},
@ -45,7 +50,7 @@
if (!stateHistory || stateHistory.length === 0) {
return;
}
// debugger;
var chart = new google.visualization.Timeline(this);
var dataTable = new google.visualization.DataTable();
@ -59,14 +64,18 @@
dataTable.addRow([entityDisplay, stateStr, start, end]);
};
// people can pass in history of 1 entityId or a collection.
// var stateHistory;
// if (_.isArray(data[0])) {
// stateHistory = data;
// } else {
// stateHistory = [data];
// isSingleDevice = true;
// }
var startTime = new Date(stateHistory.map(function(stateInfo) {
return stateInfo[0].lastChangedAsDate;
}).reduce(function(prev, cur) {
return Math.min(prev, cur);
}, new Date()));
// end time is Math.min(curTime, start time + 1 day)
var endTime = new Date(startTime);
endTime.setDate(endTime.getDate()+1);
if (endTime > new Date()) {
endTime = new Date();
}
var numTimelines = 0;
// stateHistory is a list of lists of sorted state objects
@ -90,17 +99,13 @@
}
});
addRow(entityDisplay, prevState, prevLastChanged, new Date());
addRow(entityDisplay, prevState, prevLastChanged, endTime);
numTimelines++;
}.bind(this));
chart.draw(dataTable, {
height: 55 + numTimelines * 42,
// interactive properties require CSS, the JS api puts it on the document
// instead of inside our Shadow DOM.
enableInteractivity: false,
timeline: {
showRowLabels: stateHistory.length > 1
},

View file

@ -16,29 +16,34 @@
text-align: center;
padding: 8px;
}
.loading {
height: 0px;
overflow: hidden;
}
</style>
<template>
<google-legacy-loader on-api-load="googleApiLoaded"></google-legacy-loader>
<div hidden$="{{!isLoading}}" class='loading-container'>
<loading-box>Loading history data</loading-box>
<loading-box>Updating history data</loading-box>
</div>
<template is='dom-if' if='[[!isLoading]]'>
<template is='dom-if' if='[[groupedStateHistory.timeline]]'>
<state-history-chart-timeline data='[[groupedStateHistory.timeline]]'
is-single-device='[[isSingleDevice]]'>
</state-history-chart-timeline>
<div class$='[[computeContentClasses(isLoading)]]'>
<template is='dom-if' if='[[computeIsEmpty(stateHistory)]]'>
No state history found.
</template>
<template is='dom-if' if='[[groupedStateHistory.line]]'>
<template is='dom-repeat' items='[[groupedStateHistory.line]]'>
<state-history-chart-line unit='[[extractUnit(item)]]'
data='[[extractData(item)]]' is-single-device='[[isSingleDevice]]'>
</state-history-chart-line>
</template>
<state-history-chart-timeline data='[[groupedStateHistory.timeline]]'
is-single-device='[[isSingleDevice]]'>
</state-history-chart-timeline>
<template is='dom-repeat' items='[[groupedStateHistory.line]]'>
<state-history-chart-line unit='[[extractUnit(item)]]'
data='[[extractData(item)]]' is-single-device='[[isSingleDevice]]'>
</state-history-chart-line>
</template>
</template>
</div>
</template>
</dom-module>
@ -69,7 +74,7 @@
groupedStateHistory: {
type: Object,
computed: 'computeGroupedStateHistory(stateHistory)',
computed: 'computeGroupedStateHistory(isLoading, stateHistory)',
},
isSingleDevice: {
@ -82,11 +87,11 @@
return stateHistory && stateHistory.length == 1;
},
computeGroupedStateHistory: function(stateHistory) {
computeGroupedStateHistory: function(isLoading, stateHistory) {
var lineChartDevices = {};
var timelineDevices = [];
if (!stateHistory) {
if (isLoading || !stateHistory) {
return {line: unitStates, timeline: timelineDevices};
}
@ -129,10 +134,18 @@
});
},
computeContentClasses: function(isLoading) {
return isLoading ? 'loading' : '';
},
computeIsLoading: function(isLoadingData, apiLoaded) {
return isLoadingData || !apiLoaded;
},
computeIsEmpty: function(stateHistory) {
return stateHistory && stateHistory.length === 0;
},
extractUnit: function(arr) {
return arr[0];
},

View file

@ -129,12 +129,16 @@
},
changeEntityId: function(entityId) {
if (entityId == this.entityId) {
return;
}
this.entityId = entityId;
this.stateStoreChanged();
this.stateHistoryStoreChanged();
if (this.hasHistoryComponent && stateHistoryStore.isStale(entityId)) {
if (this.hasHistoryComponent && stateHistoryStore.shouldFetchEntity(entityId)) {
this.isLoadingHistoryData = true;
stateHistoryActions.fetch(entityId);
}

@ -1 +1 @@
Subproject commit 015edf9c28a63122aa8f6bc153f0c0ddfaad1caa
Subproject commit 5a7165b272fe2ed3e1b1432e2e621c3b971cc4bf

View file

@ -39,6 +39,10 @@
cursor: pointer;
}
paper-icon-item.logout {
margin-top: 16px;
}
.divider {
border-top: 1px solid #e0e0e0;
}
@ -62,10 +66,10 @@
<paper-toolbar>
<!-- forces paper toolbar to style title appropriate -->
<paper-icon-button hidden></paper-icon-button>
<div title>Home Assistant</div>
<div class="title">Home Assistant</div>
</paper-toolbar>
<paper-menu id='menu' class='layout vertical fit'
<paper-menu id='menu'
selectable='[data-panel]' attr-for-selected='data-panel'
on-iron-select='menuSelect' selected='[[selected]]'>
<paper-icon-item data-panel='states'>
@ -93,9 +97,7 @@
</paper-icon-item>
</template>
<div class='flex'></div>
<paper-icon-item data-panel='logout'>
<paper-icon-item data-panel='logout' class='logout'>
<iron-icon item-icon icon='exit-to-app'></iron-icon>
Log Out
</paper-icon-item>

View file

@ -10,7 +10,7 @@
<paper-scroll-header-panel class='fit'>
<paper-toolbar>
<paper-icon-button icon='menu' hidden$='[[!narrow]]' on-click='toggleMenu'></paper-icon-button>
<div title>
<div class="title">
<content select='[header-title]'></content>
</div>
<content select='[header-buttons]'></content>

View file

@ -1,5 +1,6 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html">
<link rel="import" href="../bower_components/paper-input/paper-input.html">
<link rel="import" href="./partial-base.html">
@ -14,6 +15,14 @@
.content.wide {
padding: 8px;
}
paper-input {
max-width: 200px;
}
.narrow paper-input {
margin-left: 8px;
}
</style>
<template>
<partial-base narrow="[[narrow]]">
@ -23,6 +32,9 @@
on-click="handleRefreshClick"></paper-icon-button>
<div class$="[[computeContentClasses(narrow)]]">
<paper-input label='Showing entries for' on-click='handleShowDatePicker'
value='[[computeDateCaption(selectedDate)]]'></paper-input>
<state-history-charts state-history="[[stateHistory]]"
is-loading-data="[[isLoadingData]]"></state-history-charts>
</div>
@ -32,6 +44,12 @@
<script>
(function() {
var stateHistoryActions = window.hass.stateHistoryActions;
var stateHistoryStore = window.hass.stateHistoryStore;
var uiActions = window.hass.uiActions;
function date_to_str(date) {
return date.getFullYear() + '-' + (date.getMonth()+1) + '-' + date.getDate();
}
Polymer({
is: 'partial-history',
@ -51,23 +69,42 @@
type: Boolean,
value: false,
},
selectedDate: {
type: String,
value: null,
observer: 'fetchIfNeeded',
},
},
stateHistoryStoreChanged: function(stateHistoryStore) {
if (stateHistoryStore.isStale()) {
this.isLoadingData = true;
stateHistoryActions.fetchAll();
}
else {
this.isLoadingData = false;
}
stateHistoryStoreChanged: function() {
this.isLoadingData = this.fetchIfNeeded();
this.stateHistory = this.isLoadingData ?
[] : stateHistoryStore.all(this.selectedDate);
},
this.stateHistory = stateHistoryStore.all;
computeDateCaption: function(selectedDate) {
return selectedDate || 'today';
},
fetchIfNeeded: function() {
if (stateHistoryStore.shouldFetch(this.selectedDate)) {
this.isLoadingData = true;
stateHistoryActions.fetchAll(this.selectedDate);
return true;
}
return false;
},
handleRefreshClick: function() {
this.isLoadingData = true;
stateHistoryActions.fetchAll();
stateHistoryActions.fetchAll(this.selectedDate);
},
handleShowDatePicker: function() {
uiActions.showDatePicker(function(selectedDate) {
this.selectedDate = date_to_str(selectedDate);
}.bind(this), this.selectedDate);
},
computeContentClasses: function(narrow) {

View file

@ -1,17 +1,22 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html">
<link rel="import" href="../bower_components/paper-input/paper-input.html">
<link rel="import" href="./partial-base.html">
<link rel="import" href="../components/ha-logbook.html">
<link rel="import" href="../components/loading-box.html">
<dom-module id="partial-logbook">
<style>
.content {
background-color: white;
padding: 8px;
}
.selected-date-container {
padding: 0 16px;
}
paper-input {
max-width: 200px;
}
</style>
<template>
<partial-base narrow="[[narrow]]">
@ -20,7 +25,15 @@
<paper-icon-button icon="refresh" header-buttons
on-click="handleRefresh"></paper-icon-button>
<ha-logbook entries="[[entries]]"></ha-logbook>
<div>
<div class='selected-date-container'>
<paper-input label='Showing entries for' on-click='handleShowDatePicker'
value='[[computeDateCaption(selectedDate)]]'></paper-input>
<loading-box hidden$='[[!isLoading]]'>Loading logbook entries</loading-box>
</div>
<ha-logbook entries="[[entries]]" hidden$='[[isLoading]]'></ha-logbook>
</div>
</partial-base>
</template>
</dom-module>
@ -29,6 +42,12 @@
(function() {
var storeListenerMixIn = window.hass.storeListenerMixIn;
var logbookActions = window.hass.logbookActions;
var logbookStore = window.hass.logbookStore;
var uiActions = window.hass.uiActions;
function date_to_str(date) {
return date.getFullYear() + '-' + (date.getMonth()+1) + '-' + date.getDate();
}
Polymer({
is: 'partial-logbook',
@ -41,22 +60,50 @@
value: false,
},
selectedDate: {
type: String,
value: null,
observer: 'fetchIfNeeded',
},
isLoading: {
type: Boolean,
value: true,
},
entries: {
type: Array,
value: [],
value: null,
},
},
logbookStoreChanged: function(logbookStore) {
if (logbookStore.isStale()) {
logbookActions.fetch();
}
logbookStoreChanged: function() {
this.isLoading = this.fetchIfNeeded();
var entries = logbookStore.all.toArray();
this.entries = entries.length > 0 ? entries : false;
},
this.entries = logbookStore.all.toArray();
computeDateCaption: function(selectedDate) {
return selectedDate || 'today';
},
fetchIfNeeded: function() {
if (logbookStore.shouldFetch(this.selectedDate)) {
this.isLoading = true;
logbookActions.fetch(this.selectedDate);
return true;
}
return false;
},
handleShowDatePicker: function() {
uiActions.showDatePicker(function(selectedDate) {
this.selectedDate = date_to_str(selectedDate);
}.bind(this), this.selectedDate);
},
handleRefresh: function() {
logbookActions.fetch();
logbookActions.fetch(this.selectedDate);
},
});
})();

View file

@ -1,10 +1,14 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-date-picker/paper-date-picker-dialog.html">
<link rel="import" href="../dialogs/more-info-dialog.html">
<dom-module id="modal-manager">
<template>
<more-info-dialog id="moreInfoDialog"></more-info-dialog>
<paper-date-picker-dialog id="datePicker"
on-value-changed='datePickerValueChanged'></paper-date-picker-dialog>
</template>
</dom-module>
@ -16,15 +20,32 @@
Polymer({
is: 'modal-manager',
properties: {
datePickerCallback: {
type: Function,
value: null,
},
},
ready: function() {
dispatcher.register(function(payload) {
switch (payload.actionType) {
case uiConstants.ACTION_SHOW_DIALOG_MORE_INFO:
this.$.moreInfoDialog.show(payload.entityId);
break;
case uiConstants.ACTION_SHOW_DATE_PICKER:
this.datePickerCallback = payload.dateSelectedCallback;
this.$.date = payload.date;
this.$.datePicker.toggle();
break;
}
}.bind(this));
},
datePickerValueChanged: function(ev) {
this.datePickerCallback(ev.target.value);
},
});
})();
</script>

View file

@ -8,8 +8,7 @@
text-transform: capitalize;
}
/* Accent the power button because the user should use that first */
paper-icon-button[focus] {
paper-icon-button[highlight] {
color: var(--accent-color);
}
@ -21,7 +20,7 @@
transition: max-height .5s ease-in;
}
.has-media_volume .volume {
.has-volume_level .volume {
max-height: 40px;
}
</style>
@ -29,25 +28,26 @@
<div class$='[[computeClassNames(stateObj)]]'>
<div class='layout horizontal'>
<div class='flex'>
<paper-icon-button icon='power-settings-new' focus$='[[isIdle]]'
on-tap='handleTogglePower'></paper-icon-button>
<paper-icon-button icon='power-settings-new' highlight$='[[isOff]]'
on-tap='handleTogglePower'
hidden$='[[computeHidePowerButton(isOff, supportsTurnOn, supportsTurnOff)]]'></paper-icon-button>
</div>
<div>
<template is='dom-if' if='[[!isIdle]]'>
<paper-icon-button icon='av:skip-previous'
on-tap='handlePrevious'></paper-icon-button>
<paper-icon-button icon='[[computePlayPauseIcon(stateObj)]]' focus$
on-tap='handlePlayPause'></paper-icon-button>
<paper-icon-button icon='av:skip-next'
on-tap='handleNext'></paper-icon-button>
<template is='dom-if' if='[[!isOff]]'>
<paper-icon-button icon='av:skip-previous' on-tap='handlePrevious'
hidden$='[[!supportsPreviousTrack]]'></paper-icon-button>
<paper-icon-button icon='[[computePlaybackControlIcon(stateObj)]]'
on-tap='handlePlaybackControl' highlight></paper-icon-button>
<paper-icon-button icon='av:skip-next' on-tap='handleNext'
hidden$='[[!supportsNextTrack]]'></paper-icon-button>
</template>
</div>
</div>
<div class='volume center horizontal layout'>
<div class='volume center horizontal layout' hidden$='[[!supportsVolumeSet]]'>
<paper-icon-button on-tap="handleVolumeTap"
icon="[[computeMuteVolumeIcon(isMuted)]]"></paper-icon-button>
<paper-slider hidden='[[isMuted]]'
min='0' max='100' value='{{volumeSliderValue}}'
<paper-slider disabled$='[[isMuted]]'
min='0' max='100' value='[[volumeSliderValue]]'
on-change='volumeSliderChanged' class='flex'>
</paper-slider>
</div>
@ -59,7 +59,7 @@
(function() {
var serviceActions = window.hass.serviceActions;
var uiUtil = window.hass.uiUtil;
var ATTRIBUTE_CLASSES = ['media_volume'];
var ATTRIBUTE_CLASSES = ['volume_level'];
Polymer({
is: 'more-info-media_player',
@ -70,9 +70,14 @@
observer: 'stateObjChanged',
},
isIdle: {
isOff: {
type: Boolean,
computed: 'computeIsIdle(stateObj)',
value: false,
},
isPlaying: {
type: Boolean,
value: false,
},
isMuted: {
@ -83,13 +88,58 @@
volumeSliderValue: {
type: Number,
value: 0,
}
},
supportsPause: {
type: Boolean,
value: false,
},
supportsVolumeSet: {
type: Boolean,
value: false,
},
supportsVolumeMute: {
type: Boolean,
value: false,
},
supportsPreviousTrack: {
type: Boolean,
value: false,
},
supportsNextTrack: {
type: Boolean,
value: false,
},
supportsTurnOn: {
type: Boolean,
value: false,
},
supportsTurnOff: {
type: Boolean,
value: false,
},
},
stateObjChanged: function(newVal, oldVal) {
if (newVal) {
this.volumeSliderValue = newVal.attributes.media_volume * 100;
this.isMuted = newVal.attributes.media_is_volume_muted;
this.isOff = newVal.state == 'off';
this.isPlaying = newVal.state == 'playing';
this.volumeSliderValue = newVal.attributes.volume_level * 100;
this.isMuted = newVal.attributes.is_volume_muted;
this.supportsPause = (newVal.attributes.supported_media_commands & 1) !== 0;
this.supportsVolumeSet = (newVal.attributes.supported_media_commands & 4) !== 0;
this.supportsVolumeMute = (newVal.attributes.supported_media_commands & 8) !== 0;
this.supportsPreviousTrack = (newVal.attributes.supported_media_commands & 16) !== 0;
this.supportsNextTrack = (newVal.attributes.supported_media_commands & 32) !== 0;
this.supportsTurnOn = (newVal.attributes.supported_media_commands & 128) !== 0;
this.supportsTurnOff = (newVal.attributes.supported_media_commands & 256) !== 0;
}
this.debounce('more-info-volume-animation-finish', function() {
@ -101,35 +151,37 @@
return uiUtil.attributeClassNames(stateObj, ATTRIBUTE_CLASSES);
},
computeMediaState: function(stateObj) {
return stateObj.state == 'idle' ? 'idle' : stateObj.attributes.media_state;
},
computeIsIdle: function(stateObj) {
return stateObj.state == 'idle';
},
computePowerButtonCaption: function(isIdle) {
return isIdle ? 'Turn on' : 'Turn off';
computeIsOff: function(stateObj) {
return stateObj.state == 'off';
},
computeMuteVolumeIcon: function(isMuted) {
return isMuted ? 'av:volume-off' : 'av:volume-up';
},
computePlayPauseIcon: function(stateObj) {
return stateObj.attributes.media_state == 'playing' ? 'av:pause' : 'av:play-arrow';
computePlaybackControlIcon: function(stateObj) {
if (this.isPlaying) {
return this.supportsPause ? 'av:pause' : 'av:stop';
}
return 'av:play-arrow';
},
computeHidePowerButton: function(isOff, supportsTurnOn, supportsTurnOff) {
return isOff ? !supportsTurnOn : !supportsTurnOff;
},
handleTogglePower: function() {
this.callService(this.isIdle ? 'turn_on' : 'turn_off');
this.callService(this.isOff ? 'turn_on' : 'turn_off');
},
handlePrevious: function() {
this.callService('media_prev_track');
this.callService('media_previous_track');
},
handlePlayPause: function() {
handlePlaybackControl: function() {
if (this.isPlaying && !this.supportsPause) {
alert('This case is not supported yet');
}
this.callService('media_play_pause');
},
@ -138,14 +190,16 @@
},
handleVolumeTap: function() {
this.callService('volume_mute', { mute: !this.isMuted });
if (!this.supportsVolumeMute) {
return;
}
this.callService('volume_mute', { is_volume_muted: !this.isMuted });
},
volumeSliderChanged: function(ev) {
var volPercentage = parseFloat(ev.target.value);
var vol = volPercentage > 0 ? volPercentage / 100 : 0;
this.callService('volume_set', { volume: vol });
this.callService('volume_set', { volume_level: vol });
},
callService: function(service, data) {

View file

@ -48,7 +48,7 @@ window.hass.uiUtil.domainIcon = function(domain, state) {
case "media_player":
var icon = "hardware:cast";
if (state && state !== "idle") {
if (state && state !== "off" && state !== 'idle') {
icon += "-connected";
}

View file

@ -36,6 +36,7 @@
window.hass.uiConstants = {
ACTION_SHOW_DIALOG_MORE_INFO: 'ACTION_SHOW_DIALOG_MORE_INFO',
ACTION_SHOW_DATE_PICKER: 'ACTION_SHOW_DATE_PICKER',
STATE_FILTERS: {
'group': 'Groups',
@ -52,6 +53,16 @@
});
},
showDatePicker: function(dateSelectedCallback, startDate) {
startDate = startDate || null;
dispatcher.dispatch({
actionType: window.hass.uiConstants.ACTION_SHOW_DATE_PICKER,
dateSelectedCallback: dateSelectedCallback,
startDate: startDate,
});
},
validateAuth: function(authToken, rememberLogin) {
authActions.validate(authToken, {
useStreaming: preferenceStore.useStreaming,

File diff suppressed because one or more lines are too long

View file

@ -9,12 +9,16 @@ from datetime import timedelta
from itertools import groupby
from collections import defaultdict
import homeassistant.util.dt as date_util
import homeassistant.util.dt as dt_util
import homeassistant.components.recorder as recorder
from homeassistant.const import HTTP_BAD_REQUEST
DOMAIN = 'history'
DEPENDENCIES = ['recorder', 'http']
URL_HISTORY_PERIOD = re.compile(
r'/api/history/period(?:/(?P<date>\d{4}-\d{1,2}-\d{1,2})|)')
def last_5_states(entity_id):
""" Return the last 5 states for entity_id. """
@ -111,8 +115,7 @@ def setup(hass, config):
r'recent_states'),
_api_last_5_states)
hass.http.register_path(
'GET', re.compile(r'/api/history/period'), _api_history_period)
hass.http.register_path('GET', URL_HISTORY_PERIOD, _api_history_period)
return True
@ -128,10 +131,25 @@ def _api_last_5_states(handler, path_match, data):
def _api_history_period(handler, path_match, data):
""" Return history over a period of time. """
# 1 day for now..
start_time = date_util.utcnow() - timedelta(seconds=86400)
date_str = path_match.group('date')
one_day = timedelta(seconds=86400)
if date_str:
start_date = dt_util.date_str_to_date(date_str)
if start_date is None:
handler.write_json_message("Error parsing JSON", HTTP_BAD_REQUEST)
return
start_time = dt_util.as_utc(dt_util.start_of_local_day(start_date))
else:
start_time = dt_util.utcnow() - one_day
end_time = start_time + one_day
print("Fetchign", start_time, end_time)
entity_id = data.get('filter_entity_id')
handler.write_json(
state_changes_during_period(start_time, entity_id=entity_id).values())
state_changes_during_period(start_time, end_time, entity_id).values())

View file

@ -8,7 +8,7 @@ import logging
from homeassistant.const import (
SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK,
SERVICE_MEDIA_PLAY_PAUSE)
@ -43,7 +43,7 @@ def media_next_track(hass):
def media_prev_track(hass):
""" Press the keyboard button for prev track. """
hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK)
hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK)
def setup(hass, config):
@ -79,7 +79,7 @@ def setup(hass, config):
lambda service:
keyboard.tap_key(keyboard.media_next_track_key))
hass.services.register(DOMAIN, SERVICE_MEDIA_PREV_TRACK,
hass.services.register(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK,
lambda service:
keyboard.tap_key(keyboard.media_prev_track_key))

View file

@ -53,6 +53,7 @@ import os
import csv
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import ToggleEntity
import homeassistant.util as util
from homeassistant.const import (
@ -96,6 +97,11 @@ DISCOVERY_PLATFORMS = {
discovery.services.PHILIPS_HUE: 'hue',
}
PROP_TO_ATTR = {
'brightness': ATTR_BRIGHTNESS,
'color_xy': ATTR_XY_COLOR,
}
_LOGGER = logging.getLogger(__name__)
@ -251,7 +257,8 @@ def setup(hass, config):
light.turn_on(**params)
for light in target_lights:
light.update_ha_state(True)
if light.should_poll:
light.update_ha_state(True)
# Listen for light on and light off service calls
hass.services.register(DOMAIN, SERVICE_TURN_ON,
@ -261,3 +268,41 @@ def setup(hass, config):
handle_light_service)
return True
class Light(ToggleEntity):
""" Represents a light within Home Assistant. """
# pylint: disable=no-self-use
@property
def brightness(self):
""" Brightness of this light between 0..255. """
return None
@property
def color_xy(self):
""" XY color value [float, float]. """
return None
@property
def device_state_attributes(self):
""" Returns device specific state attributes. """
return None
@property
def state_attributes(self):
""" Returns optional state attributes. """
data = {}
if self.is_on:
for prop, attr in PROP_TO_ATTR.items():
value = getattr(self, prop)
if value:
data[attr] = value
device_attr = self.device_state_attributes
if device_attr is not None:
data.update(device_attr)
return data

View file

@ -7,9 +7,8 @@ Demo platform that implements lights.
"""
import random
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME
from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_XY_COLOR
from homeassistant.components.light import (
Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR)
LIGHT_COLORS = [
@ -22,16 +21,16 @@ LIGHT_COLORS = [
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Find and return demo lights. """
add_devices_callback([
DemoLight("Bed Light", STATE_OFF),
DemoLight("Ceiling", STATE_ON),
DemoLight("Kitchen", STATE_ON)
DemoLight("Bed Light", False),
DemoLight("Ceiling", True),
DemoLight("Kitchen", True)
])
class DemoLight(ToggleEntity):
class DemoLight(Light):
""" Provides a demo switch. """
def __init__(self, name, state, xy=None, brightness=180):
self._name = name or DEVICE_DEFAULT_NAME
self._name = name
self._state = state
self._xy = xy or random.choice(LIGHT_COLORS)
self._brightness = brightness
@ -47,27 +46,23 @@ class DemoLight(ToggleEntity):
return self._name
@property
def state(self):
""" Returns the name of the device if any. """
return self._state
def brightness(self):
""" Brightness of this light between 0..255. """
return self._brightness
@property
def state_attributes(self):
""" Returns optional state attributes. """
if self.is_on:
return {
ATTR_BRIGHTNESS: self._brightness,
ATTR_XY_COLOR: self._xy,
}
def color_xy(self):
""" XY color value. """
return self._xy
@property
def is_on(self):
""" True if device is on. """
return self._state == STATE_ON
return self._state
def turn_on(self, **kwargs):
""" Turn the device on. """
self._state = STATE_ON
self._state = True
if ATTR_XY_COLOR in kwargs:
self._xy = kwargs[ATTR_XY_COLOR]
@ -75,6 +70,9 @@ class DemoLight(ToggleEntity):
if ATTR_BRIGHTNESS in kwargs:
self._brightness = kwargs[ATTR_BRIGHTNESS]
self.update_ha_state()
def turn_off(self, **kwargs):
""" Turn the device off. """
self._state = STATE_OFF
self._state = False
self.update_ha_state()

View file

@ -6,10 +6,9 @@ from urllib.parse import urlparse
from homeassistant.loader import get_component
import homeassistant.util as util
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import CONF_HOST
from homeassistant.const import CONF_HOST, DEVICE_DEFAULT_NAME
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_TRANSITION,
Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_TRANSITION,
ATTR_FLASH, FLASH_LONG, FLASH_SHORT)
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
@ -131,7 +130,7 @@ def request_configuration(host, hass, add_devices_callback):
)
class HueLight(ToggleEntity):
class HueLight(Light):
""" Represents a Hue light """
def __init__(self, light_id, info, bridge, update_lights):
@ -149,19 +148,17 @@ class HueLight(ToggleEntity):
@property
def name(self):
""" Get the mame of the Hue light. """
return self.info.get('name', 'No name')
return self.info.get('name', DEVICE_DEFAULT_NAME)
@property
def state_attributes(self):
""" Returns optional state attributes. """
attr = {}
def brightness(self):
""" Brightness of this light between 0..255. """
return self.info['state']['bri']
if self.is_on:
attr[ATTR_BRIGHTNESS] = self.info['state']['bri']
if 'xy' in self.info['state']:
attr[ATTR_XY_COLOR] = self.info['state']['xy']
return attr
@property
def color_xy(self):
""" XY color value. """
return self.info['state'].get('xy')
@property
def is_on(self):

View file

@ -23,9 +23,8 @@ light:
"""
import logging
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME
from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.const import DEVICE_DEFAULT_NAME
from homeassistant.components.light import Light, ATTR_BRIGHTNESS
_LOGGER = logging.getLogger(__name__)
@ -43,18 +42,12 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
lights = []
for i in range(1, 5):
if 'group_%d_name' % (i) in config:
lights.append(
LimitlessLED(
led,
i,
config['group_%d_name' % (i)]
)
)
lights.append(LimitlessLED(led, i, config['group_%d_name' % (i)]))
add_devices_callback(lights)
class LimitlessLED(ToggleEntity):
class LimitlessLED(Light):
""" Represents a LimitlessLED light """
def __init__(self, led, group, name):
@ -65,7 +58,7 @@ class LimitlessLED(ToggleEntity):
self.led.off(self.group)
self._name = name or DEVICE_DEFAULT_NAME
self._state = STATE_OFF
self._state = False
self._brightness = 100
@property
@ -79,33 +72,26 @@ class LimitlessLED(ToggleEntity):
return self._name
@property
def state(self):
""" Returns the name of the device if any. """
return self._state
@property
def state_attributes(self):
""" Returns optional state attributes. """
if self.is_on:
return {
ATTR_BRIGHTNESS: self._brightness,
}
def brightness(self):
return self._brightness
@property
def is_on(self):
""" True if device is on. """
return self._state == STATE_ON
return self._state
def turn_on(self, **kwargs):
""" Turn the device on. """
self._state = STATE_ON
self._state = True
if ATTR_BRIGHTNESS in kwargs:
self._brightness = kwargs[ATTR_BRIGHTNESS]
self.led.set_brightness(self._brightness, self.group)
self.update_ha_state()
def turn_off(self, **kwargs):
""" Turn the device off. """
self._state = STATE_OFF
self._state = False
self.led.off(self.group)
self.update_ha_state()

View file

@ -1,9 +1,8 @@
""" Support for Tellstick lights. """
import logging
# pylint: disable=no-name-in-module, import-error
from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.components.light import Light, ATTR_BRIGHTNESS
from homeassistant.const import ATTR_FRIENDLY_NAME
from homeassistant.helpers.entity import ToggleEntity
import tellcore.constants as tellcore_constants
@ -27,7 +26,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
add_devices_callback(lights)
class TellstickLight(ToggleEntity):
class TellstickLight(Light):
""" Represents a tellstick light """
last_sent_command_mask = (tellcore_constants.TELLSTICK_TURNON |
tellcore_constants.TELLSTICK_TURNOFF |
@ -38,7 +37,7 @@ class TellstickLight(ToggleEntity):
def __init__(self, tellstick):
self.tellstick = tellstick
self.state_attr = {ATTR_FRIENDLY_NAME: tellstick.name}
self.brightness = 0
self._brightness = 0
@property
def name(self):
@ -48,34 +47,28 @@ class TellstickLight(ToggleEntity):
@property
def is_on(self):
""" True if switch is on. """
return self.brightness > 0
return self._brightness > 0
@property
def brightness(self):
""" Brightness of this light between 0..255. """
return self._brightness
def turn_off(self, **kwargs):
""" Turns the switch off. """
self.tellstick.turn_off()
self.brightness = 0
self._brightness = 0
def turn_on(self, **kwargs):
""" Turns the switch on. """
brightness = kwargs.get(ATTR_BRIGHTNESS)
if brightness is None:
self.brightness = 255
self._brightness = 255
else:
self.brightness = brightness
self._brightness = brightness
self.tellstick.dim(self.brightness)
@property
def state_attributes(self):
""" Returns optional state attributes. """
attr = {
ATTR_FRIENDLY_NAME: self.name
}
attr[ATTR_BRIGHTNESS] = int(self.brightness)
return attr
self.tellstick.dim(self._brightness)
def update(self):
""" Update state of the light. """
@ -83,12 +76,12 @@ class TellstickLight(ToggleEntity):
self.last_sent_command_mask)
if last_command == tellcore_constants.TELLSTICK_TURNON:
self.brightness = 255
self._brightness = 255
elif last_command == tellcore_constants.TELLSTICK_TURNOFF:
self.brightness = 0
self._brightness = 0
elif (last_command == tellcore_constants.TELLSTICK_DIM or
last_command == tellcore_constants.TELLSTICK_UP or
last_command == tellcore_constants.TELLSTICK_DOWN):
last_sent_value = self.tellstick.last_sent_value()
if last_sent_value is not None:
self.brightness = last_sent_value
self._brightness = last_sent_value

View file

@ -4,12 +4,14 @@ homeassistant.components.logbook
Parses events and generates a human log.
"""
from datetime import timedelta
from itertools import groupby
import re
from homeassistant import State, DOMAIN as HA_DOMAIN
from homeassistant.const import (
EVENT_STATE_CHANGED, STATE_HOME, STATE_ON, STATE_OFF,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, HTTP_BAD_REQUEST)
import homeassistant.util.dt as dt_util
import homeassistant.components.recorder as recorder
import homeassistant.components.sun as sun
@ -17,12 +19,10 @@ import homeassistant.components.sun as sun
DOMAIN = "logbook"
DEPENDENCIES = ['recorder', 'http']
URL_LOGBOOK = '/api/logbook'
URL_LOGBOOK = re.compile(r'/api/logbook(?:/(?P<date>\d{4}-\d{1,2}-\d{1,2})|)')
QUERY_EVENTS_AFTER = "SELECT * FROM events WHERE time_fired > ?"
QUERY_EVENTS_BETWEEN = """
SELECT * FROM events WHERE time_fired > ? AND time_fired < ?
ORDER BY time_fired
"""
GROUP_BY_MINUTES = 15
@ -37,11 +37,26 @@ def setup(hass, config):
def _handle_get_logbook(handler, path_match, data):
""" Return logbook entries. """
start_today = dt_util.now().replace(hour=0, minute=0, second=0)
date_str = path_match.group('date')
handler.write_json(humanify(
recorder.query_events(
QUERY_EVENTS_AFTER, (dt_util.as_utc(start_today),))))
if date_str:
start_date = dt_util.date_str_to_date(date_str)
if start_date is None:
handler.write_json_message("Error parsing JSON", HTTP_BAD_REQUEST)
return
start_day = dt_util.start_of_local_day(start_date)
else:
start_day = dt_util.start_of_local_day()
end_day = start_day + timedelta(days=1)
events = recorder.query_events(
QUERY_EVENTS_BETWEEN,
(dt_util.as_utc(start_day), dt_util.as_utc(end_day)))
handler.write_json(humanify(events))
class Entry(object):

View file

@ -10,11 +10,12 @@ from homeassistant.components import discovery
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON,
STATE_OFF, STATE_UNKNOWN, STATE_PLAYING,
ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_SET,
SERVICE_VOLUME_MUTE,
SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK)
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK)
DOMAIN = 'media_player'
DEPENDENCIES = []
@ -28,28 +29,70 @@ DISCOVERY_PLATFORMS = {
SERVICE_YOUTUBE_VIDEO = 'play_youtube_video'
STATE_NO_APP = 'idle'
ATTR_STATE = 'state'
ATTR_OPTIONS = 'options'
ATTR_MEDIA_STATE = 'media_state'
ATTR_MEDIA_VOLUME_LEVEL = 'volume_level'
ATTR_MEDIA_VOLUME_MUTED = 'is_volume_muted'
ATTR_MEDIA_SEEK_POSITION = 'seek_position'
ATTR_MEDIA_CONTENT_ID = 'media_content_id'
ATTR_MEDIA_CONTENT_TYPE = 'media_content_type'
ATTR_MEDIA_DURATION = 'media_duration'
ATTR_MEDIA_TITLE = 'media_title'
ATTR_MEDIA_ARTIST = 'media_artist'
ATTR_MEDIA_ALBUM = 'media_album'
ATTR_MEDIA_IMAGE_URL = 'media_image_url'
ATTR_MEDIA_VOLUME = 'media_volume'
ATTR_MEDIA_IS_VOLUME_MUTED = 'media_is_volume_muted'
ATTR_MEDIA_DURATION = 'media_duration'
ATTR_MEDIA_DATE = 'media_date'
ATTR_MEDIA_ALBUM_NAME = 'media_album_name'
ATTR_MEDIA_ALBUM_ARTIST = 'media_album_artist'
ATTR_MEDIA_TRACK = 'media_track'
ATTR_MEDIA_SERIES_TITLE = 'media_series_title'
ATTR_MEDIA_SEASON = 'media_season'
ATTR_MEDIA_EPISODE = 'media_episode'
ATTR_APP_ID = 'app_id'
ATTR_APP_NAME = 'app_name'
ATTR_SUPPORTED_MEDIA_COMMANDS = 'supported_media_commands'
MEDIA_STATE_UNKNOWN = 'unknown'
MEDIA_STATE_PLAYING = 'playing'
MEDIA_STATE_PAUSED = 'paused'
MEDIA_STATE_STOPPED = 'stopped'
MEDIA_TYPE_MUSIC = 'music'
MEDIA_TYPE_TVSHOW = 'tvshow'
MEDIA_TYPE_VIDEO = 'movie'
SUPPORT_PAUSE = 1
SUPPORT_SEEK = 2
SUPPORT_VOLUME_SET = 4
SUPPORT_VOLUME_MUTE = 8
SUPPORT_PREVIOUS_TRACK = 16
SUPPORT_NEXT_TRACK = 32
SUPPORT_YOUTUBE = 64
SUPPORT_TURN_ON = 128
SUPPORT_TURN_OFF = 256
YOUTUBE_COVER_URL_FORMAT = 'http://img.youtube.com/vi/{}/1.jpg'
YOUTUBE_COVER_URL_FORMAT = 'https://img.youtube.com/vi/{}/1.jpg'
SERVICE_TO_METHOD = {
SERVICE_TURN_ON: 'turn_on',
SERVICE_TURN_OFF: 'turn_off',
SERVICE_VOLUME_UP: 'volume_up',
SERVICE_VOLUME_DOWN: 'volume_down',
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_PREVIOUS_TRACK: 'media_previous_track',
}
ATTR_TO_PROPERTY = [
ATTR_MEDIA_VOLUME_LEVEL,
ATTR_MEDIA_VOLUME_MUTED,
ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE,
ATTR_MEDIA_DURATION,
ATTR_MEDIA_TITLE,
ATTR_MEDIA_ARTIST,
ATTR_MEDIA_ALBUM_NAME,
ATTR_MEDIA_ALBUM_ARTIST,
ATTR_MEDIA_TRACK,
ATTR_MEDIA_SERIES_TITLE,
ATTR_MEDIA_SEASON,
ATTR_MEDIA_EPISODE,
ATTR_APP_ID,
ATTR_APP_NAME,
ATTR_SUPPORTED_MEDIA_COMMANDS,
]
def is_on(hass, entity_id=None):
@ -58,7 +101,7 @@ def is_on(hass, entity_id=None):
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_OFF)
for entity_id in entity_ids)
@ -90,21 +133,22 @@ def volume_down(hass, entity_id=None):
hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN, data)
def volume_mute(hass, entity_id=None):
""" Send the media player the command to toggle its mute state. """
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
def mute_volume(hass, mute, entity_id=None):
""" Send the media player the command for volume down. """
data = {ATTR_MEDIA_VOLUME_MUTED: mute}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE, data)
def volume_set(hass, entity_id=None, volume=None):
""" Set volume on media player. """
data = {
key: value for key, value in [
(ATTR_ENTITY_ID, entity_id),
(ATTR_MEDIA_VOLUME, volume),
] if value is not None
}
def set_volume_level(hass, volume, entity_id=None):
""" Send the media player the command for volume down. """
data = {ATTR_MEDIA_VOLUME_LEVEL: volume}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_VOLUME_SET, data)
@ -137,24 +181,11 @@ def media_next_track(hass, entity_id=None):
hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, data)
def media_prev_track(hass, entity_id=None):
def media_previous_track(hass, entity_id=None):
""" Send the media player the command for prev track. """
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
hass.services.call(DOMAIN, SERVICE_MEDIA_PREV_TRACK, data)
SERVICE_TO_METHOD = {
SERVICE_TURN_ON: 'turn_on',
SERVICE_TURN_OFF: 'turn_off',
SERVICE_VOLUME_UP: 'volume_up',
SERVICE_VOLUME_DOWN: 'volume_down',
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',
}
hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data)
def setup(hass, config):
@ -180,35 +211,56 @@ def setup(hass, config):
for service in SERVICE_TO_METHOD:
hass.services.register(DOMAIN, service, media_player_service_handler)
def volume_set_service(service, volume):
def volume_set_service(service):
""" Set specified volume on the media player. """
target_players = component.extract_from_service(service)
if ATTR_MEDIA_VOLUME_LEVEL not in service.data:
return
volume = service.data[ATTR_MEDIA_VOLUME_LEVEL]
for player in target_players:
player.volume_set(volume)
player.set_volume_level(volume)
if player.should_poll:
player.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_VOLUME_SET,
lambda service:
volume_set_service(
service, service.data.get('volume')))
hass.services.register(DOMAIN, SERVICE_VOLUME_SET, volume_set_service)
def volume_mute_service(service, mute):
def volume_mute_service(service):
""" Mute (true) or unmute (false) the media player. """
target_players = component.extract_from_service(service)
if ATTR_MEDIA_VOLUME_MUTED not in service.data:
return
mute = service.data[ATTR_MEDIA_VOLUME_MUTED]
for player in target_players:
player.volume_mute(mute)
player.mute_volume(mute)
if player.should_poll:
player.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE,
lambda service:
volume_mute_service(
service, service.data.get('mute')))
hass.services.register(DOMAIN, SERVICE_VOLUME_MUTE, volume_mute_service)
def media_seek_service(service):
""" Seek to a position. """
target_players = component.extract_from_service(service)
if ATTR_MEDIA_SEEK_POSITION not in service.data:
return
position = service.data[ATTR_MEDIA_SEEK_POSITION]
for player in target_players:
player.seek(position)
if player.should_poll:
player.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_MEDIA_SEEK, media_seek_service)
def play_youtube_video_service(service, media_id):
""" Plays specified media_id on the media player. """
@ -239,51 +291,217 @@ def setup(hass, config):
class MediaPlayerDevice(Entity):
""" ABC for media player devices. """
# pylint: disable=too-many-public-methods,no-self-use
# Implement these for your media player
@property
def state(self):
""" State of the player. """
return STATE_UNKNOWN
@property
def volume_level(self):
""" Volume level of the media player (0..1). """
return None
@property
def is_volume_muted(self):
""" Boolean if volume is currently muted. """
return None
@property
def media_content_id(self):
""" Content ID of current playing media. """
return None
@property
def media_content_type(self):
""" Content type of current playing media. """
return None
@property
def media_duration(self):
""" Duration of current playing media in seconds. """
return None
@property
def media_image_url(self):
""" Image url of current playing media. """
return None
@property
def media_title(self):
""" Title of current playing media. """
return None
@property
def media_artist(self):
""" Artist of current playing media. (Music track only) """
return None
@property
def media_album_name(self):
""" Album name of current playing media. (Music track only) """
return None
@property
def media_album_artist(self):
""" Album arist of current playing media. (Music track only) """
return None
@property
def media_track(self):
""" Track number of current playing media. (Music track only) """
return None
@property
def media_series_title(self):
""" Series title of current playing media. (TV Show only)"""
return None
@property
def media_season(self):
""" Season of current playing media. (TV Show only) """
return None
@property
def media_episode(self):
""" Episode of current playing media. (TV Show only) """
return None
@property
def app_id(self):
""" ID of the current running app. """
return None
@property
def app_name(self):
""" Name of the current running app. """
return None
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
return 0
@property
def device_state_attributes(self):
""" Extra attributes a device wants to expose. """
return None
def turn_on(self):
""" turn media player on. """
pass
""" turn the media player on. """
raise NotImplementedError()
def turn_off(self):
""" turn media player off. """
pass
""" turn the media player off. """
raise NotImplementedError()
def volume_up(self):
""" volume_up media player. """
pass
def mute_volume(self, mute):
""" mute the volume. """
raise NotImplementedError()
def volume_down(self):
""" volume_down media player. """
pass
def volume_mute(self, mute):
""" mute (true) or unmute (false) media player. """
pass
def volume_set(self, volume):
""" set volume level of media player. """
pass
def media_play_pause(self):
""" media_play_pause media player. """
pass
def set_volume_level(self, volume):
""" set volume level, range 0..1. """
raise NotImplementedError()
def media_play(self):
""" media_play media player. """
pass
""" Send play commmand. """
raise NotImplementedError()
def media_pause(self):
""" media_pause media player. """
pass
""" Send pause command. """
raise NotImplementedError()
def media_prev_track(self):
""" media_prev_track media player. """
pass
def media_previous_track(self):
""" Send previous track command. """
raise NotImplementedError()
def media_next_track(self):
""" media_next_track media player. """
pass
""" Send next track command. """
raise NotImplementedError()
def media_seek(self, position):
""" Send seek command. """
raise NotImplementedError()
def play_youtube(self, media_id):
""" Plays a YouTube media. """
pass
raise NotImplementedError()
# No need to overwrite these.
@property
def support_pause(self):
""" Boolean if pause is supported. """
return bool(self.supported_media_commands & SUPPORT_PAUSE)
@property
def support_seek(self):
""" Boolean if seek is supported. """
return bool(self.supported_media_commands & SUPPORT_SEEK)
@property
def support_volume_set(self):
""" Boolean if setting volume is supported. """
return bool(self.supported_media_commands & SUPPORT_VOLUME_SET)
@property
def support_volume_mute(self):
""" Boolean if muting volume is supported. """
return bool(self.supported_media_commands & SUPPORT_VOLUME_MUTE)
@property
def support_previous_track(self):
""" Boolean if previous track command supported. """
return bool(self.supported_media_commands & SUPPORT_PREVIOUS_TRACK)
@property
def support_next_track(self):
""" Boolean if next track command supported. """
return bool(self.supported_media_commands & SUPPORT_NEXT_TRACK)
@property
def support_youtube(self):
""" Boolean if YouTube is supported. """
return bool(self.supported_media_commands & SUPPORT_YOUTUBE)
def volume_up(self):
""" volume_up media player. """
if self.volume_level < 1:
self.set_volume_level(min(1, self.volume_level + .1))
def volume_down(self):
""" volume_down media player. """
if self.volume_level > 0:
self.set_volume_level(max(0, self.volume_level - .1))
def media_play_pause(self):
""" media_play_pause media player. """
if self.state == STATE_PLAYING:
self.media_pause()
else:
self.media_play()
@property
def state_attributes(self):
""" Return the state attributes. """
if self.state == STATE_OFF:
state_attr = {
ATTR_SUPPORTED_MEDIA_COMMANDS: self.supported_media_commands,
}
else:
state_attr = {
attr: getattr(self, attr) for attr
in ATTR_TO_PROPERTY if getattr(self, attr)
}
if self.media_image_url:
state_attr[ATTR_ENTITY_PICTURE] = self.media_image_url
device_attr = self.device_state_attributes
if device_attr:
state_attr.update(device_attr)
return state_attr

View file

@ -14,18 +14,21 @@ try:
except ImportError:
pychromecast = None
from homeassistant.const import ATTR_ENTITY_PICTURE
from homeassistant.const import (
STATE_PLAYING, STATE_PAUSED, STATE_IDLE, STATE_OFF,
STATE_UNKNOWN)
# ATTR_MEDIA_ALBUM, ATTR_MEDIA_IMAGE_URL,
# ATTR_MEDIA_ARTIST,
from homeassistant.components.media_player import (
MediaPlayerDevice, STATE_NO_APP, ATTR_MEDIA_STATE, ATTR_MEDIA_TITLE,
ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_DURATION,
ATTR_MEDIA_VOLUME, ATTR_MEDIA_IS_VOLUME_MUTED,
MEDIA_STATE_PLAYING, MEDIA_STATE_PAUSED, MEDIA_STATE_STOPPED,
MEDIA_STATE_UNKNOWN)
MediaPlayerDevice,
SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_YOUTUBE,
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO)
CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png'
SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_YOUTUBE
# pylint: disable=unused-argument
@ -61,6 +64,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class CastDevice(MediaPlayerDevice):
""" Represents a Cast device on the network. """
# pylint: disable=too-many-public-methods
def __init__(self, host):
self.cast = pychromecast.Chromecast(host)
self.youtube = youtube.YouTubeController()
@ -73,6 +78,8 @@ class CastDevice(MediaPlayerDevice):
self.cast_status = self.cast.status
self.media_status = self.cast.media_controller.status
# Entity properties and methods
@property
def should_poll(self):
return False
@ -82,57 +89,121 @@ class CastDevice(MediaPlayerDevice):
""" Returns the name of the device. """
return self.cast.device.friendly_name
# MediaPlayerDevice properties and methods
@property
def state(self):
""" Returns the state of the device. """
if self.cast.is_idle:
return STATE_NO_APP
""" State of the player. """
if self.media_status is None:
return STATE_UNKNOWN
elif self.media_status.player_is_playing:
return STATE_PLAYING
elif self.media_status.player_is_paused:
return STATE_PAUSED
elif self.media_status.player_is_idle:
return STATE_IDLE
elif self.cast.is_idle:
return STATE_OFF
else:
return self.cast.app_display_name
return STATE_UNKNOWN
@property
def media_state(self):
""" Returns the media state. """
media_controller = self.cast.media_controller
if media_controller.is_playing:
return MEDIA_STATE_PLAYING
elif media_controller.is_paused:
return MEDIA_STATE_PAUSED
elif media_controller.is_idle:
return MEDIA_STATE_STOPPED
else:
return MEDIA_STATE_UNKNOWN
def volume_level(self):
""" Volume level of the media player (0..1). """
return self.cast_status.volume_level if self.cast_status else None
@property
def state_attributes(self):
""" Returns the state attributes. """
cast_status = self.cast_status
media_status = self.media_status
media_controller = self.cast.media_controller
def is_volume_muted(self):
""" Boolean if volume is currently muted. """
return self.cast_status.volume_muted if self.cast_status else None
state_attr = {
ATTR_MEDIA_STATE: self.media_state,
'application_id': self.cast.app_id,
}
@property
def media_content_id(self):
""" Content ID of current playing media. """
return self.media_status.content_id if self.media_status else None
if cast_status:
state_attr[ATTR_MEDIA_VOLUME] = cast_status.volume_level
state_attr[ATTR_MEDIA_IS_VOLUME_MUTED] = cast_status.volume_muted
@property
def media_content_type(self):
""" Content type of current playing media. """
if self.media_status is None:
return None
elif self.media_status.media_is_tvshow:
return MEDIA_TYPE_TVSHOW
elif self.media_status.media_is_movie:
return MEDIA_TYPE_VIDEO
elif self.media_status.media_is_musictrack:
return MEDIA_TYPE_MUSIC
return None
if media_status.content_id:
state_attr[ATTR_MEDIA_CONTENT_ID] = media_status.content_id
@property
def media_duration(self):
""" Duration of current playing media in seconds. """
return self.media_status.duration if self.media_status else None
if media_status.duration:
state_attr[ATTR_MEDIA_DURATION] = media_status.duration
@property
def media_image_url(self):
""" Image url of current playing media. """
if self.media_status is None:
return None
if media_controller.title:
state_attr[ATTR_MEDIA_TITLE] = media_controller.title
images = self.media_status.images
if media_controller.thumbnail:
state_attr[ATTR_ENTITY_PICTURE] = media_controller.thumbnail
return images[0].url if images else None
return state_attr
@property
def media_title(self):
""" Title of current playing media. """
return self.media_status.title if self.media_status else None
@property
def media_artist(self):
""" Artist of current playing media. (Music track only) """
return self.media_status.artist if self.media_status else None
@property
def media_album(self):
""" Album of current playing media. (Music track only) """
return self.media_status.album_name if self.media_status else None
@property
def media_album_artist(self):
""" Album arist of current playing media. (Music track only) """
return self.media_status.album_artist if self.media_status else None
@property
def media_track(self):
""" Track number of current playing media. (Music track only) """
return self.media_status.track if self.media_status else None
@property
def media_series_title(self):
""" Series title of current playing media. (TV Show only)"""
return self.media_status.series_title if self.media_status else None
@property
def media_season(self):
""" Season of current playing media. (TV Show only) """
return self.media_status.season if self.media_status else None
@property
def media_episode(self):
""" Episode of current playing media. (TV Show only) """
return self.media_status.episode if self.media_status else None
@property
def app_id(self):
""" ID of the current running app. """
return self.cast.app_id
@property
def app_name(self):
""" Name of the current running app. """
return self.cast.app_display_name
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
return SUPPORT_CAST
def turn_on(self):
""" Turns on the ChromeCast. """
@ -145,57 +216,42 @@ class CastDevice(MediaPlayerDevice):
CAST_SPLASH, pychromecast.STREAM_TYPE_BUFFERED)
def turn_off(self):
""" Service to exit any running app on the specimedia player ChromeCast and
shows idle screen. Will quit all ChromeCasts if nothing specified.
"""
""" Turns Chromecast off. """
self.cast.quit_app()
def volume_up(self):
""" Service to send the chromecast the command for volume up. """
self.cast.volume_up()
def volume_down(self):
""" Service to send the chromecast the command for volume down. """
self.cast.volume_down()
def volume_mute(self, mute):
""" Set media player to mute volume. """
def mute_volume(self, mute):
""" mute the volume. """
self.cast.set_volume_muted(mute)
def volume_set(self, volume):
""" Set media player volume, range of volume 0..1 """
def set_volume_level(self, volume):
""" set volume level, range 0..1. """
self.cast.set_volume(volume)
def media_play_pause(self):
""" Service to send the chromecast the command for play/pause. """
media_state = self.media_state
if media_state in (MEDIA_STATE_STOPPED, MEDIA_STATE_PAUSED):
self.cast.media_controller.play()
elif media_state == MEDIA_STATE_PLAYING:
self.cast.media_controller.pause()
def media_play(self):
""" Service to send the chromecast the command for play/pause. """
if self.media_state in (MEDIA_STATE_STOPPED, MEDIA_STATE_PAUSED):
self.cast.media_controller.play()
""" Send play commmand. """
self.cast.media_controller.play()
def media_pause(self):
""" Service to send the chromecast the command for play/pause. """
if self.media_state == MEDIA_STATE_PLAYING:
self.cast.media_controller.pause()
""" Send pause command. """
self.cast.media_controller.pause()
def media_prev_track(self):
""" media_prev_track media player. """
def media_previous_track(self):
""" Send previous track command. """
self.cast.media_controller.rewind()
def media_next_track(self):
""" media_next_track media player. """
""" Send next track command. """
self.cast.media_controller.skip()
def play_youtube_video(self, video_id):
""" Plays specified video_id on the Chromecast's YouTube channel. """
self.youtube.play_video(video_id)
def media_seek(self, position):
""" Seek the media to a specific location. """
self.cast.media_controller.seek(position)
def play_youtube(self, media_id):
""" Plays a YouTube media. """
self.youtube.play_video(media_id)
# implementation of chromecast status_listener methods
def new_cast_status(self, status):
""" Called when a new cast status is received. """

View file

@ -5,121 +5,334 @@ homeassistant.components.media_player.demo
Demo implementation of the media player.
"""
from homeassistant.const import (
STATE_PLAYING, STATE_PAUSED, STATE_OFF)
from homeassistant.components.media_player import (
MediaPlayerDevice, STATE_NO_APP, ATTR_MEDIA_STATE,
ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_TITLE, ATTR_MEDIA_DURATION,
ATTR_MEDIA_VOLUME, MEDIA_STATE_PLAYING, MEDIA_STATE_STOPPED,
YOUTUBE_COVER_URL_FORMAT, ATTR_MEDIA_IS_VOLUME_MUTED)
from homeassistant.const import ATTR_ENTITY_PICTURE
MediaPlayerDevice, YOUTUBE_COVER_URL_FORMAT,
MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW,
SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_YOUTUBE,
SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PREVIOUS_TRACK,
SUPPORT_NEXT_TRACK)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the cast platform. """
add_devices([
DemoMediaPlayer(
DemoYoutubePlayer(
'Living Room', 'eyU3bRy2x44',
'♥♥ The Best Fireplace Video (3 hours)'),
DemoMediaPlayer('Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours')
DemoYoutubePlayer('Bedroom', 'kxopViU98Xo', 'Epic sax guy 10 hours'),
DemoMusicPlayer(), DemoTVShowPlayer(),
])
class DemoMediaPlayer(MediaPlayerDevice):
""" A Demo media player that only supports YouTube. """
YOUTUBE_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_YOUTUBE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF
def __init__(self, name, youtube_id=None, media_title=None):
MUSIC_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF
NETFLIX_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF
class AbstractDemoPlayer(MediaPlayerDevice):
""" Base class for demo media players. """
# We only implement the methods that we support
# pylint: disable=abstract-method
def __init__(self, name):
self._name = name
self.is_playing = youtube_id is not None
self.youtube_id = youtube_id
self.media_title = media_title
self.volume = 1.0
self.is_volume_muted = False
self._player_state = STATE_PLAYING
self._volume_level = 1.0
self._volume_muted = False
@property
def should_poll(self):
""" No polling needed for a demo componentn. """
""" We will push an update after each command. """
return False
@property
def name(self):
""" Returns the name of the device. """
""" Name of the media player. """
return self._name
@property
def state(self):
""" Returns the state of the device. """
return STATE_NO_APP if self.youtube_id is None else "YouTube"
""" State of the player. """
return self._player_state
@property
def state_attributes(self):
""" Returns the state attributes. """
if self.youtube_id is None:
return
def volume_level(self):
""" Volume level of the media player (0..1). """
return self._volume_level
state_attr = {
ATTR_MEDIA_CONTENT_ID: self.youtube_id,
ATTR_MEDIA_TITLE: self.media_title,
ATTR_MEDIA_DURATION: 100,
ATTR_MEDIA_VOLUME: self.volume,
ATTR_MEDIA_IS_VOLUME_MUTED: self.is_volume_muted,
ATTR_ENTITY_PICTURE:
YOUTUBE_COVER_URL_FORMAT.format(self.youtube_id)
}
if self.is_playing:
state_attr[ATTR_MEDIA_STATE] = MEDIA_STATE_PLAYING
else:
state_attr[ATTR_MEDIA_STATE] = MEDIA_STATE_STOPPED
return state_attr
@property
def is_volume_muted(self):
""" Boolean if volume is currently muted. """
return self._volume_muted
def turn_on(self):
""" turn_off media player. """
self.youtube_id = "eyU3bRy2x44"
self.is_playing = False
""" turn the media player on. """
self._player_state = STATE_PLAYING
self.update_ha_state()
def turn_off(self):
""" turn_off media player. """
self.youtube_id = None
self.is_playing = False
""" turn the media player off. """
self._player_state = STATE_OFF
self.update_ha_state()
def volume_up(self):
""" volume_up media player. """
if self.volume < 1:
self.volume += 0.1
self.update_ha_state()
def volume_down(self):
""" volume_down media player. """
if self.volume > 0:
self.volume -= 0.1
self.update_ha_state()
def volume_mute(self, mute):
""" mute (true) or unmute (false) media player. """
self.is_volume_muted = mute
def mute_volume(self, mute):
""" mute the volume. """
self._volume_muted = mute
self.update_ha_state()
def media_play_pause(self):
""" media_play_pause media player. """
self.is_playing = not self.is_playing
def set_volume_level(self, volume):
""" set volume level, range 0..1. """
self._volume_level = volume
self.update_ha_state()
def media_play(self):
""" media_play media player. """
self.is_playing = True
""" Send play commmand. """
self._player_state = STATE_PLAYING
self.update_ha_state()
def media_pause(self):
""" media_pause media player. """
self.is_playing = False
""" Send pause command. """
self._player_state = STATE_PAUSED
self.update_ha_state()
class DemoYoutubePlayer(AbstractDemoPlayer):
""" A Demo media player that only supports YouTube. """
# We only implement the methods that we support
# pylint: disable=abstract-method
def __init__(self, name, youtube_id=None, media_title=None):
super().__init__(name)
self.youtube_id = youtube_id
self._media_title = media_title
@property
def media_content_id(self):
""" Content ID of current playing media. """
return self.youtube_id
@property
def media_content_type(self):
""" Content type of current playing media. """
return MEDIA_TYPE_VIDEO
@property
def media_duration(self):
""" Duration of current playing media in seconds. """
return 360
@property
def media_image_url(self):
""" Image url of current playing media. """
return YOUTUBE_COVER_URL_FORMAT.format(self.youtube_id)
@property
def media_title(self):
""" Title of current playing media. """
return self._media_title
@property
def app_name(self):
""" Current running app. """
return "YouTube"
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
return YOUTUBE_PLAYER_SUPPORT
def play_youtube(self, media_id):
""" Plays a YouTube media. """
self.youtube_id = media_id
self.media_title = 'Demo media title'
self.is_playing = True
self._media_title = 'some YouTube video'
self.update_ha_state()
class DemoMusicPlayer(AbstractDemoPlayer):
""" A Demo media player that only supports YouTube. """
# We only implement the methods that we support
# pylint: disable=abstract-method
tracks = [
('Technohead', 'I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)'),
('Paul Elstak', 'Luv U More'),
('Dune', 'Hardcore Vibes'),
('Nakatomi', 'Children Of The Night'),
('Party Animals',
'Have You Ever Been Mellow? (Flamman & Abraxas Radio Mix)'),
('Rob G.*', 'Ecstasy, You Got What I Need'),
('Lipstick', "I'm A Raver"),
('4 Tune Fairytales', 'My Little Fantasy (Radio Edit)'),
('Prophet', "The Big Boys Don't Cry"),
('Lovechild', 'All Out Of Love (DJ Weirdo & Sim Remix)'),
('Stingray & Sonic Driver', 'Cold As Ice (El Bruto Remix)'),
('Highlander', 'Hold Me Now (Bass-D & King Matthew Remix)'),
('Juggernaut', 'Ruffneck Rules Da Artcore Scene (12" Edit)'),
('Diss Reaction', 'Jiiieehaaaa '),
('Flamman And Abraxas', 'Good To Go (Radio Mix)'),
('Critical Mass', 'Dancing Together'),
('Charly Lownoise & Mental Theo',
'Ultimate Sex Track (Bass-D & King Matthew Remix)'),
]
def __init__(self):
super().__init__('Walkman')
self._cur_track = 0
@property
def media_content_id(self):
""" Content ID of current playing media. """
return 'bounzz-1'
@property
def media_content_type(self):
""" Content type of current playing media. """
return MEDIA_TYPE_MUSIC
@property
def media_duration(self):
""" Duration of current playing media in seconds. """
return 213
@property
def media_image_url(self):
""" Image url of current playing media. """
return 'https://graph.facebook.com/107771475912710/picture'
@property
def media_title(self):
""" Title of current playing media. """
return self.tracks[self._cur_track][1]
@property
def media_artist(self):
""" Artist of current playing media. (Music track only) """
return self.tracks[self._cur_track][0]
@property
def media_album_name(self):
""" Album of current playing media. (Music track only) """
# pylint: disable=no-self-use
return "Bounzz"
@property
def media_track(self):
""" Track number of current playing media. (Music track only) """
return self._cur_track + 1
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
support = MUSIC_PLAYER_SUPPORT
if self._cur_track > 1:
support |= SUPPORT_PREVIOUS_TRACK
if self._cur_track < len(self.tracks)-1:
support |= SUPPORT_NEXT_TRACK
return support
def media_previous_track(self):
""" Send previous track command. """
if self._cur_track > 0:
self._cur_track -= 1
self.update_ha_state()
def media_next_track(self):
""" Send next track command. """
if self._cur_track < len(self.tracks)-1:
self._cur_track += 1
self.update_ha_state()
class DemoTVShowPlayer(AbstractDemoPlayer):
""" A Demo media player that only supports YouTube. """
# We only implement the methods that we support
# pylint: disable=abstract-method
def __init__(self):
super().__init__('Lounge room')
self._cur_episode = 1
self._episode_count = 13
@property
def media_content_id(self):
""" Content ID of current playing media. """
return 'house-of-cards-1'
@property
def media_content_type(self):
""" Content type of current playing media. """
return MEDIA_TYPE_TVSHOW
@property
def media_duration(self):
""" Duration of current playing media in seconds. """
return 3600
@property
def media_image_url(self):
""" Image url of current playing media. """
return 'https://graph.facebook.com/HouseofCards/picture'
@property
def media_title(self):
""" Title of current playing media. """
return 'Chapter {}'.format(self._cur_episode)
@property
def media_series_title(self):
""" Series title of current playing media. (TV Show only)"""
return 'House of Cards'
@property
def media_season(self):
""" Season of current playing media. (TV Show only) """
return 1
@property
def media_episode(self):
""" Episode of current playing media. (TV Show only) """
return self._cur_episode
@property
def app_name(self):
""" Current running app. """
return "Netflix"
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
support = NETFLIX_PLAYER_SUPPORT
if self._cur_episode > 1:
support |= SUPPORT_PREVIOUS_TRACK
if self._cur_episode < self._episode_count:
support |= SUPPORT_NEXT_TRACK
return support
def media_previous_track(self):
""" Send previous track command. """
if self._cur_episode > 1:
self._cur_episode -= 1
self.update_ha_state()
def media_next_track(self):
""" Send next track command. """
if self._cur_episode < self._episode_count:
self._cur_episode += 1
self.update_ha_state()

View file

@ -32,16 +32,28 @@ Location of your Music Player Daemon.
import logging
import socket
try:
import mpd
except ImportError:
mpd = None
from homeassistant.const import (
STATE_PLAYING, STATE_PAUSED, STATE_OFF)
from homeassistant.components.media_player import (
MediaPlayerDevice, STATE_NO_APP, ATTR_MEDIA_STATE,
ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_TITLE, ATTR_MEDIA_ARTIST,
ATTR_MEDIA_ALBUM, ATTR_MEDIA_DATE, ATTR_MEDIA_DURATION,
ATTR_MEDIA_VOLUME, MEDIA_STATE_PAUSED, MEDIA_STATE_PLAYING,
MEDIA_STATE_STOPPED, MEDIA_STATE_UNKNOWN)
MediaPlayerDevice,
SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_TURN_OFF,
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
MEDIA_TYPE_MUSIC)
_LOGGER = logging.getLogger(__name__)
SUPPORT_MPD = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the MPD platform. """
@ -50,10 +62,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
port = config.get('port', 6600)
location = config.get('location', 'MPD')
try:
from mpd import MPDClient
except ImportError:
if mpd is None:
_LOGGER.exception(
"Unable to import mpd2. "
"Did you maybe not install the 'python-mpd2' package?")
@ -62,7 +71,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
# pylint: disable=no-member
try:
mpd_client = MPDClient()
mpd_client = mpd.MPDClient()
mpd_client.connect(daemon, port)
mpd_client.close()
mpd_client.disconnect()
@ -73,110 +82,112 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return False
mpd = []
mpd.append(MpdDevice(daemon, port, location))
add_devices(mpd)
add_devices([MpdDevice(daemon, port, location)])
class MpdDevice(MediaPlayerDevice):
""" Represents a MPD server. """
def __init__(self, server, port, location):
from mpd import MPDClient
# MPD confuses pylint
# pylint: disable=no-member, abstract-method
def __init__(self, server, port, location):
self.server = server
self.port = port
self._name = location
self.state_attr = {ATTR_MEDIA_STATE: MEDIA_STATE_STOPPED}
self.status = None
self.currentsong = None
self.client = MPDClient()
self.client = mpd.MPDClient()
self.client.timeout = 10
self.client.idletimeout = None
self.client.connect(self.server, self.port)
self.update()
def update(self):
try:
self.status = self.client.status()
self.currentsong = self.client.currentsong()
except mpd.ConnectionError:
self.client.connect(self.server, self.port)
self.status = self.client.status()
self.currentsong = self.client.currentsong()
@property
def name(self):
""" Returns the name of the device. """
return self._name
# pylint: disable=no-member
@property
def state(self):
""" Returns the state of the device. """
status = self.client.status()
if status is None:
return STATE_NO_APP
else:
return self.client.currentsong()['artist']
@property
def media_state(self):
""" Returns the media state. """
media_controller = self.client.status()
if media_controller['state'] == 'play':
return MEDIA_STATE_PLAYING
elif media_controller['state'] == 'pause':
return MEDIA_STATE_PAUSED
elif media_controller['state'] == 'stop':
return MEDIA_STATE_STOPPED
if self.status['state'] == 'play':
return STATE_PLAYING
elif self.status['state'] == 'pause':
return STATE_PAUSED
else:
return MEDIA_STATE_UNKNOWN
return STATE_OFF
# pylint: disable=no-member
@property
def state_attributes(self):
""" Returns the state attributes. """
status = self.client.status()
current_song = self.client.currentsong()
def media_content_id(self):
""" Content ID of current playing media. """
return self.currentsong['id']
if not status and not current_song:
state_attr = {}
@property
def media_content_type(self):
""" Content type of current playing media. """
return MEDIA_TYPE_MUSIC
if current_song['id']:
state_attr[ATTR_MEDIA_CONTENT_ID] = current_song['id']
@property
def media_duration(self):
""" Duration of current playing media in seconds. """
# Time does not exist for streams
return self.currentsong.get('time')
if current_song['date']:
state_attr[ATTR_MEDIA_DATE] = current_song['date']
@property
def media_title(self):
""" Title of current playing media. """
return self.currentsong['title']
if current_song['title']:
state_attr[ATTR_MEDIA_TITLE] = current_song['title']
@property
def media_artist(self):
""" Artist of current playing media. (Music track only) """
return self.currentsong.get('artist')
if current_song['time']:
state_attr[ATTR_MEDIA_DURATION] = current_song['time']
@property
def media_album_name(self):
""" Album of current playing media. (Music track only) """
return self.currentsong.get('album')
if current_song['artist']:
state_attr[ATTR_MEDIA_ARTIST] = current_song['artist']
@property
def volume_level(self):
return int(self.status['volume'])/100
if current_song['album']:
state_attr[ATTR_MEDIA_ALBUM] = current_song['album']
state_attr[ATTR_MEDIA_VOLUME] = status['volume']
return state_attr
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
return SUPPORT_MPD
def turn_off(self):
""" Service to exit the running MPD. """
self.client.stop()
def set_volume_level(self, volume):
""" Sets volume """
self.client.setvol(int(volume * 100))
def volume_up(self):
""" Service to send the MPD the command for volume up. """
current_volume = self.client.status()['volume']
current_volume = int(self.status['volume'])
if int(current_volume) <= 100:
self.client.setvol(int(current_volume) + 5)
if current_volume <= 100:
self.client.setvol(current_volume + 5)
def volume_down(self):
""" Service to send the MPD the command for volume down. """
current_volume = self.client.status()['volume']
current_volume = int(self.status['volume'])
if int(current_volume) >= 0:
self.client.setvol(int(current_volume) - 5)
def media_play_pause(self):
""" Service to send the MPD the command for play/pause. """
self.client.pause()
if current_volume >= 0:
self.client.setvol(current_volume - 5)
def media_play(self):
""" Service to send the MPD the command for play/pause. """
@ -190,6 +201,6 @@ class MpdDevice(MediaPlayerDevice):
""" Service to send the MPD the command for next track. """
self.client.next()
def media_prev_track(self):
def media_previous_track(self):
""" Service to send the MPD the command for previous track. """
self.client.previous()

View file

@ -97,5 +97,5 @@ def setup(hass, config):
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_modbus)
# Tells the bootstrapper that the component was succesfully initialized
# Tells the bootstrapper that the component was successfully initialized
return True

View file

@ -0,0 +1,164 @@
"""
homeassistant.components.notify.mail
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Mail notification service.
Configuration:
To use the Mail notifier you will need to add something like the following
to your config/configuration.yaml
notify:
platform: mail
server: MAIL_SERVER
port: YOUR_SMTP_PORT
sender: SENDER_EMAIL_ADDRESS
starttls: 1 or 0
username: YOUR_SMTP_USERNAME
password: YOUR_SMTP_PASSWORD
recipient: YOUR_RECIPIENT
Variables:
server
*Required
SMTP server which is used to end the notifications. For Google Mail, eg.
smtp.gmail.com. Keep in mind that Google has some extra layers of protection
which need special attention (Hint: 'Less secure apps').
port
*Required
The port that the SMTP server is using, eg. 587 for Google Mail and STARTTLS
or 465/993 depending on your SMTP servers.
sender
*Required
E-Mail address of the sender.
starttls
*Optional
Enables STARTTLS, eg. 1 or 0.
username
*Required
Username for the SMTP account.
password
*Required
Password for the SMTP server that belongs to the given username. If the
password contains a colon it need to be wrapped in apostrophes.
recipient
*Required
Recipient of the notification.
"""
import logging
import smtplib
from email.mime.text import MIMEText
from homeassistant.helpers import validate_config
from homeassistant.components.notify import (
DOMAIN, ATTR_TITLE, BaseNotificationService)
_LOGGER = logging.getLogger(__name__)
def get_service(hass, config):
""" Get the mail notification service. """
if not validate_config(config,
{DOMAIN: ['server',
'port',
'sender',
'username',
'password',
'recipient']},
_LOGGER):
return None
smtp_server = config[DOMAIN]['server']
port = int(config[DOMAIN]['port'])
username = config[DOMAIN]['username']
password = config[DOMAIN]['password']
server = None
try:
server = smtplib.SMTP(smtp_server, port)
server.ehlo()
if int(config[DOMAIN]['starttls']) == 1:
server.starttls()
server.ehlo()
try:
server.login(username, password)
except (smtplib.SMTPException, smtplib.SMTPSenderRefused) as error:
_LOGGER.exception(error,
"Please check your settings.")
return None
except smtplib.socket.gaierror:
_LOGGER.exception(
"SMTP server not found. "
"Please check the IP address or hostname of your SMTP server.")
return None
except smtplib.SMTPAuthenticationError:
_LOGGER.exception(
"Login not possible. "
"Please check your setting and/or your credentials.")
return None
if server:
server.quit()
return MailNotificationService(
config[DOMAIN]['server'],
config[DOMAIN]['port'],
config[DOMAIN]['sender'],
config[DOMAIN]['starttls'],
config[DOMAIN]['username'],
config[DOMAIN]['password'],
config[DOMAIN]['recipient']
)
# pylint: disable=too-few-public-methods, too-many-instance-attributes
class MailNotificationService(BaseNotificationService):
""" Implements notification service for E-Mail messages. """
# pylint: disable=too-many-arguments
def __init__(self, server, port, sender, starttls, username,
password, recipient):
self._server = server
self._port = port
self._sender = sender
self.starttls = int(starttls)
self.username = username
self.password = password
self.recipient = recipient
self.mail = smtplib.SMTP(self._server, self._port)
self.mail.ehlo_or_helo_if_needed()
if self.starttls == 1:
self.mail.starttls()
self.mail.ehlo()
self.mail.login(self.username, self.password)
def send_message(self, message="", **kwargs):
""" Send a message to a user. """
subject = kwargs.get(ATTR_TITLE)
msg = MIMEText(message)
msg['Subject'] = subject
msg['To'] = self.recipient
msg['From'] = self._sender
msg['X-Mailer'] = 'HomeAssistant'
self.mail.sendmail(self._sender, self.recipient, msg.as_string())

View file

@ -0,0 +1,110 @@
"""
homeassistant.components.notify.syslog
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Syslog notification service.
Configuration:
To use the Syslog notifier you will need to add something like the following
to your config/configuration.yaml
notify:
platform: syslog
facility: SYSLOG_FACILITY
option: SYSLOG_LOG_OPTION
priority: SYSLOG_PRIORITY
Variables:
facility
*Optional
Facility according to RFC 3164 (http://tools.ietf.org/html/rfc3164). Default
is 'syslog' if no value is given.
option
*Option
Log option. Default is 'pid' if no value is given.
priority
*Optional
Priority of the messages. Default is 'info' if no value is given.
"""
import logging
import syslog
from homeassistant.helpers import validate_config
from homeassistant.components.notify import (
DOMAIN, ATTR_TITLE, BaseNotificationService)
_LOGGER = logging.getLogger(__name__)
FACILITIES = {'kernel': syslog.LOG_KERN,
'user': syslog.LOG_USER,
'mail': syslog.LOG_MAIL,
'daemon': syslog.LOG_DAEMON,
'auth': syslog.LOG_KERN,
'LPR': syslog.LOG_LPR,
'news': syslog.LOG_NEWS,
'uucp': syslog.LOG_UUCP,
'cron': syslog.LOG_CRON,
'syslog': syslog.LOG_SYSLOG,
'local0': syslog.LOG_LOCAL0,
'local1': syslog.LOG_LOCAL1,
'local2': syslog.LOG_LOCAL2,
'local3': syslog.LOG_LOCAL3,
'local4': syslog.LOG_LOCAL4,
'local5': syslog.LOG_LOCAL5,
'local6': syslog.LOG_LOCAL6,
'local7': syslog.LOG_LOCAL7}
OPTIONS = {'pid': syslog.LOG_PID,
'cons': syslog.LOG_CONS,
'ndelay': syslog.LOG_NDELAY,
'nowait': syslog.LOG_NOWAIT,
'perror': syslog.LOG_PERROR}
PRIORITIES = {5: syslog.LOG_EMERG,
4: syslog.LOG_ALERT,
3: syslog.LOG_CRIT,
2: syslog.LOG_ERR,
1: syslog.LOG_WARNING,
0: syslog.LOG_NOTICE,
-1: syslog.LOG_INFO,
-2: syslog.LOG_DEBUG}
def get_service(hass, config):
""" Get the mail notification service. """
if not validate_config(config,
{DOMAIN: ['facility',
'option',
'priority']},
_LOGGER):
return None
_facility = FACILITIES.get(config[DOMAIN]['facility'], 40)
_option = OPTIONS.get(config[DOMAIN]['option'], 10)
_priority = PRIORITIES.get(config[DOMAIN]['priority'], -1)
return SyslogNotificationService(_facility, _option, _priority)
# pylint: disable=too-few-public-methods
class SyslogNotificationService(BaseNotificationService):
""" Implements syslog notification service. """
# pylint: disable=too-many-arguments
def __init__(self, facility, option, priority):
self._facility = facility
self._option = option
self._priority = priority
def send_message(self, message="", **kwargs):
""" Send a message to a user. """
title = kwargs.get(ATTR_TITLE)
syslog.openlog(title, self._option, self._facility)
syslog.syslog(self._priority, message)
syslog.closelog()

View file

@ -0,0 +1,132 @@
"""
homeassistant.components.sensor.swiss_public_transport
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The Swiss public transport sensor will give you the next two departure times
from a given location to another one. This sensor is limited to Switzerland.
Configuration:
To use the Swiss public transport sensor you will need to add something like
the following to your config/configuration.yaml
sensor:
platform: swiss_public_transport
from: STATION_ID
to: STATION_ID
Variables:
from
*Required
Start station/stop of your trip. To search for the ID of the station, use the
an URL like this: http://transport.opendata.ch/v1/locations?query=Wankdorf
to query for the station. If the score is 100 ("score":"100" in the response),
it is a perfect match.
to
*Required
Destination station/stop of the trip. Same procedure as for the start station.
Details for the API : http://transport.opendata.ch
"""
import logging
from datetime import timedelta
from requests import get
from homeassistant.util import Throttle
import homeassistant.util.dt as dt_util
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
_RESOURCE = 'http://transport.opendata.ch/v1/'
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Get the Swiss public transport sensor. """
# journal contains [0] Station ID start, [1] Station ID destination
# [2] Station name start, and [3] Station name destination
journey = [config.get('from'), config.get('to')]
try:
for location in [config.get('from', None), config.get('to', None)]:
# transport.opendata.ch doesn't play nice with requests.Session
result = get(_RESOURCE + 'locations?query=%s' % location)
journey.append(result.json()['stations'][0]['name'])
except KeyError:
_LOGGER.exception(
"Unable to determine stations. "
"Check your settings and/or the availability of opendata.ch")
return None
dev = []
data = PublicTransportData(journey)
dev.append(SwissPublicTransportSensor(data, journey))
add_devices(dev)
# pylint: disable=too-few-public-methods
class SwissPublicTransportSensor(Entity):
""" Implements an Swiss public transport sensor. """
def __init__(self, data, journey):
self.data = data
self._name = '{}-{}'.format(journey[2], journey[3])
self.update()
@property
def name(self):
""" Returns the name. """
return self._name
@property
def state(self):
""" Returns the state of the device. """
return self._state
# pylint: disable=too-many-branches
def update(self):
""" Gets the latest data from opendata.ch and updates the states. """
times = self.data.update()
try:
self._state = ', '.join(times)
except TypeError:
pass
# pylint: disable=too-few-public-methods
class PublicTransportData(object):
""" Class for handling the data retrieval. """
def __init__(self, journey):
self.start = journey[0]
self.destination = journey[1]
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
""" Gets the latest data from opendata.ch. """
response = get(
_RESOURCE +
'connections?' +
'from=' + self.start + '&' +
'to=' + self.destination + '&' +
'fields[]=connections/from/departureTimestamp/&' +
'fields[]=connections/')
connections = response.json()['connections'][:2]
try:
return [
dt_util.datetime_to_time_str(
dt_util.as_local(dt_util.utc_from_timestamp(
item['from']['departureTimestamp']))
)
for item in connections
]
except KeyError:
return ['n/a']

View file

@ -89,9 +89,9 @@ class TimeDateSensor(Entity):
""" Gets the latest data and updates the states. """
time_date = dt_util.utcnow()
time = dt_util.datetime_to_short_time_str(dt_util.as_local(time_date))
time_utc = dt_util.datetime_to_short_time_str(time_date)
date = dt_util.datetime_to_short_date_str(dt_util.as_local(time_date))
time = dt_util.datetime_to_time_str(dt_util.as_local(time_date))
time_utc = dt_util.datetime_to_time_str(time_date)
date = dt_util.datetime_to_date_str(dt_util.as_local(time_date))
# Calculate the beat (Swatch Internet Time) time without date.
hours, minutes, seconds = time_date.strftime('%H:%M:%S').split(':')

View file

@ -7,6 +7,7 @@ import logging
from datetime import timedelta
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
@ -33,6 +34,11 @@ DISCOVERY_PLATFORMS = {
isy994.DISCOVER_SWITCHES: 'isy994',
}
PROP_TO_ATTR = {
'current_power_mwh': ATTR_CURRENT_POWER_MWH,
'today_power_mw': ATTR_TODAY_MWH,
}
_LOGGER = logging.getLogger(__name__)
@ -74,10 +80,48 @@ def setup(hass, config):
else:
switch.turn_off()
switch.update_ha_state(True)
if switch.should_poll:
switch.update_ha_state(True)
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_switch_service)
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_switch_service)
return True
class SwitchDevice(ToggleEntity):
""" Represents a switch within Home Assistant. """
# pylint: disable=no-self-use
@property
def current_power_mwh(self):
""" Current power usage in mwh. """
return None
@property
def today_power_mw(self):
""" Today total power usage in mw. """
return None
@property
def device_state_attributes(self):
""" Returns device specific state attributes. """
return None
@property
def state_attributes(self):
""" Returns optional state attributes. """
data = {}
for prop, attr in PROP_TO_ATTR.items():
value = getattr(self, prop)
if value:
data[attr] = value
device_attr = self.device_state_attributes
if device_attr is not None:
data.update(device_attr)
return data

View file

@ -6,8 +6,7 @@ homeassistant.components.switch.command_switch
Allows to configure custom shell commands to turn a switch on/off.
"""
import logging
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME
from homeassistant.components.switch import SwitchDevice
import subprocess
_LOGGER = logging.getLogger(__name__)
@ -30,11 +29,11 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
add_devices_callback(devices)
class CommandSwitch(ToggleEntity):
class CommandSwitch(SwitchDevice):
""" Represents a switch that can be togggled using shell commands """
def __init__(self, name, command_on, command_off):
self._name = name or DEVICE_DEFAULT_NAME
self._state = STATE_OFF
self._name = name
self._state = False
self._command_on = command_on
self._command_off = command_off
@ -60,22 +59,19 @@ class CommandSwitch(ToggleEntity):
""" The name of the switch """
return self._name
@property
def state(self):
""" Returns the state of the switch. """
return self._state
@property
def is_on(self):
""" True if device is on. """
return self._state == STATE_ON
return self._state
def turn_on(self, **kwargs):
""" Turn the device on. """
if CommandSwitch._switch(self._command_on):
self._state = STATE_ON
self._state = True
self.update_ha_state()
def turn_off(self, **kwargs):
""" Turn the device off. """
if CommandSwitch._switch(self._command_off):
self._state = STATE_OFF
self._state = False
self.update_ha_state()

View file

@ -5,20 +5,20 @@ homeassistant.components.switch.demo
Demo platform that has two fake switches.
"""
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME
from homeassistant.components.switch import SwitchDevice
from homeassistant.const import DEVICE_DEFAULT_NAME
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Find and return demo switches. """
add_devices_callback([
DemoSwitch('Ceiling', STATE_ON),
DemoSwitch('AC', STATE_OFF)
DemoSwitch('Ceiling', True),
DemoSwitch('AC', False)
])
class DemoSwitch(ToggleEntity):
class DemoSwitch(SwitchDevice):
""" Provides a demo switch. """
def __init__(self, name, state):
self._name = name or DEVICE_DEFAULT_NAME
@ -35,19 +35,27 @@ class DemoSwitch(ToggleEntity):
return self._name
@property
def state(self):
""" Returns the state of the device if any. """
return self._state
def current_power_mwh(self):
""" Current power usage in mwh. """
if self._state:
return 100
@property
def today_power_mw(self):
""" Today total power usage in mw. """
return 1500
@property
def is_on(self):
""" True if device is on. """
return self._state == STATE_ON
return self._state
def turn_on(self, **kwargs):
""" Turn the device on. """
self._state = STATE_ON
self._state = True
self.update_ha_state()
def turn_off(self, **kwargs):
""" Turn the device off. """
self._state = STATE_OFF
self._state = False
self.update_ha_state()

View file

@ -0,0 +1,139 @@
"""
homeassistant.components.switch.hikvision
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support turning on/off motion detection on Hikvision cameras.
Note: Currently works using default https port only.
CGI API Guide:
http://bit.ly/1RuyUuF
Configuration:
To use the Hikvision motion detection
switch you will need to add something like the
following to your config/configuration.yaml
switch:
platform: hikvisioncam
name: Hikvision Cam 1 Motion Detection
host: 192.168.1.26
username: YOUR_USERNAME
password: YOUR_PASSWORD
Variables:
host
*Required
This is the IP address of your Hikvision camera. Example: 192.168.1.32
username
*Required
Your Hikvision camera username
password
*Required
Your Hikvision camera username
name
*Optional
The name to use when displaying this switch instance.
"""
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import STATE_ON, STATE_OFF
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
import logging
try:
import hikvision.api
from hikvision.error import HikvisionError, MissingParamError
except ImportError:
hikvision.api = None
_LOGGING = logging.getLogger(__name__)
# pylint: disable=too-many-arguments
# pylint: disable=too-many-instance-attributes
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Setup Hikvision Camera config. """
host = config.get(CONF_HOST, None)
port = config.get('port', "80")
name = config.get('name', "Hikvision Camera Motion Detection")
username = config.get(CONF_USERNAME, "admin")
password = config.get(CONF_PASSWORD, "12345")
if hikvision.api is None:
_LOGGING.error((
"Failed to import hikvision. Did you maybe not install the "
"'hikvision' dependency?"))
return False
try:
hikvision_cam = hikvision.api.CreateDevice(
host, port=port, username=username,
password=password, is_https=False)
except MissingParamError as param_err:
_LOGGING.error("Missing required param: %s", param_err)
return False
except HikvisionError as conn_err:
_LOGGING.error("Unable to connect: %s", conn_err)
return False
add_devices_callback([
HikvisionMotionSwitch(name, hikvision_cam)
])
class HikvisionMotionSwitch(ToggleEntity):
""" Provides a switch to toggle on/off motion detection. """
def __init__(self, name, hikvision_cam):
self._name = name
self._hikvision_cam = hikvision_cam
self._state = STATE_OFF
@property
def should_poll(self):
""" Poll for status regularly. """
return True
@property
def name(self):
""" Returns the name of the device if any. """
return self._name
@property
def state(self):
""" Returns the state of the device if any. """
return self._state
@property
def is_on(self):
""" True if device is on. """
return self._state == STATE_ON
def turn_on(self, **kwargs):
""" Turn the device on. """
_LOGGING.info("Turning on Motion Detection ")
self._hikvision_cam.enable_motion_detection()
def turn_off(self, **kwargs):
""" Turn the device off. """
_LOGGING.info("Turning off Motion Detection ")
self._hikvision_cam.disable_motion_detection()
def update(self):
""" Update Motion Detection state """
enabled = self._hikvision_cam.is_motion_detection_enabled()
_LOGGING.info('enabled: %s', enabled)
self._state = STATE_ON if enabled else STATE_OFF

View file

@ -6,9 +6,7 @@ Support for WeMo switches.
"""
import logging
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.components.switch import (
ATTR_TODAY_MWH, ATTR_CURRENT_POWER_MWH)
from homeassistant.components.switch import SwitchDevice
# pylint: disable=unused-argument
@ -43,10 +41,11 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
if isinstance(switch, pywemo.Switch)])
class WemoSwitch(ToggleEntity):
class WemoSwitch(SwitchDevice):
""" Represents a WeMo switch within Home Assistant. """
def __init__(self, wemo):
self.wemo = wemo
self.insight_params = None
@property
def unique_id(self):
@ -59,15 +58,16 @@ class WemoSwitch(ToggleEntity):
return self.wemo.name
@property
def state_attributes(self):
""" Returns optional state attributes. """
if self.wemo.model.startswith('Belkin Insight'):
cur_info = self.wemo.insight_params
def current_power_mwh(self):
""" Current power usage in mwh. """
if self.insight_params:
return self.insight_params['currentpower']
return {
ATTR_CURRENT_POWER_MWH: cur_info['currentpower'],
ATTR_TODAY_MWH: cur_info['todaymw']
}
@property
def today_power_mw(self):
""" Today total power usage in mw. """
if self.insight_params:
return self.insight_params['todaymw']
@property
def is_on(self):
@ -85,3 +85,5 @@ class WemoSwitch(ToggleEntity):
def update(self):
""" Update WeMo state. """
self.wemo.get_state(True)
if self.wemo.model.startswith('Belkin Insight'):
self.insight_params = self.wemo.insight_params

View file

@ -93,4 +93,4 @@ class WinkToggleDevice(ToggleEntity):
def update(self):
""" Update state of the light. """
self.wink.wait_till_desired_reached()
self.wink.updateState()

View file

@ -40,6 +40,9 @@ STATE_NOT_HOME = 'not_home'
STATE_UNKNOWN = "unknown"
STATE_OPEN = 'open'
STATE_CLOSED = 'closed'
STATE_PLAYING = 'playing'
STATE_PAUSED = 'paused'
STATE_IDLE = 'idle'
# #### STATE AND EVENT ATTRIBUTES ####
# Contains current time for a TIME_CHANGED event
@ -104,7 +107,8 @@ 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"
SERVICE_MEDIA_PREVIOUS_TRACK = "media_previous_track"
SERVICE_MEDIA_SEEK = "media_seek"
# #### API / REMOTE ####
SERVER_PORT = 8123

View file

@ -9,9 +9,9 @@ import datetime as dt
import pytz
DATE_STR_FORMAT = "%H:%M:%S %d-%m-%Y"
DATE_SHORT_STR_FORMAT = "%Y-%m-%d"
TIME_SHORT_STR_FORMAT = "%H:%M"
DATETIME_STR_FORMAT = "%H:%M:%S %d-%m-%Y"
DATE_STR_FORMAT = "%Y-%m-%d"
TIME_STR_FORMAT = "%H:%M"
UTC = DEFAULT_TIME_ZONE = pytz.utc
@ -34,7 +34,7 @@ def get_time_zone(time_zone_str):
def utcnow():
""" Get now in UTC time. """
return dt.datetime.now(pytz.utc)
return dt.datetime.now(UTC)
def now(time_zone=None):
@ -45,12 +45,12 @@ def now(time_zone=None):
def as_utc(dattim):
""" Return a datetime as UTC time.
Assumes datetime without tzinfo to be in the DEFAULT_TIME_ZONE. """
if dattim.tzinfo == pytz.utc:
if dattim.tzinfo == UTC:
return dattim
elif dattim.tzinfo is None:
dattim = dattim.replace(tzinfo=DEFAULT_TIME_ZONE)
return dattim.astimezone(pytz.utc)
return dattim.astimezone(UTC)
def as_local(dattim):
@ -58,17 +58,28 @@ def as_local(dattim):
if dattim.tzinfo == DEFAULT_TIME_ZONE:
return dattim
elif dattim.tzinfo is None:
dattim = dattim.replace(tzinfo=pytz.utc)
dattim = dattim.replace(tzinfo=UTC)
return dattim.astimezone(DEFAULT_TIME_ZONE)
def utc_from_timestamp(timestamp):
""" Returns a UTC time from a timestamp. """
return dt.datetime.utcfromtimestamp(timestamp).replace(tzinfo=pytz.utc)
return dt.datetime.utcfromtimestamp(timestamp).replace(tzinfo=UTC)
def datetime_to_local_str(dattim, time_zone=None):
def start_of_local_day(dt_or_d=None):
""" Return local datetime object of start of day from date or datetime. """
if dt_or_d is None:
dt_or_d = now().date()
elif isinstance(dt_or_d, dt.datetime):
dt_or_d = dt_or_d.date()
return dt.datetime.combine(dt_or_d, dt.time()).replace(
tzinfo=DEFAULT_TIME_ZONE)
def datetime_to_local_str(dattim):
""" Converts datetime to specified time_zone and returns a string. """
return datetime_to_str(as_local(dattim))
@ -76,27 +87,27 @@ def datetime_to_local_str(dattim, time_zone=None):
def datetime_to_str(dattim):
""" Converts datetime to a string format.
@rtype : str
"""
return dattim.strftime(DATETIME_STR_FORMAT)
def datetime_to_time_str(dattim):
""" Converts datetime to a string containing only the time.
@rtype : str
"""
return dattim.strftime(TIME_STR_FORMAT)
def datetime_to_date_str(dattim):
""" Converts datetime to a string containing only the date.
@rtype : str
"""
return dattim.strftime(DATE_STR_FORMAT)
def datetime_to_short_time_str(dattim):
""" Converts datetime to a string format as short time.
@rtype : str
"""
return dattim.strftime(TIME_SHORT_STR_FORMAT)
def datetime_to_short_date_str(dattim):
""" Converts datetime to a string format as short date.
@rtype : str
"""
return dattim.strftime(DATE_SHORT_STR_FORMAT)
def str_to_datetime(dt_str):
""" Converts a string to a UTC datetime object.
@ -104,7 +115,15 @@ def str_to_datetime(dt_str):
"""
try:
return dt.datetime.strptime(
dt_str, DATE_STR_FORMAT).replace(tzinfo=pytz.utc)
dt_str, DATETIME_STR_FORMAT).replace(tzinfo=pytz.utc)
except ValueError: # If dt_str did not match our format
return None
def date_str_to_date(dt_str):
""" Converts a date string to a date object. """
try:
return dt.datetime.strptime(dt_str, DATE_STR_FORMAT).date()
except ValueError: # If dt_str did not match our format
return None

View file

@ -18,7 +18,7 @@ phue>=0.8
ledcontroller>=1.0.7
# Chromecast bindings (media_player.cast)
pychromecast>=0.6.4
pychromecast>=0.6.6
# Keyboard (keyboard)
pyuserinput>=0.1.9
@ -38,7 +38,7 @@ python-nest>=2.3.1
# Z-Wave (*.zwave)
pydispatcher>=2.0.5
# ISY994 bindings (*.isy994
# ISY994 bindings (*.isy994)
PyISY>=1.0.2
# PSutil (sensor.systemmonitor)
@ -62,5 +62,11 @@ blockchain>=1.1.2
# MPD Bindings (media_player.mpd)
python-mpd2>=0.5.4
# Hikvision (switch.hikvisioncam)
hikvision>=0.4
# console log coloring
colorlog>=2.6.0
# JSON-RPC interface
jsonrpc-requests>=0.1

View file

@ -14,7 +14,8 @@ import homeassistant as ha
import homeassistant.loader as loader
import homeassistant.util.dt as dt_util
from homeassistant.const import (
STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, CONF_PLATFORM)
STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, CONF_PLATFORM,
DEVICE_DEFAULT_NAME)
import homeassistant.components.device_tracker as device_tracker
from helpers import get_test_home_assistant
@ -96,7 +97,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
# 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',
['DEV1,{},0,\n'.format(DEVICE_DEFAULT_NAME), 'DEV2,dev2,0,\n',
'device,name,track,picture\n'],
sorted(fil))

View file

@ -10,9 +10,10 @@ import unittest
import homeassistant as ha
from homeassistant.const import (
STATE_OFF,
SERVICE_TURN_ON, 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)
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, ATTR_ENTITY_ID)
import homeassistant.components.media_player as media_player
from helpers import mock_service
@ -29,7 +30,7 @@ class TestMediaPlayer(unittest.TestCase):
self.hass = ha.HomeAssistant()
self.test_entity = media_player.ENTITY_ID_FORMAT.format('living_room')
self.hass.states.set(self.test_entity, media_player.STATE_NO_APP)
self.hass.states.set(self.test_entity, STATE_OFF)
self.test_entity2 = media_player.ENTITY_ID_FORMAT.format('bedroom')
self.hass.states.set(self.test_entity2, "YouTube")
@ -56,7 +57,7 @@ class TestMediaPlayer(unittest.TestCase):
SERVICE_MEDIA_PLAY: media_player.media_play,
SERVICE_MEDIA_PAUSE: media_player.media_pause,
SERVICE_MEDIA_NEXT_TRACK: media_player.media_next_track,
SERVICE_MEDIA_PREV_TRACK: media_player.media_prev_track
SERVICE_MEDIA_PREVIOUS_TRACK: media_player.media_previous_track
}
for service_name, service_method in services.items():