mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Merge branch 'dev'
Conflicts: homeassistant/components/media_player/cast.py requirements.txt
This commit is contained in:
commit
26de87951a
@ -31,14 +31,19 @@ omit =
|
||||
homeassistant/components/keyboard.py
|
||||
homeassistant/components/light/hue.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
homeassistant/components/media_player/mpd.py
|
||||
homeassistant/components/notify/instapush.py
|
||||
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.
|
||||
|
22
README.md
22
README.md
@ -6,18 +6,20 @@ 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
|
||||
* Turn off all lights and devices when everybody leaves the house
|
||||
* 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:
|
||||
|
||||
@ -33,17 +35,17 @@ 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.
|
||||
|
||||
Please see [the getting started guide](https://home-assistant.io/getting-started/) on how to further configure Home Asssitant.
|
||||
Please see [the getting started guide](https://home-assistant.io/getting-started/) on how to further configure Home Assistant.
|
||||
|
@ -68,7 +68,7 @@ device_sun_light_trigger:
|
||||
|
||||
# A comma separated list of states that have to be tracked as a single group
|
||||
# Grouped states should share the same type of states (ON/OFF or HOME/NOT_HOME)
|
||||
group:
|
||||
group:
|
||||
living_room:
|
||||
- light.Bowl
|
||||
- light.Ceiling
|
||||
|
@ -961,12 +961,14 @@ class Config(object):
|
||||
|
||||
def as_dict(self):
|
||||
""" Converts config to a dictionary. """
|
||||
time_zone = self.time_zone or date_util.UTC
|
||||
|
||||
return {
|
||||
'latitude': self.latitude,
|
||||
'longitude': self.longitude,
|
||||
'temperature_unit': self.temperature_unit,
|
||||
'location_name': self.location_name,
|
||||
'time_zone': self.time_zone.zone,
|
||||
'time_zone': time_zone.zone,
|
||||
'components': self.components,
|
||||
}
|
||||
|
||||
|
@ -141,9 +141,9 @@ def main():
|
||||
|
||||
def open_browser(event):
|
||||
""" Open the webinterface in a browser. """
|
||||
if hass.local_api is not None:
|
||||
if hass.config.api is not None:
|
||||
import webbrowser
|
||||
webbrowser.open(hass.local_api.base_url)
|
||||
webbrowser.open(hass.config.api.base_url)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser)
|
||||
|
||||
|
@ -235,8 +235,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]
|
||||
|
@ -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"
|
||||
@ -41,6 +41,8 @@ CONF_SECONDS = "interval_seconds"
|
||||
|
||||
DEFAULT_CONF_SECONDS = 12
|
||||
|
||||
TRACK_NEW_DEVICES = "track_new_devices"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -78,7 +80,10 @@ def setup(hass, config):
|
||||
seconds = util.convert(config[DOMAIN].get(CONF_SECONDS), int,
|
||||
DEFAULT_CONF_SECONDS)
|
||||
|
||||
tracker = DeviceTracker(hass, device_scanner, seconds)
|
||||
track_new_devices = config[DOMAIN].get(TRACK_NEW_DEVICES) or False
|
||||
_LOGGER.info("Tracking new devices: %s", track_new_devices)
|
||||
|
||||
tracker = DeviceTracker(hass, device_scanner, seconds, track_new_devices)
|
||||
|
||||
# We only succeeded if we got to parse the known devices file
|
||||
return not tracker.invalid_known_devices_file
|
||||
@ -87,13 +92,16 @@ def setup(hass, config):
|
||||
class DeviceTracker(object):
|
||||
""" Class that tracks which devices are home and which are not. """
|
||||
|
||||
def __init__(self, hass, device_scanner, seconds):
|
||||
def __init__(self, hass, device_scanner, seconds, track_new_devices):
|
||||
self.hass = hass
|
||||
|
||||
self.device_scanner = device_scanner
|
||||
|
||||
self.lock = threading.Lock()
|
||||
|
||||
# Do we track new devices by default?
|
||||
self.track_new_devices = track_new_devices
|
||||
|
||||
# Dictionary to keep track of known devices and devices we track
|
||||
self.tracked = {}
|
||||
self.untracked_devices = set()
|
||||
@ -161,57 +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:
|
||||
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"
|
||||
|
||||
writer.writerow((device, name, 0, ""))
|
||||
|
||||
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):
|
||||
@ -227,7 +206,6 @@ class DeviceTracker(object):
|
||||
self.untracked_devices.clear()
|
||||
|
||||
with open(known_dev_path) as inp:
|
||||
default_last_seen = dt_util.utcnow().replace(year=1990)
|
||||
|
||||
# To track which devices need an entity_id assigned
|
||||
need_entity_id = []
|
||||
@ -248,10 +226,7 @@ class DeviceTracker(object):
|
||||
# We found a new device
|
||||
need_entity_id.append(device)
|
||||
|
||||
self.tracked[device] = {
|
||||
'name': row['name'],
|
||||
'last_seen': default_last_seen
|
||||
}
|
||||
self._track_device(device, row['name'])
|
||||
|
||||
# Update state_attr with latest from file
|
||||
state_attr = {
|
||||
@ -276,21 +251,7 @@ class DeviceTracker(object):
|
||||
|
||||
self.tracked.pop(device)
|
||||
|
||||
# Setup entity_ids for the new devices
|
||||
used_entity_ids = [info['entity_id'] for device, info
|
||||
in self.tracked.items()
|
||||
if device not in need_entity_id]
|
||||
|
||||
for device in need_entity_id:
|
||||
name = self.tracked[device]['name']
|
||||
|
||||
entity_id = util.ensure_unique_string(
|
||||
ENTITY_ID_FORMAT.format(util.slugify(name)),
|
||||
used_entity_ids)
|
||||
|
||||
used_entity_ids.append(entity_id)
|
||||
|
||||
self.tracked[device]['entity_id'] = entity_id
|
||||
self._generate_entity_ids(need_entity_id)
|
||||
|
||||
if not self.tracked:
|
||||
_LOGGER.warning(
|
||||
@ -309,3 +270,72 @@ 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.
|
||||
Does not generate the entity id yet.
|
||||
"""
|
||||
default_last_seen = dt_util.utcnow().replace(year=1990)
|
||||
|
||||
self.tracked[device] = {
|
||||
'name': name,
|
||||
'last_seen': default_last_seen,
|
||||
'state_attr': {ATTR_FRIENDLY_NAME: name}
|
||||
}
|
||||
|
||||
def _generate_entity_ids(self, need_entity_id):
|
||||
""" Generate entity ids for a list of devices. """
|
||||
# Setup entity_ids for the new devices
|
||||
used_entity_ids = [info['entity_id'] for device, info
|
||||
in self.tracked.items()
|
||||
if device not in need_entity_id]
|
||||
|
||||
for device in need_entity_id:
|
||||
name = self.tracked[device]['name']
|
||||
|
||||
entity_id = util.ensure_unique_string(
|
||||
ENTITY_ID_FORMAT.format(util.slugify(name)),
|
||||
used_entity_ids)
|
||||
|
||||
used_entity_ids.append(entity_id)
|
||||
|
||||
self.tracked[device]['entity_id'] = entity_id
|
||||
|
@ -114,12 +114,13 @@ class LuciDeviceScanner(object):
|
||||
hosts = [x for x in result.values()
|
||||
if x['.type'] == 'host' and
|
||||
'mac' in x and 'name' in x]
|
||||
mac2name_list = [(x['mac'], x['name']) for x in hosts]
|
||||
mac2name_list = [
|
||||
(x['mac'].upper(), x['name']) for x in hosts]
|
||||
self.mac2name = dict(mac2name_list)
|
||||
else:
|
||||
# Error, handled in the _req_json_rpc
|
||||
return
|
||||
return self.mac2name.get(device, None)
|
||||
return self.mac2name.get(device.upper(), None)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
|
@ -120,7 +120,7 @@ class NmapDeviceScanner(object):
|
||||
else:
|
||||
mac = _arp(host.ipv4)
|
||||
if mac:
|
||||
device = Device(mac, name, host.ipv4, now)
|
||||
device = Device(mac.upper(), name, host.ipv4, now)
|
||||
self.last_results.append(device)
|
||||
_LOGGER.info("nmap scan successful")
|
||||
return True
|
||||
|
@ -9,19 +9,19 @@
|
||||
<meta name='apple-mobile-web-app-capable' content='yes'>
|
||||
<meta name='mobile-web-app-capable' content='yes'>
|
||||
|
||||
<meta name='viewport' content='width=device-width,
|
||||
<meta name='viewport' content='width=device-width,
|
||||
user-scalable=no' />
|
||||
|
||||
<link rel='shortcut icon' href='/static/favicon.ico' />
|
||||
<link rel='icon' type='image/png'
|
||||
<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>
|
||||
<h3 id='init' align='center'>Initializing Home Assistant</h3>
|
||||
<script src='/static/webcomponents.min.js'></script>
|
||||
<script src='/static/webcomponents-lite.min.js'></script>
|
||||
<link rel='import' href='/static/{{ app_url }}' />
|
||||
<home-assistant auth='{{ auth }}'></home-assistant>
|
||||
</body>
|
||||
|
@ -1,2 +1,2 @@
|
||||
""" DO NOT MODIFY. Auto-generated by build_frontend script """
|
||||
VERSION = "28c0680cf6ebd969dc5710c22d9c4075"
|
||||
VERSION = "ddf42f54c15daf472d4c8641fab8d418"
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
File diff suppressed because one or more lines are too long
@ -10,38 +10,36 @@
|
||||
"ignore": [
|
||||
"bower_components"
|
||||
],
|
||||
"dependencies": {
|
||||
"webcomponentsjs": "Polymer/webcomponentsjs#~0.6",
|
||||
"font-roboto": "Polymer/font-roboto#~0.5.5",
|
||||
"core-header-panel": "polymer/core-header-panel#~0.5.5",
|
||||
"core-toolbar": "polymer/core-toolbar#~0.5.5",
|
||||
"core-tooltip": "Polymer/core-tooltip#~0.5.5",
|
||||
"core-menu": "polymer/core-menu#~0.5.5",
|
||||
"core-item": "Polymer/core-item#~0.5.5",
|
||||
"core-input": "Polymer/core-input#~0.5.5",
|
||||
"core-icons": "polymer/core-icons#~0.5.5",
|
||||
"core-image": "polymer/core-image#~0.5.5",
|
||||
"core-style": "polymer/core-style#~0.5.5",
|
||||
"core-label": "polymer/core-label#~0.5.5",
|
||||
"paper-toast": "Polymer/paper-toast#~0.5.5",
|
||||
"paper-dialog": "Polymer/paper-dialog#~0.5.5",
|
||||
"paper-spinner": "Polymer/paper-spinner#~0.5.5",
|
||||
"paper-button": "Polymer/paper-button#~0.5.5",
|
||||
"paper-input": "Polymer/paper-input#~0.5.5",
|
||||
"paper-toggle-button": "polymer/paper-toggle-button#~0.5.5",
|
||||
"paper-icon-button": "polymer/paper-icon-button#~0.5.5",
|
||||
"paper-menu-button": "polymer/paper-menu-button#~0.5.5",
|
||||
"paper-dropdown": "polymer/paper-dropdown#~0.5.5",
|
||||
"paper-item": "polymer/paper-item#~0.5.5",
|
||||
"paper-slider": "polymer/paper-slider#~0.5.5",
|
||||
"paper-checkbox": "polymer/paper-checkbox#~0.5.5",
|
||||
"color-picker-element": "~0.0.2",
|
||||
"google-apis": "GoogleWebComponents/google-apis#~0.4.4",
|
||||
"core-drawer-panel": "polymer/core-drawer-panel#~0.5.5",
|
||||
"core-scroll-header-panel": "polymer/core-scroll-header-panel#~0.5.5",
|
||||
"moment": "~2.10.2"
|
||||
"devDependencies": {
|
||||
"polymer": "Polymer/polymer#^1.0.0",
|
||||
"webcomponentsjs": "Polymer/webcomponentsjs#^0.7",
|
||||
"paper-header-panel": "PolymerElements/paper-header-panel#^1.0.0",
|
||||
"paper-toolbar": "PolymerElements/paper-toolbar#^1.0.0",
|
||||
"paper-menu": "PolymerElements/paper-menu#^1.0.0",
|
||||
"iron-input": "PolymerElements/iron-input#^1.0.0",
|
||||
"iron-icons": "PolymerElements/iron-icons#^1.0.0",
|
||||
"iron-image": "PolymerElements/iron-image#^1.0.0",
|
||||
"paper-toast": "PolymerElements/paper-toast#^1.0.0",
|
||||
"paper-dialog": "PolymerElements/paper-dialog#^1.0.0",
|
||||
"paper-dialog-scrollable": "polymerelements/paper-dialog-scrollable#^1.0.0",
|
||||
"paper-spinner": "PolymerElements/paper-spinner#^1.0.0",
|
||||
"paper-button": "PolymerElements/paper-button#^1.0.0",
|
||||
"paper-input": "PolymerElements/paper-input#^1.0.0",
|
||||
"paper-toggle-button": "PolymerElements/paper-toggle-button#^1.0.0",
|
||||
"paper-icon-button": "PolymerElements/paper-icon-button#^1.0.0",
|
||||
"paper-item": "PolymerElements/paper-item#^1.0.0",
|
||||
"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",
|
||||
"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"
|
||||
},
|
||||
"resolutions": {
|
||||
"webcomponentsjs": "~0.6"
|
||||
"polymer": "^1.0.0",
|
||||
"webcomponentsjs": "^0.7.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,29 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel='import' href='../bower_components/polymer/polymer.html'>
|
||||
|
||||
<link rel="import" href="./state-card-display.html">
|
||||
<link rel="import" href="../components/state-info.html">
|
||||
<link rel='import' href='./state-card-display.html'>
|
||||
<link rel='import' href='../components/state-info.html'>
|
||||
|
||||
<polymer-element name="state-card-configurator" attributes="stateObj" noscript>
|
||||
<dom-module id='state-card-configurator'>
|
||||
<template>
|
||||
<state-card-display stateObj="{{stateObj}}"></state-card-display>
|
||||
<state-card-display state-obj='[[stateObj]]'></state-card-display>
|
||||
|
||||
<!-- pre load the image so the dialog is rendered the proper size -->
|
||||
<template if="{{stateObj.attributes.description_image}}">
|
||||
<img hidden src="{{stateObj.attributes.description_image}}" />
|
||||
<template is='dom-if' if='[[stateObj.attributes.description_image]]'>
|
||||
<img hidden src='[[stateObj.attributes.description_image]]' />
|
||||
</template>
|
||||
</template>
|
||||
</polymer-element>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
Polymer({
|
||||
is: 'state-card-configurator',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
@ -5,44 +5,46 @@
|
||||
<link rel="import" href="state-card-thermostat.html">
|
||||
<link rel="import" href="state-card-configurator.html">
|
||||
<link rel="import" href="state-card-scene.html">
|
||||
<link rel="import" href="state-card-media_player.html">
|
||||
|
||||
<polymer-element name="state-card-content" attributes="stateObj">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id='cardContainer'></div>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
stateObjChanged: function(oldVal, newVal) {
|
||||
var cardContainer = this.$.cardContainer;
|
||||
(function() {
|
||||
var uiUtil = window.hass.uiUtil;
|
||||
|
||||
if (!newVal) {
|
||||
if (cardContainer.lastChild) {
|
||||
cardContainer.removeChild(cardContainer.lastChild);
|
||||
Polymer({
|
||||
is: 'state-card-content',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: 'stateObjChanged',
|
||||
}
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
if (!oldVal || oldVal.cardType != newVal.cardType) {
|
||||
if (cardContainer.lastChild) {
|
||||
cardContainer.removeChild(cardContainer.lastChild);
|
||||
stateObjChanged: function(newVal, oldVal) {
|
||||
var root = Polymer.dom(this);
|
||||
|
||||
if (!newVal) {
|
||||
if (root.lastChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var stateCard = document.createElement("state-card-" + newVal.cardType);
|
||||
stateCard.stateObj = newVal;
|
||||
cardContainer.appendChild(stateCard);
|
||||
var newCardType = uiUtil.stateCardType(newVal);
|
||||
|
||||
} else {
|
||||
if (!oldVal || uiUtil.stateCardType(oldVal) != newCardType) {
|
||||
if (root.lastChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
|
||||
cardContainer.lastChild.stateObj = newVal;
|
||||
|
||||
}
|
||||
},
|
||||
});
|
||||
var stateCard = document.createElement("state-card-" + newCardType);
|
||||
stateCard.stateObj = newVal;
|
||||
root.appendChild(stateCard);
|
||||
} else {
|
||||
root.lastChild.stateObj = newVal;
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</polymer-element>
|
||||
|
@ -2,21 +2,35 @@
|
||||
|
||||
<link rel="import" href="../components/state-info.html">
|
||||
|
||||
<polymer-element name="state-card-display" attributes="stateObj" noscript>
|
||||
<template>
|
||||
<style>
|
||||
.state {
|
||||
margin-left: 16px;
|
||||
text-transform: capitalize;
|
||||
font-weight: 300;
|
||||
font-size: 1.3rem;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
<dom-module id="state-card-display">
|
||||
<style>
|
||||
.state {
|
||||
margin-left: 16px;
|
||||
text-transform: capitalize;
|
||||
font-weight: 300;
|
||||
font-size: 1.3rem;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div horizontal justified layout>
|
||||
<state-info stateObj="{{stateObj}}"></state-info>
|
||||
<div class='state'>{{stateObj.stateDisplay}}</div>
|
||||
<template>
|
||||
<div class='horizontal justified layout'>
|
||||
<state-info state-obj="[[stateObj]]"></state-info>
|
||||
<div class='state'>[[stateObj.stateDisplay]]</div>
|
||||
</div>
|
||||
</template>
|
||||
</polymer-element>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
Polymer({
|
||||
is: 'state-card-display',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
@ -0,0 +1,91 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../components/state-info.html">
|
||||
|
||||
<dom-module id="state-card-media_player">
|
||||
<style>
|
||||
:host {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.state {
|
||||
margin-left: 16px;
|
||||
text-align: right;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.main-text {
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-transform: capitalize;
|
||||
font-weight: 300;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.secondary-text {
|
||||
color: darkgrey;
|
||||
margin-top: -2px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class='horizontal justified layout'>
|
||||
<state-info state-obj="[[stateObj]]"></state-info>
|
||||
<div class='state'>
|
||||
<div class='main-text'>[[computePrimaryText(stateObj, isPlaying)]]</div>
|
||||
<div class='secondary-text'>[[computeSecondaryText(stateObj, isPlaying)]]</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var PLAYING_STATES = ['playing', 'paused'];
|
||||
Polymer({
|
||||
is: 'state-card-media_player',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
isPlaying: {
|
||||
type: Boolean,
|
||||
computed: 'computeIsPlaying(stateObj)',
|
||||
},
|
||||
},
|
||||
|
||||
computeIsPlaying: function(stateObj) {
|
||||
return PLAYING_STATES.indexOf(stateObj.state) !== -1;
|
||||
},
|
||||
|
||||
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 '';
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
@ -3,25 +3,37 @@
|
||||
<link rel="import" href="./state-card-display.html">
|
||||
<link rel="import" href="./state-card-toggle.html">
|
||||
|
||||
<polymer-element name="state-card-scene" attributes="stateObj">
|
||||
<dom-module id="state-card-scene">
|
||||
<template>
|
||||
<template if={{allowToggle}}>
|
||||
<state-card-toggle stateObj="{{stateObj}}"></state-card-toggle>
|
||||
<template is='dom-if' if='[[allowToggle]]'>
|
||||
<state-card-toggle state-obj="[[stateObj]]"></state-card-toggle>
|
||||
</template>
|
||||
<template if={{!allowToggle}}>
|
||||
<state-card-display stateObj="{{stateObj}}"></state-card-display>
|
||||
<template is='dom-if' if='[[!allowToggle]]'>
|
||||
<state-card-display state-obj="[[stateObj]]"></state-card-display>
|
||||
</template>
|
||||
</template>
|
||||
<script>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
Polymer({
|
||||
allowToggle: false,
|
||||
is: 'state-card-scene',
|
||||
|
||||
stateObjChanged: function(oldVal, newVal) {
|
||||
this.allowToggle = newVal.state === 'off' ||
|
||||
newVal.attributes.active_requested;
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
allowToggle: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
computed: 'computeAllowToggle(stateObj)',
|
||||
},
|
||||
},
|
||||
|
||||
computeAllowToggle: function(stateObj) {
|
||||
return stateObj.state === 'off' || stateObj.attributes.active_requested;
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</polymer-element>
|
||||
</script>
|
@ -2,9 +2,12 @@
|
||||
|
||||
<link rel="import" href="../components/state-info.html">
|
||||
|
||||
<polymer-element name="state-card-thermostat" attributes="stateObj api">
|
||||
<template>
|
||||
<dom-module id="state-card-thermostat">
|
||||
<style>
|
||||
:host {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.state {
|
||||
margin-left: 16px;
|
||||
text-align: right;
|
||||
@ -19,23 +22,36 @@
|
||||
.current {
|
||||
color: darkgrey;
|
||||
margin-top: -2px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class='horizontal justified layout'>
|
||||
<state-info state-obj="[[stateObj]]"></state-info>
|
||||
<div class='state'>
|
||||
<div class='target'>[[stateObj.stateDisplay]]</div>
|
||||
|
||||
<div horizontal justified layout>
|
||||
<state-info stateObj="{{stateObj}}"></state-info>
|
||||
<div class='state'>
|
||||
<div class='target'>
|
||||
{{stateObj.stateDisplay}}
|
||||
</div>
|
||||
|
||||
<div class='current'>
|
||||
Currently: {{stateObj.attributes.current_temperature}} {{stateObj.attributes.unit_of_measurement}}
|
||||
<div class='current'>
|
||||
<span>Currently: </span>
|
||||
<span>[[stateObj.attributes.current_temperature]]</span>
|
||||
<span> </span>
|
||||
<span>[[stateObj.attributes.unit_of_measurement]]</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
Polymer({});
|
||||
(function() {
|
||||
Polymer({
|
||||
is: 'state-card-thermostat',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</polymer-element>
|
||||
|
@ -1,39 +1,53 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/paper-toggle-button/paper-toggle-button.html">
|
||||
<link rel="import" href="../bower_components/core-style/core-style.html">
|
||||
|
||||
<link rel="import" href="../components/state-info.html">
|
||||
|
||||
<polymer-element name="state-card-toggle" attributes="stateObj">
|
||||
<dom-module id="state-card-toggle">
|
||||
<style>
|
||||
paper-toggle-button {
|
||||
margin-left: 16px;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<core-style ref='ha-paper-toggle'></core-style>
|
||||
<div class='horizontal justified layout'>
|
||||
<state-info state-obj="[[stateObj]]"></state-info>
|
||||
|
||||
<div horizontal justified layout>
|
||||
<state-info flex stateObj="{{stateObj}}"></state-info>
|
||||
|
||||
<paper-toggle-button self-center
|
||||
checked="{{toggleChecked}}"
|
||||
on-change="{{toggleChanged}}"
|
||||
on-click="{{toggleClicked}}">
|
||||
<paper-toggle-button class='self-center'
|
||||
checked="[[toggleChecked]]"
|
||||
on-change="toggleChanged"
|
||||
on-tap="toggleTapped">
|
||||
</paper-toggle-button>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var serviceActions = window.hass.serviceActions;
|
||||
|
||||
Polymer({
|
||||
toggleChecked: false,
|
||||
is: 'state-card-toggle',
|
||||
|
||||
observe: {
|
||||
'stateObj.state': 'stateChanged'
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: 'stateObjChanged',
|
||||
},
|
||||
|
||||
toggleChecked: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
|
||||
ready: function() {
|
||||
this.forceStateChange = this.forceStateChange.bind(this);
|
||||
this.forceStateChange();
|
||||
},
|
||||
|
||||
toggleClicked: function(ev) {
|
||||
toggleTapped: function(ev) {
|
||||
ev.stopPropagation();
|
||||
},
|
||||
|
||||
@ -47,22 +61,22 @@
|
||||
}
|
||||
},
|
||||
|
||||
stateObjChanged: function(oldVal, newVal) {
|
||||
stateObjChanged: function(newVal) {
|
||||
if (newVal) {
|
||||
this.stateChanged(null, newVal.state);
|
||||
this.updateToggle(newVal);
|
||||
}
|
||||
},
|
||||
|
||||
stateChanged: function(oldVal, newVal) {
|
||||
this.toggleChecked = newVal === "on";
|
||||
updateToggle: function(stateObj) {
|
||||
this.toggleChecked = stateObj && stateObj.state === "on";
|
||||
},
|
||||
|
||||
forceStateChange: function() {
|
||||
this.stateChanged(null, this.stateObj.state);
|
||||
this.updateToggle(this.stateObj);
|
||||
},
|
||||
|
||||
turn_on: function() {
|
||||
// We call stateChanged after a successful call to re-sync the toggle
|
||||
// We call updateToggle after a successful call to re-sync the toggle
|
||||
// with the state. It will be out of sync if our service call did not
|
||||
// result in the entity to be turned on. Since the state is not changing,
|
||||
// the resync is not called automatic.
|
||||
@ -70,12 +84,12 @@
|
||||
},
|
||||
|
||||
turn_off: function() {
|
||||
// We call stateChanged after a successful call to re-sync the toggle
|
||||
// We call updateToggle after a successful call to re-sync the toggle
|
||||
// with the state. It will be out of sync if our service call did not
|
||||
// result in the entity to be turned on. Since the state is not changing,
|
||||
// the resync is not called automatic.
|
||||
serviceActions.callTurnOff(this.stateObj.entityId).then(this.forceStateChange);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
||||
})();
|
||||
</script>
|
||||
|
@ -2,8 +2,7 @@
|
||||
|
||||
<link rel="import" href="state-card-content.html">
|
||||
|
||||
<polymer-element name="state-card" attributes="stateObj" on-click="cardClicked">
|
||||
<template>
|
||||
<dom-module id="state-card">
|
||||
<style>
|
||||
:host {
|
||||
border-radius: 2px;
|
||||
@ -16,18 +15,37 @@
|
||||
width: 100%;
|
||||
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<state-card-content stateObj={{stateObj}}></state-card-content>
|
||||
</template>
|
||||
<script>
|
||||
var uiActions = window.hass.uiActions;
|
||||
<template>
|
||||
<state-card-content state-obj="[[stateObj]]"></state-card-content>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
Polymer({
|
||||
cardClicked: function() {
|
||||
uiActions.showMoreInfoDialog(this.stateObj.entityId);
|
||||
},
|
||||
});
|
||||
<script>
|
||||
(function(){
|
||||
var uiActions = window.hass.uiActions;
|
||||
|
||||
Polymer({
|
||||
is: 'state-card',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
|
||||
listeners: {
|
||||
// listening for click instead of tap as a work around
|
||||
// https://github.com/PolymerElements/iron-overlay-behavior/issues/14
|
||||
'click': 'cardTapped',
|
||||
},
|
||||
|
||||
cardTapped: function() {
|
||||
uiActions.showMoreInfoDialog(this.stateObj.entityId);
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</polymer-element>
|
||||
|
@ -1,26 +1,25 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../resources/moment-js.html">
|
||||
<dom-module id="display-time">
|
||||
<template>[[computeTime(dateObj)]]</template>
|
||||
</dom-module>
|
||||
|
||||
<polymer-element name="display-time" attributes="dateObj">
|
||||
<template>
|
||||
{{ time }}
|
||||
</template>
|
||||
<script>
|
||||
(function() {
|
||||
var uiUtil = window.hass.uiUtil;
|
||||
<script>
|
||||
(function() {
|
||||
var uiUtil = window.hass.uiUtil;
|
||||
|
||||
Polymer({
|
||||
time: "",
|
||||
Polymer({
|
||||
is: 'display-time',
|
||||
|
||||
dateObjChanged: function(oldVal, newVal) {
|
||||
if (newVal) {
|
||||
this.time = uiUtil.formatTime(newVal);
|
||||
} else {
|
||||
this.time = "";
|
||||
}
|
||||
properties: {
|
||||
dateObj: {
|
||||
type: Object,
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</polymer-element>
|
||||
},
|
||||
|
||||
computeTime: function(dateObj) {
|
||||
return dateObj ? uiUtil.formatTime(dateObj) : '';
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
@ -1,24 +1,37 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../bower_components/iron-icon/iron-icon.html">
|
||||
|
||||
<link rel="import" href="../resources/home-assistant-icons.html">
|
||||
|
||||
<polymer-element name="domain-icon"
|
||||
attributes="domain state" constructor="DomainIcon">
|
||||
<dom-module id="domain-icon">
|
||||
<template>
|
||||
<core-icon icon="{{icon}}"></core-icon>
|
||||
<iron-icon icon="[[computeIcon(domain, state)]]"></iron-icon>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
icon: '',
|
||||
</dom-module>
|
||||
|
||||
observe: {
|
||||
'domain': 'updateIcon',
|
||||
'state' : 'updateIcon',
|
||||
<script>
|
||||
(function() {
|
||||
var uiUtil = window.hass.uiUtil;
|
||||
|
||||
Polymer({
|
||||
is: 'domain-icon',
|
||||
|
||||
properties: {
|
||||
domain: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
|
||||
state: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
|
||||
updateIcon: function() {
|
||||
this.icon = window.hass.uiUtil.domainIcon(this.domain, this.state);
|
||||
computeIcon: function(domain, state) {
|
||||
return uiUtil.domainIcon(domain, state);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
||||
})();
|
||||
</script>
|
||||
|
@ -1,60 +1,53 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<polymer-element name="entity-list" attributes="cbEntityClicked">
|
||||
<dom-module id="entity-list">
|
||||
<style>
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
line-height: 2em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.entityContainer {
|
||||
font-size: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template if={{cbEntityClicked}}>
|
||||
<style>
|
||||
a {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<template repeat="{{entityID in entityIDs}}">
|
||||
<div class='eventContainer'>
|
||||
<a on-click={{handleClick}}>{{entityID}}</a>
|
||||
</div>
|
||||
<ul>
|
||||
<template is='dom-repeat' items='[[entities]]' as='entity'>
|
||||
<li><a href='#' on-click='entitySelected'>[[entity]]</a></li>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</ul>
|
||||
</template>
|
||||
<script>
|
||||
var storeListenerMixIn = window.hass.storeListenerMixIn;
|
||||
</dom-module>
|
||||
|
||||
Polymer(Polymer.mixin({
|
||||
cbEventClicked: null,
|
||||
entityIDs: [],
|
||||
<script>
|
||||
(function() {
|
||||
Polymer({
|
||||
is: 'entity-list',
|
||||
|
||||
attached: function() {
|
||||
this.listenToStores(true);
|
||||
},
|
||||
behaviors: [StoreListenerBehavior],
|
||||
|
||||
detached: function() {
|
||||
this.stopListeningToStores();
|
||||
properties: {
|
||||
entities: {
|
||||
type: Array,
|
||||
value: [],
|
||||
},
|
||||
},
|
||||
|
||||
stateStoreChanged: function(stateStore) {
|
||||
this.entityIDs = stateStore.entityIDs.toArray();
|
||||
this.entities = stateStore.entityIDs.toArray();
|
||||
},
|
||||
|
||||
handleClick: function(ev) {
|
||||
if(this.cbEntityClicked) {
|
||||
this.cbEntityClicked(ev.path[0].innerHTML);
|
||||
}
|
||||
entitySelected: function(ev) {
|
||||
ev.preventDefault();
|
||||
this.fire('entity-selected', {entityId: ev.model.entity});
|
||||
},
|
||||
|
||||
}, storeListenerMixIn));
|
||||
</script>
|
||||
</polymer-element>
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
@ -1,62 +1,56 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<polymer-element name="events-list" attributes="cbEventClicked">
|
||||
<dom-module id="events-list">
|
||||
<style>
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
line-height: 2em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.eventContainer {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<template if={{cbEventClicked}}>
|
||||
<style>
|
||||
a {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<template repeat="{{event in events}}">
|
||||
<div class='eventContainer'>
|
||||
<a on-click={{handleClick}}>{{event.event}}</a>
|
||||
({{event.listener_count}} listeners)
|
||||
</div>
|
||||
<ul>
|
||||
<template is='dom-repeat' items='[[events]]' as='event'>
|
||||
<li>
|
||||
<a href='#' on-click='eventSelected'>{{event.event}}</a>
|
||||
<span> (</span><span>{{event.listener_count}}</span><span> listeners)</span>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</ul>
|
||||
</template>
|
||||
<script>
|
||||
var storeListenerMixIn = window.hass.storeListenerMixIn;
|
||||
</dom-module>
|
||||
|
||||
Polymer(Polymer.mixin({
|
||||
cbEventClicked: null,
|
||||
events: [],
|
||||
<script>
|
||||
(function() {
|
||||
Polymer({
|
||||
is: 'events-list',
|
||||
|
||||
attached: function() {
|
||||
this.listenToStores(true);
|
||||
},
|
||||
behaviors: [StoreListenerBehavior],
|
||||
|
||||
detached: function() {
|
||||
this.stopListeningToStores();
|
||||
properties: {
|
||||
events: {
|
||||
type: Array,
|
||||
value: [],
|
||||
},
|
||||
},
|
||||
|
||||
eventStoreChanged: function(eventStore) {
|
||||
this.events = eventStore.all.toArray();
|
||||
},
|
||||
|
||||
handleClick: function(ev) {
|
||||
if(this.cbEventClicked) {
|
||||
this.cbEventClicked(ev.path[0].innerHTML);
|
||||
}
|
||||
eventSelected: function(ev) {
|
||||
ev.preventDefault();
|
||||
this.fire('event-selected', {eventType: ev.model.event.event});
|
||||
},
|
||||
|
||||
}, storeListenerMixIn));
|
||||
</script>
|
||||
</polymer-element>
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
@ -2,16 +2,31 @@
|
||||
|
||||
<link rel="import" href="../components/logbook-entry.html">
|
||||
|
||||
<polymer-element name="ha-logbook" attributes="entries" noscript>
|
||||
<template>
|
||||
<dom-module id="ha-logbook">
|
||||
<style>
|
||||
.logbook {
|
||||
:host {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
<div class='logbook'>
|
||||
<template repeat="{{entries as entry}}">
|
||||
<logbook-entry entryObj="{{entry}}"></logbook-entry>
|
||||
<template>
|
||||
<template is='dom-repeat' items="[[entries]]">
|
||||
<logbook-entry entry-obj="[[item]]"></logbook-entry>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</polymer>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
Polymer({
|
||||
is: 'ha-logbook',
|
||||
|
||||
properties: {
|
||||
entries: {
|
||||
type: Object,
|
||||
value: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
@ -1,37 +0,0 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/paper-toast/paper-toast.html">
|
||||
|
||||
<polymer-element name="ha-notifications">
|
||||
<template>
|
||||
<paper-toast id="toast" role="alert" text=""></paper-toast>
|
||||
</template>
|
||||
<script>
|
||||
var storeListenerMixIn = window.hass.storeListenerMixIn;
|
||||
|
||||
Polymer(Polymer.mixin({
|
||||
lastId: null,
|
||||
|
||||
attached: function() {
|
||||
this.listenToStores(true);
|
||||
},
|
||||
|
||||
detached: function() {
|
||||
this.stopListeningToStores();
|
||||
},
|
||||
|
||||
notificationStoreChanged: function(notificationStore) {
|
||||
if (notificationStore.hasNewNotifications(this.lastId)) {
|
||||
var toast = this.$.toast;
|
||||
var notification = notificationStore.lastNotification;
|
||||
|
||||
if (notification) {
|
||||
this.lastId = notification.id;
|
||||
toast.text = notification.message;
|
||||
toast.show();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
}, storeListenerMixIn));
|
||||
</script>
|
||||
</polymer-element>
|
@ -1,25 +1,27 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/paper-spinner/paper-spinner.html">
|
||||
|
||||
<polymer-element name="loading-box" attributes="text">
|
||||
<dom-module id="loading-box">
|
||||
<style>
|
||||
.text {
|
||||
display: inline-block;
|
||||
line-height: 28px;
|
||||
vertical-align: top;
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<style>
|
||||
.text {
|
||||
display: inline-block;
|
||||
line-height: 28px;
|
||||
vertical-align: top;
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
<div layout='horizontal'>
|
||||
<paper-spinner active="true"></paper-spinner>
|
||||
<div class='text'>{{text}}…</div>
|
||||
<div class='text'><content></content>…</div>
|
||||
</div>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
<script>
|
||||
(function() {
|
||||
Polymer({
|
||||
text: "Loading"
|
||||
is: 'loading-box',
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
||||
})();
|
||||
</script>
|
||||
|
@ -1,25 +1,25 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/core-style/core-style.html">
|
||||
|
||||
<link rel="import" href="domain-icon.html">
|
||||
<link rel="import" href="display-time.html">
|
||||
<link rel="import" href="relative-ha-datetime.html">
|
||||
|
||||
<polymer-element name="logbook-entry" attributes="entryObj">
|
||||
<template>
|
||||
<core-style ref='ha-main'></core-style>
|
||||
<dom-module id="logbook-entry">
|
||||
<style>
|
||||
.logbook-entry {
|
||||
:host {
|
||||
display: block;
|
||||
line-height: 2em;
|
||||
}
|
||||
|
||||
.time {
|
||||
display-time {
|
||||
width: 55px;
|
||||
font-size: .8em;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.icon {
|
||||
domain-icon {
|
||||
margin: 0 8px 0 16px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.name {
|
||||
@ -27,29 +27,38 @@
|
||||
}
|
||||
|
||||
.message {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div horizontal layout class='logbook-entry'>
|
||||
<display-time dateObj="{{entryObj.when}}" class='time secondary-text-color'></display-time>
|
||||
<domain-icon domain="{{entryObj.domain}}" class='icon primary-text-color'></domain-icon>
|
||||
<div class='message primary-text-color' flex>
|
||||
<template if="{{!entryObj.entityId}}">
|
||||
<span class='name'>{{entryObj.name}}</span>
|
||||
</template>
|
||||
<template if="{{entryObj.entityId}}">
|
||||
<a href='#' on-click="{{entityClicked}}" class='name'>{{entryObj.name}}</a>
|
||||
</template>
|
||||
{{entryObj.message}}
|
||||
<template>
|
||||
<div class='horizontal layout'>
|
||||
<display-time date-obj="[[entryObj.when]]"></display-time>
|
||||
<domain-icon domain="[[entryObj.domain]]" class='icon'></domain-icon>
|
||||
<div class='message' flex>
|
||||
<template is='dom-if' if="[[!entryObj.entityId]]">
|
||||
<span class='name'>[[entryObj.name]]</span>
|
||||
</template>
|
||||
<template is='dom-if' if="[[entryObj.entityId]]">
|
||||
<a href='#' on-click="entityClicked" class='name'>[[entryObj.name]]</a>
|
||||
<span> </span>
|
||||
</template>
|
||||
<span>[[entryObj.message]]</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var uiActions = window.hass.uiActions;
|
||||
|
||||
Polymer({
|
||||
is: 'logbook-entry',
|
||||
|
||||
entityClicked: function(ev) {
|
||||
ev.preventDefault();
|
||||
uiActions.showMoreInfoDialog(this.entryObj.entityId);
|
||||
@ -58,4 +67,3 @@
|
||||
|
||||
})();
|
||||
</script>
|
||||
</polymer-element>
|
||||
|
@ -1,50 +0,0 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/core-style/core-style.html">
|
||||
|
||||
<link rel="import" href="./loading-box.html">
|
||||
<link rel="import" href="relative-ha-datetime.html">
|
||||
|
||||
<polymer-element name="recent-states" attributes="stateObj">
|
||||
<template>
|
||||
<core-style ref='ha-data-table'></core-style>
|
||||
|
||||
<template if="{{recentStates === null}}">
|
||||
<loading-box text="Loading recent states"></loading-box>
|
||||
</template>
|
||||
|
||||
<template if="{{recentStates !== null}}">
|
||||
<div layout vertical>
|
||||
<template repeat="{{recentStates as state}}">
|
||||
<div layout justified horizontal class='data-entry'>
|
||||
<div>
|
||||
{{state.state}}
|
||||
</div>
|
||||
<div class='data'>
|
||||
<relative-ha-datetime datetime="{{stateObj.last_changed}}">
|
||||
</relative-ha-datetime>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template if="{{recentStates.length == 0}}">
|
||||
There are no recent states.
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
recentStates: null,
|
||||
|
||||
stateObjChanged: function() {
|
||||
this.recentStates = null;
|
||||
|
||||
window.hass.callApi(
|
||||
'GET', 'history/entity/' + this.stateObj.entityId + '/recent_states').then(
|
||||
function(states) {
|
||||
this.recentStates = states.slice(1);
|
||||
}.bind(this));
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -2,49 +2,73 @@
|
||||
|
||||
<link rel="import" href="../resources/moment-js.html">
|
||||
|
||||
<polymer-element name="relative-ha-datetime" attributes="datetime datetimeObj">
|
||||
<dom-module id="relative-ha-datetime">
|
||||
<template>
|
||||
{{ relativeTime }}
|
||||
<span>[[relativeTime]]</span>
|
||||
</template>
|
||||
<script>
|
||||
(function() {
|
||||
var UPDATE_INTERVAL = 60000; // 60 seconds
|
||||
</dom-module>
|
||||
|
||||
var parseDateTime = window.hass.util.parseDateTime;
|
||||
<script>
|
||||
(function() {
|
||||
var UPDATE_INTERVAL = 60000; // 60 seconds
|
||||
|
||||
Polymer({
|
||||
relativeTime: "",
|
||||
parsedDateTime: null,
|
||||
var parseDateTime = window.hass.util.parseDateTime;
|
||||
|
||||
created: function() {
|
||||
this.updateRelative = this.updateRelative.bind(this);
|
||||
Polymer({
|
||||
is: 'relative-ha-datetime',
|
||||
|
||||
properties: {
|
||||
datetime: {
|
||||
type: String,
|
||||
observer: 'datetimeChanged',
|
||||
},
|
||||
|
||||
attached: function() {
|
||||
this._interval = setInterval(this.updateRelative, UPDATE_INTERVAL);
|
||||
datetimeObj: {
|
||||
type: Object,
|
||||
observer: 'datetimeObjChanged',
|
||||
},
|
||||
|
||||
detached: function() {
|
||||
clearInterval(this._interval);
|
||||
parsedDateTime: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
datetimeChanged: function(oldVal, newVal) {
|
||||
this.parsedDateTime = newVal ? parseDateTime(newVal) : null;
|
||||
|
||||
this.updateRelative();
|
||||
relativeTime: {
|
||||
type: String,
|
||||
value: 'not set',
|
||||
},
|
||||
},
|
||||
|
||||
datetimeObjChanged: function(oldVal, newVal) {
|
||||
this.parsedDateTime = newVal;
|
||||
relativeTime: "",
|
||||
parsedDateTime: null,
|
||||
|
||||
this.updateRelative();
|
||||
},
|
||||
created: function() {
|
||||
this.updateRelative = this.updateRelative.bind(this);
|
||||
},
|
||||
|
||||
updateRelative: function() {
|
||||
this.relativeTime = this.parsedDateTime ?
|
||||
moment(this.parsedDateTime).fromNow() : "";
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</polymer-element>
|
||||
attached: function() {
|
||||
this._interval = setInterval(this.updateRelative, UPDATE_INTERVAL);
|
||||
},
|
||||
|
||||
detached: function() {
|
||||
clearInterval(this._interval);
|
||||
},
|
||||
|
||||
datetimeChanged: function(newVal) {
|
||||
this.parsedDateTime = newVal ? parseDateTime(newVal) : null;
|
||||
|
||||
this.updateRelative();
|
||||
},
|
||||
|
||||
datetimeObjChanged: function(newVal) {
|
||||
this.parsedDateTime = newVal;
|
||||
|
||||
this.updateRelative();
|
||||
},
|
||||
|
||||
updateRelative: function() {
|
||||
this.relativeTime = this.parsedDateTime ?
|
||||
moment(this.parsedDateTime).fromNow() : "";
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
@ -1,72 +1,58 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/core-menu/core-menu.html">
|
||||
<link rel="import" href="../bower_components/core-menu/core-submenu.html">
|
||||
<link rel="import" href="../bower_components/core-item/core-item.html">
|
||||
|
||||
<link rel="import" href="../bower_components/paper-menu/paper-menu.html">
|
||||
|
||||
<link rel="import" href="domain-icon.html">
|
||||
|
||||
<polymer-element name="services-list" attributes="cbServiceClicked">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
<dom-module id="services-list">
|
||||
<style>
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
core-menu {
|
||||
margin-top: 0;
|
||||
font-size: 1rem;
|
||||
li {
|
||||
list-style: none;
|
||||
line-height: 2em;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
<template if={{cbServiceClicked}}>
|
||||
<style>
|
||||
a, core-submenu {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<core-menu selected="0">
|
||||
|
||||
<template repeat="{{domain in domains}}">
|
||||
<core-submenu icon="{{domain | getIcon}}" label="{{domain}}">
|
||||
<template repeat="{{service in domain | getServices}}">
|
||||
<a on-click={{serviceClicked}} data-domain={{domain}}>{{service}}</a>
|
||||
</template>
|
||||
</core-submenu>
|
||||
</template>
|
||||
|
||||
</core-menu>
|
||||
|
||||
</div>
|
||||
<template>
|
||||
<ul>
|
||||
<template is='dom-repeat' items="[[domains]]" as="domain">
|
||||
<template is='dom-repeat' items="[[computeServices(domain)]]" as="service">
|
||||
<li><a href='#' on-click='serviceClicked'>
|
||||
<span>[[domain]]</span>/<span>[[service]]</span>
|
||||
</a></li>
|
||||
</template>
|
||||
</template>
|
||||
</ul>
|
||||
</template>
|
||||
<script>
|
||||
var storeListenerMixIn = window.hass.storeListenerMixIn;
|
||||
</dom-module>
|
||||
|
||||
Polymer(Polymer.mixin({
|
||||
domains: [],
|
||||
services: null,
|
||||
cbServiceClicked: null,
|
||||
<script>
|
||||
(function() {
|
||||
Polymer({
|
||||
is: 'services-list',
|
||||
|
||||
attached: function() {
|
||||
this.listenToStores(true);
|
||||
behaviors: [StoreListenerBehavior],
|
||||
|
||||
properties: {
|
||||
domains: {
|
||||
type: Array,
|
||||
value: [],
|
||||
},
|
||||
|
||||
services: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
|
||||
detached: function() {
|
||||
this.stopListeningToStores();
|
||||
},
|
||||
|
||||
getIcon: function(domain) {
|
||||
return hass.uiUtil.domainIcon(domain);
|
||||
},
|
||||
|
||||
getServices: function(domain) {
|
||||
computeServices: function(domain) {
|
||||
return this.services.get(domain).toArray();
|
||||
},
|
||||
|
||||
@ -76,15 +62,10 @@
|
||||
},
|
||||
|
||||
serviceClicked: function(ev) {
|
||||
if(this.cbServiceClicked) {
|
||||
var target = ev.path[0];
|
||||
var domain = target.getAttributeNode("data-domain").value;
|
||||
var service = target.innerHTML;
|
||||
|
||||
this.cbServiceClicked(domain, service);
|
||||
}
|
||||
}
|
||||
|
||||
}, storeListenerMixIn));
|
||||
</script>
|
||||
</polymer-element>
|
||||
ev.preventDefault();
|
||||
this.fire(
|
||||
'service-selected', {domain: ev.model.domain, service: ev.model.service});
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
@ -1,105 +1,105 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/core-image/core-image.html">
|
||||
<link rel='import' href='../bower_components/polymer/polymer.html'>
|
||||
<link rel='import' href='../bower_components/iron-image/iron-image.html'>
|
||||
|
||||
<link rel="import" href="domain-icon.html">
|
||||
<link rel='import' href='domain-icon.html'>
|
||||
|
||||
<dom-module id='state-badge'>
|
||||
<style>
|
||||
:host {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 45px;
|
||||
background-color: #4fc3f7;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
div {
|
||||
height: 45px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
iron-image {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
domain-icon {
|
||||
margin: 0 auto;
|
||||
transition: color .3s ease-in-out;
|
||||
}
|
||||
|
||||
/* Color the icon if light or sun is on */
|
||||
domain-icon[data-domain=light][data-state=on],
|
||||
domain-icon[data-domain=switch][data-state=on],
|
||||
domain-icon[data-domain=sun][data-state=above_horizon] {
|
||||
color: #fff176;
|
||||
}
|
||||
</style>
|
||||
|
||||
<polymer-element name="state-badge" attributes="stateObj">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 45px;
|
||||
background-color: #4fc3f7;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
transition: all .3s ease-in-out;
|
||||
}
|
||||
|
||||
div {
|
||||
height: 45px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
core-image {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
domain-icon {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Color the icon if light or sun is on */
|
||||
domain-icon[data-domain=light][data-state=on],
|
||||
domain-icon[data-domain=switch][data-state=on],
|
||||
domain-icon[data-domain=sun][data-state=above_horizon] {
|
||||
color: #fff176;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div horizontal layout center>
|
||||
<domain-icon id="icon"
|
||||
domain="{{stateObj.domain}}" data-domain="{{stateObj.domain}}"
|
||||
state="{{stateObj.state}}" data-state="{{stateObj.state}}">
|
||||
<div class='layout horizontal center'>
|
||||
<domain-icon id='icon'
|
||||
domain='[[stateObj.domain]]' data-domain$='[[stateObj.domain]]'
|
||||
state='[[stateObj.state]]' data-state$='[[stateObj.state]]'>
|
||||
</domain-icon>
|
||||
<template if="{{stateObj.attributes.entity_picture}}">
|
||||
<core-image
|
||||
sizing="cover" fit
|
||||
src="{{stateObj.attributes.entity_picture}}"></core-image>
|
||||
<template is='dom-if' if='[[stateObj.attributes.entity_picture]]'>
|
||||
<iron-image
|
||||
sizing='cover' class='fit'
|
||||
src$="[[stateObj.attributes.entity_picture]]"></iron-image>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
observe: {
|
||||
'stateObj.state': 'updateIconColor',
|
||||
'stateObj.attributes.brightness': 'updateIconColor',
|
||||
'stateObj.attributes.xy_color[0]': 'updateIconColor',
|
||||
'stateObj.attributes.xy_color[1]': 'updateIconColor'
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
Polymer({
|
||||
is: 'state-badge',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: 'updateIconColor',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when an attribute changes that influences the color of the icon.
|
||||
*/
|
||||
updateIconColor: function(oldVal, newVal) {
|
||||
var state = this.stateObj;
|
||||
/**
|
||||
* Called when an attribute changes that influences the color of the icon.
|
||||
*/
|
||||
updateIconColor: function(newVal) {
|
||||
// for domain light, set color of icon to light color if available
|
||||
if(newVal.domain == "light" && newVal.state == "on" &&
|
||||
newVal.attributes.brightness && newVal.attributes.xy_color) {
|
||||
|
||||
// for domain light, set color of icon to light color if available
|
||||
if(state.domain == "light" && state.state == "on" &&
|
||||
state.attributes.brightness && state.attributes.xy_color) {
|
||||
|
||||
var rgb = this.xyBriToRgb(state.attributes.xy_color[0],
|
||||
state.attributes.xy_color[1],
|
||||
state.attributes.brightness);
|
||||
this.$.icon.style.color = "rgb(" + rgb.map(Math.floor).join(",") + ")";
|
||||
} else {
|
||||
this.$.icon.style.color = null;
|
||||
}
|
||||
},
|
||||
|
||||
// from http://stackoverflow.com/questions/22894498/philips-hue-convert-xy-from-api-to-hex-or-rgb
|
||||
xyBriToRgb: function (x, y, bri) {
|
||||
z = 1.0 - x - y;
|
||||
Y = bri / 255.0; // Brightness of lamp
|
||||
X = (Y / y) * x;
|
||||
Z = (Y / y) * z;
|
||||
r = X * 1.612 - Y * 0.203 - Z * 0.302;
|
||||
g = -X * 0.509 + Y * 1.412 + Z * 0.066;
|
||||
b = X * 0.026 - Y * 0.072 + Z * 0.962;
|
||||
r = r <= 0.0031308 ? 12.92 * r : (1.0 + 0.055) * Math.pow(r, (1.0 / 2.4)) - 0.055;
|
||||
g = g <= 0.0031308 ? 12.92 * g : (1.0 + 0.055) * Math.pow(g, (1.0 / 2.4)) - 0.055;
|
||||
b = b <= 0.0031308 ? 12.92 * b : (1.0 + 0.055) * Math.pow(b, (1.0 / 2.4)) - 0.055;
|
||||
maxValue = Math.max(r,g,b);
|
||||
r /= maxValue;
|
||||
g /= maxValue;
|
||||
b /= maxValue;
|
||||
r = r * 255; if (r < 0) { r = 255 };
|
||||
g = g * 255; if (g < 0) { g = 255 };
|
||||
b = b * 255; if (b < 0) { b = 255 };
|
||||
return [r, g, b]
|
||||
var rgb = this.xyBriToRgb(newVal.attributes.xy_color[0],
|
||||
newVal.attributes.xy_color[1],
|
||||
newVal.attributes.brightness);
|
||||
this.$.icon.style.color = "rgb(" + rgb.map(Math.floor).join(",") + ")";
|
||||
} else {
|
||||
this.$.icon.style.color = null;
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
||||
// from http://stackoverflow.com/questions/22894498/philips-hue-convert-xy-from-api-to-hex-or-rgb
|
||||
xyBriToRgb: function (x, y, bri) {
|
||||
z = 1.0 - x - y;
|
||||
Y = bri / 255.0; // Brightness of lamp
|
||||
X = (Y / y) * x;
|
||||
Z = (Y / y) * z;
|
||||
r = X * 1.612 - Y * 0.203 - Z * 0.302;
|
||||
g = -X * 0.509 + Y * 1.412 + Z * 0.066;
|
||||
b = X * 0.026 - Y * 0.072 + Z * 0.962;
|
||||
r = r <= 0.0031308 ? 12.92 * r : (1.0 + 0.055) * Math.pow(r, (1.0 / 2.4)) - 0.055;
|
||||
g = g <= 0.0031308 ? 12.92 * g : (1.0 + 0.055) * Math.pow(g, (1.0 / 2.4)) - 0.055;
|
||||
b = b <= 0.0031308 ? 12.92 * b : (1.0 + 0.055) * Math.pow(b, (1.0 / 2.4)) - 0.055;
|
||||
maxValue = Math.max(r,g,b);
|
||||
r /= maxValue;
|
||||
g /= maxValue;
|
||||
b /= maxValue;
|
||||
r = r * 255; if (r < 0) { r = 255; }
|
||||
g = g * 255; if (g < 0) { g = 255; }
|
||||
b = b * 255; if (b < 0) { b = 255; }
|
||||
return [r, g, b];
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
|
@ -2,55 +2,79 @@
|
||||
|
||||
<link rel="import" href="../cards/state-card.html">
|
||||
|
||||
<polymer-element name="state-cards" attributes="states" noscript>
|
||||
<dom-module id="state-cards">
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media all and (min-width: 1020px) {
|
||||
.state-card {
|
||||
width: calc(50% - 44px);
|
||||
margin: 8px 0 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 1356px) {
|
||||
.state-card {
|
||||
width: calc(33% - 38px);
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 1706px) {
|
||||
.state-card {
|
||||
width: calc(25% - 42px);
|
||||
}
|
||||
}
|
||||
|
||||
.no-states-content {
|
||||
max-width: 500px;
|
||||
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: 0 16px 8px;
|
||||
margin: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
<div class='horizontal layout wrap'>
|
||||
|
||||
@media all and (min-width: 1020px) {
|
||||
.state-card {
|
||||
width: calc(50% - 44px);
|
||||
margin: 8px 0 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 1356px) {
|
||||
.state-card {
|
||||
width: calc(33% - 38px);
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 1706px) {
|
||||
.state-card {
|
||||
width: calc(25% - 42px);
|
||||
}
|
||||
}
|
||||
|
||||
.no-states-content {
|
||||
max-width: 500px;
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div horizontal layout wrap>
|
||||
|
||||
<template repeat="{{states as state}}">
|
||||
<state-card class="state-card" stateObj={{state}}></state-card>
|
||||
<template is='dom-repeat' items="{{states}}">
|
||||
<state-card class="state-card" state-obj="[[item]]"></state-card>
|
||||
</template>
|
||||
|
||||
<template if="{{states.length == 0}}">
|
||||
<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>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
</polymer-element>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
Polymer({
|
||||
is: 'state-cards',
|
||||
|
||||
properties: {
|
||||
states: {
|
||||
type: Array,
|
||||
value: [],
|
||||
},
|
||||
},
|
||||
|
||||
computeEmptyStates: function(states) {
|
||||
return states.length === 0;
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
@ -0,0 +1,196 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
Polymer({
|
||||
is: 'state-history-chart-line',
|
||||
|
||||
properties: {
|
||||
data: {
|
||||
type: Object,
|
||||
observer: 'dataChanged',
|
||||
},
|
||||
|
||||
unit: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
isSingleDevice: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
isAttached: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
observer: 'dataChanged',
|
||||
},
|
||||
},
|
||||
|
||||
created: function() {
|
||||
this.style.display = 'block';
|
||||
},
|
||||
|
||||
attached: function() {
|
||||
this.isAttached = true;
|
||||
},
|
||||
|
||||
dataChanged: function() {
|
||||
this.drawChart();
|
||||
},
|
||||
|
||||
/**************************************************
|
||||
The following code gererates line line graphs for devices with continuous
|
||||
values(which are devices that have a unit_of_measurement values defined).
|
||||
On each graph the devices are grouped by their unit of measurement, eg. all
|
||||
sensors measuring MB will be a separate line on single graph. The google
|
||||
chart API takes data as a 2 dimensional array in the format:
|
||||
|
||||
DateTime, device1, device2, device3
|
||||
2015-04-01, 1, 2, 0
|
||||
2015-04-01, 0, 1, 0
|
||||
2015-04-01, 2, 1, 1
|
||||
|
||||
NOTE: the first column is a javascript date objects.
|
||||
|
||||
The first thing we do is build up the data with rows for each time of a state
|
||||
change and initialise the values to 0. THen we loop through each device and
|
||||
fill in its data.
|
||||
|
||||
**************************************************/
|
||||
drawChart: function() {
|
||||
if (!this.isAttached) {
|
||||
return;
|
||||
}
|
||||
|
||||
var root = Polymer.dom(this);
|
||||
var unit = this.unit;
|
||||
var deviceStates = this.data;
|
||||
|
||||
while (root.lastChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
|
||||
if (deviceStates.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var chart = new google.visualization.LineChart(this);
|
||||
var dataTable = new google.visualization.DataTable();
|
||||
|
||||
dataTable.addColumn({ type: 'datetime', id: 'Time' });
|
||||
|
||||
var options = {
|
||||
legend: { position: 'top' },
|
||||
titlePosition: 'none',
|
||||
vAxes: {
|
||||
// Adds units to the left hand side of the graph
|
||||
0: {title: unit}
|
||||
},
|
||||
hAxis: {
|
||||
format: 'H:mm'
|
||||
},
|
||||
lineWidth: 1,
|
||||
chartArea:{left:'60',width:"95%"},
|
||||
explorer: {
|
||||
actions: ['dragToZoom', 'rightClickToReset', 'dragToPan'],
|
||||
keepInBounds: true,
|
||||
axis: 'horizontal',
|
||||
maxZoomIn: 0.1
|
||||
}
|
||||
};
|
||||
|
||||
if(this.isSingleDevice) {
|
||||
options.legend.position = 'none';
|
||||
options.vAxes[0].title = null;
|
||||
options.chartArea.left = 40;
|
||||
options.chartArea.height = '80%';
|
||||
options.chartArea.top = 5;
|
||||
options.enableInteractivity = false;
|
||||
}
|
||||
|
||||
// Get a unique list of times of state changes for all the device
|
||||
// for a particular unit of measureent.
|
||||
var times = _.pluck(_.flatten(deviceStates), "lastChangedAsDate");
|
||||
times = _.uniq(times, function(e) {
|
||||
return e.getTime();
|
||||
});
|
||||
|
||||
times = _.sortBy(times, function(o) { return o; });
|
||||
|
||||
var data = [];
|
||||
var empty = new Array(deviceStates.length);
|
||||
for(var i = 0; i < empty.length; i++) {
|
||||
empty[i] = 0;
|
||||
}
|
||||
|
||||
var timeIndex = 1;
|
||||
var endDate = new Date();
|
||||
var prevDate = times[0];
|
||||
|
||||
for(i = 0; i < times.length; i++) {
|
||||
var currentDate = new Date(prevDate);
|
||||
|
||||
// because we only have state changes we add an extra point at the same time
|
||||
// that holds the previous state which makes the line display correctly
|
||||
var beforePoint = new Date(times[i]);
|
||||
data.push([beforePoint].concat(empty));
|
||||
|
||||
data.push([times[i]].concat(empty));
|
||||
prevDate = times[i];
|
||||
timeIndex++;
|
||||
}
|
||||
data.push([endDate].concat(empty));
|
||||
|
||||
|
||||
var deviceCount = 0;
|
||||
deviceStates.forEach(function(device) {
|
||||
var attributes = device[device.length - 1].attributes;
|
||||
dataTable.addColumn('number', attributes.friendly_name);
|
||||
|
||||
var currentState = 0;
|
||||
var previousState = 0;
|
||||
var lastIndex = 0;
|
||||
var count = 0;
|
||||
var prevTime = data[0][0];
|
||||
device.forEach(function(state) {
|
||||
|
||||
currentState = state.state;
|
||||
var start = state.lastChangedAsDate;
|
||||
if(state.state == 'None') {
|
||||
currentState = previousState;
|
||||
}
|
||||
for(var i = lastIndex; i < data.length; i++) {
|
||||
data[i][1 + deviceCount] = parseFloat(previousState);
|
||||
// this is where data gets filled in for each time for the particular device
|
||||
// because for each time two entries were create we fill the first one with the
|
||||
// previous value and the second one with the new value
|
||||
if(prevTime.getTime() == data[i][0].getTime() && data[i][0].getTime() == start.getTime()) {
|
||||
data[i][1 + deviceCount] = parseFloat(currentState);
|
||||
lastIndex = i;
|
||||
prevTime = data[i][0];
|
||||
break;
|
||||
}
|
||||
prevTime = data[i][0];
|
||||
}
|
||||
|
||||
previousState = currentState;
|
||||
|
||||
count++;
|
||||
}.bind(this));
|
||||
|
||||
//fill in the rest of the Array
|
||||
for(var i = lastIndex; i < data.length; i++) {
|
||||
data[i][1 + deviceCount] = parseFloat(previousState);
|
||||
}
|
||||
|
||||
deviceCount++;
|
||||
}.bind(this));
|
||||
|
||||
dataTable.addRows(data);
|
||||
chart.draw(dataTable, options);
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
||||
</script>
|
@ -0,0 +1,115 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
Polymer({
|
||||
is: 'state-history-chart-timeline',
|
||||
|
||||
properties: {
|
||||
data: {
|
||||
type: Object,
|
||||
observer: 'dataChanged',
|
||||
},
|
||||
|
||||
isAttached: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
observer: 'dataChanged',
|
||||
},
|
||||
},
|
||||
|
||||
created: function() {
|
||||
this.style.display = 'block';
|
||||
},
|
||||
|
||||
attached: function() {
|
||||
this.isAttached = true;
|
||||
},
|
||||
|
||||
dataChanged: function() {
|
||||
this.drawChart();
|
||||
},
|
||||
|
||||
drawChart: function() {
|
||||
if (!this.isAttached) {
|
||||
return;
|
||||
}
|
||||
|
||||
var root = Polymer.dom(this);
|
||||
var stateHistory = this.data;
|
||||
|
||||
while (root.lastChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
|
||||
if (!stateHistory || stateHistory.length === 0) {
|
||||
return;
|
||||
}
|
||||
// debugger;
|
||||
var chart = new google.visualization.Timeline(this);
|
||||
var dataTable = new google.visualization.DataTable();
|
||||
|
||||
dataTable.addColumn({ type: 'string', id: 'Entity' });
|
||||
dataTable.addColumn({ type: 'string', id: 'State' });
|
||||
dataTable.addColumn({ type: 'date', id: 'Start' });
|
||||
dataTable.addColumn({ type: 'date', id: 'End' });
|
||||
|
||||
var addRow = function(entityDisplay, stateStr, start, end) {
|
||||
stateStr = stateStr.replace(/_/g, ' ');
|
||||
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 numTimelines = 0;
|
||||
// stateHistory is a list of lists of sorted state objects
|
||||
stateHistory.forEach(function(stateInfo) {
|
||||
if(stateInfo.length === 0) return;
|
||||
|
||||
var entityDisplay = stateInfo[0].entityDisplay;
|
||||
var newLastChanged, prevState = null, prevLastChanged = null;
|
||||
|
||||
stateInfo.forEach(function(state) {
|
||||
if (prevState !== null && state.state !== prevState) {
|
||||
newLastChanged = state.lastChangedAsDate;
|
||||
|
||||
addRow(entityDisplay, prevState, prevLastChanged, newLastChanged);
|
||||
|
||||
prevState = state.state;
|
||||
prevLastChanged = newLastChanged;
|
||||
} else if (prevState === null) {
|
||||
prevState = state.state;
|
||||
prevLastChanged = state.lastChangedAsDate;
|
||||
}
|
||||
});
|
||||
|
||||
addRow(entityDisplay, prevState, prevLastChanged, new Date());
|
||||
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
|
||||
},
|
||||
|
||||
hAxis: {
|
||||
format: 'H:mm'
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
@ -0,0 +1,145 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../bower_components/google-apis/google-legacy-loader.html">
|
||||
|
||||
<link rel="import" href="./loading-box.html">
|
||||
<link rel="import" href="./state-history-chart-timeline.html">
|
||||
<link rel="import" href="./state-history-chart-line.html">
|
||||
|
||||
<dom-module id="state-history-charts">
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
}
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
Polymer({
|
||||
is: 'state-history-charts',
|
||||
|
||||
properties: {
|
||||
stateHistory: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
isLoadingData: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
apiLoaded: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
computed: 'computeIsLoading(isLoadingData, apiLoaded)',
|
||||
},
|
||||
|
||||
groupedStateHistory: {
|
||||
type: Object,
|
||||
computed: 'computeGroupedStateHistory(stateHistory)',
|
||||
},
|
||||
|
||||
isSingleDevice: {
|
||||
type: Boolean,
|
||||
computed: 'computeIsSingleDevice(stateHistory)',
|
||||
},
|
||||
},
|
||||
|
||||
computeIsSingleDevice: function(stateHistory) {
|
||||
return stateHistory && stateHistory.length == 1;
|
||||
},
|
||||
|
||||
computeGroupedStateHistory: function(stateHistory) {
|
||||
var lineChartDevices = {};
|
||||
var timelineDevices = [];
|
||||
|
||||
if (!stateHistory) {
|
||||
return {line: unitStates, timeline: timelineDevices};
|
||||
}
|
||||
|
||||
stateHistory.forEach(function(stateInfo) {
|
||||
if (!stateInfo || stateInfo.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var unit;
|
||||
|
||||
for (var i = 0; i < stateInfo.length && !unit; i++) {
|
||||
unit = stateInfo[i].attributes.unit_of_measurement;
|
||||
}
|
||||
|
||||
if (unit) {
|
||||
if (!(unit in lineChartDevices)) {
|
||||
lineChartDevices[unit] = [stateInfo];
|
||||
} else {
|
||||
lineChartDevices[unit].push(stateInfo);
|
||||
}
|
||||
} else {
|
||||
timelineDevices.push(stateInfo);
|
||||
}
|
||||
});
|
||||
|
||||
timelineDevices = timelineDevices.length > 0 && timelineDevices;
|
||||
|
||||
var unitStates = Object.keys(lineChartDevices).map(function(unit) {
|
||||
return [unit, lineChartDevices[unit]]; });
|
||||
|
||||
return {line: unitStates, timeline: timelineDevices};
|
||||
},
|
||||
|
||||
googleApiLoaded: function() {
|
||||
google.load("visualization", "1", {
|
||||
packages: ["timeline", "corechart"],
|
||||
callback: function() {
|
||||
this.apiLoaded = true;
|
||||
}.bind(this)
|
||||
});
|
||||
},
|
||||
|
||||
computeIsLoading: function(isLoadingData, apiLoaded) {
|
||||
return isLoadingData || !apiLoaded;
|
||||
},
|
||||
|
||||
extractUnit: function(arr) {
|
||||
return arr[0];
|
||||
},
|
||||
|
||||
extractData: function(arr) {
|
||||
return arr[1];
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
@ -1,48 +1,75 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/core-tooltip/core-tooltip.html">
|
||||
<link rel="import" href="../bower_components/core-style/core-style.html">
|
||||
<!-- <link rel="import" href="../bower_components/core-tooltip/core-tooltip.html"> -->
|
||||
|
||||
<link rel="import" href="state-badge.html">
|
||||
<link rel="import" href="relative-ha-datetime.html">
|
||||
|
||||
<polymer-element name="state-info" attributes="stateObj" noscript>
|
||||
<dom-module id="state-info">
|
||||
<style>
|
||||
:host {
|
||||
line-height: normal;
|
||||
min-width: 150px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
state-badge {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-left: 60px;
|
||||
}
|
||||
|
||||
.name {
|
||||
text-transform: capitalize;
|
||||
font-weight: 300;
|
||||
font-size: 1.3rem;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.time-ago {
|
||||
color: darkgrey;
|
||||
margin-top: -2px;
|
||||
font-size: 1rem;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<style>
|
||||
state-badge {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.name {
|
||||
text-transform: capitalize;
|
||||
font-weight: 300;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-left: 60px;
|
||||
}
|
||||
|
||||
.time-ago {
|
||||
color: darkgrey;
|
||||
margin-top: -2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>
|
||||
<state-badge stateObj="{{stateObj}}"></state-badge>
|
||||
<state-badge state-obj='[[stateObj]]'></state-badge>
|
||||
|
||||
<div class='info'>
|
||||
<div class='name'>
|
||||
{{stateObj.entityDisplay}}
|
||||
<div class='name'>[[stateObj.entityDisplay]]</div>
|
||||
|
||||
<div class='time-ago'>
|
||||
<!-- <core-tooltip label="[[computeTooltipLabel(stateObj)]]" position="bottom"> -->
|
||||
<relative-ha-datetime datetime-obj='[[stateObj.lastChangedAsDate]]'></relative-ha-datetime>
|
||||
<!-- </core-tooltip> -->
|
||||
</div>
|
||||
|
||||
<div class="time-ago">
|
||||
<core-tooltip label="{{stateObj.lastChangedAsDate | formatDateTime}}" position="bottom">
|
||||
<relative-ha-datetime datetimeObj="{{stateObj.lastChangedAsDate}}"></relative-ha-datetime>
|
||||
</core-tooltip>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</polymer-element>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
Polymer({
|
||||
is: 'state-info',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
|
||||
computeTooltipLabel: function(stateObj) {
|
||||
// stateObj.lastChangedAsDate | formatDateTime
|
||||
return 'Label TODO';
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
@ -1,321 +0,0 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../bower_components/google-apis/google-jsapi.html">
|
||||
|
||||
<polymer-element name="state-timeline" attributes="stateHistory isLoadingData">
|
||||
<template>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#loadingbox {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loadingmessage {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.singlelinechart {
|
||||
min-height:140px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div style='width: 100%; height: auto;' hidden?="{{!isLoading}}" >
|
||||
<div layout horizontal center id="splash">
|
||||
<div layout vertical center flex>
|
||||
<div id="loadingbox">
|
||||
<paper-spinner active="true"></paper-spinner><br />
|
||||
<div class="loadingmessage">{{spinnerMessage}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<google-jsapi on-api-load="{{googleApiLoaded}}"></google-jsapi>
|
||||
<div id="timeline" style='width: 100%; height: auto;' class="{{ {singlelinechart: isSingleDevice && hasLineChart } | tokenList}}" hidden?="{{isLoadingData}}"></div>
|
||||
<div id="line_graphs" style='width: 100%; height: auto;' hidden?="{{isLoadingData}}"></div>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
apiLoaded: false,
|
||||
stateHistory: null,
|
||||
isLoading: true,
|
||||
isLoadingData: false,
|
||||
spinnerMessage: "Loading history data...",
|
||||
isSingleDevice: false,
|
||||
hasLineChart: false,
|
||||
|
||||
googleApiLoaded: function() {
|
||||
google.load("visualization", "1", {
|
||||
packages: ["timeline", "corechart"],
|
||||
callback: function() {
|
||||
this.apiLoaded = true;
|
||||
this.drawChart();
|
||||
}.bind(this)
|
||||
});
|
||||
},
|
||||
|
||||
stateHistoryChanged: function() {
|
||||
this.drawChart();
|
||||
},
|
||||
|
||||
isLoadingDataChanged: function() {
|
||||
if(this.isLoadingData) {
|
||||
isLoading = true;
|
||||
}
|
||||
},
|
||||
|
||||
drawChart: function() {
|
||||
if (!this.apiLoaded || !this.stateHistory) {
|
||||
return;
|
||||
}
|
||||
this.isLoading = true;
|
||||
|
||||
var container = this.$.timeline;
|
||||
var chart = new google.visualization.Timeline(container);
|
||||
var dataTable = new google.visualization.DataTable();
|
||||
|
||||
dataTable.addColumn({ type: 'string', id: 'Entity' });
|
||||
dataTable.addColumn({ type: 'string', id: 'State' });
|
||||
dataTable.addColumn({ type: 'date', id: 'Start' });
|
||||
dataTable.addColumn({ type: 'date', id: 'End' });
|
||||
|
||||
var addRow = function(entityDisplay, stateStr, start, end) {
|
||||
stateStr = stateStr.replace(/_/g, ' ');
|
||||
dataTable.addRow([entityDisplay, stateStr, start, end]);
|
||||
};
|
||||
|
||||
if (this.stateHistory.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this.hasLineChart = false;
|
||||
this.isSingleDevice = false;
|
||||
|
||||
// people can pass in history of 1 entityId or a collection.
|
||||
var stateHistory;
|
||||
if (_.isArray(this.stateHistory[0])) {
|
||||
stateHistory = this.stateHistory;
|
||||
} else {
|
||||
stateHistory = [this.stateHistory];
|
||||
this.isSingleDevice = true;
|
||||
}
|
||||
|
||||
var lineChartDevices = {};
|
||||
var numTimelines = 0;
|
||||
// stateHistory is a list of lists of sorted state objects
|
||||
stateHistory.forEach(function(stateInfo) {
|
||||
if(stateInfo.length === 0) return;
|
||||
|
||||
var entityDisplay = stateInfo[0].entityDisplay;
|
||||
var newLastChanged, prevState = null, prevLastChanged = null;
|
||||
//get the latest update to get the graph type from the component attributes
|
||||
var attributes = stateInfo[stateInfo.length - 1].attributes;
|
||||
|
||||
//if the device has a unit of meaurment it will be added as a line graph further down
|
||||
if(attributes.unit_of_measurement) {
|
||||
if(!lineChartDevices[attributes.unit_of_measurement]){
|
||||
lineChartDevices[attributes.unit_of_measurement] = [];
|
||||
}
|
||||
lineChartDevices[attributes.unit_of_measurement].push(stateInfo);
|
||||
this.hasLineChart = true;
|
||||
return;
|
||||
}
|
||||
|
||||
stateInfo.forEach(function(state) {
|
||||
if (prevState !== null && state.state !== prevState) {
|
||||
newLastChanged = state.lastChangedAsDate;
|
||||
|
||||
addRow(entityDisplay, prevState, prevLastChanged, newLastChanged);
|
||||
|
||||
prevState = state.state;
|
||||
prevLastChanged = newLastChanged;
|
||||
} else if (prevState === null) {
|
||||
prevState = state.state;
|
||||
prevLastChanged = state.lastChangedAsDate;
|
||||
}
|
||||
});
|
||||
|
||||
addRow(entityDisplay, prevState, prevLastChanged, new Date());
|
||||
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
|
||||
},
|
||||
|
||||
hAxis: {
|
||||
format: 'H:mm'
|
||||
},
|
||||
});
|
||||
|
||||
/**************************************************
|
||||
The following code gererates line line graphs for devices with continuous
|
||||
values(which are devices that have a unit_of_measurment values defined).
|
||||
On each graph the devices are grouped by their unit of measurement, eg. all
|
||||
sensors measuring MB will be a separate line on single graph. The google
|
||||
chart API takes data as a 2 dimensional array in the format:
|
||||
|
||||
DateTime, device1, device2, device3
|
||||
2015-04-01, 1, 2, 0
|
||||
2015-04-01, 0, 1, 0
|
||||
2015-04-01, 2, 1, 1
|
||||
|
||||
NOTE: the first column is a javascript date objects.
|
||||
|
||||
The first thing we do is build up the data with rows for each time of a state
|
||||
change and initialise the values to 0. THen we loop through each device and
|
||||
fill in its data.
|
||||
|
||||
**************************************************/
|
||||
|
||||
|
||||
while (this.$.line_graphs.firstChild) {
|
||||
this.$.line_graphs.removeChild(this.$.line_graphs.firstChild);
|
||||
}
|
||||
|
||||
for (var key in lineChartDevices) {
|
||||
var deviceStates = lineChartDevices[key];
|
||||
|
||||
if(this.isSingleDevice) {
|
||||
container = this.$.timeline;
|
||||
}
|
||||
else {
|
||||
container = document.createElement("DIV");
|
||||
this.$.line_graphs.appendChild(container);
|
||||
}
|
||||
|
||||
|
||||
var chart = new google.visualization.LineChart(container);
|
||||
|
||||
|
||||
var dataTable = new google.visualization.DataTable();
|
||||
dataTable.addColumn({ type: 'datetime', id: 'Time' });
|
||||
|
||||
var options = {
|
||||
legend: { position: 'top' },
|
||||
titlePosition: 'none',
|
||||
vAxes: {
|
||||
// Adds units to the left hand side of the graph
|
||||
0: {title: key}
|
||||
},
|
||||
hAxis: {
|
||||
format: 'H:mm'
|
||||
},
|
||||
lineWidth: 1,
|
||||
chartArea:{left:'60',width:"95%"},
|
||||
explorer: {
|
||||
actions: ['dragToZoom', 'rightClickToReset', 'dragToPan'],
|
||||
keepInBounds: true,
|
||||
axis: 'horizontal',
|
||||
maxZoomIn: 0.1
|
||||
}
|
||||
};
|
||||
|
||||
if(this.isSingleDevice) {
|
||||
options.legend.position = 'none';
|
||||
options.vAxes[0].title = null;
|
||||
options.chartArea.left = 40;
|
||||
options.chartArea.height = '80%';
|
||||
options.chartArea.top = 5;
|
||||
options.enableInteractivity = false;
|
||||
}
|
||||
|
||||
// Get a unique list of times of state changes for all the device
|
||||
// for a particular unit of measureent.
|
||||
var times = _.pluck(_.flatten(deviceStates), "lastChangedAsDate");
|
||||
times = _.uniq(times, function(e) {
|
||||
return e.getTime();
|
||||
});
|
||||
|
||||
times = _.sortBy(times, function(o) { return o; });
|
||||
|
||||
var data = [];
|
||||
var empty = new Array(deviceStates.length);
|
||||
for(var i = 0; i < empty.length; i++) {
|
||||
empty[i] = 0;
|
||||
}
|
||||
|
||||
var timeIndex = 1;
|
||||
var endDate = new Date();
|
||||
var prevDate = times[0];
|
||||
|
||||
for(var i = 0; i < times.length; i++) {
|
||||
var currentDate = new Date(prevDate);
|
||||
|
||||
// because we only have state changes we add an extra point at the same time
|
||||
// that holds the previous state which makes the line display correctly
|
||||
var beforePoint = new Date(times[i]);
|
||||
data.push([beforePoint].concat(empty));
|
||||
|
||||
data.push([times[i]].concat(empty));
|
||||
prevDate = times[i];
|
||||
timeIndex++;
|
||||
}
|
||||
data.push([endDate].concat(empty));
|
||||
|
||||
|
||||
var deviceCount = 0;
|
||||
deviceStates.forEach(function(device) {
|
||||
var attributes = device[device.length - 1].attributes;
|
||||
dataTable.addColumn('number', attributes.friendly_name);
|
||||
|
||||
var currentState = 0;
|
||||
var previousState = 0;
|
||||
var lastIndex = 0;
|
||||
var count = 0;
|
||||
var prevTime = data[0][0];
|
||||
device.forEach(function(state) {
|
||||
|
||||
currentState = state.state;
|
||||
var start = state.lastChangedAsDate;
|
||||
if(state.state == 'None') {
|
||||
currentState = previousState;
|
||||
}
|
||||
for(var i = lastIndex; i < data.length; i++) {
|
||||
data[i][1 + deviceCount] = parseFloat(previousState);
|
||||
// this is where data gets filled in for each time for the particular device
|
||||
// because for each time two entires were create we fill the first one with the
|
||||
// previous value and the second one with the new value
|
||||
if(prevTime.getTime() == data[i][0].getTime() && data[i][0].getTime() == start.getTime()) {
|
||||
data[i][1 + deviceCount] = parseFloat(currentState);
|
||||
lastIndex = i;
|
||||
prevTime = data[i][0];
|
||||
break;
|
||||
}
|
||||
prevTime = data[i][0];
|
||||
}
|
||||
|
||||
previousState = currentState;
|
||||
|
||||
count++;
|
||||
}.bind(this));
|
||||
|
||||
//fill in the rest of the Array
|
||||
for(var i = lastIndex; i < data.length; i++) {
|
||||
data[i][1 + deviceCount] = parseFloat(previousState);
|
||||
}
|
||||
|
||||
deviceCount++;
|
||||
}.bind(this));
|
||||
|
||||
dataTable.addRows(data);
|
||||
chart.draw(dataTable, options);
|
||||
}
|
||||
this.isLoading = (!this.isLoadingData) ? false : true;
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -1,11 +1,11 @@
|
||||
<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/core-style/core-style.html">
|
||||
|
||||
<link rel="import" href="../bower_components/core-icons/notification-icons.html">
|
||||
<link rel="import" href="../bower_components/iron-icon/iron-icon.html">
|
||||
<link rel="import" href="../bower_components/paper-toggle-button/paper-toggle-button.html">
|
||||
|
||||
<polymer-element name="stream-status">
|
||||
<template>
|
||||
<link rel="import" href="../bower_components/iron-icons/notification-icons.html">
|
||||
|
||||
<dom-module id="stream-status">
|
||||
<style>
|
||||
:host {
|
||||
display: inline-block;
|
||||
@ -16,29 +16,31 @@
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
<core-style ref='ha-paper-toggle'></core-style>
|
||||
<template>
|
||||
<iron-icon icon="warning" hidden$="{{!hasError}}"></iron-icon>
|
||||
<paper-toggle-button id="toggle" on-change='toggleChanged' hidden$="{{hasError}}"></paper-toggle-button>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<core-icon icon="warning" hidden?="{{!hasError}}"></core-icon>
|
||||
<paper-toggle-button id="toggle" on-change={{toggleChanged}} hidden?="{{hasError}}"></paper-toggle-button>
|
||||
</template>
|
||||
<script>
|
||||
var streamActions = window.hass.streamActions;
|
||||
var authStore = window.hass.authStore;
|
||||
var storeListenerMixIn = window.hass.storeListenerMixIn;
|
||||
|
||||
Polymer(Polymer.mixin({
|
||||
isStreaming: false,
|
||||
hasError: false,
|
||||
Polymer({
|
||||
is: 'stream-status',
|
||||
|
||||
icon: "swap-vert-circle",
|
||||
color: 'red',
|
||||
behaviors: [StoreListenerBehavior],
|
||||
|
||||
attached: function() {
|
||||
this.listenToStores(true);
|
||||
},
|
||||
properties: {
|
||||
isStreaming: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
detached: function() {
|
||||
this.stopListeningToStores();
|
||||
hasError: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
|
||||
streamStoreChanged: function(streamStore) {
|
||||
@ -53,6 +55,5 @@
|
||||
streamActions.start(authStore.authToken);
|
||||
}
|
||||
},
|
||||
}, storeListenerMixIn));
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
||||
|
@ -1,18 +0,0 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/core-style/core-style.html">
|
||||
<link rel="import" href="../bower_components/paper-dialog/paper-dialog.html">
|
||||
<link rel="import" href="../bower_components/paper-dialog/paper-dialog-transition.html">
|
||||
|
||||
<polymer-element name="ha-dialog" extends="paper-dialog">
|
||||
<template>
|
||||
<core-style ref='ha-dialog'></core-style>
|
||||
<shadow></shadow>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
layered: true,
|
||||
backdrop: true,
|
||||
transition: 'core-transition-bottom',
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
@ -1,120 +1,152 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="ha-dialog.html">
|
||||
<link rel="import" href="../bower_components/paper-dialog/paper-dialog.html">
|
||||
<link rel="import" href="../bower_components/paper-dialog-scrollable/paper-dialog-scrollable.html">
|
||||
<!-- <link rel="import" href="../bower_components/neon-animation/animations/slide-up-animation.html">
|
||||
<link rel="import" href="../bower_components/neon-animation/animations/slide-down-animation.html">
|
||||
-->
|
||||
<link rel="import" href="../cards/state-card-content.html">
|
||||
<link rel="import" href="../components/state-timeline.html">
|
||||
<link rel="import" href="../components/state-history-charts.html">
|
||||
<link rel="import" href="../more-infos/more-info-content.html">
|
||||
|
||||
<polymer-element name="more-info-dialog">
|
||||
<template>
|
||||
<ha-dialog id="dialog" on-core-overlay-open="{{dialogOpenChanged}}">
|
||||
<div>
|
||||
<state-card-content stateObj="{{stateObj}}" style='margin-bottom: 24px;'>
|
||||
</state-card-content>
|
||||
<template if="{{hasHistoryComponent}}">
|
||||
<state-timeline stateHistory="{{stateHistory}}" isLoadingData="{{isLoadingHistoryData}}"></state-timeline>
|
||||
</template>
|
||||
<more-info-content
|
||||
stateObj="{{stateObj}}"
|
||||
dialogOpen="{{dialogOpen}}"></more-info-content>
|
||||
</div>
|
||||
</ha-dialog>
|
||||
</template>
|
||||
<dom-module id="more-info-dialog">
|
||||
<style>
|
||||
state-card-content {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px) {
|
||||
paper-dialog {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
max-height: calc(100% - 64px);
|
||||
|
||||
position: fixed !important;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
overflow: scroll;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<!-- entry-animation='slide-up-animation' exit-animation='slide-down-animation' -->
|
||||
<paper-dialog id="dialog" with-backdrop>
|
||||
<h2><state-card-content state-obj="[[stateObj]]"></state-card-content></h2>
|
||||
<div>
|
||||
<template is='dom-if' if="[[hasHistoryComponent]]">
|
||||
<state-history-charts state-history="[[stateHistory]]"
|
||||
is-loading-data="[[isLoadingHistoryData]]"></state-history-charts>
|
||||
</template>
|
||||
<paper-dialog-scrollable>
|
||||
<more-info-content state-obj="[[stateObj]]"
|
||||
dialog-open="[[dialogOpen]]"></more-info-content>
|
||||
</paper-dialog-scrollable>
|
||||
</div>
|
||||
</paper-dialog>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
var storeListenerMixIn = window.hass.storeListenerMixIn;
|
||||
var stateStore = window.hass.stateStore;
|
||||
var stateHistoryStore = window.hass.stateHistoryStore;
|
||||
var stateHistoryActions = window.hass.stateHistoryActions;
|
||||
(function() {
|
||||
var stateStore = window.hass.stateStore;
|
||||
var stateHistoryStore = window.hass.stateHistoryStore;
|
||||
var stateHistoryActions = window.hass.stateHistoryActions;
|
||||
|
||||
Polymer(Polymer.mixin({
|
||||
entityId: false,
|
||||
stateObj: null,
|
||||
stateHistory: null,
|
||||
hasHistoryComponent: false,
|
||||
dialogOpen: false,
|
||||
isLoadingHistoryData: false,
|
||||
Polymer({
|
||||
is: 'more-info-dialog',
|
||||
|
||||
observe: {
|
||||
'stateObj.attributes': 'reposition'
|
||||
},
|
||||
behaviors: [StoreListenerBehavior],
|
||||
|
||||
created: function() {
|
||||
this.dialogOpenChanged = this.dialogOpenChanged.bind(this);
|
||||
},
|
||||
properties: {
|
||||
entityId: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
attached: function() {
|
||||
this.listenToStores(true);
|
||||
},
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
detached: function() {
|
||||
this.stopListeningToStores();
|
||||
},
|
||||
stateHistory: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
componentStoreChanged: function(componentStore) {
|
||||
this.hasHistoryComponent = componentStore.isLoaded('history');
|
||||
},
|
||||
isLoadingHistoryData: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
stateStoreChanged: function() {
|
||||
var newState = this.entityId ? stateStore.get(this.entityId) : null;
|
||||
hasHistoryComponent: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
if (newState !== this.stateObj) {
|
||||
this.stateObj = newState;
|
||||
}
|
||||
},
|
||||
dialogOpen: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
|
||||
stateHistoryStoreChanged: function() {
|
||||
var newHistory;
|
||||
listeners: {
|
||||
'iron-overlay-opened': 'onIronOverlayOpened',
|
||||
'iron-overlay-closed': 'onIronOverlayClosed'
|
||||
},
|
||||
|
||||
if (this.hasHistoryComponent && this.entityId) {
|
||||
newHistory = stateHistoryStore.get(this.entityId);
|
||||
} else {
|
||||
newHistory = null;
|
||||
}
|
||||
this.isLoadingHistoryData = false;
|
||||
if (newHistory !== this.stateHistory) {
|
||||
this.stateHistory = newHistory;
|
||||
}
|
||||
},
|
||||
componentStoreChanged: function(componentStore) {
|
||||
this.hasHistoryComponent = componentStore.isLoaded('history');
|
||||
},
|
||||
|
||||
dialogOpenChanged: function(ev) {
|
||||
// we get CustomEvent, undefined and true/false from polymer…
|
||||
if (typeof ev === 'object') {
|
||||
this.dialogOpen = ev.detail;
|
||||
}
|
||||
},
|
||||
stateStoreChanged: function() {
|
||||
var newState = this.entityId ? stateStore.get(this.entityId) : null;
|
||||
|
||||
changeEntityId: function(entityId) {
|
||||
this.entityId = entityId;
|
||||
if (newState !== this.stateObj) {
|
||||
this.stateObj = newState;
|
||||
}
|
||||
},
|
||||
|
||||
this.stateStoreChanged();
|
||||
this.stateHistoryStoreChanged();
|
||||
stateHistoryStoreChanged: function() {
|
||||
var newHistory;
|
||||
|
||||
if (this.hasHistoryComponent && stateHistoryStore.isStale(entityId)) {
|
||||
this.isLoadingHistoryData = true;
|
||||
stateHistoryActions.fetch(entityId);
|
||||
}
|
||||
},
|
||||
if (this.hasHistoryComponent && this.entityId) {
|
||||
newHistory = [stateHistoryStore.get(this.entityId)];
|
||||
} else {
|
||||
newHistory = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whenever the attributes change, the more info component can
|
||||
* hide or show elements. We will reposition the dialog.
|
||||
*/
|
||||
reposition: function(oldVal, newVal) {
|
||||
// Only resize if already open
|
||||
if(this.$.dialog.opened) {
|
||||
this.job('resizeAfterLayoutChange', function() {
|
||||
this.$.dialog.resizeHandler();
|
||||
}.bind(this), 1000);
|
||||
}
|
||||
},
|
||||
this.isLoadingHistoryData = false;
|
||||
|
||||
show: function(entityId) {
|
||||
this.changeEntityId(entityId);
|
||||
if (newHistory !== this.stateHistory) {
|
||||
this.stateHistory = newHistory;
|
||||
}
|
||||
},
|
||||
|
||||
this.job('showDialogAfterRender', function() {
|
||||
this.$.dialog.toggle();
|
||||
}.bind(this));
|
||||
},
|
||||
}, storeListenerMixIn));
|
||||
onIronOverlayOpened: function() {
|
||||
this.dialogOpen = true;
|
||||
},
|
||||
|
||||
onIronOverlayClosed: function() {
|
||||
this.dialogOpen = false;
|
||||
},
|
||||
|
||||
changeEntityId: function(entityId) {
|
||||
this.entityId = entityId;
|
||||
|
||||
this.stateStoreChanged();
|
||||
this.stateHistoryStoreChanged();
|
||||
|
||||
if (this.hasHistoryComponent && stateHistoryStore.isStale(entityId)) {
|
||||
this.isLoadingHistoryData = true;
|
||||
stateHistoryActions.fetch(entityId);
|
||||
}
|
||||
},
|
||||
|
||||
show: function(entityId) {
|
||||
this.changeEntityId(entityId);
|
||||
|
||||
this.debounce('showDialogAfterRender', function() {
|
||||
this.$.dialog.toggle();
|
||||
}.bind(this));
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</polymer-element>
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 69ee1c49af12caf00655c66d56474b5c1bcac1c1
|
||||
Subproject commit 015edf9c28a63122aa8f6bc153f0c0ddfaad1caa
|
@ -1,41 +1,62 @@
|
||||
<link rel="import" href="bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="bower_components/font-roboto/roboto.html">
|
||||
<link rel='import' href='bower_components/polymer/polymer.html'>
|
||||
|
||||
<link rel="import" href="resources/home-assistant-style.html">
|
||||
<link rel="import" href="resources/home-assistant-js.html">
|
||||
<link rel='import' href='bower_components/paper-styles/typography.html'>
|
||||
|
||||
<link rel="import" href="layouts/login-form.html">
|
||||
<link rel="import" href="layouts/home-assistant-main.html">
|
||||
<link rel='import' href='resources/home-assistant-js.html'>
|
||||
<link rel='import' href='resources/home-assistant-icons.html'>
|
||||
<link rel='import' href='resources/store-listener-behavior.html'>
|
||||
|
||||
<link rel='import' href='layouts/login-form.html'>
|
||||
<link rel='import' href='layouts/home-assistant-main.html'>
|
||||
|
||||
<link rel='import' href='resources/home-assistant-style.html'>
|
||||
|
||||
<dom-module id='home-assistant'>
|
||||
<style>
|
||||
:host {
|
||||
font-family: 'Roboto', 'Noto', sans-serif;
|
||||
font-weight: 300;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
</style>
|
||||
|
||||
<home-assistant-icons></home-assistant-icons>
|
||||
|
||||
<polymer-element name="home-assistant" attributes="auth">
|
||||
<template>
|
||||
<style>
|
||||
|
||||
:host {
|
||||
font-family: RobotoDraft, 'Helvetica Neue', Helvetica, Arial;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<home-assistant-api auth="{{auth}}"></home-assistant-api>
|
||||
|
||||
<template if="{{!loaded}}">
|
||||
<template is='dom-if' if='[[!loaded]]'>
|
||||
<login-form></login-form>
|
||||
</template>
|
||||
|
||||
<template if="{{loaded}}">
|
||||
<template is='dom-if' if='[[loaded]]'>
|
||||
<home-assistant-main></home-assistant-main>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
|
||||
var storeListenerMixIn = window.hass.storeListenerMixIn,
|
||||
uiActions = window.hass.uiActions,
|
||||
preferenceStore = window.hass.preferenceStore;
|
||||
|
||||
Polymer(Polymer.mixin({
|
||||
loaded: false,
|
||||
Polymer({
|
||||
is: 'home-assistant',
|
||||
|
||||
hostAttributes: {
|
||||
auth: null,
|
||||
},
|
||||
|
||||
behaviors: [StoreListenerBehavior],
|
||||
|
||||
properties: {
|
||||
loaded: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
|
||||
ready: function() {
|
||||
// remove the HTML init message
|
||||
@ -43,23 +64,16 @@
|
||||
|
||||
// if auth was given, tell the backend
|
||||
if(this.auth) {
|
||||
uiActions.validateAuth(this.auth, false);
|
||||
uiActions.validateAuth(this.auth, false);
|
||||
} else if (preferenceStore.hasAuthToken) {
|
||||
uiActions.validateAuth(preferenceStore.authToken, false);
|
||||
uiActions.validateAuth(preferenceStore.authToken, false);
|
||||
}
|
||||
},
|
||||
|
||||
attached: function() {
|
||||
this.listenToStores(true);
|
||||
},
|
||||
|
||||
detached: function() {
|
||||
this.stopListeningToStores();
|
||||
},
|
||||
|
||||
syncStoreChanged: function(syncStore) {
|
||||
this.loaded = syncStore.initialLoadDone;
|
||||
},
|
||||
}, storeListenerMixIn));
|
||||
</script>
|
||||
</polymer-element>
|
||||
});
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
@ -0,0 +1,11 @@
|
||||
{
|
||||
"removeComments": true,
|
||||
"removeCommentsFromCDATA": true,
|
||||
"removeCDATASectionsFromCDATA": true,
|
||||
"collapseWhitespace": true,
|
||||
"collapseBooleanAttributes": true,
|
||||
"removeScriptTypeAttributes": true,
|
||||
"removeStyleLinkTypeAttributes": true,
|
||||
"minifyJS": true,
|
||||
"minifyCSS": true
|
||||
}
|
@ -1,64 +1,54 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel='import' href='../bower_components/polymer/polymer.html'>
|
||||
<link rel='import' href='../bower_components/layout/layout.html'>
|
||||
|
||||
<link rel="import" href="../bower_components/core-drawer-panel/core-drawer-panel.html">
|
||||
<link rel="import" href="../bower_components/core-header-panel/core-header-panel.html">
|
||||
<link rel="import" href="../bower_components/core-toolbar/core-toolbar.html">
|
||||
<link rel="import" href="../bower_components/core-menu/core-menu.html">
|
||||
<link rel="import" href="../bower_components/core-style/core-style.html">
|
||||
<link rel="import" href="../bower_components/core-icon/core-icon.html">
|
||||
<link rel="import" href="../bower_components/paper-item/paper-item.html">
|
||||
<link rel='import' href='../bower_components/paper-drawer-panel/paper-drawer-panel.html'>
|
||||
<link rel='import' href='../bower_components/paper-header-panel/paper-header-panel.html'>
|
||||
<link rel='import' href='../bower_components/paper-toolbar/paper-toolbar.html'>
|
||||
<link rel='import' href='../bower_components/paper-menu/paper-menu.html'>
|
||||
<link rel='import' href='../bower_components/iron-icon/iron-icon.html'>
|
||||
<link rel='import' href='../bower_components/paper-item/paper-item.html'>
|
||||
<link rel='import' href='../bower_components/paper-item/paper-icon-item.html'>
|
||||
<link rel='import' href='../bower_components/paper-icon-button/paper-icon-button.html'>
|
||||
|
||||
<link rel="import" href="../layouts/partial-states.html">
|
||||
<link rel="import" href="../layouts/partial-history.html">
|
||||
<link rel="import" href="../layouts/partial-logbook.html">
|
||||
<link rel="import" href="../layouts/partial-dev-fire-event.html">
|
||||
<link rel="import" href="../layouts/partial-dev-call-service.html">
|
||||
<link rel="import" href="../layouts/partial-dev-set-state.html">
|
||||
<link rel='import' href='../layouts/partial-states.html'>
|
||||
<link rel='import' href='../layouts/partial-logbook.html'>
|
||||
<link rel='import' href='../layouts/partial-history.html'>
|
||||
<link rel='import' href='../layouts/partial-dev-call-service.html'>
|
||||
<link rel='import' href='../layouts/partial-dev-fire-event.html'>
|
||||
<link rel='import' href='../layouts/partial-dev-set-state.html'>
|
||||
|
||||
<link rel="import" href="../components/ha-notifications.html">
|
||||
<link rel="import" href="../components/ha-modals.html">
|
||||
<link rel="import" href="../components/stream-status.html">
|
||||
<link rel='import' href='../managers/notification-manager.html'>
|
||||
<link rel='import' href='../managers/modal-manager.html'>
|
||||
|
||||
<polymer-element name="home-assistant-main">
|
||||
<template>
|
||||
<core-style ref="ha-headers"></core-style>
|
||||
<link rel='import' href='../components/stream-status.html'>
|
||||
|
||||
<dom-module id='home-assistant-main'>
|
||||
<style>
|
||||
.sidenav {
|
||||
background: #fafafa;
|
||||
box-shadow: 1px 0 1px rgba(0, 0, 0, 0.1);
|
||||
color: #757575;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
core-toolbar {
|
||||
font-weight: normal;
|
||||
padding-left: 24px;
|
||||
.sidenav paper-menu {
|
||||
--paper-menu-color: var(--secondary-text-color);
|
||||
--paper-menu-background-color: #fafafa;
|
||||
}
|
||||
|
||||
.sidenav-menu {
|
||||
overflow: auto;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
paper-icon-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sidenav-menu core-icon {
|
||||
margin-right: 24px;
|
||||
paper-icon-item.logout {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.sidenav-menu > paper-item {
|
||||
min-height: 53px;
|
||||
.divider {
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.text {
|
||||
padding: 16px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@ -67,192 +57,286 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<ha-notifications></ha-notifications>
|
||||
<ha-modals></ha-modals>
|
||||
<template>
|
||||
<notification-manager></notification-manager>
|
||||
<modal-manager></modal-manager>
|
||||
|
||||
<core-drawer-panel id="drawer" on-core-responsive-change="{{responsiveChanged}}">
|
||||
<core-header-panel mode="scroll" drawer class='sidenav'>
|
||||
<core-toolbar>
|
||||
Home Assistant
|
||||
</core-toolbar>
|
||||
<core-menu id="menu" class="sidenav-menu"
|
||||
selected="0" excludedLocalNames="div" on-core-select="{{menuSelect}}"
|
||||
layout vertical>
|
||||
<paper-item data-panel="states">
|
||||
<core-icon icon="apps"></core-icon>
|
||||
States
|
||||
</paper-item>
|
||||
<paper-drawer-panel id='drawer' narrow='{{narrow}}'>
|
||||
<paper-header-panel mode='scroll' drawer class='sidenav fit'>
|
||||
<paper-toolbar>
|
||||
<!-- forces paper toolbar to style title appropriate -->
|
||||
<paper-icon-button hidden></paper-icon-button>
|
||||
<div class="title">Home Assistant</div>
|
||||
</paper-toolbar>
|
||||
|
||||
<template repeat="{{activeFilters as filter}}">
|
||||
<paper-item data-panel="states_{{filter}}">
|
||||
<core-icon icon="{{filter | filterIcon}}"></core-icon>
|
||||
{{filter | filterName}}
|
||||
<paper-menu id='menu'
|
||||
selectable='[data-panel]' attr-for-selected='data-panel'
|
||||
on-iron-select='menuSelect' selected='[[selected]]'>
|
||||
<paper-icon-item data-panel='states'>
|
||||
<iron-icon item-icon icon='apps'></iron-icon> States
|
||||
</paper-icon-item>
|
||||
|
||||
<template is='dom-repeat' items='{{activeFilters}}'>
|
||||
<paper-icon-item data-panel$='[[filterType(item)]]'>
|
||||
<iron-icon item-icon icon='[[filterIcon(item)]]'></iron-icon>
|
||||
<span>[[filterName(item)]]</span>
|
||||
</paper-icon-item>
|
||||
</template>
|
||||
|
||||
<template is='dom-if' if='[[hasHistoryComponent]]'>
|
||||
<paper-icon-item data-panel='history'>
|
||||
<iron-icon item-icon icon='assessment'></iron-icon>
|
||||
History
|
||||
</paper-icon-item>
|
||||
</template>
|
||||
|
||||
<template is='dom-if' if='[[hasLogbookComponent]]'>
|
||||
<paper-icon-item data-panel='logbook'>
|
||||
<iron-icon item-icon icon='list'></iron-icon>
|
||||
Logbook
|
||||
</paper-icon-item>
|
||||
</template>
|
||||
|
||||
<paper-icon-item data-panel='logout' class='logout'>
|
||||
<iron-icon item-icon icon='exit-to-app'></iron-icon>
|
||||
Log Out
|
||||
</paper-icon-item>
|
||||
|
||||
<paper-item class='divider horizontal layout justified'>
|
||||
<div>Streaming updates</div>
|
||||
<stream-status></stream-status>
|
||||
</paper-item>
|
||||
</template>
|
||||
|
||||
<template if="{{hasHistoryComponent}}">
|
||||
<paper-item data-panel="history">
|
||||
<core-icon icon="assessment"></core-icon>
|
||||
History
|
||||
</paper-item>
|
||||
</template>
|
||||
<div class='text label divider'>Developer Tools</div>
|
||||
<div class='dev-tools layout horizontal justified'>
|
||||
<paper-icon-button
|
||||
icon='settings-remote' data-panel$='[[selectedDevService]]'
|
||||
on-click='handleDevClick'></paper-icon-button>
|
||||
<paper-icon-button
|
||||
icon='settings-ethernet' data-panel$='[[selectedDevState]]'
|
||||
on-click='handleDevClick'></paper-icon-button>
|
||||
<paper-icon-button
|
||||
icon='settings-input-antenna' data-panel$='[[selectedDevEvent]]'
|
||||
on-click='handleDevClick'></paper-icon-button>
|
||||
</div>
|
||||
</paper-menu>
|
||||
</paper-header-panel>
|
||||
|
||||
<template if="{{hasLogbookComponent}}">
|
||||
<paper-item data-panel="logbook">
|
||||
<core-icon icon="list"></core-icon>
|
||||
Logbook
|
||||
</paper-item>
|
||||
</template>
|
||||
<template is='dom-if' if='[[!hideStates]]'>
|
||||
<partial-states
|
||||
main narrow='[[narrow]]'
|
||||
filter='[[stateFilter]]'>
|
||||
</partial-states>
|
||||
</template>
|
||||
|
||||
<div flex></div>
|
||||
<template is='dom-if' if='[[isSelectedLogbook]]'>
|
||||
<partial-logbook main narrow='[[narrow]]'></partial-logbook>
|
||||
</template>
|
||||
<template is='dom-if' if='[[isSelectedHistory]]'>
|
||||
<partial-history main narrow='[[narrow]]'></partial-history>
|
||||
</template>
|
||||
<template is='dom-if' if='[[isSelectedDevService]]'>
|
||||
<partial-dev-call-service main narrow='[[narrow]]'></partial-dev-call-service>
|
||||
</template>
|
||||
<template is='dom-if' if='[[isSelectedDevEvent]]'>
|
||||
<partial-dev-fire-event main narrow='[[narrow]]'></partial-dev-fire-event>
|
||||
</template>
|
||||
<template is='dom-if' if='[[isSelectedDevState]]'>
|
||||
<partial-dev-set-state main narrow='[[narrow]]'></partial-dev-set-state>
|
||||
</template>
|
||||
</paper-drawer-panel>
|
||||
|
||||
<paper-item on-click="{{handleLogOutClick}}">
|
||||
<core-icon icon="exit-to-app"></core-icon>
|
||||
Log Out
|
||||
</paper-item>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<div class='text' horizontal layout center>
|
||||
<div flex>Streaming updates</div>
|
||||
<stream-status></stream-status>
|
||||
</div>
|
||||
|
||||
<div class='text label'>Developer Tools</div>
|
||||
<div class='dev-tools' layout horizontal justified>
|
||||
<paper-icon-button
|
||||
icon="settings-remote" data-panel='call-service'
|
||||
on-click="{{handleDevClick}}"></paper-icon-button>
|
||||
<paper-icon-button
|
||||
icon="settings-ethernet" data-panel='set-state'
|
||||
on-click="{{handleDevClick}}"></paper-icon-button>
|
||||
<paper-icon-button
|
||||
icon="settings-input-antenna" data-panel='fire-event'
|
||||
on-click="{{handleDevClick}}"></paper-icon-button>
|
||||
</div>
|
||||
</core-menu>
|
||||
</core-header-panel>
|
||||
|
||||
<!--
|
||||
This is the main partial, never remove it from the DOM but hide it
|
||||
to speed up when people click on states.
|
||||
-->
|
||||
<partial-states hidden?="{{hideStates}}"
|
||||
main narrow="{{narrow}}"
|
||||
togglePanel="{{togglePanel}}"
|
||||
filter="{{stateFilter}}">
|
||||
</partial-states>
|
||||
|
||||
<template if="{{selected == 'history'}}">
|
||||
<partial-history main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-history>
|
||||
</template>
|
||||
<template if="{{selected == 'logbook'}}">
|
||||
<partial-logbook main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-logbook>
|
||||
</template>
|
||||
<template if="{{selected == 'fire-event'}}">
|
||||
<partial-dev-fire-event main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-dev-fire-event>
|
||||
</template>
|
||||
<template if="{{selected == 'set-state'}}">
|
||||
<partial-dev-set-state main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-dev-set-state>
|
||||
</template>
|
||||
<template if="{{selected == 'call-service'}}">
|
||||
<partial-dev-call-service main narrow="{{narrow}}" togglePanel="{{togglePanel}}"></partial-dev-call-service>
|
||||
</template>
|
||||
</core-drawer-panel>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
(function() {
|
||||
var storeListenerMixIn = window.hass.storeListenerMixIn;
|
||||
var authActions = window.hass.authActions;
|
||||
var uiUtil = window.hass.uiUtil;
|
||||
var uiConstants = window.hass.uiConstants;
|
||||
var authActions = window.hass.authActions;
|
||||
|
||||
Polymer(Polymer.mixin({
|
||||
selected: "states",
|
||||
stateFilter: null,
|
||||
narrow: false,
|
||||
activeFilters: [],
|
||||
hasHistoryComponent: false,
|
||||
hasLogbookComponent: false,
|
||||
var uiUtil = window.hass.uiUtil;
|
||||
var uiConstants = window.hass.uiConstants;
|
||||
|
||||
isStreaming: false,
|
||||
hasStreamError: false,
|
||||
Polymer({
|
||||
is: 'home-assistant-main',
|
||||
|
||||
hideStates: false,
|
||||
behaviors: [StoreListenerBehavior],
|
||||
|
||||
attached: function() {
|
||||
this.togglePanel = this.togglePanel.bind(this);
|
||||
properties: {
|
||||
selected: {
|
||||
type: String,
|
||||
value: 'states',
|
||||
},
|
||||
|
||||
this.listenToStores(true);
|
||||
},
|
||||
stateFilter: {
|
||||
type: String,
|
||||
value: null,
|
||||
},
|
||||
|
||||
detached: function() {
|
||||
this.stopListeningToStores();
|
||||
},
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
stateStoreChanged: function(stateStore) {
|
||||
this.activeFilters = stateStore.domains.filter(function(domain) {
|
||||
return domain in uiConstants.STATE_FILTERS;
|
||||
}).toArray();
|
||||
},
|
||||
activeFilters: {
|
||||
type: Array,
|
||||
value: [],
|
||||
},
|
||||
|
||||
componentStoreChanged: function(componentStore) {
|
||||
this.hasHistoryComponent = componentStore.isLoaded('history');
|
||||
this.hasLogbookComponent = componentStore.isLoaded('logbook');
|
||||
},
|
||||
hasHistoryComponent: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
streamStoreChanged: function(streamStore) {
|
||||
this.isStreaming = streamStore.isStreaming;
|
||||
this.hasStreamError = streamStore.hasError;
|
||||
},
|
||||
hasLogbookComponent: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
menuSelect: function(ev, detail, sender) {
|
||||
if (detail.isSelected) {
|
||||
this.selectPanel(detail.item);
|
||||
}
|
||||
},
|
||||
isStreaming: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
handleDevClick: function(ev, detail, sender) {
|
||||
this.$.menu.selected = -1;
|
||||
this.selectPanel(ev.target);
|
||||
},
|
||||
hasStreamError: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
selectPanel: function(element) {
|
||||
var newChoice = element.dataset.panel;
|
||||
hideStates: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
if(newChoice !== this.selected) {
|
||||
this.togglePanel();
|
||||
selectedHistory: {
|
||||
type: String,
|
||||
value: 'history',
|
||||
readOnly: true,
|
||||
},
|
||||
|
||||
isSelectedHistory: {
|
||||
type: Boolean,
|
||||
computed: 'computeIsSelected(selected, selectedHistory)',
|
||||
},
|
||||
|
||||
selectedLogbook: {
|
||||
type: String,
|
||||
value: 'logbook',
|
||||
readOnly: true,
|
||||
},
|
||||
|
||||
isSelectedLogbook: {
|
||||
type: Boolean,
|
||||
computed: 'computeIsSelected(selected, selectedLogbook)',
|
||||
},
|
||||
|
||||
selectedDevEvent: {
|
||||
type: String,
|
||||
value: 'devEvent',
|
||||
readOnly: true,
|
||||
},
|
||||
|
||||
isSelectedDevEvent: {
|
||||
type: Boolean,
|
||||
computed: 'computeIsSelected(selected, selectedDevEvent)',
|
||||
},
|
||||
|
||||
selectedDevState: {
|
||||
type: String,
|
||||
value: 'devState',
|
||||
readOnly: true,
|
||||
},
|
||||
|
||||
isSelectedDevState: {
|
||||
type: Boolean,
|
||||
computed: 'computeIsSelected(selected, selectedDevState)',
|
||||
},
|
||||
|
||||
selectedDevService: {
|
||||
type: String,
|
||||
value: 'devService',
|
||||
readOnly: true,
|
||||
},
|
||||
|
||||
isSelectedDevService: {
|
||||
type: Boolean,
|
||||
computed: 'computeIsSelected(selected, selectedDevService)',
|
||||
},
|
||||
|
||||
|
||||
},
|
||||
|
||||
listeners: {
|
||||
'menu.core-select': 'menuSelect',
|
||||
'open-menu': 'openDrawer',
|
||||
},
|
||||
|
||||
stateStoreChanged: function(stateStore) {
|
||||
this.activeFilters = stateStore.domains.filter(function(domain) {
|
||||
return domain in uiConstants.STATE_FILTERS;
|
||||
}).toArray();
|
||||
},
|
||||
|
||||
componentStoreChanged: function(componentStore) {
|
||||
this.hasHistoryComponent = componentStore.isLoaded('history');
|
||||
this.hasLogbookComponent = componentStore.isLoaded('logbook');
|
||||
},
|
||||
|
||||
menuSelect: function(ev, detail, sender) {
|
||||
this.selectPanel(this.$.menu.selected);
|
||||
},
|
||||
|
||||
handleDevClick: function(ev, detail, sender) {
|
||||
// prevent it from highlighting first menu item
|
||||
document.activeElement.blur();
|
||||
this.selectPanel(ev.target.parentElement.dataset.panel);
|
||||
},
|
||||
|
||||
selectPanel: function(newChoice) {
|
||||
if (newChoice == 'logout') {
|
||||
this.handleLogOut();
|
||||
return;
|
||||
} else if(newChoice == this.selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeDrawer();
|
||||
this.selected = newChoice;
|
||||
|
||||
if (newChoice.substr(0, 7) === 'states_') {
|
||||
this.hideStates = false;
|
||||
this.stateFilter = newChoice.substr(7);
|
||||
} else {
|
||||
this.hideStates = newChoice !== 'states';
|
||||
this.stateFilter = null;
|
||||
}
|
||||
},
|
||||
|
||||
openDrawer: function() {
|
||||
this.$.drawer.openDrawer();
|
||||
},
|
||||
|
||||
closeDrawer: function() {
|
||||
this.$.drawer.closeDrawer();
|
||||
},
|
||||
|
||||
handleLogOut: function() {
|
||||
authActions.logOut();
|
||||
},
|
||||
|
||||
computeIsSelected: function(selected, selectedType) {
|
||||
return selected === selectedType;
|
||||
},
|
||||
|
||||
filterIcon: function(filter) {
|
||||
return uiUtil.domainIcon(filter);
|
||||
},
|
||||
|
||||
filterName: function(filter) {
|
||||
return uiConstants.STATE_FILTERS[filter];
|
||||
},
|
||||
|
||||
filterType: function(filter) {
|
||||
return 'states_' + filter;
|
||||
}
|
||||
|
||||
if (this.selected.substr(0, 7) === 'states_') {
|
||||
this.hideStates = false;
|
||||
this.stateFilter = this.selected.substr(7);
|
||||
} else {
|
||||
this.hideStates = this.selected !== 'states';
|
||||
this.stateFilter = null;
|
||||
}
|
||||
},
|
||||
|
||||
responsiveChanged: function(ev, detail, sender) {
|
||||
this.narrow = detail.narrow;
|
||||
},
|
||||
|
||||
togglePanel: function() {
|
||||
this.$.drawer.togglePanel();
|
||||
},
|
||||
|
||||
handleLogOutClick: function() {
|
||||
authActions.logOut();
|
||||
},
|
||||
|
||||
filterIcon: function(filter) {
|
||||
return uiUtil.domainIcon(filter);
|
||||
},
|
||||
|
||||
filterName: function(filter) {
|
||||
return uiConstants.STATE_FILTERS[filter];
|
||||
},
|
||||
}, storeListenerMixIn));
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</polymer-element>
|
||||
</script>
|
||||
|
@ -1,79 +1,79 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/layout/layout.html">
|
||||
|
||||
<link rel="import" href="../bower_components/core-label/core-label.html">
|
||||
<!-- WIP <link rel="import" href="../bower_components/core-label/core-label.html"> -->
|
||||
<link rel="import" href="../bower_components/paper-checkbox/paper-checkbox.html">
|
||||
<link rel="import" href="../bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input-decorator.html">
|
||||
<link rel="import" href="../bower_components/core-input/core-input.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input-container.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input-error.html">
|
||||
<link rel="import" href="../bower_components/iron-input/iron-input.html">
|
||||
<link rel="import" href="../bower_components/paper-spinner/paper-spinner.html">
|
||||
|
||||
<polymer-element name="login-form">
|
||||
<link rel="import" href="../resources/store-listener-behavior.html">
|
||||
|
||||
<dom-module id="login-form">
|
||||
<style>
|
||||
#passwordDecorator {
|
||||
display: block;
|
||||
height: 57px;
|
||||
}
|
||||
|
||||
paper-checkbox {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
paper-checkbox::shadow #checkbox.checked {
|
||||
background-color: #03a9f4;
|
||||
border-color: #03a9f4;
|
||||
}
|
||||
|
||||
paper-checkbox::shadow #ink[checked] {
|
||||
color: #03a9f4;
|
||||
}
|
||||
|
||||
paper-button {
|
||||
margin-left: 72px;
|
||||
}
|
||||
|
||||
.interact {
|
||||
height: 125px;
|
||||
}
|
||||
|
||||
#validatebox {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.validatemessage {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<style>
|
||||
#passwordDecorator {
|
||||
display: block;
|
||||
height: 57px;
|
||||
}
|
||||
|
||||
paper-checkbox {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
paper-checkbox::shadow #checkbox.checked {
|
||||
background-color: #03a9f4;
|
||||
border-color: #03a9f4;
|
||||
}
|
||||
|
||||
paper-checkbox::shadow #ink[checked] {
|
||||
color: #03a9f4;
|
||||
}
|
||||
|
||||
paper-button {
|
||||
margin-left: 72px;
|
||||
}
|
||||
|
||||
.interact {
|
||||
height: 125px;
|
||||
}
|
||||
|
||||
#validatebox {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.validatemessage {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div layout horizontal center fit class='login' id="splash">
|
||||
<div layout vertical center flex>
|
||||
<div class="layout horizontal center fit login" id="splash">
|
||||
<div class="layout vertical center flex">
|
||||
|
||||
<img src="/static/favicon-192x192.png" />
|
||||
<h1>Home Assistant</h1>
|
||||
|
||||
<a href="#" id="hideKeyboardOnFocus"></a>
|
||||
|
||||
<div class='interact' layout vertical>
|
||||
<div class='interact'>
|
||||
<div id='loginform' hidden$="[[isValidating]]">
|
||||
<paper-input-container id="passwordDecorator" invalid="[[isInvalid]]">
|
||||
<label>Password</label>
|
||||
<input is="iron-input" type="password" id="passwordInput" />
|
||||
<paper-input-error invalid="[[isInvalid]]">[[errorMessage]]</paper-input-error>
|
||||
</paper-input-container>
|
||||
|
||||
<div id='loginform' hidden?="{{isValidating || isLoggedIn}}">
|
||||
<paper-input-decorator label="Password" id="passwordDecorator">
|
||||
<input is="core-input" type="password" id="passwordInput"
|
||||
value="{{authToken}}" on-keyup="{{passwordKeyup}}">
|
||||
</paper-input-decorator>
|
||||
|
||||
<div horizontal center layout>
|
||||
<core-label horizontal layout>
|
||||
<paper-checkbox for checked={{rememberLogin}}></paper-checkbox>
|
||||
Remember
|
||||
</core-label>
|
||||
|
||||
<paper-button on-click={{validatePassword}}>Log In</paper-button>
|
||||
<div class="layout horizontal center">
|
||||
<paper-checkbox for id='rememberLogin'>Remember</paper-checkbox>
|
||||
<paper-button id='loginButton'>Log In</paper-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="validatebox" hidden?="{{!(isValidating || isLoggedIn)}}">
|
||||
<div id="validatebox" hidden$="[[!isValidating]]">
|
||||
<paper-spinner active="true"></paper-spinner><br />
|
||||
<div class="validatemessage">{{spinnerMessage}}</div>
|
||||
<div class="validatemessage">Loading data…</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -81,43 +81,53 @@
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
var storeListenerMixIn = window.hass.storeListenerMixIn;
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var uiActions = window.hass.uiActions;
|
||||
|
||||
Polymer(Polymer.mixin({
|
||||
MSG_VALIDATING: "Validating password…",
|
||||
MSG_LOADING_DATA: "Loading data…",
|
||||
Polymer({
|
||||
is: 'login-form',
|
||||
|
||||
authToken: "",
|
||||
rememberLogin: false,
|
||||
behaviors: [StoreListenerBehavior],
|
||||
|
||||
isValidating: false,
|
||||
isLoggedIn: false,
|
||||
properties: {
|
||||
isValidating: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
spinnerMessage: "",
|
||||
isInvalid: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
errorMessage: {
|
||||
type: String,
|
||||
value: '',
|
||||
}
|
||||
},
|
||||
|
||||
listeners: {
|
||||
'passwordInput.keydown': 'passwordKeyDown',
|
||||
'loginButton.click': 'validatePassword',
|
||||
},
|
||||
|
||||
attached: function() {
|
||||
this.focusPassword();
|
||||
this.listenToStores(true);
|
||||
},
|
||||
|
||||
detached: function() {
|
||||
this.stopListeningToStores();
|
||||
},
|
||||
|
||||
authStoreChanged: function(authStore) {
|
||||
this.isValidating = authStore.isValidating;
|
||||
this.isLoggedIn = authStore.isLoggedIn;
|
||||
this.spinnerMessage = this.isValidating ? this.MSG_VALIDATING : this.MSG_LOADING_DATA;
|
||||
|
||||
if (authStore.lastAttemptInvalid) {
|
||||
this.$.passwordDecorator.error = authStore.lastAttemptMessage;
|
||||
this.$.passwordDecorator.isInvalid = true;
|
||||
this.errorMessage = authStore.lastAttemptMessage;
|
||||
this.isInvalid = true;
|
||||
}
|
||||
|
||||
if (!(this.isValidating && this.isLoggedIn)) {
|
||||
this.job('focusPasswordBox', this.focusPassword.bind(this));
|
||||
if (!this.isValidating) {
|
||||
setTimeout(this.focusPassword.bind(this), 0);
|
||||
}
|
||||
},
|
||||
|
||||
@ -125,23 +135,25 @@
|
||||
this.$.passwordInput.focus();
|
||||
},
|
||||
|
||||
passwordKeyup: function(ev) {
|
||||
passwordKeyDown: function(ev) {
|
||||
// validate on enter
|
||||
if(ev.keyCode === 13) {
|
||||
this.validatePassword();
|
||||
ev.preventDefault();
|
||||
|
||||
// clear error after we start typing again
|
||||
} else if(this.$.passwordDecorator.isInvalid) {
|
||||
this.$.passwordDecorator.isInvalid = false;
|
||||
} else if(this.isInvalid) {
|
||||
this.isInvalid = false;
|
||||
}
|
||||
},
|
||||
|
||||
validatePassword: function() {
|
||||
this.$.hideKeyboardOnFocus.focus();
|
||||
|
||||
uiActions.validateAuth(this.authToken, this.rememberLogin);
|
||||
uiActions.validateAuth(this.$.passwordInput.value, this.$.rememberLogin.checked);
|
||||
},
|
||||
}, storeListenerMixIn));
|
||||
</script>
|
||||
</polymer-element>
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
|
@ -1,28 +1,41 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel='import' href='../bower_components/polymer/polymer.html'>
|
||||
|
||||
<link rel="import" href="../bower_components/core-scroll-header-panel/core-scroll-header-panel.html">
|
||||
<link rel="import" href="../bower_components/core-toolbar/core-toolbar.html">
|
||||
<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html">
|
||||
<link rel='import' href='../bower_components/paper-scroll-header-panel/paper-scroll-header-panel.html'>
|
||||
|
||||
<link rel="import" href="../bower_components/core-style/core-style.html">
|
||||
<link rel='import' href='../bower_components/paper-toolbar/paper-toolbar.html'>
|
||||
<link rel='import' href='../bower_components/paper-icon-button/paper-icon-button.html'>
|
||||
|
||||
<polymer-element name="partial-base" attributes="narrow togglePanel" noscript>
|
||||
<template>
|
||||
<core-style ref="ha-headers"></core-style>
|
||||
<dom-module id='partial-base'>
|
||||
<template>
|
||||
<paper-scroll-header-panel class='fit'>
|
||||
<paper-toolbar>
|
||||
<paper-icon-button icon='menu' hidden$='[[!narrow]]' on-click='toggleMenu'></paper-icon-button>
|
||||
<div class="title">
|
||||
<content select='[header-title]'></content>
|
||||
</div>
|
||||
<content select='[header-buttons]'></content>
|
||||
</paper-toolbar>
|
||||
|
||||
<core-scroll-header-panel fit fixed="{{!narrow}}">
|
||||
<core-toolbar>
|
||||
<paper-icon-button
|
||||
id="navicon" icon="menu" hidden?="{{!narrow}}"
|
||||
on-click="{{togglePanel}}"></paper-icon-button>
|
||||
<div flex>
|
||||
<content select="[header-title]"></content>
|
||||
</div>
|
||||
<content select="[header-buttons]"></content>
|
||||
</core-toolbar>
|
||||
<content></content>
|
||||
</paper-scroll-header-panel>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<content></content>
|
||||
<script>
|
||||
(function() {
|
||||
Polymer({
|
||||
is: 'partial-base',
|
||||
|
||||
</core-scroll-header-panel>
|
||||
</template>
|
||||
</polymer>
|
||||
properties: {
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
|
||||
toggleMenu: function() {
|
||||
this.fire('open-menu');
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
@ -2,87 +2,100 @@
|
||||
|
||||
<link rel="import" href="../bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input-decorator.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-autogrow-textarea.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-textarea.html">
|
||||
|
||||
<link rel="import" href="./partial-base.html">
|
||||
|
||||
<link rel="import" href="../components/services-list.html">
|
||||
|
||||
<polymer-element name="partial-dev-call-service" attributes="narrow togglePanel">
|
||||
<template>
|
||||
|
||||
<dom-module id="partial-dev-call-service">
|
||||
<style>
|
||||
.form {
|
||||
padding: 24px;
|
||||
background-color: white;
|
||||
}
|
||||
.form {
|
||||
padding: 24px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.ha-form {
|
||||
margin-right: 16px;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<partial-base narrow="[[narrow]]">
|
||||
<span header-title>Call Service</span>
|
||||
|
||||
<partial-base narrow="{{narrow}}" togglePanel="{{togglePanel}}">
|
||||
<span header-title>Call Service</span>
|
||||
<div class='form fit'>
|
||||
<p>
|
||||
Call a service from a component.
|
||||
</p>
|
||||
|
||||
<div class='form' fit>
|
||||
<p>
|
||||
Call a service from a component.
|
||||
</p>
|
||||
<div class$='[[computeFormClasses(narrow)]]'>
|
||||
<div class='ha-form flex'>
|
||||
<paper-input label="Domain" autofocus value='{{domain}}'></paper-input>
|
||||
<paper-input label="Service" value='{{service}}'></paper-input>
|
||||
<paper-textarea label="Service Data (JSON, optional)" value='{{serviceData}}'></paper-textarea>
|
||||
<paper-button on-click='callService' raised>Call Service</paper-button>
|
||||
</div>
|
||||
|
||||
<div layout horizontal?="{{!narrow}}" vertical?="{{narrow}}">
|
||||
<div class='ha-form' flex?="{{!narrow}}">
|
||||
<paper-input id="inputDomain" label="Domain" floatingLabel="true" autofocus required></paper-input>
|
||||
<paper-input id="inputService" label="Service" floatingLabel="true" required></paper-input>
|
||||
<paper-input-decorator
|
||||
label="Service Data (JSON, optional)"
|
||||
floatingLabel="true">
|
||||
|
||||
<paper-autogrow-textarea id="inputDataWrapper">
|
||||
<textarea id="inputData"></textarea>
|
||||
</paper-autogrow-textarea>
|
||||
|
||||
</paper-input-decorator>
|
||||
<paper-button on-click={{clickCallService}}>Call Service</paper-button>
|
||||
</div>
|
||||
|
||||
<div class='sidebar'>
|
||||
<b>Available services:</b>
|
||||
<services-list cbServiceClicked={{serviceSelected}}></services-list>
|
||||
<div>
|
||||
<h4>Available services:</h4>
|
||||
<services-list on-service-selected='serviceSelected'></services-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</partial-base>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
|
||||
</partial-base>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
var serviceActions = window.hass.serviceActions;
|
||||
(function() {
|
||||
var serviceActions = window.hass.serviceActions;
|
||||
|
||||
Polymer({
|
||||
ready: function() {
|
||||
// to ensure callback methods work..
|
||||
this.serviceSelected = this.serviceSelected.bind(this);
|
||||
},
|
||||
Polymer({
|
||||
is: 'partial-dev-call-service',
|
||||
|
||||
setService: function(domain, service) {
|
||||
this.$.inputDomain.value = domain;
|
||||
this.$.inputService.value = service;
|
||||
},
|
||||
properties: {
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
serviceSelected: function(domain, service) {
|
||||
this.setService(domain, service);
|
||||
},
|
||||
domain: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
|
||||
clickCallService: function() {
|
||||
try {
|
||||
serviceActions.callService(
|
||||
this.$.inputDomain.value,
|
||||
this.$.inputService.value,
|
||||
this.$.inputData.value ? JSON.parse(this.$.inputData.value) : {});
|
||||
} catch (err) {
|
||||
alert("Error parsing JSON: " + err);
|
||||
}
|
||||
}
|
||||
service: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
|
||||
});
|
||||
serviceData: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
|
||||
serviceSelected: function(ev) {
|
||||
this.domain = ev.detail.domain;
|
||||
this.service = ev.detail.service;
|
||||
},
|
||||
|
||||
callService: function() {
|
||||
var serviceData;
|
||||
|
||||
try {
|
||||
serviceData = this.serviceData ? JSON.parse(this.serviceData): {};
|
||||
} catch (err) {
|
||||
alert("Error parsing JSON: " + err);
|
||||
return;
|
||||
}
|
||||
|
||||
serviceActions.callService(this.domain, this.service, serviceData);
|
||||
},
|
||||
|
||||
computeFormClasses: function(narrow) {
|
||||
return 'layout ' + (narrow ? 'vertical' : 'horizontal');
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</polymer-element>
|
||||
|
@ -2,79 +2,90 @@
|
||||
|
||||
<link rel="import" href="../bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input-decorator.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-autogrow-textarea.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-textarea.html">
|
||||
|
||||
<link rel="import" href="./partial-base.html">
|
||||
|
||||
<link rel="import" href="../components/events-list.html">
|
||||
|
||||
<polymer-element name="partial-dev-fire-event" attributes="narrow togglePanel">
|
||||
<template>
|
||||
|
||||
<dom-module id="partial-dev-fire-event">
|
||||
<style>
|
||||
.form {
|
||||
padding: 24px;
|
||||
background-color: white;
|
||||
}
|
||||
.form {
|
||||
padding: 24px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.ha-form {
|
||||
margin-right: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<partial-base narrow="{{narrow}}" togglePanel="{{togglePanel}}">
|
||||
<span header-title>Fire Event</span>
|
||||
<template>
|
||||
<partial-base narrow="{{narrow}}">
|
||||
<span header-title>Fire Event</span>
|
||||
|
||||
<div class='form' fit>
|
||||
<p>
|
||||
Fire an event on the event bus.
|
||||
</p>
|
||||
<div class='form fit'>
|
||||
<p>
|
||||
Fire an event on the event bus.
|
||||
</p>
|
||||
|
||||
<div layout horizontal?="{{!narrow}}" vertical?="{{narrow}}">
|
||||
<div class='ha-form' flex?="{{!narrow}}">
|
||||
<paper-input
|
||||
id="inputType" label="Event Type" floatingLabel="true"
|
||||
autofocus required></paper-input>
|
||||
<paper-input-decorator
|
||||
label="Event Data (JSON, optional)"
|
||||
floatingLabel="true">
|
||||
<div class$='[[computeFormClasses(narrow)]]'>
|
||||
<div class='ha-form flex'>
|
||||
<paper-input label="Event Type" autofocus required value='{{eventType}}'></paper-input>
|
||||
<paper-textarea label="Event Data (JSON, optional)" value='{{eventData}}'></paper-textarea>
|
||||
<paper-button on-click='fireEvent' raised>Fire Event</paper-button>
|
||||
</div>
|
||||
|
||||
<paper-autogrow-textarea id="inputDataWrapper">
|
||||
<textarea id="inputData"></textarea>
|
||||
</paper-autogrow-textarea>
|
||||
</paper-input-decorator>
|
||||
|
||||
<paper-button on-click={{clickFireEvent}}>Fire Event</paper-button>
|
||||
</div>
|
||||
|
||||
<div class='sidebar'>
|
||||
<b>Available events:</b>
|
||||
<events-list cbEventClicked={{eventSelected}}></event-list>
|
||||
<div>
|
||||
<h4>Available events:</h4>
|
||||
<events-list on-event-selected='eventSelected'></event-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</partial-base>
|
||||
</partial-base>
|
||||
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
var eventActions = window.hass.eventActions;
|
||||
(function() {
|
||||
var eventActions = window.hass.eventActions;
|
||||
|
||||
Polymer({
|
||||
ready: function() {
|
||||
// to ensure callback methods work..
|
||||
this.eventSelected = this.eventSelected.bind(this);
|
||||
},
|
||||
Polymer({
|
||||
is: 'partial-dev-fire-event',
|
||||
|
||||
eventSelected: function(eventType) {
|
||||
this.$.inputType.value = eventType;
|
||||
},
|
||||
properties: {
|
||||
eventType: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
|
||||
clickFireEvent: function() {
|
||||
try {
|
||||
eventActions.fire(
|
||||
this.$.inputType.value,
|
||||
this.$.inputData.value ? JSON.parse(this.$.inputData.value) : {});
|
||||
} catch (err) {
|
||||
alert("Error parsing JSON: " + err);
|
||||
}
|
||||
}
|
||||
});
|
||||
eventData: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
|
||||
eventSelected: function(ev) {
|
||||
this.eventType = ev.detail.eventType;
|
||||
},
|
||||
|
||||
fireEvent: function() {
|
||||
var eventData;
|
||||
|
||||
try {
|
||||
eventData = this.eventData ? JSON.parse(this.eventData) : {};
|
||||
} catch (err) {
|
||||
alert("Error parsing JSON: " + err);
|
||||
return;
|
||||
}
|
||||
|
||||
eventActions.fire(this.eventType, eventData);
|
||||
},
|
||||
|
||||
computeFormClasses: function(narrow) {
|
||||
return 'layout ' + (narrow ? 'vertical' : 'horizontal');
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</polymer-element>
|
||||
|
@ -2,105 +2,109 @@
|
||||
|
||||
<link rel="import" href="../bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-input-decorator.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-autogrow-textarea.html">
|
||||
<link rel="import" href="../bower_components/paper-input/paper-textarea.html">
|
||||
|
||||
<link rel="import" href="./partial-base.html">
|
||||
|
||||
<link rel="import" href="../components/entity-list.html">
|
||||
|
||||
<polymer-element name="partial-dev-set-state" attributes="narrow togglePanel">
|
||||
<template>
|
||||
|
||||
<dom-module id="partial-dev-set-state">
|
||||
<style>
|
||||
.form {
|
||||
padding: 24px;
|
||||
background-color: white;
|
||||
}
|
||||
.form {
|
||||
padding: 24px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.ha-form {
|
||||
margin-right: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<partial-base narrow="{{narrow}}" togglePanel="{{togglePanel}}">
|
||||
<span header-title>Set State</span>
|
||||
<template>
|
||||
<partial-base narrow="[[narrow]]">
|
||||
<span header-title>Set State</span>
|
||||
|
||||
<div class='form' fit>
|
||||
<div>
|
||||
Set the representation of a device within Home Assistant.<br />
|
||||
This will not communicate with the actual device.
|
||||
</div>
|
||||
|
||||
<div layout horizontal?="{{!narrow}}" vertical?="{{narrow}}">
|
||||
<div class='ha-form' flex?="{{!narrow}}">
|
||||
<paper-input id="inputEntityID" label="Entity ID" floatingLabel="true" autofocus required></paper-input>
|
||||
<paper-input id="inputState" label="State" floatingLabel="true" required></paper-input>
|
||||
<paper-input-decorator
|
||||
label="State attributes (JSON, optional)"
|
||||
floatingLabel="true">
|
||||
|
||||
<paper-autogrow-textarea id="inputDataWrapper">
|
||||
<textarea id="inputData"></textarea>
|
||||
</paper-autogrow-textarea>
|
||||
|
||||
</paper-input-decorator>
|
||||
|
||||
<paper-button on-click={{clickSetState}}>Set State</paper-button>
|
||||
<div class='form fit'>
|
||||
<div>
|
||||
Set the representation of a device within Home Assistant.<br />
|
||||
This will not communicate with the actual device.
|
||||
</div>
|
||||
|
||||
<div class='sidebar'>
|
||||
<b>Current entities:</b>
|
||||
<entity-list cbEntityClicked={{entitySelected}}></entity-list>
|
||||
<div class$='[[computeFormClasses(narrow)]]'>
|
||||
<div class='ha-form flex'>
|
||||
<paper-input label="Entity ID" autofocus required value='{{entityId}}'></paper-input>
|
||||
<paper-input label="State" required value='{{state}}'></paper-input>
|
||||
<paper-textarea label="State attributes (JSON, optional)" value='{{stateAttributes}}'></paper-textarea>
|
||||
<paper-button on-click='handleSetState' raised>Set State</paper-button>
|
||||
</div>
|
||||
|
||||
<div class='sidebar'>
|
||||
<h4>Current entities:</h4>
|
||||
<entity-list on-entity-selected='entitySelected'></entity-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</partial-base>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
</div>
|
||||
</partial-base>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
var stateStore = window.hass.stateStore;
|
||||
var stateActions = window.hass.stateActions;
|
||||
(function() {
|
||||
var stateStore = window.hass.stateStore;
|
||||
var stateActions = window.hass.stateActions;
|
||||
|
||||
Polymer({
|
||||
ready: function() {
|
||||
// to ensure callback methods work..
|
||||
this.entitySelected = this.entitySelected.bind(this);
|
||||
},
|
||||
Polymer({
|
||||
is: 'partial-dev-set-state',
|
||||
|
||||
setEntityId: function(entityId) {
|
||||
this.$.inputEntityID.value = entityId;
|
||||
},
|
||||
properties: {
|
||||
entityId: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
|
||||
setState: function(state) {
|
||||
this.$.inputState.value = state;
|
||||
},
|
||||
state: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
|
||||
setStateData: function(stateData) {
|
||||
var value = stateData ? JSON.stringify(stateData, null, ' ') : "";
|
||||
stateAttributes: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
|
||||
this.$.inputData.value = value;
|
||||
setStateData: function(stateData) {
|
||||
var value = stateData ? JSON.stringify(stateData, null, ' ') : "";
|
||||
|
||||
// not according to the spec but it works...
|
||||
this.$.inputDataWrapper.update(this.$.inputData);
|
||||
},
|
||||
this.$.inputData.value = value;
|
||||
|
||||
entitySelected: function(entityId) {
|
||||
this.setEntityId(entityId);
|
||||
// not according to the spec but it works...
|
||||
this.$.inputDataWrapper.update(this.$.inputData);
|
||||
},
|
||||
|
||||
var state = stateStore.get(entityId);
|
||||
this.setState(state.state);
|
||||
this.setStateData(state.attributes);
|
||||
},
|
||||
entitySelected: function(ev) {
|
||||
var state = stateStore.get(ev.detail.entityId);
|
||||
|
||||
clickSetState: function(ev) {
|
||||
try {
|
||||
stateActions.set(
|
||||
this.$.inputEntityID.value,
|
||||
this.$.inputState.value,
|
||||
this.$.inputData.value ? JSON.parse(this.$.inputData.value) : {}
|
||||
);
|
||||
} catch (err) {
|
||||
alert("Error parsing JSON: " + err);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.entityId = state.entityId;
|
||||
this.state = state.state;
|
||||
this.stateAttributes = JSON.stringify(state.attributes, null, ' ');
|
||||
},
|
||||
|
||||
handleSetState: function() {
|
||||
var attr;
|
||||
try {
|
||||
attr = this.stateAttributes ? JSON.parse(this.stateAttributes) : {};
|
||||
} catch (err) {
|
||||
alert("Error parsing JSON: " + err);
|
||||
return;
|
||||
}
|
||||
|
||||
stateActions.set(this.entityId, this.state, attr);
|
||||
},
|
||||
|
||||
computeFormClasses: function(narrow) {
|
||||
return 'layout ' + (narrow ? 'vertical' : 'horizontal');
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</polymer-element>
|
||||
|
@ -1,13 +1,11 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/core-style/core-style.html">
|
||||
<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html">
|
||||
|
||||
<link rel="import" href="./partial-base.html">
|
||||
|
||||
<link rel="import" href="../components/state-timeline.html">
|
||||
<link rel="import" href="../components/state-history-charts.html">
|
||||
|
||||
<polymer-element name="partial-history" attributes="narrow togglePanel">
|
||||
<template>
|
||||
<dom-module id="partial-history">
|
||||
<style>
|
||||
.content {
|
||||
background-color: white;
|
||||
@ -17,33 +15,42 @@
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
<partial-base narrow="{{narrow}}" togglePanel="{{togglePanel}}">
|
||||
<span header-title>History</span>
|
||||
<template>
|
||||
<partial-base narrow="[[narrow]]">
|
||||
<span header-title>History</span>
|
||||
|
||||
<span header-buttons>
|
||||
<paper-icon-button icon="refresh"
|
||||
on-click="{{handleRefreshClick}}"></paper-icon-button>
|
||||
</span>
|
||||
<paper-icon-button icon="refresh" header-buttons
|
||||
on-click="handleRefreshClick"></paper-icon-button>
|
||||
|
||||
<div flex class="{{ {content: true, narrow: narrow, wide: !narrow} | tokenList }}">
|
||||
<state-timeline stateHistory="{{stateHistory}}" isLoadingData="{{isLoadingData}}"></state-timeline>
|
||||
</div>
|
||||
</partial-base>
|
||||
</template>
|
||||
<div class$="[[computeContentClasses(narrow)]]">
|
||||
<state-history-charts state-history="[[stateHistory]]"
|
||||
is-loading-data="[[isLoadingData]]"></state-history-charts>
|
||||
</div>
|
||||
</partial-base>
|
||||
</template>
|
||||
</dom-module>
|
||||
<script>
|
||||
var storeListenerMixIn = window.hass.storeListenerMixIn;
|
||||
(function() {
|
||||
var stateHistoryActions = window.hass.stateHistoryActions;
|
||||
|
||||
Polymer(Polymer.mixin({
|
||||
stateHistory: null,
|
||||
isLoadingData: false,
|
||||
Polymer({
|
||||
is: 'partial-history',
|
||||
|
||||
attached: function() {
|
||||
this.listenToStores(true);
|
||||
},
|
||||
behaviors: [StoreListenerBehavior],
|
||||
|
||||
detached: function() {
|
||||
this.stopListeningToStores();
|
||||
properties: {
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
stateHistory: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
isLoadingData: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
|
||||
stateHistoryStoreChanged: function(stateHistoryStore) {
|
||||
@ -62,6 +69,10 @@
|
||||
this.isLoadingData = true;
|
||||
stateHistoryActions.fetchAll();
|
||||
},
|
||||
}, storeListenerMixIn));
|
||||
</script>
|
||||
</polymer>
|
||||
|
||||
computeContentClasses: function(narrow) {
|
||||
return 'flex content ' + (narrow ? 'narrow' : 'wide');
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
@ -1,43 +1,50 @@
|
||||
<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="./partial-base.html">
|
||||
|
||||
<link rel="import" href="../components/ha-logbook.html">
|
||||
|
||||
<polymer-element name="partial-logbook" attributes="narrow togglePanel">
|
||||
<template>
|
||||
<dom-module id="partial-logbook">
|
||||
<style>
|
||||
.content {
|
||||
background-color: white;
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
<partial-base narrow="{{narrow}}" togglePanel="{{togglePanel}}">
|
||||
<span header-title>Logbook</span>
|
||||
<template>
|
||||
<partial-base narrow="[[narrow]]">
|
||||
<span header-title>Logbook</span>
|
||||
|
||||
<span header-buttons>
|
||||
<paper-icon-button icon="refresh"
|
||||
on-click="{{handleRefreshClick}}"></paper-icon-button>
|
||||
</span>
|
||||
<paper-icon-button icon="refresh" header-buttons
|
||||
on-click="handleRefresh"></paper-icon-button>
|
||||
|
||||
<ha-logbook entries="[[entries]]"></ha-logbook>
|
||||
</partial-base>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<div flex class="{{ {content: true, narrow: narrow, wide: !narrow} | tokenList }}">
|
||||
<ha-logbook entries="{{entries}}"></ha-logbook>
|
||||
</div>
|
||||
</partial-base>
|
||||
</template>
|
||||
<script>
|
||||
(function() {
|
||||
var storeListenerMixIn = window.hass.storeListenerMixIn;
|
||||
var logbookActions = window.hass.logbookActions;
|
||||
|
||||
Polymer(Polymer.mixin({
|
||||
entries: null,
|
||||
Polymer({
|
||||
is: 'partial-logbook',
|
||||
|
||||
attached: function() {
|
||||
this.listenToStores(true);
|
||||
},
|
||||
behaviors: [StoreListenerBehavior],
|
||||
|
||||
detached: function() {
|
||||
this.stopListeningToStores();
|
||||
properties: {
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
entries: {
|
||||
type: Array,
|
||||
value: [],
|
||||
},
|
||||
},
|
||||
|
||||
logbookStoreChanged: function(logbookStore) {
|
||||
@ -48,9 +55,9 @@
|
||||
this.entries = logbookStore.all.toArray();
|
||||
},
|
||||
|
||||
handleRefreshClick: function() {
|
||||
handleRefresh: function() {
|
||||
logbookActions.fetch();
|
||||
},
|
||||
}, storeListenerMixIn));
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</polymer>
|
||||
|
@ -1,18 +1,21 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/core-icon/core-icon.html">
|
||||
<link rel="import" href="../bower_components/core-style/core-style.html">
|
||||
<link rel="import" href="../bower_components/iron-icon/iron-icon.html">
|
||||
|
||||
<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html">
|
||||
|
||||
<link rel="import" href="./partial-base.html">
|
||||
|
||||
<link rel="import" href="../components/state-cards.html">
|
||||
|
||||
<polymer-element name="partial-states" attributes="narrow togglePanel filter">
|
||||
<template>
|
||||
<core-style ref="ha-animations"></core-style>
|
||||
|
||||
<dom-module id="partial-states">
|
||||
<style>
|
||||
.listening {
|
||||
.content-wrapper {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
background-color: #E5E5E5;
|
||||
}
|
||||
|
||||
.content-wrapper ::content .listening {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@ -36,65 +39,124 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<partial-base narrow="{{narrow}}" togglePanel="{{togglePanel}}">
|
||||
<span header-title>{{headerTitle}}</span>
|
||||
<template>
|
||||
<partial-base narrow="[[narrow]]">
|
||||
<span header-title>{{headerTitle}}</span>
|
||||
|
||||
<span header-buttons>
|
||||
<paper-icon-button icon="refresh" class="{{isFetching && 'ha-spin'}}"
|
||||
on-click="{{handleRefreshClick}}" hidden?="{{isStreaming}}"></paper-icon-button>
|
||||
<paper-icon-button icon="{{isListening ? 'av:mic-off' : 'av:mic' }}" hidden?={{!canListen}}
|
||||
on-click="{{handleListenClick}}"></paper-icon-button>
|
||||
</span>
|
||||
<span header-buttons>
|
||||
<paper-icon-button icon="refresh" class$="[[computeRefreshButtonClass(isFetching)]]"
|
||||
on-click="handleRefresh" hidden$="[[isStreaming]]"></paper-icon-button>
|
||||
<paper-icon-button icon="[[listenButtonIcon]]" hidden$={{!canListen}}
|
||||
on-click="handleListenClick"></paper-icon-button>
|
||||
</span>
|
||||
|
||||
<div class='listening' hidden?="{{!isListening && !isTransmitting}}" on-click={{handleListenClick}}>
|
||||
<core-icon icon="av:hearing"></core-icon> {{finalTranscript}}
|
||||
<span class='interimTranscript'>{{interimTranscript}}</span>
|
||||
<paper-spinner active?="{{isTransmitting}}"></paper-spinner>
|
||||
</div>
|
||||
<div class='content-wrapper'>
|
||||
<div class='listening' hidden$="[[!showListenInterface]]"
|
||||
on-click="handleListenClick">
|
||||
<iron-icon icon="av:hearing"></iron-icon> <span>{{finalTranscript}}</span>
|
||||
<span class='interimTranscript'>[[interimTranscript]]</span>
|
||||
<paper-spinner active$="[[isTransmitting]]" alt="Sending voice command to Home Assistant"></paper-spinner>
|
||||
</div>
|
||||
|
||||
<state-cards states="[[states]]">
|
||||
<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>
|
||||
</state-cards>
|
||||
</div>
|
||||
</partial-base>
|
||||
</template>
|
||||
|
||||
</dom-module>
|
||||
|
||||
<state-cards states="{{states}}">
|
||||
<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>
|
||||
</state-cards>
|
||||
</partial-base>
|
||||
</template>
|
||||
<script>
|
||||
(function(){
|
||||
var storeListenerMixIn = window.hass.storeListenerMixIn;
|
||||
var syncActions = window.hass.syncActions;
|
||||
var voiceActions = window.hass.voiceActions;
|
||||
var stateStore = window.hass.stateStore;
|
||||
var uiConstants = window.hass.uiConstants;
|
||||
|
||||
Polymer(Polymer.mixin({
|
||||
headerTitle: "States",
|
||||
states: [],
|
||||
isFetching: false,
|
||||
isStreaming: false,
|
||||
Polymer({
|
||||
is: 'partial-states',
|
||||
|
||||
canListen: false,
|
||||
voiceSupported: false,
|
||||
hasConversationComponent: false,
|
||||
isListening: false,
|
||||
isTransmittingVoice: false,
|
||||
interimTranscript: '',
|
||||
finalTranscript: '',
|
||||
behaviors: [StoreListenerBehavior],
|
||||
|
||||
ready: function() {
|
||||
this.voiceSupported = voiceActions.isSupported();
|
||||
},
|
||||
properties: {
|
||||
/**
|
||||
* Title to show in the header
|
||||
*/
|
||||
headerTitle: {
|
||||
type: String,
|
||||
value: 'States',
|
||||
},
|
||||
|
||||
attached: function() {
|
||||
this.listenToStores(true);
|
||||
},
|
||||
/**
|
||||
* If header is to be shown in narrow mode.
|
||||
*/
|
||||
narrow: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
detached: function() {
|
||||
this.stopListeningToStores();
|
||||
filter: {
|
||||
type: String,
|
||||
value: null,
|
||||
observer: 'filterChanged',
|
||||
},
|
||||
|
||||
voiceSupported: {
|
||||
type: Boolean,
|
||||
value: voiceActions.isSupported(),
|
||||
},
|
||||
|
||||
isFetching: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
isStreaming: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
canListen: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
isListening: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
isTransmitting: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
interimTranscript: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
|
||||
finalTranscript: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
|
||||
listenButtonIcon: {
|
||||
type: String,
|
||||
computed: 'computeListenButtonIcon(isListening)'
|
||||
},
|
||||
|
||||
showListenInterface: {
|
||||
type: Boolean,
|
||||
computed: 'computeShowListenInterface(isListening,isTransmitting)'
|
||||
}
|
||||
},
|
||||
|
||||
componentStoreChanged: function(componentStore) {
|
||||
@ -143,12 +205,12 @@
|
||||
return !(state.domain in uiConstants.STATE_FILTERS);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
this.states = states.toArray().filter(
|
||||
function (el) {return !el.attributes.hidden});
|
||||
function (el) {return !el.attributes.hidden;});
|
||||
},
|
||||
|
||||
handleRefreshClick: function() {
|
||||
handleRefresh: function() {
|
||||
syncActions.fetchAll();
|
||||
},
|
||||
|
||||
@ -159,7 +221,20 @@
|
||||
voiceActions.listen();
|
||||
}
|
||||
},
|
||||
}, storeListenerMixIn));
|
||||
|
||||
computeListenButtonIcon: function(isListening) {
|
||||
return isListening ? 'av:mic-off' : 'av:mic';
|
||||
},
|
||||
|
||||
computeShowListenInterface: function(isListening,isTransmitting) {
|
||||
return isListening || isTransmitting;
|
||||
},
|
||||
|
||||
computeRefreshButtonClass: function(isFetching) {
|
||||
if (isFetching) {
|
||||
return 'ha-spin';
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</polymer>
|
||||
|
@ -2,24 +2,29 @@
|
||||
|
||||
<link rel="import" href="../dialogs/more-info-dialog.html">
|
||||
|
||||
<polymer-element name="ha-modals">
|
||||
<dom-module id="modal-manager">
|
||||
<template>
|
||||
<more-info-dialog id="moreInfoDialog"></more-info-dialog>
|
||||
</template>
|
||||
<script>
|
||||
var uiActions = window.hass.uiActions,
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var uiConstants = window.hass.uiConstants,
|
||||
dispatcher = window.hass.dispatcher;
|
||||
|
||||
Polymer({
|
||||
is: 'modal-manager',
|
||||
|
||||
ready: function() {
|
||||
dispatcher.register(function(payload) {
|
||||
switch (payload.actionType) {
|
||||
case uiActions.ACTION_SHOW_DIALOG_MORE_INFO:
|
||||
case uiConstants.ACTION_SHOW_DIALOG_MORE_INFO:
|
||||
this.$.moreInfoDialog.show(payload.entityId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
||||
})();
|
||||
</script>
|
@ -0,0 +1,47 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<link rel="import" href="../bower_components/paper-toast/paper-toast.html">
|
||||
|
||||
<dom-module id="notification-manager">
|
||||
<style>
|
||||
paper-toast {
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<paper-toast id="toast" text='{{text}}'></paper-toast>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
Polymer({
|
||||
is: 'notification-manager',
|
||||
|
||||
behaviors: [StoreListenerBehavior],
|
||||
|
||||
properties: {
|
||||
text: {
|
||||
type: String,
|
||||
value: '',
|
||||
},
|
||||
|
||||
lastId: {
|
||||
type: Number,
|
||||
},
|
||||
},
|
||||
|
||||
notificationStoreChanged: function(notificationStore) {
|
||||
if (notificationStore.hasNewNotifications(this.lastId)) {
|
||||
var notification = notificationStore.lastNotification;
|
||||
|
||||
this.lastId = notification.id;
|
||||
this.text = notification.message;
|
||||
|
||||
this.$.toast.show();
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
})();
|
||||
</script>
|
@ -1,9 +1,9 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/paper-button/paper-button.html">
|
||||
<link rel="import" href="../bower_components/paper-spinner/paper-spinner.html">
|
||||
<link rel='import' href='../bower_components/polymer/polymer.html'>
|
||||
<link rel='import' href='../bower_components/paper-button/paper-button.html'>
|
||||
|
||||
<polymer-element name="more-info-configurator" attributes="stateObj">
|
||||
<template>
|
||||
<link rel='import' href='../components/loading-box.html'>
|
||||
|
||||
<dom-module id='more-info-configurator'>
|
||||
<style>
|
||||
p {
|
||||
margin: 8px 0;
|
||||
@ -25,62 +25,78 @@
|
||||
text-align: center;
|
||||
height: 41px;
|
||||
}
|
||||
|
||||
p.submit paper-spinner {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
p.submit span {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-top: 6px;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class='layout vertical'>
|
||||
<template is='dom-if' if='[[isConfigurable]]'>
|
||||
|
||||
<div layout vertical>
|
||||
<template if="{{stateObj.state == 'configure'}}">
|
||||
<p hidden$='[[!stateObj.attributes.description]]'>[[stateObj.attributes.description]]</p>
|
||||
|
||||
<p hidden?="{{!stateObj.attributes.description}}">
|
||||
{{stateObj.attributes.description}}
|
||||
</p>
|
||||
<p class='error' hidden$='[[!stateObj.attributes.errors]]'>[[stateObj.attributes.errors]]</p>
|
||||
|
||||
<p class='error' hidden?="{{!stateObj.attributes.errors}}">
|
||||
{{stateObj.attributes.errors}}
|
||||
</p>
|
||||
<p class='center' hidden$='[[!stateObj.attributes.description_image]]'>
|
||||
<img src='[[stateObj.attributes.description_image]]' />
|
||||
</p>
|
||||
|
||||
<p class='center' hidden?="{{!stateObj.attributes.description_image}}">
|
||||
<img src='{{stateObj.attributes.description_image}}' />
|
||||
</p>
|
||||
<p class='submit'>
|
||||
<paper-button raised on-click='submitClicked'
|
||||
hidden$='[[isConfiguring]]'>[[submitCaption]]</paper-button>
|
||||
|
||||
<p class='submit'>
|
||||
<paper-button raised on-click="{{submitClicked}}"
|
||||
hidden?="{{action !== 'display'}}">
|
||||
{{stateObj.attributes.submit_caption || "Set configuration"}}
|
||||
</paper-button>
|
||||
<loading-box hidden$='[[!isConfiguring]]'>Configuring</loading-box>
|
||||
</p>
|
||||
|
||||
<span hidden?="{{action !== 'configuring'}}">
|
||||
<paper-spinner active="true"></paper-spinner><span>Configuring…</span>
|
||||
</span>
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
var storeListenerMixIn = window.hass.storeListenerMixIn;
|
||||
(function() {
|
||||
var syncActions = window.hass.syncActions;
|
||||
var serviceActions = window.hass.serviceActions;
|
||||
|
||||
Polymer(Polymer.mixin({
|
||||
action: "display",
|
||||
isStreaming: false,
|
||||
Polymer({
|
||||
is: 'more-info-configurator',
|
||||
|
||||
attached: function() {
|
||||
this.listenToStores(true);
|
||||
behaviors: [StoreListenerBehavior],
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
action: {
|
||||
type: String,
|
||||
value: 'display',
|
||||
},
|
||||
|
||||
isStreaming: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
isConfigurable: {
|
||||
type: Boolean,
|
||||
computed: 'computeIsConfigurable(stateObj)',
|
||||
},
|
||||
|
||||
isConfiguring: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
submitCaption: {
|
||||
type: String,
|
||||
computed: 'computeSubmitCaption(stateObj)',
|
||||
},
|
||||
},
|
||||
|
||||
detached: function() {
|
||||
this.stopListeningToStores();
|
||||
computeIsConfigurable: function(stateObj) {
|
||||
return stateObj.state == 'configure';
|
||||
},
|
||||
|
||||
computeSubmitCaption: function(stateObj) {
|
||||
return stateObj.attributes.submit_caption || 'Set configuration';
|
||||
},
|
||||
|
||||
streamStoreChanged: function(streamStore) {
|
||||
@ -88,15 +104,15 @@
|
||||
},
|
||||
|
||||
submitClicked: function() {
|
||||
this.action = "configuring";
|
||||
this.isConfiguring = true;
|
||||
|
||||
var data = {
|
||||
configure_id: this.stateObj.attributes.configure_id
|
||||
};
|
||||
|
||||
serviceActions.callService('configurator', 'configure', data).then(
|
||||
|
||||
function() {
|
||||
this.action = 'display';
|
||||
this.isConfiguring = false;
|
||||
|
||||
if (!this.isStreaming) {
|
||||
syncActions.fetchAll();
|
||||
@ -104,9 +120,9 @@
|
||||
}.bind(this),
|
||||
|
||||
function() {
|
||||
this.action = 'display';
|
||||
this.isConfiguring = false;
|
||||
}.bind(this));
|
||||
},
|
||||
}, storeListenerMixIn));
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</polymer-element>
|
||||
|
@ -1,73 +1,78 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel='import' href='../bower_components/polymer/polymer.html'>
|
||||
|
||||
<link rel="import" href="more-info-default.html">
|
||||
<link rel="import" href="more-info-light.html">
|
||||
<link rel="import" href="more-info-group.html">
|
||||
<link rel="import" href="more-info-sun.html">
|
||||
<link rel="import" href="more-info-configurator.html">
|
||||
<link rel="import" href="more-info-thermostat.html">
|
||||
<link rel="import" href="more-info-script.html">
|
||||
<link rel='import' href='more-info-default.html'>
|
||||
<link rel='import' href='more-info-group.html'>
|
||||
<link rel='import' href='more-info-sun.html'>
|
||||
<link rel='import' href='more-info-configurator.html'>
|
||||
<link rel='import' href='more-info-thermostat.html'>
|
||||
<link rel='import' href='more-info-script.html'>
|
||||
<link rel='import' href='more-info-light.html'>
|
||||
<link rel='import' href='more-info-media_player.html'>
|
||||
|
||||
<polymer-element name="more-info-content" attributes="stateObj dialogOpen">
|
||||
<template>
|
||||
<dom-module id='more-info-content'>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<div id='moreInfoContainer' class='{{classNames}}'></div>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
Polymer({
|
||||
classNames: '',
|
||||
dialogOpen: false,
|
||||
(function() {
|
||||
var uiUtil = window.hass.uiUtil;
|
||||
|
||||
observe: {
|
||||
'stateObj.attributes': 'stateAttributesChanged',
|
||||
},
|
||||
Polymer({
|
||||
is: 'more-info-content',
|
||||
|
||||
dialogOpenChanged: function(oldVal, newVal) {
|
||||
var moreInfoContainer = this.$.moreInfoContainer;
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: 'stateObjChanged',
|
||||
},
|
||||
|
||||
if (moreInfoContainer.lastChild) {
|
||||
moreInfoContainer.lastChild.dialogOpen = newVal;
|
||||
}
|
||||
},
|
||||
dialogOpen: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
|
||||
stateObjChanged: function(oldVal, newVal) {
|
||||
var moreInfoContainer = this.$.moreInfoContainer;
|
||||
dialogOpenChanged: function(newVal, oldVal) {
|
||||
var root = Polymer.dom(this);
|
||||
|
||||
if (!newVal) {
|
||||
if (moreInfoContainer.lastChild) {
|
||||
moreInfoContainer.removeChild(moreInfoContainer.lastChild);
|
||||
if (root.lastChild) {
|
||||
root.lastChild.dialogOpen = newVal;
|
||||
}
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
if (!oldVal || oldVal.moreInfoType != newVal.moreInfoType) {
|
||||
if (moreInfoContainer.lastChild) {
|
||||
moreInfoContainer.removeChild(moreInfoContainer.lastChild);
|
||||
stateObjChanged: function(newVal, oldVal) {
|
||||
var root = Polymer.dom(this);
|
||||
var newMoreInfoType;
|
||||
|
||||
if (!newVal || !(newMoreInfoType = uiUtil.stateMoreInfoType(newVal))) {
|
||||
if (root.lastChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var moreInfo = document.createElement("more-info-" + newVal.moreInfoType);
|
||||
moreInfo.stateObj = newVal;
|
||||
moreInfo.dialogOpen = this.dialogOpen;
|
||||
moreInfoContainer.appendChild(moreInfo);
|
||||
if (!oldVal || uiUtil.stateMoreInfoType(oldVal) != newMoreInfoType) {
|
||||
|
||||
} else {
|
||||
if (root.lastChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
|
||||
moreInfoContainer.lastChild.dialogOpen = this.dialogOpen;
|
||||
moreInfoContainer.lastChild.stateObj = newVal;
|
||||
var moreInfo = document.createElement('more-info-' + newMoreInfoType);
|
||||
moreInfo.stateObj = newVal;
|
||||
moreInfo.dialogOpen = this.dialogOpen;
|
||||
root.appendChild(moreInfo);
|
||||
|
||||
}
|
||||
},
|
||||
} else {
|
||||
|
||||
stateAttributesChanged: function(oldVal, newVal) {
|
||||
if (!newVal) return;
|
||||
root.lastChild.dialogOpen = this.dialogOpen;
|
||||
root.lastChild.stateObj = newVal;
|
||||
|
||||
this.classNames = Object.keys(newVal).map(
|
||||
function(key) { return "has-" + key; }).join(' ');
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</polymer-element>
|
||||
|
@ -1,34 +1,41 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/core-style/core-style.html">
|
||||
|
||||
<polymer-element name="more-info-default" attributes="stateObj">
|
||||
<template>
|
||||
<core-style ref='ha-key-value-table'></core-style>
|
||||
<dom-module id="more-info-default">
|
||||
<style>
|
||||
.data-entry .value {
|
||||
max-width: 200px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div layout vertical>
|
||||
|
||||
<template repeat="{{key in stateObj.attributes | getKeys}}">
|
||||
<div layout justified horizontal class='data-entry'>
|
||||
<div class='key'>
|
||||
{{key}}
|
||||
<template>
|
||||
<div class='layout vertical'>
|
||||
<template is='dom-repeat' items="[[getAttributes(stateObj)]]" as="attribute">
|
||||
<div class='data-entry layout justified horizontal'>
|
||||
<div class='key'>[[attribute]]</div>
|
||||
<div class='value'>[[getAttributeValue(stateObj, attribute)]]</div>
|
||||
</div>
|
||||
<div class='value'>
|
||||
{{stateObj.attributes[key]}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
Polymer({
|
||||
getKeys: function(obj) {
|
||||
return Object.keys(obj || {});
|
||||
}
|
||||
is: 'more-info-default',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
|
||||
getAttributes: function(stateObj) {
|
||||
return stateObj ? Object.keys(stateObj.attributes) : [];
|
||||
},
|
||||
|
||||
getAttributeValue: function(stateObj, attribute) {
|
||||
return stateObj.attributes[attribute];
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</polymer-element>
|
||||
|
@ -2,8 +2,7 @@
|
||||
|
||||
<link rel="import" href="../cards/state-card-content.html">
|
||||
|
||||
<polymer-element name="more-info-group" attributes="stateObj">
|
||||
<template>
|
||||
<dom-module id="more-info-group">
|
||||
<style>
|
||||
.child-card {
|
||||
margin-bottom: 8px;
|
||||
@ -13,37 +12,44 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template repeat="{{states as state}}">
|
||||
<state-card-content stateObj="{{state}}" class='child-card'>
|
||||
</state-card-content>
|
||||
<template>
|
||||
<template is='dom-repeat' items="[[states]]" as='state'>
|
||||
<div class='child-card'>
|
||||
<state-card-content state-obj="[[state]]"></state-card-content>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
var storeListenerMixIn = window.hass.storeListenerMixIn;
|
||||
var stateStore = window.hass.stateStore;
|
||||
(function() {
|
||||
var stateStore = window.hass.stateStore;
|
||||
|
||||
Polymer(Polymer.mixin({
|
||||
attached: function() {
|
||||
this.listenToStores(true);
|
||||
},
|
||||
Polymer({
|
||||
is: 'more-info-group',
|
||||
|
||||
detached: function() {
|
||||
this.stopListeningToStores();
|
||||
},
|
||||
behaviors: [StoreListenerBehavior],
|
||||
|
||||
stateStoreChanged: function() {
|
||||
this.updateStates();
|
||||
},
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: 'updateStates',
|
||||
},
|
||||
|
||||
stateObjChanged: function() {
|
||||
this.updateStates();
|
||||
},
|
||||
states: {
|
||||
type: Array,
|
||||
value: [],
|
||||
},
|
||||
},
|
||||
|
||||
updateStates: function() {
|
||||
this.states = this.stateObj && this.stateObj.attributes.entity_id ?
|
||||
stateStore.gets(this.stateObj.attributes.entity_id).toArray() : [];
|
||||
},
|
||||
}, storeListenerMixIn));
|
||||
stateStoreChanged: function() {
|
||||
this.updateStates();
|
||||
},
|
||||
|
||||
updateStates: function() {
|
||||
this.states = this.stateObj && this.stateObj.attributes.entity_id ?
|
||||
stateStore.gets(this.stateObj.attributes.entity_id).toArray() : [];
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</polymer-element>
|
||||
|
@ -1,10 +1,9 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/paper-slider/paper-slider.html">
|
||||
<link rel='import' href='../bower_components/polymer/polymer.html'>
|
||||
<link rel='import' href='../bower_components/paper-slider/paper-slider.html'>
|
||||
|
||||
<link rel="import" href="../bower_components/color-picker-element/dist/color-picker.html">
|
||||
<link rel='import' href='../bower_components/color-picker-element/dist/color-picker.html'>
|
||||
|
||||
<polymer-element name="more-info-light" attributes="stateObj">
|
||||
<template>
|
||||
<dom-module id='more-info-light'>
|
||||
<style>
|
||||
.brightness {
|
||||
margin-bottom: 8px;
|
||||
@ -14,11 +13,6 @@
|
||||
transition: max-height .5s ease-in;
|
||||
}
|
||||
|
||||
.brightness paper-slider::shadow #sliderKnobInner,
|
||||
.brightness paper-slider::shadow #sliderBar::shadow #activeProgress {
|
||||
background-color: #039be5;
|
||||
}
|
||||
|
||||
color-picker {
|
||||
display: block;
|
||||
width: 350px;
|
||||
@ -26,81 +20,92 @@
|
||||
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
transition: max-height .5s ease-in .3s;
|
||||
transition: max-height .5s ease-in;
|
||||
}
|
||||
|
||||
:host-context(.has-brightness) .brightness {
|
||||
.has-brightness .brightness {
|
||||
max-height: 40px;
|
||||
}
|
||||
|
||||
.has-xy_color color-picker {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
:host-context(.has-xy_color) color-picker {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
</style>
|
||||
<div>
|
||||
<div class='brightness'>
|
||||
<div center horizontal layout>
|
||||
<template>
|
||||
<div class$='[[computeClassNames(stateObj)]]'>
|
||||
<div class='brightness center horizontal layout'>
|
||||
<div>Brightness</div>
|
||||
<paper-slider
|
||||
max="255" flex id='brightness' value='{{brightnessSliderValue}}'
|
||||
on-change="{{brightnessSliderChanged}}">
|
||||
max='255' id='brightness' value='{{brightnessSliderValue}}'
|
||||
on-change='brightnessSliderChanged' class='flex'>
|
||||
</paper-slider>
|
||||
</div>
|
||||
|
||||
<color-picker on-colorselected='colorPicked' width='350' height='200'>
|
||||
</color-picker>
|
||||
</div>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<color-picker id="colorpicker" width="350" height="200">
|
||||
</color-picker>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
var serviceActions = window.hass.serviceActions;
|
||||
(function() {
|
||||
var serviceActions = window.hass.serviceActions;
|
||||
var uiUtil = window.hass.uiUtil;
|
||||
var ATTRIBUTE_CLASSES = ['brightness', 'xy_color'];
|
||||
|
||||
Polymer({
|
||||
brightnessSliderValue: 0,
|
||||
Polymer({
|
||||
is: 'more-info-light',
|
||||
|
||||
observe: {
|
||||
'stateObj.attributes.brightness': 'stateObjBrightnessChanged',
|
||||
},
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: 'stateObjChanged',
|
||||
},
|
||||
|
||||
stateObjChanged: function(oldVal, newVal) {
|
||||
if (newVal && newVal.state === 'on') {
|
||||
this.brightnessSliderValue = newVal.attributes.brightness;
|
||||
}
|
||||
},
|
||||
brightnessSliderValue: {
|
||||
type: Number,
|
||||
value: 0,
|
||||
}
|
||||
},
|
||||
|
||||
stateObjBrightnessChanged: function(oldVal, newVal) {
|
||||
this.brightnessSliderValue = newVal;
|
||||
},
|
||||
stateObjChanged: function(newVal, oldVal) {
|
||||
if (newVal && newVal.state === 'on') {
|
||||
this.brightnessSliderValue = newVal.attributes.brightness;
|
||||
}
|
||||
|
||||
domReady: function() {
|
||||
this.$.colorpicker.addEventListener('colorselected', this.colorPicked.bind(this));
|
||||
},
|
||||
this.debounce('more-info-light-animation-finish', function() {
|
||||
this.fire('iron-resize');
|
||||
}.bind(this), 500);
|
||||
},
|
||||
|
||||
brightnessSliderChanged: function(ev, details, target) {
|
||||
var bri = parseInt(target.value);
|
||||
computeClassNames: function(stateObj) {
|
||||
return uiUtil.attributeClassNames(stateObj, ATTRIBUTE_CLASSES);
|
||||
},
|
||||
|
||||
if(isNaN(bri)) return;
|
||||
brightnessSliderChanged: function(ev) {
|
||||
var bri = parseInt(ev.target.value);
|
||||
|
||||
if(bri === 0) {
|
||||
serviceActions.callTurnOff(this.stateObj.entityId);
|
||||
} else {
|
||||
serviceActions.callService("light", "turn_on", {
|
||||
if(isNaN(bri)) return;
|
||||
|
||||
if(bri === 0) {
|
||||
serviceActions.callTurnOff(this.stateObj.entityId);
|
||||
} else {
|
||||
serviceActions.callService('light', 'turn_on', {
|
||||
entity_id: this.stateObj.entityId,
|
||||
brightness: bri
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
colorPicked: function(ev) {
|
||||
var color = ev.detail.rgb;
|
||||
|
||||
serviceActions.callService('light', 'turn_on', {
|
||||
entity_id: this.stateObj.entityId,
|
||||
brightness: bri
|
||||
rgb_color: [color.r, color.g, color.b]
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
colorPicked: function(ev) {
|
||||
var color = ev.detail.rgb;
|
||||
|
||||
serviceActions.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj.entityId,
|
||||
rgb_color: [color.r, color.g, color.b]
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</polymer-element>
|
||||
|
@ -0,0 +1,212 @@
|
||||
<link rel='import' href='../bower_components/polymer/polymer.html'>
|
||||
|
||||
<link rel='import' href='../bower_components/paper-icon-button/paper-icon-button.html'>
|
||||
|
||||
<dom-module id='more-info-media_player'>
|
||||
<style>
|
||||
.media-state {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
paper-icon-button[highlight] {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.volume {
|
||||
margin-bottom: 8px;
|
||||
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
transition: max-height .5s ease-in;
|
||||
}
|
||||
|
||||
.has-volume_level .volume {
|
||||
max-height: 40px;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class$='[[computeClassNames(stateObj)]]'>
|
||||
<div class='layout horizontal'>
|
||||
<div class='flex'>
|
||||
<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='[[!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' hidden$='[[!supportsVolumeSet]]'>
|
||||
<paper-icon-button on-tap="handleVolumeTap"
|
||||
icon="[[computeMuteVolumeIcon(isMuted)]]"></paper-icon-button>
|
||||
<paper-slider disabled$='[[isMuted]]'
|
||||
min='0' max='100' value='[[volumeSliderValue]]'
|
||||
on-change='volumeSliderChanged' class='flex'>
|
||||
</paper-slider>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var serviceActions = window.hass.serviceActions;
|
||||
var uiUtil = window.hass.uiUtil;
|
||||
var ATTRIBUTE_CLASSES = ['volume_level'];
|
||||
|
||||
Polymer({
|
||||
is: 'more-info-media_player',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: 'stateObjChanged',
|
||||
},
|
||||
|
||||
isOff: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
isPlaying: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
isMuted: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
},
|
||||
|
||||
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.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() {
|
||||
this.fire('iron-resize');
|
||||
}.bind(this), 500);
|
||||
},
|
||||
|
||||
computeClassNames: function(stateObj) {
|
||||
return uiUtil.attributeClassNames(stateObj, ATTRIBUTE_CLASSES);
|
||||
},
|
||||
|
||||
computeIsOff: function(stateObj) {
|
||||
return stateObj.state == 'off';
|
||||
},
|
||||
|
||||
computeMuteVolumeIcon: function(isMuted) {
|
||||
return isMuted ? 'av:volume-off' : 'av:volume-up';
|
||||
},
|
||||
|
||||
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.isOff ? 'turn_on' : 'turn_off');
|
||||
},
|
||||
|
||||
handlePrevious: function() {
|
||||
this.callService('media_previous_track');
|
||||
},
|
||||
|
||||
handlePlaybackControl: function() {
|
||||
if (this.isPlaying && !this.supportsPause) {
|
||||
alert('This case is not supported yet');
|
||||
}
|
||||
this.callService('media_play_pause');
|
||||
},
|
||||
|
||||
handleNext: function() {
|
||||
this.callService('media_next_track');
|
||||
},
|
||||
|
||||
handleVolumeTap: function() {
|
||||
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_level: vol });
|
||||
},
|
||||
|
||||
callService: function(service, data) {
|
||||
data = data || {};
|
||||
data.entity_id = this.stateObj.entityId;
|
||||
serviceActions.callService('media_player', service, data);
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
@ -1,22 +1,26 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/core-style/core-style.html">
|
||||
<link rel='import' href='../bower_components/polymer/polymer.html'>
|
||||
|
||||
<polymer-element name="more-info-script" attributes="stateObj" noscript>
|
||||
<template>
|
||||
<core-style ref='ha-key-value-table'></core-style>
|
||||
<style>
|
||||
.data-entry .value {
|
||||
max-width: 200px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div layout vertical>
|
||||
<div layout justified horizontal class='data-entry'>
|
||||
<div class='key'>Last Action</div>
|
||||
<div class='value'>
|
||||
{{stateObj.attributes.last_action}}
|
||||
<dom-module id='more-info-script'>
|
||||
<template>
|
||||
<div class='layout vertical'>
|
||||
<div class='data-entry layout justified horizontal'>
|
||||
<div class='key'>Last Action</div>
|
||||
<div class='value'>[[stateObj.attributes.last_action]]</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</polymer-element>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
Polymer({
|
||||
is: 'more-info-script',
|
||||
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
@ -1,53 +1,71 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/core-style/core-style.html">
|
||||
|
||||
<link rel="import" href="../components/relative-ha-datetime.html">
|
||||
|
||||
<polymer-element name="more-info-sun" attributes="stateObj">
|
||||
<template>
|
||||
<core-style ref='ha-key-value-table'></core-style>
|
||||
|
||||
<div layout vertical id='sunData'>
|
||||
|
||||
<div layout justified horizontal class='data-entry' id='rising'>
|
||||
<dom-module id="more-info-sun">
|
||||
<template>
|
||||
<div class='data-entry layout justified horizontal' id='rising'>
|
||||
<div class='key'>
|
||||
Rising <relative-ha-datetime datetimeObj="{{rising}}"></relative-ha-datetime>
|
||||
</div>
|
||||
<div class='value'>
|
||||
{{rising | formatTime}}
|
||||
Rising <relative-ha-datetime datetime-obj="[[risingDate]]"></relative-ha-datetime>
|
||||
</div>
|
||||
<div class='value'>[[risingTime]]</div>
|
||||
</div>
|
||||
|
||||
<div layout justified horizontal class='data-entry' id='setting'>
|
||||
<div class='data-entry layout justified horizontal' id='setting'>
|
||||
<div class='key'>
|
||||
Setting <relative-ha-datetime datetimeObj="{{setting}}"></relative-ha-datetime>
|
||||
</div>
|
||||
<div class='value'>
|
||||
{{setting | formatTime}}
|
||||
Setting <relative-ha-datetime datetime-obj="[[settingDate]]"></relative-ha-datetime>
|
||||
</div>
|
||||
<div class='value'>[[settingTime]]</div>
|
||||
</div>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
(function() {
|
||||
var parseDateTime = window.hass.util.parseDateTime;
|
||||
var parseDateTime = window.hass.util.parseDateTime;
|
||||
var formatTime = window.hass.uiUtil.formatTime;
|
||||
|
||||
Polymer({
|
||||
rising: null,
|
||||
setting: null,
|
||||
Polymer({
|
||||
is: 'more-info-sun',
|
||||
|
||||
stateObjChanged: function() {
|
||||
this.rising = parseDateTime(this.stateObj.attributes.next_rising);
|
||||
this.setting = parseDateTime(this.stateObj.attributes.next_setting);
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: 'stateObjChanged',
|
||||
},
|
||||
|
||||
if(self.rising > self.setting) {
|
||||
this.$.sunData.appendChild(this.$.rising);
|
||||
} else {
|
||||
this.$.sunData.appendChild(this.$.setting);
|
||||
risingDate: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
settingDate: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
risingTime: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
settingTime: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
|
||||
stateObjChanged: function() {
|
||||
this.risingDate = parseDateTime(this.stateObj.attributes.next_rising);
|
||||
this.risingTime = formatTime(this.risingDate);
|
||||
|
||||
this.settingDate = parseDateTime(this.stateObj.attributes.next_setting);
|
||||
this.settingTime = formatTime(this.settingDate);
|
||||
|
||||
var root = Polymer.dom(this);
|
||||
|
||||
if(self.risingDate > self.settingDate) {
|
||||
root.appendChild(this.$.rising);
|
||||
} else {
|
||||
root.appendChild(this.$.setting);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</polymer-element>
|
||||
|
@ -1,114 +1,128 @@
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/paper-slider/paper-slider.html">
|
||||
<link rel="import" href="../bower_components/paper-toggle-button/paper-toggle-button.html">
|
||||
<link rel='import' href='../bower_components/polymer/polymer.html'>
|
||||
<link rel='import' href='../bower_components/paper-slider/paper-slider.html'>
|
||||
<link rel='import' href='../bower_components/paper-toggle-button/paper-toggle-button.html'>
|
||||
|
||||
<polymer-element name="more-info-thermostat" attributes="stateObj">
|
||||
<template>
|
||||
<dom-module id='more-info-thermostat'>
|
||||
<style>
|
||||
paper-slider {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
paper-slider::shadow #sliderKnobInner,
|
||||
paper-slider::shadow #sliderBar::shadow #activeProgress {
|
||||
background-color: #039be5;
|
||||
}
|
||||
|
||||
.away-mode-toggle {
|
||||
display: none;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
:host-context(.has-away_mode) .away-mode-toggle {
|
||||
.has-away_mode .away-mode-toggle {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class$='[[computeClassNames(stateObj)]]'>
|
||||
<div>
|
||||
<div>Target Temperature</div>
|
||||
<paper-slider
|
||||
min='[[tempMin]]' max='[[tempMax]]'
|
||||
value='[[targetTemperatureSliderValue]]' pin
|
||||
on-change='targetTemperatureSliderChanged'>
|
||||
</paper-slider>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<div>Target Temperature</div>
|
||||
<paper-slider
|
||||
min="{{tempMin}}" max="{{tempMax}}"
|
||||
value='{{targetTemperatureSliderValue}}' pin
|
||||
on-change="{{targetTemperatureSliderChanged}}">
|
||||
</paper-slider>
|
||||
</div>
|
||||
|
||||
<div class='away-mode-toggle'>
|
||||
<div center horizontal layout>
|
||||
<div flex>Away Mode</div>
|
||||
<paper-toggle-button
|
||||
checked="{{awayToggleChecked}}"
|
||||
on-change="{{toggleChanged}}">
|
||||
</paper-toggle-button>
|
||||
<div class='away-mode-toggle'>
|
||||
<div class='center horizontal layout'>
|
||||
<div class='flex'>Away Mode</div>
|
||||
<paper-toggle-button checked='[[awayToggleChecked]]' on-change='toggleChanged'>
|
||||
</paper-toggle-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<script>
|
||||
var constants = window.hass.constants;
|
||||
(function() {
|
||||
var constants = window.hass.constants;
|
||||
var serviceActions = window.hass.serviceActions;
|
||||
var uiUtil = window.hass.uiUtil;
|
||||
var ATTRIBUTE_CLASSES = ['away_mode'];
|
||||
|
||||
Polymer({
|
||||
tempMin: 10,
|
||||
tempMax: 40,
|
||||
targetTemperatureSliderValue: 0,
|
||||
Polymer({
|
||||
is: 'more-info-thermostat',
|
||||
|
||||
awayToggleChecked: false,
|
||||
properties: {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
observer: 'stateObjChanged',
|
||||
},
|
||||
|
||||
observe: {
|
||||
'stateObj.attributes.away_mode': 'awayChanged'
|
||||
},
|
||||
tempMin: {
|
||||
type: Number,
|
||||
},
|
||||
|
||||
stateObjChanged: function(oldVal, newVal) {
|
||||
this.targetTemperatureSliderValue = this.stateObj.state;
|
||||
tempMax: {
|
||||
type: Number,
|
||||
},
|
||||
|
||||
if (this.stateObj.attributes.unit_of_measurement === constants.UNIT_TEMP_F) {
|
||||
this.tempMin = 45;
|
||||
this.tempMax = 95;
|
||||
} else {
|
||||
this.tempMin = 7;
|
||||
this.tempMax = 35;
|
||||
}
|
||||
},
|
||||
targetTemperatureSliderValue: {
|
||||
type: Number,
|
||||
},
|
||||
|
||||
targetTemperatureSliderChanged: function(ev, details, target) {
|
||||
var temp = parseInt(target.value);
|
||||
awayToggleChecked: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
|
||||
if(isNaN(temp)) return;
|
||||
stateObjChanged: function(newVal, oldVal) {
|
||||
this.targetTemperatureSliderValue = this.stateObj.state;
|
||||
this.awayToggleChecked = this.stateObj.attributes.away_mode == 'on';
|
||||
|
||||
serviceActions.callService("thermostat", "set_temperature", {
|
||||
entity_id: this.stateObj.entityId,
|
||||
temperature: temp
|
||||
});
|
||||
},
|
||||
if (this.stateObj.attributes.unit_of_measurement === constants.UNIT_TEMP_F) {
|
||||
this.tempMin = 45;
|
||||
this.tempMax = 95;
|
||||
} else {
|
||||
this.tempMin = 7;
|
||||
this.tempMax = 35;
|
||||
}
|
||||
},
|
||||
|
||||
toggleChanged: function(ev) {
|
||||
var newVal = ev.target.checked;
|
||||
computeClassNames: function(stateObj) {
|
||||
return uiUtil.attributeClassNames(stateObj, ATTRIBUTE_CLASSES);
|
||||
},
|
||||
|
||||
if(newVal && this.stateObj.attributes.away_mode === 'off') {
|
||||
this.service_set_away(true);
|
||||
} else if(!newVal && this.stateObj.attributes.away_mode === 'on') {
|
||||
this.service_set_away(false);
|
||||
}
|
||||
},
|
||||
targetTemperatureSliderChanged: function(ev) {
|
||||
var temp = parseInt(ev.target.value);
|
||||
|
||||
awayChanged: function(oldVal, newVal) {
|
||||
this.awayToggleChecked = newVal == 'on';
|
||||
},
|
||||
if(isNaN(temp)) return;
|
||||
|
||||
service_set_away: function(away_mode) {
|
||||
// We call stateChanged after a successful call to re-sync the toggle
|
||||
// with the state. It will be out of sync if our service call did not
|
||||
// result in the entity to be turned on. Since the state is not changing,
|
||||
// the resync is not called automatic.
|
||||
serviceActions.callService(
|
||||
'thermostat', 'set_away_mode',
|
||||
{entity_id: this.stateObj.entityId, away_mode: away_mode})
|
||||
serviceActions.callService('thermostat', 'set_temperature', {
|
||||
entity_id: this.stateObj.entityId,
|
||||
temperature: temp
|
||||
});
|
||||
},
|
||||
|
||||
.then(function() {
|
||||
this.awayChanged(null, this.stateObj.attributes.away_mode);
|
||||
}.bind(this));
|
||||
},
|
||||
});
|
||||
toggleChanged: function(ev) {
|
||||
var newVal = ev.target.checked;
|
||||
|
||||
if(newVal && this.stateObj.attributes.away_mode === 'off') {
|
||||
this.service_set_away(true);
|
||||
} else if(!newVal && this.stateObj.attributes.away_mode === 'on') {
|
||||
this.service_set_away(false);
|
||||
}
|
||||
},
|
||||
|
||||
service_set_away: function(away_mode) {
|
||||
// We call stateChanged after a successful call to re-sync the toggle
|
||||
// with the state. It will be out of sync if our service call did not
|
||||
// result in the entity to be turned on. Since the state is not changing,
|
||||
// the resync is not called automatic.
|
||||
serviceActions.callService(
|
||||
'thermostat', 'set_away_mode',
|
||||
{entity_id: this.stateObj.entityId, away_mode: away_mode})
|
||||
|
||||
.then(function() {
|
||||
this.stateObjChanged(this.stateObj);
|
||||
}.bind(this));
|
||||
},
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</polymer-element>
|
||||
|
@ -1,24 +1,22 @@
|
||||
<link rel="import" href="../bower_components/core-icon/core-icon.html">
|
||||
<link rel="import" href="../bower_components/core-iconset-svg/core-iconset-svg.html">
|
||||
<link rel="import" href="../bower_components/iron-iconset-svg/iron-iconset-svg.html">
|
||||
|
||||
<link rel="import" href="../bower_components/core-icon/core-icon.html">
|
||||
<link rel="import" href="../bower_components/core-icons/social-icons.html">
|
||||
<link rel="import" href="../bower_components/core-icons/image-icons.html">
|
||||
<link rel="import" href="../bower_components/core-icons/hardware-icons.html">
|
||||
<link rel="import" href="../bower_components/core-icons/av-icons.html">
|
||||
<link rel="import" href="../bower_components/iron-icons/iron-icons.html">
|
||||
<link rel="import" href="../bower_components/iron-icons/social-icons.html">
|
||||
<link rel="import" href="../bower_components/iron-icons/image-icons.html">
|
||||
<link rel="import" href="../bower_components/iron-icons/hardware-icons.html">
|
||||
<link rel="import" href="../bower_components/iron-icons/av-icons.html">
|
||||
|
||||
<core-iconset-svg id="homeassistant-100" iconSize="100">
|
||||
<iron-iconset-svg name="homeassistant-100" size="100">
|
||||
<svg><defs>
|
||||
<g id="thermostat">
|
||||
<!--
|
||||
Thermostat icon created by Scott Lewis from the Noun Project
|
||||
Licensed under CC BY 3.0 - http://creativecommons.org/licenses/by/3.0/us/
|
||||
-->
|
||||
<path d="M66.861,60.105V17.453c0-9.06-7.347-16.405-16.408-16.405c-9.06,0-16.404,7.345-16.404,16.405v42.711 c-4.04,4.14-6.533,9.795-6.533,16.035c0,12.684,10.283,22.967,22.967,22.967c12.682,0,22.964-10.283,22.964-22.967 C73.447,69.933,70.933,64.254,66.861,60.105z M60.331,20.38h-13.21v6.536h6.63v6.539h-6.63v6.713h6.63v6.538h-6.63v6.5h6.63v6.536 h-6.63v7.218c-3.775,1.373-6.471,4.993-6.471,9.24h-6.626c0-5.396,2.598-10.182,6.61-13.185V17.446c0-0.038,0.004-0.075,0.004-0.111 l-0.004-0.007c0-5.437,4.411-9.846,9.849-9.846c5.438,0,9.848,4.409,9.848,9.846V20.38z"/></g>
|
||||
<g id="thermostat"><path d="M66.861,60.105V17.453c0-9.06-7.347-16.405-16.408-16.405c-9.06,0-16.404,7.345-16.404,16.405v42.711 c-4.04,4.14-6.533,9.795-6.533,16.035c0,12.684,10.283,22.967,22.967,22.967c12.682,0,22.964-10.283,22.964-22.967 C73.447,69.933,70.933,64.254,66.861,60.105z M60.331,20.38h-13.21v6.536h6.63v6.539h-6.63v6.713h6.63v6.538h-6.63v6.5h6.63v6.536 h-6.63v7.218c-3.775,1.373-6.471,4.993-6.471,9.24h-6.626c0-5.396,2.598-10.182,6.61-13.185V17.446c0-0.038,0.004-0.075,0.004-0.111 l-0.004-0.007c0-5.437,4.411-9.846,9.849-9.846c5.438,0,9.848,4.409,9.848,9.846V20.38z"/></g>
|
||||
</defs></svg>
|
||||
</core-iconset-svg>
|
||||
</iron-iconset-svg>
|
||||
|
||||
<core-iconset-svg id="homeassistant-24" iconSize="24">
|
||||
<iron-iconset-svg name="homeassistant-24" size="24">
|
||||
<svg><defs>
|
||||
<!--
|
||||
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
|
||||
@ -29,9 +27,8 @@
|
||||
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
||||
-->
|
||||
<g id="group"><path d="M9 12c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm5-3c0-1.1-.9-2-2-2s-2 .9-2 2 .9 2 2 2 2-.9 2-2zm-2-7c-5.52 0-10 4.48-10 10s4.48 10 10 10 10-4.48 10-10-4.48-10-10-10zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm3-8c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></g>
|
||||
|
||||
</defs></svg>
|
||||
</core-iconset-svg>
|
||||
</iron-iconset-svg>
|
||||
|
||||
<script>
|
||||
window.hass.uiUtil.domainIcon = function(domain, state) {
|
||||
@ -51,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";
|
||||
}
|
||||
|
||||
@ -88,7 +85,7 @@ window.hass.uiUtil.domainIcon = function(domain, state) {
|
||||
return 'social:pages';
|
||||
|
||||
default:
|
||||
return "bookmark-outline";
|
||||
return "bookmark";
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -2,9 +2,12 @@
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var DOMAINS_WITH_CARD = ['thermostat', 'configurator', 'scene'];
|
||||
var DOMAINS_WITH_CARD = ['thermostat', 'configurator', 'scene', 'media_player'];
|
||||
var DOMAINS_WITH_MORE_INFO = [
|
||||
'light', 'group', 'sun', 'configurator', 'thermostat', 'script'
|
||||
'light', 'group', 'sun', 'configurator', 'thermostat', 'script', 'media_player'
|
||||
];
|
||||
var DOMAINS_HIDE_MORE_INFO = [
|
||||
'sensor',
|
||||
];
|
||||
|
||||
// Add some frontend specific helpers to the models
|
||||
@ -12,24 +15,16 @@
|
||||
// how to render the card for this state
|
||||
cardType: {
|
||||
get: function() {
|
||||
if(DOMAINS_WITH_CARD.indexOf(this.domain) !== -1) {
|
||||
return this.domain;
|
||||
} else if(this.canToggle) {
|
||||
return "toggle";
|
||||
} else {
|
||||
return "display";
|
||||
}
|
||||
console.warn('Deprecated method. Please use hass.uiUtil.stateCardType');
|
||||
return window.hass.uiUtil.stateCardType(this);
|
||||
}
|
||||
},
|
||||
|
||||
// how to render the more info of this state
|
||||
moreInfoType: {
|
||||
get: function() {
|
||||
if(DOMAINS_WITH_MORE_INFO.indexOf(this.domain) !== -1) {
|
||||
return this.domain;
|
||||
} else {
|
||||
return 'default';
|
||||
}
|
||||
console.warn('Deprecated method. Please use hass.uiUtil.stateMoreInfoType');
|
||||
return window.hass.uiUtil.stateMoreInfoType(this);
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -52,7 +47,7 @@
|
||||
window.hass.uiActions = {
|
||||
showMoreInfoDialog: function(entityId) {
|
||||
dispatcher.dispatch({
|
||||
actionType: this.ACTION_SHOW_DIALOG_MORE_INFO,
|
||||
actionType: window.hass.uiConstants.ACTION_SHOW_DIALOG_MORE_INFO,
|
||||
entityId: entityId,
|
||||
});
|
||||
},
|
||||
@ -66,7 +61,34 @@
|
||||
};
|
||||
|
||||
// UI specific util methods
|
||||
window.hass.uiUtil = {};
|
||||
window.hass.uiUtil = {
|
||||
stateCardType: function(state) {
|
||||
if(DOMAINS_WITH_CARD.indexOf(state.domain) !== -1) {
|
||||
return state.domain;
|
||||
} else if(state.canToggle) {
|
||||
return "toggle";
|
||||
} else {
|
||||
return "display";
|
||||
}
|
||||
},
|
||||
|
||||
stateMoreInfoType: function(state) {
|
||||
if(DOMAINS_HIDE_MORE_INFO.indexOf(state.domain) !== -1) {
|
||||
return false;
|
||||
} else if(DOMAINS_WITH_MORE_INFO.indexOf(state.domain) !== -1) {
|
||||
return state.domain;
|
||||
} else {
|
||||
return 'default';
|
||||
}
|
||||
},
|
||||
|
||||
attributeClassNames: function(stateObj, attributes) {
|
||||
if (!stateObj) return '';
|
||||
return attributes.map(function(attribute) {
|
||||
return attribute in stateObj.attributes ? 'has-' + attribute : '';
|
||||
}).join(' ');
|
||||
},
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
@ -1,31 +1,23 @@
|
||||
<link rel="import" href="../bower_components/core-style/core-style.html">
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
|
||||
<core-style id='ha-main'>
|
||||
/* Palette generated by Material Palette - materialpalette.com/light-blue/orange */
|
||||
<style is="custom-style">
|
||||
:root {
|
||||
--dark-primary-color: #0288D1;
|
||||
--default-primary-color: #03A9F4;
|
||||
--light-primary-color: #B3E5FC;
|
||||
--text-primary-color: #ffffff;
|
||||
--accent-color: #FF9800;
|
||||
--primary-background-color: #ffffff;
|
||||
--primary-text-color: #212121;
|
||||
--secondary-text-color: #727272;
|
||||
--disabled-text-color: #bdbdbd;
|
||||
--divider-color: #B6B6B6;
|
||||
|
||||
.dark-primary-color { background: #0288D1; }
|
||||
.default-primary-color { background: #03A9F4; }
|
||||
.light-primary-color { background: #B3E5FC; }
|
||||
.text-primary-color { color: #FFFFFF; }
|
||||
.accent-color { background: #FF9800; }
|
||||
.primary-text-color { color: #212121; }
|
||||
.secondary-text-color { color: #727272; }
|
||||
.divider-color { border-color: #B6B6B6; }
|
||||
--paper-toggle-button-checked-ink-color: #039be5;
|
||||
--paper-toggle-button-checked-button-color: #039be5;
|
||||
--paper-toggle-button-checked-bar-color: #039be5;
|
||||
}
|
||||
|
||||
/* extra */
|
||||
.accent-text-color { color: #FF9800; }
|
||||
|
||||
body {
|
||||
color: #212121;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #FF9800;
|
||||
text-decoration: none;
|
||||
}
|
||||
</core-style>
|
||||
|
||||
<core-style id='ha-animations'>
|
||||
@-webkit-keyframes ha-spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
@ -47,106 +39,8 @@ a {
|
||||
}
|
||||
}
|
||||
|
||||
.ha-spin {
|
||||
body /deep/ .ha-spin {
|
||||
-webkit-animation: ha-spin 2s infinite linear;
|
||||
animation: ha-spin 2s infinite linear;
|
||||
}
|
||||
</core-style>
|
||||
<core-style id="ha-headers">
|
||||
core-scroll-header-panel, core-header-panel {
|
||||
background-color: #E5E5E5;
|
||||
}
|
||||
|
||||
core-toolbar {
|
||||
background: #03a9f4;
|
||||
color: white;
|
||||
font-weight: normal;
|
||||
}
|
||||
</core-style>
|
||||
|
||||
<core-style id="ha-dialog">
|
||||
:host {
|
||||
font-family: RobotoDraft, 'Helvetica Neue', Helvetica, Arial;
|
||||
|
||||
min-width: 350px;
|
||||
max-width: 700px;
|
||||
|
||||
/* First two are from core-transition-bottom */
|
||||
transition:
|
||||
transform 0.2s ease-in-out,
|
||||
opacity 0.2s ease-in,
|
||||
top .3s,
|
||||
left .3s !important;
|
||||
}
|
||||
|
||||
:host .sidebar {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 620px) {
|
||||
:host.two-column {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
max-height: calc(100% - 64px);
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
:host .sidebar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 464px) {
|
||||
:host {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
max-height: calc(100% - 64px);
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
html /deep/ .ha-form paper-input {
|
||||
display: block;
|
||||
}
|
||||
|
||||
html /deep/ .ha-form paper-input:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
</core-style>
|
||||
|
||||
<core-style id='ha-key-value-table'>
|
||||
.data-entry {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.data-entry:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.data-entry .key {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.data-entry .value {
|
||||
text-align: right;
|
||||
word-break: break-all;
|
||||
}
|
||||
</core-style>
|
||||
|
||||
<core-style id='ha-paper-toggle'>
|
||||
paper-toggle-button::shadow .toggle-ink {
|
||||
color: #039be5;
|
||||
}
|
||||
|
||||
paper-toggle-button::shadow [checked] .toggle-bar {
|
||||
background-color: #039be5;
|
||||
}
|
||||
|
||||
paper-toggle-button::shadow [checked] .toggle-button {
|
||||
background-color: #039be5;
|
||||
}
|
||||
</core-style>
|
||||
</style>
|
||||
|
@ -17,8 +17,4 @@ window.hass.uiUtil.formatDate = function(dateObj) {
|
||||
return moment(dateObj).format('ll');
|
||||
};
|
||||
|
||||
PolymerExpressions.prototype.formatTime = window.hass.uiUtil.formatTime;
|
||||
PolymerExpressions.prototype.formatDateTime = window.hass.uiUtil.formatDateTime;
|
||||
PolymerExpressions.prototype.formatDate = window.hass.uiUtil.formatDate;
|
||||
|
||||
</script>
|
||||
|
@ -0,0 +1,21 @@
|
||||
<script>
|
||||
|
||||
(function() {
|
||||
|
||||
var StoreListenerMixIn = window.hass.storeListenerMixIn;
|
||||
|
||||
window.StoreListenerBehavior = {
|
||||
|
||||
attached: function() {
|
||||
StoreListenerMixIn.listenToStores(true, this);
|
||||
},
|
||||
|
||||
detached: function() {
|
||||
StoreListenerMixIn.stopListeningToStores(this);
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
</script>
|
13
homeassistant/components/frontend/www_static/webcomponents-lite.min.js
vendored
Normal file
13
homeassistant/components/frontend/www_static/webcomponents-lite.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -77,7 +77,12 @@ import logging
|
||||
import time
|
||||
import gzip
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
from datetime import timedelta
|
||||
from homeassistant.util import Throttle
|
||||
from http.server import SimpleHTTPRequestHandler, HTTPServer
|
||||
from http import cookies
|
||||
from socketserver import ThreadingMixIn
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
@ -90,6 +95,7 @@ from homeassistant.const import (
|
||||
HTTP_NOT_FOUND, HTTP_METHOD_NOT_ALLOWED, HTTP_UNPROCESSABLE_ENTITY)
|
||||
import homeassistant.remote as rem
|
||||
import homeassistant.util as util
|
||||
import homeassistant.util.dt as date_util
|
||||
import homeassistant.bootstrap as bootstrap
|
||||
|
||||
DOMAIN = "http"
|
||||
@ -99,9 +105,15 @@ CONF_API_PASSWORD = "api_password"
|
||||
CONF_SERVER_HOST = "server_host"
|
||||
CONF_SERVER_PORT = "server_port"
|
||||
CONF_DEVELOPMENT = "development"
|
||||
CONF_SESSIONS_ENABLED = "sessions_enabled"
|
||||
|
||||
DATA_API_PASSWORD = 'api_password'
|
||||
|
||||
# Throttling time in seconds for expired sessions check
|
||||
MIN_SEC_SESSION_CLEARING = timedelta(seconds=20)
|
||||
SESSION_TIMEOUT_SECONDS = 1800
|
||||
SESSION_KEY = 'sessionId'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -125,9 +137,11 @@ def setup(hass, config=None):
|
||||
|
||||
development = str(config[DOMAIN].get(CONF_DEVELOPMENT, "")) == "1"
|
||||
|
||||
sessions_enabled = config[DOMAIN].get(CONF_SESSIONS_ENABLED, True)
|
||||
|
||||
server = HomeAssistantHTTPServer(
|
||||
(server_host, server_port), RequestHandler, hass, api_password,
|
||||
development, no_password_set)
|
||||
development, no_password_set, sessions_enabled)
|
||||
|
||||
hass.bus.listen_once(
|
||||
ha.EVENT_HOMEASSISTANT_START,
|
||||
@ -140,6 +154,7 @@ def setup(hass, config=None):
|
||||
return True
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
|
||||
""" Handle HTTP requests in a threaded fashion. """
|
||||
# pylint: disable=too-few-public-methods
|
||||
@ -149,7 +164,8 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, server_address, request_handler_class,
|
||||
hass, api_password, development, no_password_set):
|
||||
hass, api_password, development, no_password_set,
|
||||
sessions_enabled):
|
||||
super().__init__(server_address, request_handler_class)
|
||||
|
||||
self.server_address = server_address
|
||||
@ -158,6 +174,7 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
|
||||
self.development = development
|
||||
self.no_password_set = no_password_set
|
||||
self.paths = []
|
||||
self.sessions = SessionStore(sessions_enabled)
|
||||
|
||||
# We will lazy init this one if needed
|
||||
self.event_forwarder = None
|
||||
@ -197,6 +214,11 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||
|
||||
server_version = "HomeAssistant/1.0"
|
||||
|
||||
def __init__(self, req, client_addr, server):
|
||||
""" Contructor, call the base constructor and set up session """
|
||||
self._session = None
|
||||
SimpleHTTPRequestHandler.__init__(self, req, client_addr, server)
|
||||
|
||||
def _handle_request(self, method): # pylint: disable=too-many-branches
|
||||
""" Does some common checks and calls appropriate method. """
|
||||
url = urlparse(self.path)
|
||||
@ -225,6 +247,7 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||
"Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
|
||||
self._session = self.get_session()
|
||||
if self.server.no_password_set:
|
||||
api_password = self.server.api_password
|
||||
else:
|
||||
@ -233,6 +256,10 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||
if not api_password and DATA_API_PASSWORD in data:
|
||||
api_password = data[DATA_API_PASSWORD]
|
||||
|
||||
if not api_password and self._session is not None:
|
||||
api_password = self._session.cookie_values.get(
|
||||
CONF_API_PASSWORD)
|
||||
|
||||
if '_METHOD' in data:
|
||||
method = data.pop('_METHOD')
|
||||
|
||||
@ -271,6 +298,10 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||
"API password missing or incorrect.", HTTP_UNAUTHORIZED)
|
||||
|
||||
else:
|
||||
if self._session is None and require_auth:
|
||||
self._session = self.server.sessions.create(
|
||||
api_password)
|
||||
|
||||
handle_request_method(self, path_match, data)
|
||||
|
||||
elif path_matched_but_not_method:
|
||||
@ -313,6 +344,8 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||
if location:
|
||||
self.send_header('Location', location)
|
||||
|
||||
self.set_session_cookie_header()
|
||||
|
||||
self.end_headers()
|
||||
|
||||
if data is not None:
|
||||
@ -342,6 +375,7 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||
self.send_header(HTTP_HEADER_CONTENT_TYPE, content_type)
|
||||
|
||||
self.set_cache_header()
|
||||
self.set_session_cookie_header()
|
||||
|
||||
if do_gzip:
|
||||
gzip_data = gzip.compress(inp.read())
|
||||
@ -377,3 +411,113 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||
self.send_header(
|
||||
HTTP_HEADER_EXPIRES,
|
||||
self.date_time_string(time.time()+cache_time))
|
||||
|
||||
def set_session_cookie_header(self):
|
||||
""" Add the header for the session cookie """
|
||||
if self.server.sessions.enabled and self._session is not None:
|
||||
existing_sess_id = self.get_current_session_id()
|
||||
|
||||
if existing_sess_id != self._session.session_id:
|
||||
self.send_header(
|
||||
'Set-Cookie',
|
||||
SESSION_KEY+'='+self._session.session_id)
|
||||
|
||||
def get_session(self):
|
||||
""" Get the requested session object from cookie value """
|
||||
if self.server.sessions.enabled is not True:
|
||||
return None
|
||||
|
||||
session_id = self.get_current_session_id()
|
||||
if session_id is not None:
|
||||
session = self.server.sessions.get(session_id)
|
||||
if session is not None:
|
||||
session.reset_expiry()
|
||||
return session
|
||||
|
||||
return None
|
||||
|
||||
def get_current_session_id(self):
|
||||
"""
|
||||
Extracts the current session id from the
|
||||
cookie or returns None if not set
|
||||
"""
|
||||
cookie = cookies.SimpleCookie()
|
||||
|
||||
if self.headers.get('Cookie', None) is not None:
|
||||
cookie.load(self.headers.get("Cookie"))
|
||||
|
||||
if cookie.get(SESSION_KEY, False):
|
||||
return cookie[SESSION_KEY].value
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class ServerSession:
|
||||
""" A very simple session class """
|
||||
def __init__(self, session_id):
|
||||
""" Set up the expiry time on creation """
|
||||
self._expiry = 0
|
||||
self.reset_expiry()
|
||||
self.cookie_values = {}
|
||||
self.session_id = session_id
|
||||
|
||||
def reset_expiry(self):
|
||||
""" Resets the expiry based on current time """
|
||||
self._expiry = date_util.utcnow() + timedelta(
|
||||
seconds=SESSION_TIMEOUT_SECONDS)
|
||||
|
||||
@property
|
||||
def is_expired(self):
|
||||
""" Return true if the session is expired based on the expiry time """
|
||||
return self._expiry < date_util.utcnow()
|
||||
|
||||
|
||||
class SessionStore:
|
||||
""" Responsible for storing and retrieving http sessions """
|
||||
def __init__(self, enabled=True):
|
||||
""" Set up the session store """
|
||||
self._sessions = {}
|
||||
self.enabled = enabled
|
||||
self.session_lock = threading.RLock()
|
||||
|
||||
@Throttle(MIN_SEC_SESSION_CLEARING)
|
||||
def remove_expired(self):
|
||||
""" Remove any expired sessions. """
|
||||
if self.session_lock.acquire(False):
|
||||
try:
|
||||
keys = []
|
||||
for key in self._sessions.keys():
|
||||
keys.append(key)
|
||||
|
||||
for key in keys:
|
||||
if self._sessions[key].is_expired:
|
||||
del self._sessions[key]
|
||||
_LOGGER.info("Cleared expired session %s", key)
|
||||
finally:
|
||||
self.session_lock.release()
|
||||
|
||||
def add(self, key, session):
|
||||
""" Add a new session to the list of tracked sessions """
|
||||
self.remove_expired()
|
||||
with self.session_lock:
|
||||
self._sessions[key] = session
|
||||
|
||||
def get(self, key):
|
||||
""" get a session by key """
|
||||
self.remove_expired()
|
||||
session = self._sessions.get(key, None)
|
||||
if session is not None and session.is_expired:
|
||||
return None
|
||||
return session
|
||||
|
||||
def create(self, api_password):
|
||||
""" Creates a new session and adds it to the sessions """
|
||||
if self.enabled is not True:
|
||||
return None
|
||||
|
||||
chars = string.ascii_letters + string.digits
|
||||
session_id = ''.join([random.choice(chars) for i in range(20)])
|
||||
session = ServerSession(session_id)
|
||||
session.cookie_values[CONF_API_PASSWORD] = api_password
|
||||
self.add(session_id, session)
|
||||
return session
|
||||
|
@ -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))
|
||||
|
||||
|
@ -77,7 +77,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
controller = veraApi.VeraController(base_url)
|
||||
devices = []
|
||||
try:
|
||||
devices = controller.get_devices('Switch')
|
||||
devices = controller.get_devices(['Switch', 'On/Off Switch'])
|
||||
except RequestException:
|
||||
# There was a network related error connecting to the vera controller
|
||||
_LOGGER.exception("Error communicating with Vera API")
|
||||
|
@ -2,7 +2,7 @@
|
||||
homeassistant.components.media_player
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Component to interface with various media players
|
||||
Component to interface with various media players.
|
||||
"""
|
||||
import logging
|
||||
|
||||
@ -10,9 +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_VOLUME_UP,
|
||||
SERVICE_VOLUME_DOWN, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY,
|
||||
SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREV_TRACK)
|
||||
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_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK)
|
||||
|
||||
DOMAIN = 'media_player'
|
||||
DEPENDENCIES = []
|
||||
@ -26,26 +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_DURATION = 'media_duration'
|
||||
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):
|
||||
@ -54,10 +101,17 @@ 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)
|
||||
|
||||
|
||||
def turn_on(hass, entity_id=None):
|
||||
""" Will turn on specified media player or all. """
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
|
||||
hass.services.call(DOMAIN, SERVICE_TURN_ON, data)
|
||||
|
||||
|
||||
def turn_off(hass, entity_id=None):
|
||||
""" Will turn off specified media player or all. """
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
@ -79,6 +133,26 @@ def volume_down(hass, entity_id=None):
|
||||
hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN, data)
|
||||
|
||||
|
||||
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 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)
|
||||
|
||||
|
||||
def media_play_pause(hass, entity_id=None):
|
||||
""" Send the media player the command for play/pause. """
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
@ -107,22 +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_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',
|
||||
}
|
||||
hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@ -148,6 +211,57 @@ def setup(hass, config):
|
||||
for service in SERVICE_TO_METHOD:
|
||||
hass.services.register(DOMAIN, service, media_player_service_handler)
|
||||
|
||||
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.set_volume_level(volume)
|
||||
|
||||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_VOLUME_SET, volume_set_service)
|
||||
|
||||
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.mute_volume(mute)
|
||||
|
||||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
|
||||
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. """
|
||||
target_players = component.extract_from_service(service)
|
||||
@ -156,6 +270,9 @@ def setup(hass, config):
|
||||
for player in target_players:
|
||||
player.play_youtube(media_id)
|
||||
|
||||
if player.should_poll:
|
||||
player.update_ha_state(True)
|
||||
|
||||
hass.services.register(DOMAIN, "start_fireplace",
|
||||
lambda service:
|
||||
play_youtube_video_service(service, "eyU3bRy2x44"))
|
||||
@ -174,35 +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 the media player on. """
|
||||
raise NotImplementedError()
|
||||
|
||||
def turn_off(self):
|
||||
""" turn_off media player. """
|
||||
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 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_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
|
||||
|
@ -12,18 +12,23 @@ try:
|
||||
import pychromecast
|
||||
import pychromecast.controllers.youtube as youtube
|
||||
except ImportError:
|
||||
# We will throw error later
|
||||
pass
|
||||
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,
|
||||
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
|
||||
@ -31,21 +36,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Sets up the cast platform. """
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
# pylint: disable=redefined-outer-name
|
||||
import pychromecast
|
||||
except ImportError:
|
||||
logger.exception(("Failed to import pychromecast. "
|
||||
"Did you maybe not install the 'pychromecast' "
|
||||
"dependency?"))
|
||||
if pychromecast is None:
|
||||
logger.error((
|
||||
"Failed to import pychromecast. Did you maybe not install the "
|
||||
"'pychromecast' dependency?"))
|
||||
|
||||
return
|
||||
return False
|
||||
|
||||
if discovery_info:
|
||||
hosts = [discovery_info[0]]
|
||||
|
||||
else:
|
||||
hosts = pychromecast.discover_chromecasts()
|
||||
hosts = (host_port[0] for host_port
|
||||
in pychromecast.discover_chromecasts())
|
||||
|
||||
casts = []
|
||||
|
||||
@ -61,100 +64,201 @@ 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()
|
||||
self.cast.register_handler(self.youtube)
|
||||
|
||||
self.cast.socket_client.receiver_controller.register_status_listener(
|
||||
self)
|
||||
self.cast.socket_client.media_controller.register_status_listener(self)
|
||||
|
||||
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
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" 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_controller = self.cast.media_controller
|
||||
media_status = media_controller.status
|
||||
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,
|
||||
@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. """
|
||||
# The only way we can turn the Chromecast is on is by launching an app
|
||||
if not self.cast.status or not self.cast.status.is_active_input:
|
||||
if self.cast.app_id:
|
||||
self.cast.quit_app()
|
||||
|
||||
self.cast.play_media(
|
||||
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 mute_volume(self, mute):
|
||||
""" mute the volume. """
|
||||
self.cast.set_volume_muted(mute)
|
||||
|
||||
def volume_down(self):
|
||||
""" Service to send the chromecast the command for volume down. """
|
||||
self.cast.volume_down()
|
||||
|
||||
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 set_volume_level(self, volume):
|
||||
""" set volume level, range 0..1. """
|
||||
self.cast.set_volume(volume)
|
||||
|
||||
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 play_youtube_video(self, video_id):
|
||||
""" Plays specified video_id on the Chromecast's YouTube channel. """
|
||||
self.youtube.play_video(video_id)
|
||||
def media_previous_track(self):
|
||||
""" Send previous track command. """
|
||||
self.cast.media_controller.rewind()
|
||||
|
||||
def media_next_track(self):
|
||||
""" Send next track command. """
|
||||
self.cast.media_controller.skip()
|
||||
|
||||
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. """
|
||||
self.cast_status = status
|
||||
self.update_ha_state()
|
||||
|
||||
def new_media_status(self, status):
|
||||
""" Called when a new media status is received. """
|
||||
self.media_status = status
|
||||
self.update_ha_state()
|
||||
|
@ -5,101 +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)
|
||||
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._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_ENTITY_PICTURE:
|
||||
YOUTUBE_COVER_URL_FORMAT.format(self.youtube_id)
|
||||
}
|
||||
@property
|
||||
def is_volume_muted(self):
|
||||
""" Boolean if volume is currently muted. """
|
||||
return self._volume_muted
|
||||
|
||||
if self.is_playing:
|
||||
state_attr[ATTR_MEDIA_STATE] = MEDIA_STATE_PLAYING
|
||||
else:
|
||||
state_attr[ATTR_MEDIA_STATE] = MEDIA_STATE_STOPPED
|
||||
|
||||
return state_attr
|
||||
def turn_on(self):
|
||||
""" 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
|
||||
def mute_volume(self, mute):
|
||||
""" mute the volume. """
|
||||
self._volume_muted = mute
|
||||
self.update_ha_state()
|
||||
|
||||
def volume_down(self):
|
||||
""" volume_down media player. """
|
||||
if self.volume > 0:
|
||||
self.volume -= 0.1
|
||||
|
||||
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()
|
||||
|
206
homeassistant/components/media_player/mpd.py
Normal file
206
homeassistant/components/media_player/mpd.py
Normal file
@ -0,0 +1,206 @@
|
||||
"""
|
||||
homeassistant.components.media_player.mpd
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Provides functionality to interact with a Music Player Daemon.
|
||||
|
||||
Configuration:
|
||||
|
||||
To use MPD you will need to add something like the following to your
|
||||
config/configuration.yaml
|
||||
|
||||
media_player:
|
||||
platform: mpd
|
||||
server: 127.0.0.1
|
||||
port: 6600
|
||||
location: bedroom
|
||||
|
||||
Variables:
|
||||
|
||||
server
|
||||
*Required
|
||||
IP address of the Music Player Daemon. Example: 192.168.1.32
|
||||
|
||||
port
|
||||
*Optional
|
||||
Port of the Music Player Daemon, defaults to 6600. Example: 6600
|
||||
|
||||
location
|
||||
*Optional
|
||||
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,
|
||||
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. """
|
||||
|
||||
daemon = config.get('server', None)
|
||||
port = config.get('port', 6600)
|
||||
location = config.get('location', 'MPD')
|
||||
|
||||
if mpd is None:
|
||||
_LOGGER.exception(
|
||||
"Unable to import mpd2. "
|
||||
"Did you maybe not install the 'python-mpd2' package?")
|
||||
|
||||
return False
|
||||
|
||||
# pylint: disable=no-member
|
||||
try:
|
||||
mpd_client = mpd.MPDClient()
|
||||
mpd_client.connect(daemon, port)
|
||||
mpd_client.close()
|
||||
mpd_client.disconnect()
|
||||
except socket.error:
|
||||
_LOGGER.error(
|
||||
"Unable to connect to MPD. "
|
||||
"Please check your settings")
|
||||
|
||||
return False
|
||||
|
||||
add_devices([MpdDevice(daemon, port, location)])
|
||||
|
||||
|
||||
class MpdDevice(MediaPlayerDevice):
|
||||
""" Represents a MPD server. """
|
||||
|
||||
# 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.status = None
|
||||
self.currentsong = None
|
||||
|
||||
self.client = mpd.MPDClient()
|
||||
self.client.timeout = 10
|
||||
self.client.idletimeout = None
|
||||
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
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the media state. """
|
||||
if self.status['state'] == 'play':
|
||||
return STATE_PLAYING
|
||||
elif self.status['state'] == 'pause':
|
||||
return STATE_PAUSED
|
||||
else:
|
||||
return STATE_OFF
|
||||
|
||||
@property
|
||||
def media_content_id(self):
|
||||
""" Content ID of current playing media. """
|
||||
return self.currentsong['id']
|
||||
|
||||
@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. """
|
||||
# Time does not exist for streams
|
||||
return self.currentsong.get('time')
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
""" Title of current playing media. """
|
||||
return self.currentsong['title']
|
||||
|
||||
@property
|
||||
def media_artist(self):
|
||||
""" Artist of current playing media. (Music track only) """
|
||||
return self.currentsong.get('artist')
|
||||
|
||||
@property
|
||||
def media_album_name(self):
|
||||
""" Album of current playing media. (Music track only) """
|
||||
return self.currentsong.get('album')
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
return int(self.status['volume'])/100
|
||||
|
||||
@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 = int(self.status['volume'])
|
||||
|
||||
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 = int(self.status['volume'])
|
||||
|
||||
if current_volume >= 0:
|
||||
self.client.setvol(current_volume - 5)
|
||||
|
||||
def media_play(self):
|
||||
""" Service to send the MPD the command for play/pause. """
|
||||
self.client.start()
|
||||
|
||||
def media_pause(self):
|
||||
""" Service to send the MPD the command for play/pause. """
|
||||
self.client.pause()
|
||||
|
||||
def media_next_track(self):
|
||||
""" Service to send the MPD the command for next track. """
|
||||
self.client.next()
|
||||
|
||||
def media_previous_track(self):
|
||||
""" Service to send the MPD the command for previous track. """
|
||||
self.client.previous()
|
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()
|
250
homeassistant/components/sensor/bitcoin.py
Normal file
250
homeassistant/components/sensor/bitcoin.py
Normal file
@ -0,0 +1,250 @@
|
||||
"""
|
||||
homeassistant.components.sensor.bitcoin
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Bitcoin information service that uses blockchain.info and its online wallet.
|
||||
|
||||
Configuration:
|
||||
|
||||
You need to enable the API access for your online wallet to get the balance.
|
||||
To do that log in and move to 'Account Setting', choose 'IP Restrictions', and
|
||||
check 'Enable Api Access'. You will get an email message from blockchain.info
|
||||
where you must authorize the API access.
|
||||
|
||||
To use the Bitcoin sensor you will need to add something like the following
|
||||
to your config/configuration.yaml
|
||||
|
||||
sensor:
|
||||
platform: bitcoin
|
||||
wallet: 'YOUR WALLET_ID'
|
||||
password: YOUR_ACCOUNT_PASSWORD
|
||||
currency: YOUR CURRENCY
|
||||
display_options:
|
||||
- exchangerate
|
||||
- trade_volume_btc
|
||||
- miners_revenue_usd
|
||||
- btc_mined
|
||||
- trade_volume_usd
|
||||
- difficulty
|
||||
- minutes_between_blocks
|
||||
- number_of_transactions
|
||||
- hash_rate
|
||||
- timestamp
|
||||
- mined_blocks
|
||||
- blocks_size
|
||||
- total_fees_btc
|
||||
- total_btc_sent
|
||||
- estimated_btc_sent
|
||||
- total_btc
|
||||
- total_blocks
|
||||
- next_retarget
|
||||
- estimated_transaction_volume_usd
|
||||
- miners_revenue_btc
|
||||
- market_price_usd
|
||||
|
||||
Variables:
|
||||
|
||||
wallet
|
||||
*Optional
|
||||
This is your wallet identifier from https://blockchain.info to access the
|
||||
online wallet.
|
||||
|
||||
password
|
||||
*Optional
|
||||
Password your your online wallet.
|
||||
|
||||
currency
|
||||
*Optional
|
||||
The currency to exchange to, eg. CHF, USD, EUR,etc. Default is USD.
|
||||
|
||||
display_options
|
||||
*Optional
|
||||
An array specifying the variables to display.
|
||||
|
||||
These are the variables for the display_options array. See the configuration
|
||||
example above for a list of all available variables.
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
OPTION_TYPES = {
|
||||
'wallet': ['Wallet balance', 'BTC'],
|
||||
'exchangerate': ['Exchange rate (1 BTC)', ''],
|
||||
'trade_volume_btc': ['Trade volume', 'BTC'],
|
||||
'miners_revenue_usd': ['Miners revenue', 'USD'],
|
||||
'btc_mined': ['Mined', 'BTC'],
|
||||
'trade_volume_usd': ['Trade volume', 'USD'],
|
||||
'difficulty': ['Difficulty', ''],
|
||||
'minutes_between_blocks': ['Time between Blocks', 'min'],
|
||||
'number_of_transactions': ['No. of Transactions', ''],
|
||||
'hash_rate': ['Hash rate', 'PH/s'],
|
||||
'timestamp': ['Timestamp', ''],
|
||||
'mined_blocks': ['Minded Blocks', ''],
|
||||
'blocks_size': ['Block size', ''],
|
||||
'total_fees_btc': ['Total fees', 'BTC'],
|
||||
'total_btc_sent': ['Total sent', 'BTC'],
|
||||
'estimated_btc_sent': ['Estimated sent', 'BTC'],
|
||||
'total_btc': ['Total', 'BTC'],
|
||||
'total_blocks': ['Total Blocks', ''],
|
||||
'next_retarget': ['Next retarget', ''],
|
||||
'estimated_transaction_volume_usd': ['Est. Transaction volume', 'USD'],
|
||||
'miners_revenue_btc': ['Miners revenue', 'BTC'],
|
||||
'market_price_usd': ['Market price', 'USD']
|
||||
}
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Get the Bitcoin sensor. """
|
||||
|
||||
try:
|
||||
from blockchain.wallet import Wallet
|
||||
from blockchain import exchangerates, exceptions
|
||||
|
||||
except ImportError:
|
||||
_LOGGER.exception(
|
||||
"Unable to import blockchain. "
|
||||
"Did you maybe not install the 'blockchain' package?")
|
||||
|
||||
return None
|
||||
|
||||
wallet_id = config.get('wallet', None)
|
||||
password = config.get('password', None)
|
||||
currency = config.get('currency', 'USD')
|
||||
|
||||
if currency not in exchangerates.get_ticker():
|
||||
_LOGGER.error('Currency "%s" is not available. Using "USD".', currency)
|
||||
currency = 'USD'
|
||||
|
||||
wallet = Wallet(wallet_id, password)
|
||||
|
||||
try:
|
||||
wallet.get_balance()
|
||||
except exceptions.APIException as error:
|
||||
_LOGGER.error(error)
|
||||
wallet = None
|
||||
|
||||
data = BitcoinData()
|
||||
dev = []
|
||||
if wallet is not None and password is not None:
|
||||
dev.append(BitcoinSensor(data, 'wallet', currency, wallet))
|
||||
|
||||
for variable in config['display_options']:
|
||||
if variable not in OPTION_TYPES:
|
||||
_LOGGER.error('Option type: "%s" does not exist', variable)
|
||||
else:
|
||||
dev.append(BitcoinSensor(data, variable, currency))
|
||||
|
||||
add_devices(dev)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class BitcoinSensor(Entity):
|
||||
""" Implements a Bitcoin sensor. """
|
||||
|
||||
def __init__(self, data, option_type, currency, wallet=''):
|
||||
self.data = data
|
||||
self._name = OPTION_TYPES[option_type][0]
|
||||
self._unit_of_measurement = OPTION_TYPES[option_type][1]
|
||||
self._currency = currency
|
||||
self._wallet = wallet
|
||||
self.type = option_type
|
||||
self._state = None
|
||||
self.update()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Returns the name of the device. """
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
return self._unit_of_measurement
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def update(self):
|
||||
""" Gets the latest data and updates the states. """
|
||||
|
||||
self.data.update()
|
||||
stats = self.data.stats
|
||||
ticker = self.data.ticker
|
||||
|
||||
# pylint: disable=no-member
|
||||
if self.type == 'wallet' and self._wallet is not None:
|
||||
self._state = '{0:.8f}'.format(self._wallet.get_balance() *
|
||||
0.00000001)
|
||||
elif self.type == 'exchangerate':
|
||||
self._state = ticker[self._currency].p15min
|
||||
self._unit_of_measurement = self._currency
|
||||
elif self.type == 'trade_volume_btc':
|
||||
self._state = '{0:.1f}'.format(stats.trade_volume_btc)
|
||||
elif self.type == 'miners_revenue_usd':
|
||||
self._state = '{0:.0f}'.format(stats.miners_revenue_usd)
|
||||
elif self.type == 'btc_mined':
|
||||
self._state = '{}'.format(stats.btc_mined * 0.00000001)
|
||||
elif self.type == 'trade_volume_usd':
|
||||
self._state = '{0:.1f}'.format(stats.trade_volume_usd)
|
||||
elif self.type == 'difficulty':
|
||||
self._state = '{0:.0f}'.format(stats.difficulty)
|
||||
elif self.type == 'minutes_between_blocks':
|
||||
self._state = '{0:.2f}'.format(stats.minutes_between_blocks)
|
||||
elif self.type == 'number_of_transactions':
|
||||
self._state = '{}'.format(stats.number_of_transactions)
|
||||
elif self.type == 'hash_rate':
|
||||
self._state = '{0:.1f}'.format(stats.hash_rate * 0.000001)
|
||||
elif self.type == 'timestamp':
|
||||
self._state = stats.timestamp
|
||||
elif self.type == 'mined_blocks':
|
||||
self._state = '{}'.format(stats.mined_blocks)
|
||||
elif self.type == 'blocks_size':
|
||||
self._state = '{0:.1f}'.format(stats.blocks_size)
|
||||
elif self.type == 'total_fees_btc':
|
||||
self._state = '{0:.2f}'.format(stats.total_fees_btc * 0.00000001)
|
||||
elif self.type == 'total_btc_sent':
|
||||
self._state = '{0:.2f}'.format(stats.total_btc_sent * 0.00000001)
|
||||
elif self.type == 'estimated_btc_sent':
|
||||
self._state = '{0:.2f}'.format(stats.estimated_btc_sent *
|
||||
0.00000001)
|
||||
elif self.type == 'total_btc':
|
||||
self._state = '{0:.2f}'.format(stats.total_btc * 0.00000001)
|
||||
elif self.type == 'total_blocks':
|
||||
self._state = '{0:.2f}'.format(stats.total_blocks)
|
||||
elif self.type == 'next_retarget':
|
||||
self._state = '{0:.2f}'.format(stats.next_retarget)
|
||||
elif self.type == 'estimated_transaction_volume_usd':
|
||||
self._state = '{0:.2f}'.format(
|
||||
stats.estimated_transaction_volume_usd)
|
||||
elif self.type == 'miners_revenue_btc':
|
||||
self._state = '{0:.1f}'.format(stats.miners_revenue_btc *
|
||||
0.00000001)
|
||||
elif self.type == 'market_price_usd':
|
||||
self._state = '{0:.2f}'.format(stats.market_price_usd)
|
||||
|
||||
|
||||
class BitcoinData(object):
|
||||
""" Gets the latest data and updates the states. """
|
||||
|
||||
def __init__(self):
|
||||
self.stats = None
|
||||
self.ticker = None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
""" Gets the latest data from blockchain.info. """
|
||||
|
||||
from blockchain import statistics, exchangerates
|
||||
|
||||
self.stats = statistics.get()
|
||||
self.ticker = exchangerates.get_ticker()
|
@ -13,44 +13,44 @@ sensor:
|
||||
platform: openweathermap
|
||||
api_key: YOUR_APP_KEY
|
||||
monitored_variables:
|
||||
- type: 'weather'
|
||||
- type: 'temperature'
|
||||
- type: 'wind_speed'
|
||||
- type: 'humidity'
|
||||
- type: 'pressure'
|
||||
- type: 'clouds'
|
||||
- type: 'rain'
|
||||
- type: 'snow'
|
||||
- weather
|
||||
- temperature
|
||||
- wind_speed
|
||||
- humidity
|
||||
- pressure
|
||||
- clouds
|
||||
- rain
|
||||
- snow
|
||||
|
||||
VARIABLES:
|
||||
Variables:
|
||||
|
||||
api_key
|
||||
*Required
|
||||
To retrieve this value log into your account at http://openweathermap.org/
|
||||
|
||||
monitored_variables
|
||||
monitored_conditions
|
||||
*Required
|
||||
An array specifying the variables to monitor.
|
||||
|
||||
These are the variables for the monitored_variables array:
|
||||
These are the variables for the monitored_conditions array:
|
||||
|
||||
type
|
||||
*Required
|
||||
The variable you wish to monitor, see the configuration example above for a
|
||||
list of all available variables
|
||||
list of all available conditions to monitor.
|
||||
|
||||
Details for the API : http://bugs.openweathermap.org/projects/api/wiki
|
||||
|
||||
Only metric measurements are supported at the moment.
|
||||
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.const import (CONF_API_KEY, TEMP_CELCIUS, TEMP_FAHRENHEIT)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_THROTTLED_REFRESH = None
|
||||
SENSOR_TYPES = {
|
||||
'weather': ['Condition', ''],
|
||||
'temperature': ['Temperature', ''],
|
||||
@ -62,6 +62,9 @@ SENSOR_TYPES = {
|
||||
'snow': ['Snow', 'mm']
|
||||
}
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Get the OpenWeatherMap sensor. """
|
||||
@ -83,7 +86,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
SENSOR_TYPES['temperature'][1] = hass.config.temperature_unit
|
||||
unit = hass.config.temperature_unit
|
||||
owm = OWM(config.get(CONF_API_KEY, None))
|
||||
obs = owm.weather_at_coords(hass.config.latitude, hass.config.longitude)
|
||||
|
||||
if not owm:
|
||||
_LOGGER.error(
|
||||
@ -91,12 +93,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"Please check your settings for OpenWeatherMap.")
|
||||
return None
|
||||
|
||||
data = WeatherData(owm, hass.config.latitude, hass.config.longitude)
|
||||
dev = []
|
||||
for variable in config['monitored_variables']:
|
||||
if variable['type'] not in SENSOR_TYPES:
|
||||
_LOGGER.error('Sensor type: "%s" does not exist', variable['type'])
|
||||
for variable in config['monitored_conditions']:
|
||||
if variable not in SENSOR_TYPES:
|
||||
_LOGGER.error('Sensor type: "%s" does not exist', variable)
|
||||
else:
|
||||
dev.append(OpenWeatherMapSensor(variable['type'], obs, unit))
|
||||
dev.append(OpenWeatherMapSensor(data, variable, unit))
|
||||
|
||||
add_devices(dev)
|
||||
|
||||
@ -105,7 +108,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class OpenWeatherMapSensor(Entity):
|
||||
""" Implements an OpenWeatherMap sensor. """
|
||||
|
||||
def __init__(self, sensor_type, weather_data, unit):
|
||||
def __init__(self, weather_data, sensor_type, unit):
|
||||
self.client_name = 'Weather - '
|
||||
self._name = SENSOR_TYPES[sensor_type][0]
|
||||
self.owa_client = weather_data
|
||||
@ -132,12 +135,13 @@ class OpenWeatherMapSensor(Entity):
|
||||
# pylint: disable=too-many-branches
|
||||
def update(self):
|
||||
""" Gets the latest data from OWM and updates the states. """
|
||||
data = self.owa_client.get_weather()
|
||||
|
||||
self.owa_client.update()
|
||||
data = self.owa_client.data
|
||||
|
||||
if self.type == 'weather':
|
||||
self._state = data.get_detailed_status()
|
||||
|
||||
if self.type == 'temperature':
|
||||
elif self.type == 'temperature':
|
||||
if self._unit == TEMP_CELCIUS:
|
||||
self._state = round(data.get_temperature('celsius')['temp'],
|
||||
1)
|
||||
@ -146,29 +150,40 @@ class OpenWeatherMapSensor(Entity):
|
||||
1)
|
||||
else:
|
||||
self._state = round(data.get_temperature()['temp'], 1)
|
||||
|
||||
elif self.type == 'wind_speed':
|
||||
self._state = data.get_wind()['speed']
|
||||
|
||||
elif self.type == 'humidity':
|
||||
self._state = data.get_humidity()
|
||||
|
||||
elif self.type == 'pressure':
|
||||
self._state = round(data.get_pressure()['press'], 0)
|
||||
|
||||
elif self.type == 'clouds':
|
||||
self._state = data.get_clouds()
|
||||
|
||||
elif self.type == 'rain':
|
||||
if data.get_rain():
|
||||
self._state = round(data.get_rain()['3h'], 0)
|
||||
else:
|
||||
self._state = 'not raining'
|
||||
self._unit_of_measurement = ''
|
||||
|
||||
elif self.type == 'snow':
|
||||
if data.get_snow():
|
||||
self._state = round(data.get_snow(), 0)
|
||||
else:
|
||||
self._state = 'not snowing'
|
||||
self._unit_of_measurement = ''
|
||||
|
||||
|
||||
class WeatherData(object):
|
||||
""" Gets the latest data from OpenWeatherMap. """
|
||||
|
||||
def __init__(self, owm, latitude, longitude):
|
||||
self.owm = owm
|
||||
self.latitude = latitude
|
||||
self.longitude = longitude
|
||||
self.data = None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
""" Gets the latest data from OpenWeatherMap. """
|
||||
|
||||
obs = self.owm.weather_at_coords(self.latitude, self.longitude)
|
||||
self.data = obs.get_weather()
|
||||
|
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_short_time_str(
|
||||
dt_util.as_local(dt_util.utc_from_timestamp(
|
||||
item['from']['departureTimestamp']))
|
||||
)
|
||||
for item in connections
|
||||
]
|
||||
except KeyError:
|
||||
return ['n/a']
|
@ -51,8 +51,8 @@ it should be set to "true" if you want this device excluded
|
||||
|
||||
"""
|
||||
import logging
|
||||
import time
|
||||
from requests.exceptions import RequestException
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import (
|
||||
@ -146,11 +146,12 @@ class VeraSensor(Entity):
|
||||
|
||||
if self.vera_device.is_trippable:
|
||||
last_tripped = self.vera_device.refresh_value('LastTrip')
|
||||
trip_time_str = time.strftime(
|
||||
"%Y-%m-%d %H:%M",
|
||||
time.localtime(int(last_tripped))
|
||||
)
|
||||
attr[ATTR_LAST_TRIP_TIME] = trip_time_str
|
||||
if last_tripped is not None:
|
||||
utc_time = dt_util.utc_from_timestamp(int(last_tripped))
|
||||
attr[ATTR_LAST_TRIP_TIME] = dt_util.datetime_to_str(
|
||||
utc_time)
|
||||
else:
|
||||
attr[ATTR_LAST_TRIP_TIME] = None
|
||||
tripped = self.vera_device.refresh_value('Tripped')
|
||||
attr[ATTR_TRIPPED] = 'True' if tripped == '1' else 'False'
|
||||
|
||||
|
@ -4,13 +4,13 @@ homeassistant.components.sun
|
||||
|
||||
Provides functionality to keep track of the sun.
|
||||
|
||||
|
||||
Event listener
|
||||
--------------
|
||||
The suns event listener will call the service
|
||||
when the sun rises or sets with an offset.
|
||||
The sun evnt need to have the type 'sun', which service to call,
|
||||
which event (sunset or sunrise) and the offset.
|
||||
The suns event listener will call the service when the sun rises or sets with
|
||||
an offset.
|
||||
|
||||
The sun event need to have the type 'sun', which service to call, which event
|
||||
(sunset or sunrise) and the offset.
|
||||
|
||||
{
|
||||
"type": "sun",
|
||||
@ -18,8 +18,6 @@ which event (sunset or sunrise) and the offset.
|
||||
"event": "sunset",
|
||||
"offset": "-01:00:00"
|
||||
}
|
||||
|
||||
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
@ -240,12 +238,12 @@ class SunEventListener(ServiceEventListener):
|
||||
return next_time
|
||||
|
||||
def schedule_next_event(self, hass, next_event):
|
||||
""" Schedule the event """
|
||||
""" Schedule the event. """
|
||||
next_time = self.__get_next_time(next_event)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def execute(now):
|
||||
""" Call the execute method """
|
||||
""" Call the execute method. """
|
||||
self.execute(hass)
|
||||
|
||||
hass.track_point_in_time(execute, next_time)
|
||||
@ -272,7 +270,7 @@ class SunriseEventListener(SunEventListener):
|
||||
""" This class is used the call a service when the sun rises. """
|
||||
|
||||
def schedule(self, hass):
|
||||
""" Schedule the event """
|
||||
""" Schedule the event. """
|
||||
next_rising_dt = next_rising(hass)
|
||||
|
||||
next_time_dt = self.schedule_next_event(hass, next_rising_dt)
|
||||
|
81
homeassistant/components/switch/command_switch.py
Normal file
81
homeassistant/components/switch/command_switch.py
Normal file
@ -0,0 +1,81 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
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
|
||||
import subprocess
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Find and return switches controlled by shell commands. """
|
||||
|
||||
switches = config.get('switches', {})
|
||||
devices = []
|
||||
|
||||
for dev_name, properties in switches.items():
|
||||
devices.append(
|
||||
CommandSwitch(
|
||||
dev_name,
|
||||
properties.get('oncmd', 'true'),
|
||||
properties.get('offcmd', 'true')))
|
||||
|
||||
add_devices_callback(devices)
|
||||
|
||||
|
||||
class CommandSwitch(ToggleEntity):
|
||||
""" 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._command_on = command_on
|
||||
self._command_off = command_off
|
||||
|
||||
@staticmethod
|
||||
def _switch(command):
|
||||
""" Execute the actual commands """
|
||||
_LOGGER.info('Running command: %s', command)
|
||||
|
||||
success = (subprocess.call(command, shell=True) == 0)
|
||||
|
||||
if not success:
|
||||
_LOGGER.error('Command failed: %s', command)
|
||||
|
||||
return success
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed """
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" 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
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
if CommandSwitch._switch(self._command_on):
|
||||
self._state = STATE_ON
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
if CommandSwitch._switch(self._command_off):
|
||||
self._state = STATE_OFF
|
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
|
135
homeassistant/components/switch/transmission.py
Normal file
135
homeassistant/components/switch/transmission.py
Normal file
@ -0,0 +1,135 @@
|
||||
"""
|
||||
homeassistant.components.switch.transmission
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Enable or disable Transmission BitTorrent client Turtle Mode
|
||||
|
||||
Configuration:
|
||||
|
||||
To use the Transmission switch you will need to add something like the
|
||||
following to your config/configuration.yaml
|
||||
|
||||
switch:
|
||||
platform: transmission
|
||||
name: Transmission
|
||||
host: 192.168.1.26
|
||||
port: 9091
|
||||
username: YOUR_USERNAME
|
||||
password: YOUR_PASSWORD
|
||||
|
||||
Variables:
|
||||
|
||||
host
|
||||
*Required
|
||||
This is the IP address of your Transmission daemon. Example: 192.168.1.32
|
||||
|
||||
port
|
||||
*Optional
|
||||
The port your Transmission daemon uses, defaults to 9091. Example: 8080
|
||||
|
||||
username
|
||||
*Optional
|
||||
Your Transmission username, if you use authentication
|
||||
|
||||
password
|
||||
*Optional
|
||||
Your Transmission username, if you use authentication
|
||||
|
||||
name
|
||||
*Optional
|
||||
The name to use when displaying this Transmission instance.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
# pylint: disable=no-name-in-module, import-error
|
||||
import transmissionrpc
|
||||
|
||||
from transmissionrpc.error import TransmissionError
|
||||
|
||||
import logging
|
||||
|
||||
_LOGGING = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Sets up the sensors. """
|
||||
host = config.get(CONF_HOST)
|
||||
username = config.get(CONF_USERNAME, None)
|
||||
password = config.get(CONF_PASSWORD, None)
|
||||
port = config.get('port', 9091)
|
||||
|
||||
name = config.get("name", "Transmission Turtle Mode")
|
||||
if not host:
|
||||
_LOGGING.error('Missing config variable %s', CONF_HOST)
|
||||
return False
|
||||
|
||||
# import logging
|
||||
# logging.getLogger('transmissionrpc').setLevel(logging.DEBUG)
|
||||
|
||||
transmission_api = transmissionrpc.Client(
|
||||
host, port=port, user=username, password=password)
|
||||
try:
|
||||
transmission_api.session_stats()
|
||||
except TransmissionError:
|
||||
_LOGGING.exception("Connection to Transmission API failed.")
|
||||
return False
|
||||
|
||||
add_devices_callback([
|
||||
TransmissionSwitch(transmission_api, name)
|
||||
])
|
||||
|
||||
|
||||
class TransmissionSwitch(ToggleEntity):
|
||||
|
||||
""" A Transmission sensor. """
|
||||
|
||||
def __init__(self, transmission_client, name):
|
||||
self._name = name
|
||||
self.transmission_client = transmission_client
|
||||
self._state = STATE_OFF
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
""" Returns the state of the device. """
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" Poll for status regularly. """
|
||||
return True
|
||||
|
||||
@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 Turtle Mode")
|
||||
self.transmission_client.set_session(
|
||||
alt_speed_enabled=True)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
|
||||
_LOGGING.info("Turning off Turtle Mode ")
|
||||
self.transmission_client.set_session(
|
||||
alt_speed_enabled=False)
|
||||
|
||||
def update(self):
|
||||
""" Gets the latest data from Transmission and updates the state. """
|
||||
|
||||
active = self.transmission_client.get_session(
|
||||
).alt_speed_enabled
|
||||
self._state = STATE_ON if active else STATE_OFF
|
@ -51,6 +51,7 @@ it should be set to "true" if you want this device excluded
|
||||
import logging
|
||||
import time
|
||||
from requests.exceptions import RequestException
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.const import (
|
||||
@ -78,7 +79,8 @@ def get_devices(hass, config):
|
||||
vera_controller = veraApi.VeraController(base_url)
|
||||
devices = []
|
||||
try:
|
||||
devices = vera_controller.get_devices(['Switch', 'Armable Sensor'])
|
||||
devices = vera_controller.get_devices([
|
||||
'Switch', 'Armable Sensor', 'On/Off Switch'])
|
||||
except RequestException:
|
||||
# There was a network related error connecting to the vera controller
|
||||
_LOGGER.exception("Error communicating with Vera API")
|
||||
@ -132,11 +134,12 @@ class VeraSwitch(ToggleEntity):
|
||||
|
||||
if self.vera_device.is_trippable:
|
||||
last_tripped = self.vera_device.refresh_value('LastTrip')
|
||||
trip_time_str = time.strftime(
|
||||
"%Y-%m-%d %H:%M",
|
||||
time.localtime(int(last_tripped))
|
||||
)
|
||||
attr[ATTR_LAST_TRIP_TIME] = trip_time_str
|
||||
if last_tripped is not None:
|
||||
utc_time = dt_util.utc_from_timestamp(int(last_tripped))
|
||||
attr[ATTR_LAST_TRIP_TIME] = dt_util.datetime_to_str(
|
||||
utc_time)
|
||||
else:
|
||||
attr[ATTR_LAST_TRIP_TIME] = None
|
||||
tripped = self.vera_device.refresh_value('Tripped')
|
||||
attr[ATTR_TRIPPED] = 'True' if tripped == '1' else 'False'
|
||||
|
||||
|
@ -74,7 +74,21 @@ class NestThermostat(ThermostatDevice):
|
||||
@property
|
||||
def target_temperature(self):
|
||||
""" Returns the temperature we try to reach. """
|
||||
return round(self.device.target, 1)
|
||||
target = self.device.target
|
||||
|
||||
if isinstance(target, tuple):
|
||||
low, high = target
|
||||
|
||||
if self.current_temperature < low:
|
||||
temp = low
|
||||
elif self.current_temperature > high:
|
||||
temp = high
|
||||
else:
|
||||
temp = (low + high)/2
|
||||
else:
|
||||
temp = target
|
||||
|
||||
return round(temp, 1)
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
|
@ -19,8 +19,20 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
YAML_CONFIG_FILE = 'configuration.yaml'
|
||||
CONF_CONFIG_FILE = 'home-assistant.conf'
|
||||
|
||||
DEFAULT_CONFIG = [
|
||||
# Tuples (attribute, default, auto detect property, description)
|
||||
(CONF_NAME, 'Home', None, 'Name of the location where Home Assistant is '
|
||||
'running'),
|
||||
(CONF_LATITUDE, None, 'latitude', 'Location required to calculate the time'
|
||||
' the sun rises and sets'),
|
||||
(CONF_LONGITUDE, None, 'longitude', None),
|
||||
(CONF_TEMPERATURE_UNIT, 'C', None, 'C for Celcius, F for Fahrenheit'),
|
||||
(CONF_TIME_ZONE, 'UTC', 'time_zone', 'Pick yours from here: http://en.wiki'
|
||||
'pedia.org/wiki/List_of_tz_database_time_zones'),
|
||||
]
|
||||
DEFAULT_COMPONENTS = [
|
||||
'discovery', 'frontend', 'conversation', 'history', 'logbook']
|
||||
'discovery', 'frontend', 'conversation', 'history', 'logbook', 'sun']
|
||||
|
||||
|
||||
def ensure_config_exists(config_dir, detect_location=True):
|
||||
@ -41,29 +53,33 @@ def create_default_config(config_dir, detect_location=True):
|
||||
Returns path to new config file if success, None if failed. """
|
||||
config_path = os.path.join(config_dir, YAML_CONFIG_FILE)
|
||||
|
||||
info = {attr: default for attr, default, *_ in DEFAULT_CONFIG}
|
||||
|
||||
location_info = detect_location and util.detect_location_info()
|
||||
|
||||
if location_info:
|
||||
if location_info.use_fahrenheit:
|
||||
info[CONF_TEMPERATURE_UNIT] = 'F'
|
||||
|
||||
for attr, default, prop, _ in DEFAULT_CONFIG:
|
||||
if prop is None:
|
||||
continue
|
||||
info[attr] = getattr(location_info, prop) or default
|
||||
|
||||
# Writing files with YAML does not create the most human readable results
|
||||
# So we're hard coding a YAML template.
|
||||
try:
|
||||
with open(config_path, 'w') as config_file:
|
||||
location_info = detect_location and util.detect_location_info()
|
||||
config_file.write("homeassistant:\n")
|
||||
|
||||
if location_info:
|
||||
temp_unit = 'F' if location_info.use_fahrenheit else 'C'
|
||||
for attr, _, _, description in DEFAULT_CONFIG:
|
||||
if info[attr] is None:
|
||||
continue
|
||||
elif description:
|
||||
config_file.write(" # {}\n".format(description))
|
||||
config_file.write(" {}: {}\n".format(attr, info[attr]))
|
||||
|
||||
auto_config = {
|
||||
CONF_NAME: 'Home',
|
||||
CONF_LATITUDE: location_info.latitude,
|
||||
CONF_LONGITUDE: location_info.longitude,
|
||||
CONF_TEMPERATURE_UNIT: temp_unit,
|
||||
CONF_TIME_ZONE: location_info.time_zone,
|
||||
}
|
||||
|
||||
config_file.write("homeassistant:\n")
|
||||
|
||||
for key, value in auto_config.items():
|
||||
config_file.write(" {}: {}\n".format(key, value))
|
||||
|
||||
config_file.write("\n")
|
||||
config_file.write("\n")
|
||||
|
||||
for component in DEFAULT_COMPONENTS:
|
||||
config_file.write("{}:\n\n".format(component))
|
||||
@ -103,15 +119,32 @@ def load_yaml_config_file(config_path):
|
||||
""" Parse a YAML configuration file. """
|
||||
import yaml
|
||||
|
||||
try:
|
||||
with open(config_path) as conf_file:
|
||||
# If configuration file is empty YAML returns None
|
||||
# We convert that to an empty dict
|
||||
conf_dict = yaml.load(conf_file) or {}
|
||||
def parse(fname):
|
||||
""" Actually parse the file. """
|
||||
try:
|
||||
with open(fname) as conf_file:
|
||||
# If configuration file is empty YAML returns None
|
||||
# We convert that to an empty dict
|
||||
conf_dict = yaml.load(conf_file) or {}
|
||||
except yaml.YAMLError:
|
||||
_LOGGER.exception('Error reading YAML configuration file %s',
|
||||
fname)
|
||||
raise HomeAssistantError()
|
||||
return conf_dict
|
||||
|
||||
except yaml.YAMLError:
|
||||
_LOGGER.exception('Error reading YAML configuration file')
|
||||
raise HomeAssistantError()
|
||||
def yaml_include(loader, node):
|
||||
"""
|
||||
Loads another YAML file and embeds it using the !include tag.
|
||||
|
||||
Example:
|
||||
device_tracker: !include device_tracker.yaml
|
||||
"""
|
||||
fname = os.path.join(os.path.dirname(loader.name), node.value)
|
||||
return parse(fname)
|
||||
|
||||
yaml.add_constructor('!include', yaml_include)
|
||||
|
||||
conf_dict = parse(config_path)
|
||||
|
||||
if not isinstance(conf_dict, dict):
|
||||
_LOGGER.error(
|
||||
|
@ -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
|
||||
@ -99,11 +102,13 @@ SERVICE_TURN_OFF = 'turn_off'
|
||||
SERVICE_VOLUME_UP = "volume_up"
|
||||
SERVICE_VOLUME_DOWN = "volume_down"
|
||||
SERVICE_VOLUME_MUTE = "volume_mute"
|
||||
SERVICE_VOLUME_SET = "volume_set"
|
||||
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
|
||||
|
2
homeassistant/external/vera
vendored
2
homeassistant/external/vera
vendored
@ -1 +1 @@
|
||||
Subproject commit fedbb5c3af1e5f36b7008d894e9fc1ecf3cc2ea8
|
||||
Subproject commit 30c59781d63322db2160ff00a4b99f16ead40b85
|
@ -2,7 +2,8 @@
|
||||
Helper methods for components within Home Assistant.
|
||||
"""
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_PLATFORM, DEVICE_DEFAULT_NAME)
|
||||
from homeassistant.util import ensure_unique_string, slugify
|
||||
|
||||
# Deprecated 3/5/2015 - Moved to homeassistant.helpers.entity
|
||||
@ -12,6 +13,7 @@ from .entity import Entity as Device, ToggleEntity as ToggleDevice # noqa
|
||||
|
||||
def generate_entity_id(entity_id_format, name, current_ids=None, hass=None):
|
||||
""" Generate a unique entity ID based on given entity IDs or used ids. """
|
||||
name = name.lower() or DEVICE_DEFAULT_NAME.lower()
|
||||
if current_ids is None:
|
||||
if hass is None:
|
||||
raise RuntimeError("Missing required parameter currentids or hass")
|
||||
|
@ -1,58 +1,66 @@
|
||||
# required for Home Assistant core
|
||||
# Required for Home Assistant core
|
||||
requests>=2.0
|
||||
pyyaml>=3.11
|
||||
pytz>=2015.2
|
||||
|
||||
# optional, needed for specific components
|
||||
# Optional, needed for specific components
|
||||
|
||||
# discovery
|
||||
# Discovery platform (discovery)
|
||||
zeroconf>=0.16.0
|
||||
|
||||
# sun
|
||||
# Sun (sun)
|
||||
pyephem>=3.7
|
||||
|
||||
# lights.hue
|
||||
# Philips Hue library (lights.hue)
|
||||
phue>=0.8
|
||||
|
||||
# lights.limitlessled
|
||||
# Limitlessled/Easybulb/Milight library (lights.limitlessled)
|
||||
ledcontroller>=1.0.7
|
||||
|
||||
# media_player.cast
|
||||
pychromecast>=0.6.0.3
|
||||
# Chromecast bindings (media_player.cast)
|
||||
pychromecast>=0.6.5
|
||||
|
||||
# keyboard
|
||||
# Keyboard (keyboard)
|
||||
pyuserinput>=0.1.9
|
||||
|
||||
# switch.tellstick, sensor.tellstick
|
||||
# Tellstick bindings (*.tellstick)
|
||||
tellcore-py>=1.0.4
|
||||
|
||||
# device_tracker.nmap
|
||||
# Nmap bindings (device_tracker.nmap)
|
||||
python-libnmap>=0.6.2
|
||||
|
||||
# notify.pushbullet
|
||||
# PushBullet bindings (notify.pushbullet)
|
||||
pushbullet.py>=0.7.1
|
||||
|
||||
# thermostat.nest
|
||||
python-nest>=2.1
|
||||
# Nest Thermostat bindings (thermostat.nest)
|
||||
python-nest>=2.3.1
|
||||
|
||||
# z-wave
|
||||
# Z-Wave (*.zwave)
|
||||
pydispatcher>=2.0.5
|
||||
|
||||
# isy994
|
||||
# ISY994 bindings (*.isy994)
|
||||
PyISY>=1.0.2
|
||||
|
||||
# sensor.systemmonitor
|
||||
# PSutil (sensor.systemmonitor)
|
||||
psutil>=2.2.1
|
||||
|
||||
# pushover notifications
|
||||
# Pushover bindings (notify.pushover)
|
||||
python-pushover>=0.2
|
||||
|
||||
# Transmission Torrent Client
|
||||
# Transmission Torrent Client (*.transmission)
|
||||
transmissionrpc>=0.11
|
||||
|
||||
# OpenWeatherMap Web API
|
||||
# OpenWeatherMap Web API (sensor.openweathermap)
|
||||
pyowm>=2.2.0
|
||||
|
||||
# XMPP Bindings (notify.xmpp)
|
||||
sleekxmpp>=1.3.1
|
||||
|
||||
# Blockchain (sensor.bitcoin)
|
||||
blockchain>=1.1.2
|
||||
|
||||
# MPD Bindings (media_player.mpd)
|
||||
python-mpd2>=0.5.4
|
||||
|
||||
# Hikvision (switch.hikvisioncam)
|
||||
hikvision>=0.4
|
||||
|
@ -8,23 +8,19 @@ fi
|
||||
|
||||
scripts/build_js $1
|
||||
|
||||
# To build the frontend, you need node, bower and vulcanize
|
||||
# npm install -g bower vulcanize
|
||||
# To build the frontend, you need node, bower, vulcanize and html-minifier
|
||||
# npm install -g bower vulcanize html-minifier
|
||||
|
||||
# Install dependencies
|
||||
cd homeassistant/components/frontend/www_static/polymer
|
||||
bower install
|
||||
cd ..
|
||||
cp polymer/bower_components/webcomponentsjs/webcomponents.min.js .
|
||||
cp polymer/bower_components/webcomponentsjs/webcomponents-lite.min.js .
|
||||
|
||||
# Let Polymer refer to the minified JS version before we compile
|
||||
sed -i.bak 's/polymer\.js/polymer\.min\.js/' polymer/bower_components/polymer/polymer.html
|
||||
vulcanize --inline-css --inline-scripts --strip-comments polymer/home-assistant.html > frontend.html
|
||||
|
||||
vulcanize -o frontend.html --inline --strip polymer/home-assistant.html
|
||||
|
||||
# Revert back the change to the Polymer component
|
||||
rm polymer/bower_components/polymer/polymer.html
|
||||
mv polymer/bower_components/polymer/polymer.html.bak polymer/bower_components/polymer/polymer.html
|
||||
# html-minifier crashes on frontend, minimize kills the CSS
|
||||
# html-minifier --config-file polymer/html-minifier.conf -o frontend.html frontend.html
|
||||
|
||||
# Generate the MD5 hash of the new frontend
|
||||
cd ..
|
||||
|
@ -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))
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user