gitignore ready for emacs

This commit is contained in:
Malte Deiseroth 2015-09-16 08:34:08 +02:00
commit 8842e4e94f
275 changed files with 21134 additions and 6724 deletions

View file

@ -2,32 +2,84 @@
source = homeassistant
omit =
homeassistant/external/*
homeassistant/__main__.py
# omit pieces of code that rely on external devices being present
homeassistant/components/arduino.py
homeassistant/components/*/arduino.py
homeassistant/components/isy994.py
homeassistant/components/*/isy994.py
homeassistant/components/modbus.py
homeassistant/components/*/modbus.py
homeassistant/components/*/tellstick.py
homeassistant/components/*/vera.py
homeassistant/components/verisure.py
homeassistant/components/*/verisure.py
homeassistant/components/wink.py
homeassistant/components/*/wink.py
homeassistant/components/zwave.py
homeassistant/components/*/zwave.py
homeassistant/components/*/tellstick.py
homeassistant/components/*/vera.py
homeassistant/components/keyboard.py
homeassistant/components/switch/wemo.py
homeassistant/components/thermostat/nest.py
homeassistant/components/light/hue.py
homeassistant/components/sensor/systemmonitor.py
homeassistant/components/sensor/sabnzbd.py
homeassistant/components/notify/pushbullet.py
homeassistant/components/notify/pushover.py
homeassistant/components/media_player/cast.py
homeassistant/components/ifttt.py
homeassistant/components/browser.py
homeassistant/components/camera/*
homeassistant/components/device_tracker/actiontec.py
homeassistant/components/device_tracker/aruba.py
homeassistant/components/device_tracker/asuswrt.py
homeassistant/components/device_tracker/ddwrt.py
homeassistant/components/device_tracker/luci.py
homeassistant/components/device_tracker/tomato.py
homeassistant/components/device_tracker/netgear.py
homeassistant/components/device_tracker/nmap_tracker.py
homeassistant/components/device_tracker/ddwrt.py
homeassistant/components/device_tracker/thomson.py
homeassistant/components/device_tracker/tomato.py
homeassistant/components/device_tracker/tplink.py
homeassistant/components/discovery.py
homeassistant/components/downloader.py
homeassistant/components/keyboard.py
homeassistant/components/light/hue.py
homeassistant/components/light/limitlessled.py
homeassistant/components/media_player/cast.py
homeassistant/components/media_player/denon.py
homeassistant/components/media_player/kodi.py
homeassistant/components/media_player/mpd.py
homeassistant/components/media_player/squeezebox.py
homeassistant/components/notify/file.py
homeassistant/components/notify/instapush.py
homeassistant/components/notify/nma.py
homeassistant/components/notify/pushbullet.py
homeassistant/components/notify/pushover.py
homeassistant/components/notify/slack.py
homeassistant/components/notify/smtp.py
homeassistant/components/notify/syslog.py
homeassistant/components/notify/xmpp.py
homeassistant/components/sensor/arest.py
homeassistant/components/sensor/bitcoin.py
homeassistant/components/sensor/dht.py
homeassistant/components/sensor/efergy.py
homeassistant/components/sensor/forecast.py
homeassistant/components/sensor/mysensors.py
homeassistant/components/sensor/openweathermap.py
homeassistant/components/sensor/rfxtrx.py
homeassistant/components/sensor/rpi_gpio.py
homeassistant/components/sensor/sabnzbd.py
homeassistant/components/sensor/swiss_public_transport.py
homeassistant/components/sensor/systemmonitor.py
homeassistant/components/sensor/temper.py
homeassistant/components/sensor/time_date.py
homeassistant/components/sensor/transmission.py
homeassistant/components/switch/command_switch.py
homeassistant/components/switch/edimax.py
homeassistant/components/switch/hikvisioncam.py
homeassistant/components/switch/rpi_gpio.py
homeassistant/components/switch/transmission.py
homeassistant/components/switch/wemo.py
homeassistant/components/thermostat/nest.py
[report]

12
.gitignore vendored
View file

@ -7,6 +7,9 @@ homeassistant/components/frontend/www_static/polymer/bower_components/*
config/custom_components/*
!config/custom_components/example.py
!config/custom_components/hello_world.py
!config/custom_components/mqtt_example.py
tests/config/home-assistant.log
# Hide sublime text stuff
*.sublime-project
@ -64,5 +67,12 @@ nosetests.xml
.project
.pydevproject
# Hide emacs backups
# emacs auto backups
*~
*#
*.orig
.python-version
# venv stuff
pyvenv.cfg
pip-selfcheck.json

24
.gitmodules vendored
View file

@ -1,21 +1,3 @@
[submodule "homeassistant/external/pynetgear"]
path = homeassistant/external/pynetgear
url = https://github.com/balloob/pynetgear.git
[submodule "homeassistant/external/pywemo"]
path = homeassistant/external/pywemo
url = https://github.com/balloob/pywemo.git
[submodule "homeassistant/external/netdisco"]
path = homeassistant/external/netdisco
url = https://github.com/balloob/netdisco.git
[submodule "homeassistant/external/noop"]
path = homeassistant/external/noop
url = https://github.com/balloob/noop.git
[submodule "homeassistant/components/frontend/www_static/polymer/home-assistant-js"]
path = homeassistant/components/frontend/www_static/polymer/home-assistant-js
url = https://github.com/balloob/home-assistant-js.git
[submodule "homeassistant/external/vera"]
path = homeassistant/external/vera
url = https://github.com/jamespcole/home-assistant-vera-api.git
[submodule "homeassistant/external/nzbclients"]
path = homeassistant/external/nzbclients
url = https://github.com/jamespcole/home-assistant-nzb-clients.git
[submodule "homeassistant/components/frontend/www_static/home-assistant-polymer"]
path = homeassistant/components/frontend/www_static/home-assistant-polymer
url = https://github.com/balloob/home-assistant-polymer.git

View file

@ -1,11 +1,12 @@
sudo: false
language: python
python:
- "3.4"
install:
- pip install -r requirements.txt
- pip install -r requirements_all.txt
- pip install flake8 pylint coveralls
script:
- flake8 homeassistant --exclude bower_components,external
- flake8 homeassistant
- pylint homeassistant
- coverage run -m unittest discover tests
after_success:

View file

@ -1,24 +1,41 @@
# Adding support for a new device
# Contributing to Home Assistant
For help on building your component, please see the See the [developer documentation on home-assistant.io](https://home-assistant.io/developers/).
Everybody is invited and welcome to contribute to Home Assistant. There is a lot to do...if you are not a developer perhaps you would like to help with the documentation on [home-assistant.io](https://home-assistant.io/)? If you are a developer and have devices in your home which aren't working with Home Assistant yet, why not spent a couple of hours and help to integrate them?
The process is straight-forward.
- Fork the Home Assistant [git repository](https://github.com/balloob/home-assistant).
- Write the code for your device, notification service, sensor, or IoT thing.
- Check it with ``pylint`` and ``flake8``.
- Create a Pull Request against the [**dev**](https://github.com/balloob/home-assistant/tree/dev) branch of Home Assistant.
Still interested? Then you should read the next sections and get more details.
## Adding support for a new device
For help on building your component, please see the [developer documentation](https://home-assistant.io/developers/) on [home-assistant.io](https://home-assistant.io/).
After you finish adding support for your device:
- update the supported devices in README.md.
- add any new dependencies to requirements.txt.
- Make sure all your code passes Pylint, flake8 (PEP8 and some more) validation. To generate reports, run `pylint homeassistant > pylint.txt` and `flake8 homeassistant --exclude bower_components,external > flake8.txt`.
- Update the supported devices in the `README.md` file.
- Add any new dependencies to `requirements.txt`.
- Update the `.coveragerc` file.
- Provide some documentation for [home-assistant.io](https://home-assistant.io/). The documentation is handled in a separate [git repository](https://github.com/balloob/home-assistant.io).
- Make sure all your code passes Pylint and flake8 (PEP8 and some more) validation. To generate reports, run `pylint homeassistant > pylint.txt` and `flake8 homeassistant --exclude bower_components,external > flake8.txt`.
- Create a Pull Request against the [**dev**](https://github.com/balloob/home-assistant/tree/dev) branch of Home Assistant.
- Check for comments and suggestions on your Pull Request and keep an eye on the [Travis output](https://travis-ci.org/balloob/home-assistant/).
If you've added a component:
- update the file [`domain-icon.html`](https://github.com/balloob/home-assistant/blob/master/homeassistant/components/http/www_static/polymer/domain-icon.html) with an icon for your domain ([pick from this list](https://www.polymer-project.org/components/core-icons/demo.html))
- 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.
@ -27,15 +44,29 @@ A state can have several attributes that will help the frontend in displaying yo
- `friendly_name`: this name will be used as the name of the device
- `entity_picture`: this picture will be shown instead of the domain icon
- `unit_of_measurement`: this will be appended to the state in the interface
- `hidden`: This is a suggestion to the frontend on if the state should be hidden
These attributes are defined in [homeassistant.components](https://github.com/balloob/home-assistant/blob/master/homeassistant/components/__init__.py#L25).
## Working on the frontend
### Proper Visibility Handling
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.
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
self.hidden = True
```
This will SUGGEST that the active frontend hides the entity. This requires that the active frontend support hidden cards (the default frontend does) and that the value of hidden be included in your attributes dictionary (see above). The Entity abstract class will take care of this for you.
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
The frontend is composed of [Polymer](https://www.polymer-project.org) web-components and compiled into the file `frontend.html`. During development you do not want to work with the compiled version but with the seperate files. To have Home Assistant serve the seperate files, set `development=1` for the *http-component* in your config.
When you are done with development and ready to commit your changes, run `build_frontend`, set `development=0` in your config and validate that everything still works.
## Notes on PyLint and PEP8 validation
### Notes on PyLint and PEP8 validation
In case a PyLint warning cannot be avoided, add a comment to disable the PyLint check for that line. This can be done using the format `# pylint: disable=YOUR-ERROR-NAME`. Example of an unavoidable PyLint warning is if you do not use the passed in datetime if you're listening for time change.

View file

@ -3,10 +3,18 @@ MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
VOLUME /config
RUN pip3 install --no-cache-dir -r requirements_all.txt
# For the nmap tracker
RUN apt-get update && \
apt-get install -y cython3 libudev-dev && \
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
pip3 install cython && \
scripts/build_python_openzwave
apt-get install -y --no-install-recommends nmap net-tools && \
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Open Z-Wave disabled because broken
#RUN apt-get update && \
# apt-get install -y cython3 libudev-dev && \
# apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
# pip3 install cython && \
# scripts/build_python_openzwave
CMD [ "python", "-m", "homeassistant", "--config", "/config" ]

1
MANIFEST.in Normal file
View file

@ -0,0 +1 @@
recursive-exclude tests *

View file

@ -1,49 +1,37 @@
# Home Assistant [![Build Status](https://travis-ci.org/balloob/home-assistant.svg?branch=master)](https://travis-ci.org/balloob/home-assistant) [![Coverage Status](https://img.shields.io/coveralls/balloob/home-assistant.svg)](https://coveralls.io/r/balloob/home-assistant?branch=master)
# Home Assistant [![Build Status](https://travis-ci.org/balloob/home-assistant.svg?branch=master)](https://travis-ci.org/balloob/home-assistant) [![Coverage Status](https://img.shields.io/coveralls/balloob/home-assistant.svg)](https://coveralls.io/r/balloob/home-assistant?branch=master) [![Join the chat at https://gitter.im/balloob/home-assistant](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/balloob/home-assistant?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
This is the source code for Home Assistant. For installation instructions, tutorials and the docs, please see [the website](https://home-assistant.io). For a functioning demo frontend of Home Assistant, [click here](https://home-assistant.io/demo/).
[demo]: https://home-assistant.io/demo/
Home Assistant is a home automation platform running on Python 3. The goal of Home Assistant is to be able to track and control all devices at home and offer a platform for automating control.
It offers the following functionality through built-in components:
To get started:
```bash
python3 -m pip install homeassistant
hass --open-ui
```
* 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)
Check out [the website](https://home-assistant.io) for [a demo][demo], installation instructions, tutorials and documentation.
[![screenshot-states](https://raw.github.com/balloob/home-assistant/master/docs/screenshots.png)][demo]
Examples of devices it can interface it:
* Monitoring connected devices to a wireless router: [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index), [TPLink](http://www.tp-link.us/), and [ASUSWRT](http://event.asus.com/2013/nw/ASUSWRT/)
* [Philips Hue](http://meethue.com) lights, [WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) switches, [Edimax](http://www.edimax.com/) switches, [Efergy](https://efergy.com) energy monitoring, RFXtrx sensors, and [Tellstick](http://www.telldus.se/products/tellstick) devices and sensors
* [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/), [Logitech Squeezebox](https://en.wikipedia.org/wiki/Squeezebox_%28network_music_player%29), and [Kodi (XBMC)](http://kodi.tv/)
* 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/), [Arduino](https://www.arduino.cc/), [Raspberry Pi](https://www.raspberrypi.org/), and [Modbus](http://www.modbus.org/)
* Integrate data from the [Bitcoin](https://bitcoin.org) network, meteorological data from [OpenWeatherMap](http://openweathermap.org/) and [Forecast.io](https://forecast.io/), [Transmission](http://www.transmissionbt.com/), or [SABnzbd](http://sabnzbd.org).
* [See full list of supported devices](https://home-assistant.io/components/)
Built home automation on top of your devices:
* Keep a precise history of every change to the state of your house
* Turn on the lights when people get home after sun set
* Turn on lights slowly during sun set to compensate for light loss
* Turn on lights slowly during sun set to compensate for less light
* 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)
Home Assistant also includes functionality for controlling HTPCs:
* Simulate key presses for Play/Pause, Next track, Prev track, Volume up, Volume Down
* Download files
* Open URLs in the default browser
[![screenshot-states](https://raw.github.com/balloob/home-assistant/master/docs/screenshots.png)](https://home-assistant.io/demo/)
* Offers a [REST API](https://home-assistant.io/developers/api.html) and can interface with MQTT for easy integration with other projects
* 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/), [Slack](https://slack.com/), and [Jabber (XMPP)](http://xmpp.org)
The system is built modular so support for other devices or actions can be implemented easily. See also the [section on architecture](https://home-assistant.io/developers/architecture.html) and the [section on creating your own components](https://home-assistant.io/developers/creating_components.html).
If you run into issues while using Home Assistant or during development of a component, reach out to the [Home Assistant developer community](https://groups.google.com/forum/#!forum/home-assistant-dev).
## 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:
```python
git clone --recursive https://github.com/balloob/home-assistant.git
cd home-assistant
pip3 install -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.
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.
If you run into issues while using Home Assistant or during development of a component, check the [Home Assistant help section](https://home-assistant.io/help/) how to reach us.

View file

@ -28,7 +28,8 @@ wink:
access_token: 'YOUR_TOKEN'
device_tracker:
# The following types are available: netgear, tomato, luci, nmap_tracker
# The following types are available: ddwrt, netgear, tomato, luci,
# and nmap_tracker
platform: netgear
host: 192.168.1.1
username: admin
@ -65,9 +66,9 @@ device_sun_light_trigger:
# Optional: disable lights being turned off when everybody leaves the house
# disable_turn_off: 1
# A comma seperated list of states that have to be tracked as a single group
# 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
@ -158,5 +159,5 @@ scene:
light.tv_back_light: on
light.ceiling:
state: on
color: [0.33, 0.66]
xy_color: [0.33, 0.66]
brightness: 200

View file

@ -8,6 +8,22 @@ Example component to target an entity_id to:
- turn it off if all lights are turned off
- turn it off if all people leave the house
- offer a service to turn it on for 10 seconds
Configuration:
To use the Example custom component you will need to add the following to
your configuration.yaml file.
example:
target: TARGET_ENTITY
Variable:
target
*Required
TARGET_ENTITY should be one of your devices that can be turned on and off,
ie a light or a switch. Example value could be light.Ceiling or switch.AC
(if you have these devices with those names).
"""
import time
import logging
@ -22,7 +38,7 @@ DOMAIN = "example"
# List of component names (string) your component depends upon
# We depend on group because group will be loaded after all the components that
# initalize devices have been setup.
# initialize devices have been setup.
DEPENDENCIES = ['group']
# Configuration key for the entity id we are targetting
@ -31,6 +47,7 @@ CONF_TARGET = 'target'
# Name of the service that we expose
SERVICE_FLASH = 'flash'
# Shortcut for the logger
_LOGGER = logging.getLogger(__name__)
@ -115,5 +132,5 @@ def setup(hass, config):
# Register our service with HASS.
hass.services.register(DOMAIN, SERVICE_FLASH, flash_service)
# Tells the bootstrapper that the component was succesfully initialized
# Tells the bootstrapper that the component was successfully initialized
return True

View file

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

View file

@ -0,0 +1,59 @@
"""
custom_components.mqtt_example
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Shows how to communicate with MQTT. Follows a topic on MQTT and updates the
state of an entity to the last message received on that topic.
Also offers a service 'set_state' that will publish a message on the topic that
will be passed via MQTT to our message received listener. Call the service with
example payload {"new_state": "some new state"}.
Configuration:
To use the mqtt_example component you will need to add the following to your
configuration.yaml file.
mqtt_example:
topic: home-assistant/mqtt_example
"""
import homeassistant.loader as loader
# The domain of your component. Should be equal to the name of your component
DOMAIN = "mqtt_example"
# List of component names (string) your component depends upon
DEPENDENCIES = ['mqtt']
CONF_TOPIC = 'topic'
DEFAULT_TOPIC = 'home-assistant/mqtt_example'
def setup(hass, config):
""" Setup our mqtt_example component. """
mqtt = loader.get_component('mqtt')
topic = config[DOMAIN].get('topic', DEFAULT_TOPIC)
entity_id = 'mqtt_example.last_message'
# Listen to a message on MQTT
def message_received(topic, payload, qos):
""" A new MQTT message has been received. """
hass.states.set(entity_id, payload)
mqtt.subscribe(hass, topic, message_received)
hass.states.set(entity_id, 'No messages')
# Service to publish a message on MQTT
def set_state_service(call):
""" Service to send a message. """
mqtt.publish(hass, topic, call.data.get('new_state'))
# Register our service with Home Assistant
hass.services.register(DOMAIN, 'set_state', set_state_service)
# return boolean to indicate that initialization was successful
return True

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View file

@ -1,946 +0,0 @@
"""
homeassistant
~~~~~~~~~~~~~
Home Assistant is a Home Automation framework for observing the state
of entities and react to changes.
"""
import os
import time
import logging
import threading
import enum
import re
import datetime as dt
import functools as ft
import requests
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
SERVICE_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED,
EVENT_CALL_SERVICE, ATTR_NOW, ATTR_DOMAIN, ATTR_SERVICE, MATCH_ALL,
EVENT_SERVICE_EXECUTED, ATTR_SERVICE_CALL_ID, EVENT_SERVICE_REGISTERED,
TEMP_CELCIUS, TEMP_FAHRENHEIT)
import homeassistant.util as util
DOMAIN = "homeassistant"
# How often time_changed event should fire
TIMER_INTERVAL = 1 # seconds
# How long we wait for the result of a service call
SERVICE_CALL_LIMIT = 10 # seconds
# Define number of MINIMUM worker threads.
# During bootstrap of HA (see bootstrap.from_config_dict()) worker threads
# will be added for each component that polls devices.
MIN_WORKER_THREAD = 2
# Pattern for validating entity IDs (format: <domain>.<entity>)
ENTITY_ID_PATTERN = re.compile(r"^(?P<domain>\w+)\.(?P<entity>\w+)$")
_LOGGER = logging.getLogger(__name__)
class HomeAssistant(object):
""" Core class to route all communication to right components. """
def __init__(self):
self.pool = pool = create_worker_pool()
self.bus = EventBus(pool)
self.services = ServiceRegistry(self.bus, pool)
self.states = StateMachine(self.bus)
self.config = Config()
@property
def components(self):
""" DEPRECATED 3/21/2015. Use hass.config.components """
_LOGGER.warning(
'hass.components is deprecated. Use hass.config.components')
return self.config.components
@property
def local_api(self):
""" DEPRECATED 3/21/2015. Use hass.config.api """
_LOGGER.warning(
'hass.local_api is deprecated. Use hass.config.api')
return self.config.api
@property
def config_dir(self):
""" DEPRECATED 3/18/2015. Use hass.config.config_dir """
_LOGGER.warning(
'hass.config_dir is deprecated. Use hass.config.config_dir')
return self.config.config_dir
def get_config_path(self, path):
""" DEPRECATED 3/18/2015. Use hass.config.path """
_LOGGER.warning(
'hass.get_config_path is deprecated. Use hass.config.path')
return self.config.path(path)
def start(self):
""" Start home assistant. """
_LOGGER.info(
"Starting Home Assistant (%d threads)", self.pool.worker_count)
Timer(self)
self.bus.fire(EVENT_HOMEASSISTANT_START)
def block_till_stopped(self):
""" Will register service homeassistant/stop and
will block until called. """
request_shutdown = threading.Event()
self.services.register(DOMAIN, SERVICE_HOMEASSISTANT_STOP,
lambda service: request_shutdown.set())
while not request_shutdown.isSet():
try:
time.sleep(1)
except KeyboardInterrupt:
break
self.stop()
def track_point_in_time(self, action, point_in_time):
"""
Adds a listener that fires once at or after a spefic point in time.
"""
@ft.wraps(action)
def point_in_time_listener(event):
""" Listens for matching time_changed events. """
now = event.data[ATTR_NOW]
if now >= point_in_time and \
not hasattr(point_in_time_listener, 'run'):
# Set variable so that we will never run twice.
# Because the event bus might have to wait till a thread comes
# available to execute this listener it might occur that the
# listener gets lined up twice to be executed. This will make
# sure the second time it does nothing.
point_in_time_listener.run = True
self.bus.remove_listener(EVENT_TIME_CHANGED,
point_in_time_listener)
action(now)
self.bus.listen(EVENT_TIME_CHANGED, point_in_time_listener)
return point_in_time_listener
# pylint: disable=too-many-arguments
def track_time_change(self, action,
year=None, month=None, day=None,
hour=None, minute=None, second=None):
""" Adds a listener that will fire if time matches a pattern. """
# We do not have to wrap the function with time pattern matching logic
# if no pattern given
if any((val is not None for val in
(year, month, day, hour, minute, second))):
pmp = _process_match_param
year, month, day = pmp(year), pmp(month), pmp(day)
hour, minute, second = pmp(hour), pmp(minute), pmp(second)
@ft.wraps(action)
def time_listener(event):
""" Listens for matching time_changed events. """
now = event.data[ATTR_NOW]
mat = _matcher
if mat(now.year, year) and \
mat(now.month, month) and \
mat(now.day, day) and \
mat(now.hour, hour) and \
mat(now.minute, minute) and \
mat(now.second, second):
action(now)
else:
@ft.wraps(action)
def time_listener(event):
""" Fires every time event that comes in. """
action(event.data[ATTR_NOW])
self.bus.listen(EVENT_TIME_CHANGED, time_listener)
return time_listener
def stop(self):
""" Stops Home Assistant and shuts down all threads. """
_LOGGER.info("Stopping")
self.bus.fire(EVENT_HOMEASSISTANT_STOP)
# Wait till all responses to homeassistant_stop are done
self.pool.block_till_done()
self.pool.stop()
def get_entity_ids(self, domain_filter=None):
"""
Returns known entity ids.
THIS METHOD IS DEPRECATED. Use hass.states.entity_ids
"""
_LOGGER.warning(
"hass.get_entiy_ids is deprecated. Use hass.states.entity_ids")
return self.states.entity_ids(domain_filter)
def listen_once_event(self, event_type, listener):
""" Listen once for event of a specific type.
To listen to all events specify the constant ``MATCH_ALL``
as event_type.
Note: at the moment it is impossible to remove a one time listener.
THIS METHOD IS DEPRECATED. Please use hass.events.listen_once.
"""
_LOGGER.warning(
"hass.listen_once_event is deprecated. Use hass.bus.listen_once")
self.bus.listen_once(event_type, listener)
def track_state_change(self, entity_ids, action,
from_state=None, to_state=None):
"""
Track specific state changes.
entity_ids, from_state and to_state can be string or list.
Use list to match multiple.
THIS METHOD IS DEPRECATED. Use hass.states.track_change
"""
_LOGGER.warning((
"hass.track_state_change is deprecated. "
"Use hass.states.track_change"))
self.states.track_change(entity_ids, action, from_state, to_state)
def call_service(self, domain, service, service_data=None):
"""
Fires event to call specified service.
THIS METHOD IS DEPRECATED. Use hass.services.call
"""
_LOGGER.warning((
"hass.services.call is deprecated. "
"Use hass.services.call"))
self.services.call(domain, service, service_data)
def _process_match_param(parameter):
""" Wraps parameter in a list if it is not one and returns it. """
if parameter is None or parameter == MATCH_ALL:
return MATCH_ALL
elif isinstance(parameter, str) or not hasattr(parameter, '__iter__'):
return (parameter,)
else:
return tuple(parameter)
def _matcher(subject, pattern):
""" Returns True if subject matches the pattern.
Pattern is either a list of allowed subjects or a `MATCH_ALL`.
"""
return MATCH_ALL == pattern or subject in pattern
class JobPriority(util.OrderedEnum):
""" Provides priorities for bus events. """
# pylint: disable=no-init,too-few-public-methods
EVENT_CALLBACK = 0
EVENT_SERVICE = 1
EVENT_STATE = 2
EVENT_TIME = 3
EVENT_DEFAULT = 4
@staticmethod
def from_event_type(event_type):
""" Returns a priority based on event type. """
if event_type == EVENT_TIME_CHANGED:
return JobPriority.EVENT_TIME
elif event_type == EVENT_STATE_CHANGED:
return JobPriority.EVENT_STATE
elif event_type == EVENT_CALL_SERVICE:
return JobPriority.EVENT_SERVICE
elif event_type == EVENT_SERVICE_EXECUTED:
return JobPriority.EVENT_CALLBACK
else:
return JobPriority.EVENT_DEFAULT
def create_worker_pool():
""" Creates a worker pool to be used. """
def job_handler(job):
""" Called whenever a job is available to do. """
try:
func, arg = job
func(arg)
except Exception: # pylint: disable=broad-except
# Catch any exception our service/event_listener might throw
# We do not want to crash our ThreadPool
_LOGGER.exception("BusHandler:Exception doing job")
def busy_callback(worker_count, current_jobs, pending_jobs_count):
""" Callback to be called when the pool queue gets too big. """
_LOGGER.warning(
"WorkerPool:All %d threads are busy and %d jobs pending",
worker_count, pending_jobs_count)
for start, job in current_jobs:
_LOGGER.warning("WorkerPool:Current job from %s: %s",
util.datetime_to_str(start), job)
return util.ThreadPool(job_handler, MIN_WORKER_THREAD, busy_callback)
class EventOrigin(enum.Enum):
""" Distinguish between origin of event. """
# pylint: disable=no-init,too-few-public-methods
local = "LOCAL"
remote = "REMOTE"
def __str__(self):
return self.value
# pylint: disable=too-few-public-methods
class Event(object):
""" Represents an event within the Bus. """
__slots__ = ['event_type', 'data', 'origin']
def __init__(self, event_type, data=None, origin=EventOrigin.local):
self.event_type = event_type
self.data = data or {}
self.origin = origin
def as_dict(self):
""" Returns a dict representation of this Event. """
return {
'event_type': self.event_type,
'data': dict(self.data),
'origin': str(self.origin)
}
def __repr__(self):
# pylint: disable=maybe-no-member
if self.data:
return "<Event {}[{}]: {}>".format(
self.event_type, str(self.origin)[0],
util.repr_helper(self.data))
else:
return "<Event {}[{}]>".format(self.event_type,
str(self.origin)[0])
class EventBus(object):
""" Class that allows different components to communicate via services
and events.
"""
def __init__(self, pool=None):
self._listeners = {}
self._lock = threading.Lock()
self._pool = pool or create_worker_pool()
@property
def listeners(self):
""" Dict with events that is being listened for and the number
of listeners.
"""
with self._lock:
return {key: len(self._listeners[key])
for key in self._listeners}
def fire(self, event_type, event_data=None, origin=EventOrigin.local):
""" Fire an event. """
with self._lock:
# Copy the list of the current listeners because some listeners
# remove themselves as a listener while being executed which
# causes the iterator to be confused.
get = self._listeners.get
listeners = get(MATCH_ALL, []) + get(event_type, [])
event = Event(event_type, event_data, origin)
if event_type != EVENT_TIME_CHANGED:
_LOGGER.info("Bus:Handling %s", event)
if not listeners:
return
job_priority = JobPriority.from_event_type(event_type)
for func in listeners:
self._pool.add_job(job_priority, (func, event))
def listen(self, event_type, listener):
""" Listen for all events or events of a specific type.
To listen to all events specify the constant ``MATCH_ALL``
as event_type.
"""
with self._lock:
if event_type in self._listeners:
self._listeners[event_type].append(listener)
else:
self._listeners[event_type] = [listener]
def listen_once(self, event_type, listener):
""" Listen once for event of a specific type.
To listen to all events specify the constant ``MATCH_ALL``
as event_type.
Note: at the moment it is impossible to remove a one time listener.
"""
@ft.wraps(listener)
def onetime_listener(event):
""" Removes listener from eventbus and then fires listener. """
if not hasattr(onetime_listener, 'run'):
# Set variable so that we will never run twice.
# Because the event bus might have to wait till a thread comes
# available to execute this listener it might occur that the
# listener gets lined up twice to be executed.
# This will make sure the second time it does nothing.
onetime_listener.run = True
self.remove_listener(event_type, onetime_listener)
listener(event)
self.listen(event_type, onetime_listener)
def remove_listener(self, event_type, listener):
""" Removes a listener of a specific event_type. """
with self._lock:
try:
self._listeners[event_type].remove(listener)
# delete event_type list if empty
if not self._listeners[event_type]:
self._listeners.pop(event_type)
except (KeyError, ValueError):
# KeyError is key event_type listener did not exist
# ValueError if listener did not exist within event_type
pass
class State(object):
"""
Object to represent a state within the state machine.
entity_id: the entity that is represented.
state: the state of the entity
attributes: extra information on entity and state
last_changed: last time the state was changed, not the attributes.
last_updated: last time this object was updated.
"""
__slots__ = ['entity_id', 'state', 'attributes',
'last_changed', 'last_updated']
def __init__(self, entity_id, state, attributes=None, last_changed=None):
if not ENTITY_ID_PATTERN.match(entity_id):
raise InvalidEntityFormatError((
"Invalid entity id encountered: {}. "
"Format should be <domain>.<object_id>").format(entity_id))
self.entity_id = entity_id.lower()
self.state = state
self.attributes = attributes or {}
self.last_updated = dt.datetime.now()
# Strip microsecond from last_changed else we cannot guarantee
# state == State.from_dict(state.as_dict())
# This behavior occurs because to_dict uses datetime_to_str
# which does not preserve microseconds
self.last_changed = util.strip_microseconds(
last_changed or self.last_updated)
@property
def domain(self):
""" Returns domain of this state. """
return util.split_entity_id(self.entity_id)[0]
def copy(self):
""" Creates a copy of itself. """
return State(self.entity_id, self.state,
dict(self.attributes), self.last_changed)
def as_dict(self):
""" Converts State to a dict to be used within JSON.
Ensures: state == State.from_dict(state.as_dict()) """
return {'entity_id': self.entity_id,
'state': self.state,
'attributes': self.attributes,
'last_changed': util.datetime_to_str(self.last_changed)}
@classmethod
def from_dict(cls, json_dict):
""" Static method to create a state from a dict.
Ensures: state == State.from_json_dict(state.to_json_dict()) """
if not (json_dict and
'entity_id' in json_dict and
'state' in json_dict):
return None
last_changed = json_dict.get('last_changed')
if last_changed:
last_changed = util.str_to_datetime(last_changed)
return cls(json_dict['entity_id'], json_dict['state'],
json_dict.get('attributes'), last_changed)
def __eq__(self, other):
return (self.__class__ == other.__class__ and
self.entity_id == other.entity_id and
self.state == other.state and
self.attributes == other.attributes)
def __repr__(self):
attr = "; {}".format(util.repr_helper(self.attributes)) \
if self.attributes else ""
return "<state {}={}{} @ {}>".format(
self.entity_id, self.state, attr,
util.datetime_to_str(self.last_changed))
class StateMachine(object):
""" Helper class that tracks the state of different entities. """
def __init__(self, bus):
self._states = {}
self._bus = bus
self._lock = threading.Lock()
def entity_ids(self, domain_filter=None):
""" List of entity ids that are being tracked. """
if domain_filter is not None:
domain_filter = domain_filter.lower()
return [state.entity_id for key, state
in self._states.items()
if util.split_entity_id(key)[0] == domain_filter]
else:
return list(self._states.keys())
def all(self):
""" Returns a list of all states. """
return [state.copy() for state in self._states.values()]
def get(self, entity_id):
""" Returns the state of the specified entity. """
state = self._states.get(entity_id.lower())
# Make a copy so people won't mutate the state
return state.copy() if state else None
def get_since(self, point_in_time):
"""
Returns all states that have been changed since point_in_time.
"""
point_in_time = util.strip_microseconds(point_in_time)
with self._lock:
return [state for state in self._states.values()
if state.last_updated >= point_in_time]
def is_state(self, entity_id, state):
""" Returns True if entity exists and is specified state. """
entity_id = entity_id.lower()
return (entity_id in self._states and
self._states[entity_id].state == state)
def remove(self, entity_id):
""" Removes an entity from the state machine.
Returns boolean to indicate if an entity was removed. """
entity_id = entity_id.lower()
with self._lock:
return self._states.pop(entity_id, None) is not None
def set(self, entity_id, new_state, attributes=None):
""" Set the state of an entity, add entity if it does not exist.
Attributes is an optional dict to specify attributes of this state.
If you just update the attributes and not the state, last changed will
not be affected.
"""
entity_id = entity_id.lower()
new_state = str(new_state)
attributes = attributes or {}
with self._lock:
old_state = self._states.get(entity_id)
is_existing = old_state is not None
same_state = is_existing and old_state.state == new_state
same_attr = is_existing and old_state.attributes == attributes
# If state did not exist or is different, set it
if not (same_state and same_attr):
last_changed = old_state.last_changed if same_state else None
state = State(entity_id, new_state, attributes, last_changed)
self._states[entity_id] = state
event_data = {'entity_id': entity_id, 'new_state': state}
if old_state:
event_data['old_state'] = old_state
self._bus.fire(EVENT_STATE_CHANGED, event_data)
def track_change(self, entity_ids, action, from_state=None, to_state=None):
"""
Track specific state changes.
entity_ids, from_state and to_state can be string or list.
Use list to match multiple.
Returns the listener that listens on the bus for EVENT_STATE_CHANGED.
Pass the return value into hass.bus.remove_listener to remove it.
"""
from_state = _process_match_param(from_state)
to_state = _process_match_param(to_state)
# Ensure it is a lowercase list with entity ids we want to match on
if isinstance(entity_ids, str):
entity_ids = (entity_ids.lower(),)
else:
entity_ids = tuple(entity_id.lower() for entity_id in entity_ids)
@ft.wraps(action)
def state_listener(event):
""" The listener that listens for specific state changes. """
if event.data['entity_id'] not in entity_ids:
return
if 'old_state' in event.data:
old_state = event.data['old_state'].state
else:
old_state = None
if _matcher(old_state, from_state) and \
_matcher(event.data['new_state'].state, to_state):
action(event.data['entity_id'],
event.data.get('old_state'),
event.data['new_state'])
self._bus.listen(EVENT_STATE_CHANGED, state_listener)
return state_listener
# pylint: disable=too-few-public-methods
class ServiceCall(object):
""" Represents a call to a service. """
__slots__ = ['domain', 'service', 'data']
def __init__(self, domain, service, data=None):
self.domain = domain
self.service = service
self.data = data or {}
def __repr__(self):
if self.data:
return "<ServiceCall {}.{}: {}>".format(
self.domain, self.service, util.repr_helper(self.data))
else:
return "<ServiceCall {}.{}>".format(self.domain, self.service)
class ServiceRegistry(object):
""" Offers services over the eventbus. """
def __init__(self, bus, pool=None):
self._services = {}
self._lock = threading.Lock()
self._pool = pool or create_worker_pool()
self._bus = bus
self._cur_id = 0
bus.listen(EVENT_CALL_SERVICE, self._event_to_service_call)
@property
def services(self):
""" Dict with per domain a list of available services. """
with self._lock:
return {domain: list(self._services[domain].keys())
for domain in self._services}
def has_service(self, domain, service):
""" Returns True if specified service exists. """
return service in self._services.get(domain, [])
def register(self, domain, service, service_func):
""" Register a service. """
with self._lock:
if domain in self._services:
self._services[domain][service] = service_func
else:
self._services[domain] = {service: service_func}
self._bus.fire(
EVENT_SERVICE_REGISTERED,
{ATTR_DOMAIN: domain, ATTR_SERVICE: service})
def call(self, domain, service, service_data=None, blocking=False):
"""
Calls specified service.
Specify blocking=True to wait till service is executed.
Waits a maximum of SERVICE_CALL_LIMIT.
If blocking = True, will return boolean if service executed
succesfully within SERVICE_CALL_LIMIT.
This method will fire an event to call the service.
This event will be picked up by this ServiceRegistry and any
other ServiceRegistry that is listening on the EventBus.
Because the service is sent as an event you are not allowed to use
the keys ATTR_DOMAIN and ATTR_SERVICE in your service_data.
"""
call_id = self._generate_unique_id()
event_data = service_data or {}
event_data[ATTR_DOMAIN] = domain
event_data[ATTR_SERVICE] = service
event_data[ATTR_SERVICE_CALL_ID] = call_id
if blocking:
executed_event = threading.Event()
def service_executed(call):
"""
Called when a service is executed.
Will set the event if matches our service call.
"""
if call.data[ATTR_SERVICE_CALL_ID] == call_id:
executed_event.set()
self._bus.remove_listener(
EVENT_SERVICE_EXECUTED, service_executed)
self._bus.listen(EVENT_SERVICE_EXECUTED, service_executed)
self._bus.fire(EVENT_CALL_SERVICE, event_data)
if blocking:
# wait will return False if event not set after our limit has
# passed. If not set, clean up the listener
if not executed_event.wait(SERVICE_CALL_LIMIT):
self._bus.remove_listener(
EVENT_SERVICE_EXECUTED, service_executed)
return False
return True
def _event_to_service_call(self, event):
""" Calls a service from an event. """
service_data = dict(event.data)
domain = service_data.pop(ATTR_DOMAIN, None)
service = service_data.pop(ATTR_SERVICE, None)
with self._lock:
if domain in self._services and service in self._services[domain]:
service_call = ServiceCall(domain, service, service_data)
# Add a job to the pool that calls _execute_service
self._pool.add_job(JobPriority.EVENT_SERVICE,
(self._execute_service,
(self._services[domain][service],
service_call)))
def _execute_service(self, service_and_call):
""" Executes a service and fires a SERVICE_EXECUTED event. """
service, call = service_and_call
service(call)
self._bus.fire(
EVENT_SERVICE_EXECUTED, {
ATTR_SERVICE_CALL_ID: call.data[ATTR_SERVICE_CALL_ID]
})
def _generate_unique_id(self):
""" Generates a unique service call id. """
self._cur_id += 1
return "{}-{}".format(id(self), self._cur_id)
class Timer(threading.Thread):
""" Timer will sent out an event every TIMER_INTERVAL seconds. """
def __init__(self, hass, interval=None):
threading.Thread.__init__(self)
self.daemon = True
self.hass = hass
self.interval = interval or TIMER_INTERVAL
self._stop_event = threading.Event()
# We want to be able to fire every time a minute starts (seconds=0).
# We want this so other modules can use that to make sure they fire
# every minute.
assert 60 % self.interval == 0, "60 % TIMER_INTERVAL should be 0!"
hass.bus.listen_once(EVENT_HOMEASSISTANT_START,
lambda event: self.start())
def run(self):
""" Start the timer. """
self.hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
lambda event: self._stop_event.set())
_LOGGER.info("Timer:starting")
last_fired_on_second = -1
calc_now = dt.datetime.now
interval = self.interval
while not self._stop_event.isSet():
now = calc_now()
# First check checks if we are not on a second matching the
# timer interval. Second check checks if we did not already fire
# this interval.
if now.second % interval or \
now.second == last_fired_on_second:
# Sleep till it is the next time that we have to fire an event.
# Aim for halfway through the second that fits TIMER_INTERVAL.
# If TIMER_INTERVAL is 10 fire at .5, 10.5, 20.5, etc seconds.
# This will yield the best results because time.sleep() is not
# 100% accurate because of non-realtime OS's
slp_seconds = interval - now.second % interval + \
.5 - now.microsecond/1000000.0
time.sleep(slp_seconds)
now = calc_now()
last_fired_on_second = now.second
self.hass.bus.fire(EVENT_TIME_CHANGED, {ATTR_NOW: now})
class Config(object):
""" Configuration settings for Home Assistant. """
# pylint: disable=too-many-instance-attributes
def __init__(self):
self.latitude = None
self.longitude = None
self.temperature_unit = None
self.location_name = None
self.time_zone = None
# List of loaded components
self.components = []
# Remote.API object pointing at local API
self.api = None
# Directory that holds the configuration
self.config_dir = os.path.join(os.getcwd(), 'config')
def auto_detect(self):
""" Will attempt to detect config of Home Assistant. """
# Only detect if location or temp unit missing
if None not in (self.latitude, self.longitude, self.temperature_unit):
return
_LOGGER.info('Auto detecting location and temperature unit')
try:
info = requests.get('https://freegeoip.net/json/').json()
except requests.RequestException:
return
if self.latitude is None and self.longitude is None:
self.latitude = info['latitude']
self.longitude = info['longitude']
if self.temperature_unit is None:
# From Wikipedia:
# Fahrenheit is used in the Bahamas, Belize, the Cayman Islands,
# Palau, and the United States and associated territories of
# American Samoa and the U.S. Virgin Islands
if info['country_code'] in ('BS', 'BZ', 'KY', 'PW',
'US', 'AS', 'VI'):
self.temperature_unit = TEMP_FAHRENHEIT
else:
self.temperature_unit = TEMP_CELCIUS
if self.location_name is None:
self.location_name = info['city']
if self.time_zone is None:
self.time_zone = info['time_zone']
def path(self, path):
""" Returns path to the file within the config dir. """
return os.path.join(self.config_dir, path)
def temperature(self, value, unit):
""" Converts temperature to user preferred unit if set. """
if not (unit and self.temperature_unit and
unit != self.temperature_unit):
return value, unit
try:
if unit == TEMP_CELCIUS:
# Convert C to F
return round(float(value) * 1.8 + 32.0, 1), TEMP_FAHRENHEIT
# Convert F to C
return round((float(value)-32.0)/1.8, 1), TEMP_CELCIUS
except ValueError:
# Could not convert value to float
return value, unit
class HomeAssistantError(Exception):
""" General Home Assistant exception occured. """
pass
class InvalidEntityFormatError(HomeAssistantError):
""" When an invalid formatted entity is encountered. """
pass
class NoEntitySpecifiedError(HomeAssistantError):
""" When no entity is specified. """
pass

View file

@ -4,7 +4,10 @@ from __future__ import print_function
import sys
import os
import argparse
import importlib
from homeassistant import bootstrap
import homeassistant.config as config_util
from homeassistant.const import __version__, EVENT_HOMEASSISTANT_START
def validate_python():
@ -13,93 +16,58 @@ def validate_python():
if major < 3 or (major == 3 and minor < 4):
print("Home Assistant requires atleast Python 3.4")
sys.exit()
def validate_dependencies():
""" Validate all dependencies that HA uses. """
import_fail = False
for module in ['requests']:
try:
importlib.import_module(module)
except ImportError:
import_fail = True
print(
'Fatal Error: Unable to find dependency {}'.format(module))
if import_fail:
print(("Install dependencies by running: "
"pip3 install -r requirements.txt"))
sys.exit()
def ensure_path_and_load_bootstrap():
""" Ensure sys load path is correct and load Home Assistant bootstrap. """
try:
from homeassistant import bootstrap
except ImportError:
# This is to add support to load Home Assistant using
# `python3 homeassistant` instead of `python3 -m homeassistant`
# Insert the parent directory of this file into the module search path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from homeassistant import bootstrap
return bootstrap
def validate_git_submodules():
""" Validate the git submodules are cloned. """
try:
# pylint: disable=no-name-in-module, unused-variable
from homeassistant.external.noop import WORKING # noqa
except ImportError:
print("Repository submodules have not been initialized")
print("Please run: git submodule update --init --recursive")
sys.exit()
sys.exit(1)
def ensure_config_path(config_dir):
""" Gets the path to the configuration file.
Creates one if it not exists. """
""" Validates configuration directory. """
lib_dir = os.path.join(config_dir, 'lib')
# Test if configuration directory exists
if not os.path.isdir(config_dir):
print(('Fatal Error: Unable to find specified configuration '
'directory {} ').format(config_dir))
sys.exit()
if config_dir != config_util.get_default_config_dir():
print(('Fatal Error: Specified configuration directory does '
'not exist {} ').format(config_dir))
sys.exit(1)
# Try to use yaml configuration first
config_path = os.path.join(config_dir, 'configuration.yaml')
if not os.path.isfile(config_path):
config_path = os.path.join(config_dir, 'home-assistant.conf')
# Ensure a config file exists to make first time usage easier
if not os.path.isfile(config_path):
config_path = os.path.join(config_dir, 'configuration.yaml')
try:
with open(config_path, 'w') as conf:
conf.write("frontend:\n\n")
conf.write("discovery:\n\n")
conf.write("history:\n\n")
except IOError:
print(('Fatal Error: No configuration file found and unable '
'to write a default one to {}').format(config_path))
sys.exit()
os.mkdir(config_dir)
except OSError:
print(('Fatal Error: Unable to create default configuration '
'directory {} ').format(config_dir))
sys.exit(1)
# Test if library directory exists
if not os.path.isdir(lib_dir):
try:
os.mkdir(lib_dir)
except OSError:
print(('Fatal Error: Unable to create library '
'directory {} ').format(lib_dir))
sys.exit(1)
def ensure_config_file(config_dir):
""" Ensure configuration file exists. """
config_path = config_util.ensure_config_exists(config_dir)
if config_path is None:
print('Error getting configuration path')
sys.exit(1)
return config_path
def get_arguments():
""" Get parsed passed in arguments. """
parser = argparse.ArgumentParser()
parser = argparse.ArgumentParser(
description="Home Assistant: Observe, Control, Automate.")
parser.add_argument('--version', action='version', version=__version__)
parser.add_argument(
'-c', '--config',
metavar='path_to_config_dir',
default="config",
default=config_util.get_default_config_dir(),
help="Directory that contains the Home Assistant configuration")
parser.add_argument(
'--demo-mode',
@ -109,43 +77,120 @@ def get_arguments():
'--open-ui',
action='store_true',
help='Open the webinterface in a browser')
parser.add_argument(
'--skip-pip',
action='store_true',
help='Skips pip install of required packages on startup')
parser.add_argument(
'-v', '--verbose',
action='store_true',
help="Enable verbose logging to file.")
parser.add_argument(
'--pid-file',
metavar='path_to_pid_file',
default=None,
help='Path to PID file useful for running as daemon')
parser.add_argument(
'--log-rotate-days',
type=int,
default=None,
help='Enables daily log rotation and keeps up to the specified days')
if os.name != "nt":
parser.add_argument(
'--daemon',
action='store_true',
help='Run Home Assistant as daemon')
return parser.parse_args()
arguments = parser.parse_args()
if os.name == "nt":
arguments.daemon = False
return arguments
def daemonize():
""" Move current process to daemon process """
# create first fork
pid = os.fork()
if pid > 0:
sys.exit(0)
# decouple fork
os.setsid()
os.umask(0)
# create second fork
pid = os.fork()
if pid > 0:
sys.exit(0)
def check_pid(pid_file):
""" Check that HA is not already running """
# check pid file
try:
pid = int(open(pid_file, 'r').readline())
except IOError:
# PID File does not exist
return
try:
os.kill(pid, 0)
except OSError:
# PID does not exist
return
print('Fatal Error: HomeAssistant is already running.')
sys.exit(1)
def write_pid(pid_file):
""" Create PID File """
pid = os.getpid()
try:
open(pid_file, 'w').write(str(pid))
except IOError:
print('Fatal Error: Unable to write pid file {}'.format(pid_file))
sys.exit(1)
def main():
""" Starts Home Assistant. """
validate_python()
validate_dependencies()
bootstrap = ensure_path_and_load_bootstrap()
validate_git_submodules()
args = get_arguments()
config_dir = os.path.join(os.getcwd(), args.config)
config_path = ensure_config_path(config_dir)
ensure_config_path(config_dir)
# daemon functions
if args.pid_file:
check_pid(args.pid_file)
if args.daemon:
daemonize()
if args.pid_file:
write_pid(args.pid_file)
if args.demo_mode:
from homeassistant.components import http, demo
# Demo mode only requires http and demo components.
hass = bootstrap.from_config_dict({
http.DOMAIN: {},
demo.DOMAIN: {}
})
config = {
'frontend': {},
'demo': {}
}
hass = bootstrap.from_config_dict(
config, config_dir=config_dir, daemon=args.daemon,
verbose=args.verbose, skip_pip=args.skip_pip,
log_rotate_days=args.log_rotate_days)
else:
hass = bootstrap.from_config_file(config_path)
config_file = ensure_config_file(config_dir)
print('Config directory:', config_dir)
hass = bootstrap.from_config_file(
config_file, daemon=args.daemon, verbose=args.verbose,
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days)
if args.open_ui:
from homeassistant.const import EVENT_HOMEASSISTANT_START
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)

View file

@ -10,24 +10,30 @@ start by calling homeassistant.start_home_assistant(bus)
"""
import os
import configparser
import yaml
import io
import sys
import logging
import logging.handlers
from collections import defaultdict
import homeassistant
import homeassistant.core as core
import homeassistant.util.dt as date_util
import homeassistant.util.package as pkg_util
import homeassistant.util.location as loc_util
import homeassistant.config as config_util
import homeassistant.loader as loader
import homeassistant.components as core_components
import homeassistant.components.group as group
from homeassistant.helpers.entity import Entity
from homeassistant.const import (
EVENT_COMPONENT_LOADED, CONF_LATITUDE, CONF_LONGITUDE,
CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_TIME_ZONE, TEMP_CELCIUS,
TEMP_FAHRENHEIT)
CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_TIME_ZONE, CONF_CUSTOMIZE,
TEMP_CELCIUS, TEMP_FAHRENHEIT)
_LOGGER = logging.getLogger(__name__)
ATTR_COMPONENT = "component"
ATTR_COMPONENT = 'component'
PLATFORM_FORMAT = '{}.{}'
def setup_component(hass, domain, config=None):
@ -48,17 +54,30 @@ def setup_component(hass, domain, config=None):
return False
for component in components:
if component in hass.config.components:
continue
if not _setup_component(hass, component, config):
return False
return True
def _handle_requirements(hass, component, name):
""" Installs requirements for component. """
if hass.config.skip_pip or not hasattr(component, 'REQUIREMENTS'):
return True
for req in component.REQUIREMENTS:
if not pkg_util.install_package(req, target=hass.config.path('lib')):
_LOGGER.error('Not initializing %s because could not install '
'dependency %s', name, req)
return False
return True
def _setup_component(hass, domain, config):
""" Setup a component for Home Assistant. """
if domain in hass.config.components:
return True
component = loader.get_component(domain)
missing_deps = [dep for dep in component.DEPENDENCIES
@ -66,47 +85,96 @@ def _setup_component(hass, domain, config):
if missing_deps:
_LOGGER.error(
"Not initializing %s because not all dependencies loaded: %s",
'Not initializing %s because not all dependencies loaded: %s',
domain, ", ".join(missing_deps))
return False
if not _handle_requirements(hass, component, domain):
return False
try:
if component.setup(hass, config):
hass.config.components.append(component.DOMAIN)
# Assumption: if a component does not depend on groups
# it communicates with devices
if group.DOMAIN not in component.DEPENDENCIES:
hass.pool.add_worker()
hass.bus.fire(
EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN})
return True
else:
_LOGGER.error("component %s failed to initialize", domain)
if not component.setup(hass, config):
_LOGGER.error('component %s failed to initialize', domain)
return False
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Error during setup of component %s", domain)
_LOGGER.exception('Error during setup of component %s', domain)
return False
return False
hass.config.components.append(component.DOMAIN)
# Assumption: if a component does not depend on groups
# it communicates with devices
if group.DOMAIN not in component.DEPENDENCIES:
hass.pool.add_worker()
hass.bus.fire(
EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN})
return True
# pylint: disable=too-many-branches, too-many-statements
def from_config_dict(config, hass=None):
def prepare_setup_platform(hass, config, domain, platform_name):
""" Loads a platform and makes sure dependencies are setup. """
_ensure_loader_prepared(hass)
platform_path = PLATFORM_FORMAT.format(domain, platform_name)
platform = loader.get_component(platform_path)
# Not found
if platform is None:
return None
# Already loaded
elif platform_path in hass.config.components:
return platform
# Load dependencies
if hasattr(platform, 'DEPENDENCIES'):
for component in platform.DEPENDENCIES:
if not setup_component(hass, component, config):
_LOGGER.error(
'Unable to prepare setup for platform %s because '
'dependency %s could not be initialized', platform_path,
component)
return None
if not _handle_requirements(hass, platform, platform_path):
return None
return platform
def mount_local_lib_path(config_dir):
""" Add local library to Python Path """
sys.path.insert(0, os.path.join(config_dir, 'lib'))
# pylint: disable=too-many-branches, too-many-statements, too-many-arguments
def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
verbose=False, daemon=False, skip_pip=False,
log_rotate_days=None):
"""
Tries to configure Home Assistant from a config dict.
Dynamically loads required components and its dependencies.
"""
if hass is None:
hass = homeassistant.HomeAssistant()
hass = core.HomeAssistant()
if config_dir is not None:
config_dir = os.path.abspath(config_dir)
hass.config.config_dir = config_dir
mount_local_lib_path(config_dir)
process_ha_core_config(hass, config.get(homeassistant.DOMAIN, {}))
process_ha_core_config(hass, config.get(core.DOMAIN, {}))
enable_logging(hass)
if enable_log:
enable_logging(hass, verbose, daemon, log_rotate_days)
hass.config.skip_pip = skip_pip
if skip_pip:
_LOGGER.warning('Skipping pip installation of required modules. '
'This may cause issues.')
_ensure_loader_prepared(hass)
@ -118,15 +186,15 @@ def from_config_dict(config, hass=None):
# Filter out the repeating and common config section [homeassistant]
components = (key for key in config.keys()
if ' ' not in key and key != homeassistant.DOMAIN)
if ' ' not in key and key != core.DOMAIN)
if not core_components.setup(hass, config):
_LOGGER.error("Home Assistant core failed to initialize. "
"Further initialization aborted.")
_LOGGER.error('Home Assistant core failed to initialize. '
'Further initialization aborted.')
return hass
_LOGGER.info("Home Assistant core initialized")
_LOGGER.info('Home Assistant core initialized')
# Setup the components
for domain in loader.load_order_components(components):
@ -135,48 +203,55 @@ def from_config_dict(config, hass=None):
return hass
def from_config_file(config_path, hass=None):
def from_config_file(config_path, hass=None, verbose=False, daemon=False,
skip_pip=True, log_rotate_days=None):
"""
Reads the configuration file and tries to start all the required
functionality. Will add functionality to 'hass' parameter if given,
instantiates a new Home Assistant object if 'hass' is not given.
"""
if hass is None:
hass = homeassistant.HomeAssistant()
hass = core.HomeAssistant()
# Set config dir to directory holding config file
hass.config.config_dir = os.path.abspath(os.path.dirname(config_path))
config_dir = os.path.abspath(os.path.dirname(config_path))
hass.config.config_dir = config_dir
mount_local_lib_path(config_dir)
config_dict = {}
# check config file type
if os.path.splitext(config_path)[1] == '.yaml':
# Read yaml
config_dict = yaml.load(io.open(config_path, 'r'))
enable_logging(hass, verbose, daemon, log_rotate_days)
# If YAML file was empty
if config_dict is None:
config_dict = {}
config_dict = config_util.load_config_file(config_path)
else:
# Read config
config = configparser.ConfigParser()
config.read(config_path)
for section in config.sections():
config_dict[section] = {}
for key, val in config.items(section):
config_dict[section][key] = val
return from_config_dict(config_dict, hass)
return from_config_dict(config_dict, hass, enable_log=False,
skip_pip=skip_pip)
def enable_logging(hass):
def enable_logging(hass, verbose=False, daemon=False, log_rotate_days=None):
""" Setup the logging for home assistant. """
logging.basicConfig(level=logging.INFO)
if not daemon:
logging.basicConfig(level=logging.INFO)
fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) "
"[%(name)s] %(message)s%(reset)s")
try:
from colorlog import ColoredFormatter
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
fmt,
datefmt='%y-%m-%d %H:%M:%S',
reset=True,
log_colors={
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red',
}
))
except ImportError:
_LOGGER.warning(
"Colorlog package not found, console coloring disabled")
# Log errors to a file if we have write access to file or config dir
err_log_path = hass.config.path("home-assistant.log")
err_log_path = hass.config.path('home-assistant.log')
err_path_exists = os.path.isfile(err_log_path)
# Check if we can write to the error log if it exists or that
@ -184,38 +259,95 @@ def enable_logging(hass):
if (err_path_exists and os.access(err_log_path, os.W_OK)) or \
(not err_path_exists and os.access(hass.config.config_dir, os.W_OK)):
err_handler = logging.FileHandler(
err_log_path, mode='w', delay=True)
if log_rotate_days:
err_handler = logging.handlers.TimedRotatingFileHandler(
err_log_path, when='midnight', backupCount=log_rotate_days)
else:
err_handler = logging.FileHandler(
err_log_path, mode='w', delay=True)
err_handler.setLevel(logging.WARNING)
err_handler.setLevel(logging.INFO if verbose else logging.WARNING)
err_handler.setFormatter(
logging.Formatter('%(asctime)s %(name)s: %(message)s',
datefmt='%H:%M %d-%m-%y'))
logging.getLogger('').addHandler(err_handler)
datefmt='%y-%m-%d %H:%M:%S'))
logger = logging.getLogger('')
logger.addHandler(err_handler)
logger.setLevel(logging.INFO) # this sets the minimum log level
else:
_LOGGER.error(
"Unable to setup error log %s (access denied)", err_log_path)
'Unable to setup error log %s (access denied)', err_log_path)
def process_ha_core_config(hass, config):
""" Processes the [homeassistant] section from the config. """
hac = hass.config
def set_time_zone(time_zone_str):
""" Helper method to set time zone in HA. """
if time_zone_str is None:
return
time_zone = date_util.get_time_zone(time_zone_str)
if time_zone:
hac.time_zone = time_zone
date_util.set_default_time_zone(time_zone)
else:
_LOGGER.error('Received invalid time zone %s', time_zone_str)
for key, attr in ((CONF_LATITUDE, 'latitude'),
(CONF_LONGITUDE, 'longitude'),
(CONF_NAME, 'location_name'),
(CONF_TIME_ZONE, 'time_zone')):
(CONF_NAME, 'location_name')):
if key in config:
setattr(hass.config, attr, config[key])
setattr(hac, attr, config[key])
set_time_zone(config.get(CONF_TIME_ZONE))
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]
if unit == 'C':
hass.config.temperature_unit = TEMP_CELCIUS
hac.temperature_unit = TEMP_CELCIUS
elif unit == 'F':
hass.config.temperature_unit = TEMP_FAHRENHEIT
hac.temperature_unit = TEMP_FAHRENHEIT
hass.config.auto_detect()
# If we miss some of the needed values, auto detect them
if None not in (
hac.latitude, hac.longitude, hac.temperature_unit, hac.time_zone):
return
_LOGGER.info('Auto detecting location and temperature unit')
info = loc_util.detect_location_info()
if info is None:
_LOGGER.error('Could not detect location information')
return
if hac.latitude is None and hac.longitude is None:
hac.latitude = info.latitude
hac.longitude = info.longitude
if hac.temperature_unit is None:
if info.use_fahrenheit:
hac.temperature_unit = TEMP_FAHRENHEIT
else:
hac.temperature_unit = TEMP_CELCIUS
if hac.location_name is None:
hac.location_name = info.city
if hac.time_zone is None:
set_time_zone(info.time_zone)
def _ensure_loader_prepared(hass):

View file

@ -17,7 +17,7 @@ Each component should publish services only under its own domain.
import itertools as it
import logging
import homeassistant as ha
import homeassistant.core as ha
import homeassistant.util as util
from homeassistant.helpers import extract_entity_ids
from homeassistant.loader import get_component

View file

@ -9,12 +9,13 @@ import logging
import threading
import json
import homeassistant as ha
import homeassistant.core as ha
from homeassistant.helpers.state import TrackStates
import homeassistant.remote as rem
from homeassistant.const import (
URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES, URL_API_STREAM,
URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, URL_API_COMPONENTS,
URL_API_CONFIG, URL_API_BOOTSTRAP,
EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, MATCH_ALL,
HTTP_OK, HTTP_CREATED, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
HTTP_UNPROCESSABLE_ENTITY)
@ -42,6 +43,13 @@ def setup(hass, config):
# /api/stream
hass.http.register_path('GET', URL_API_STREAM, _handle_get_api_stream)
# /api/config
hass.http.register_path('GET', URL_API_CONFIG, _handle_get_api_config)
# /api/bootstrap
hass.http.register_path(
'GET', URL_API_BOOTSTRAP, _handle_get_api_bootstrap)
# /states
hass.http.register_path('GET', URL_API_STATES, _handle_get_api_states)
hass.http.register_path(
@ -140,6 +148,23 @@ def _handle_get_api_stream(handler, path_match, data):
hass.bus.remove_listener(MATCH_ALL, forward_events)
def _handle_get_api_config(handler, path_match, data):
""" Returns the Home Assistant config. """
handler.write_json(handler.server.hass.config.as_dict())
def _handle_get_api_bootstrap(handler, path_match, data):
""" Returns all data needed to bootstrap Home Assistant. """
hass = handler.server.hass
handler.write_json({
'config': hass.config.as_dict(),
'states': hass.states.all(),
'events': _events_json(hass),
'services': _services_json(hass),
})
def _handle_get_api_states(handler, path_match, data):
""" Returns a dict containing all entity ids and their state. """
handler.write_json(handler.server.hass.states.all())
@ -190,9 +215,7 @@ def _handle_post_state_entity(handler, path_match, data):
def _handle_get_api_events(handler, path_match, data):
""" Handles getting overview of event listeners. """
handler.write_json([{"event": key, "listener_count": value}
for key, value
in handler.server.hass.bus.listeners.items()])
handler.write_json(_events_json(handler.server.hass))
def _handle_api_post_events_event(handler, path_match, event_data):
@ -227,10 +250,7 @@ def _handle_api_post_events_event(handler, path_match, event_data):
def _handle_get_api_services(handler, path_match, data):
""" Handles getting overview of services. """
handler.write_json(
[{"domain": key, "services": value}
for key, value
in handler.server.hass.services.services.items()])
handler.write_json(_services_json(handler.server.hass))
# pylint: disable=invalid-name
@ -312,3 +332,15 @@ def _handle_get_api_components(handler, path_match, data):
""" Returns all the loaded components. """
handler.write_json(handler.server.hass.config.components)
def _services_json(hass):
""" Generate services data to JSONify. """
return [{"domain": key, "services": value}
for key, value in hass.services.services.items()]
def _events_json(hass):
""" Generate event data to JSONify. """
return [{"event": key, "listener_count": value}
for key, value in hass.bus.listeners.items()]

View file

@ -0,0 +1,143 @@
"""
components.arduino
~~~~~~~~~~~~~~~~~~
Arduino component that connects to a directly attached Arduino board which
runs with the Firmata firmware.
Configuration:
To use the Arduino board you will need to add something like the following
to your configuration.yaml file.
arduino:
port: /dev/ttyACM0
Variables:
port
*Required
The port where is your board connected to your Home Assistant system.
If you are using an original Arduino the port will be named ttyACM*. The exact
number can be determined with 'ls /dev/ttyACM*' or check your 'dmesg'/
'journalctl -f' output. Keep in mind that Arduino clones are often using a
different name for the port (e.g. '/dev/ttyUSB*').
A word of caution: The Arduino is not storing states. This means that with
every initialization the pins are set to off/low.
"""
import logging
try:
from PyMata.pymata import PyMata
except ImportError:
PyMata = None
from homeassistant.helpers import validate_config
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP)
DOMAIN = "arduino"
DEPENDENCIES = []
REQUIREMENTS = ['PyMata==2.07a']
BOARD = None
_LOGGER = logging.getLogger(__name__)
def setup(hass, config):
""" Setup the Arduino component. """
global PyMata # pylint: disable=invalid-name
if PyMata is None:
from PyMata.pymata import PyMata as PyMata_
PyMata = PyMata_
import serial
if not validate_config(config,
{DOMAIN: ['port']},
_LOGGER):
return False
global BOARD
try:
BOARD = ArduinoBoard(config[DOMAIN]['port'])
except (serial.serialutil.SerialException, FileNotFoundError):
_LOGGER.exception("Your port is not accessible.")
return False
if BOARD.get_firmata()[1] <= 2:
_LOGGER.error("The StandardFirmata sketch should be 2.2 or newer.")
return False
def stop_arduino(event):
""" Stop the Arduino service. """
BOARD.disconnect()
def start_arduino(event):
""" Start the Arduino service. """
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_arduino)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_arduino)
return True
class ArduinoBoard(object):
""" Represents an Arduino board. """
def __init__(self, port):
self._port = port
self._board = PyMata(self._port, verbose=False)
def set_mode(self, pin, direction, mode):
""" Sets the mode and the direction of a given pin. """
if mode == 'analog' and direction == 'in':
self._board.set_pin_mode(pin,
self._board.INPUT,
self._board.ANALOG)
elif mode == 'analog' and direction == 'out':
self._board.set_pin_mode(pin,
self._board.OUTPUT,
self._board.ANALOG)
elif mode == 'digital' and direction == 'in':
self._board.set_pin_mode(pin,
self._board.OUTPUT,
self._board.DIGITAL)
elif mode == 'digital' and direction == 'out':
self._board.set_pin_mode(pin,
self._board.OUTPUT,
self._board.DIGITAL)
elif mode == 'pwm':
self._board.set_pin_mode(pin,
self._board.OUTPUT,
self._board.PWM)
def get_analog_inputs(self):
""" Get the values from the pins. """
self._board.capability_query()
return self._board.get_analog_response_table()
def set_digital_out_high(self, pin):
""" Sets a given digital pin to high. """
self._board.digital_write(pin, 1)
def set_digital_out_low(self, pin):
""" Sets a given digital pin to low. """
self._board.digital_write(pin, 0)
def get_digital_in(self, pin):
""" Gets the value from a given digital pin. """
self._board.digital_read(pin)
def get_analog_in(self, pin):
""" Gets the value from a given analog pin. """
self._board.analog_read(pin)
def get_firmata(self):
""" Return the version of the Firmata firmware. """
return self._board.get_firmata_version()
def disconnect(self):
""" Disconnects the board and closes the serial connection. """
self._board.reset()
self._board.close()

View file

@ -6,7 +6,7 @@ Allows to setup simple automation rules via the config file.
"""
import logging
from homeassistant.loader import get_component
from homeassistant.bootstrap import prepare_setup_platform
from homeassistant.helpers import config_per_platform
from homeassistant.util import split_entity_id
from homeassistant.const import ATTR_ENTITY_ID
@ -25,9 +25,10 @@ _LOGGER = logging.getLogger(__name__)
def setup(hass, config):
""" Sets up automation. """
success = False
for p_type, p_config in config_per_platform(config, DOMAIN, _LOGGER):
platform = get_component('automation.{}'.format(p_type))
platform = prepare_setup_platform(hass, config, DOMAIN, p_type)
if platform is None:
_LOGGER.error("Unknown automation platform specified: %s", p_type)
@ -36,11 +37,12 @@ def setup(hass, config):
if platform.register(hass, p_config, _get_action(hass, p_config)):
_LOGGER.info(
"Initialized %s rule %s", p_type, p_config.get(CONF_ALIAS, ""))
success = True
else:
_LOGGER.error(
"Error setting up rule %s", p_config.get(CONF_ALIAS, ""))
return True
return success
def _get_action(hass, config):
@ -56,13 +58,16 @@ def _get_action(hass, config):
service_data = config.get(CONF_SERVICE_DATA, {})
if not isinstance(service_data, dict):
_LOGGER.error(
"%s should be a serialized JSON object", CONF_SERVICE_DATA)
_LOGGER.error("%s should be a dictionary", CONF_SERVICE_DATA)
service_data = {}
if CONF_SERVICE_ENTITY_ID in config:
service_data[ATTR_ENTITY_ID] = \
config[CONF_SERVICE_ENTITY_ID].split(",")
try:
service_data[ATTR_ENTITY_ID] = \
config[CONF_SERVICE_ENTITY_ID].split(",")
except AttributeError:
service_data[ATTR_ENTITY_ID] = \
config[CONF_SERVICE_ENTITY_ID]
hass.services.call(domain, service, service_data)

View file

@ -0,0 +1,34 @@
"""
homeassistant.components.automation.mqtt
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Offers MQTT listening automation rules.
"""
import logging
import homeassistant.components.mqtt as mqtt
DEPENDENCIES = ['mqtt']
CONF_TOPIC = 'mqtt_topic'
CONF_PAYLOAD = 'mqtt_payload'
def register(hass, config, action):
""" Listen for state changes based on `config`. """
topic = config.get(CONF_TOPIC)
payload = config.get(CONF_PAYLOAD)
if topic is None:
logging.getLogger(__name__).error(
"Missing configuration key %s", CONF_TOPIC)
return False
def mqtt_automation_listener(msg_topic, msg_payload, qos):
""" Listens for MQTT messages. """
if payload is None or payload == msg_payload:
action()
mqtt.subscribe(hass, topic, mqtt_automation_listener)
return True

View file

@ -6,6 +6,7 @@ Offers state listening automation rules.
"""
import logging
from homeassistant.helpers.event import track_state_change
from homeassistant.const import MATCH_ALL
@ -30,7 +31,7 @@ def register(hass, config, action):
""" Listens for state changes and calls action. """
action()
hass.states.track_change(
entity_id, state_automation_listener, from_state, to_state)
track_state_change(
hass, entity_id, state_automation_listener, from_state, to_state)
return True

View file

@ -5,6 +5,7 @@ homeassistant.components.automation.time
Offers time listening automation rules.
"""
from homeassistant.util import convert
from homeassistant.helpers.event import track_time_change
CONF_HOURS = "time_hours"
CONF_MINUTES = "time_minutes"
@ -21,8 +22,7 @@ def register(hass, config, action):
""" Listens for time changes and calls action. """
action()
hass.track_time_change(
time_automation_listener,
hour=hours, minute=minutes, second=seconds)
track_time_change(hass, time_automation_listener,
hour=hours, minute=minutes, second=seconds)
return True

View file

@ -0,0 +1,229 @@
# pylint: disable=too-many-lines
"""
homeassistant.components.camera
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Component to interface with various cameras.
The following features are supported:
- Returning recorded camera images and streams
- Proxying image requests via HA for external access
- Converting a still image url into a live video stream
Upcoming features
- Recording
- Snapshot
- Motion Detection Recording(for supported cameras)
- Automatic Configuration(for supported cameras)
- Creation of child entities for supported functions
- Collating motion event images passed via FTP into time based events
- A service for calling camera functions
- Camera movement(panning)
- Zoom
- Light/Nightvision toggling
- Support for more devices
- Expanded documentation
"""
import requests
import logging
import time
import re
from homeassistant.helpers.entity import Entity
from homeassistant.const import (
ATTR_ENTITY_PICTURE,
HTTP_NOT_FOUND,
ATTR_ENTITY_ID,
)
from homeassistant.helpers.entity_component import EntityComponent
DOMAIN = 'camera'
DEPENDENCIES = ['http']
GROUP_NAME_ALL_CAMERAS = 'all_cameras'
SCAN_INTERVAL = 30
ENTITY_ID_FORMAT = DOMAIN + '.{}'
SWITCH_ACTION_RECORD = 'record'
SWITCH_ACTION_SNAPSHOT = 'snapshot'
SERVICE_CAMERA = 'camera_service'
STATE_RECORDING = 'recording'
DEFAULT_RECORDING_SECONDS = 30
# Maps discovered services to their platforms
DISCOVERY_PLATFORMS = {}
FILE_DATETIME_FORMAT = '%Y-%m-%d_%H-%M-%S-%f'
DIR_DATETIME_FORMAT = '%Y-%m-%d_%H-%M-%S'
REC_DIR_PREFIX = 'recording-'
REC_IMG_PREFIX = 'recording_image-'
STATE_STREAMING = 'streaming'
STATE_IDLE = 'idle'
CAMERA_PROXY_URL = '/api/camera_proxy_stream/{0}'
CAMERA_STILL_URL = '/api/camera_proxy/{0}'
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?time={1}'
MULTIPART_BOUNDARY = '--jpegboundary'
MJPEG_START_HEADER = 'Content-type: {0}\r\n\r\n'
# pylint: disable=too-many-branches
def setup(hass, config):
""" Track states and offer events for sensors. """
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
DISCOVERY_PLATFORMS)
component.setup(config)
# -------------------------------------------------------------------------
# CAMERA COMPONENT ENDPOINTS
# -------------------------------------------------------------------------
# The following defines the endpoints for serving images from the camera
# via the HA http server. This is means that you can access images from
# your camera outside of your LAN without the need for port forwards etc.
# Because the authentication header can't be added in image requests these
# endpoints are secured with session based security.
# pylint: disable=unused-argument
def _proxy_camera_image(handler, path_match, data):
""" Proxies the camera image via the HA server. """
entity_id = path_match.group(ATTR_ENTITY_ID)
camera = None
if entity_id in component.entities.keys():
camera = component.entities[entity_id]
if camera:
response = camera.camera_image()
handler.wfile.write(response)
else:
handler.send_response(HTTP_NOT_FOUND)
hass.http.register_path(
'GET',
re.compile(r'/api/camera_proxy/(?P<entity_id>[a-zA-Z\._0-9]+)'),
_proxy_camera_image)
# pylint: disable=unused-argument
def _proxy_camera_mjpeg_stream(handler, path_match, data):
""" Proxies the camera image as an mjpeg stream via the HA server.
This function takes still images from the IP camera and turns them
into an MJPEG stream. This means that HA can return a live video
stream even with only a still image URL available.
"""
entity_id = path_match.group(ATTR_ENTITY_ID)
camera = None
if entity_id in component.entities.keys():
camera = component.entities[entity_id]
if not camera:
handler.send_response(HTTP_NOT_FOUND)
handler.end_headers()
return
try:
camera.is_streaming = True
camera.update_ha_state()
handler.request.sendall(bytes('HTTP/1.1 200 OK\r\n', 'utf-8'))
handler.request.sendall(bytes(
'Content-type: multipart/x-mixed-replace; \
boundary=--jpgboundary\r\n\r\n', 'utf-8'))
handler.request.sendall(bytes('--jpgboundary\r\n', 'utf-8'))
# MJPEG_START_HEADER.format()
while True:
img_bytes = camera.camera_image()
headers_str = '\r\n'.join((
'Content-length: {}'.format(len(img_bytes)),
'Content-type: image/jpeg',
)) + '\r\n\r\n'
handler.request.sendall(
bytes(headers_str, 'utf-8') +
img_bytes +
bytes('\r\n', 'utf-8'))
handler.request.sendall(
bytes('--jpgboundary\r\n', 'utf-8'))
except (requests.RequestException, IOError):
camera.is_streaming = False
camera.update_ha_state()
camera.is_streaming = False
hass.http.register_path(
'GET',
re.compile(
r'/api/camera_proxy_stream/(?P<entity_id>[a-zA-Z\._0-9]+)'),
_proxy_camera_mjpeg_stream)
return True
class Camera(Entity):
""" The base class for camera components """
def __init__(self):
self.is_streaming = False
@property
# pylint: disable=no-self-use
def is_recording(self):
""" Returns true if the device is recording """
return False
@property
# pylint: disable=no-self-use
def brand(self):
""" Should return a string of the camera brand """
return None
@property
# pylint: disable=no-self-use
def model(self):
""" Returns string of camera model """
return None
def camera_image(self):
""" Return bytes of camera image """
raise NotImplementedError()
@property
def state(self):
""" Returns the state of the entity. """
if self.is_recording:
return STATE_RECORDING
elif self.is_streaming:
return STATE_STREAMING
else:
return STATE_IDLE
@property
def state_attributes(self):
""" Returns optional state attributes. """
attr = {
ATTR_ENTITY_PICTURE: ENTITY_IMAGE_URL.format(
self.entity_id, time.time()),
}
if self.model:
attr['model_name'] = self.model
if self.brand:
attr['brand'] = self.brand
return attr

View file

@ -0,0 +1,91 @@
"""
homeassistant.components.camera.generic
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for IP Cameras.
This component provides basic support for IP cameras. For the basic support to
work you camera must support accessing a JPEG snapshot via a URL and you will
need to specify the "still_image_url" parameter which should be the location of
the JPEG image.
As part of the basic support the following features will be provided:
- MJPEG video streaming
- Saving a snapshot
- Recording(JPEG frame capture)
To use this component, add the following to your configuration.yaml file.
camera:
platform: generic
name: Door Camera
username: YOUR_USERNAME
password: YOUR_PASSWORD
still_image_url: http://YOUR_CAMERA_IP_AND_PORT/image.jpg
Variables:
still_image_url
*Required
The URL your camera serves the image on, eg. http://192.168.1.21:2112/
name
*Optional
This parameter allows you to override the name of your camera in Home
Assistant.
username
*Optional
The username for accessing your camera.
password
*Optional
The password for accessing your camera.
"""
import logging
from requests.auth import HTTPBasicAuth
from homeassistant.helpers import validate_config
from homeassistant.components.camera import DOMAIN
from homeassistant.components.camera import Camera
import requests
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Adds a generic IP Camera. """
if not validate_config({DOMAIN: config}, {DOMAIN: ['still_image_url']},
_LOGGER):
return None
add_devices_callback([GenericCamera(config)])
# pylint: disable=too-many-instance-attributes
class GenericCamera(Camera):
"""
A generic implementation of an IP camera that is reachable over a URL.
"""
def __init__(self, device_info):
super().__init__()
self._name = device_info.get('name', 'Generic Camera')
self._username = device_info.get('username')
self._password = device_info.get('password')
self._still_image_url = device_info['still_image_url']
def camera_image(self):
""" Return a still image reponse from the camera. """
if self._username and self._password:
response = requests.get(
self._still_image_url,
auth=HTTPBasicAuth(self._username, self._password))
else:
response = requests.get(self._still_image_url)
return response.content
@property
def name(self):
""" Return the name of this device. """
return self._name

View file

@ -8,9 +8,9 @@ This is more a proof of concept.
import logging
import re
import homeassistant
from homeassistant import core
from homeassistant.const import (
ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
DOMAIN = "conversation"
DEPENDENCIES = []
@ -44,7 +44,7 @@ def setup(hass, config):
entity_ids = [
state.entity_id for state in hass.states.all()
if state.attributes.get(ATTR_FRIENDLY_NAME, "").lower() == name]
if state.name.lower() == name]
if not entity_ids:
logger.error(
@ -52,16 +52,14 @@ def setup(hass, config):
return
if command == 'on':
hass.services.call(
homeassistant.DOMAIN, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity_ids,
}, blocking=True)
hass.services.call(core.DOMAIN, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity_ids,
}, blocking=True)
elif command == 'off':
hass.services.call(
homeassistant.DOMAIN, SERVICE_TURN_OFF, {
ATTR_ENTITY_ID: entity_ids,
}, blocking=True)
hass.services.call(core.DOMAIN, SERVICE_TURN_OFF, {
ATTR_ENTITY_ID: entity_ids,
}, blocking=True)
else:
logger.error(

View file

@ -1,20 +1,20 @@
"""
homeassistant.components.demo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sets up a demo environment that mimics interaction with devices
Sets up a demo environment that mimics interaction with devices.
"""
import time
import homeassistant as ha
import homeassistant.core as ha
import homeassistant.bootstrap as bootstrap
import homeassistant.loader as loader
from homeassistant.const import (
CONF_PLATFORM, ATTR_ENTITY_PICTURE, ATTR_ENTITY_ID)
CONF_PLATFORM, ATTR_ENTITY_PICTURE, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME)
DOMAIN = "demo"
DEPENDENCIES = []
DEPENDENCIES = ['introduction', 'conversation']
COMPONENTS_WITH_DEMO_PLATFORM = [
'switch', 'light', 'thermostat', 'sensor', 'media_player', 'notify']
@ -32,7 +32,13 @@ def setup(hass, config):
hass.states.set('a.Demo_Mode', 'Enabled')
# Setup sun
loader.get_component('sun').setup(hass, config)
if not hass.config.latitude:
hass.config.latitude = '32.87336'
if not hass.config.longitude:
hass.config.longitude = '117.22743'
bootstrap.setup_component(hass, 'sun')
# Setup demo platforms
for component in COMPONENTS_WITH_DEMO_PLATFORM:
@ -40,17 +46,29 @@ def setup(hass, config):
hass, component, {component: {CONF_PLATFORM: 'demo'}})
# Setup room groups
lights = hass.states.entity_ids('light')
switches = hass.states.entity_ids('switch')
group.setup_group(hass, 'living room', [lights[0], lights[1], switches[0]])
group.setup_group(hass, 'bedroom', [lights[2], switches[1]])
lights = sorted(hass.states.entity_ids('light'))
switches = sorted(hass.states.entity_ids('switch'))
media_players = sorted(hass.states.entity_ids('media_player'))
group.setup_group(hass, 'living room', [lights[2], lights[1], switches[0],
media_players[1]])
group.setup_group(hass, 'bedroom', [lights[0], switches[1],
media_players[0]])
# Setup IP Camera
bootstrap.setup_component(
hass, 'camera',
{'camera': {
'platform': 'generic',
'name': 'IP Camera',
'still_image_url': 'http://194.218.96.92/jpg/image.jpg',
}})
# Setup scripts
bootstrap.setup_component(
hass, 'script',
{'script': {
'demo': {
'alias': 'Demo {}'.format(lights[0]),
'alias': 'Toggle {}'.format(lights[0].split('.')[1]),
'sequence': [{
'execute_service': 'light.turn_off',
'service_data': {ATTR_ENTITY_ID: lights[0]}
@ -87,17 +105,17 @@ def setup(hass, config):
# Setup fake device tracker
hass.states.set("device_tracker.paulus", "home",
{ATTR_ENTITY_PICTURE:
"http://graph.facebook.com/schoutsen/picture"})
"http://graph.facebook.com/297400035/picture",
ATTR_FRIENDLY_NAME: 'Paulus'})
hass.states.set("device_tracker.anne_therese", "not_home",
{ATTR_ENTITY_PICTURE:
"http://graph.facebook.com/anne.t.frederiksen/picture"})
{ATTR_FRIENDLY_NAME: 'Anne Therese'})
hass.states.set("group.all_devices", "home",
{
"auto": True,
ATTR_ENTITY_ID: [
"device_tracker.Paulus",
"device_tracker.Anne_Therese"
"device_tracker.paulus",
"device_tracker.anne_therese"
]
})

View file

@ -6,8 +6,10 @@ Provides functionality to turn on lights based on
the state of the sun and devices.
"""
import logging
from datetime import datetime, timedelta
from datetime import timedelta
from homeassistant.helpers.event import track_point_in_time, track_state_change
import homeassistant.util.dt as dt_util
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
from . import light, sun, device_tracker, group
@ -90,14 +92,14 @@ def setup(hass, config):
if start_point:
for index, light_id in enumerate(light_ids):
hass.track_point_in_time(turn_on(light_id),
(start_point +
index * LIGHT_TRANSITION_TIME))
track_point_in_time(
hass, turn_on(light_id),
(start_point + index * LIGHT_TRANSITION_TIME))
# Track every time sun rises so we can schedule a time-based
# pre-sun set event
hass.states.track_change(sun.ENTITY_ID, schedule_light_on_sun_rise,
sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON)
track_state_change(hass, sun.ENTITY_ID, schedule_light_on_sun_rise,
sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON)
# If the sun is already above horizon
# schedule the time-based pre-sun set event
@ -115,7 +117,7 @@ def setup(hass, config):
new_state.state == STATE_HOME:
# These variables are needed for the elif check
now = datetime.now()
now = dt_util.now()
start_point = calc_time_for_light_when_sunset()
# Do we need lights?
@ -156,13 +158,13 @@ def setup(hass, config):
light.turn_off(hass, light_ids)
# Track home coming of each device
hass.states.track_change(
device_entity_ids, check_light_on_dev_state_change,
track_state_change(
hass, device_entity_ids, check_light_on_dev_state_change,
STATE_NOT_HOME, STATE_HOME)
# Track when all devices are gone to shut down lights
hass.states.track_change(
device_group, check_light_on_dev_state_change,
track_state_change(
hass, device_group, check_light_on_dev_state_change,
STATE_HOME, STATE_NOT_HOME)
return True

View file

@ -8,15 +8,18 @@ import logging
import threading
import os
import csv
from datetime import datetime, timedelta
from datetime import timedelta
from homeassistant.loader import get_component
from homeassistant.helpers import validate_config
from homeassistant.helpers.entity import _OVERWRITE
import homeassistant.util as util
import homeassistant.util.dt as dt_util
from homeassistant.bootstrap import prepare_setup_platform
from homeassistant.helpers.event import track_utc_time_change
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"
@ -40,6 +43,8 @@ CONF_SECONDS = "interval_seconds"
DEFAULT_CONF_SECONDS = 12
TRACK_NEW_DEVICES = "track_new_devices"
_LOGGER = logging.getLogger(__name__)
@ -58,18 +63,19 @@ def setup(hass, config):
tracker_type = config[DOMAIN].get(CONF_PLATFORM)
tracker_implementation = get_component(
'device_tracker.{}'.format(tracker_type))
tracker_implementation = \
prepare_setup_platform(hass, config, DOMAIN, tracker_type)
if tracker_implementation is None:
_LOGGER.error("Unknown device_tracker type specified.")
_LOGGER.error("Unknown device_tracker type specified: %s.",
tracker_type)
return False
device_scanner = tracker_implementation.get_scanner(hass, config)
if device_scanner is None:
_LOGGER.error("Failed to initialize device scanner for %s",
_LOGGER.error("Failed to initialize device scanner: %s",
tracker_type)
return False
@ -77,7 +83,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
@ -86,13 +95,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()
@ -113,7 +125,7 @@ class DeviceTracker(object):
""" Reload known devices file. """
self._read_known_devices_file()
self.update_devices(datetime.now())
self.update_devices(dt_util.utcnow())
dev_group.update_tracked_entity_ids(self.device_entity_ids)
@ -125,7 +137,7 @@ class DeviceTracker(object):
seconds = range(0, 60, seconds)
_LOGGER.info("Device tracker interval second=%s", seconds)
hass.track_time_change(update_device_state, second=seconds)
track_utc_time_change(hass, update_device_state, second=seconds)
hass.services.register(DOMAIN,
SERVICE_DEVICE_TRACKER_RELOAD,
@ -151,66 +163,40 @@ class DeviceTracker(object):
state = STATE_HOME if is_home else STATE_NOT_HOME
# overwrite properties that have been set in the config file
attr = dict(dev_info['state_attr'])
attr.update(_OVERWRITE.get(dev_info['entity_id'], {}))
self.hass.states.set(
dev_info['entity_id'], state,
dev_info['state_attr'])
dev_info['entity_id'], state, attr)
def update_devices(self, now):
""" Update device states based on the found devices. """
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):
@ -226,7 +212,6 @@ class DeviceTracker(object):
self.untracked_devices.clear()
with open(known_dev_path) as inp:
default_last_seen = datetime(1990, 1, 1)
# To track which devices need an entity_id assigned
need_entity_id = []
@ -247,10 +232,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 = {
@ -275,21 +257,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(
@ -308,3 +276,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

View file

@ -0,0 +1,191 @@
"""
homeassistant.components.device_tracker.actiontec
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning an Actiontec MI424WR
(Verizon FIOS) router for device presence.
This device tracker needs telnet to be enabled on the router.
Configuration:
To use the Actiontec tracker you will need to add something like the
following to your configuration.yaml file. If you experience disconnects
you can modify the home_interval variable.
device_tracker:
platform: actiontec
host: YOUR_ROUTER_IP
username: YOUR_ADMIN_USERNAME
password: YOUR_ADMIN_PASSWORD
# optional:
home_interval: 10
Variables:
host
*Required
The IP address of your router, e.g. 192.168.1.1.
username
*Required
The username of an user with administrative privileges, usually 'admin'.
password
*Required
The password for your given admin account.
home_interval
*Optional
If the home_interval is set then the component will not let a device
be AWAY if it has been HOME in the last home_interval minutes. This is
in addition to the 3 minute wait built into the device_tracker component.
"""
import logging
from datetime import timedelta
from collections import namedtuple
import re
import threading
import telnetlib
import homeassistant.util.dt as dt_util
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers import validate_config
from homeassistant.util import Throttle, convert
from homeassistant.components.device_tracker import DOMAIN
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
# interval in minutes to exclude devices from a scan while they are home
CONF_HOME_INTERVAL = "home_interval"
_LOGGER = logging.getLogger(__name__)
_LEASES_REGEX = re.compile(
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})' +
r'\smac:\s(?P<mac>([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))')
# pylint: disable=unused-argument
def get_scanner(hass, config):
""" Validates config and returns an Actiontec scanner. """
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
_LOGGER):
return None
scanner = ActiontecDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
Device = namedtuple("Device", ["mac", "ip", "last_update"])
class ActiontecDeviceScanner(object):
"""
This class queries a an actiontec router for connected devices.
Adapted from DD-WRT scanner.
"""
def __init__(self, config):
self.host = config[CONF_HOST]
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
minutes = convert(config.get(CONF_HOME_INTERVAL), int, 0)
self.home_interval = timedelta(minutes=minutes)
self.lock = threading.Lock()
self.last_results = []
# Test the router is accessible
data = self.get_actiontec_data()
self.success_init = data is not None
_LOGGER.info("actiontec scanner initialized")
if self.home_interval:
_LOGGER.info("home_interval set to: %s", self.home_interval)
def scan_devices(self):
"""
Scans for new devices and return a list containing found device ids.
"""
self._update_info()
return [client.mac for client in self.last_results]
def get_device_name(self, device):
""" Returns the name of the given device or None if we don't know. """
if not self.last_results:
return None
for client in self.last_results:
if client.mac == device:
return client.ip
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""
Ensures the information from the Actiontec MI424WR router is up
to date. Returns boolean if scanning successful.
"""
_LOGGER.info("Scanning")
if not self.success_init:
return False
with self.lock:
exclude_targets = set()
exclude_target_list = []
now = dt_util.now()
if self.home_interval:
for host in self.last_results:
if host.last_update + self.home_interval > now:
exclude_targets.add(host)
if len(exclude_targets) > 0:
exclude_target_list = [t.ip for t in exclude_targets]
actiontec_data = self.get_actiontec_data()
if not actiontec_data:
return False
self.last_results = []
for client in exclude_target_list:
if client in actiontec_data:
actiontec_data.pop(client)
for name, data in actiontec_data.items():
device = Device(data['mac'], name, now)
self.last_results.append(device)
self.last_results.extend(exclude_targets)
_LOGGER.info("actiontec scan successful")
return True
def get_actiontec_data(self):
""" Retrieve data from Actiontec MI424WR and return parsed result. """
try:
telnet = telnetlib.Telnet(self.host)
telnet.read_until(b'Username: ')
telnet.write((self.username + '\n').encode('ascii'))
telnet.read_until(b'Password: ')
telnet.write((self.password + '\n').encode('ascii'))
prompt = telnet.read_until(
b'Wireless Broadband Router> ').split(b'\n')[-1]
telnet.write('firewall mac_cache_dump\n'.encode('ascii'))
telnet.write('\n'.encode('ascii'))
telnet.read_until(prompt)
leases_result = telnet.read_until(prompt).split(b'\n')[1:-1]
telnet.write('exit\n'.encode('ascii'))
except EOFError:
_LOGGER.exception("Unexpected response from router")
return
except ConnectionRefusedError:
_LOGGER.exception("Connection refused by router," +
" is telnet enabled?")
return None
devices = {}
for lease in leases_result:
match = _LEASES_REGEX.search(lease.decode('utf-8'))
if match is not None:
devices[match.group('ip')] = {
'ip': match.group('ip'),
'mac': match.group('mac').upper()
}
return devices

View file

@ -0,0 +1,148 @@
"""
homeassistant.components.device_tracker.aruba
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning a Aruba Access Point for device
presence.
This device tracker needs telnet to be enabled on the router.
Configuration:
To use the Aruba tracker you will need to add something like the following
to your configuration.yaml file. You also need to enable Telnet in the
configuration page of your router.
device_tracker:
platform: aruba
host: YOUR_ACCESS_POINT_IP
username: YOUR_ADMIN_USERNAME
password: YOUR_ADMIN_PASSWORD
Variables:
host
*Required
The IP address of your router, e.g. 192.168.1.1.
username
*Required
The username of an user with administrative privileges, usually 'admin'.
password
*Required
The password for your given admin account.
"""
import logging
from datetime import timedelta
import re
import threading
import telnetlib
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers import validate_config
from homeassistant.util import Throttle
from homeassistant.components.device_tracker import DOMAIN
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
_LOGGER = logging.getLogger(__name__)
_DEVICES_REGEX = re.compile(
r'(?P<name>([^\s]+))\s+' +
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s+' +
r'(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))\s+')
# pylint: disable=unused-argument
def get_scanner(hass, config):
""" Validates config and returns a Aruba scanner. """
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
_LOGGER):
return None
scanner = ArubaDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
class ArubaDeviceScanner(object):
""" This class queries a Aruba Acces Point for connected devices. """
def __init__(self, config):
self.host = config[CONF_HOST]
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.lock = threading.Lock()
self.last_results = {}
# Test the router is accessible
data = self.get_aruba_data()
self.success_init = data is not None
def scan_devices(self):
"""
Scans for new devices and return a list containing found device IDs.
"""
self._update_info()
return [client['mac'] for client in self.last_results]
def get_device_name(self, device):
""" Returns the name of the given device or None if we don't know. """
if not self.last_results:
return None
for client in self.last_results:
if client['mac'] == device:
return client['name']
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""
Ensures the information from the Aruba Access Point is up to date.
Returns boolean if scanning successful.
"""
if not self.success_init:
return False
with self.lock:
data = self.get_aruba_data()
if not data:
return False
self.last_results = data.values()
return True
def get_aruba_data(self):
""" Retrieve data from Aruba Access Point and return parsed result. """
try:
telnet = telnetlib.Telnet(self.host)
telnet.read_until(b'User: ')
telnet.write((self.username + '\r\n').encode('ascii'))
telnet.read_until(b'Password: ')
telnet.write((self.password + '\r\n').encode('ascii'))
telnet.read_until(b'#')
telnet.write(('show clients\r\n').encode('ascii'))
devices_result = telnet.read_until(b'#').split(b'\r\n')
telnet.write('exit\r\n'.encode('ascii'))
except EOFError:
_LOGGER.exception("Unexpected response from router")
return
except ConnectionRefusedError:
_LOGGER.exception("Connection refused by router," +
" is telnet enabled?")
return
devices = {}
for device in devices_result:
match = _DEVICES_REGEX.search(device.decode('utf-8'))
if match:
devices[match.group('ip')] = {
'ip': match.group('ip'),
'mac': match.group('mac').upper(),
'name': match.group('name')
}
return devices

View file

@ -0,0 +1,171 @@
"""
homeassistant.components.device_tracker.asuswrt
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning a ASUSWRT router for device
presence.
This device tracker needs telnet to be enabled on the router.
Configuration:
To use the ASUSWRT tracker you will need to add something like the following
to your configuration.yaml file.
device_tracker:
platform: asuswrt
host: YOUR_ROUTER_IP
username: YOUR_ADMIN_USERNAME
password: YOUR_ADMIN_PASSWORD
Variables:
host
*Required
The IP address of your router, e.g. 192.168.1.1.
username
*Required
The username of an user with administrative privileges, usually 'admin'.
password
*Required
The password for your given admin account.
"""
import logging
from datetime import timedelta
import re
import threading
import telnetlib
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers import validate_config
from homeassistant.util import Throttle
from homeassistant.components.device_tracker import DOMAIN
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
_LEASES_REGEX = re.compile(
r'\w+\s' +
r'(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))\s' +
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' +
r'(?P<host>([^\s]+))')
_IP_NEIGH_REGEX = re.compile(
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' +
r'\w+\s' +
r'\w+\s' +
r'(\w+\s(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))))?\s' +
r'(?P<status>(\w+))')
# pylint: disable=unused-argument
def get_scanner(hass, config):
""" Validates config and returns an ASUS-WRT scanner. """
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
_LOGGER):
return None
scanner = AsusWrtDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
class AsusWrtDeviceScanner(object):
"""
This class queries a router running ASUSWRT firmware
for connected devices. Adapted from DD-WRT scanner.
"""
def __init__(self, config):
self.host = config[CONF_HOST]
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.lock = threading.Lock()
self.last_results = {}
# Test the router is accessible
data = self.get_asuswrt_data()
self.success_init = data is not None
def scan_devices(self):
"""
Scans for new devices and return a list containing found device IDs.
"""
self._update_info()
return [client['mac'] for client in self.last_results]
def get_device_name(self, device):
""" Returns the name of the given device or None if we don't know. """
if not self.last_results:
return None
for client in self.last_results:
if client['mac'] == device:
return client['host']
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""
Ensures the information from the ASUSWRT router is up to date.
Returns boolean if scanning successful.
"""
if not self.success_init:
return False
with self.lock:
_LOGGER.info("Checking ARP")
data = self.get_asuswrt_data()
if not data:
return False
active_clients = [client for client in data.values() if
client['status'] == 'REACHABLE' or
client['status'] == 'DELAY' or
client['status'] == 'STALE']
self.last_results = active_clients
return True
def get_asuswrt_data(self):
""" Retrieve data from ASUSWRT and return parsed result. """
try:
telnet = telnetlib.Telnet(self.host)
telnet.read_until(b'login: ')
telnet.write((self.username + '\n').encode('ascii'))
telnet.read_until(b'Password: ')
telnet.write((self.password + '\n').encode('ascii'))
prompt_string = telnet.read_until(b'#').split(b'\n')[-1]
telnet.write('ip neigh\n'.encode('ascii'))
neighbors = telnet.read_until(prompt_string).split(b'\n')[1:-1]
telnet.write('cat /var/lib/misc/dnsmasq.leases\n'.encode('ascii'))
leases_result = telnet.read_until(prompt_string).split(b'\n')[1:-1]
telnet.write('exit\n'.encode('ascii'))
except EOFError:
_LOGGER.exception("Unexpected response from router")
return
except ConnectionRefusedError:
_LOGGER.exception("Connection refused by router," +
" is telnet enabled?")
return
devices = {}
for lease in leases_result:
match = _LEASES_REGEX.search(lease.decode('utf-8'))
devices[match.group('ip')] = {
'ip': match.group('ip'),
'mac': match.group('mac').upper(),
'host': match.group('host'),
'status': ''
}
for neighbor in neighbors:
match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8'))
if match.group('ip') in devices:
devices[match.group('ip')]['status'] = match.group('status')
return devices

View file

@ -1,4 +1,34 @@
""" Supports scanning a DD-WRT router. """
"""
homeassistant.components.device_tracker.ddwrt
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning a DD-WRT router for device
presence.
Configuration:
To use the DD-WRT tracker you will need to add something like the following
to your configuration.yaml file.
device_tracker:
platform: ddwrt
host: YOUR_ROUTER_IP
username: YOUR_ADMIN_USERNAME
password: YOUR_ADMIN_PASSWORD
Variables:
host
*Required
The IP address of your router, e.g. 192.168.1.1.
username
*Required
The username of an user with administrative privileges, usually 'admin'.
password
*Required
The password for your given admin account.
"""
import logging
from datetime import timedelta
import re
@ -20,7 +50,7 @@ _DDWRT_DATA_REGEX = re.compile(r'\{(\w+)::([^\}]*)\}')
# pylint: disable=unused-argument
def get_scanner(hass, config):
""" Validates config and returns a DdWrt scanner. """
""" Validates config and returns a DD-WRT scanner. """
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
_LOGGER):
@ -33,7 +63,8 @@ def get_scanner(hass, config):
# pylint: disable=too-many-instance-attributes
class DdWrtDeviceScanner(object):
""" This class queries a wireless router running DD-WRT firmware
"""
This class queries a wireless router running DD-WRT firmware
for connected devices. Adapted from Tomato scanner.
"""
@ -54,8 +85,9 @@ class DdWrtDeviceScanner(object):
self.success_init = data is not None
def scan_devices(self):
""" Scans for new devices and return a
list containing found device ids. """
"""
Scans for new devices and return a list containing found device ids.
"""
self._update_info()
@ -93,8 +125,10 @@ class DdWrtDeviceScanner(object):
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
""" Ensures the information from the DdWrt router is up to date.
Returns boolean if scanning successful. """
"""
Ensures the information from the DD-WRT router is up to date.
Returns boolean if scanning successful.
"""
if not self.success_init:
return False
@ -111,8 +145,8 @@ class DdWrtDeviceScanner(object):
self.last_results = []
active_clients = data.get('active_wireless', None)
if active_clients:
# This is really lame, instead of using JSON the ddwrt UI
# uses it's own data format for some reason and then
# This is really lame, instead of using JSON the DD-WRT UI
# uses its own data format for some reason and then
# regex's out values so I guess I have to do the same,
# LAME!!!
@ -132,7 +166,7 @@ class DdWrtDeviceScanner(object):
return False
def get_ddwrt_data(self, url):
""" Retrieve data from DD-WRT and return parsed result """
""" Retrieve data from DD-WRT and return parsed result. """
try:
response = requests.get(
url,
@ -154,8 +188,7 @@ class DdWrtDeviceScanner(object):
def _parse_ddwrt_response(data_str):
""" Parse the awful DD-WRT data format, why didn't they use JSON????.
This code is a python version of how they are parsing in the JS """
""" Parse the DD-WRT data format. """
return {
key: val for key, val in _DDWRT_DATA_REGEX
.findall(data_str)}

View file

@ -1,4 +1,37 @@
""" Supports scanning a OpenWRT router. """
"""
homeassistant.components.device_tracker.luci
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning a OpenWRT router for device
presence.
It's required that the luci RPC package is installed on the OpenWRT router:
# opkg install luci-mod-rpc
Configuration:
To use the Luci tracker you will need to add something like the following
to your configuration.yaml file.
device_tracker:
platform: luci
host: YOUR_ROUTER_IP
username: YOUR_ADMIN_USERNAME
password: YOUR_ADMIN_PASSWORD
Variables:
host
*Required
The IP address of your router, e.g. 192.168.1.1.
username
*Required
The username of an user with administrative privileges, usually 'admin'.
password
*Required
The password for your given admin account.
"""
import logging
import json
from datetime import timedelta
@ -31,7 +64,8 @@ def get_scanner(hass, config):
# pylint: disable=too-many-instance-attributes
class LuciDeviceScanner(object):
""" This class queries a wireless router running OpenWrt firmware
"""
This class queries a wireless router running OpenWrt firmware
for connected devices. Adapted from Tomato scanner.
# opkg install luci-mod-rpc
@ -60,8 +94,9 @@ class LuciDeviceScanner(object):
self.success_init = self.token is not None
def scan_devices(self):
""" Scans for new devices and return a
list containing found device ids. """
"""
Scans for new devices and return a list containing found device ids.
"""
self._update_info()
@ -79,17 +114,20 @@ 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):
""" Ensures the information from the Luci router is up to date.
Returns boolean if scanning successful. """
"""
Ensures the information from the Luci router is up to date.
Returns boolean if scanning successful.
"""
if not self.success_init:
return False
@ -143,6 +181,6 @@ def _req_json_rpc(url, method, *args, **kwargs):
def _get_token(host, username, password):
""" Get authentication token for the given host+username+password """
""" Get authentication token for the given host+username+password. """
url = 'http://{}/cgi-bin/luci/rpc/auth'.format(host)
return _req_json_rpc(url, 'login', username, password)

View file

@ -1,10 +1,39 @@
""" Supports scanning a Netgear router. """
"""
homeassistant.components.device_tracker.netgear
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning a Netgear router for device
presence.
Configuration:
To use the Netgear tracker you will need to add something like the following
to your configuration.yaml file.
device_tracker:
platform: netgear
host: YOUR_ROUTER_IP
username: YOUR_ADMIN_USERNAME
password: YOUR_ADMIN_PASSWORD
Variables:
host
*Required
The IP address of your router, e.g. 192.168.1.1.
username
*Required
The username of an user with administrative privileges, usually 'admin'.
password
*Required
The password for your given admin account.
"""
import logging
from datetime import timedelta
import threading
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers import validate_config
from homeassistant.util import Throttle
from homeassistant.components.device_tracker import DOMAIN
@ -12,58 +41,57 @@ from homeassistant.components.device_tracker import DOMAIN
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pynetgear==0.3']
def get_scanner(hass, config):
""" Validates config and returns a Netgear scanner. """
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
_LOGGER):
info = config[DOMAIN]
host = info.get(CONF_HOST)
username = info.get(CONF_USERNAME)
password = info.get(CONF_PASSWORD)
if password is not None and host is None:
_LOGGER.warning('Found username or password but no host')
return None
info = config[DOMAIN]
scanner = NetgearDeviceScanner(
info[CONF_HOST], info[CONF_USERNAME], info[CONF_PASSWORD])
scanner = NetgearDeviceScanner(host, username, password)
return scanner if scanner.success_init else None
class NetgearDeviceScanner(object):
""" This class queries a Netgear wireless router using the SOAP-api. """
""" This class queries a Netgear wireless router using the SOAP-API. """
def __init__(self, host, username, password):
import pynetgear
self.last_results = []
try:
# Pylint does not play nice if not every folders has an __init__.py
# pylint: disable=no-name-in-module, import-error
import homeassistant.external.pynetgear.pynetgear as pynetgear
except ImportError:
_LOGGER.exception(
("Failed to import pynetgear. "
"Did you maybe not run `git submodule init` "
"and `git submodule update`?"))
self.success_init = False
return
self._api = pynetgear.Netgear(host, username, password)
self.lock = threading.Lock()
if host is None:
print("BIER")
self._api = pynetgear.Netgear()
elif username is None:
self._api = pynetgear.Netgear(password, host)
else:
self._api = pynetgear.Netgear(password, host, username)
_LOGGER.info("Logging in")
self.success_init = self._api.login()
results = self._api.get_attached_devices()
self.success_init = results is not None
if self.success_init:
self._update_info()
self.last_results = results
else:
_LOGGER.error("Failed to Login")
def scan_devices(self):
""" Scans for new devices and return a
list containing found device ids. """
"""
Scans for new devices and return a list containing found device ids.
"""
self._update_info()
return (device.mac for device in self.last_results)
@ -78,8 +106,10 @@ class NetgearDeviceScanner(object):
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
""" Retrieves latest information from the Netgear router.
Returns boolean if scanning successful. """
"""
Retrieves latest information from the Netgear router.
Returns boolean if scanning successful.
"""
if not self.success_init:
return

View file

@ -1,13 +1,36 @@
""" Supports scanning using nmap. """
"""
homeassistant.components.device_tracker.nmap
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning a network with nmap.
Configuration:
To use the nmap tracker you will need to add something like the following
to your configuration.yaml file.
device_tracker:
platform: nmap_tracker
hosts: 192.168.1.1/24
Variables:
hosts
*Required
The IP addresses to scan in the network-prefix notation (192.168.1.1/24) or
the range notation (192.168.1.1-255).
home_interval
*Optional
Number of minutes it will not scan devices that it found in previous results.
This is to save battery.
"""
import logging
from datetime import timedelta, datetime
from datetime import timedelta
from collections import namedtuple
import subprocess
import re
from libnmap.process import NmapProcess
from libnmap.parser import NmapParser, NmapParserException
import homeassistant.util.dt as dt_util
from homeassistant.const import CONF_HOSTS
from homeassistant.helpers import validate_config
from homeassistant.util import Throttle, convert
@ -21,6 +44,8 @@ _LOGGER = logging.getLogger(__name__)
# interval in minutes to exclude devices from a scan while they are home
CONF_HOME_INTERVAL = "home_interval"
REQUIREMENTS = ['python-nmap==0.4.1']
def get_scanner(hass, config):
""" Validates config and returns a Nmap scanner. """
@ -36,7 +61,7 @@ Device = namedtuple("Device", ["mac", "name", "ip", "last_update"])
def _arp(ip_address):
""" Get the MAC address for a given IP """
""" Get the MAC address for a given IP. """
cmd = ['arp', '-n', ip_address]
arp = subprocess.Popen(cmd, stdout=subprocess.PIPE)
out, _ = arp.communicate()
@ -44,11 +69,11 @@ def _arp(ip_address):
if match:
return match.group(0)
_LOGGER.info("No MAC address found for %s", ip_address)
return ''
return None
class NmapDeviceScanner(object):
""" This class scans for devices using nmap """
""" This class scans for devices using nmap. """
def __init__(self, config):
self.last_results = []
@ -57,13 +82,13 @@ class NmapDeviceScanner(object):
minutes = convert(config.get(CONF_HOME_INTERVAL), int, 0)
self.home_interval = timedelta(minutes=minutes)
self.success_init = True
self._update_info()
self.success_init = self._update_info()
_LOGGER.info("nmap scanner initialized")
def scan_devices(self):
""" Scans for new devices and return a
list containing found device ids. """
"""
Scans for new devices and return a list containing found device ids.
"""
self._update_info()
@ -80,46 +105,21 @@ class NmapDeviceScanner(object):
else:
return None
def _parse_results(self, stdout):
""" Parses results from an nmap scan.
Returns True if successful, False otherwise. """
try:
results = NmapParser.parse(stdout)
now = datetime.now()
self.last_results = []
for host in results.hosts:
if host.is_up():
if host.hostnames:
name = host.hostnames[0]
else:
name = host.ipv4
if host.mac:
mac = host.mac
else:
mac = _arp(host.ipv4)
if mac:
device = Device(mac, name, host.ipv4, now)
self.last_results.append(device)
_LOGGER.info("nmap scan successful")
return True
except NmapParserException as parse_exc:
_LOGGER.error("failed to parse nmap results: %s", parse_exc.msg)
self.last_results = []
return False
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
""" Scans the network for devices.
Returns boolean if scanning successful. """
if not self.success_init:
return False
"""
Scans the network for devices.
Returns boolean if scanning successful.
"""
_LOGGER.info("Scanning")
from nmap import PortScanner, PortScannerError
scanner = PortScanner()
options = "-F --host-timeout 5"
exclude_targets = set()
if self.home_interval:
now = datetime.now()
now = dt_util.now()
for host in self.last_results:
if host.last_update + self.home_interval > now:
exclude_targets.add(host)
@ -127,14 +127,24 @@ class NmapDeviceScanner(object):
target_list = [t.ip for t in exclude_targets]
options += " --exclude {}".format(",".join(target_list))
nmap = NmapProcess(targets=self.hosts, options=options)
nmap.run()
if nmap.rc == 0:
if self._parse_results(nmap.stdout):
self.last_results.extend(exclude_targets)
else:
self.last_results = []
_LOGGER.error(nmap.stderr)
try:
result = scanner.scan(hosts=self.hosts, arguments=options)
except PortScannerError:
return False
now = dt_util.now()
self.last_results = []
for ipv4, info in result['scan'].items():
if info['status']['state'] != 'up':
continue
name = info['hostnames'][0] if info['hostnames'] else ipv4
# Mac address only returned if nmap ran as root
mac = info['addresses'].get('mac') or _arp(ipv4)
if mac is None:
continue
device = Device(mac.upper(), name, ipv4, now)
self.last_results.append(device)
self.last_results.extend(exclude_targets)
_LOGGER.info("nmap scan successful")
return True

View file

@ -0,0 +1,160 @@
"""
homeassistant.components.device_tracker.thomson
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning a THOMSON router for device
presence.
This device tracker needs telnet to be enabled on the router.
Configuration:
To use the THOMSON tracker you will need to add something like the following
to your configuration.yaml file.
device_tracker:
platform: thomson
host: YOUR_ROUTER_IP
username: YOUR_ADMIN_USERNAME
password: YOUR_ADMIN_PASSWORD
Variables:
host
*Required
The IP address of your router, e.g. 192.168.1.1.
username
*Required
The username of an user with administrative privileges, usually 'admin'.
password
*Required
The password for your given admin account.
"""
import logging
from datetime import timedelta
import re
import threading
import telnetlib
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers import validate_config
from homeassistant.util import Throttle
from homeassistant.components.device_tracker import DOMAIN
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
_LOGGER = logging.getLogger(__name__)
_DEVICES_REGEX = re.compile(
r'(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))\s' +
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s+' +
r'(?P<status>([^\s]+))\s+' +
r'(?P<type>([^\s]+))\s+' +
r'(?P<intf>([^\s]+))\s+' +
r'(?P<hwintf>([^\s]+))\s+' +
r'(?P<host>([^\s]+))')
# pylint: disable=unused-argument
def get_scanner(hass, config):
""" Validates config and returns a THOMSON scanner. """
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
_LOGGER):
return None
scanner = ThomsonDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
class ThomsonDeviceScanner(object):
"""
This class queries a router running THOMSON firmware
for connected devices. Adapted from ASUSWRT scanner.
"""
def __init__(self, config):
self.host = config[CONF_HOST]
self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD]
self.lock = threading.Lock()
self.last_results = {}
# Test the router is accessible
data = self.get_thomson_data()
self.success_init = data is not None
def scan_devices(self):
""" Scans for new devices and return a
list containing found device ids. """
self._update_info()
return [client['mac'] for client in self.last_results]
def get_device_name(self, device):
""" Returns the name of the given device
or None if we don't know. """
if not self.last_results:
return None
for client in self.last_results:
if client['mac'] == device:
return client['host']
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""
Ensures the information from the THOMSON router is up to date.
Returns boolean if scanning successful.
"""
if not self.success_init:
return False
with self.lock:
_LOGGER.info("Checking ARP")
data = self.get_thomson_data()
if not data:
return False
# flag C stands for CONNECTED
active_clients = [client for client in data.values() if
client['status'].find('C') != -1]
self.last_results = active_clients
return True
def get_thomson_data(self):
""" Retrieve data from THOMSON and return parsed result. """
try:
telnet = telnetlib.Telnet(self.host)
telnet.read_until(b'Username : ')
telnet.write((self.username + '\r\n').encode('ascii'))
telnet.read_until(b'Password : ')
telnet.write((self.password + '\r\n').encode('ascii'))
telnet.read_until(b'=>')
telnet.write(('hostmgr list\r\n').encode('ascii'))
devices_result = telnet.read_until(b'=>').split(b'\r\n')
telnet.write('exit\r\n'.encode('ascii'))
except EOFError:
_LOGGER.exception("Unexpected response from router")
return
except ConnectionRefusedError:
_LOGGER.exception("Connection refused by router," +
" is telnet enabled?")
return
devices = {}
for device in devices_result:
match = _DEVICES_REGEX.search(device.decode('utf-8'))
if match:
devices[match.group('ip')] = {
'ip': match.group('ip'),
'mac': match.group('mac').upper(),
'host': match.group('host'),
'status': match.group('status')
}
return devices

View file

@ -1,4 +1,40 @@
""" Supports scanning a Tomato router. """
"""
homeassistant.components.device_tracker.tomato
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning a Tomato router for device
presence.
Configuration:
To use the Tomato tracker you will need to add something like the following
to your configuration.yaml file.
device_tracker:
platform: tomato
host: YOUR_ROUTER_IP
username: YOUR_ADMIN_USERNAME
password: YOUR_ADMIN_PASSWORD
http_id: ABCDEFG
Variables:
host
*Required
The IP address of your router, e.g. 192.168.1.1.
username
*Required
The username of an user with administrative privileges, usually 'admin'.
password
*Required
The password for your given admin account.
http_id
*Required
The value can be obtained by logging in to the Tomato admin interface and
search for http_id in the page source code.
"""
import logging
import json
from datetime import timedelta

View file

@ -0,0 +1,189 @@
"""
homeassistant.components.device_tracker.tplink
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning a TP-Link router for device
presence.
Configuration:
To use the TP-Link tracker you will need to add something like the following
to your configuration.yaml file.
device_tracker:
platform: tplink
host: YOUR_ROUTER_IP
username: YOUR_ADMIN_USERNAME
password: YOUR_ADMIN_PASSWORD
Variables:
host
*Required
The IP address of your router, e.g. 192.168.1.1.
username
*Required
The username of an user with administrative privileges, usually 'admin'.
password
*Required
The password for your given admin account.
"""
import base64
import logging
from datetime import timedelta
import re
import threading
import requests
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers import validate_config
from homeassistant.util import Throttle
from homeassistant.components.device_tracker import DOMAIN
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
def get_scanner(hass, config):
""" Validates config and returns a TP-Link scanner. """
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
_LOGGER):
return None
scanner = Tplink2DeviceScanner(config[DOMAIN])
if not scanner.success_init:
scanner = TplinkDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
class TplinkDeviceScanner(object):
"""
This class queries a wireless router running TP-Link firmware
for connected devices.
"""
def __init__(self, config):
host = config[CONF_HOST]
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
self.parse_macs = re.compile('[0-9A-F]{2}-[0-9A-F]{2}-[0-9A-F]{2}-' +
'[0-9A-F]{2}-[0-9A-F]{2}-[0-9A-F]{2}')
self.host = host
self.username = username
self.password = password
self.last_results = {}
self.lock = threading.Lock()
self.success_init = self._update_info()
def scan_devices(self):
"""
Scans for new devices and return a list containing found device ids.
"""
self._update_info()
return self.last_results
# pylint: disable=no-self-use
def get_device_name(self, device):
"""
The TP-Link firmware doesn't save the name of the wireless device.
"""
return None
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""
Ensures the information from the TP-Link router is up to date.
Returns boolean if scanning successful.
"""
with self.lock:
_LOGGER.info("Loading wireless clients...")
url = 'http://{}/userRpm/WlanStationRpm.htm'.format(self.host)
referer = 'http://{}'.format(self.host)
page = requests.get(url, auth=(self.username, self.password),
headers={'referer': referer})
result = self.parse_macs.findall(page.text)
if result:
self.last_results = [mac.replace("-", ":") for mac in result]
return True
return False
class Tplink2DeviceScanner(TplinkDeviceScanner):
"""
This class queries a wireless router running newer version of TP-Link
firmware for connected devices.
"""
def scan_devices(self):
"""
Scans for new devices and return a list containing found device ids.
"""
self._update_info()
return self.last_results.keys()
# pylint: disable=no-self-use
def get_device_name(self, device):
"""
The TP-Link firmware doesn't save the name of the wireless device.
"""
return self.last_results.get(device)
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
"""
Ensures the information from the TP-Link router is up to date.
Returns boolean if scanning successful.
"""
with self.lock:
_LOGGER.info("Loading wireless clients...")
url = 'http://{}/data/map_access_wireless_client_grid.json'\
.format(self.host)
referer = 'http://{}'.format(self.host)
# Router uses Authorization cookie instead of header
# Let's create the cookie
username_password = '{}:{}'.format(self.username, self.password)
b64_encoded_username_password = base64.b64encode(
username_password.encode('ascii')
).decode('ascii')
cookie = 'Authorization=Basic {}'\
.format(b64_encoded_username_password)
response = requests.post(url, headers={'referer': referer,
'cookie': cookie})
try:
result = response.json().get('data')
except ValueError:
_LOGGER.error("Router didn't respond with JSON. "
"Check if credentials are correct.")
return False
if result:
self.last_results = {
device['mac_addr'].replace('-', ':'): device['name']
for device in result
}
return True
return False

View file

@ -1,18 +1,17 @@
"""
homeassistant.components.discovery
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Starts a service to scan in intervals for new devices.
Will emit EVENT_PLATFORM_DISCOVERED whenever a new service has been discovered.
Knows which components handle certain types, will make sure they are
loaded before the EVENT_PLATFORM_DISCOVERED is fired.
"""
import logging
import threading
# pylint: disable=no-name-in-module, import-error
import homeassistant.external.netdisco.netdisco.const as services
from homeassistant import bootstrap
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_PLATFORM_DISCOVERED,
@ -20,13 +19,22 @@ from homeassistant.const import (
DOMAIN = "discovery"
DEPENDENCIES = []
REQUIREMENTS = ['netdisco==0.3']
SCAN_INTERVAL = 300 # seconds
# Next 3 lines for now a mirror from netdisco.const
# Should setup a mapping netdisco.const -> own constants
SERVICE_WEMO = 'belkin_wemo'
SERVICE_HUE = 'philips_hue'
SERVICE_CAST = 'google_cast'
SERVICE_NETGEAR = 'netgear_router'
SERVICE_HANDLERS = {
services.BELKIN_WEMO: "switch",
services.GOOGLE_CAST: "media_player",
services.PHILIPS_HUE: "light",
SERVICE_WEMO: "switch",
SERVICE_CAST: "media_player",
SERVICE_HUE: "light",
SERVICE_NETGEAR: 'device_tracker',
}
@ -53,14 +61,7 @@ def setup(hass, config):
""" Starts a discovery service. """
logger = logging.getLogger(__name__)
try:
from homeassistant.external.netdisco.netdisco.service import \
DiscoveryService
except ImportError:
logger.exception(
"Unable to import netdisco. "
"Did you install all the zeroconf dependency?")
return False
from netdisco.service import DiscoveryService
# Disable zeroconf logging, it spams
logging.getLogger('zeroconf').setLevel(logging.CRITICAL)
@ -78,6 +79,13 @@ def setup(hass, config):
if not component:
return
# Hack - fix when device_tracker supports discovery
if service == SERVICE_NETGEAR:
bootstrap.setup_component(hass, component, {
'device_tracker': {'platform': 'netgear'}
})
return
# This component cannot be setup.
if not bootstrap.setup_component(hass, component, config):
return

View file

@ -20,13 +20,21 @@ INDEX_PATH = os.path.join(os.path.dirname(__file__), 'index.html.template')
_LOGGER = logging.getLogger(__name__)
FRONTEND_URLS = [
URL_ROOT, '/logbook', '/history', '/devService', '/devState', '/devEvent']
STATES_URL = re.compile(r'/states(/([a-zA-Z\._\-0-9/]+)|)')
def setup(hass, config):
""" Setup serving the frontend. """
if 'http' not in hass.config.components:
_LOGGER.error('Dependency http is not loaded')
return False
hass.http.register_path('GET', URL_ROOT, _handle_get_root, False)
for url in FRONTEND_URLS:
hass.http.register_path('GET', url, _handle_get_root, False)
hass.http.register_path('GET', STATES_URL, _handle_get_root, False)
# Static files
hass.http.register_path(
@ -47,7 +55,7 @@ def _handle_get_root(handler, path_match, data):
handler.end_headers()
if handler.server.development:
app_url = "polymer/home-assistant.html"
app_url = "home-assistant-polymer/src/home-assistant.html"
else:
app_url = "frontend-{}.html".format(version.VERSION)

View file

@ -5,24 +5,47 @@
<title>Home Assistant</title>
<link rel='manifest' href='/static/manifest.json' />
<meta name='apple-mobile-web-app-capable' content='yes'>
<meta name='mobile-web-app-capable' content='yes'>
<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='180x180'
href='/apple-icon-180x180.png'>
href='/static/favicon-apple-180x180.png'>
<meta name='apple-mobile-web-app-capable' content='yes'>
<meta name='mobile-web-app-capable' content='yes'>
<meta name='viewport' content='width=device-width,
user-scalable=no' />
<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>
<link rel='import' href='/static/{{ app_url }}' />
<home-assistant auth='{{ auth }}'></home-assistant>
<style>
#init {
display: -webkit-flex;
display: flex;
-webkit-flex-direction: column;
-webkit-justify-content: center;
-webkit-align-items: center;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
font-family: 'Roboto', 'Noto', sans-serif;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
#init div {
line-height: 34px;
margin-bottom: 89px;
}
</style>
</head>
<body fullbleed>
<div id='init'>
<img src='/static/splash.png' height='230' />
<div>Initializing</div>
</div>
<script src='/static/webcomponents-lite.min.js'></script>
<link rel='import' href='/static/{{ app_url }}' />
<home-assistant auth='{{ auth }}'></home-assistant>
</body>
</html>

View file

@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """
VERSION = "a063d1482fd49e9297d64e1329324f1c"
VERSION = "35ecb5457a9ff0f4142c2605b53eb843"

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

@ -0,0 +1 @@
Subproject commit b0b12e20e0f61df849c414c2dfbcf9923f784631

View file

@ -1,44 +0,0 @@
{
"name": "Home Assistant",
"version": "0.1.0",
"authors": [
"Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>"
],
"main": "splash-login.html",
"license": "MIT",
"private": true,
"ignore": [
"bower_components"
],
"dependencies": {
"webcomponentsjs": "Polymer/webcomponentsjs#~0.5.5",
"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.2",
"core-drawer-panel": "polymer/core-drawer-panel#~0.5.5",
"core-scroll-header-panel": "polymer/core-scroll-header-panel#~0.5.5",
"moment": "~2.9.0"
}
}

View file

@ -1,15 +0,0 @@
<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">
<polymer-element name="state-card-configurator" attributes="stateObj" noscript>
<template>
<state-card-display stateObj="{{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>
</template>
</polymer-element>

View file

@ -1,48 +0,0 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="state-card-display.html">
<link rel="import" href="state-card-toggle.html">
<link rel="import" href="state-card-thermostat.html">
<link rel="import" href="state-card-configurator.html">
<link rel="import" href="state-card-scene.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;
if (!newVal) {
if (cardContainer.lastChild) {
cardContainer.removeChild(cardContainer.lastChild);
}
return;
}
if (!oldVal || oldVal.cardType != newVal.cardType) {
if (cardContainer.lastChild) {
cardContainer.removeChild(cardContainer.lastChild);
}
var stateCard = document.createElement("state-card-" + newVal.cardType);
stateCard.stateObj = newVal;
cardContainer.appendChild(stateCard);
} else {
cardContainer.lastChild.stateObj = newVal;
}
},
});
</script>
</polymer-element>

View file

@ -1,22 +0,0 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<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>
<div horizontal justified layout>
<state-info stateObj="{{stateObj}}"></state-info>
<div class='state'>{{stateObj.stateDisplay}}</div>
</div>
</template>
</polymer-element>

View file

@ -1,27 +0,0 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="./state-card-display.html">
<link rel="import" href="./state-card-toggle.html">
<polymer-element name="state-card-scene" attributes="stateObj">
<template>
<template if={{allowToggle}}>
<state-card-toggle stateObj="{{stateObj}}"></state-card-toggle>
</template>
<template if={{!allowToggle}}>
<state-card-display stateObj="{{stateObj}}"></state-card-display>
</template>
</template>
<script>
(function() {
Polymer({
allowToggle: false,
stateObjChanged: function(oldVal, newVal) {
this.allowToggle = newVal.state === 'off' ||
newVal.attributes.active_requested;
},
});
})();
</script>
</polymer-element>

View file

@ -1,41 +0,0 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../components/state-info.html">
<polymer-element name="state-card-thermostat" attributes="stateObj api">
<template>
<style>
.state {
margin-left: 16px;
text-align: right;
}
.target {
text-transform: capitalize;
font-weight: 300;
font-size: 1.3rem;
}
.current {
color: darkgrey;
margin-top: -2px;
}
</style>
<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>
</div>
</div>
</template>
<script>
Polymer({});
</script>
</polymer-element>

View file

@ -1,81 +0,0 @@
<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">
<template>
<core-style ref='ha-paper-toggle'></core-style>
<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>
</div>
</template>
<script>
var serviceActions = window.hass.serviceActions;
Polymer({
toggleChecked: false,
observe: {
'stateObj.state': 'stateChanged'
},
ready: function() {
this.forceStateChange = this.forceStateChange.bind(this);
},
toggleClicked: function(ev) {
ev.stopPropagation();
},
toggleChanged: function(ev) {
var newVal = ev.target.checked;
if(newVal && this.stateObj.state === "off") {
this.turn_on();
} else if(!newVal && this.stateObj.state === "on") {
this.turn_off();
}
},
stateObjChanged: function(oldVal, newVal) {
if (newVal) {
this.stateChanged(null, newVal.state);
}
},
stateChanged: function(oldVal, newVal) {
this.toggleChecked = newVal === "on";
},
forceStateChange: function() {
this.stateChanged(null, this.stateObj.state);
},
turn_on: function() {
// We call stateChanged after a successful call to re-sync the toggle
// with the state. It will be out of sync if our service call did not
// result in the entity to be turned on. Since the state is not changing,
// the resync is not called automatic.
serviceActions.callTurnOn(this.stateObj.entityId).then(this.forceStateChange);
},
turn_off: function() {
// We call stateChanged after a successful call to re-sync the toggle
// with the state. It will be out of sync if our service call did not
// result in the entity to be turned on. Since the state is not changing,
// the resync is not called automatic.
serviceActions.callTurnOff(this.stateObj.entityId).then(this.forceStateChange);
},
});
</script>
</polymer-element>

View file

@ -1,33 +0,0 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="state-card-content.html">
<polymer-element name="state-card" attributes="stateObj" on-click="cardClicked">
<template>
<style>
:host {
border-radius: 2px;
box-shadow: rgba(0, 0, 0, 0.098) 0px 2px 4px, rgba(0, 0, 0, 0.098) 0px 0px 3px;
transition: all 0.30s ease-out;
position: relative;
background-color: white;
padding: 16px;
width: 100%;
cursor: pointer;
}
</style>
<state-card-content stateObj={{stateObj}}></state-card-content>
</template>
<script>
var uiActions = window.hass.uiActions;
Polymer({
cardClicked: function() {
uiActions.showMoreInfoDialog(this.stateObj.entityId);
},
});
</script>
</polymer-element>

View file

@ -1,24 +0,0 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../resources/home-assistant-icons.html">
<polymer-element name="domain-icon"
attributes="domain state" constructor="DomainIcon">
<template>
<core-icon icon="{{icon}}"></core-icon>
</template>
<script>
Polymer({
icon: '',
observe: {
'domain': 'updateIcon',
'state' : 'updateIcon',
},
updateIcon: function() {
this.icon = window.hass.uiUtil.domainIcon(this.domain, this.state);
},
});
</script>
</polymer-element>

View file

@ -1,60 +0,0 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<polymer-element name="entity-list" attributes="cbEntityClicked">
<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>
</template>
</div>
</template>
<script>
var storeListenerMixIn = window.hass.storeListenerMixIn;
Polymer(Polymer.mixin({
cbEventClicked: null,
entityIDs: [],
attached: function() {
this.listenToStores(true);
},
detached: function() {
this.stopListeningToStores();
},
stateStoreChanged: function(stateStore) {
this.entityIDs = stateStore.entityIDs.toArray();
},
handleClick: function(ev) {
if(this.cbEntityClicked) {
this.cbEntityClicked(ev.path[0].innerHTML);
}
},
}, storeListenerMixIn));
</script>
</polymer-element>

View file

@ -1,62 +0,0 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<polymer-element name="events-list" attributes="cbEventClicked">
<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>
</template>
</div>
</template>
<script>
var storeListenerMixIn = window.hass.storeListenerMixIn;
Polymer(Polymer.mixin({
cbEventClicked: null,
events: [],
attached: function() {
this.listenToStores(true);
},
detached: function() {
this.stopListeningToStores();
},
eventStoreChanged: function(eventStore) {
this.events = eventStore.all.toArray();
},
handleClick: function(ev) {
if(this.cbEventClicked) {
this.cbEventClicked(ev.path[0].innerHTML);
}
},
}, storeListenerMixIn));
</script>
</polymer-element>

View file

@ -1,25 +0,0 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../dialogs/more-info-dialog.html">
<polymer-element name="ha-modals">
<template>
<more-info-dialog id="moreInfoDialog"></more-info-dialog>
</template>
<script>
var uiActions = window.hass.uiActions,
dispatcher = window.hass.dispatcher;
Polymer({
ready: function() {
dispatcher.register(function(payload) {
switch (payload.actionType) {
case uiActions.ACTION_SHOW_DIALOG_MORE_INFO:
this.$.moreInfoDialog.show(payload.entityId);
break;
}
}.bind(this));
},
});
</script>
</polymer-element>

View file

@ -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>

View file

@ -1,25 +0,0 @@
<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">
<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>
</template>
<script>
Polymer({
text: "Loading"
});
</script>
</polymer-element>

View file

@ -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>

View file

@ -1,43 +0,0 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../resources/moment-js.html">
<polymer-element name="relative-ha-datetime" attributes="datetime">
<template>
{{ relativeTime }}
</template>
<script>
(function() {
var UPDATE_INTERVAL = 60000; // 60 seconds
var parseDateTime = window.hass.util.parseDateTime;
Polymer({
relativeTime: "",
parsedDateTime: null,
created: function() {
this.updateRelative = this.updateRelative.bind(this);
},
attached: function() {
this._interval = setInterval(this.updateRelative, UPDATE_INTERVAL);
},
detached: function() {
clearInterval(this._interval);
},
datetimeChanged: function(oldVal, newVal) {
this.parsedDateTime = newVal ? parseDateTime(newVal) : null;
this.updateRelative();
},
updateRelative: function() {
this.relativeTime = this.parsedDateTime ? moment(this.parsedDateTime).fromNow() : "";
},
});
})();
</script>
</polymer-element>

View file

@ -1,90 +0,0 @@
<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="domain-icon.html">
<polymer-element name="services-list" attributes="cbServiceClicked">
<template>
<style>
:host {
display: block;
}
core-menu {
margin-top: 0;
font-size: 1rem;
}
a {
display: block;
}
</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>
<script>
var storeListenerMixIn = window.hass.storeListenerMixIn;
Polymer(Polymer.mixin({
domains: [],
services: null,
cbServiceClicked: null,
attached: function() {
this.listenToStores(true);
},
detached: function() {
this.stopListeningToStores();
},
getIcon: function(domain) {
return hass.uiUtil.domainIcon(domain);
},
getServices: function(domain) {
return this.services.get(domain).toArray();
},
serviceStoreChanged: function(serviceStore) {
this.services = serviceStore.all;
this.domains = this.services.keySeq().sort().toArray();
},
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>

View file

@ -1,105 +0,0 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-image/core-image.html">
<link rel="import" href="domain-icon.html">
<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}}">
</domain-icon>
<template if="{{stateObj.attributes.entity_picture}}">
<core-image
sizing="cover" fit
src="{{stateObj.attributes.entity_picture}}"></core-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'
},
/**
* Called when an attribute changes that influences the color of the icon.
*/
updateIconColor: function(oldVal, newVal) {
var state = this.stateObj;
// 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]
}
});
</script>
</polymer-element>

View file

@ -1,56 +0,0 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../cards/state-card.html">
<polymer-element name="state-cards" attributes="states" noscript>
<template>
<style>
:host {
display: block;
width: 100%;
}
@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>
<template if="{{states.length == 0}}">
<div class='no-states-content'>
<content></content>
</div>
</template>
</div>
</template>
</polymer-element>

View file

@ -1,48 +0,0 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-tooltip/core-tooltip.html">
<link rel="import" href="../bower_components/core-style/core-style.html">
<link rel="import" href="state-badge.html">
<link rel="import" href="relative-ha-datetime.html">
<polymer-element name="state-info" attributes="stateObj" noscript>
<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>
<div class='info'>
<div class='name'>
{{stateObj.entityDisplay}}
</div>
<div class="time-ago">
<core-tooltip label="{{stateObj.lastChanged}}" position="bottom">
<relative-ha-datetime datetime="{{stateObj.lastChanged}}"></relative-ha-datetime>
</core-tooltip>
</div>
</div>
</div>
</template>
</polymer-element>

View file

@ -1,110 +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">
<template>
<style>
:host {
display: block;
}
</style>
<google-jsapi on-api-load="{{googleApiLoaded}}"></google-jsapi>
<div id="timeline" style='width: 100%; height: auto;'></div>
</template>
<script>
Polymer({
apiLoaded: false,
stateHistory: null,
googleApiLoaded: function() {
google.load("visualization", "1", {
packages: ["timeline"],
callback: function() {
this.apiLoaded = true;
this.drawChart();
}.bind(this)
});
},
stateHistoryChanged: function() {
this.drawChart();
},
drawChart: function() {
if (!this.apiLoaded || !this.stateHistory) {
return;
}
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) {
dataTable.addRow([entityDisplay, stateStr, start, end]);
};
if (this.stateHistory.length === 0) {
return;
}
// 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];
}
// 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());
}.bind(this));
chart.draw(dataTable, {
height: 55 + stateHistory.length * 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>
</polymer-element>

View file

@ -1,58 +0,0 @@
<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">
<polymer-element name="stream-status">
<template>
<style>
:host {
display: inline-block;
height: 24px;
}
paper-toggle-button {
vertical-align: middle;
}
</style>
<core-style ref='ha-paper-toggle'></core-style>
<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,
icon: "swap-vert-circle",
color: 'red',
attached: function() {
this.listenToStores(true);
},
detached: function() {
this.stopListeningToStores();
},
streamStoreChanged: function(streamStore) {
this.hasError = streamStore.hasError;
this.$.toggle.checked = this.isStreaming = streamStore.isStreaming;
},
toggleChanged: function(ev) {
if (this.isStreaming) {
streamActions.stop();
} else {
streamActions.start(authStore.authToken);
}
},
}, storeListenerMixIn));
</script>
</polymer-element>

View file

@ -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>

View file

@ -1,116 +0,0 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="ha-dialog.html">
<link rel="import" href="../cards/state-card-content.html">
<link rel="import" href="../components/state-timeline.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>
<state-timeline stateHistory="{{stateHistory}}"></state-timeline>
<more-info-content
stateObj="{{stateObj}}"
dialogOpen="{{dialogOpen}}"></more-info-content>
</div>
</ha-dialog>
</template>
<script>
var storeListenerMixIn = window.hass.storeListenerMixIn;
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,
observe: {
'stateObj.attributes': 'reposition'
},
created: function() {
this.dialogOpenChanged = this.dialogOpenChanged.bind(this);
},
attached: function() {
this.listenToStores(true);
},
detached: function() {
this.stopListeningToStores();
},
componentStoreChanged: function(componentStore) {
this.hasHistoryComponent = componentStore.isLoaded('history');
},
stateStoreChanged: function() {
var newState = this.entityId ? stateStore.get(this.entityId) : null;
if (newState !== this.stateObj) {
this.stateObj = newState;
}
},
stateHistoryStoreChanged: function() {
var newHistory;
if (this.hasHistoryComponent && this.entityId) {
newHistory = stateHistoryStore.get(this.entityId);
} else {
newHistory = null;
}
if (newHistory !== this.stateHistory) {
this.stateHistory = newHistory;
}
},
dialogOpenChanged: function(ev) {
// we get CustomEvent, undefined and true/false from polymer…
if (typeof ev === 'object') {
this.dialogOpen = ev.detail;
}
},
changeEntityId: function(entityId) {
this.entityId = entityId;
this.stateStoreChanged();
this.stateHistoryStoreChanged();
if (this.hasHistoryComponent && stateHistoryStore.isStale(entityId)) {
stateHistoryActions.fetch(entityId);
}
},
/**
* 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);
}
},
show: function(entityId) {
this.changeEntityId(entityId);
this.job('showDialogAfterRender', function() {
this.$.dialog.toggle();
}.bind(this));
},
}, storeListenerMixIn));
</script>
</polymer-element>

@ -1 +0,0 @@
Subproject commit e048bf6ece91983b9f03aafeb414ae5c535288a2

View file

@ -1,65 +0,0 @@
<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/font-roboto/roboto.html">
<link rel="import" href="resources/home-assistant-style.html">
<link rel="import" href="resources/home-assistant-js.html">
<link rel="import" href="layouts/login-form.html">
<link rel="import" href="layouts/home-assistant-main.html">
<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}}">
<login-form></login-form>
</template>
<template if="{{loaded}}">
<home-assistant-main></home-assistant-main>
</template>
</template>
<script>
var storeListenerMixIn = window.hass.storeListenerMixIn,
uiActions = window.hass.uiActions,
preferenceStore = window.hass.preferenceStore;
Polymer(Polymer.mixin({
loaded: false,
ready: function() {
// remove the HTML init message
document.getElementById('init').remove();
// if auth was given, tell the backend
if(this.auth) {
uiActions.validateAuth(this.auth, false);
} else if (preferenceStore.hasAuthToken) {
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>

View file

@ -1,246 +0,0 @@
<link rel="import" href="../bower_components/polymer/polymer.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="../layouts/partial-states.html">
<link rel="import" href="../layouts/partial-history.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="../components/ha-notifications.html">
<link rel="import" href="../components/ha-modals.html">
<link rel="import" href="../components/stream-status.html">
<polymer-element name="home-assistant-main">
<template>
<core-style ref="ha-headers"></core-style>
<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-menu {
overflow: auto;
position: absolute;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
}
.sidenav-menu core-icon {
margin-right: 24px;
}
.sidenav-menu > paper-item {
min-height: 53px;
}
.text {
padding: 16px;
border-top: 1px solid #e0e0e0;
}
.label {
font-size: 14px;
}
.dev-tools {
padding: 0 8px;
}
</style>
<ha-notifications></ha-notifications>
<ha-modals></ha-modals>
<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>
<template repeat="{{activeFilters as filter}}">
<paper-item data-panel="states_{{filter}}">
<core-icon icon="{{filter | filterIcon}}"></core-icon>
{{filter | filterName}}
</paper-item>
</template>
<template if="{{hasHistoryComponent}}">
<paper-item data-panel="history">
<core-icon icon="assessment"></core-icon>
History
</paper-item>
</template>
<div flex></div>
<paper-item on-click="{{handleLogOutClick}}">
<core-icon icon="exit-to-app"></core-icon>
Log Out
</paper-item>
<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 == '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;
Polymer(Polymer.mixin({
selected: "states",
stateFilter: null,
narrow: false,
activeFilters: [],
hasHistoryComponent: false,
isStreaming: false,
hasStreamError: false,
hideStates: false,
attached: function() {
this.togglePanel = this.togglePanel.bind(this);
this.listenToStores(true);
},
detached: function() {
this.stopListeningToStores();
},
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.hasScriptComponent = componentStore.isLoaded('script');
},
streamStoreChanged: function(streamStore) {
this.isStreaming = streamStore.isStreaming;
this.hasStreamError = streamStore.hasError;
},
menuSelect: function(ev, detail, sender) {
if (detail.isSelected) {
this.selectPanel(detail.item);
}
},
handleDevClick: function(ev, detail, sender) {
this.$.menu.selected = -1;
this.selectPanel(ev.target);
},
selectPanel: function(element) {
var newChoice = element.dataset.panel;
if(newChoice !== this.selected) {
this.togglePanel();
this.selected = newChoice;
}
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>

View file

@ -1,147 +0,0 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<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-spinner/paper-spinner.html">
<polymer-element name="login-form">
<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>
<img src="/static/favicon-192x192.png" />
<h1>Home Assistant</h1>
<a href="#" id="hideKeyboardOnFocus"></a>
<div class='interact' layout vertical>
<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>
</div>
<div id="validatebox" hidden?="{{!(isValidating || isLoggedIn)}}">
<paper-spinner active="true"></paper-spinner><br />
<div class="validatemessage">{{spinnerMessage}}</div>
</div>
</div>
</div>
</div>
</template>
<script>
var storeListenerMixIn = window.hass.storeListenerMixIn;
var uiActions = window.hass.uiActions;
Polymer(Polymer.mixin({
MSG_VALIDATING: "Validating password…",
MSG_LOADING_DATA: "Loading data…",
authToken: "",
rememberLogin: false,
isValidating: false,
isLoggedIn: false,
spinnerMessage: "",
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;
}
if (!(this.isValidating && this.isLoggedIn)) {
this.job('focusPasswordBox', this.focusPassword.bind(this));
}
},
focusPassword: function() {
this.$.passwordInput.focus();
},
passwordKeyup: function(ev) {
// validate on enter
if(ev.keyCode === 13) {
this.validatePassword();
// clear error after we start typing again
} else if(this.$.passwordDecorator.isInvalid) {
this.$.passwordDecorator.isInvalid = false;
}
},
validatePassword: function() {
this.$.hideKeyboardOnFocus.focus();
uiActions.validateAuth(this.authToken, this.rememberLogin);
},
}, storeListenerMixIn));
</script>
</polymer-element>

View file

@ -1,28 +0,0 @@
<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/core-style/core-style.html">
<polymer-element name="partial-base" attributes="narrow togglePanel" noscript>
<template>
<core-style ref="ha-headers"></core-style>
<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>
</core-scroll-header-panel>
</template>
</polymer>

View file

@ -1,88 +0,0 @@
<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-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="./partial-base.html">
<link rel="import" href="../components/services-list.html">
<polymer-element name="partial-dev-call-service" attributes="narrow togglePanel">
<template>
<style>
.form {
padding: 24px;
background-color: white;
}
</style>
<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 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>
</div>
</div>
</partial-base>
</template>
<script>
var serviceActions = window.hass.serviceActions;
Polymer({
ready: function() {
// to ensure callback methods work..
this.serviceSelected = this.serviceSelected.bind(this);
},
setService: function(domain, service) {
this.$.inputDomain.value = domain;
this.$.inputService.value = service;
},
serviceSelected: function(domain, service) {
this.setService(domain, service);
},
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);
}
}
});
</script>
</polymer-element>

View file

@ -1,80 +0,0 @@
<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-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="./partial-base.html">
<link rel="import" href="../components/events-list.html">
<polymer-element name="partial-dev-fire-event" attributes="narrow togglePanel">
<template>
<style>
.form {
padding: 24px;
background-color: white;
}
</style>
<partial-base narrow="{{narrow}}" togglePanel="{{togglePanel}}">
<span header-title>Fire Event</span>
<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">
<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>
</div>
</div>
</partial-base>
</template>
<script>
var eventActions = window.hass.eventActions;
Polymer({
ready: function() {
// to ensure callback methods work..
this.eventSelected = this.eventSelected.bind(this);
},
eventSelected: function(eventType) {
this.$.inputType.value = eventType;
},
clickFireEvent: function() {
try {
eventActions.fire(
this.$.inputType.value,
this.$.inputData.value ? JSON.parse(this.$.inputData.value) : {});
} catch (err) {
alert("Error parsing JSON: " + err);
}
}
});
</script>
</polymer-element>

View file

@ -1,106 +0,0 @@
<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-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="./partial-base.html">
<link rel="import" href="../components/entity-list.html">
<polymer-element name="partial-dev-set-state" attributes="narrow togglePanel">
<template>
<style>
.form {
padding: 24px;
background-color: white;
}
</style>
<partial-base narrow="{{narrow}}" togglePanel="{{togglePanel}}">
<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>
<div class='sidebar'>
<b>Current entities:</b>
<entity-list cbEntityClicked={{entitySelected}}></entity-list>
</div>
</div>
</div>
</partial-base>
</template>
<script>
var stateStore = window.hass.stateStore;
var stateActions = window.hass.stateActions;
Polymer({
ready: function() {
// to ensure callback methods work..
this.entitySelected = this.entitySelected.bind(this);
},
setEntityId: function(entityId) {
this.$.inputEntityID.value = entityId;
},
setState: function(state) {
this.$.inputState.value = state;
},
setStateData: function(stateData) {
var value = stateData ? JSON.stringify(stateData, null, ' ') : "";
this.$.inputData.value = value;
// not according to the spec but it works...
this.$.inputDataWrapper.update(this.$.inputData);
},
entitySelected: function(entityId) {
this.setEntityId(entityId);
var state = stateStore.get(entityId);
this.setState(state.state);
this.setStateData(state.attributes);
},
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);
}
}
});
</script>
</polymer-element>

View file

@ -1,61 +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-icon-button/paper-icon-button.html">
<link rel="import" href="./partial-base.html">
<link rel="import" href="../components/state-timeline.html">
<polymer-element name="partial-history" attributes="narrow togglePanel">
<template>
<style>
.content {
background-color: white;
}
.content.wide {
padding: 8px;
}
</style>
<partial-base narrow="{{narrow}}" togglePanel="{{togglePanel}}">
<span header-title>History</span>
<span header-buttons>
<paper-icon-button icon="refresh"
on-click="{{handleRefreshClick}}"></paper-icon-button>
</span>
<div flex class="{{ {content: true, narrow: narrow, wide: !narrow} | tokenList }}">
<state-timeline stateHistory="{{stateHistory}}"></state-timeline>
</div>
</partial-base>
</template>
<script>
var storeListenerMixIn = window.hass.storeListenerMixIn;
var stateHistoryActions = window.hass.stateHistoryActions;
Polymer(Polymer.mixin({
stateHistory: null,
attached: function() {
this.listenToStores(true);
},
detached: function() {
this.stopListeningToStores();
},
stateHistoryStoreChanged: function(stateHistoryStore) {
if (stateHistoryStore.isStale()) {
stateHistoryActions.fetchAll();
}
this.stateHistory = stateHistoryStore.all;
},
handleRefreshClick: function() {
stateHistoryActions.fetchAll();
},
}, storeListenerMixIn));
</script>
</polymer>

View file

@ -1,164 +0,0 @@
<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/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>
<style>
.listening {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 1;
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;
background-color: rgba(255, 255, 255, 0.95);
line-height: 2em;
cursor: pointer;
}
.interimTranscript {
color: darkgrey;
}
.listening paper-spinner {
float: right;
}
</style>
<partial-base narrow="{{narrow}}" togglePanel="{{togglePanel}}">
<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>
<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>
<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,
canListen: false,
voiceSupported: false,
hasConversationComponent: false,
isListening: false,
isTransmittingVoice: false,
interimTranscript: '',
finalTranscript: '',
ready: function() {
this.voiceSupported = voiceActions.isSupported();
},
attached: function() {
this.listenToStores(true);
},
detached: function() {
this.stopListeningToStores();
},
componentStoreChanged: function(componentStore) {
this.canListen = this.voiceSupported &&
componentStore.isLoaded('conversation');
},
stateStoreChanged: function() {
this.refreshStates();
},
syncStoreChanged: function(syncStore) {
this.isFetching = syncStore.isFetching;
},
streamStoreChanged: function(streamStore) {
this.isStreaming = streamStore.isStreaming;
},
voiceStoreChanged: function(voiceStore) {
this.isListening = voiceStore.isListening;
this.isTransmitting = voiceStore.isTransmitting;
this.finalTranscript = voiceStore.finalTranscript;
this.interimTranscript = voiceStore.interimTranscript.slice(
this.finalTranscript.length);
},
filterChanged: function() {
this.refreshStates();
this.headerTitle = uiConstants.STATE_FILTERS[this.filter] || 'States';
},
refreshStates: function() {
var states;
if (this.filter) {
var filter = this.filter;
states = stateStore.all.filter(function(state) {
return state.domain === filter;
});
} else {
// all but the STATE_FILTER keys
states = stateStore.all.filter(function(state) {
return !(state.domain in uiConstants.STATE_FILTERS);
});
}
this.states = states.toArray();
},
handleRefreshClick: function() {
syncActions.fetchAll();
},
handleListenClick: function() {
if (this.isListening) {
voiceActions.stop();
} else {
voiceActions.listen();
}
},
}, storeListenerMixIn));
})();
</script>
</polymer>

View file

@ -1,112 +0,0 @@
<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">
<polymer-element name="more-info-configurator" attributes="stateObj">
<template>
<style>
p {
margin: 8px 0;
}
p > img {
max-width: 100%;
}
p.center {
text-align: center;
}
p.error {
color: #C62828;
}
p.submit {
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>
<div layout vertical>
<template if="{{stateObj.state == 'configure'}}">
<p hidden?="{{!stateObj.attributes.description}}">
{{stateObj.attributes.description}}
</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='submit'>
<paper-button raised on-click="{{submitClicked}}"
hidden?="{{action !== 'display'}}">
{{stateObj.attributes.submit_caption || "Set configuration"}}
</paper-button>
<span hidden?="{{action !== 'configuring'}}">
<paper-spinner active="true"></paper-spinner><span>Configuring…</span>
</span>
</p>
</template>
</div>
</template>
<script>
var storeListenerMixIn = window.hass.storeListenerMixIn;
var syncActions = window.hass.syncActions;
var serviceActions = window.hass.serviceActions;
Polymer(Polymer.mixin({
action: "display",
isStreaming: false,
attached: function() {
this.listenToStores(true);
},
detached: function() {
this.stopListeningToStores();
},
streamStoreChanged: function(streamStore) {
this.isStreaming = streamStore.isStreaming;
},
submitClicked: function() {
this.action = "configuring";
var data = {
configure_id: this.stateObj.attributes.configure_id
};
serviceActions.callService('configurator', 'configure', data).then(
function() {
this.action = 'display';
if (!this.isStreaming) {
syncActions.fetchAll();
}
}.bind(this),
function() {
this.action = 'display';
}.bind(this));
},
}, storeListenerMixIn));
</script>
</polymer-element>

View file

@ -1,73 +0,0 @@
<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">
<polymer-element name="more-info-content" attributes="stateObj dialogOpen">
<template>
<style>
:host {
display: block;
}
</style>
<div id='moreInfoContainer' class='{{classNames}}'></div>
</template>
<script>
Polymer({
classNames: '',
dialogOpen: false,
observe: {
'stateObj.attributes': 'stateAttributesChanged',
},
dialogOpenChanged: function(oldVal, newVal) {
var moreInfoContainer = this.$.moreInfoContainer;
if (moreInfoContainer.lastChild) {
moreInfoContainer.lastChild.dialogOpen = newVal;
}
},
stateObjChanged: function(oldVal, newVal) {
var moreInfoContainer = this.$.moreInfoContainer;
if (!newVal) {
if (moreInfoContainer.lastChild) {
moreInfoContainer.removeChild(moreInfoContainer.lastChild);
}
return;
}
if (!oldVal || oldVal.moreInfoType != newVal.moreInfoType) {
if (moreInfoContainer.lastChild) {
moreInfoContainer.removeChild(moreInfoContainer.lastChild);
}
var moreInfo = document.createElement("more-info-" + newVal.moreInfoType);
moreInfo.stateObj = newVal;
moreInfo.dialogOpen = this.dialogOpen;
moreInfoContainer.appendChild(moreInfo);
} else {
moreInfoContainer.lastChild.dialogOpen = this.dialogOpen;
moreInfoContainer.lastChild.stateObj = newVal;
}
},
stateAttributesChanged: function(oldVal, newVal) {
if (!newVal) return;
this.classNames = Object.keys(newVal).map(
function(key) { return "has-" + key; }).join(' ');
},
});
</script>
</polymer-element>

View file

@ -1,34 +0,0 @@
<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>
<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}}
</div>
<div class='value'>
{{stateObj.attributes[key]}}
</div>
</div>
</template>
</div>
</template>
<script>
Polymer({
getKeys: function(obj) {
return Object.keys(obj || {});
}
});
</script>
</polymer-element>

View file

@ -1,49 +0,0 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../cards/state-card-content.html">
<polymer-element name="more-info-group" attributes="stateObj">
<template>
<style>
.child-card {
margin-bottom: 8px;
}
.child-card:last-child {
margin-bottom: 0;
}
</style>
<template repeat="{{states as state}}">
<state-card-content stateObj="{{state}}" class='child-card'>
</state-card-content>
</template>
</template>
<script>
var storeListenerMixIn = window.hass.storeListenerMixIn;
var stateStore = window.hass.stateStore;
Polymer(Polymer.mixin({
attached: function() {
this.listenToStores(true);
},
detached: function() {
this.stopListeningToStores();
},
stateStoreChanged: function() {
this.updateStates();
},
stateObjChanged: function() {
this.updateStates();
},
updateStates: function() {
this.states = this.stateObj && this.stateObj.attributes.entity_id ?
stateStore.gets(this.stateObj.attributes.entity_id).toArray() : [];
},
}, storeListenerMixIn));
</script>
</polymer-element>

View file

@ -1,106 +0,0 @@
<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">
<polymer-element name="more-info-light" attributes="stateObj">
<template>
<style>
.brightness {
margin-bottom: 8px;
max-height: 0px;
overflow: hidden;
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;
margin: 0 auto;
max-height: 0px;
overflow: hidden;
transition: max-height .5s ease-in .3s;
}
:host-context(.has-brightness) .brightness {
max-height: 500px;
}
:host-context(.has-xy_color) color-picker {
max-height: 500px;
}
</style>
<div>
<div class='brightness'>
<div center horizontal layout>
<div>Brightness</div>
<paper-slider
max="255" flex id='brightness' value='{{brightnessSliderValue}}'
on-change="{{brightnessSliderChanged}}">
</paper-slider>
</div>
</div>
<color-picker id="colorpicker" width="350" height="200">
</color-picker>
</div>
</template>
<script>
var serviceActions = window.hass.serviceActions;
Polymer({
brightnessSliderValue: 0,
observe: {
'stateObj.attributes.brightness': 'stateObjBrightnessChanged',
},
stateObjChanged: function(oldVal, newVal) {
if (newVal && newVal.state === 'on') {
this.brightnessSliderValue = newVal.attributes.brightness;
}
},
stateObjBrightnessChanged: function(oldVal, newVal) {
this.brightnessSliderValue = newVal;
},
domReady: function() {
this.$.colorpicker.addEventListener('colorselected', this.colorPicked.bind(this));
},
brightnessSliderChanged: function(ev, details, target) {
var bri = parseInt(target.value);
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,
rgb_color: [color.r, color.g, color.b]
});
}
});
</script>
</polymer-element>

View file

@ -1,22 +0,0 @@
<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-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}}
</div>
</div>
</div>
</template>
</polymer-element>

View file

@ -1,48 +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="../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'>
<div class='key'>
Rising <relative-ha-datetime datetime="{{stateObj.attributes.next_rising}}"></relative-ha-datetime>
</div>
<div class='value'>
{{stateObj.attributes.next_rising | HATimeStripDate}}
</div>
</div>
<div layout justified horizontal class='data-entry' id='setting'>
<div class='key'>
Setting <relative-ha-datetime datetime="{{stateObj.attributes.next_setting}}"></relative-ha-datetime>
</div>
<div class='value'>
{{stateObj.attributes.next_setting | HATimeStripDate}}
</div>
</div>
</div>
</template>
<script>
var parseDateTime = window.hass.util.parseDateTime;
Polymer({
stateObjChanged: function() {
var rising = parseDateTime(this.stateObj.attributes.next_rising);
var setting = parseDateTime(this.stateObj.attributes.next_setting);
if(rising > setting) {
this.$.sunData.appendChild(this.$.rising);
} else {
this.$.sunData.appendChild(this.$.setting);
}
}
});
</script>
</polymer-element>

View file

@ -1,114 +0,0 @@
<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>
<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 {
display: block;
}
</style>
<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>
</div>
</div>
</template>
<script>
var constants = window.hass.constants;
Polymer({
tempMin: 10,
tempMax: 40,
targetTemperatureSliderValue: 0,
awayToggleChecked: false,
observe: {
'stateObj.attributes.away_mode': 'awayChanged'
},
stateObjChanged: function(oldVal, newVal) {
this.targetTemperatureSliderValue = this.stateObj.state;
if (this.stateObj.attributes.unit_of_measurement === constants.UNIT_TEMP_F) {
this.tempMin = 45;
this.tempMax = 95;
} else {
this.tempMin = 7;
this.tempMax = 35;
}
},
targetTemperatureSliderChanged: function(ev, details, target) {
var temp = parseInt(target.value);
if(isNaN(temp)) return;
serviceActions.callService("thermostat", "set_temperature", {
entity_id: this.stateObj.entityId,
temperature: temp
});
},
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);
}
},
awayChanged: function(oldVal, newVal) {
this.awayToggleChecked = newVal == 'on';
},
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.awayChanged(null, this.stateObj.attributes.away_mode);
}.bind(this));
},
});
</script>
</polymer-element>

View file

@ -1,94 +0,0 @@
<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/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">
<core-iconset-svg id="homeassistant-100" iconSize="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>
</defs></svg>
</core-iconset-svg>
<core-iconset-svg id="homeassistant-24" iconSize="24">
<svg><defs>
<!--
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
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>
<script>
window.hass.uiUtil.domainIcon = function(domain, state) {
switch(domain) {
case "homeassistant":
return "home";
case "group":
return "homeassistant-24:group";
case "device_tracker":
return "social:person";
case "switch":
return "image:flash-on";
case "media_player":
var icon = "hardware:cast";
if (state !== "idle") {
icon += "-connected";
}
return icon;
case "sun":
return "image:wb-sunny";
case "light":
return "image:wb-incandescent";
case "simple_alarm":
return "social:notifications";
case "notify":
return "announcement";
case "thermostat":
return "homeassistant-100:thermostat";
case "sensor":
return "visibility";
case "configurator":
return "settings";
case "conversation":
return "av:hearing";
case "script":
return "description";
case 'scene':
return 'social:pages';
default:
return "bookmark-outline";
}
}
</script>

View file

@ -1,83 +0,0 @@
<script src="../home-assistant-js/dist/homeassistant.min.js"></script>
<script>
(function() {
var DOMAINS_WITH_CARD = ['thermostat', 'configurator', 'scene'];
var DOMAINS_WITH_MORE_INFO = [
'light', 'group', 'sun', 'configurator', 'thermostat', 'script'
];
// Register some polymer filters
PolymerExpressions.prototype.HATimeToDate = function(timeString) {
if (!timeString) return;
return window.hass.util.parseDateTime(timeString);
};
PolymerExpressions.prototype.HATimeStripDate = function(timeString) {
return (timeString || "").split(' ')[0];
};
// Add some frontend specific helpers to the models
Object.defineProperties(window.hass.stateModel.prototype, {
// 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";
}
}
},
// 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';
}
}
},
});
var dispatcher = window.hass.dispatcher,
constants = window.hass.constants,
preferenceStore = window.hass.preferenceStore,
authActions = window.hass.authActions;
window.hass.uiConstants = {
ACTION_SHOW_DIALOG_MORE_INFO: 'ACTION_SHOW_DIALOG_MORE_INFO',
STATE_FILTERS: {
'group': 'Groups',
'script': 'Scripts',
'scene': 'Scenes',
},
};
window.hass.uiActions = {
showMoreInfoDialog: function(entityId) {
dispatcher.dispatch({
actionType: this.ACTION_SHOW_DIALOG_MORE_INFO,
entityId: entityId,
});
},
validateAuth: function(authToken, rememberLogin) {
authActions.validate(authToken, {
useStreaming: preferenceStore.useStreaming,
rememberLogin: rememberLogin,
});
},
};
// UI specific util methods
window.hass.uiUtil = {};
})();
</script>

View file

@ -1,127 +0,0 @@
<link rel="import" href="../bower_components/core-style/core-style.html">
<core-style id='ha-animations'>
@-webkit-keyframes ha-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@keyframes ha-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
.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>

View file

@ -1,5 +0,0 @@
<!--
Wrapping JS in an HTML file will prevent it from being loaded twice.
-->
<script src="../bower_components/moment/moment.js"></script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,15 +1,17 @@
"""
homeassistant.components.groups
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
homeassistant.components.group
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to group devices that can be turned on or off.
"""
import homeassistant as ha
import homeassistant.core as ha
from homeassistant.helpers import generate_entity_id
from homeassistant.helpers.event import track_state_change
from homeassistant.helpers.entity import Entity
import homeassistant.util as util
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, STATE_ON, STATE_OFF,
ATTR_ENTITY_ID, STATE_ON, STATE_OFF,
STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN)
DOMAIN = "group"
@ -101,44 +103,50 @@ def get_entity_ids(hass, entity_id, domain_filter=None):
def setup(hass, config):
""" Sets up all groups found definded in the configuration. """
for name, entity_ids in config.get(DOMAIN, {}).items():
# Support old deprecated method - 2/28/2015
if isinstance(entity_ids, str):
entity_ids = entity_ids.split(",")
entity_ids = [ent.strip() for ent in entity_ids.split(",")]
setup_group(hass, name, entity_ids)
return True
class Group(object):
class Group(Entity):
""" Tracks a group of entity ids. """
# pylint: disable=too-many-instance-attributes
def __init__(self, hass, name, entity_ids=None, user_defined=True):
self.hass = hass
self.name = name
self._name = name
self._state = STATE_UNKNOWN
self.user_defined = user_defined
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=hass)
self.tracking = []
self.group_on, self.group_off = None, None
self.group_on = None
self.group_off = None
if entity_ids is not None:
self.update_tracked_entity_ids(entity_ids)
else:
self.force_update()
self.update_ha_state(True)
@property
def should_poll(self):
return False
@property
def name(self):
return self._name
@property
def state(self):
""" Return the current state from the group. """
return self.hass.states.get(self.entity_id)
return self._state
@property
def state_attr(self):
""" State attributes of this group. """
def state_attributes(self):
return {
ATTR_ENTITY_ID: self.tracking,
ATTR_AUTO: not self.user_defined,
ATTR_FRIENDLY_NAME: self.name
}
def update_tracked_entity_ids(self, entity_ids):
@ -147,71 +155,69 @@ class Group(object):
self.tracking = tuple(ent_id.lower() for ent_id in entity_ids)
self.group_on, self.group_off = None, None
self.force_update()
self.update_ha_state(True)
self.start()
def force_update(self):
""" Query all the tracked states and update group state. """
for entity_id in self.tracking:
state = self.hass.states.get(entity_id)
if state is not None:
self._update_group_state(state.entity_id, None, state)
# If parsing the entitys did not result in a state, set UNKNOWN
if self.state is None:
self.hass.states.set(
self.entity_id, STATE_UNKNOWN, self.state_attr)
def start(self):
""" Starts the tracking. """
self.hass.states.track_change(self.tracking, self._update_group_state)
track_state_change(
self.hass, self.tracking, self._state_changed_listener)
def stop(self):
""" Unregisters the group from Home Assistant. """
self.hass.states.remove(self.entity_id)
self.hass.bus.remove_listener(
ha.EVENT_STATE_CHANGED, self._update_group_state)
ha.EVENT_STATE_CHANGED, self._state_changed_listener)
def _update_group_state(self, entity_id, old_state, new_state):
""" Updates the group state based on a state change by
a tracked entity. """
def update(self):
""" Query all the tracked states and determine current group state. """
self._state = STATE_UNKNOWN
for entity_id in self.tracking:
state = self.hass.states.get(entity_id)
if state is not None:
self._process_tracked_state(state)
def _state_changed_listener(self, entity_id, old_state, new_state):
""" Listener to receive state changes of tracked entities. """
self._process_tracked_state(new_state)
self.update_ha_state()
def _process_tracked_state(self, tr_state):
""" Updates group state based on a new state of a tracked entity. """
# We have not determined type of group yet
if self.group_on is None:
self.group_on, self.group_off = _get_group_on_off(new_state.state)
self.group_on, self.group_off = _get_group_on_off(tr_state.state)
if self.group_on is not None:
# New state of the group is going to be based on the first
# state that we can recognize
self.hass.states.set(
self.entity_id, new_state.state, self.state_attr)
self._state = tr_state.state
return
# There is already a group state
cur_gr_state = self.hass.states.get(self.entity_id).state
cur_gr_state = self._state
group_on, group_off = self.group_on, self.group_off
# if cur_gr_state = OFF and new_state = ON: set ON
# if cur_gr_state = ON and new_state = OFF: research
# if cur_gr_state = OFF and tr_state = ON: set ON
# if cur_gr_state = ON and tr_state = OFF: research
# else: ignore
if cur_gr_state == group_off and new_state.state == group_on:
if cur_gr_state == group_off and tr_state.state == group_on:
self._state = group_on
self.hass.states.set(
self.entity_id, group_on, self.state_attr)
elif cur_gr_state == group_on and tr_state.state == group_off:
elif (cur_gr_state == group_on and
new_state.state == group_off):
# Check if any of the other states is still on
# Set to off if no other states are on
if not any(self.hass.states.is_state(ent_id, group_on)
for ent_id in self.tracking if entity_id != ent_id):
self.hass.states.set(
self.entity_id, group_off, self.state_attr)
for ent_id in self.tracking
if tr_state.entity_id != ent_id):
self._state = group_off
def setup_group(hass, name, entity_ids, user_defined=True):

Some files were not shown because too many files have changed in this diff Show more