gitignore ready for emacs
This commit is contained in:
commit
8842e4e94f
275 changed files with 21134 additions and 6724 deletions
82
.coveragerc
82
.coveragerc
|
@ -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
12
.gitignore
vendored
|
@ -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
24
.gitmodules
vendored
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
|
16
Dockerfile
16
Dockerfile
|
@ -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
1
MANIFEST.in
Normal file
|
@ -0,0 +1 @@
|
|||
recursive-exclude tests *
|
66
README.md
66
README.md
|
@ -1,49 +1,37 @@
|
|||
# Home Assistant [](https://travis-ci.org/balloob/home-assistant) [](https://coveralls.io/r/balloob/home-assistant?branch=master)
|
||||
# Home Assistant [](https://travis-ci.org/balloob/home-assistant) [](https://coveralls.io/r/balloob/home-assistant?branch=master) [](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.
|
||||
|
||||
[][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
|
||||
|
||||
[](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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
59
config/custom_components/mqtt_example.py
Normal file
59
config/custom_components/mqtt_example.py
Normal 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 |
|
@ -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
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()]
|
||||
|
|
143
homeassistant/components/arduino.py
Normal file
143
homeassistant/components/arduino.py
Normal 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()
|
|
@ -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)
|
||||
|
||||
|
|
34
homeassistant/components/automation/mqtt.py
Normal file
34
homeassistant/components/automation/mqtt.py
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
229
homeassistant/components/camera/__init__.py
Normal file
229
homeassistant/components/camera/__init__.py
Normal 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
|
91
homeassistant/components/camera/generic.py
Normal file
91
homeassistant/components/camera/generic.py
Normal 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
|
|
@ -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(
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
})
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
191
homeassistant/components/device_tracker/actiontec.py
Normal file
191
homeassistant/components/device_tracker/actiontec.py
Normal 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
|
148
homeassistant/components/device_tracker/aruba.py
Normal file
148
homeassistant/components/device_tracker/aruba.py
Normal 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
|
171
homeassistant/components/device_tracker/asuswrt.py
Normal file
171
homeassistant/components/device_tracker/asuswrt.py
Normal 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
|
|
@ -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)}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
160
homeassistant/components/device_tracker/thomson.py
Normal file
160
homeassistant/components/device_tracker/thomson.py
Normal 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
|
|
@ -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
|
||||
|
|
189
homeassistant/components/device_tracker/tplink.py
Executable file
189
homeassistant/components/device_tracker/tplink.py
Executable 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
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
""" DO NOT MODIFY. Auto-generated by build_frontend script """
|
||||
VERSION = "a063d1482fd49e9297d64e1329324f1c"
|
||||
VERSION = "35ecb5457a9ff0f4142c2605b53eb843"
|
||||
|
|
0
homeassistant/components/frontend/www_static/__init__.py
Normal file
0
homeassistant/components/frontend/www_static/__init__.py
Normal file
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
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,37 +0,0 @@
|
|||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/paper-toast/paper-toast.html">
|
||||
|
||||
<polymer-element name="ha-notifications">
|
||||
<template>
|
||||
<paper-toast id="toast" role="alert" text=""></paper-toast>
|
||||
</template>
|
||||
<script>
|
||||
var storeListenerMixIn = window.hass.storeListenerMixIn;
|
||||
|
||||
Polymer(Polymer.mixin({
|
||||
lastId: null,
|
||||
|
||||
attached: function() {
|
||||
this.listenToStores(true);
|
||||
},
|
||||
|
||||
detached: function() {
|
||||
this.stopListeningToStores();
|
||||
},
|
||||
|
||||
notificationStoreChanged: function(notificationStore) {
|
||||
if (notificationStore.hasNewNotifications(this.lastId)) {
|
||||
var toast = this.$.toast;
|
||||
var notification = notificationStore.lastNotification;
|
||||
|
||||
if (notification) {
|
||||
this.lastId = notification.id;
|
||||
toast.text = notification.message;
|
||||
toast.show();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
}, storeListenerMixIn));
|
||||
</script>
|
||||
</polymer-element>
|
|
@ -1,25 +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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,18 +0,0 @@
|
|||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/core-style/core-style.html">
|
||||
<link rel="import" href="../bower_components/paper-dialog/paper-dialog.html">
|
||||
<link rel="import" href="../bower_components/paper-dialog/paper-dialog-transition.html">
|
||||
|
||||
<polymer-element name="ha-dialog" extends="paper-dialog">
|
||||
<template>
|
||||
<core-style ref='ha-dialog'></core-style>
|
||||
<shadow></shadow>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
layered: true,
|
||||
backdrop: true,
|
||||
transition: 'core-transition-bottom',
|
||||
});
|
||||
</script>
|
||||
</polymer-element>
|
|
@ -1,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
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
BIN
homeassistant/components/frontend/www_static/splash.png
Normal file
BIN
homeassistant/components/frontend/www_static/splash.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 51 KiB |
12
homeassistant/components/frontend/www_static/webcomponents-lite.min.js
vendored
Normal file
12
homeassistant/components/frontend/www_static/webcomponents-lite.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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
Loading…
Add table
Reference in a new issue