Merge branch 'dev' of github.com:balloob/home-assistant into dev
Conflicts: requirements.txt
This commit is contained in:
commit
61638e8b72
61 changed files with 6631 additions and 3291 deletions
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
19
README.md
19
README.md
|
@ -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.
|
||||
|
||||
|
|
|
@ -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__)
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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",
|
||||
{
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 '';
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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];
|
||||
},
|
||||
|
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
@ -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())
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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. """
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
164
homeassistant/components/notify/smtp.py
Normal file
164
homeassistant/components/notify/smtp.py
Normal 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())
|
110
homeassistant/components/notify/syslog.py
Normal file
110
homeassistant/components/notify/syslog.py
Normal 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()
|
132
homeassistant/components/sensor/swiss_public_transport.py
Normal file
132
homeassistant/components/sensor/swiss_public_transport.py
Normal 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']
|
|
@ -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(':')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
139
homeassistant/components/switch/hikvisioncam.py
Normal file
139
homeassistant/components/switch/hikvisioncam.py
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -93,4 +93,4 @@ class WinkToggleDevice(ToggleEntity):
|
|||
|
||||
def update(self):
|
||||
""" Update state of the light. """
|
||||
self.wink.wait_till_desired_reached()
|
||||
self.wink.updateState()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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():
|
||||
|
|
Loading…
Add table
Reference in a new issue