Merge remote-tracking branch 'refs/remotes/balloob/dev' into dev

This commit is contained in:
sander 2016-01-01 14:44:25 +01:00
commit b3227e491b
295 changed files with 15285 additions and 7626 deletions

View File

@ -15,8 +15,15 @@ omit =
homeassistant/components/*/modbus.py homeassistant/components/*/modbus.py
homeassistant/components/*/tellstick.py homeassistant/components/*/tellstick.py
homeassistant/components/tellduslive.py
homeassistant/components/*/tellduslive.py
homeassistant/components/*/vera.py homeassistant/components/*/vera.py
homeassistant/components/ecobee.py
homeassistant/components/*/ecobee.py
homeassistant/components/verisure.py homeassistant/components/verisure.py
homeassistant/components/*/verisure.py homeassistant/components/*/verisure.py
@ -29,26 +36,34 @@ omit =
homeassistant/components/rfxtrx.py homeassistant/components/rfxtrx.py
homeassistant/components/*/rfxtrx.py homeassistant/components/*/rfxtrx.py
homeassistant/components/ifttt.py homeassistant/components/binary_sensor/arest.py
homeassistant/components/binary_sensor/rest.py
homeassistant/components/browser.py homeassistant/components/browser.py
homeassistant/components/camera/* homeassistant/components/camera/*
homeassistant/components/device_tracker/actiontec.py homeassistant/components/device_tracker/actiontec.py
homeassistant/components/device_tracker/aruba.py homeassistant/components/device_tracker/aruba.py
homeassistant/components/device_tracker/asuswrt.py homeassistant/components/device_tracker/asuswrt.py
homeassistant/components/device_tracker/ddwrt.py homeassistant/components/device_tracker/ddwrt.py
homeassistant/components/device_tracker/fritz.py
homeassistant/components/device_tracker/icloud.py
homeassistant/components/device_tracker/luci.py homeassistant/components/device_tracker/luci.py
homeassistant/components/device_tracker/netgear.py homeassistant/components/device_tracker/netgear.py
homeassistant/components/device_tracker/nmap_tracker.py homeassistant/components/device_tracker/nmap_tracker.py
homeassistant/components/device_tracker/owntracks.py
homeassistant/components/device_tracker/snmp.py
homeassistant/components/device_tracker/thomson.py homeassistant/components/device_tracker/thomson.py
homeassistant/components/device_tracker/tomato.py homeassistant/components/device_tracker/tomato.py
homeassistant/components/device_tracker/tplink.py homeassistant/components/device_tracker/tplink.py
homeassistant/components/device_tracker/snmp.py homeassistant/components/device_tracker/ubus.py
homeassistant/components/discovery.py homeassistant/components/discovery.py
homeassistant/components/downloader.py homeassistant/components/downloader.py
homeassistant/components/ifttt.py
homeassistant/components/influxdb.py
homeassistant/components/keyboard.py homeassistant/components/keyboard.py
homeassistant/components/light/hue.py
homeassistant/components/light/limitlessled.py
homeassistant/components/light/blinksticklight.py homeassistant/components/light/blinksticklight.py
homeassistant/components/light/hue.py
homeassistant/components/light/hyperion.py
homeassistant/components/light/limitlessled.py
homeassistant/components/media_player/cast.py homeassistant/components/media_player/cast.py
homeassistant/components/media_player/denon.py homeassistant/components/media_player/denon.py
homeassistant/components/media_player/firetv.py homeassistant/components/media_player/firetv.py
@ -56,12 +71,12 @@ omit =
homeassistant/components/media_player/kodi.py homeassistant/components/media_player/kodi.py
homeassistant/components/media_player/mpd.py homeassistant/components/media_player/mpd.py
homeassistant/components/media_player/plex.py homeassistant/components/media_player/plex.py
homeassistant/components/media_player/squeezebox.py
homeassistant/components/media_player/sonos.py homeassistant/components/media_player/sonos.py
homeassistant/components/notify/file.py homeassistant/components/media_player/squeezebox.py
homeassistant/components/notify/instapush.py homeassistant/components/notify/instapush.py
homeassistant/components/notify/nma.py homeassistant/components/notify/nma.py
homeassistant/components/notify/pushbullet.py homeassistant/components/notify/pushbullet.py
homeassistant/components/notify/pushetta.py
homeassistant/components/notify/pushover.py homeassistant/components/notify/pushover.py
homeassistant/components/notify/slack.py homeassistant/components/notify/slack.py
homeassistant/components/notify/smtp.py homeassistant/components/notify/smtp.py
@ -70,9 +85,11 @@ omit =
homeassistant/components/notify/xmpp.py homeassistant/components/notify/xmpp.py
homeassistant/components/sensor/arest.py homeassistant/components/sensor/arest.py
homeassistant/components/sensor/bitcoin.py homeassistant/components/sensor/bitcoin.py
homeassistant/components/sensor/command_sensor.py homeassistant/components/sensor/cpuspeed.py
homeassistant/components/sensor/dht.py homeassistant/components/sensor/dht.py
homeassistant/components/sensor/dweet.py
homeassistant/components/sensor/efergy.py homeassistant/components/sensor/efergy.py
homeassistant/components/sensor/eliqonline.py
homeassistant/components/sensor/forecast.py homeassistant/components/sensor/forecast.py
homeassistant/components/sensor/glances.py homeassistant/components/sensor/glances.py
homeassistant/components/sensor/mysensors.py homeassistant/components/sensor/mysensors.py
@ -84,17 +101,24 @@ omit =
homeassistant/components/sensor/systemmonitor.py homeassistant/components/sensor/systemmonitor.py
homeassistant/components/sensor/temper.py homeassistant/components/sensor/temper.py
homeassistant/components/sensor/time_date.py homeassistant/components/sensor/time_date.py
homeassistant/components/sensor/torque.py
homeassistant/components/sensor/transmission.py homeassistant/components/sensor/transmission.py
homeassistant/components/sensor/twitch.py
homeassistant/components/sensor/worldclock.py homeassistant/components/sensor/worldclock.py
homeassistant/components/switch/arest.py homeassistant/components/switch/arest.py
homeassistant/components/switch/command_switch.py
homeassistant/components/switch/edimax.py homeassistant/components/switch/edimax.py
homeassistant/components/switch/hikvisioncam.py homeassistant/components/switch/hikvisioncam.py
homeassistant/components/switch/mystrom.py
homeassistant/components/switch/orvibo.py
homeassistant/components/switch/rest.py
homeassistant/components/switch/rpi_gpio.py homeassistant/components/switch/rpi_gpio.py
homeassistant/components/switch/transmission.py homeassistant/components/switch/transmission.py
homeassistant/components/switch/wemo.py homeassistant/components/switch/wemo.py
homeassistant/components/thermostat/honeywell_round_connected.py homeassistant/components/thermostat/heatmiser.py
homeassistant/components/thermostat/homematic.py
homeassistant/components/thermostat/honeywell.py
homeassistant/components/thermostat/nest.py homeassistant/components/thermostat/nest.py
homeassistant/components/thermostat/radiotherm.py
[report] [report]

1
.gitignore vendored
View File

@ -41,6 +41,7 @@ Icon
dist dist
build build
eggs eggs
.eggs
parts parts
bin bin
var var

View File

@ -2,10 +2,16 @@ sudo: false
language: python language: python
cache: cache:
directories: directories:
- $HOME/virtualenv/python3.4.2/ - $HOME/.cache/pip
# - "$HOME/virtualenv/python$TRAVIS_PYTHON_VERSION"
python: python:
- "3.4" - 3.4
- 3.5
install: install:
# Validate requirements_all.txt on Python 3.4
- if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then python3 setup.py -q develop 2>/dev/null; tput setaf 1; script/gen_requirements_all.py validate; tput sgr0; fi
- script/bootstrap_server - script/bootstrap_server
script: script:
- script/cibuild - script/cibuild
matrix:
fast_finish: true

View File

@ -17,19 +17,18 @@ For help on building your component, please see the [developer documentation](ht
After you finish adding support for your device: After you finish adding support for your device:
- Update the supported devices in the `README.md` file. - Add any new dependencies to `requirements_all.txt` if needed. Use `script/gen_requirements_all.py`.
- Add any new dependencies to `requirements_all.txt`. There is no ordering right now, so just add it to the end. - Update the `.coveragerc` file to exclude your platform if there are no tests available.
- Update the `.coveragerc` file. - Provide some documentation for [home-assistant.io](https://home-assistant.io/). It's OK to just add a docstring with configuration details (sample entry for `configuration.yaml` file and alike) to the file header as a start. Visit the [website documentation](https://home-assistant.io/developers/website/) for further information on contributing to [home-assistant.io](https://github.com/balloob/home-assistant.io).
- 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 check your repository, run `./script/lint`.
- 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. - 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/). - 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: If you add a platform for an existing component, there is usually no need for updating the frontend. Only if you've added a new component that should show up in the frontend, there are more steps needed:
- 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 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 - Update the demo component with two states that it provides.
- Add your component to home-assistant.conf.example - Add your component to `home-assistant.conf.example`.
Since you've updated `home-assistant-icons.html`, you've made changes to the frontend: Since you've updated `home-assistant-icons.html`, you've made changes to the frontend:

View File

@ -1,20 +1,27 @@
FROM python:3-onbuild FROM python:3.4
MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
VOLUME /config VOLUME /config
RUN pip3 install --no-cache-dir -r requirements_all.txt RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
# For the nmap tracker # For the nmap tracker
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends nmap net-tools && \ apt-get install -y --no-install-recommends nmap net-tools && \
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Open Z-Wave disabled because broken COPY script/build_python_openzwave script/build_python_openzwave
#RUN apt-get update && \ RUN apt-get update && \
# apt-get install -y cython3 libudev-dev && \ apt-get install -y cython3 libudev-dev && \
# apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \ apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
# pip3 install cython && \ pip3 install "cython<0.23" && \
# scripts/build_python_openzwave script/build_python_openzwave
COPY requirements_all.txt requirements_all.txt
RUN pip3 install --no-cache-dir -r requirements_all.txt
# Copy source
COPY . .
CMD [ "python", "-m", "homeassistant", "--config", "/config" ] CMD [ "python", "-m", "homeassistant", "--config", "/config" ]

View File

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2013 Paulus Schoutsen Copyright (c) 2016 Paulus Schoutsen
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in this software and associated documentation files (the "Software"), to deal in

View File

@ -1,4 +1,4 @@
include README.md include README.rst
include LICENSE include LICENSE
graft homeassistant graft homeassistant
prune homeassistant/components/frontend/www_static/home-assistant-polymer prune homeassistant/components/frontend/www_static/home-assistant-polymer

View File

@ -1,39 +0,0 @@
# Home Assistant [![Build Status](https://travis-ci.org/balloob/home-assistant.svg?branch=master)](https://travis-ci.org/balloob/home-assistant) [![Coverage Status](https://img.shields.io/coveralls/balloob/home-assistant.svg)](https://coveralls.io/r/balloob/home-assistant?branch=master) [![Join the chat at https://gitter.im/balloob/home-assistant](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/balloob/home-assistant?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[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.
To get started:
```bash
python3 -m pip install homeassistant
hass --open-ui
```
Check out [the website](https://home-assistant.io) for [a demo][demo], installation instructions, tutorials and documentation.
[![screenshot-states](https://raw.github.com/balloob/home-assistant/master/docs/screenshots.png)][demo]
Examples of devices it can interface it:
* Monitoring connected devices to a wireless router: [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index), [TPLink](http://www.tp-link.us/), [ASUSWRT](http://event.asus.com/2013/nw/ASUSWRT/) and any SNMP capable Linksys WAP/WRT
*
* [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), [Plex](https://plex.tv/), [Kodi (XBMC)](http://kodi.tv/), iTunes (by way of [itunes-api](https://github.com/maddox/itunes-api)), and Amazon Fire TV (by way of [python-firetv](https://github.com/happyleavesaoc/python-firetv))
* 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/), [RFXtrx](http://www.rfxcom.com/), [Arduino](https://www.arduino.cc/), [Raspberry Pi](https://www.raspberrypi.org/), and [Modbus](http://www.modbus.org/)
* Interaction with [IFTTT](https://ifttt.com/)
* 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 less light
* Turn off all lights and devices when everybody leaves the house
* Offers a [REST API](https://home-assistant.io/developers/api.html) and can interface with MQTT for easy integration with other projects like [OwnTracks](http://owntracks.org/)
* 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/), [Telegram](https://telegram.org/), 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, check the [Home Assistant help section](https://home-assistant.io/help/) how to reach us.

98
README.rst Normal file
View File

@ -0,0 +1,98 @@
Home Assistant |Build Status| |Coverage Status| |Join the chat at https://gitter.im/balloob/home-assistant|
===========================================================================================================
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.
To get started:
.. code:: bash
python3 -m pip install homeassistant
hass --open-ui
Check out `the website <https://home-assistant.io>`__ for `a
demo <https://home-assistant.io/demo/>`__, installation instructions,
tutorials and documentation.
|screenshot-states|
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/>`__,
`ASUSWRT <http://event.asus.com/2013/nw/ASUSWRT/>`__ and any SNMP
capable Linksys WAP/WRT
- `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, 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>`__,
`Plex <https://plex.tv/>`__, `Kodi (XBMC) <http://kodi.tv/>`__,
iTunes (by way of
`itunes-api <https://github.com/maddox/itunes-api>`__), and Amazon
Fire TV (by way of
`python-firetv <https://github.com/happyleavesaoc/python-firetv>`__)
- 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/>`__,
`RFXtrx <http://www.rfxcom.com/>`__,
`Arduino <https://www.arduino.cc/>`__, `Raspberry
Pi <https://www.raspberrypi.org/>`__, and
`Modbus <http://www.modbus.org/>`__
- Interaction with `IFTTT <https://ifttt.com/>`__
- 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 less light
- Turn off all lights and devices when everybody leaves the house
- Offers a `REST API <https://home-assistant.io/developers/api/>`__
and can interface with MQTT for easy integration with other projects
like `OwnTracks <http://owntracks.org/>`__
- 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/>`__,
`Telegram <https://telegram.org/>`__, 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, check the `Home Assistant help
section <https://home-assistant.io/help/>`__ how to reach us.
.. |Build Status| image:: https://travis-ci.org/balloob/home-assistant.svg?branch=master
:target: https://travis-ci.org/balloob/home-assistant
.. |Coverage Status| image:: https://img.shields.io/coveralls/balloob/home-assistant.svg
:target: https://coveralls.io/r/balloob/home-assistant?branch=master
.. |Join the chat at https://gitter.im/balloob/home-assistant| image:: https://badges.gitter.im/Join%20Chat.svg
:target: https://gitter.im/balloob/home-assistant?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
.. |screenshot-states| image:: https://raw.github.com/balloob/home-assistant/master/docs/screenshots.png
:target: https://home-assistant.io/demo/

View File

@ -9,11 +9,12 @@ After bootstrapping you can add your own components or
start by calling homeassistant.start_home_assistant(bus) start by calling homeassistant.start_home_assistant(bus)
""" """
import os from collections import defaultdict
import sys
import logging import logging
import logging.handlers import logging.handlers
from collections import defaultdict import os
import shutil
import sys
import homeassistant.core as core import homeassistant.core as core
import homeassistant.util.dt as date_util import homeassistant.util.dt as date_util
@ -25,7 +26,7 @@ import homeassistant.components as core_components
import homeassistant.components.group as group import homeassistant.components.group as group
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.const import ( from homeassistant.const import (
EVENT_COMPONENT_LOADED, CONF_LATITUDE, CONF_LONGITUDE, __version__, EVENT_COMPONENT_LOADED, CONF_LATITUDE, CONF_LONGITUDE,
CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_TIME_ZONE, CONF_CUSTOMIZE, CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_TIME_ZONE, CONF_CUSTOMIZE,
TEMP_CELCIUS, TEMP_FAHRENHEIT) TEMP_CELCIUS, TEMP_FAHRENHEIT)
@ -34,6 +35,7 @@ _LOGGER = logging.getLogger(__name__)
ATTR_COMPONENT = 'component' ATTR_COMPONENT = 'component'
PLATFORM_FORMAT = '{}.{}' PLATFORM_FORMAT = '{}.{}'
ERROR_LOG_FILENAME = 'home-assistant.log'
def setup_component(hass, domain, config=None): def setup_component(hass, domain, config=None):
@ -80,7 +82,7 @@ def _setup_component(hass, domain, config):
return True return True
component = loader.get_component(domain) component = loader.get_component(domain)
missing_deps = [dep for dep in component.DEPENDENCIES missing_deps = [dep for dep in getattr(component, 'DEPENDENCIES', [])
if dep not in hass.config.components] if dep not in hass.config.components]
if missing_deps: if missing_deps:
@ -104,7 +106,7 @@ def _setup_component(hass, domain, config):
# Assumption: if a component does not depend on groups # Assumption: if a component does not depend on groups
# it communicates with devices # it communicates with devices
if group.DOMAIN not in component.DEPENDENCIES: if group.DOMAIN not in getattr(component, 'DEPENDENCIES', []):
hass.pool.add_worker() hass.pool.add_worker()
hass.bus.fire( hass.bus.fire(
@ -131,8 +133,7 @@ def prepare_setup_platform(hass, config, domain, platform_name):
return platform return platform
# Load dependencies # Load dependencies
if hasattr(platform, 'DEPENDENCIES'): for component in getattr(platform, 'DEPENDENCIES', []):
for component in platform.DEPENDENCIES:
if not setup_component(hass, component, config): if not setup_component(hass, component, config):
_LOGGER.error( _LOGGER.error(
'Unable to prepare setup for platform %s because ' 'Unable to prepare setup for platform %s because '
@ -167,6 +168,7 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
hass.config.config_dir = config_dir hass.config.config_dir = config_dir
mount_local_lib_path(config_dir) mount_local_lib_path(config_dir)
process_ha_config_upgrade(hass)
process_ha_core_config(hass, config.get(core.DOMAIN, {})) process_ha_core_config(hass, config.get(core.DOMAIN, {}))
if enable_log: if enable_log:
@ -252,7 +254,7 @@ def enable_logging(hass, verbose=False, daemon=False, log_rotate_days=None):
"Colorlog package not found, console coloring disabled") "Colorlog package not found, console coloring disabled")
# Log errors to a file if we have write access to file or config dir # 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(ERROR_LOG_FILENAME)
err_path_exists = os.path.isfile(err_log_path) err_path_exists = os.path.isfile(err_log_path)
# Check if we can write to the error log if it exists or that # Check if we can write to the error log if it exists or that
@ -273,13 +275,38 @@ def enable_logging(hass, verbose=False, daemon=False, log_rotate_days=None):
datefmt='%y-%m-%d %H:%M:%S')) datefmt='%y-%m-%d %H:%M:%S'))
logger = logging.getLogger('') logger = logging.getLogger('')
logger.addHandler(err_handler) logger.addHandler(err_handler)
logger.setLevel(logging.INFO) # this sets the minimum log level logger.setLevel(logging.INFO)
else: else:
_LOGGER.error( _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_config_upgrade(hass):
""" Upgrade config if necessary. """
version_path = hass.config.path('.HA_VERSION')
try:
with open(version_path, 'rt') as inp:
conf_version = inp.readline().strip()
except FileNotFoundError:
# Last version to not have this file
conf_version = '0.7.7'
if conf_version == __version__:
return
_LOGGER.info('Upgrading config directory from %s to %s', conf_version,
__version__)
lib_path = hass.config.path('lib')
if os.path.isdir(lib_path):
shutil.rmtree(lib_path)
with open(version_path, 'wt') as outp:
outp.write(__version__)
def process_ha_core_config(hass, config): def process_ha_core_config(hass, config):
""" Processes the [homeassistant] section from the config. """ """ Processes the [homeassistant] section from the config. """
hac = hass.config hac = hass.config

View File

@ -1,7 +1,6 @@
""" """
homeassistant.components homeassistant.components
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~
This package contains components that can be plugged into Home Assistant. This package contains components that can be plugged into Home Assistant.
Component design guidelines: Component design guidelines:
@ -12,7 +11,6 @@ Each component that tracks states should create state entity names in the
format "<DOMAIN>.<OBJECT_ID>". format "<DOMAIN>.<OBJECT_ID>".
Each component should publish services only under its own domain. Each component should publish services only under its own domain.
""" """
import itertools as it import itertools as it
import logging import logging

View File

@ -15,7 +15,6 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
DOMAIN = 'alarm_control_panel' DOMAIN = 'alarm_control_panel'
DEPENDENCIES = []
SCAN_INTERVAL = 30 SCAN_INTERVAL = 30
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'

View File

@ -0,0 +1,13 @@
"""
homeassistant.components.alarm_control_panel.demo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Demo platform that has two fake alarm control panels.
"""
import homeassistant.components.alarm_control_panel.manual as manual
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the Demo alarm control panels. """
add_devices([
manual.ManualAlarm(hass, 'Alarm', '1234', 5, 10),
])

View File

@ -4,7 +4,7 @@ homeassistant.components.alarm_control_panel.manual
Support for manual alarms. Support for manual alarms.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.manual.html https://home-assistant.io/components/alarm_control_panel.manual/
""" """
import logging import logging
import datetime import datetime
@ -18,8 +18,6 @@ from homeassistant.const import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = []
DEFAULT_ALARM_NAME = 'HA Alarm' DEFAULT_ALARM_NAME = 'HA Alarm'
DEFAULT_PENDING_TIME = 60 DEFAULT_PENDING_TIME = 60
DEFAULT_TRIGGER_TIME = 120 DEFAULT_TRIGGER_TIME = 120
@ -76,9 +74,10 @@ class ManualAlarm(alarm.AlarmControlPanel):
return STATE_ALARM_PENDING return STATE_ALARM_PENDING
if self._state == STATE_ALARM_TRIGGERED and self._trigger_time: if self._state == STATE_ALARM_TRIGGERED and self._trigger_time:
if self._state_ts + self._trigger_time > dt_util.utcnow(): if self._state_ts + self._pending_time > dt_util.utcnow():
return STATE_ALARM_PENDING return STATE_ALARM_PENDING
elif dt_util.utcnow() >= self._state_ts + (2 * self._trigger_time): elif (self._state_ts + self._pending_time +
self._trigger_time) < dt_util.utcnow():
return STATE_ALARM_DISARMED return STATE_ALARM_DISARMED
return self._state return self._state
@ -134,11 +133,11 @@ class ManualAlarm(alarm.AlarmControlPanel):
if self._trigger_time: if self._trigger_time:
track_point_in_time( track_point_in_time(
self._hass, self.update_ha_state, self._hass, self.update_ha_state,
self._state_ts + self._trigger_time) self._state_ts + self._pending_time)
track_point_in_time( track_point_in_time(
self._hass, self.update_ha_state, self._hass, self.update_ha_state,
self._state_ts + 2 * self._trigger_time) self._state_ts + self._pending_time + self._trigger_time)
def _validate_code(self, code, state): def _validate_code(self, code, state):
""" Validate given code. """ """ Validate given code. """

View File

@ -4,7 +4,7 @@ homeassistant.components.alarm_control_panel.mqtt
This platform enables the possibility to control a MQTT alarm. This platform enables the possibility to control a MQTT alarm.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.mqtt.html https://home-assistant.io/components/alarm_control_panel.mqtt/
""" """
import logging import logging
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt

View File

@ -2,6 +2,9 @@
homeassistant.components.alarm_control_panel.verisure homeassistant.components.alarm_control_panel.verisure
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Interfaces with Verisure alarm control panel. Interfaces with Verisure alarm control panel.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/verisure/
""" """
import logging import logging
@ -26,7 +29,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
alarms.extend([ alarms.extend([
VerisureAlarm(value) VerisureAlarm(value)
for value in verisure.get_alarm_status().values() for value in verisure.ALARM_STATUS.values()
if verisure.SHOW_ALARM if verisure.SHOW_ALARM
]) ])
@ -39,7 +42,6 @@ class VerisureAlarm(alarm.AlarmControlPanel):
def __init__(self, alarm_status): def __init__(self, alarm_status):
self._id = alarm_status.id self._id = alarm_status.id
self._device = verisure.MY_PAGES.DEVICE_ALARM
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
@property @property
@ -59,36 +61,36 @@ class VerisureAlarm(alarm.AlarmControlPanel):
def update(self): def update(self):
""" Update alarm status """ """ Update alarm status """
verisure.update() verisure.update_alarm()
if verisure.STATUS[self._device][self._id].status == 'unarmed': if verisure.ALARM_STATUS[self._id].status == 'unarmed':
self._state = STATE_ALARM_DISARMED self._state = STATE_ALARM_DISARMED
elif verisure.STATUS[self._device][self._id].status == 'armedhome': elif verisure.ALARM_STATUS[self._id].status == 'armedhome':
self._state = STATE_ALARM_ARMED_HOME self._state = STATE_ALARM_ARMED_HOME
elif verisure.STATUS[self._device][self._id].status == 'armedaway': elif verisure.ALARM_STATUS[self._id].status == 'armedaway':
self._state = STATE_ALARM_ARMED_AWAY self._state = STATE_ALARM_ARMED_AWAY
elif verisure.STATUS[self._device][self._id].status != 'pending': elif verisure.ALARM_STATUS[self._id].status != 'pending':
_LOGGER.error( _LOGGER.error(
'Unknown alarm state %s', 'Unknown alarm state %s',
verisure.STATUS[self._device][self._id].status) verisure.ALARM_STATUS[self._id].status)
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
""" Send disarm command. """ """ Send disarm command. """
verisure.MY_PAGES.set_alarm_status( verisure.MY_PAGES.alarm.set(code, 'DISARMED')
code, _LOGGER.info('verisure alarm disarming')
verisure.MY_PAGES.ALARM_DISARMED) verisure.MY_PAGES.alarm.wait_while_pending()
_LOGGER.warning('disarming') verisure.update_alarm()
def alarm_arm_home(self, code=None): def alarm_arm_home(self, code=None):
""" Send arm home command. """ """ Send arm home command. """
verisure.MY_PAGES.set_alarm_status( verisure.MY_PAGES.alarm.set(code, 'ARMED_HOME')
code, _LOGGER.info('verisure alarm arming home')
verisure.MY_PAGES.ALARM_ARMED_HOME) verisure.MY_PAGES.alarm.wait_while_pending()
_LOGGER.warning('arming home') verisure.update_alarm()
def alarm_arm_away(self, code=None): def alarm_arm_away(self, code=None):
""" Send arm away command. """ """ Send arm away command. """
verisure.MY_PAGES.set_alarm_status( verisure.MY_PAGES.alarm.set(code, 'ARMED_AWAY')
code, _LOGGER.info('verisure alarm arming away')
verisure.MY_PAGES.ALARM_ARMED_AWAY) verisure.MY_PAGES.alarm.wait_while_pending()
_LOGGER.warning('arming away') verisure.update_alarm()

View File

@ -0,0 +1,186 @@
"""
components.alexa
~~~~~~~~~~~~~~~~
Component to offer a service end point for an Alexa skill.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/alexa/
"""
import enum
import logging
from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY
from homeassistant.util import template
DOMAIN = 'alexa'
DEPENDENCIES = ['http']
_LOGGER = logging.getLogger(__name__)
_CONFIG = {}
API_ENDPOINT = '/api/alexa'
CONF_INTENTS = 'intents'
CONF_CARD = 'card'
CONF_SPEECH = 'speech'
def setup(hass, config):
""" Activate Alexa component. """
_CONFIG.update(config[DOMAIN].get(CONF_INTENTS, {}))
hass.http.register_path('POST', API_ENDPOINT, _handle_alexa, True)
return True
def _handle_alexa(handler, path_match, data):
""" Handle Alexa. """
_LOGGER.debug('Received Alexa request: %s', data)
req = data.get('request')
if req is None:
_LOGGER.error('Received invalid data from Alexa: %s', data)
handler.write_json_message(
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
return
req_type = req['type']
if req_type == 'SessionEndedRequest':
handler.send_response(HTTP_OK)
handler.end_headers()
return
intent = req.get('intent')
response = AlexaResponse(handler.server.hass, intent)
if req_type == 'LaunchRequest':
response.add_speech(
SpeechType.plaintext,
"Hello, and welcome to the future. How may I help?")
handler.write_json(response.as_dict())
return
if req_type != 'IntentRequest':
_LOGGER.warning('Received unsupported request: %s', req_type)
return
intent_name = intent['name']
config = _CONFIG.get(intent_name)
if config is None:
_LOGGER.warning('Received unknown intent %s', intent_name)
response.add_speech(
SpeechType.plaintext,
"This intent is not yet configured within Home Assistant.")
handler.write_json(response.as_dict())
return
speech = config.get(CONF_SPEECH)
card = config.get(CONF_CARD)
# pylint: disable=unsubscriptable-object
if speech is not None:
response.add_speech(SpeechType[speech['type']], speech['text'])
if card is not None:
response.add_card(CardType[card['type']], card['title'],
card['content'])
handler.write_json(response.as_dict())
class SpeechType(enum.Enum):
""" Alexa speech types. """
plaintext = "PlainText"
ssml = "SSML"
class CardType(enum.Enum):
""" Alexa card types. """
simple = "Simple"
link_account = "LinkAccount"
class AlexaResponse(object):
""" Helps generating the response for Alexa. """
def __init__(self, hass, intent=None):
self.hass = hass
self.speech = None
self.card = None
self.reprompt = None
self.session_attributes = {}
self.should_end_session = True
if intent is not None and 'slots' in intent:
self.variables = {key: value['value'] for key, value
in intent['slots'].items() if 'value' in value}
else:
self.variables = {}
def add_card(self, card_type, title, content):
""" Add a card to the response. """
assert self.card is None
card = {
"type": card_type.value
}
if card_type == CardType.link_account:
self.card = card
return
card["title"] = self._render(title),
card["content"] = self._render(content)
self.card = card
def add_speech(self, speech_type, text):
""" Add speech to the response. """
assert self.speech is None
key = 'ssml' if speech_type == SpeechType.ssml else 'text'
self.speech = {
'type': speech_type.value,
key: self._render(text)
}
def add_reprompt(self, speech_type, text):
""" Add repromopt if user does not answer. """
assert self.reprompt is None
key = 'ssml' if speech_type == SpeechType.ssml else 'text'
self.reprompt = {
'type': speech_type.value,
key: self._render(text)
}
def as_dict(self):
""" Returns response in an Alexa valid dict. """
response = {
'shouldEndSession': self.should_end_session
}
if self.card is not None:
response['card'] = self.card
if self.speech is not None:
response['outputSpeech'] = self.speech
if self.reprompt is not None:
response['reprompt'] = {
'outputSpeech': self.reprompt
}
return {
'version': '1.0',
'sessionAttributes': self.session_attributes,
'response': response,
}
def _render(self, template_string):
""" Render a response, adding data from intent if available. """
return template.render(self.hass, template_string, self.variables)

View File

@ -1,8 +1,10 @@
""" """
homeassistant.components.api homeassistant.components.api
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides a Rest API for Home Assistant. Provides a Rest API for Home Assistant.
For more details about the RESTful API, please refer to the documentation at
https://home-assistant.io/developers/api/
""" """
import re import re
import logging import logging
@ -10,15 +12,19 @@ import threading
import json import json
import homeassistant.core as ha import homeassistant.core as ha
from homeassistant.exceptions import TemplateError
from homeassistant.helpers.state import TrackStates from homeassistant.helpers.state import TrackStates
import homeassistant.remote as rem import homeassistant.remote as rem
from homeassistant.util import template
from homeassistant.bootstrap import ERROR_LOG_FILENAME
from homeassistant.const import ( from homeassistant.const import (
URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES, URL_API_STREAM, 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_EVENT_FORWARD, URL_API_STATES_ENTITY, URL_API_COMPONENTS,
URL_API_CONFIG, URL_API_BOOTSTRAP, URL_API_CONFIG, URL_API_BOOTSTRAP, URL_API_ERROR_LOG, URL_API_LOG_OUT,
EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, MATCH_ALL, URL_API_TEMPLATE, EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, MATCH_ALL,
HTTP_OK, HTTP_CREATED, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_OK, HTTP_CREATED, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
HTTP_UNPROCESSABLE_ENTITY) HTTP_UNPROCESSABLE_ENTITY, HTTP_HEADER_CONTENT_TYPE,
CONTENT_TYPE_TEXT_PLAIN)
DOMAIN = 'api' DOMAIN = 'api'
@ -33,10 +39,6 @@ _LOGGER = logging.getLogger(__name__)
def setup(hass, config): def setup(hass, config):
""" Register the API with the HTTP interface. """ """ Register the API with the HTTP interface. """
if 'http' not in hass.config.components:
_LOGGER.error('Dependency http is not loaded')
return False
# /api - for validation purposes # /api - for validation purposes
hass.http.register_path('GET', URL_API, _handle_get_api) hass.http.register_path('GET', URL_API, _handle_get_api)
@ -87,6 +89,14 @@ def setup(hass, config):
hass.http.register_path( hass.http.register_path(
'GET', URL_API_COMPONENTS, _handle_get_api_components) 'GET', URL_API_COMPONENTS, _handle_get_api_components)
hass.http.register_path('GET', URL_API_ERROR_LOG,
_handle_get_api_error_log)
hass.http.register_path('POST', URL_API_LOG_OUT, _handle_post_api_log_out)
hass.http.register_path('POST', URL_API_TEMPLATE,
_handle_post_api_template)
return True return True
@ -102,6 +112,7 @@ def _handle_get_api_stream(handler, path_match, data):
wfile = handler.wfile wfile = handler.wfile
write_lock = threading.Lock() write_lock = threading.Lock()
block = threading.Event() block = threading.Event()
session_id = None
restrict = data.get('restrict') restrict = data.get('restrict')
if restrict: if restrict:
@ -115,27 +126,34 @@ def _handle_get_api_stream(handler, path_match, data):
try: try:
wfile.write(msg.encode("UTF-8")) wfile.write(msg.encode("UTF-8"))
wfile.flush() wfile.flush()
except IOError: except (IOError, ValueError):
# IOError: socket errors
# ValueError: raised when 'I/O operation on closed file'
block.set() block.set()
def forward_events(event): def forward_events(event):
""" Forwards events to the open request. """ """ Forwards events to the open request. """
nonlocal gracefully_closed nonlocal gracefully_closed
if block.is_set() or event.event_type == EVENT_TIME_CHANGED or \ if block.is_set() or event.event_type == EVENT_TIME_CHANGED:
restrict and event.event_type not in restrict:
return return
elif event.event_type == EVENT_HOMEASSISTANT_STOP: elif event.event_type == EVENT_HOMEASSISTANT_STOP:
gracefully_closed = True gracefully_closed = True
block.set() block.set()
return return
handler.server.sessions.extend_validation(session_id)
write_message(json.dumps(event, cls=rem.JSONEncoder)) write_message(json.dumps(event, cls=rem.JSONEncoder))
handler.send_response(HTTP_OK) handler.send_response(HTTP_OK)
handler.send_header('Content-type', 'text/event-stream') handler.send_header('Content-type', 'text/event-stream')
session_id = handler.set_session_cookie_header()
handler.end_headers() handler.end_headers()
if restrict:
for event in restrict:
hass.bus.listen(event, forward_events)
else:
hass.bus.listen(MATCH_ALL, forward_events) hass.bus.listen(MATCH_ALL, forward_events)
while True: while True:
@ -150,6 +168,10 @@ def _handle_get_api_stream(handler, path_match, data):
_LOGGER.info("Found broken event stream to %s, cleaning up", _LOGGER.info("Found broken event stream to %s, cleaning up",
handler.client_address[0]) handler.client_address[0])
if restrict:
for event in restrict:
hass.bus.remove_listener(event, forward_events)
else:
hass.bus.remove_listener(MATCH_ALL, forward_events) hass.bus.remove_listener(MATCH_ALL, forward_events)
@ -339,6 +361,35 @@ def _handle_get_api_components(handler, path_match, data):
handler.write_json(handler.server.hass.config.components) handler.write_json(handler.server.hass.config.components)
def _handle_get_api_error_log(handler, path_match, data):
""" Returns the logged errors for this session. """
handler.write_file(handler.server.hass.config.path(ERROR_LOG_FILENAME),
False)
def _handle_post_api_log_out(handler, path_match, data):
""" Log user out. """
handler.send_response(HTTP_OK)
handler.destroy_session()
handler.end_headers()
def _handle_post_api_template(handler, path_match, data):
""" Log user out. """
template_string = data.get('template', '')
try:
rendered = template.render(handler.server.hass, template_string)
handler.send_response(HTTP_OK)
handler.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN)
handler.end_headers()
handler.wfile.write(rendered.encode('utf-8'))
except TemplateError as e:
handler.write_json_message(str(e), HTTP_UNPROCESSABLE_ENTITY)
return
def _services_json(hass): def _services_json(hass):
""" Generate services data to JSONify. """ """ Generate services data to JSONify. """
return [{"domain": key, "services": value} return [{"domain": key, "services": value}

View File

@ -4,26 +4,8 @@ components.arduino
Arduino component that connects to a directly attached Arduino board which Arduino component that connects to a directly attached Arduino board which
runs with the Firmata firmware. runs with the Firmata firmware.
Configuration: For more details about this component, please refer to the documentation at
https://home-assistant.io/components/arduino/
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 import logging
@ -37,7 +19,6 @@ from homeassistant.const import (EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP) EVENT_HOMEASSISTANT_STOP)
DOMAIN = "arduino" DOMAIN = "arduino"
DEPENDENCIES = []
REQUIREMENTS = ['PyMata==2.07a'] REQUIREMENTS = ['PyMata==2.07a']
BOARD = None BOARD = None
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -1,8 +1,10 @@
""" """
homeassistant.components.automation homeassistant.components.automation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Allows to setup simple automation rules via the config file. Allows to setup simple automation rules via the config file.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/automation/
""" """
import logging import logging
@ -121,7 +123,7 @@ def _migrate_old_config(config):
_LOGGER.warning( _LOGGER.warning(
'You are using an old configuration format. Please upgrade: ' 'You are using an old configuration format. Please upgrade: '
'https://home-assistant.io/components/automation.html') 'https://home-assistant.io/components/automation/')
new_conf = { new_conf = {
CONF_TRIGGER: dict(config), CONF_TRIGGER: dict(config),

View File

@ -4,7 +4,7 @@ homeassistant.components.automation.event
Offers event listening automation rules. Offers event listening automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation.html#event-trigger at https://home-assistant.io/components/automation/#event-trigger
""" """
import logging import logging

View File

@ -4,7 +4,7 @@ homeassistant.components.automation.mqtt
Offers MQTT listening automation rules. Offers MQTT listening automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation.html#mqtt-trigger at https://home-assistant.io/components/automation/#mqtt-trigger
""" """
import logging import logging

View File

@ -1,14 +1,16 @@
""" """
homeassistant.components.automation.numeric_state homeassistant.components.automation.numeric_state
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Offers numeric state listening automation rules. Offers numeric state listening automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation.html#numeric-state-trigger at https://home-assistant.io/components/automation/#numeric-state-trigger
""" """
import logging import logging
from homeassistant.const import CONF_VALUE_TEMPLATE
from homeassistant.helpers.event import track_state_change from homeassistant.helpers.event import track_state_change
from homeassistant.util import template
CONF_ENTITY_ID = "entity_id" CONF_ENTITY_ID = "entity_id"
@ -28,6 +30,7 @@ def trigger(hass, config, action):
below = config.get(CONF_BELOW) below = config.get(CONF_BELOW)
above = config.get(CONF_ABOVE) above = config.get(CONF_ABOVE)
value_template = config.get(CONF_VALUE_TEMPLATE)
if below is None and above is None: if below is None and above is None:
_LOGGER.error("Missing configuration key." _LOGGER.error("Missing configuration key."
@ -35,13 +38,20 @@ def trigger(hass, config, action):
CONF_BELOW, CONF_ABOVE) CONF_BELOW, CONF_ABOVE)
return False return False
if value_template is not None:
renderer = lambda value: template.render(hass,
value_template,
{'state': value})
else:
renderer = lambda value: value.state
# pylint: disable=unused-argument # pylint: disable=unused-argument
def state_automation_listener(entity, from_s, to_s): def state_automation_listener(entity, from_s, to_s):
""" Listens for state changes and calls action. """ """ Listens for state changes and calls action. """
# Fire action if we go from outside range into range # Fire action if we go from outside range into range
if _in_range(to_s.state, above, below) and \ if _in_range(above, below, renderer(to_s)) and \
(from_s is None or not _in_range(from_s.state, above, below)): (from_s is None or not _in_range(above, below, renderer(from_s))):
action() action()
track_state_change( track_state_change(
@ -61,6 +71,7 @@ def if_action(hass, config):
below = config.get(CONF_BELOW) below = config.get(CONF_BELOW)
above = config.get(CONF_ABOVE) above = config.get(CONF_ABOVE)
value_template = config.get(CONF_VALUE_TEMPLATE)
if below is None and above is None: if below is None and above is None:
_LOGGER.error("Missing configuration key." _LOGGER.error("Missing configuration key."
@ -68,21 +79,28 @@ def if_action(hass, config):
CONF_BELOW, CONF_ABOVE) CONF_BELOW, CONF_ABOVE)
return None return None
if value_template is not None:
renderer = lambda value: template.render(hass,
value_template,
{'state': value})
else:
renderer = lambda value: value.state
def if_numeric_state(): def if_numeric_state():
""" Test numeric state condition. """ """ Test numeric state condition. """
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
return state is not None and _in_range(state.state, above, below) return state is not None and _in_range(above, below, renderer(state))
return if_numeric_state return if_numeric_state
def _in_range(value, range_start, range_end): def _in_range(range_start, range_end, value):
""" Checks if value is inside the range """ """ Checks if value is inside the range """
try: try:
value = float(value) value = float(value)
except ValueError: except ValueError:
_LOGGER.warn("Missing value in numeric check") _LOGGER.warning("Value returned from template is not a number: %s",
value)
return False return False
if range_start is not None and range_end is not None: if range_start is not None and range_end is not None:

View File

@ -4,7 +4,7 @@ homeassistant.components.automation.state
Offers state listening automation rules. Offers state listening automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation.html#state-trigger at https://home-assistant.io/components/automation/#state-trigger
""" """
import logging import logging

View File

@ -4,7 +4,7 @@ homeassistant.components.automation.sun
Offers sun based automation rules. Offers sun based automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation.html#sun-trigger at https://home-assistant.io/components/automation/#sun-trigger
""" """
import logging import logging
from datetime import timedelta from datetime import timedelta
@ -17,6 +17,10 @@ DEPENDENCIES = ['sun']
CONF_OFFSET = 'offset' CONF_OFFSET = 'offset'
CONF_EVENT = 'event' CONF_EVENT = 'event'
CONF_BEFORE = "before"
CONF_BEFORE_OFFSET = "before_offset"
CONF_AFTER = "after"
CONF_AFTER_OFFSET = "after_offset"
EVENT_SUNSET = 'sunset' EVENT_SUNSET = 'sunset'
EVENT_SUNRISE = 'sunrise' EVENT_SUNRISE = 'sunrise'
@ -37,27 +41,10 @@ def trigger(hass, config, action):
_LOGGER.error("Invalid value for %s: %s", CONF_EVENT, event) _LOGGER.error("Invalid value for %s: %s", CONF_EVENT, event)
return False return False
if CONF_OFFSET in config: offset = _parse_offset(config.get(CONF_OFFSET))
raw_offset = config.get(CONF_OFFSET) if offset is False:
negative_offset = False
if raw_offset.startswith('-'):
negative_offset = True
raw_offset = raw_offset[1:]
try:
(hour, minute, second) = [int(x) for x in raw_offset.split(':')]
except ValueError:
_LOGGER.error('Could not parse offset %s', raw_offset)
return False return False
offset = timedelta(hours=hour, minutes=minute, seconds=second)
if negative_offset:
offset *= -1
else:
offset = timedelta(0)
# Do something to call action # Do something to call action
if event == EVENT_SUNRISE: if event == EVENT_SUNRISE:
trigger_sunrise(hass, action, offset) trigger_sunrise(hass, action, offset)
@ -67,6 +54,65 @@ def trigger(hass, config, action):
return True return True
def if_action(hass, config):
""" Wraps action method with sun based condition. """
before = config.get(CONF_BEFORE)
after = config.get(CONF_AFTER)
# Make sure required configuration keys are present
if before is None and after is None:
logging.getLogger(__name__).error(
"Missing if-condition configuration key %s or %s",
CONF_BEFORE, CONF_AFTER)
return None
# Make sure configuration keys have the right value
if before not in (None, EVENT_SUNRISE, EVENT_SUNSET) or \
after not in (None, EVENT_SUNRISE, EVENT_SUNSET):
logging.getLogger(__name__).error(
"%s and %s can only be set to %s or %s",
CONF_BEFORE, CONF_AFTER, EVENT_SUNRISE, EVENT_SUNSET)
return None
before_offset = _parse_offset(config.get(CONF_BEFORE_OFFSET))
after_offset = _parse_offset(config.get(CONF_AFTER_OFFSET))
if before_offset is False or after_offset is False:
return None
if before is None:
before_func = lambda: None
elif before == EVENT_SUNRISE:
before_func = lambda: sun.next_rising_utc(hass) + before_offset
else:
before_func = lambda: sun.next_setting_utc(hass) + before_offset
if after is None:
after_func = lambda: None
elif after == EVENT_SUNRISE:
after_func = lambda: sun.next_rising_utc(hass) + after_offset
else:
after_func = lambda: sun.next_setting_utc(hass) + after_offset
def time_if():
""" Validate time based if-condition """
now = dt_util.utcnow()
before = before_func()
after = after_func()
if before is not None and now > now.replace(hour=before.hour,
minute=before.minute):
return False
if after is not None and now < now.replace(hour=after.hour,
minute=after.minute):
return False
return True
return time_if
def trigger_sunrise(hass, action, offset): def trigger_sunrise(hass, action, offset):
""" Trigger action at next sun rise. """ """ Trigger action at next sun rise. """
def next_rise(): def next_rise():
@ -103,3 +149,26 @@ def trigger_sunset(hass, action, offset):
action() action()
track_point_in_utc_time(hass, sunset_automation_listener, next_set()) track_point_in_utc_time(hass, sunset_automation_listener, next_set())
def _parse_offset(raw_offset):
if raw_offset is None:
return timedelta(0)
negative_offset = False
if raw_offset.startswith('-'):
negative_offset = True
raw_offset = raw_offset[1:]
try:
(hour, minute, second) = [int(x) for x in raw_offset.split(':')]
except ValueError:
_LOGGER.error('Could not parse offset %s', raw_offset)
return False
offset = timedelta(hours=hour, minutes=minute, seconds=second)
if negative_offset:
offset *= -1
return offset

View File

@ -0,0 +1,65 @@
"""
homeassistant.components.automation.template
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Offers template automation rules.
For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#template-trigger
"""
import logging
from homeassistant.const import CONF_VALUE_TEMPLATE, EVENT_STATE_CHANGED
from homeassistant.exceptions import TemplateError
from homeassistant.util import template
_LOGGER = logging.getLogger(__name__)
def trigger(hass, config, action):
""" Listen for state changes based on `config`. """
value_template = config.get(CONF_VALUE_TEMPLATE)
if value_template is None:
_LOGGER.error("Missing configuration key %s", CONF_VALUE_TEMPLATE)
return False
# Local variable to keep track of if the action has already been triggered
already_triggered = False
def event_listener(event):
""" Listens for state changes and calls action. """
nonlocal already_triggered
template_result = _check_template(hass, value_template)
# Check to see if template returns true
if template_result and not already_triggered:
already_triggered = True
action()
elif not template_result:
already_triggered = False
hass.bus.listen(EVENT_STATE_CHANGED, event_listener)
return True
def if_action(hass, config):
""" Wraps action method with state based condition. """
value_template = config.get(CONF_VALUE_TEMPLATE)
if value_template is None:
_LOGGER.error("Missing configuration key %s", CONF_VALUE_TEMPLATE)
return False
return lambda: _check_template(hass, value_template)
def _check_template(hass, value_template):
""" Checks if result of template is true """
try:
value = template.render(hass, value_template, {})
except TemplateError:
_LOGGER.exception('Error parsing template')
return False
return value.lower() == 'true'

View File

@ -4,7 +4,7 @@ homeassistant.components.automation.time
Offers time listening automation rules. Offers time listening automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation.html#time-trigger at https://home-assistant.io/components/automation/#time-trigger
""" """
import logging import logging

View File

@ -4,7 +4,7 @@ homeassistant.components.automation.zone
Offers zone automation rules. Offers zone automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation.html#zone-trigger at https://home-assistant.io/components/automation/#zone-trigger
""" """
import logging import logging
@ -46,6 +46,7 @@ def trigger(hass, config, action):
from_match = _in_zone(hass, zone_entity_id, from_s) if from_s else None from_match = _in_zone(hass, zone_entity_id, from_s) if from_s else None
to_match = _in_zone(hass, zone_entity_id, to_s) to_match = _in_zone(hass, zone_entity_id, to_s)
# pylint: disable=too-many-boolean-expressions
if event == EVENT_ENTER and not from_match and to_match or \ if event == EVENT_ENTER and not from_match and to_match or \
event == EVENT_LEAVE and from_match and not to_match: event == EVENT_LEAVE and from_match and not to_match:
action() action()

View File

@ -0,0 +1,49 @@
"""
homeassistant.components.binary_sensor
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Component to interface with binary sensors (sensors which only know two states)
that can be monitored.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/binary_sensor/
"""
import logging
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import Entity
from homeassistant.const import (STATE_ON, STATE_OFF)
DOMAIN = 'binary_sensor'
SCAN_INTERVAL = 30
ENTITY_ID_FORMAT = DOMAIN + '.{}'
def setup(hass, config):
""" Track states and offer events for binary sensors. """
component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
component.setup(config)
return True
# pylint: disable=no-self-use
class BinarySensorDevice(Entity):
""" Represents a binary sensor. """
@property
def is_on(self):
""" True if the binary sensor is on. """
return None
@property
def state(self):
""" Returns the state of the binary sensor. """
return STATE_ON if self.is_on else STATE_OFF
@property
def friendly_state(self):
""" Returns the friendly state of the binary sensor. """
return None

View File

@ -0,0 +1,107 @@
"""
homeassistant.components.binary_sensor.arest
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The arest sensor will consume an exposed aREST API of a device.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.arest/
"""
from datetime import timedelta
import logging
import requests
from homeassistant.util import Throttle
from homeassistant.components.binary_sensor import BinarySensorDevice
_LOGGER = logging.getLogger(__name__)
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
CONF_RESOURCE = 'resource'
CONF_PIN = 'pin'
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Get the aREST binary sensor. """
resource = config.get(CONF_RESOURCE)
pin = config.get(CONF_PIN)
if None in (resource, pin):
_LOGGER.error('Not all required config keys present: %s',
', '.join((CONF_RESOURCE, CONF_PIN)))
return False
try:
response = requests.get(resource, timeout=10).json()
except requests.exceptions.MissingSchema:
_LOGGER.error('Missing resource or schema in configuration. '
'Add http:// to your URL.')
return False
except requests.exceptions.ConnectionError:
_LOGGER.error('No route to device at %s. '
'Please check the IP address in the configuration file.',
resource)
return False
arest = ArestData(resource, pin)
add_devices([ArestBinarySensor(arest,
resource,
config.get('name', response['name']),
pin)])
# pylint: disable=too-many-instance-attributes, too-many-arguments
class ArestBinarySensor(BinarySensorDevice):
""" Implements an aREST binary sensor for a pin. """
def __init__(self, arest, resource, name, pin):
self.arest = arest
self._resource = resource
self._name = name
self._pin = pin
self.update()
if self._pin is not None:
request = requests.get('{}/mode/{}/i'.format
(self._resource, self._pin), timeout=10)
if request.status_code is not 200:
_LOGGER.error("Can't set mode. Is device offline?")
@property
def name(self):
""" The name of the binary sensor. """
return self._name
@property
def is_on(self):
""" True if the binary sensor is on. """
return bool(self.arest.data.get('state'))
def update(self):
""" Gets the latest data from aREST API. """
self.arest.update()
# pylint: disable=too-few-public-methods
class ArestData(object):
""" Class for handling the data retrieval for pins. """
def __init__(self, resource, pin):
self._resource = resource
self._pin = pin
self.data = {}
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
""" Gets the latest data from aREST device. """
try:
response = requests.get('{}/digital/{}'.format(
self._resource, self._pin), timeout=10)
self.data = {'state': response.json()['return_value']}
except requests.exceptions.ConnectionError:
_LOGGER.error("No route to device '%s'. Is device offline?",
self._resource)

View File

@ -0,0 +1,37 @@
"""
homeassistant.components.binary_sensor.demo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Demo platform that has two fake binary sensors.
"""
from homeassistant.components.binary_sensor import BinarySensorDevice
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the Demo binary sensors. """
add_devices([
DemoBinarySensor('Basement Floor Wet', False),
DemoBinarySensor('Movement Backyard', True),
])
class DemoBinarySensor(BinarySensorDevice):
""" A Demo binary sensor. """
def __init__(self, name, state):
self._name = name
self._state = state
@property
def should_poll(self):
""" No polling needed for a demo binary sensor. """
return False
@property
def name(self):
""" Returns the name of the binary sensor. """
return self._name
@property
def is_on(self):
""" True if the binary sensor is on. """
return self._state

View File

@ -0,0 +1,84 @@
"""
homeassistant.components.binary_sensor.mqtt
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Allows to configure a MQTT binary sensor.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.mqtt/
"""
import logging
from homeassistant.const import CONF_VALUE_TEMPLATE
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.util import template
import homeassistant.components.mqtt as mqtt
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'MQTT Binary sensor'
DEFAULT_QOS = 0
DEFAULT_PAYLOAD_ON = 'ON'
DEFAULT_PAYLOAD_OFF = 'OFF'
DEPENDENCIES = ['mqtt']
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Add MQTT binary sensor. """
if config.get('state_topic') is None:
_LOGGER.error('Missing required variable: state_topic')
return False
add_devices([MqttBinarySensor(
hass,
config.get('name', DEFAULT_NAME),
config.get('state_topic', None),
config.get('qos', DEFAULT_QOS),
config.get('payload_on', DEFAULT_PAYLOAD_ON),
config.get('payload_off', DEFAULT_PAYLOAD_OFF),
config.get(CONF_VALUE_TEMPLATE))])
# pylint: disable=too-many-arguments, too-many-instance-attributes
class MqttBinarySensor(BinarySensorDevice):
""" Represents a binary sensor that is updated by MQTT. """
def __init__(self, hass, name, state_topic, qos, payload_on, payload_off,
value_template):
self._hass = hass
self._name = name
self._state = False
self._state_topic = state_topic
self._payload_on = payload_on
self._payload_off = payload_off
self._qos = qos
def message_received(topic, payload, qos):
""" A new MQTT message has been received. """
if value_template is not None:
payload = template.render_with_possible_json_value(
hass, value_template, payload)
if payload == self._payload_on:
self._state = True
self.update_ha_state()
elif payload == self._payload_off:
self._state = False
self.update_ha_state()
mqtt.subscribe(hass, self._state_topic, message_received, self._qos)
@property
def should_poll(self):
""" No polling needed. """
return False
@property
def name(self):
""" The name of the binary sensor. """
return self._name
@property
def is_on(self):
""" True if the binary sensor is on. """
return self._state

View File

@ -0,0 +1,144 @@
"""
homeassistant.components.binary_sensor.rest
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The rest binary sensor will consume responses sent by an exposed REST API.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.rest/
"""
from datetime import timedelta
import logging
import requests
from homeassistant.const import CONF_VALUE_TEMPLATE
from homeassistant.util import template, Throttle
from homeassistant.components.binary_sensor import BinarySensorDevice
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'REST Binary Sensor'
DEFAULT_METHOD = 'GET'
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
# pylint: disable=unused-variable
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Get the REST binary sensor. """
use_get = False
use_post = False
resource = config.get('resource', None)
method = config.get('method', DEFAULT_METHOD)
payload = config.get('payload', None)
verify_ssl = config.get('verify_ssl', True)
if method == 'GET':
use_get = True
elif method == 'POST':
use_post = True
try:
if use_get:
response = requests.get(resource, timeout=10, verify=verify_ssl)
elif use_post:
response = requests.post(resource, data=payload, timeout=10,
verify=verify_ssl)
if not response.ok:
_LOGGER.error("Response status is '%s'", response.status_code)
return False
except requests.exceptions.MissingSchema:
_LOGGER.error("Missing resource or schema in configuration. "
"Add http:// or https:// to your URL")
return False
except requests.exceptions.ConnectionError:
_LOGGER.error('No route to resource/endpoint: %s', resource)
return False
if use_get:
rest = RestDataGet(resource, verify_ssl)
elif use_post:
rest = RestDataPost(resource, payload, verify_ssl)
add_devices([RestBinarySensor(hass,
rest,
config.get('name', DEFAULT_NAME),
config.get(CONF_VALUE_TEMPLATE))])
# pylint: disable=too-many-arguments
class RestBinarySensor(BinarySensorDevice):
""" Implements a REST binary sensor. """
def __init__(self, hass, rest, name, value_template):
self._hass = hass
self.rest = rest
self._name = name
self._state = False
self._value_template = value_template
self.update()
@property
def name(self):
""" The name of the binary sensor. """
return self._name
@property
def is_on(self):
""" True if the binary sensor is on. """
if self.rest.data is False:
return False
else:
if self._value_template is not None:
self.rest.data = template.render_with_possible_json_value(
self._hass, self._value_template, self.rest.data, False)
return bool(int(self.rest.data))
def update(self):
""" Gets the latest data from REST API and updates the state. """
self.rest.update()
# pylint: disable=too-few-public-methods
class RestDataGet(object):
""" Class for handling the data retrieval with GET method. """
def __init__(self, resource, verify_ssl):
self._resource = resource
self._verify_ssl = verify_ssl
self.data = False
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
""" Gets the latest data from REST service with GET method. """
try:
response = requests.get(self._resource, timeout=10,
verify=self._verify_ssl)
self.data = response.text
except requests.exceptions.ConnectionError:
_LOGGER.error("No route to resource/endpoint: %s", self._resource)
self.data = False
# pylint: disable=too-few-public-methods
class RestDataPost(object):
""" Class for handling the data retrieval with POST method. """
def __init__(self, resource, payload, verify_ssl):
self._resource = resource
self._payload = payload
self._verify_ssl = verify_ssl
self.data = False
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
""" Gets the latest data from REST service with POST method. """
try:
response = requests.post(self._resource, data=self._payload,
timeout=10, verify=self._verify_ssl)
self.data = response.text
except requests.exceptions.ConnectionError:
_LOGGER.error("No route to resource/endpoint: %s", self._resource)
self.data = False

View File

@ -1,12 +1,13 @@
""" """
homeassistant.components.browser homeassistant.components.browser
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to launch a webbrowser on the host machine. Provides functionality to launch a webbrowser on the host machine.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/browser/
""" """
DOMAIN = "browser" DOMAIN = "browser"
DEPENDENCIES = []
SERVICE_BROWSE_URL = "browse_url" SERVICE_BROWSE_URL = "browse_url"

View File

@ -4,38 +4,23 @@ homeassistant.components.camera
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Component to interface with various cameras. Component to interface with various cameras.
The following features are supported: For more details about this component, please refer to the documentation at
- Returning recorded camera images and streams https://home-assistant.io/components/camera/
- 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 logging
import time
import re import re
import time
import requests
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_PICTURE, ATTR_ENTITY_PICTURE,
HTTP_NOT_FOUND, HTTP_NOT_FOUND,
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
) )
from homeassistant.helpers.entity_component import EntityComponent
DOMAIN = 'camera' DOMAIN = 'camera'
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
@ -74,7 +59,7 @@ MJPEG_START_HEADER = 'Content-type: {0}\r\n\r\n'
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
def setup(hass, config): def setup(hass, config):
""" Track states and offer events for sensors. """ """ Track states and offer events for cameras. """
component = EntityComponent( component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL, logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
@ -96,16 +81,21 @@ def setup(hass, config):
def _proxy_camera_image(handler, path_match, data): def _proxy_camera_image(handler, path_match, data):
""" Proxies the camera image via the HA server. """ """ Proxies the camera image via the HA server. """
entity_id = path_match.group(ATTR_ENTITY_ID) entity_id = path_match.group(ATTR_ENTITY_ID)
camera = component.entities.get(entity_id)
camera = None if camera is 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) handler.send_response(HTTP_NOT_FOUND)
handler.end_headers()
return
response = camera.camera_image()
if response is None:
handler.send_response(HTTP_NOT_FOUND)
handler.end_headers()
return
handler.wfile.write(response)
hass.http.register_path( hass.http.register_path(
'GET', 'GET',
@ -114,18 +104,16 @@ def setup(hass, config):
# pylint: disable=unused-argument # pylint: disable=unused-argument
def _proxy_camera_mjpeg_stream(handler, path_match, data): def _proxy_camera_mjpeg_stream(handler, path_match, data):
""" Proxies the camera image as an mjpeg stream via the HA server. """
Proxies the camera image as an mjpeg stream via the HA server.
This function takes still images from the IP camera and turns them 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 into an MJPEG stream. This means that HA can return a live video
stream even with only a still image URL available. stream even with only a still image URL available.
""" """
entity_id = path_match.group(ATTR_ENTITY_ID) entity_id = path_match.group(ATTR_ENTITY_ID)
camera = component.entities.get(entity_id)
camera = None if camera is None:
if entity_id in component.entities.keys():
camera = component.entities[entity_id]
if not camera:
handler.send_response(HTTP_NOT_FOUND) handler.send_response(HTTP_NOT_FOUND)
handler.end_headers() handler.end_headers()
return return
@ -143,9 +131,9 @@ def setup(hass, config):
# MJPEG_START_HEADER.format() # MJPEG_START_HEADER.format()
while True: while True:
img_bytes = camera.camera_image() img_bytes = camera.camera_image()
if img_bytes is None:
continue
headers_str = '\r\n'.join(( headers_str = '\r\n'.join((
'Content-length: {}'.format(len(img_bytes)), 'Content-length: {}'.format(len(img_bytes)),
'Content-type: image/jpeg', 'Content-type: image/jpeg',
@ -159,12 +147,12 @@ def setup(hass, config):
handler.request.sendall( handler.request.sendall(
bytes('--jpgboundary\r\n', 'utf-8')) bytes('--jpgboundary\r\n', 'utf-8'))
time.sleep(0.5)
except (requests.RequestException, IOError): except (requests.RequestException, IOError):
camera.is_streaming = False camera.is_streaming = False
camera.update_ha_state() camera.update_ha_state()
camera.is_streaming = False
hass.http.register_path( hass.http.register_path(
'GET', 'GET',
re.compile( re.compile(
@ -175,7 +163,7 @@ def setup(hass, config):
class Camera(Entity): class Camera(Entity):
""" The base class for camera components """ """ The base class for camera components. """
def __init__(self): def __init__(self):
self.is_streaming = False self.is_streaming = False
@ -183,23 +171,23 @@ class Camera(Entity):
@property @property
# pylint: disable=no-self-use # pylint: disable=no-self-use
def is_recording(self): def is_recording(self):
""" Returns true if the device is recording """ """ Returns true if the device is recording. """
return False return False
@property @property
# pylint: disable=no-self-use # pylint: disable=no-self-use
def brand(self): def brand(self):
""" Should return a string of the camera brand """ """ Should return a string of the camera brand. """
return None return None
@property @property
# pylint: disable=no-self-use # pylint: disable=no-self-use
def model(self): def model(self):
""" Returns string of camera model """ """ Returns string of camera model. """
return None return None
def camera_image(self): def camera_image(self):
""" Return bytes of camera image """ """ Return bytes of camera image. """
raise NotImplementedError() raise NotImplementedError()
@property @property

View File

@ -0,0 +1,37 @@
"""
homeassistant.components.camera.demo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Demo platform that has a fake camera.
"""
import os
from homeassistant.components.camera import Camera
import homeassistant.util.dt as dt_util
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the Demo camera. """
add_devices([
DemoCamera('Demo camera')
])
class DemoCamera(Camera):
""" A Demo camera. """
def __init__(self, name):
super().__init__()
self._name = name
def camera_image(self):
""" Return a faked still image response. """
now = dt_util.utcnow()
image_path = os.path.join(os.path.dirname(__file__),
'demo_{}.jpg'.format(now.second % 4))
with open(image_path, 'rb') as file:
return file.read()
@property
def name(self):
""" Return the name of this device. """
return self._name

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -4,14 +4,14 @@ homeassistant.components.camera.foscam
This component provides basic support for Foscam IP cameras. This component provides basic support for Foscam IP cameras.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.foscam.html https://home-assistant.io/components/camera.foscam/
""" """
import logging import logging
from homeassistant.helpers import validate_config
from homeassistant.components.camera import DOMAIN
from homeassistant.components.camera import Camera
import requests import requests
import re
from homeassistant.helpers import validate_config
from homeassistant.components.camera import DOMAIN, Camera
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -40,7 +40,7 @@ class FoscamCamera(Camera):
self._username = device_info.get('username') self._username = device_info.get('username')
self._password = device_info.get('password') self._password = device_info.get('password')
self._snap_picture_url = self._base_url \ self._snap_picture_url = self._base_url \
+ 'cgi-bin/CGIProxy.fcgi?cmd=snapPicture&usr=' \ + 'cgi-bin/CGIProxy.fcgi?cmd=snapPicture2&usr=' \
+ self._username + '&pwd=' + self._password + self._username + '&pwd=' + self._password
self._name = device_info.get('name', 'Foscam Camera') self._name = device_info.get('name', 'Foscam Camera')
@ -50,17 +50,9 @@ class FoscamCamera(Camera):
def camera_image(self): def camera_image(self):
""" Return a still image reponse from the camera. """ """ Return a still image reponse from the camera. """
# send the request to snap a picture # Send the request to snap a picture and return raw jpg data
response = requests.get(self._snap_picture_url) response = requests.get(self._snap_picture_url)
# parse the response to find the image file name
pattern = re.compile('src="[.][.]/(.*[.]jpg)"')
filename = pattern.search(response.content.decode("utf-8")).group(1)
# send request for the image
response = requests.get(self._base_url + filename)
return response.content return response.content
@property @property

View File

@ -4,14 +4,15 @@ homeassistant.components.camera.generic
Support for IP Cameras. Support for IP Cameras.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.generic.html https://home-assistant.io/components/camera.generic/
""" """
import logging 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 import requests
from requests.auth import HTTPBasicAuth
from homeassistant.helpers import validate_config
from homeassistant.components.camera import DOMAIN, Camera
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -40,13 +41,21 @@ class GenericCamera(Camera):
self._still_image_url = device_info['still_image_url'] self._still_image_url = device_info['still_image_url']
def camera_image(self): def camera_image(self):
""" Return a still image reponse from the camera. """ """ Return a still image response from the camera. """
if self._username and self._password: if self._username and self._password:
try:
response = requests.get( response = requests.get(
self._still_image_url, self._still_image_url,
auth=HTTPBasicAuth(self._username, self._password)) auth=HTTPBasicAuth(self._username, self._password))
except requests.exceptions.RequestException as error:
_LOGGER.error('Error getting camera image: %s', error)
return None
else: else:
try:
response = requests.get(self._still_image_url) response = requests.get(self._still_image_url)
except requests.exceptions.RequestException as error:
_LOGGER.error('Error getting camera image: %s', error)
return None
return response.content return response.content

View File

@ -0,0 +1,72 @@
"""
homeassistant.components.camera.mjpeg
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for IP Cameras.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.mjpeg/
"""
from contextlib import closing
import logging
import requests
from requests.auth import HTTPBasicAuth
from homeassistant.helpers import validate_config
from homeassistant.components.camera import DOMAIN, Camera
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Adds a mjpeg IP Camera. """
if not validate_config({DOMAIN: config}, {DOMAIN: ['mjpeg_url']},
_LOGGER):
return None
add_devices_callback([MjpegCamera(config)])
# pylint: disable=too-many-instance-attributes
class MjpegCamera(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', 'Mjpeg Camera')
self._username = device_info.get('username')
self._password = device_info.get('password')
self._mjpeg_url = device_info['mjpeg_url']
def camera_image(self):
""" Return a still image response from the camera. """
def process_response(response):
""" Take in a response object, return the jpg from it. """
data = b''
for chunk in response.iter_content(1024):
data += chunk
jpg_start = data.find(b'\xff\xd8')
jpg_end = data.find(b'\xff\xd9')
if jpg_start != -1 and jpg_end != -1:
jpg = data[jpg_start:jpg_end + 2]
return jpg
if self._username and self._password:
with closing(requests.get(self._mjpeg_url,
auth=HTTPBasicAuth(self._username,
self._password),
stream=True)) as response:
return process_response(response)
else:
with closing(requests.get(self._mjpeg_url,
stream=True)) as response:
return process_response(response)
@property
def name(self):
""" Return the name of this device. """
return self._name

View File

@ -15,7 +15,6 @@ from homeassistant.helpers import generate_entity_id
from homeassistant.const import EVENT_TIME_CHANGED from homeassistant.const import EVENT_TIME_CHANGED
DOMAIN = "configurator" DOMAIN = "configurator"
DEPENDENCIES = []
ENTITY_ID_FORMAT = DOMAIN + ".{}" ENTITY_ID_FORMAT = DOMAIN + ".{}"
SERVICE_CONFIGURE = "configure" SERVICE_CONFIGURE = "configure"

View File

@ -1,19 +1,20 @@
""" """
homeassistant.components.conversation homeassistant.components.conversation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to have conversations with Home Assistant. Provides functionality to have conversations with Home Assistant.
This is more a proof of concept.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/conversation/
""" """
import logging import logging
import re import re
from homeassistant import core from homeassistant import core
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF) ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
DOMAIN = "conversation" DOMAIN = "conversation"
DEPENDENCIES = []
SERVICE_PROCESS = "process" SERVICE_PROCESS = "process"
@ -21,9 +22,13 @@ ATTR_TEXT = "text"
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)') REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
REQUIREMENTS = ['fuzzywuzzy==0.8.0']
def setup(hass, config): def setup(hass, config):
""" Registers the process service. """ """ Registers the process service. """
from fuzzywuzzy import process as fuzzyExtract
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def process(service): def process(service):
@ -42,9 +47,11 @@ def setup(hass, config):
name, command = match.groups() name, command = match.groups()
entity_ids = [ entities = {state.entity_id: state.name for state in hass.states.all()}
state.entity_id for state in hass.states.all()
if state.name.lower() == name] entity_ids = fuzzyExtract.extractOne(name,
entities,
score_cutoff=65)[2]
if not entity_ids: if not entity_ids:
logger.error( logger.error(

View File

@ -10,14 +10,26 @@ import homeassistant.core as ha
import homeassistant.bootstrap as bootstrap import homeassistant.bootstrap as bootstrap
import homeassistant.loader as loader import homeassistant.loader as loader
from homeassistant.const import ( from homeassistant.const import (
CONF_PLATFORM, ATTR_ENTITY_PICTURE, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME) CONF_PLATFORM, ATTR_ENTITY_ID)
DOMAIN = "demo" DOMAIN = "demo"
DEPENDENCIES = ['introduction', 'conversation'] DEPENDENCIES = ['conversation', 'introduction', 'zone']
COMPONENTS_WITH_DEMO_PLATFORM = [ COMPONENTS_WITH_DEMO_PLATFORM = [
'switch', 'light', 'sensor', 'thermostat', 'media_player', 'notify'] 'alarm_control_panel',
'binary_sensor',
'camera',
'device_tracker',
'light',
'lock',
'media_player',
'notify',
'rollershutter',
'sensor',
'switch',
'thermostat',
]
def setup(hass, config): def setup(hass, config):
@ -41,9 +53,10 @@ def setup(hass, config):
bootstrap.setup_component(hass, 'sun') bootstrap.setup_component(hass, 'sun')
# Setup demo platforms # Setup demo platforms
demo_config = config.copy()
for component in COMPONENTS_WITH_DEMO_PLATFORM: for component in COMPONENTS_WITH_DEMO_PLATFORM:
bootstrap.setup_component( demo_config[component] = {CONF_PLATFORM: 'demo'}
hass, component, {component: {CONF_PLATFORM: 'demo'}}) bootstrap.setup_component(hass, component, demo_config)
# Setup room groups # Setup room groups
lights = sorted(hass.states.entity_ids('light')) lights = sorted(hass.states.entity_ids('light'))
@ -54,23 +67,6 @@ def setup(hass, config):
group.setup_group(hass, 'bedroom', [lights[0], switches[1], group.setup_group(hass, 'bedroom', [lights[0], switches[1],
media_players[0]]) media_players[0]])
# Setup IP Camera
bootstrap.setup_component(
hass, 'camera',
{'camera': {
'platform': 'generic',
'name': 'IP Camera',
'still_image_url': 'http://home-assistant.io/demo/webcam.jpg',
}})
# Setup alarm_control_panel
bootstrap.setup_component(
hass, 'alarm_control_panel',
{'alarm_control_panel': {
'platform': 'manual',
'name': 'Test Alarm',
}})
# Setup scripts # Setup scripts
bootstrap.setup_component( bootstrap.setup_component(
hass, 'script', hass, 'script',
@ -110,25 +106,6 @@ def setup(hass, config):
}}, }},
]}) ]})
# Setup fake device tracker
hass.states.set("device_tracker.paulus", "home",
{ATTR_ENTITY_PICTURE:
"http://graph.facebook.com/297400035/picture",
ATTR_FRIENDLY_NAME: 'Paulus'})
hass.states.set("device_tracker.anne_therese", "not_home",
{ATTR_FRIENDLY_NAME: 'Anne Therese',
'latitude': hass.config.latitude + 0.002,
'longitude': hass.config.longitude + 0.002})
hass.states.set("group.all_devices", "home",
{
"auto": True,
ATTR_ENTITY_ID: [
"device_tracker.paulus",
"device_tracker.anne_therese"
]
})
# Setup configurator # Setup configurator
configurator_ids = [] configurator_ids = []

View File

@ -1,9 +1,11 @@
""" """
homeassistant.components.device_sun_light_trigger homeassistant.components.device_sun_light_trigger
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to turn on lights based on the state of the sun and
devices.
Provides functionality to turn on lights based on For more details about this component, please refer to the documentation at
the state of the sun and devices. https://home-assistant.io/components/device_sun_light_trigger/
""" """
import logging import logging
from datetime import timedelta from datetime import timedelta

View File

@ -1,25 +1,10 @@
""" """
homeassistant.components.device_tracker homeassistant.components.device_tracker
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to keep track of devices. Provides functionality to keep track of devices.
device_tracker: For more details about this component, please refer to the documentation at
platform: netgear https://home-assistant.io/components/device_tracker/
# Optional
# How many seconds to wait after not seeing device to consider it not home
consider_home: 180
# Seconds between each scan
interval_seconds: 12
# New found devices auto found
track_new_devices: yes
# Maximum distance from home we consider people home
range_home: 100
""" """
# pylint: disable=too-many-instance-attributes, too-many-arguments # pylint: disable=too-many-instance-attributes, too-many-arguments
# pylint: disable=too-many-locals # pylint: disable=too-many-locals

View File

@ -5,7 +5,7 @@ Device tracker platform that supports scanning an Actiontec MI424WR
(Verizon FIOS) router for device presence. (Verizon FIOS) router for device presence.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.actiontec.html https://home-assistant.io/components/device_tracker.actiontec/
""" """
import logging import logging
from datetime import timedelta from datetime import timedelta
@ -17,20 +17,19 @@ import telnetlib
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers import validate_config from homeassistant.helpers import validate_config
from homeassistant.util import Throttle, convert from homeassistant.util import Throttle
from homeassistant.components.device_tracker import DOMAIN from homeassistant.components.device_tracker import DOMAIN
# Return cached results if last scan was less then this time ago # Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) 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__) _LOGGER = logging.getLogger(__name__)
_LEASES_REGEX = re.compile( _LEASES_REGEX = re.compile(
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})' + 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}))') r'\smac:\s(?P<mac>([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))' +
r'\svalid\sfor:\s(?P<timevalid>(-?\d+))' +
r'\ssec')
# pylint: disable=unused-argument # pylint: disable=unused-argument
@ -40,9 +39,7 @@ def get_scanner(hass, config):
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
_LOGGER): _LOGGER):
return None return None
scanner = ActiontecDeviceScanner(config[DOMAIN]) scanner = ActiontecDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
Device = namedtuple("Device", ["mac", "ip", "last_update"]) Device = namedtuple("Device", ["mac", "ip", "last_update"])
@ -58,19 +55,11 @@ class ActiontecDeviceScanner(object):
self.host = config[CONF_HOST] self.host = config[CONF_HOST]
self.username = config[CONF_USERNAME] self.username = config[CONF_USERNAME]
self.password = config[CONF_PASSWORD] 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.lock = threading.Lock()
self.last_results = [] self.last_results = []
# Test the router is accessible
data = self.get_actiontec_data() data = self.get_actiontec_data()
self.success_init = data is not None self.success_init = data is not None
_LOGGER.info("actiontec scanner initialized") _LOGGER.info("actiontec scanner initialized")
if self.home_interval:
_LOGGER.info("home_interval set to: %s", self.home_interval)
def scan_devices(self): def scan_devices(self):
""" """
@ -100,27 +89,13 @@ class ActiontecDeviceScanner(object):
return False return False
with self.lock: with self.lock:
exclude_targets = set()
exclude_target_list = []
now = dt_util.now() 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() actiontec_data = self.get_actiontec_data()
if not actiontec_data: if not actiontec_data:
return False return False
self.last_results = [] self.last_results = [Device(data['mac'], name, now)
for client in exclude_target_list: for name, data in actiontec_data.items()
if client in actiontec_data: if data['timevalid'] > -60]
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") _LOGGER.info("actiontec scan successful")
return True return True
@ -153,6 +128,7 @@ class ActiontecDeviceScanner(object):
if match is not None: if match is not None:
devices[match.group('ip')] = { devices[match.group('ip')] = {
'ip': match.group('ip'), 'ip': match.group('ip'),
'mac': match.group('mac').upper() 'mac': match.group('mac').upper(),
'timevalid': int(match.group('timevalid'))
} }
return devices return devices

View File

@ -5,7 +5,7 @@ Device tracker platform that supports scanning a Aruba Access Point for device
presence. presence.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.aruba.html https://home-assistant.io/components/device_tracker.aruba/
""" """
import logging import logging
from datetime import timedelta from datetime import timedelta

View File

@ -4,32 +4,8 @@ homeassistant.components.device_tracker.asuswrt
Device tracker platform that supports scanning a ASUSWRT router for device Device tracker platform that supports scanning a ASUSWRT router for device
presence. presence.
This device tracker needs telnet to be enabled on the router. For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.asuswrt/
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 import logging
from datetime import timedelta from datetime import timedelta
@ -158,10 +134,13 @@ class AsusWrtDeviceScanner(object):
for lease in leases_result: for lease in leases_result:
match = _LEASES_REGEX.search(lease.decode('utf-8')) match = _LEASES_REGEX.search(lease.decode('utf-8'))
if not match:
_LOGGER.warning("Could not parse lease row: %s", lease)
continue
# For leases where the client doesn't set a hostname, ensure # For leases where the client doesn't set a hostname, ensure
# it is blank and not '*', which breaks the entity_id down # it is blank and not '*', which breaks the entity_id down
# the line # the line
if match:
host = match.group('host') host = match.group('host')
if host == '*': if host == '*':
host = '' host = ''
@ -175,6 +154,9 @@ class AsusWrtDeviceScanner(object):
for neighbor in neighbors: for neighbor in neighbors:
match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8')) match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8'))
if match and match.group('ip') in devices: if not match:
_LOGGER.warning("Could not parse neighbor row: %s", neighbor)
continue
if match.group('ip') in devices:
devices[match.group('ip')]['status'] = match.group('status') devices[match.group('ip')]['status'] = match.group('status')
return devices return devices

View File

@ -5,7 +5,7 @@ Device tracker platform that supports scanning a DD-WRT router for device
presence. presence.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.ddwrt.html https://home-assistant.io/components/device_tracker.ddwrt/
""" """
import logging import logging
from datetime import timedelta from datetime import timedelta

View File

@ -1,7 +1,6 @@
""" """
homeassistant.components.device_tracker.demo homeassistant.components.device_tracker.demo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Demo platform for the device tracker. Demo platform for the device tracker.
device_tracker: device_tracker:

View File

@ -0,0 +1,122 @@
"""
homeassistant.components.device_tracker.fritz
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning a FRITZ!Box router for device
presence.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.fritz/
"""
import logging
from datetime import timedelta
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__)
# noinspection PyUnusedLocal
def get_scanner(hass, config):
""" Validates config and returns FritzBoxScanner. """
if not validate_config(config,
{DOMAIN: []},
_LOGGER):
return None
scanner = FritzBoxScanner(config[DOMAIN])
return scanner if scanner.success_init else None
# pylint: disable=too-many-instance-attributes
class FritzBoxScanner(object):
"""
This class queries a FRITZ!Box router. It is using the
fritzconnection library for communication with the router.
The API description can be found under:
https://pypi.python.org/pypi/fritzconnection/0.4.6
This scanner retrieves the list of known hosts and checks their
corresponding states (on, or off).
Due to a bug of the fritzbox api (router side) it is not possible
to track more than 16 hosts.
"""
def __init__(self, config):
self.last_results = []
self.host = '169.254.1.1' # This IP is valid for all fritzboxes
self.username = 'admin'
self.password = ''
self.success_init = True
# Try to import the fritzconnection library
try:
# noinspection PyPackageRequirements,PyUnresolvedReferences
import fritzconnection as fc
except ImportError:
_LOGGER.exception("""Failed to import Python library
fritzconnection. Please run
<home-assistant>/setup to install it.""")
self.success_init = False
return
# Check for user specific configuration
if CONF_HOST in config.keys():
self.host = config[CONF_HOST]
if CONF_USERNAME in config.keys():
self.username = config[CONF_USERNAME]
if CONF_PASSWORD in config.keys():
self.password = config[CONF_PASSWORD]
# Establish a connection to the FRITZ!Box
try:
self.fritz_box = fc.FritzHosts(address=self.host,
user=self.username,
password=self.password)
except (ValueError, TypeError):
self.fritz_box = None
# At this point it is difficult to tell if a connection is established.
# So just check for null objects ...
if self.fritz_box is None or not self.fritz_box.modelname:
self.success_init = False
if self.success_init:
_LOGGER.info("Successfully connected to %s",
self.fritz_box.modelname)
self._update_info()
else:
_LOGGER.error("Failed to establish connection to FRITZ!Box "
"with IP: %s", self.host)
def scan_devices(self):
""" Scan for new devices and return a list of found device ids. """
self._update_info()
active_hosts = []
for known_host in self.last_results:
if known_host["status"] == "1":
active_hosts.append(known_host["mac"])
return active_hosts
def get_device_name(self, mac):
""" Returns the name of the given device or None if is not known. """
ret = self.fritz_box.get_specific_host_entry(mac)["NewHostName"]
if ret == {}:
return None
return ret
@Throttle(MIN_TIME_BETWEEN_SCANS)
def _update_info(self):
""" Retrieves latest information from the FRITZ!Box. """
if not self.success_init:
return False
_LOGGER.info("Scanning")
self.last_results = self.fritz_box.get_hosts_info()
return True

View File

@ -1,71 +0,0 @@
"""
homeassistant.components.device_tracker.geofancy
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Geofancy platform for the device tracker.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.geofancy.html
"""
from homeassistant.const import (
HTTP_UNPROCESSABLE_ENTITY, HTTP_INTERNAL_SERVER_ERROR)
DEPENDENCIES = ['http']
_SEE = 0
URL_API_GEOFANCY_ENDPOINT = "/api/geofancy"
def setup_scanner(hass, config, see):
""" Set up an endpoint for the Geofancy app. """
# Use a global variable to keep setup_scanner compact when using a callback
global _SEE
_SEE = see
# POST would be semantically better, but that currently does not work
# since Geofancy sends the data as key1=value1&key2=value2
# in the request body, while Home Assistant expects json there.
hass.http.register_path(
'GET', URL_API_GEOFANCY_ENDPOINT, _handle_get_api_geofancy)
return True
def _handle_get_api_geofancy(handler, path_match, data):
""" Geofancy message received. """
if not isinstance(data, dict):
handler.write_json_message(
"Error while parsing Geofancy message.",
HTTP_INTERNAL_SERVER_ERROR)
return
if 'latitude' not in data or 'longitude' not in data:
handler.write_json_message(
"Location not specified.",
HTTP_UNPROCESSABLE_ENTITY)
return
if 'device' not in data or 'id' not in data:
handler.write_json_message(
"Device id or location id not specified.",
HTTP_UNPROCESSABLE_ENTITY)
return
try:
gps_coords = (float(data['latitude']), float(data['longitude']))
except ValueError:
# If invalid latitude / longitude format
handler.write_json_message(
"Invalid latitude / longitude format.",
HTTP_UNPROCESSABLE_ENTITY)
return
# entity id's in Home Assistant must be alphanumerical
device_uuid = data['device']
device_entity_id = device_uuid.replace('-', '')
_SEE(dev_id=device_entity_id, gps=gps_coords, location_name=data['id'])
handler.write_json_message("Geofancy message processed")

View File

@ -0,0 +1,87 @@
"""
homeassistant.components.device_tracker.icloud
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning iCloud devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.icloud/
"""
import logging
import re
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers.event import track_utc_time_change
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pyicloud==0.7.2']
CONF_INTERVAL = 'interval'
DEFAULT_INTERVAL = 8
def setup_scanner(hass, config, see):
""" Set up the iCloud Scanner. """
from pyicloud import PyiCloudService
from pyicloud.exceptions import PyiCloudFailedLoginException
from pyicloud.exceptions import PyiCloudNoDevicesException
# Get the username and password from the configuration
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
if username is None or password is None:
_LOGGER.error('Must specify a username and password')
return False
try:
_LOGGER.info('Logging into iCloud Account')
# Attempt the login to iCloud
api = PyiCloudService(username,
password,
verify=True)
except PyiCloudFailedLoginException as error:
_LOGGER.exception('Error logging into iCloud Service: %s', error)
return False
def keep_alive(now):
""" Keeps authenticating iCloud connection. """
api.authenticate()
_LOGGER.info("Authenticate against iCloud")
track_utc_time_change(hass, keep_alive, second=0)
def update_icloud(now):
""" Authenticate against iCloud and scan for devices. """
try:
# The session timeouts if we are not using it so we
# have to re-authenticate. This will send an email.
api.authenticate()
# Loop through every device registered with the iCloud account
for device in api.devices:
status = device.status()
location = device.location()
# If the device has a location add it. If not do nothing
if location:
see(
dev_id=re.sub(r"(\s|\W|')",
'',
status['name']),
host_name=status['name'],
gps=(location['latitude'], location['longitude']),
battery=status['batteryLevel']*100,
gps_accuracy=location['horizontalAccuracy']
)
else:
# No location found for the device so continue
continue
except PyiCloudNoDevicesException:
_LOGGER.info('No iCloud Devices found!')
track_utc_time_change(
hass, update_icloud,
minute=range(0, 60, config.get(CONF_INTERVAL, DEFAULT_INTERVAL)),
second=0
)
return True

View File

@ -0,0 +1,105 @@
"""
homeassistant.components.device_tracker.locative
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Locative platform for the device tracker.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.locative/
"""
import logging
from functools import partial
from homeassistant.const import (
HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME)
from homeassistant.components.device_tracker import DOMAIN
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http']
URL_API_LOCATIVE_ENDPOINT = "/api/locative"
def setup_scanner(hass, config, see):
""" Set up an endpoint for the Locative app. """
# POST would be semantically better, but that currently does not work
# since Locative sends the data as key1=value1&key2=value2
# in the request body, while Home Assistant expects json there.
hass.http.register_path(
'GET', URL_API_LOCATIVE_ENDPOINT,
partial(_handle_get_api_locative, hass, see))
return True
def _handle_get_api_locative(hass, see, handler, path_match, data):
""" Locative message received. """
if not _check_data(handler, data):
return
device = data['device'].replace('-', '')
location_name = data['id'].lower()
direction = data['trigger']
if direction == 'enter':
see(dev_id=device, location_name=location_name)
handler.write_text("Setting location to {}".format(location_name))
elif direction == 'exit':
current_state = hass.states.get(
"{}.{}".format(DOMAIN, device)).state
if current_state == location_name:
see(dev_id=device, location_name=STATE_NOT_HOME)
handler.write_text("Setting location to not home")
else:
# Ignore the message if it is telling us to exit a zone that we
# aren't currently in. This occurs when a zone is entered before
# the previous zone was exited. The enter message will be sent
# first, then the exit message will be sent second.
handler.write_text(
'Ignoring exit from {} (already in {})'.format(
location_name, current_state))
elif direction == 'test':
# In the app, a test message can be sent. Just return something to
# the user to let them know that it works.
handler.write_text("Received test message.")
else:
handler.write_text(
"Received unidentified message: {}".format(direction),
HTTP_UNPROCESSABLE_ENTITY)
_LOGGER.error("Received unidentified message from Locative: %s",
direction)
def _check_data(handler, data):
if 'latitude' not in data or 'longitude' not in data:
handler.write_text("Latitude and longitude not specified.",
HTTP_UNPROCESSABLE_ENTITY)
_LOGGER.error("Latitude and longitude not specified.")
return False
if 'device' not in data:
handler.write_text("Device id not specified.",
HTTP_UNPROCESSABLE_ENTITY)
_LOGGER.error("Device id not specified.")
return False
if 'id' not in data:
handler.write_text("Location id not specified.",
HTTP_UNPROCESSABLE_ENTITY)
_LOGGER.error("Location id not specified.")
return False
if 'trigger' not in data:
handler.write_text("Trigger is not specified.",
HTTP_UNPROCESSABLE_ENTITY)
_LOGGER.error("Trigger is not specified.")
return False
return True

View File

@ -5,7 +5,7 @@ Device tracker platform that supports scanning a OpenWRT router for device
presence. presence.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.luci.html https://home-assistant.io/components/device_tracker.luci/
""" """
import logging import logging
import json import json

View File

@ -4,7 +4,7 @@ homeassistant.components.device_tracker.mqtt
MQTT platform for the device tracker. MQTT platform for the device tracker.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.mqtt.html https://home-assistant.io/components/device_tracker.mqtt/
""" """
import logging import logging
from homeassistant import util from homeassistant import util

View File

@ -5,7 +5,7 @@ Device tracker platform that supports scanning a Netgear router for device
presence. presence.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.netgear.html https://home-assistant.io/components/device_tracker.netgear/
""" """
import logging import logging
from datetime import timedelta from datetime import timedelta

View File

@ -4,7 +4,7 @@ homeassistant.components.device_tracker.nmap
Device tracker platform that supports scanning a network with nmap. Device tracker platform that supports scanning a network with nmap.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.nmap_scanner.html https://home-assistant.io/components/device_tracker.nmap_scanner/
""" """
import logging import logging
from datetime import timedelta from datetime import timedelta
@ -98,7 +98,7 @@ class NmapDeviceScanner(object):
from nmap import PortScanner, PortScannerError from nmap import PortScanner, PortScannerError
scanner = PortScanner() scanner = PortScanner()
options = "-F --host-timeout 5" options = "-F --host-timeout 5s"
if self.home_interval: if self.home_interval:
boundary = dt_util.now() - self.home_interval boundary = dt_util.now() - self.home_interval

View File

@ -4,7 +4,7 @@ homeassistant.components.device_tracker.owntracks
OwnTracks platform for the device tracker. OwnTracks platform for the device tracker.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.owntracks.html https://home-assistant.io/components/device_tracker.owntracks/
""" """
import json import json
import logging import logging

View File

@ -5,7 +5,7 @@ Device tracker platform that supports fetching WiFi associations
through SNMP. through SNMP.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.snmp.html https://home-assistant.io/components/device_tracker.snmp/
""" """
import logging import logging
from datetime import timedelta from datetime import timedelta
@ -45,9 +45,12 @@ class SnmpScanner(object):
This class queries any SNMP capable Acces Point for connected devices. This class queries any SNMP capable Acces Point for connected devices.
""" """
def __init__(self, config): def __init__(self, config):
self.host = config[CONF_HOST] from pysnmp.entity.rfc3413.oneliner import cmdgen
self.community = config[CONF_COMMUNITY] self.snmp = cmdgen.CommandGenerator()
self.baseoid = config[CONF_BASEOID]
self.host = cmdgen.UdpTransportTarget((config[CONF_HOST], 161))
self.community = cmdgen.CommunityData(config[CONF_COMMUNITY])
self.baseoid = cmdgen.MibVariable(config[CONF_BASEOID])
self.lock = threading.Lock() self.lock = threading.Lock()
@ -91,16 +94,11 @@ class SnmpScanner(object):
def get_snmp_data(self): def get_snmp_data(self):
""" Fetch mac addresses from WAP via SNMP. """ """ Fetch mac addresses from WAP via SNMP. """
from pysnmp.entity.rfc3413.oneliner import cmdgen
devices = [] devices = []
snmp = cmdgen.CommandGenerator() errindication, errstatus, errindex, restable = self.snmp.nextCmd(
errindication, errstatus, errindex, restable = snmp.nextCmd( self.community, self.host, self.baseoid)
cmdgen.CommunityData(self.community),
cmdgen.UdpTransportTarget((self.host, 161)),
cmdgen.MibVariable(self.baseoid)
)
if errindication: if errindication:
_LOGGER.error("SNMPLIB error: %s", errindication) _LOGGER.error("SNMPLIB error: %s", errindication)

View File

@ -5,7 +5,7 @@ Device tracker platform that supports scanning a THOMSON router for device
presence. presence.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.thomson.html https://home-assistant.io/components/device_tracker.thomson/
""" """
import logging import logging
from datetime import timedelta from datetime import timedelta

View File

@ -5,7 +5,7 @@ Device tracker platform that supports scanning a Tomato router for device
presence. presence.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.tomato.html https://home-assistant.io/components/device_tracker.tomato/
""" """
import logging import logging
import json import json

View File

@ -5,7 +5,7 @@ Device tracker platform that supports scanning a TP-Link router for device
presence. presence.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.tplink.html https://home-assistant.io/components/device_tracker.tplink/
""" """
import base64 import base64
import logging import logging

View File

@ -0,0 +1,173 @@
"""
homeassistant.components.device_tracker.ubus
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Device tracker platform that supports scanning a OpenWRT router for device
presence.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.ubus/
"""
import logging
import json
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 Luci scanner. """
if not validate_config(config,
{DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]},
_LOGGER):
return None
scanner = UbusDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None
# pylint: disable=too-many-instance-attributes
class UbusDeviceScanner(object):
"""
This class queries a wireless router running OpenWrt firmware
for connected devices. Adapted from Tomato scanner.
Configure your routers' ubus ACL based on following instructions:
http://wiki.openwrt.org/doc/techref/ubus
Read only access will be fine.
To use this class you have to install rpcd-mod-file package
in your OpenWrt router:
opkg install rpcd-mod-file
"""
def __init__(self, config):
host = config[CONF_HOST]
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
self.parse_api_pattern = re.compile(r"(?P<param>\w*) = (?P<value>.*);")
self.lock = threading.Lock()
self.last_results = {}
self.url = 'http://{}/ubus'.format(host)
self.session_id = _get_session_id(self.url, username, password)
self.hostapd = []
self.leasefile = None
self.mac2name = None
self.success_init = self.session_id is not None
def scan_devices(self):
"""
Scans for new devices and return a list containing found device ids.
"""
self._update_info()
return self.last_results
def get_device_name(self, device):
""" Returns the name of the given device or None if we don't know. """
with self.lock:
if self.leasefile is None:
result = _req_json_rpc(self.url, self.session_id,
'call', 'uci', 'get',
config="dhcp", type="dnsmasq")
if result:
values = result["values"].values()
self.leasefile = next(iter(values))["leasefile"]
else:
return
if self.mac2name is None:
result = _req_json_rpc(self.url, self.session_id,
'call', 'file', 'read',
path=self.leasefile)
if result:
self.mac2name = dict()
for line in result["data"].splitlines():
hosts = line.split(" ")
self.mac2name[hosts[1].upper()] = hosts[3]
else:
# Error, handled in the _req_json_rpc
return
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.
"""
if not self.success_init:
return False
with self.lock:
_LOGGER.info("Checking ARP")
if not self.hostapd:
hostapd = _req_json_rpc(self.url, self.session_id,
'list', 'hostapd.*', '')
self.hostapd.extend(hostapd.keys())
self.last_results = []
results = 0
for hostapd in self.hostapd:
result = _req_json_rpc(self.url, self.session_id,
'call', hostapd, 'get_clients')
if result:
results = results + 1
self.last_results.extend(result['clients'].keys())
return bool(results)
def _req_json_rpc(url, session_id, rpcmethod, subsystem, method, **params):
""" Perform one JSON RPC operation. """
data = json.dumps({"jsonrpc": "2.0",
"id": 1,
"method": rpcmethod,
"params": [session_id,
subsystem,
method,
params]})
try:
res = requests.post(url, data=data, timeout=5)
except requests.exceptions.Timeout:
return
if res.status_code == 200:
response = res.json()
if rpcmethod == "call":
return response["result"][1]
else:
return response["result"]
def _get_session_id(url, username, password):
""" Get authentication token for the given host+username+password. """
res = _req_json_rpc(url, "00000000000000000000000000000000", 'call',
'session', 'login', username=username,
password=password)
return res["ubus_rpc_session"]

View File

@ -1,7 +1,6 @@
""" """
homeassistant.components.discovery homeassistant.components.discovery
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Starts a service to scan in intervals for new devices. Starts a service to scan in intervals for new devices.
Will emit EVENT_PLATFORM_DISCOVERED whenever a new service has been discovered. Will emit EVENT_PLATFORM_DISCOVERED whenever a new service has been discovered.
@ -18,8 +17,7 @@ from homeassistant.const import (
ATTR_SERVICE, ATTR_DISCOVERED) ATTR_SERVICE, ATTR_DISCOVERED)
DOMAIN = "discovery" DOMAIN = "discovery"
DEPENDENCIES = [] REQUIREMENTS = ['netdisco==0.5.2']
REQUIREMENTS = ['netdisco==0.4.2']
SCAN_INTERVAL = 300 # seconds SCAN_INTERVAL = 300 # seconds
@ -28,6 +26,7 @@ SERVICE_HUE = 'philips_hue'
SERVICE_CAST = 'google_cast' SERVICE_CAST = 'google_cast'
SERVICE_NETGEAR = 'netgear_router' SERVICE_NETGEAR = 'netgear_router'
SERVICE_SONOS = 'sonos' SERVICE_SONOS = 'sonos'
SERVICE_PLEX = 'plex_mediaserver'
SERVICE_HANDLERS = { SERVICE_HANDLERS = {
SERVICE_WEMO: "switch", SERVICE_WEMO: "switch",
@ -35,6 +34,7 @@ SERVICE_HANDLERS = {
SERVICE_HUE: "light", SERVICE_HUE: "light",
SERVICE_NETGEAR: 'device_tracker', SERVICE_NETGEAR: 'device_tracker',
SERVICE_SONOS: 'media_player', SERVICE_SONOS: 'media_player',
SERVICE_PLEX: 'media_player',
} }
@ -88,6 +88,7 @@ def setup(hass, config):
ATTR_DISCOVERED: info ATTR_DISCOVERED: info
}) })
# pylint: disable=unused-argument
def start_discovery(event): def start_discovery(event):
""" Start discovering. """ """ Start discovering. """
netdisco = DiscoveryService(SCAN_INTERVAL) netdisco = DiscoveryService(SCAN_INTERVAL)

View File

@ -1,8 +1,10 @@
""" """
homeassistant.components.downloader homeassistant.components.downloader
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to download files. Provides functionality to download files.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/downloader/
""" """
import os import os
import logging import logging
@ -13,7 +15,6 @@ from homeassistant.helpers import validate_config
from homeassistant.util import sanitize_filename from homeassistant.util import sanitize_filename
DOMAIN = "downloader" DOMAIN = "downloader"
DEPENDENCIES = []
SERVICE_DOWNLOAD_FILE = "download_file" SERVICE_DOWNLOAD_FILE = "download_file"
@ -42,6 +43,10 @@ def setup(hass, config):
download_path = config[DOMAIN][CONF_DOWNLOAD_DIR] download_path = config[DOMAIN][CONF_DOWNLOAD_DIR]
# If path is relative, we assume relative to HASS config dir
if not os.path.isabs(download_path):
download_path = hass.config.path(download_path)
if not os.path.isdir(download_path): if not os.path.isdir(download_path):
logger.error( logger.error(

View File

@ -0,0 +1,155 @@
"""
homeassistant.components.ecobee
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Ecobee Component
This component adds support for Ecobee3 Wireless Thermostats.
You will need to setup developer access to your thermostat,
and create and API key on the ecobee website.
The first time you run this component you will see a configuration
component card in Home Assistant. This card will contain a PIN code
that you will need to use to authorize access to your thermostat. You
can do this at https://www.ecobee.com/consumerportal/index.html
Click My Apps, Add application, Enter Pin and click Authorize.
After authorizing the application click the button in the configuration
card. Now your thermostat and sensors should shown in home-assistant.
You can use the optional hold_temp parameter to set whether or not holds
are set indefintely or until the next scheduled event.
ecobee:
api_key: asdfasdfasdfasdfasdfaasdfasdfasdfasdf
hold_temp: True
"""
from datetime import timedelta
import logging
import os
from homeassistant.loader import get_component
from homeassistant import bootstrap
from homeassistant.util import Throttle
from homeassistant.const import (
EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED, CONF_API_KEY)
DOMAIN = "ecobee"
DISCOVER_THERMOSTAT = "ecobee.thermostat"
DISCOVER_SENSORS = "ecobee.sensor"
NETWORK = None
HOLD_TEMP = 'hold_temp'
REQUIREMENTS = [
'https://github.com/nkgilley/python-ecobee-api/archive/'
'92a2f330cbaf601d0618456fdd97e5a8c42c1c47.zip#python-ecobee==0.0.4']
_LOGGER = logging.getLogger(__name__)
ECOBEE_CONFIG_FILE = 'ecobee.conf'
_CONFIGURING = {}
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180)
def request_configuration(network, hass, config):
""" Request configuration steps from the user. """
configurator = get_component('configurator')
if 'ecobee' in _CONFIGURING:
configurator.notify_errors(
_CONFIGURING['ecobee'], "Failed to register, please try again.")
return
# pylint: disable=unused-argument
def ecobee_configuration_callback(callback_data):
""" Actions to do when our configuration callback is called. """
network.request_tokens()
network.update()
setup_ecobee(hass, network, config)
_CONFIGURING['ecobee'] = configurator.request_config(
hass, "Ecobee", ecobee_configuration_callback,
description=(
'Please authorize this app at https://www.ecobee.com/consumer'
'portal/index.html with pin code: ' + network.pin),
description_image="/static/images/config_ecobee_thermostat.png",
submit_caption="I have authorized the app."
)
def setup_ecobee(hass, network, config):
""" Setup ecobee thermostat """
# If ecobee has a PIN then it needs to be configured.
if network.pin is not None:
request_configuration(network, hass, config)
return
if 'ecobee' in _CONFIGURING:
configurator = get_component('configurator')
configurator.request_done(_CONFIGURING.pop('ecobee'))
# Ensure component is loaded
bootstrap.setup_component(hass, 'thermostat', config)
bootstrap.setup_component(hass, 'sensor', config)
hold_temp = config[DOMAIN].get(HOLD_TEMP, False)
# Fire thermostat discovery event
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
ATTR_SERVICE: DISCOVER_THERMOSTAT,
ATTR_DISCOVERED: {'hold_temp': hold_temp}
})
# Fire sensor discovery event
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
ATTR_SERVICE: DISCOVER_SENSORS,
ATTR_DISCOVERED: {}
})
# pylint: disable=too-few-public-methods
class EcobeeData(object):
""" Gets the latest data and update the states. """
def __init__(self, config_file):
from pyecobee import Ecobee
self.ecobee = Ecobee(config_file)
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
""" Get the latest data from pyecobee. """
self.ecobee.update()
_LOGGER.info("ecobee data updated successfully.")
def setup(hass, config):
"""
Setup Ecobee.
Will automatically load thermostat and sensor components to support
devices discovered on the network.
"""
# pylint: disable=global-statement, import-error
global NETWORK
if 'ecobee' in _CONFIGURING:
return
from pyecobee import config_from_file
# Create ecobee.conf if it doesn't exist
if not os.path.isfile(hass.config.path(ECOBEE_CONFIG_FILE)):
if config[DOMAIN].get(CONF_API_KEY) is None:
_LOGGER.error("No ecobee api_key found in config.")
return
jsonconfig = {"API_KEY": config[DOMAIN].get(CONF_API_KEY)}
config_from_file(hass.config.path(ECOBEE_CONFIG_FILE), jsonconfig)
NETWORK = EcobeeData(hass.config.path(ECOBEE_CONFIG_FILE))
setup_ecobee(hass, NETWORK.ecobee, config)
return True

View File

@ -8,10 +8,9 @@ import re
import os import os
import logging import logging
from . import version from . import version, mdi_version
import homeassistant.util as util import homeassistant.util as util
from homeassistant.const import URL_ROOT, HTTP_OK from homeassistant.const import URL_ROOT, HTTP_OK
from homeassistant.config import get_default_config_dir
DOMAIN = 'frontend' DOMAIN = 'frontend'
DEPENDENCIES = ['api'] DEPENDENCIES = ['api']
@ -22,8 +21,9 @@ _LOGGER = logging.getLogger(__name__)
FRONTEND_URLS = [ FRONTEND_URLS = [
URL_ROOT, '/logbook', '/history', '/map', '/devService', '/devState', URL_ROOT, '/logbook', '/history', '/map', '/devService', '/devState',
'/devEvent'] '/devEvent', '/devInfo', '/devTemplate', '/states']
STATES_URL = re.compile(r'/states(/([a-zA-Z\._\-0-9/]+)|)')
_FINGERPRINT = re.compile(r'^(\w+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
def setup(hass, config): def setup(hass, config):
@ -35,7 +35,8 @@ def setup(hass, config):
for url in FRONTEND_URLS: for url in FRONTEND_URLS:
hass.http.register_path('GET', url, _handle_get_root, False) hass.http.register_path('GET', url, _handle_get_root, False)
hass.http.register_path('GET', STATES_URL, _handle_get_root, False) hass.http.register_path('GET', '/service_worker.js',
_handle_get_service_worker, False)
# Static files # Static files
hass.http.register_path( hass.http.register_path(
@ -52,8 +53,7 @@ def setup(hass, config):
def _handle_get_root(handler, path_match, data): def _handle_get_root(handler, path_match, data):
""" Renders the debug interface. """ """ Renders the frontend. """
handler.send_response(HTTP_OK) handler.send_response(HTTP_OK)
handler.send_header('Content-type', 'text/html; charset=utf-8') handler.send_header('Content-type', 'text/html; charset=utf-8')
handler.end_headers() handler.end_headers()
@ -64,7 +64,7 @@ def _handle_get_root(handler, path_match, data):
app_url = "frontend-{}.html".format(version.VERSION) app_url = "frontend-{}.html".format(version.VERSION)
# auto login if no password was set, else check api_password param # auto login if no password was set, else check api_password param
auth = ('no_password_set' if handler.server.no_password_set auth = ('no_password_set' if handler.server.api_password is None
else data.get('api_password', '')) else data.get('api_password', ''))
with open(INDEX_PATH) as template_file: with open(INDEX_PATH) as template_file:
@ -72,17 +72,30 @@ def _handle_get_root(handler, path_match, data):
template_html = template_html.replace('{{ app_url }}', app_url) template_html = template_html.replace('{{ app_url }}', app_url)
template_html = template_html.replace('{{ auth }}', auth) template_html = template_html.replace('{{ auth }}', auth)
template_html = template_html.replace('{{ icons }}', mdi_version.VERSION)
handler.wfile.write(template_html.encode("UTF-8")) handler.wfile.write(template_html.encode("UTF-8"))
def _handle_get_service_worker(handler, path_match, data):
""" Returns service worker for the frontend. """
if handler.server.development:
sw_path = "home-assistant-polymer/build/service_worker.js"
else:
sw_path = "service_worker.js"
handler.write_file(os.path.join(os.path.dirname(__file__), 'www_static',
sw_path))
def _handle_get_static(handler, path_match, data): def _handle_get_static(handler, path_match, data):
""" Returns a static file for the frontend. """ """ Returns a static file for the frontend. """
req_file = util.sanitize_path(path_match.group('file')) req_file = util.sanitize_path(path_match.group('file'))
# Strip md5 hash out of frontend filename # Strip md5 hash out
if re.match(r'^frontend-[A-Za-z0-9]{32}\.html$', req_file): fingerprinted = _FINGERPRINT.match(req_file)
req_file = "frontend.html" if fingerprinted:
req_file = "{}.{}".format(*fingerprinted.groups())
path = os.path.join(os.path.dirname(__file__), 'www_static', req_file) path = os.path.join(os.path.dirname(__file__), 'www_static', req_file)
@ -95,8 +108,6 @@ def _handle_get_local(handler, path_match, data):
""" """
req_file = util.sanitize_path(path_match.group('file')) req_file = util.sanitize_path(path_match.group('file'))
path = os.path.join(get_default_config_dir(), 'www', req_file) path = handler.server.hass.config.path('www', req_file)
if not os.path.isfile(path):
return False
handler.write_file(path) handler.write_file(path)

View File

@ -1,22 +1,19 @@
<!doctype html> <!doctype html>
<html> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Home Assistant</title> <title>Home Assistant</title>
<link rel='manifest' href='/static/manifest.json' /> <link rel='manifest' href='/static/manifest.json'>
<link rel='shortcut icon' href='/static/favicon.ico' /> <link rel='icon' href='/static/favicon.ico'>
<link rel='icon' type='image/png'
href='/static/favicon-192x192.png' sizes='192x192'>
<link rel='apple-touch-icon' sizes='180x180' <link rel='apple-touch-icon' sizes='180x180'
href='/static/favicon-apple-180x180.png'> href='/static/favicon-apple-180x180.png'>
<meta name='apple-mobile-web-app-capable' content='yes'> <meta name='apple-mobile-web-app-capable' content='yes'>
<meta name='mobile-web-app-capable' content='yes'> <meta name='mobile-web-app-capable' content='yes'>
<meta name='viewport' content='width=device-width, <meta name='viewport' content='width=device-width, user-scalable=no'>
user-scalable=no' />
<meta name='theme-color' content='#03a9f4'> <meta name='theme-color' content='#03a9f4'>
<style> <style>
#init { #ha-init-skeleton {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
-webkit-flex-direction: column; -webkit-flex-direction: column;
@ -26,26 +23,29 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
text-align: center; text-align: center;
font-family: 'Roboto', 'Noto', sans-serif;
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
} margin-bottom: 123px;
#init div {
line-height: 34px;
margin-bottom: 89px;
} }
</style> </style>
<link rel='import' href='/static/{{ app_url }}' async>
</head> </head>
<body fullbleed> <body fullbleed>
<div id='init'> <div id='ha-init-skeleton'><img src='/static/favicon-192x192.png' height='192'></div>
<img src='/static/splash.png' height='230' /> <script>
<div>Initializing</div> var webComponentsSupported = ('registerElement' in document &&
</div> 'import' in document.createElement('link') &&
<script src='/static/webcomponents-lite.min.js'></script> 'content' in document.createElement('template'))
<link rel='import' href='/static/{{ app_url }}' /> if (!webComponentsSupported) {
<home-assistant auth='{{ auth }}'></home-assistant> var script = document.createElement('script')
script.async = true
script.src = '/static/webcomponents-lite.min.js'
document.head.appendChild(script)
}
</script>
<home-assistant auth='{{ auth }}' icons='{{ icons }}'></home-assistant>
</body> </body>
</html> </html>

View File

@ -0,0 +1,2 @@
""" DO NOT MODIFY. Auto-generated by update_mdi script """
VERSION = "7d76081c37634d36af21f5cc1ca79408"

View File

@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """ """ DO NOT MODIFY. Auto-generated by build_frontend script """
VERSION = "90c41bfbaa56f9a1c88db27a54f7d36b" VERSION = "be08c5a3ce12040bbdba2db83cb1a568"

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit c91fcccf29c977bb0f8e1143fb26fc75613b6a0f Subproject commit 50aadaf880a9cb36bf144540171ff5fa029e9eaf

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -3,12 +3,17 @@
"short_name": "Assistant", "short_name": "Assistant",
"start_url": "/", "start_url": "/",
"display": "standalone", "display": "standalone",
"theme_color": "#03A9F4",
"icons": [ "icons": [
{ {
"src": "\/static\/favicon-192x192.png", "src": "/static/favicon-192x192.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image\/png", "type": "image/png",
"density": "4.0" },
{
"src": "/static/favicon-384x384.png",
"sizes": "384x384",
"type": "image/png",
} }
] ]
} }

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,5 @@
!function(e){function t(r){if(n[r])return n[r].exports;var s=n[r]={exports:{},id:r,loaded:!1};return e[r].call(s.exports,s,s.exports,t),s.loaded=!0,s.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([/*!*************************************!*\
!*** ./src/service-worker/index.js ***!
\*************************************/
function(e,t,n){"use strict";var r="0.10",s="/",c=["/","/logbook","/history","/map","/devService","/devState","/devEvent","/devInfo","/states"],i=["/static/favicon-192x192.png"];self.addEventListener("install",function(e){e.waitUntil(caches.open(r).then(function(e){return e.addAll(i.concat(s))}))}),self.addEventListener("activate",function(e){}),self.addEventListener("message",function(e){}),self.addEventListener("fetch",function(e){var t=e.request.url.substr(e.request.url.indexOf("/",8));i.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(e.request)})),c.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(s).then(function(n){var r=fetch(e.request).then(function(e){return t.put(s,e.clone()),e});return n||r})}))})}]);
//# sourceMappingURL=service_worker.js.map

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

File diff suppressed because one or more lines are too long

View File

@ -1,10 +1,11 @@
""" """
homeassistant.components.group homeassistant.components.group
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to group devices that can be turned on or off. Provides functionality to group devices that can be turned on or off.
"""
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/group/
"""
import homeassistant.core as ha import homeassistant.core as ha
from homeassistant.helpers import generate_entity_id from homeassistant.helpers import generate_entity_id
from homeassistant.helpers.event import track_state_change from homeassistant.helpers.event import track_state_change
@ -16,7 +17,6 @@ from homeassistant.const import (
STATE_UNKNOWN) STATE_UNKNOWN)
DOMAIN = "group" DOMAIN = "group"
DEPENDENCIES = []
ENTITY_ID_FORMAT = DOMAIN + ".{}" ENTITY_ID_FORMAT = DOMAIN + ".{}"

View File

@ -1,8 +1,10 @@
""" """
homeassistant.components.history homeassistant.components.history
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provide pre-made queries on top of the recorder component. Provide pre-made queries on top of the recorder component.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/history/
""" """
import re import re
from datetime import timedelta from datetime import timedelta

View File

@ -1,94 +1,27 @@
""" """
homeassistant.components.httpinterface homeassistant.components.http
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This module provides an API and a HTTP interface for debug purposes. This module provides an API and a HTTP interface for debug purposes.
By default it will run on port 8123. For more details about the RESTful API, please refer to the documentation at
https://home-assistant.io/developers/api/
All API calls have to be accompanied by an 'api_password' parameter and will
return JSON. If successful calls will return status code 200 or 201.
Other status codes that can occur are:
- 400 (Bad Request)
- 401 (Unauthorized)
- 404 (Not Found)
- 405 (Method not allowed)
The api supports the following actions:
/api - GET
Returns message if API is up and running.
Example result:
{
"message": "API running."
}
/api/states - GET
Returns a list of entities for which a state is available
Example result:
[
{ .. state object .. },
{ .. state object .. }
]
/api/states/<entity_id> - GET
Returns the current state from an entity
Example result:
{
"attributes": {
"next_rising": "07:04:15 29-10-2013",
"next_setting": "18:00:31 29-10-2013"
},
"entity_id": "weather.sun",
"last_changed": "23:24:33 28-10-2013",
"state": "below_horizon"
}
/api/states/<entity_id> - POST
Updates the current state of an entity. Returns status code 201 if successful
with location header of updated resource and as body the new state.
parameter: new_state - string
optional parameter: attributes - JSON encoded object
Example result:
{
"attributes": {
"next_rising": "07:04:15 29-10-2013",
"next_setting": "18:00:31 29-10-2013"
},
"entity_id": "weather.sun",
"last_changed": "23:24:33 28-10-2013",
"state": "below_horizon"
}
/api/events/<event_type> - POST
Fires an event with event_type
optional parameter: event_data - JSON encoded object
Example result:
{
"message": "Event download_file fired."
}
""" """
import json
import threading
import logging
import time
import gzip
import os
import random
import string
from datetime import timedelta from datetime import timedelta
from homeassistant.util import Throttle import gzip
from http.server import SimpleHTTPRequestHandler, HTTPServer
from http import cookies from http import cookies
from http.server import SimpleHTTPRequestHandler, HTTPServer
import json
import logging
import os
from socketserver import ThreadingMixIn from socketserver import ThreadingMixIn
import ssl
import threading
import time
from urllib.parse import urlparse, parse_qs from urllib.parse import urlparse, parse_qs
import homeassistant.core as ha import homeassistant.core as ha
from homeassistant.const import ( from homeassistant.const import (
SERVER_PORT, CONTENT_TYPE_JSON, SERVER_PORT, CONTENT_TYPE_JSON, CONTENT_TYPE_TEXT_PLAIN,
HTTP_HEADER_HA_AUTH, HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_ACCEPT_ENCODING, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_ACCEPT_ENCODING,
HTTP_HEADER_CONTENT_ENCODING, HTTP_HEADER_VARY, HTTP_HEADER_CONTENT_LENGTH, HTTP_HEADER_CONTENT_ENCODING, HTTP_HEADER_VARY, HTTP_HEADER_CONTENT_LENGTH,
HTTP_HEADER_CACHE_CONTROL, HTTP_HEADER_EXPIRES, HTTP_OK, HTTP_UNAUTHORIZED, HTTP_HEADER_CACHE_CONTROL, HTTP_HEADER_EXPIRES, HTTP_OK, HTTP_UNAUTHORIZED,
@ -99,51 +32,43 @@ import homeassistant.util.dt as date_util
import homeassistant.bootstrap as bootstrap import homeassistant.bootstrap as bootstrap
DOMAIN = "http" DOMAIN = "http"
DEPENDENCIES = []
CONF_API_PASSWORD = "api_password" CONF_API_PASSWORD = "api_password"
CONF_SERVER_HOST = "server_host" CONF_SERVER_HOST = "server_host"
CONF_SERVER_PORT = "server_port" CONF_SERVER_PORT = "server_port"
CONF_DEVELOPMENT = "development" CONF_DEVELOPMENT = "development"
CONF_SESSIONS_ENABLED = "sessions_enabled" CONF_SSL_CERTIFICATE = 'ssl_certificate'
CONF_SSL_KEY = 'ssl_key'
DATA_API_PASSWORD = 'api_password' DATA_API_PASSWORD = 'api_password'
# Throttling time in seconds for expired sessions check # Throttling time in seconds for expired sessions check
MIN_SEC_SESSION_CLEARING = timedelta(seconds=20) SESSION_CLEAR_INTERVAL = timedelta(seconds=20)
SESSION_TIMEOUT_SECONDS = 1800 SESSION_TIMEOUT_SECONDS = 1800
SESSION_KEY = 'sessionId' SESSION_KEY = 'sessionId'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def setup(hass, config=None): def setup(hass, config):
""" Sets up the HTTP API and debug interface. """ """ Sets up the HTTP API and debug interface. """
if config is None or DOMAIN not in config: conf = config.get(DOMAIN, {})
config = {DOMAIN: {}}
api_password = util.convert(config[DOMAIN].get(CONF_API_PASSWORD), str) api_password = util.convert(conf.get(CONF_API_PASSWORD), str)
no_password_set = api_password is None
if no_password_set:
api_password = util.get_random_string()
# If no server host is given, accept all incoming requests # If no server host is given, accept all incoming requests
server_host = config[DOMAIN].get(CONF_SERVER_HOST, '0.0.0.0') server_host = conf.get(CONF_SERVER_HOST, '0.0.0.0')
server_port = conf.get(CONF_SERVER_PORT, SERVER_PORT)
server_port = config[DOMAIN].get(CONF_SERVER_PORT, SERVER_PORT) development = str(conf.get(CONF_DEVELOPMENT, "")) == "1"
ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
development = str(config[DOMAIN].get(CONF_DEVELOPMENT, "")) == "1" ssl_key = conf.get(CONF_SSL_KEY)
sessions_enabled = config[DOMAIN].get(CONF_SESSIONS_ENABLED, True)
try: try:
server = HomeAssistantHTTPServer( server = HomeAssistantHTTPServer(
(server_host, server_port), RequestHandler, hass, api_password, (server_host, server_port), RequestHandler, hass, api_password,
development, no_password_set, sessions_enabled) development, ssl_certificate, ssl_key)
except OSError: except OSError:
# Happens if address already in use # If address already in use
_LOGGER.exception("Error setting up HTTP server") _LOGGER.exception("Error setting up HTTP server")
return False return False
@ -153,7 +78,8 @@ def setup(hass, config=None):
threading.Thread(target=server.start, daemon=True).start()) threading.Thread(target=server.start, daemon=True).start())
hass.http = server hass.http = server
hass.config.api = rem.API(util.get_local_ip(), api_password, server_port) hass.config.api = rem.API(util.get_local_ip(), api_password, server_port,
ssl_certificate is not None)
return True return True
@ -168,17 +94,16 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
def __init__(self, server_address, request_handler_class, def __init__(self, server_address, request_handler_class,
hass, api_password, development, no_password_set, hass, api_password, development, ssl_certificate, ssl_key):
sessions_enabled):
super().__init__(server_address, request_handler_class) super().__init__(server_address, request_handler_class)
self.server_address = server_address self.server_address = server_address
self.hass = hass self.hass = hass
self.api_password = api_password self.api_password = api_password
self.development = development self.development = development
self.no_password_set = no_password_set
self.paths = [] self.paths = []
self.sessions = SessionStore(sessions_enabled) self.sessions = SessionStore()
self.use_ssl = ssl_certificate is not None
# We will lazy init this one if needed # We will lazy init this one if needed
self.event_forwarder = None self.event_forwarder = None
@ -186,6 +111,12 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
if development: if development:
_LOGGER.info("running http in development mode") _LOGGER.info("running http in development mode")
if ssl_certificate is not None:
wrap_kwargs = {'certfile': ssl_certificate}
if ssl_key is not None:
wrap_kwargs['keyfile'] = ssl_key
self.socket = ssl.wrap_socket(self.socket, **wrap_kwargs)
def start(self): def start(self):
""" Starts the HTTP server. """ """ Starts the HTTP server. """
def stop_http(event): def stop_http(event):
@ -194,8 +125,11 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
self.hass.bus.listen_once(ha.EVENT_HOMEASSISTANT_STOP, stop_http) self.hass.bus.listen_once(ha.EVENT_HOMEASSISTANT_STOP, stop_http)
protocol = 'https' if self.use_ssl else 'http'
_LOGGER.info( _LOGGER.info(
"Starting web interface at http://%s:%d", *self.server_address) "Starting web interface at %s://%s:%d",
protocol, self.server_address[0], self.server_address[1])
# 31-1-2015: Refactored frontend/api components out of this component # 31-1-2015: Refactored frontend/api components out of this component
# To prevent stuff from breaking, load the two extracted components # To prevent stuff from breaking, load the two extracted components
@ -227,12 +161,13 @@ class RequestHandler(SimpleHTTPRequestHandler):
def __init__(self, req, client_addr, server): def __init__(self, req, client_addr, server):
""" Contructor, call the base constructor and set up session """ """ Contructor, call the base constructor and set up session """
self._session = None # Track if this was an authenticated request
self.authenticated = False
SimpleHTTPRequestHandler.__init__(self, req, client_addr, server) SimpleHTTPRequestHandler.__init__(self, req, client_addr, server)
def log_message(self, fmt, *arguments): def log_message(self, fmt, *arguments):
""" Redirect built-in log to HA logging """ """ Redirect built-in log to HA logging """
if self.server.no_password_set: if self.server.api_password is None:
_LOGGER.info(fmt, *arguments) _LOGGER.info(fmt, *arguments)
else: else:
_LOGGER.info( _LOGGER.info(
@ -243,12 +178,8 @@ class RequestHandler(SimpleHTTPRequestHandler):
""" Does some common checks and calls appropriate method. """ """ Does some common checks and calls appropriate method. """
url = urlparse(self.path) url = urlparse(self.path)
# Read query input # Read query input. parse_qs gives a list for each value, we want last
data = parse_qs(url.query) data = {key: data[-1] for key, data in parse_qs(url.query).items()}
# parse_qs gives a list for each value, take the latest element
for key in data:
data[key] = data[key][-1]
# Did we get post input ? # Did we get post input ?
content_length = int(self.headers.get(HTTP_HEADER_CONTENT_LENGTH, 0)) content_length = int(self.headers.get(HTTP_HEADER_CONTENT_LENGTH, 0))
@ -267,18 +198,12 @@ class RequestHandler(SimpleHTTPRequestHandler):
"Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY) "Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY)
return return
self._session = self.get_session() self.authenticated = (self.server.api_password is None
if self.server.no_password_set: or self.headers.get(HTTP_HEADER_HA_AUTH) ==
api_password = self.server.api_password self.server.api_password
else: or data.get(DATA_API_PASSWORD) ==
api_password = self.headers.get(HTTP_HEADER_HA_AUTH) self.server.api_password
or self.verify_session())
if not api_password and DATA_API_PASSWORD in data:
api_password = data[DATA_API_PASSWORD]
if not api_password and self._session is not None:
api_password = self._session.cookie_values.get(
CONF_API_PASSWORD)
if '_METHOD' in data: if '_METHOD' in data:
method = data.pop('_METHOD') method = data.pop('_METHOD')
@ -311,16 +236,11 @@ class RequestHandler(SimpleHTTPRequestHandler):
# Did we find a handler for the incoming request? # Did we find a handler for the incoming request?
if handle_request_method: if handle_request_method:
# For some calls we need a valid password # For some calls we need a valid password
if require_auth and api_password != self.server.api_password: if require_auth and not self.authenticated:
self.write_json_message( self.write_json_message(
"API password missing or incorrect.", HTTP_UNAUTHORIZED) "API password missing or incorrect.", HTTP_UNAUTHORIZED)
return
else:
if self._session is None and require_auth:
self._session = self.server.sessions.create(
api_password)
handle_request_method(self, path_match, data) handle_request_method(self, path_match, data)
@ -373,18 +293,30 @@ class RequestHandler(SimpleHTTPRequestHandler):
json.dumps(data, indent=4, sort_keys=True, json.dumps(data, indent=4, sort_keys=True,
cls=rem.JSONEncoder).encode("UTF-8")) cls=rem.JSONEncoder).encode("UTF-8"))
def write_file(self, path): def write_text(self, message, status_code=HTTP_OK):
""" Helper method to return a text message to the caller. """
self.send_response(status_code)
self.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN)
self.set_session_cookie_header()
self.end_headers()
self.wfile.write(message.encode("UTF-8"))
def write_file(self, path, cache_headers=True):
""" Returns a file to the user. """ """ Returns a file to the user. """
try: try:
with open(path, 'rb') as inp: with open(path, 'rb') as inp:
self.write_file_pointer(self.guess_type(path), inp) self.write_file_pointer(self.guess_type(path), inp,
cache_headers)
except IOError: except IOError:
self.send_response(HTTP_NOT_FOUND) self.send_response(HTTP_NOT_FOUND)
self.end_headers() self.end_headers()
_LOGGER.exception("Unable to serve %s", path) _LOGGER.exception("Unable to serve %s", path)
def write_file_pointer(self, content_type, inp): def write_file_pointer(self, content_type, inp, cache_headers=True):
""" """
Helper function to write a file pointer to the user. Helper function to write a file pointer to the user.
Does not do error handling. Does not do error handling.
@ -394,6 +326,7 @@ class RequestHandler(SimpleHTTPRequestHandler):
self.send_response(HTTP_OK) self.send_response(HTTP_OK)
self.send_header(HTTP_HEADER_CONTENT_TYPE, content_type) self.send_header(HTTP_HEADER_CONTENT_TYPE, content_type)
if cache_headers:
self.set_cache_header() self.set_cache_header()
self.set_session_cookie_header() self.set_session_cookie_header()
@ -421,7 +354,9 @@ class RequestHandler(SimpleHTTPRequestHandler):
def set_cache_header(self): def set_cache_header(self):
""" Add cache headers if not in development """ """ Add cache headers if not in development """
if not self.server.development: if self.server.development:
return
# 1 year in seconds # 1 year in seconds
cache_time = 365 * 86400 cache_time = 365 * 86400
@ -433,111 +368,112 @@ class RequestHandler(SimpleHTTPRequestHandler):
self.date_time_string(time.time()+cache_time)) self.date_time_string(time.time()+cache_time))
def set_session_cookie_header(self): def set_session_cookie_header(self):
""" Add the header for the session cookie """ """ Add the header for the session cookie and return session id. """
if self.server.sessions.enabled and self._session is not None: if not self.authenticated:
existing_sess_id = self.get_current_session_id() return None
session_id = self.get_cookie_session_id()
if session_id is not None:
self.server.sessions.extend_validation(session_id)
return session_id
if existing_sess_id != self._session.session_id:
self.send_header( self.send_header(
'Set-Cookie', 'Set-Cookie',
SESSION_KEY+'='+self._session.session_id) '{}={}'.format(SESSION_KEY, self.server.sessions.create())
)
def get_session(self): return session_id
""" Get the requested session object from cookie value """
if self.server.sessions.enabled is not True:
return None
session_id = self.get_current_session_id() def verify_session(self):
if session_id is not None: """ Verify that we are in a valid session. """
session = self.server.sessions.get(session_id) return self.get_cookie_session_id() is not None
if session is not None:
session.reset_expiry()
return session
return None def get_cookie_session_id(self):
def get_current_session_id(self):
""" """
Extracts the current session id from the Extracts the current session id from the
cookie or returns None if not set cookie or returns None if not set or invalid
""" """
if 'Cookie' not in self.headers:
return None
cookie = cookies.SimpleCookie() cookie = cookies.SimpleCookie()
try:
cookie.load(self.headers["Cookie"])
except cookies.CookieError:
return None
if self.headers.get('Cookie', None) is not None: morsel = cookie.get(SESSION_KEY)
cookie.load(self.headers.get("Cookie"))
if cookie.get(SESSION_KEY, False): if morsel is None:
return cookie[SESSION_KEY].value return None
session_id = cookie[SESSION_KEY].value
if self.server.sessions.is_valid(session_id):
return session_id
return None return None
def destroy_session(self):
""" Destroys session. """
session_id = self.get_cookie_session_id()
class ServerSession: if session_id is None:
""" A very simple session class """ return
def __init__(self, session_id):
""" Set up the expiry time on creation """
self._expiry = 0
self.reset_expiry()
self.cookie_values = {}
self.session_id = session_id
def reset_expiry(self): self.send_header('Set-Cookie', '')
""" Resets the expiry based on current time """ self.server.sessions.destroy(session_id)
self._expiry = date_util.utcnow() + timedelta(
seconds=SESSION_TIMEOUT_SECONDS)
@property
def is_expired(self): def session_valid_time():
""" Return true if the session is expired based on the expiry time """ """ Time till when a session will be valid. """
return self._expiry < date_util.utcnow() return date_util.utcnow() + timedelta(seconds=SESSION_TIMEOUT_SECONDS)
class SessionStore(object): class SessionStore(object):
""" Responsible for storing and retrieving http sessions """ """ Responsible for storing and retrieving http sessions """
def __init__(self, enabled=True): def __init__(self):
""" Set up the session store """ """ Set up the session store """
self._sessions = {} self._sessions = {}
self.enabled = enabled self._lock = threading.RLock()
self.session_lock = threading.RLock()
@Throttle(MIN_SEC_SESSION_CLEARING) @util.Throttle(SESSION_CLEAR_INTERVAL)
def remove_expired(self): def _remove_expired(self):
""" Remove any expired sessions. """ """ Remove any expired sessions. """
if self.session_lock.acquire(False): now = date_util.utcnow()
try: for key in [key for key, valid_time in self._sessions.items()
keys = [] if valid_time < now]:
for key in self._sessions.keys(): self._sessions.pop(key)
keys.append(key)
for key in keys: def is_valid(self, key):
if self._sessions[key].is_expired: """ Return True if a valid session is given. """
del self._sessions[key] with self._lock:
_LOGGER.info("Cleared expired session %s", key) self._remove_expired()
finally:
self.session_lock.release()
def add(self, key, session): return (key in self._sessions and
""" Add a new session to the list of tracked sessions """ self._sessions[key] > date_util.utcnow())
self.remove_expired()
with self.session_lock:
self._sessions[key] = session
def get(self, key): def extend_validation(self, key):
""" get a session by key """ """ Extend a session validation time. """
self.remove_expired() with self._lock:
session = self._sessions.get(key, None) if key not in self._sessions:
if session is not None and session.is_expired: return
return None self._sessions[key] = session_valid_time()
return session
def create(self, api_password): def destroy(self, key):
""" Creates a new session and adds it to the sessions """ """ Destroy a session by key. """
if self.enabled is not True: with self._lock:
return None self._sessions.pop(key, None)
chars = string.ascii_letters + string.digits def create(self):
session_id = ''.join([random.choice(chars) for i in range(20)]) """ Creates a new session. """
session = ServerSession(session_id) with self._lock:
session.cookie_values[CONF_API_PASSWORD] = api_password session_id = util.get_random_string(20)
self.add(session_id, session)
return session while session_id in self._sessions:
session_id = util.get_random_string(20)
self._sessions[session_id] = session_valid_time()
return session_id

View File

@ -1,23 +1,10 @@
""" """
homeassistant.components.ifttt homeassistant.components.ifttt
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This component enable you to trigger Maker IFTTT recipes. This component enable you to trigger Maker IFTTT recipes.
Check https://ifttt.com/maker for details.
Configuration:
To use Maker IFTTT you will need to add something like the following to your
config/configuration.yaml.
ifttt:
key: xxxxx-x-xxxxxxxxxxxxx
Variables:
key
*Required
Your api key
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/ifttt/
""" """
import logging import logging
import requests import requests
@ -35,13 +22,11 @@ ATTR_VALUE1 = 'value1'
ATTR_VALUE2 = 'value2' ATTR_VALUE2 = 'value2'
ATTR_VALUE3 = 'value3' ATTR_VALUE3 = 'value3'
DEPENDENCIES = []
REQUIREMENTS = ['pyfttt==0.3'] REQUIREMENTS = ['pyfttt==0.3']
def trigger(hass, event, value1=None, value2=None, value3=None): def trigger(hass, event, value1=None, value2=None, value3=None):
""" Trigger a Maker IFTTT recipe """ """ Trigger a Maker IFTTT recipe. """
data = { data = {
ATTR_EVENT: event, ATTR_EVENT: event,
ATTR_VALUE1: value1, ATTR_VALUE1: value1,
@ -52,7 +37,7 @@ def trigger(hass, event, value1=None, value2=None, value3=None):
def setup(hass, config): def setup(hass, config):
""" Setup the ifttt service component """ """ Setup the ifttt service component. """
if not validate_config(config, {DOMAIN: ['key']}, _LOGGER): if not validate_config(config, {DOMAIN: ['key']}, _LOGGER):
return False return False

View File

@ -0,0 +1,104 @@
"""
homeassistant.components.influxdb
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
InfluxDB component which allows you to send data to an Influx database.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/influxdb/
"""
import logging
import homeassistant.util as util
from homeassistant.helpers import validate_config
from homeassistant.const import (EVENT_STATE_CHANGED, STATE_ON, STATE_OFF,
STATE_UNLOCKED, STATE_LOCKED, STATE_UNKNOWN)
from homeassistant.components.sun import (STATE_ABOVE_HORIZON,
STATE_BELOW_HORIZON)
_LOGGER = logging.getLogger(__name__)
DOMAIN = "influxdb"
DEPENDENCIES = []
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 8086
DEFAULT_DATABASE = 'home_assistant'
REQUIREMENTS = ['influxdb==2.10.0']
CONF_HOST = 'host'
CONF_PORT = 'port'
CONF_DB_NAME = 'database'
CONF_USERNAME = 'username'
CONF_PASSWORD = 'password'
def setup(hass, config):
""" Setup the InfluxDB component. """
from influxdb import InfluxDBClient, exceptions
if not validate_config(config, {DOMAIN: ['host']}, _LOGGER):
return False
conf = config[DOMAIN]
host = conf[CONF_HOST]
port = util.convert(conf.get(CONF_PORT), int, DEFAULT_PORT)
database = util.convert(conf.get(CONF_DB_NAME), str, DEFAULT_DATABASE)
username = util.convert(conf.get(CONF_USERNAME), str)
password = util.convert(conf.get(CONF_PASSWORD), str)
try:
influx = InfluxDBClient(host=host, port=port, username=username,
password=password, database=database)
databases = [i['name'] for i in influx.get_list_database()]
except exceptions.InfluxDBClientError:
_LOGGER.error("Database host is not accessible. "
"Please check your entries in the configuration file.")
return False
if database not in databases:
_LOGGER.error("Database %s doesn't exist", database)
return False
def influx_event_listener(event):
""" Listen for new messages on the bus and sends them to Influx. """
state = event.data.get('new_state')
if state is None:
return
if state.state in (STATE_ON, STATE_LOCKED, STATE_ABOVE_HORIZON):
_state = 1
elif state.state in (STATE_OFF, STATE_UNLOCKED, STATE_UNKNOWN,
STATE_BELOW_HORIZON):
_state = 0
else:
_state = state.state
measurement = state.attributes.get('unit_of_measurement', state.domain)
json_body = [
{
'measurement': measurement,
'tags': {
'domain': state.domain,
'entity_id': state.object_id,
},
'time': event.time_fired,
'fields': {
'value': _state,
}
}
]
try:
influx.write_points(json_body)
except exceptions.InfluxDBClientError:
_LOGGER.exception('Error saving event to InfluxDB')
hass.bus.listen(EVENT_STATE_CHANGED, influx_event_listener)
return True

View File

@ -1,13 +1,14 @@
""" """
homeassistant.components.introduction homeassistant.components.introduction
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Component that will help guide the user taking its first steps. Component that will help guide the user taking its first steps.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/introduction/
""" """
import logging import logging
DOMAIN = 'introduction' DOMAIN = 'introduction'
DEPENDENCIES = []
def setup(hass, config=None): def setup(hass, config=None):
@ -24,13 +25,13 @@ def setup(hass, config=None):
Here are some resources to get started: Here are some resources to get started:
- Configuring Home Assistant: - Configuring Home Assistant:
https://home-assistant.io/getting-started/configuration.html https://home-assistant.io/getting-started/configuration/
- Available components: - Available components:
https://home-assistant.io/components/ https://home-assistant.io/components/
- Troubleshooting your configuration: - Troubleshooting your configuration:
https://home-assistant.io/getting-started/troubleshooting-configuration.html https://home-assistant.io/getting-started/troubleshooting-configuration/
- Getting help: - Getting help:
https://home-assistant.io/help/ https://home-assistant.io/help/

View File

@ -5,7 +5,7 @@ Connects to an ISY-994 controller and loads relevant components to control its
devices. Also contains the base classes for ISY Sensors, Lights, and Switches. devices. Also contains the base classes for ISY Sensors, Lights, and Switches.
For configuration details please visit the documentation for this component at For configuration details please visit the documentation for this component at
https://home-assistant.io/components/isy994.html https://home-assistant.io/components/isy994/
""" """
import logging import logging
from urllib.parse import urlparse from urllib.parse import urlparse
@ -20,7 +20,6 @@ from homeassistant.const import (
ATTR_FRIENDLY_NAME) ATTR_FRIENDLY_NAME)
DOMAIN = "isy994" DOMAIN = "isy994"
DEPENDENCIES = []
REQUIREMENTS = ['PyISY==1.0.5'] REQUIREMENTS = ['PyISY==1.0.5']
DISCOVER_LIGHTS = "isy994.lights" DISCOVER_LIGHTS = "isy994.lights"
DISCOVER_SWITCHES = "isy994.switches" DISCOVER_SWITCHES = "isy994.switches"
@ -117,7 +116,6 @@ class ISYDeviceABC(ToggleEntity):
def __init__(self, node): def __init__(self, node):
# setup properties # setup properties
self.node = node self.node = node
self.hidden = HIDDEN_STRING in self.raw_name
# track changes # track changes
self._change_handler = self.node.status. \ self._change_handler = self.node.status. \
@ -182,6 +180,11 @@ class ISYDeviceABC(ToggleEntity):
return self.raw_name.replace(HIDDEN_STRING, '').strip() \ return self.raw_name.replace(HIDDEN_STRING, '').strip() \
.replace('_', ' ') .replace('_', ' ')
@property
def hidden(self):
""" Suggestion if the entity should be hidden from UIs. """
return HIDDEN_STRING in self.raw_name
def update(self): def update(self):
""" Update state of the sensor. """ """ Update state of the sensor. """
# ISY objects are automatically updated by the ISY's event stream # ISY objects are automatically updated by the ISY's event stream

View File

@ -1,8 +1,10 @@
""" """
homeassistant.components.keyboard homeassistant.components.keyboard
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to emulate keyboard presses on host machine. Provides functionality to emulate keyboard presses on host machine.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/keyboard/
""" """
import logging import logging
@ -13,7 +15,6 @@ from homeassistant.const import (
DOMAIN = "keyboard" DOMAIN = "keyboard"
DEPENDENCIES = []
REQUIREMENTS = ['pyuserinput==0.1.9'] REQUIREMENTS = ['pyuserinput==0.1.9']

View File

@ -1,58 +1,16 @@
""" """
homeassistant.components.light homeassistant.components.light
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Provides functionality to interact with lights. Provides functionality to interact with lights.
It offers the following services: For more details about this component, please refer to the documentation at
https://home-assistant.io/components/light/
TURN_OFF - Turns one or multiple lights off.
Supports following parameters:
- transition
Integer that represents the time the light should take to transition to
the new state.
- entity_id
String or list of strings that point at entity_ids of lights.
TURN_ON - Turns one or multiple lights on and change attributes.
Supports following parameters:
- transition
Integer that represents the time the light should take to transition to
the new state.
- entity_id
String or list of strings that point at entity_ids of lights.
- profile
String with the name of one of the built-in profiles (relax, energize,
concentrate, reading) or one of the custom profiles defined in
light_profiles.csv in the current working directory.
Light profiles define a xy color and a brightness.
If a profile is given and a brightness or xy color then the profile values
will be overwritten.
- xy_color
A list containing two floats representing the xy color you want the light
to be.
- rgb_color
A list containing three integers representing the xy color you want the
light to be.
- brightness
Integer between 0 and 255 representing how bright you want the light to be.
""" """
import logging import logging
import os import os
import csv import csv
from homeassistant.components import group, discovery, wink, isy994 from homeassistant.components import group, discovery, wink, isy994, zwave
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.const import ( from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
@ -63,7 +21,6 @@ import homeassistant.util.color as color_util
DOMAIN = "light" DOMAIN = "light"
DEPENDENCIES = []
SCAN_INTERVAL = 30 SCAN_INTERVAL = 30
GROUP_NAME_ALL_LIGHTS = 'all lights' GROUP_NAME_ALL_LIGHTS = 'all lights'
@ -77,6 +34,7 @@ ATTR_TRANSITION = "transition"
# lists holding color values # lists holding color values
ATTR_RGB_COLOR = "rgb_color" ATTR_RGB_COLOR = "rgb_color"
ATTR_XY_COLOR = "xy_color" ATTR_XY_COLOR = "xy_color"
ATTR_COLOR_TEMP = "color_temp"
# int with value 0 .. 255 representing brightness of the light # int with value 0 .. 255 representing brightness of the light
ATTR_BRIGHTNESS = "brightness" ATTR_BRIGHTNESS = "brightness"
@ -92,6 +50,8 @@ FLASH_LONG = "long"
# Apply an effect to the light, can be EFFECT_COLORLOOP # Apply an effect to the light, can be EFFECT_COLORLOOP
ATTR_EFFECT = "effect" ATTR_EFFECT = "effect"
EFFECT_COLORLOOP = "colorloop" EFFECT_COLORLOOP = "colorloop"
EFFECT_RANDOM = "random"
EFFECT_WHITE = "white"
LIGHT_PROFILES_FILE = "light_profiles.csv" LIGHT_PROFILES_FILE = "light_profiles.csv"
@ -100,11 +60,14 @@ DISCOVERY_PLATFORMS = {
wink.DISCOVER_LIGHTS: 'wink', wink.DISCOVER_LIGHTS: 'wink',
isy994.DISCOVER_LIGHTS: 'isy994', isy994.DISCOVER_LIGHTS: 'isy994',
discovery.SERVICE_HUE: 'hue', discovery.SERVICE_HUE: 'hue',
zwave.DISCOVER_LIGHTS: 'zwave',
} }
PROP_TO_ATTR = { PROP_TO_ATTR = {
'brightness': ATTR_BRIGHTNESS, 'brightness': ATTR_BRIGHTNESS,
'color_xy': ATTR_XY_COLOR, 'color_temp': ATTR_COLOR_TEMP,
'rgb_color': ATTR_RGB_COLOR,
'xy_color': ATTR_XY_COLOR,
} }
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -119,8 +82,8 @@ def is_on(hass, entity_id=None):
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
def turn_on(hass, entity_id=None, transition=None, brightness=None, def turn_on(hass, entity_id=None, transition=None, brightness=None,
rgb_color=None, xy_color=None, profile=None, flash=None, rgb_color=None, xy_color=None, color_temp=None, profile=None,
effect=None): flash=None, effect=None):
""" Turns all or specified light on. """ """ Turns all or specified light on. """
data = { data = {
key: value for key, value in [ key: value for key, value in [
@ -130,6 +93,7 @@ def turn_on(hass, entity_id=None, transition=None, brightness=None,
(ATTR_BRIGHTNESS, brightness), (ATTR_BRIGHTNESS, brightness),
(ATTR_RGB_COLOR, rgb_color), (ATTR_RGB_COLOR, rgb_color),
(ATTR_XY_COLOR, xy_color), (ATTR_XY_COLOR, xy_color),
(ATTR_COLOR_TEMP, color_temp),
(ATTR_FLASH, flash), (ATTR_FLASH, flash),
(ATTR_EFFECT, effect), (ATTR_EFFECT, effect),
] if value is not None ] if value is not None
@ -150,7 +114,7 @@ def turn_off(hass, entity_id=None, transition=None):
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
# pylint: disable=too-many-branches, too-many-locals # pylint: disable=too-many-branches, too-many-locals, too-many-statements
def setup(hass, config): def setup(hass, config):
""" Exposes light control via statemachine and services. """ """ Exposes light control via statemachine and services. """
@ -240,6 +204,15 @@ def setup(hass, config):
# ValueError if value could not be converted to float # ValueError if value could not be converted to float
pass pass
if ATTR_COLOR_TEMP in dat:
# color_temp should be an int of mirads value
colortemp = dat.get(ATTR_COLOR_TEMP)
# Without this check, a ctcolor with value '99' would work
# These values are based on Philips Hue, may need ajustment later
if isinstance(colortemp, int) and 154 <= colortemp <= 500:
params[ATTR_COLOR_TEMP] = colortemp
if ATTR_RGB_COLOR in dat: if ATTR_RGB_COLOR in dat:
try: try:
# rgb_color should be a list containing 3 ints # rgb_color should be a list containing 3 ints
@ -247,26 +220,18 @@ def setup(hass, config):
if len(rgb_color) == 3: if len(rgb_color) == 3:
params[ATTR_RGB_COLOR] = [int(val) for val in rgb_color] params[ATTR_RGB_COLOR] = [int(val) for val in rgb_color]
params[ATTR_XY_COLOR] = \
color_util.color_RGB_to_xy(int(rgb_color[0]),
int(rgb_color[1]),
int(rgb_color[2]))
except (TypeError, ValueError): except (TypeError, ValueError):
# TypeError if rgb_color is not iterable # TypeError if rgb_color is not iterable
# ValueError if not all values can be converted to int # ValueError if not all values can be converted to int
pass pass
if ATTR_FLASH in dat: if dat.get(ATTR_FLASH) in (FLASH_SHORT, FLASH_LONG):
if dat[ATTR_FLASH] == FLASH_SHORT: params[ATTR_FLASH] = dat[ATTR_FLASH]
params[ATTR_FLASH] = FLASH_SHORT
elif dat[ATTR_FLASH] == FLASH_LONG: if dat.get(ATTR_EFFECT) in (EFFECT_COLORLOOP, EFFECT_WHITE,
params[ATTR_FLASH] = FLASH_LONG EFFECT_RANDOM):
params[ATTR_EFFECT] = dat[ATTR_EFFECT]
if ATTR_EFFECT in dat:
if dat[ATTR_EFFECT] == EFFECT_COLORLOOP:
params[ATTR_EFFECT] = EFFECT_COLORLOOP
for light in target_lights: for light in target_lights:
light.turn_on(**params) light.turn_on(**params)
@ -297,10 +262,20 @@ class Light(ToggleEntity):
return None return None
@property @property
def color_xy(self): def xy_color(self):
""" XY color value [float, float]. """ """ XY color value [float, float]. """
return None return None
@property
def rgb_color(self):
""" RGB color value [int, int, int] """
return None
@property
def color_temp(self):
""" CT color value in mirads. """
return None
@property @property
def device_state_attributes(self): def device_state_attributes(self):
""" Returns device specific state attributes. """ """ Returns device specific state attributes. """
@ -317,6 +292,12 @@ class Light(ToggleEntity):
if value: if value:
data[attr] = value data[attr] = value
if ATTR_RGB_COLOR not in data and ATTR_XY_COLOR in data and \
ATTR_BRIGHTNESS in data:
data[ATTR_RGB_COLOR] = color_util.color_xy_brightness_to_RGB(
data[ATTR_XY_COLOR][0], data[ATTR_XY_COLOR][1],
data[ATTR_BRIGHTNESS])
device_attr = self.device_state_attributes device_attr = self.device_state_attributes
if device_attr is not None: if device_attr is not None:

View File

@ -4,24 +4,23 @@ homeassistant.components.light.blinksticklight
Support for Blinkstick lights. Support for Blinkstick lights.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.blinksticklight.html https://home-assistant.io/components/light.blinksticklight/
""" """
import logging import logging
from blinkstick import blinkstick from homeassistant.components.light import Light, ATTR_RGB_COLOR
from homeassistant.components.light import (Light, ATTR_RGB_COLOR)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ["blinkstick==1.1.7"] REQUIREMENTS = ["blinkstick==1.1.7"]
DEPENDENCIES = []
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Add device specified by serial number. """ """ Add device specified by serial number. """
from blinkstick import blinkstick
stick = blinkstick.find_by_serial(config['serial']) stick = blinkstick.find_by_serial(config['serial'])
add_devices_callback([BlinkStickLight(stick, config['name'])]) add_devices_callback([BlinkStickLight(stick, config['name'])])

View File

@ -1,37 +1,40 @@
""" """
homeassistant.components.light.demo homeassistant.components.light.demo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Demo platform that implements lights. Demo platform that implements lights.
""" """
import random import random
from homeassistant.components.light import ( from homeassistant.components.light import (
Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR) Light, ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_COLOR_TEMP)
LIGHT_COLORS = [ LIGHT_COLORS = [
[0.368, 0.180], [237, 224, 33],
[0.460, 0.470], [255, 63, 111],
] ]
LIGHT_TEMPS = [240, 380]
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Find and return demo lights. """ """ Find and return demo lights. """
add_devices_callback([ add_devices_callback([
DemoLight("Bed Light", False), DemoLight("Bed Light", False),
DemoLight("Ceiling Lights", True, LIGHT_COLORS[0]), DemoLight("Ceiling Lights", True, LIGHT_COLORS[0], LIGHT_TEMPS[1]),
DemoLight("Kitchen Lights", True, LIGHT_COLORS[1]) DemoLight("Kitchen Lights", True, LIGHT_COLORS[1], LIGHT_TEMPS[0])
]) ])
class DemoLight(Light): class DemoLight(Light):
""" Provides a demo switch. """ """ Provides a demo switch. """
def __init__(self, name, state, xy=None, brightness=180): # pylint: disable=too-many-arguments
def __init__(self, name, state, rgb=None, ct=None, brightness=180):
self._name = name self._name = name
self._state = state self._state = state
self._xy = xy or random.choice(LIGHT_COLORS) self._rgb = rgb or random.choice(LIGHT_COLORS)
self._ct = ct or random.choice(LIGHT_TEMPS)
self._brightness = brightness self._brightness = brightness
@property @property
@ -50,9 +53,14 @@ class DemoLight(Light):
return self._brightness return self._brightness
@property @property
def color_xy(self): def rgb_color(self):
""" XY color value. """ """ rgb color value. """
return self._xy return self._rgb
@property
def color_temp(self):
""" CT color temperature. """
return self._ct
@property @property
def is_on(self): def is_on(self):
@ -63,8 +71,11 @@ class DemoLight(Light):
""" Turn the device on. """ """ Turn the device on. """
self._state = True self._state = True
if ATTR_XY_COLOR in kwargs: if ATTR_RGB_COLOR in kwargs:
self._xy = kwargs[ATTR_XY_COLOR] self._rgb = kwargs[ATTR_RGB_COLOR]
if ATTR_COLOR_TEMP in kwargs:
self._ct = kwargs[ATTR_COLOR_TEMP]
if ATTR_BRIGHTNESS in kwargs: if ATTR_BRIGHTNESS in kwargs:
self._brightness = kwargs[ATTR_BRIGHTNESS] self._brightness = kwargs[ATTR_BRIGHTNESS]

View File

@ -2,19 +2,26 @@
homeassistant.components.light.hue homeassistant.components.light.hue
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for Hue lights. Support for Hue lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.hue/
""" """
import json
import logging import logging
import os
import socket import socket
import random
from datetime import timedelta from datetime import timedelta
from urllib.parse import urlparse from urllib.parse import urlparse
from homeassistant.loader import get_component from homeassistant.loader import get_component
import homeassistant.util as util import homeassistant.util as util
import homeassistant.util.color as color_util
from homeassistant.const import CONF_HOST, DEVICE_DEFAULT_NAME from homeassistant.const import CONF_HOST, DEVICE_DEFAULT_NAME
from homeassistant.components.light import ( from homeassistant.components.light import (
Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_TRANSITION, Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_COLOR_TEMP,
ATTR_FLASH, FLASH_LONG, FLASH_SHORT, ATTR_EFFECT, ATTR_TRANSITION, ATTR_FLASH, FLASH_LONG, FLASH_SHORT,
EFFECT_COLORLOOP) ATTR_EFFECT, EFFECT_COLORLOOP, EFFECT_RANDOM, ATTR_RGB_COLOR)
REQUIREMENTS = ['phue==0.8'] REQUIREMENTS = ['phue==0.8']
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
@ -28,21 +35,37 @@ _CONFIGURING = {}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def _find_host_from_config(hass):
""" Attempt to detect host based on existing configuration. """
path = hass.config.path(PHUE_CONFIG_FILE)
if not os.path.isfile(path):
return None
try:
with open(path) as inp:
return next(json.loads(''.join(inp)).keys().__iter__())
except (ValueError, AttributeError, StopIteration):
# ValueError if can't parse as JSON
# AttributeError if JSON value is not a dict
# StopIteration if no keys
return None
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Gets the Hue lights. """ """ Gets the Hue lights. """
try:
# pylint: disable=unused-variable
import phue # noqa
except ImportError:
_LOGGER.exception("Error while importing dependency phue.")
return
if discovery_info is not None: if discovery_info is not None:
host = urlparse(discovery_info[1]).hostname host = urlparse(discovery_info[1]).hostname
else: else:
host = config.get(CONF_HOST, None) host = config.get(CONF_HOST, None)
if host is None:
host = _find_host_from_config(hass)
if host is None:
_LOGGER.error('No host found in configuration')
return False
# Only act if we are not already configuring this host # Only act if we are not already configuring this host
if host in _CONFIGURING: if host in _CONFIGURING:
return return
@ -123,6 +146,7 @@ def request_configuration(host, hass, add_devices_callback):
return return
# pylint: disable=unused-argument
def hue_configuration_callback(data): def hue_configuration_callback(data):
""" Actions to do when our configuration callback is called. """ """ Actions to do when our configuration callback is called. """
setup_bridge(host, hass, add_devices_callback) setup_bridge(host, hass, add_devices_callback)
@ -162,10 +186,15 @@ class HueLight(Light):
return self.info['state']['bri'] return self.info['state']['bri']
@property @property
def color_xy(self): def xy_color(self):
""" XY color value. """ """ XY color value. """
return self.info['state'].get('xy') return self.info['state'].get('xy')
@property
def color_temp(self):
""" CT color value. """
return self.info['state'].get('ct')
@property @property
def is_on(self): def is_on(self):
""" True if device is on. """ """ True if device is on. """
@ -178,15 +207,19 @@ class HueLight(Light):
command = {'on': True} command = {'on': True}
if ATTR_TRANSITION in kwargs: if ATTR_TRANSITION in kwargs:
# Transition time is in 1/10th seconds and cannot exceed command['transitiontime'] = kwargs[ATTR_TRANSITION] * 10
# 900 seconds.
command['transitiontime'] = min(9000, kwargs[ATTR_TRANSITION] * 10)
if ATTR_BRIGHTNESS in kwargs: if ATTR_BRIGHTNESS in kwargs:
command['bri'] = kwargs[ATTR_BRIGHTNESS] command['bri'] = kwargs[ATTR_BRIGHTNESS]
if ATTR_XY_COLOR in kwargs: if ATTR_XY_COLOR in kwargs:
command['xy'] = kwargs[ATTR_XY_COLOR] command['xy'] = kwargs[ATTR_XY_COLOR]
elif ATTR_RGB_COLOR in kwargs:
command['xy'] = color_util.color_RGB_to_xy(
*(int(val) for val in kwargs[ATTR_RGB_COLOR]))
if ATTR_COLOR_TEMP in kwargs:
command['ct'] = kwargs[ATTR_COLOR_TEMP]
flash = kwargs.get(ATTR_FLASH) flash = kwargs.get(ATTR_FLASH)
@ -201,6 +234,9 @@ class HueLight(Light):
if effect == EFFECT_COLORLOOP: if effect == EFFECT_COLORLOOP:
command['effect'] = 'colorloop' command['effect'] = 'colorloop'
elif effect == EFFECT_RANDOM:
command['hue'] = random.randrange(0, 65535)
command['sat'] = random.randrange(150, 254)
else: else:
command['effect'] = 'none' command['effect'] = 'none'

View File

@ -0,0 +1,126 @@
"""
homeassistant.components.light.hyperion
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for Hyperion remotes.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.hyperion/
"""
import logging
import socket
import json
from homeassistant.const import CONF_HOST
from homeassistant.components.light import (Light, ATTR_RGB_COLOR)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = []
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Sets up a Hyperion server remote """
host = config.get(CONF_HOST, None)
port = config.get("port", 19444)
device = Hyperion(host, port)
if device.setup():
add_devices_callback([device])
return True
else:
return False
class Hyperion(Light):
""" Represents a Hyperion remote """
def __init__(self, host, port):
self._host = host
self._port = port
self._name = host
self._is_available = True
self._rgb_color = [255, 255, 255]
@property
def name(self):
""" Return the hostname of the server. """
return self._name
@property
def rgb_color(self):
""" Last RGB color value set. """
return self._rgb_color
@property
def is_on(self):
""" True if the device is online. """
return self._is_available
def turn_on(self, **kwargs):
""" Turn the lights on. """
if self._is_available:
if ATTR_RGB_COLOR in kwargs:
self._rgb_color = kwargs[ATTR_RGB_COLOR]
self.json_request({"command": "color", "priority": 128,
"color": self._rgb_color})
def turn_off(self, **kwargs):
""" Disconnect the remote. """
self.json_request({"command": "clearall"})
def update(self):
""" Ping the remote. """
# just see if the remote port is open
self._is_available = self.json_request()
def setup(self):
""" Get the hostname of the remote. """
response = self.json_request({"command": "serverinfo"})
if response:
self._name = response["info"]["hostname"]
return True
return False
def json_request(self, request=None, wait_for_response=False):
""" Communicate with the json server. """
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
try:
sock.connect((self._host, self._port))
except OSError:
sock.close()
return False
if not request:
# no communication needed, simple presence detection returns True
sock.close()
return True
sock.send(bytearray(json.dumps(request) + "\n", "utf-8"))
try:
buf = sock.recv(4096)
except socket.timeout:
# something is wrong, assume it's offline
sock.close()
return False
# read until a newline or timeout
buffering = True
while buffering:
if "\n" in str(buf, "utf-8"):
response = str(buf, "utf-8").split("\n")[0]
buffering = False
else:
try:
more = sock.recv(4096)
except socket.timeout:
more = None
if not more:
buffering = False
response = str(buf, "utf-8")
else:
buf += more
sock.close()
return json.loads(response)

View File

@ -2,6 +2,9 @@
homeassistant.components.light.isy994 homeassistant.components.light.isy994
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for ISY994 lights. Support for ISY994 lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/isy994/
""" """
import logging import logging

View File

@ -1,194 +1,284 @@
""" """
homeassistant.components.light.limitlessled homeassistant.components.light.limitlessled
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for LimitlessLED bulbs.
Support for LimitlessLED bulbs, also known as... For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.limitlessled/
- EasyBulb
- AppLight
- AppLamp
- MiLight
- LEDme
- dekolight
- iLight
Configuration:
To use limitlessled you will need to add the following to your
configuration.yaml file.
light:
platform: limitlessled
bridges:
- host: 192.168.1.10
group_1_name: Living Room
group_2_name: Bedroom
group_3_name: Office
group_3_type: white
group_4_name: Kitchen
- host: 192.168.1.11
group_2_name: Basement
""" """
import logging import logging
from homeassistant.const import DEVICE_DEFAULT_NAME
from homeassistant.components.light import (Light, ATTR_BRIGHTNESS, from homeassistant.components.light import (Light, ATTR_BRIGHTNESS,
ATTR_XY_COLOR) ATTR_RGB_COLOR, ATTR_EFFECT,
from homeassistant.util.color import color_RGB_to_xy ATTR_COLOR_TEMP, ATTR_TRANSITION,
ATTR_FLASH, FLASH_LONG,
EFFECT_COLORLOOP, EFFECT_WHITE)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['ledcontroller==1.1.0'] REQUIREMENTS = ['limitlessled==1.0.0']
RGB_BOUNDARY = 40
DEFAULT_TRANSITION = 0
DEFAULT_PORT = 8899
DEFAULT_VERSION = 5
DEFAULT_LED_TYPE = 'rgbw'
WHITE = [255, 255, 255]
def rewrite_legacy(config):
""" Rewrite legacy configuration to new format. """
bridges = config.get('bridges', [config])
new_bridges = []
for bridge_conf in bridges:
groups = []
if 'groups' in bridge_conf:
groups = bridge_conf['groups']
else:
_LOGGER.warning("Legacy configuration format detected")
for i in range(1, 5):
name_key = 'group_%d_name' % i
if name_key in bridge_conf:
groups.append({
'number': i,
'type': bridge_conf.get('group_%d_type' % i,
DEFAULT_LED_TYPE),
'name': bridge_conf.get(name_key)
})
new_bridges.append({
'host': bridge_conf.get('host'),
'groups': groups
})
return {'bridges': new_bridges}
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Gets the LimitlessLED lights. """ """ Gets the LimitlessLED lights. """
import ledcontroller from limitlessled.bridge import Bridge
# Handle old configuration format: # Two legacy configuration formats are supported to
bridges = config.get('bridges', [config]) # maintain backwards compatibility.
config = rewrite_legacy(config)
for bridge_id, bridge in enumerate(bridges):
bridge['id'] = bridge_id
pool = ledcontroller.LedControllerPool([x['host'] for x in bridges])
# Use the expanded configuration format.
lights = [] lights = []
for bridge in bridges: for bridge_conf in config.get('bridges'):
for i in range(1, 5): bridge = Bridge(bridge_conf.get('host'),
name_key = 'group_%d_name' % i port=bridge_conf.get('port', DEFAULT_PORT),
if name_key in bridge: version=bridge_conf.get('version', DEFAULT_VERSION))
group_type = bridge.get('group_%d_type' % i, 'rgbw') for group_conf in bridge_conf.get('groups'):
lights.append(LimitlessLED.factory(pool, bridge['id'], i, group = bridge.add_group(group_conf.get('number'),
bridge[name_key], group_conf.get('name'),
group_type)) group_conf.get('type', DEFAULT_LED_TYPE))
lights.append(LimitlessLEDGroup.factory(group))
add_devices_callback(lights) add_devices_callback(lights)
class LimitlessLED(Light): def state(new_state):
""" Represents a LimitlessLED light """ """ State decorator.
Specify True (turn on) or False (turn off).
"""
def decorator(function):
""" Decorator function. """
# pylint: disable=no-member,protected-access
def wrapper(self, **kwargs):
""" Wrap a group state change. """
from limitlessled.pipeline import Pipeline
pipeline = Pipeline()
transition_time = DEFAULT_TRANSITION
# Stop any repeating pipeline.
if self.repeating:
self.repeating = False
self.group.stop()
# Not on and should be? Turn on.
if not self.is_on and new_state is True:
pipeline.on()
# Set transition time.
if ATTR_TRANSITION in kwargs:
transition_time = kwargs[ATTR_TRANSITION]
# Do group type-specific work.
function(self, transition_time, pipeline, **kwargs)
# Update state.
self._is_on = new_state
self.group.enqueue(pipeline)
self.update_ha_state()
return wrapper
return decorator
class LimitlessLEDGroup(Light):
""" LimitessLED group. """
def __init__(self, group):
""" Initialize a group. """
self.group = group
self.repeating = False
self._is_on = False
self._brightness = None
@staticmethod @staticmethod
def factory(pool, controller_id, group, name, group_type): def factory(group):
''' Construct a Limitless LED of the appropriate type ''' """ Produce LimitlessLEDGroup objects. """
if group_type == 'white': from limitlessled.group.rgbw import RgbwGroup
return WhiteLimitlessLED(pool, controller_id, group, name) from limitlessled.group.white import WhiteGroup
elif group_type == 'rgbw': if isinstance(group, WhiteGroup):
return RGBWLimitlessLED(pool, controller_id, group, name) return LimitlessLEDWhiteGroup(group)
elif isinstance(group, RgbwGroup):
# pylint: disable=too-many-arguments return LimitlessLEDRGBWGroup(group)
def __init__(self, pool, controller_id, group, name, group_type):
self.pool = pool
self.controller_id = controller_id
self.group = group
self.pool.execute(self.controller_id, "set_group_type", self.group,
group_type)
# LimitlessLEDs don't report state, we have track it ourselves.
self.pool.execute(self.controller_id, "off", self.group)
self._name = name or DEVICE_DEFAULT_NAME
self._state = False
@property @property
def should_poll(self): def should_poll(self):
""" No polling needed. """ """ No polling needed.
LimitlessLED state cannot be fetched.
"""
return False return False
@property @property
def name(self): def name(self):
""" Returns the name of the device if any. """ """ Returns the name of the group. """
return self._name return self.group.name
@property @property
def is_on(self): def is_on(self):
""" True if device is on. """ """ True if device is on. """
return self._state return self._is_on
def turn_off(self, **kwargs):
""" Turn the device off. """
self._state = False
self.pool.execute(self.controller_id, "off", self.group)
self.update_ha_state()
class RGBWLimitlessLED(LimitlessLED):
""" Represents a RGBW LimitlessLED light """
def __init__(self, pool, controller_id, group, name):
super().__init__(pool, controller_id, group, name, 'rgbw')
self._brightness = 100
self._xy_color = color_RGB_to_xy(255, 255, 255)
# Build a color table that maps an RGB color to a color string
# recognized by LedController's set_color method
self._color_table = [(color_RGB_to_xy(*x[0]), x[1]) for x in [
((0xFF, 0xFF, 0xFF), 'white'),
((0xEE, 0x82, 0xEE), 'violet'),
((0x41, 0x69, 0xE1), 'royal_blue'),
((0x87, 0xCE, 0xFA), 'baby_blue'),
((0x00, 0xFF, 0xFF), 'aqua'),
((0x7F, 0xFF, 0xD4), 'royal_mint'),
((0x2E, 0x8B, 0x57), 'seafoam_green'),
((0x00, 0x80, 0x00), 'green'),
((0x32, 0xCD, 0x32), 'lime_green'),
((0xFF, 0xFF, 0x00), 'yellow'),
((0xDA, 0xA5, 0x20), 'yellow_orange'),
((0xFF, 0xA5, 0x00), 'orange'),
((0xFF, 0x00, 0x00), 'red'),
((0xFF, 0xC0, 0xCB), 'pink'),
((0xFF, 0x00, 0xFF), 'fusia'),
((0xDA, 0x70, 0xD6), 'lilac'),
((0xE6, 0xE6, 0xFA), 'lavendar'),
]]
@property @property
def brightness(self): def brightness(self):
""" Brightness property. """
return self._brightness return self._brightness
@state(False)
def turn_off(self, transition_time, pipeline, **kwargs):
""" Turn off a group. """
if self.is_on:
pipeline.transition(transition_time, brightness=0.0).off()
class LimitlessLEDWhiteGroup(LimitlessLEDGroup):
""" LimitlessLED White group. """
def __init__(self, group):
""" Initialize White group. """
super().__init__(group)
# Initialize group with known values.
self.group.on = True
self.group.temperature = 1.0
self.group.brightness = 0.0
self._brightness = _to_hass_brightness(1.0)
self._temperature = _to_hass_temperature(self.group.temperature)
self.group.on = False
@property @property
def color_xy(self): def color_temp(self):
return self._xy_color """ Temperature property. """
return self._temperature
def _xy_to_led_color(self, xy_color):
""" Convert an XY color to the closest LedController color string. """
def abs_dist_squared(p_0, p_1):
""" Returns the absolute value of the squared distance """
return abs((p_0[0] - p_1[0])**2 + (p_0[1] - p_1[1])**2)
candidates = [(abs_dist_squared(xy_color, x[0]), x[1]) for x in
self._color_table]
# First candidate in the sorted list is closest to desired color:
return sorted(candidates)[0][1]
def turn_on(self, **kwargs):
""" Turn the device on. """
self._state = True
@state(True)
def turn_on(self, transition_time, pipeline, **kwargs):
""" Turn on (or adjust property of) a group. """
# Check arguments.
if ATTR_BRIGHTNESS in kwargs: if ATTR_BRIGHTNESS in kwargs:
self._brightness = kwargs[ATTR_BRIGHTNESS] self._brightness = kwargs[ATTR_BRIGHTNESS]
if ATTR_COLOR_TEMP in kwargs:
if ATTR_XY_COLOR in kwargs: self._temperature = kwargs[ATTR_COLOR_TEMP]
self._xy_color = kwargs[ATTR_XY_COLOR] # Set up transition.
pipeline.transition(transition_time,
self.pool.execute(self.controller_id, "set_color", brightness=_from_hass_brightness(
self._xy_to_led_color(self._xy_color), self.group) self._brightness),
self.pool.execute(self.controller_id, "set_brightness", temperature=_from_hass_temperature(
self._brightness / 255.0, self.group) self._temperature))
self.update_ha_state()
class WhiteLimitlessLED(LimitlessLED): class LimitlessLEDRGBWGroup(LimitlessLEDGroup):
""" Represents a White LimitlessLED light """ """ LimitlessLED RGBW group. """
def __init__(self, group):
""" Initialize RGBW group. """
super().__init__(group)
# Initialize group with known values.
self.group.on = True
self.group.white()
self._color = WHITE
self.group.brightness = 0.0
self._brightness = _to_hass_brightness(1.0)
self.group.on = False
def __init__(self, pool, controller_id, group, name): @property
super().__init__(pool, controller_id, group, name, 'white') def rgb_color(self):
""" Color property. """
return self._color
def turn_on(self, **kwargs): @state(True)
""" Turn the device on. """ def turn_on(self, transition_time, pipeline, **kwargs):
self._state = True """ Turn on (or adjust property of) a group. """
self.pool.execute(self.controller_id, "on", self.group) from limitlessled.presets import COLORLOOP
self.update_ha_state() # Check arguments.
if ATTR_BRIGHTNESS in kwargs:
self._brightness = kwargs[ATTR_BRIGHTNESS]
if ATTR_RGB_COLOR in kwargs:
self._color = kwargs[ATTR_RGB_COLOR]
# White is a special case.
if min(self._color) > 256 - RGB_BOUNDARY:
pipeline.white()
self._color = WHITE
# Set up transition.
pipeline.transition(transition_time,
brightness=_from_hass_brightness(
self._brightness),
color=_from_hass_color(self._color))
# Flash.
if ATTR_FLASH in kwargs:
duration = 0
if kwargs[ATTR_FLASH] == FLASH_LONG:
duration = 1
pipeline.flash(duration=duration)
# Add effects.
if ATTR_EFFECT in kwargs:
if kwargs[ATTR_EFFECT] == EFFECT_COLORLOOP:
self.repeating = True
pipeline.append(COLORLOOP)
if kwargs[ATTR_EFFECT] == EFFECT_WHITE:
pipeline.white()
self._color = WHITE
def _from_hass_temperature(temperature):
""" Convert Home Assistant color temperature
units to percentage.
"""
return (temperature - 154) / 346
def _to_hass_temperature(temperature):
""" Convert percentage to Home Assistant
color temperature units.
"""
return int(temperature * 346) + 154
def _from_hass_brightness(brightness):
""" Convert Home Assistant brightness units
to percentage.
"""
return brightness / 255
def _to_hass_brightness(brightness):
""" Convert percentage to Home Assistant
brightness units.
"""
return int(brightness * 255)
def _from_hass_color(color):
""" Convert Home Assistant RGB list
to Color tuple.
"""
from limitlessled import Color
return Color(*tuple(color))
def _to_hass_color(color):
""" Convert from Color tuple to
Home Assistant RGB list.
"""
return list([int(c) for c in color])

View File

@ -0,0 +1,179 @@
"""
homeassistant.components.light.mqtt
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Allows to configure a MQTT light.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.mqtt/
"""
from functools import partial
import logging
import homeassistant.components.mqtt as mqtt
from homeassistant.components.light import (Light,
ATTR_BRIGHTNESS, ATTR_RGB_COLOR)
from homeassistant.util.template import render_with_possible_json_value
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'MQTT Light'
DEFAULT_QOS = 0
DEFAULT_PAYLOAD_ON = 'ON'
DEFAULT_PAYLOAD_OFF = 'OFF'
DEFAULT_OPTIMISTIC = False
DEPENDENCIES = ['mqtt']
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Add MQTT Light. """
if config.get('command_topic') is None:
_LOGGER.error("Missing required variable: command_topic")
return False
add_devices_callback([MqttLight(
hass,
config.get('name', DEFAULT_NAME),
{key: config.get(key) for key in
(typ + topic
for typ in ('', 'brightness_', 'rgb_')
for topic in ('state_topic', 'command_topic'))},
{key: config.get(key + '_value_template')
for key in ('state', 'brightness', 'rgb')},
config.get('qos', DEFAULT_QOS),
{
'on': config.get('payload_on', DEFAULT_PAYLOAD_ON),
'off': config.get('payload_off', DEFAULT_PAYLOAD_OFF)
},
config.get('optimistic', DEFAULT_OPTIMISTIC))])
class MqttLight(Light):
""" Provides a MQTT light. """
# pylint: disable=too-many-arguments,too-many-instance-attributes
def __init__(self, hass, name, topic, templates, qos, payload, optimistic):
self._hass = hass
self._name = name
self._topic = topic
self._qos = qos
self._payload = payload
self._optimistic = optimistic or topic["state_topic"] is None
self._optimistic_rgb = optimistic or topic["rgb_state_topic"] is None
self._optimistic_brightness = (optimistic or
topic["brightness_state_topic"] is None)
self._state = False
templates = {key: ((lambda value: value) if tpl is None else
partial(render_with_possible_json_value, hass, tpl))
for key, tpl in templates.items()}
def state_received(topic, payload, qos):
""" A new MQTT message has been received. """
payload = templates['state'](payload)
if payload == self._payload["on"]:
self._state = True
elif payload == self._payload["off"]:
self._state = False
self.update_ha_state()
if self._topic["state_topic"] is not None:
mqtt.subscribe(self._hass, self._topic["state_topic"],
state_received, self._qos)
def brightness_received(topic, payload, qos):
""" A new MQTT message for the brightness has been received. """
self._brightness = int(templates['brightness'](payload))
self.update_ha_state()
if self._topic["brightness_state_topic"] is not None:
mqtt.subscribe(self._hass, self._topic["brightness_state_topic"],
brightness_received, self._qos)
self._brightness = 255
else:
self._brightness = None
def rgb_received(topic, payload, qos):
""" A new MQTT message has been received. """
self._rgb = [int(val) for val in
templates['rgb'](payload).split(',')]
self.update_ha_state()
if self._topic["rgb_state_topic"] is not None:
mqtt.subscribe(self._hass, self._topic["rgb_state_topic"],
rgb_received, self._qos)
self._rgb = [255, 255, 255]
else:
self._rgb = None
@property
def brightness(self):
""" Brightness of this light between 0..255. """
return self._brightness
@property
def rgb_color(self):
""" RGB color value. """
return self._rgb
@property
def should_poll(self):
""" No polling needed for a MQTT light. """
return False
@property
def name(self):
""" Returns the name of the device if any. """
return self._name
@property
def is_on(self):
""" True if device is on. """
return self._state
def turn_on(self, **kwargs):
""" Turn the device on. """
should_update = False
if ATTR_RGB_COLOR in kwargs and \
self._topic["rgb_command_topic"] is not None:
mqtt.publish(self._hass, self._topic["rgb_command_topic"],
"{},{},{}".format(*kwargs[ATTR_RGB_COLOR]), self._qos)
if self._optimistic_rgb:
self._rgb = kwargs[ATTR_RGB_COLOR]
should_update = True
if ATTR_BRIGHTNESS in kwargs and \
self._topic["brightness_command_topic"] is not None:
mqtt.publish(self._hass, self._topic["brightness_command_topic"],
kwargs[ATTR_BRIGHTNESS], self._qos)
if self._optimistic_brightness:
self._brightness = kwargs[ATTR_BRIGHTNESS]
should_update = True
mqtt.publish(self._hass, self._topic["command_topic"],
self._payload["on"], self._qos)
if self._optimistic:
# optimistically assume that switch has changed state
self._state = True
should_update = True
if should_update:
self.update_ha_state()
def turn_off(self, **kwargs):
""" Turn the device off. """
mqtt.publish(self._hass, self._topic["command_topic"],
self._payload["off"], self._qos)
if self._optimistic:
# optimistically assume that switch has changed state
self._state = False
self.update_ha_state()

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