Merge pull request #778 from balloob/dev

0.10.0.rc1
This commit is contained in:
Paulus Schoutsen 2015-12-20 15:10:28 -08:00
commit 7ee71b1831
71 changed files with 2806 additions and 507 deletions

View File

@ -33,32 +33,34 @@ omit =
homeassistant/components/*/rfxtrx.py homeassistant/components/*/rfxtrx.py
homeassistant/components/binary_sensor/arest.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/geofancy.py homeassistant/components/device_tracker/geofancy.py
homeassistant/components/device_tracker/icloud.py
homeassistant/components/device_tracker/luci.py homeassistant/components/device_tracker/luci.py
homeassistant/components/device_tracker/ubus.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/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/ifttt.py
homeassistant/components/influxdb.py homeassistant/components/influxdb.py
homeassistant/components/keyboard.py homeassistant/components/keyboard.py
homeassistant/components/light/hue.py
homeassistant/components/light/mqtt.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/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
@ -66,9 +68,8 @@ 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
@ -81,10 +82,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/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
@ -96,10 +98,11 @@ 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/mystrom.py
@ -108,6 +111,7 @@ omit =
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/heatmiser.py
homeassistant/components/thermostat/homematic.py homeassistant/components/thermostat/homematic.py
homeassistant/components/thermostat/honeywell.py homeassistant/components/thermostat/honeywell.py
homeassistant/components/thermostat/nest.py homeassistant/components/thermostat/nest.py

View File

@ -2,11 +2,16 @@ sudo: false
language: python language: python
cache: cache:
directories: directories:
- $HOME/virtualenv/python$TRAVIS_PYTHON_VERSION/ - $HOME/.cache/pip
# - "$HOME/virtualenv/python$TRAVIS_PYTHON_VERSION"
python: python:
- 3.4.2 - 3.4
- 3.5.0 - 3.5
install: install:
# Validate requirements_all.txt on Python 3.5
- if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then python3 setup.py develop; script/gen_requirements_all.py validate; fi
- script/bootstrap_server - script/bootstrap_server
script: script:
- script/cibuild - script/cibuild
matrix:
fast_finish: true

View File

@ -17,8 +17,7 @@ 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:
- Add a link to the website of your device/service/component in the "examples" listing of 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` if needed. There is no ordering right now, so just add it to the end of the file.
- Update the `.coveragerc` file to exclude your platform if there are no tests available. - Update the `.coveragerc` file to exclude your platform if there are no tests available.
- 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/). 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).
- 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 check your repository, run `./script/lint`.

View File

@ -275,7 +275,7 @@ 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.NOTSET) # this sets the minimum log level
else: else:
_LOGGER.error( _LOGGER.error(

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()}
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

@ -12,16 +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.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_ERROR_LOG, URL_API_LOG_OUT, 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'
@ -91,6 +94,9 @@ def setup(hass, config):
hass.http.register_path('POST', URL_API_LOG_OUT, _handle_post_api_log_out) 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
@ -120,22 +126,23 @@ 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()
handler.server.sessions.extend_validation(session_id) except (IOError, ValueError):
except IOError: # 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)
@ -143,7 +150,11 @@ def _handle_get_api_stream(handler, path_match, data):
session_id = handler.set_session_cookie_header() session_id = handler.set_session_cookie_header()
handler.end_headers() handler.end_headers()
hass.bus.listen(MATCH_ALL, forward_events) if restrict:
for event in restrict:
hass.bus.listen(event, forward_events)
else:
hass.bus.listen(MATCH_ALL, forward_events)
while True: while True:
write_message(STREAM_PING_PAYLOAD) write_message(STREAM_PING_PAYLOAD)
@ -157,7 +168,11 @@ 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])
hass.bus.remove_listener(MATCH_ALL, forward_events) if restrict:
for event in restrict:
hass.bus.remove_listener(event, forward_events)
else:
hass.bus.remove_listener(MATCH_ALL, forward_events)
def _handle_get_api_config(handler, path_match, data): def _handle_get_api_config(handler, path_match, data):
@ -359,6 +374,22 @@ def _handle_post_api_log_out(handler, path_match, data):
handler.end_headers() 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

@ -8,13 +8,14 @@ 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"
CONF_BELOW = "below" CONF_BELOW = "below"
CONF_ABOVE = "above" CONF_ABOVE = "above"
CONF_ATTRIBUTE = "attribute"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -29,7 +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)
attribute = config.get(CONF_ATTRIBUTE) 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."
@ -37,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, above, below, attribute) and \ if _in_range(above, below, renderer(to_s)) and \
(from_s is None or not _in_range(from_s, above, below, attribute)): (from_s is None or not _in_range(above, below, renderer(from_s))):
action() action()
track_state_change( track_state_change(
@ -63,7 +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)
attribute = config.get(CONF_ATTRIBUTE) 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."
@ -71,22 +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, above, below, attribute) return state is not None and _in_range(above, below, renderer(state))
return if_numeric_state return if_numeric_state
def _in_range(state, range_start, range_end, attribute): def _in_range(range_start, range_end, value):
""" Checks if value is inside the range """ """ Checks if value is inside the range """
value = (state.state if attribute is None
else state.attributes.get(attribute))
try: try:
value = float(value) value = float(value)
except ValueError: except ValueError:
_LOGGER.warning("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

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

@ -7,7 +7,10 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.mqtt/ https://home-assistant.io/components/binary_sensor.mqtt/
""" """
import logging import logging
from homeassistant.const import CONF_VALUE_TEMPLATE
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.util import template
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -34,13 +37,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
config.get('state_topic', None), config.get('state_topic', None),
config.get('qos', DEFAULT_QOS), config.get('qos', DEFAULT_QOS),
config.get('payload_on', DEFAULT_PAYLOAD_ON), config.get('payload_on', DEFAULT_PAYLOAD_ON),
config.get('payload_off', DEFAULT_PAYLOAD_OFF))]) config.get('payload_off', DEFAULT_PAYLOAD_OFF),
config.get(CONF_VALUE_TEMPLATE))])
# pylint: disable=too-many-arguments, too-many-instance-attributes # pylint: disable=too-many-arguments, too-many-instance-attributes
class MqttBinarySensor(BinarySensorDevice): class MqttBinarySensor(BinarySensorDevice):
""" Represents a binary sensor that is updated by MQTT. """ """ Represents a binary sensor that is updated by MQTT. """
def __init__(self, hass, name, state_topic, qos, payload_on, payload_off): def __init__(self, hass, name, state_topic, qos, payload_on, payload_off,
value_template):
self._hass = hass self._hass = hass
self._name = name self._name = name
self._state = False self._state = False
@ -51,6 +56,9 @@ class MqttBinarySensor(BinarySensorDevice):
def message_received(topic, payload, qos): def message_received(topic, payload, qos):
""" A new MQTT message has been received. """ """ 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: if payload == self._payload_on:
self._state = True self._state = True
self.update_ha_state() self.update_ha_state()

View File

@ -0,0 +1,145 @@
"""
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:// 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

@ -9,6 +9,7 @@ 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)
@ -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

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

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

@ -11,7 +11,6 @@ import logging
from . import version, mdi_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,7 @@ _LOGGER = logging.getLogger(__name__)
FRONTEND_URLS = [ FRONTEND_URLS = [
URL_ROOT, '/logbook', '/history', '/map', '/devService', '/devState', URL_ROOT, '/logbook', '/history', '/map', '/devService', '/devState',
'/devEvent', '/devInfo'] '/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) _FINGERPRINT = re.compile(r'^(\w+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
@ -37,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(
@ -78,6 +77,17 @@ def _handle_get_root(handler, path_match, data):
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'))
@ -98,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,5 +1,5 @@
<!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>
@ -13,7 +13,7 @@
<meta name='viewport' content='width=device-width, user-scalable=no'> <meta name='viewport' content='width=device-width, 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;
@ -31,11 +31,21 @@
margin-bottom: 123px; margin-bottom: 123px;
} }
</style> </style>
<link rel='import' href='/static/{{ app_url }}' async>
</head> </head>
<body fullbleed> <body fullbleed>
<div id='init'><img src='/static/favicon-192x192.png' height='192'></div> <div id='ha-init-skeleton'><img src='/static/favicon-192x192.png' height='192'></div>
<script src='/static/webcomponents-lite.min.js'></script> <script>
<link rel='import' href='/static/{{ app_url }}' /> var webComponentsSupported = ('registerElement' in document &&
'import' in document.createElement('link') &&
'content' in document.createElement('template'))
if (!webComponentsSupported) {
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> <home-assistant auth='{{ auth }}' icons='{{ icons }}'></home-assistant>
</body> </body>
</html> </html>

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 = "d07b7ed1734ae3f2472f9ae88e0c3dea" VERSION = "be08c5a3ce12040bbdba2db83cb1a568"

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit 044a6d9810b6aa662b82af0f67deb662725ad4cb Subproject commit 50aadaf880a9cb36bf144540171ff5fa029e9eaf

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

File diff suppressed because one or more lines are too long

View File

@ -178,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))
@ -363,13 +359,13 @@ class RequestHandler(SimpleHTTPRequestHandler):
def set_session_cookie_header(self): def set_session_cookie_header(self):
""" Add the header for the session cookie and return session id. """ """ Add the header for the session cookie and return session id. """
if not self.authenticated: if not self.authenticated:
return return None
session_id = self.get_cookie_session_id() session_id = self.get_cookie_session_id()
if session_id is not None: if session_id is not None:
self.server.sessions.extend_validation(session_id) self.server.sessions.extend_validation(session_id)
return return session_id
self.send_header( self.send_header(
'Set-Cookie', 'Set-Cookie',
@ -426,10 +422,10 @@ def session_valid_time():
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.lock = threading.RLock() self._lock = threading.RLock()
@util.Throttle(SESSION_CLEAR_INTERVAL) @util.Throttle(SESSION_CLEAR_INTERVAL)
def _remove_expired(self): def _remove_expired(self):
@ -441,7 +437,7 @@ class SessionStore(object):
def is_valid(self, key): def is_valid(self, key):
""" Return True if a valid session is given. """ """ Return True if a valid session is given. """
with self.lock: with self._lock:
self._remove_expired() self._remove_expired()
return (key in self._sessions and return (key in self._sessions and
@ -449,17 +445,19 @@ class SessionStore(object):
def extend_validation(self, key): def extend_validation(self, key):
""" Extend a session validation time. """ """ Extend a session validation time. """
with self.lock: with self._lock:
if key not in self._sessions:
return
self._sessions[key] = session_valid_time() self._sessions[key] = session_valid_time()
def destroy(self, key): def destroy(self, key):
""" Destroy a session by key. """ """ Destroy a session by key. """
with self.lock: with self._lock:
self._sessions.pop(key, None) self._sessions.pop(key, None)
def create(self): def create(self):
""" Creates a new session. """ """ Creates a new session. """
with self.lock: with self._lock:
session_id = util.get_random_string(20) session_id = util.get_random_string(20)
while session_id in self._sessions: while session_id in self._sessions:

View File

@ -1,19 +1,10 @@
""" """
homeassistant.components.influx homeassistant.components.influxdb
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
InfluxDB component which allows you to send data to an Influx database. InfluxDB component which allows you to send data to an Influx database.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/influx/ https://home-assistant.io/components/influxdb/
Configuration:
influx:
host: localhost
port: 8086
dbname: home_assistant
dbuser: DB_USER
dbuser_password: DB_USER_PASSWORD
""" """
import logging import logging
@ -26,7 +17,7 @@ from homeassistant.components.sun import (STATE_ABOVE_HORIZON,
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = "influx" DOMAIN = "influxdb"
DEPENDENCIES = [] DEPENDENCIES = []
DEFAULT_HOST = 'localhost' DEFAULT_HOST = 'localhost'
@ -43,7 +34,7 @@ CONF_PASSWORD = 'password'
def setup(hass, config): def setup(hass, config):
""" Setup the Influx component. """ """ Setup the InfluxDB component. """
from influxdb import InfluxDBClient, exceptions from influxdb import InfluxDBClient, exceptions
@ -54,21 +45,21 @@ def setup(hass, config):
host = conf[CONF_HOST] host = conf[CONF_HOST]
port = util.convert(conf.get(CONF_PORT), int, DEFAULT_PORT) port = util.convert(conf.get(CONF_PORT), int, DEFAULT_PORT)
dbname = util.convert(conf.get(CONF_DB_NAME), str, DEFAULT_DATABASE) database = util.convert(conf.get(CONF_DB_NAME), str, DEFAULT_DATABASE)
username = util.convert(conf.get(CONF_USERNAME), str) username = util.convert(conf.get(CONF_USERNAME), str)
password = util.convert(conf.get(CONF_PASSWORD), str) password = util.convert(conf.get(CONF_PASSWORD), str)
try: try:
influx = InfluxDBClient(host=host, port=port, username=username, influx = InfluxDBClient(host=host, port=port, username=username,
password=password, database=dbname) password=password, database=database)
databases = [i['name'] for i in influx.get_list_database()] databases = [i['name'] for i in influx.get_list_database()]
except exceptions.InfluxDBClientError: except exceptions.InfluxDBClientError:
_LOGGER.error("Database host is not accessible. " _LOGGER.error("Database host is not accessible. "
"Please check your entries in the configuration file.") "Please check your entries in the configuration file.")
return False return False
if dbname not in databases: if database not in databases:
_LOGGER.error("Database %s doesn't exist", dbname) _LOGGER.error("Database %s doesn't exist", database)
return False return False
def influx_event_listener(event): def influx_event_listener(event):
@ -106,7 +97,7 @@ def setup(hass, config):
try: try:
influx.write_points(json_body) influx.write_points(json_body)
except exceptions.InfluxDBClientError: except exceptions.InfluxDBClientError:
_LOGGER.exception('Error saving event to Influx') _LOGGER.exception('Error saving event to InfluxDB')
hass.bus.listen(EVENT_STATE_CHANGED, influx_event_listener) hass.bus.listen(EVENT_STATE_CHANGED, influx_event_listener)

View File

@ -6,24 +6,24 @@ Allows to configure a MQTT light.
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.mqtt/ https://home-assistant.io/components/light.mqtt/
""" """
from functools import partial
import logging import logging
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.components.light import (Light, from homeassistant.components.light import (Light,
ATTR_BRIGHTNESS, ATTR_RGB_COLOR) ATTR_BRIGHTNESS, ATTR_RGB_COLOR)
from homeassistant.util.template import render_with_possible_json_value
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "MQTT Light" DEFAULT_NAME = 'MQTT Light'
DEFAULT_QOS = 0 DEFAULT_QOS = 0
DEFAULT_PAYLOAD_ON = "on" DEFAULT_PAYLOAD_ON = 'ON'
DEFAULT_PAYLOAD_OFF = "off" DEFAULT_PAYLOAD_OFF = 'OFF'
DEFAULT_OPTIMISTIC = False DEFAULT_OPTIMISTIC = False
DEPENDENCIES = ['mqtt'] DEPENDENCIES = ['mqtt']
# 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 MQTT Light. """ """ Add MQTT Light. """
@ -35,18 +35,16 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
add_devices_callback([MqttLight( add_devices_callback([MqttLight(
hass, hass,
config.get('name', DEFAULT_NAME), config.get('name', DEFAULT_NAME),
{ {key: config.get(key) for key in
"state_topic": config.get('state_topic'), (typ + topic
"command_topic": config.get('command_topic'), for typ in ('', 'brightness_', 'rgb_')
"brightness_state_topic": config.get('brightness_state_topic'), for topic in ('state_topic', 'command_topic'))},
"brightness_command_topic": config.get('brightness_command_topic'), {key: config.get(key + '_value_template')
"rgb_state_topic": config.get('rgb_state_topic'), for key in ('state', 'brightness', 'rgb')},
"rgb_command_topic": config.get('rgb_command_topic')
},
config.get('qos', DEFAULT_QOS), config.get('qos', DEFAULT_QOS),
{ {
"on": config.get('payload_on', DEFAULT_PAYLOAD_ON), 'on': config.get('payload_on', DEFAULT_PAYLOAD_ON),
"off": config.get('payload_off', DEFAULT_PAYLOAD_OFF) 'off': config.get('payload_off', DEFAULT_PAYLOAD_OFF)
}, },
config.get('optimistic', DEFAULT_OPTIMISTIC))]) config.get('optimistic', DEFAULT_OPTIMISTIC))])
@ -55,7 +53,7 @@ class MqttLight(Light):
""" Provides a MQTT light. """ """ Provides a MQTT light. """
# pylint: disable=too-many-arguments,too-many-instance-attributes # pylint: disable=too-many-arguments,too-many-instance-attributes
def __init__(self, hass, name, topic, qos, payload, optimistic): def __init__(self, hass, name, topic, templates, qos, payload, optimistic):
self._hass = hass self._hass = hass
self._name = name self._name = name
@ -68,8 +66,13 @@ class MqttLight(Light):
topic["brightness_state_topic"] is None) topic["brightness_state_topic"] is None)
self._state = False 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): def state_received(topic, payload, qos):
""" A new MQTT message has been received. """ """ A new MQTT message has been received. """
payload = templates['state'](payload)
if payload == self._payload["on"]: if payload == self._payload["on"]:
self._state = True self._state = True
elif payload == self._payload["off"]: elif payload == self._payload["off"]:
@ -83,7 +86,7 @@ class MqttLight(Light):
def brightness_received(topic, payload, qos): def brightness_received(topic, payload, qos):
""" A new MQTT message for the brightness has been received. """ """ A new MQTT message for the brightness has been received. """
self._brightness = int(payload) self._brightness = int(templates['brightness'](payload))
self.update_ha_state() self.update_ha_state()
if self._topic["brightness_state_topic"] is not None: if self._topic["brightness_state_topic"] is not None:
@ -95,7 +98,8 @@ class MqttLight(Light):
def rgb_received(topic, payload, qos): def rgb_received(topic, payload, qos):
""" A new MQTT message has been received. """ """ A new MQTT message has been received. """
self._rgb = [int(val) for val in payload.split(',')] self._rgb = [int(val) for val in
templates['rgb'](payload).split(',')]
self.update_ha_state() self.update_ha_state()
if self._topic["rgb_state_topic"] is not None: if self._topic["rgb_state_topic"] is not None:

View File

@ -12,9 +12,7 @@ from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.components.wink import WinkToggleDevice from homeassistant.components.wink import WinkToggleDevice
from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.const import CONF_ACCESS_TOKEN
REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' REQUIREMENTS = ['python-wink==0.3.1']
'42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip'
'#python-wink==0.2']
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
@ -45,10 +43,10 @@ class WinkLight(WinkToggleDevice):
brightness = kwargs.get(ATTR_BRIGHTNESS) brightness = kwargs.get(ATTR_BRIGHTNESS)
if brightness is not None: if brightness is not None:
self.wink.setState(True, brightness / 255) self.wink.set_state(True, brightness=brightness / 255)
else: else:
self.wink.setState(True) self.wink.set_state(True)
@property @property
def state_attributes(self): def state_attributes(self):

View File

@ -11,9 +11,7 @@ import logging
from homeassistant.components.lock import LockDevice from homeassistant.components.lock import LockDevice
from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.const import CONF_ACCESS_TOKEN
REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' REQUIREMENTS = ['python-wink==0.3.1']
'42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip'
'#python-wink==0.2']
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
@ -43,7 +41,7 @@ class WinkLockDevice(LockDevice):
@property @property
def unique_id(self): def unique_id(self):
""" Returns the id of this wink lock """ """ Returns the id of this wink lock """
return "{}.{}".format(self.__class__, self.wink.deviceId()) return "{}.{}".format(self.__class__, self.wink.device_id())
@property @property
def name(self): def name(self):
@ -52,7 +50,7 @@ class WinkLockDevice(LockDevice):
def update(self): def update(self):
""" Update the state of the lock. """ """ Update the state of the lock. """
self.wink.updateState() self.wink.update_state()
@property @property
def is_locked(self): def is_locked(self):
@ -61,8 +59,8 @@ class WinkLockDevice(LockDevice):
def lock(self): def lock(self):
""" Lock the device. """ """ Lock the device. """
self.wink.setState(True) self.wink.set_state(True)
def unlock(self): def unlock(self):
""" Unlock the device. """ """ Unlock the device. """
self.wink.setState(False) self.wink.set_state(False)

View File

@ -6,6 +6,7 @@ Provides functionality to interact with Cast devices on the network.
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/media_player.cast/ https://home-assistant.io/components/media_player.cast/
""" """
# pylint: disable=import-error
import logging import logging
from homeassistant.const import ( from homeassistant.const import (
@ -15,16 +16,16 @@ from homeassistant.const import (
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
MediaPlayerDevice, MediaPlayerDevice,
SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_YOUTUBE, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_YOUTUBE, SUPPORT_PLAY_MEDIA,
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO) MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO)
REQUIREMENTS = ['pychromecast==0.6.12'] REQUIREMENTS = ['pychromecast==0.6.13']
CONF_IGNORE_CEC = 'ignore_cec' CONF_IGNORE_CEC = 'ignore_cec'
CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png' CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png'
SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_YOUTUBE SUPPORT_NEXT_TRACK | SUPPORT_YOUTUBE | SUPPORT_PLAY_MEDIA
KNOWN_HOSTS = [] KNOWN_HOSTS = []
# pylint: disable=invalid-name # pylint: disable=invalid-name
@ -261,6 +262,10 @@ class CastDevice(MediaPlayerDevice):
""" Seek the media to a specific location. """ """ Seek the media to a specific location. """
self.cast.media_controller.seek(position) self.cast.media_controller.seek(position)
def play_media(self, media_type, media_id):
""" Plays media from a URL """
self.cast.media_controller.play_media(media_id, media_type)
def play_youtube(self, media_id): def play_youtube(self, media_id):
""" Plays a YouTube media. """ """ Plays a YouTube media. """
self.youtube.play_video(media_id) self.youtube.play_video(media_id)

View File

@ -21,14 +21,14 @@ from homeassistant.const import (
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
MediaPlayerDevice, MediaPlayerDevice,
SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_TURN_OFF, SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_TURN_OFF,
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, SUPPORT_TURN_ON, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
MEDIA_TYPE_MUSIC) MEDIA_TYPE_MUSIC)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['python-mpd2==0.5.4'] REQUIREMENTS = ['python-mpd2==0.5.4']
SUPPORT_MPD = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \ SUPPORT_MPD = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK SUPPORT_TURN_ON | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
# pylint: disable=unused-argument # pylint: disable=unused-argument
@ -141,7 +141,13 @@ class MpdDevice(MediaPlayerDevice):
@property @property
def media_title(self): def media_title(self):
""" Title of current playing media. """ """ Title of current playing media. """
return self.currentsong['title'] name = self.currentsong.get('name', None)
title = self.currentsong['title']
if name is None:
return title
else:
return '{}: {}'.format(name, title)
@property @property
def media_artist(self): def media_artist(self):
@ -163,9 +169,13 @@ class MpdDevice(MediaPlayerDevice):
return SUPPORT_MPD return SUPPORT_MPD
def turn_off(self): def turn_off(self):
""" Service to exit the running MPD. """ """ Service to send the MPD the command to stop playing. """
self.client.stop() self.client.stop()
def turn_on(self):
""" Service to send the MPD the command to start playing. """
self.client.play()
def set_volume_level(self, volume): def set_volume_level(self, volume):
""" Sets volume """ """ Sets volume """
self.client.setvol(int(volume * 100)) self.client.setvol(int(volume * 100))

View File

@ -6,7 +6,6 @@ MQTT component, using paho-mqtt.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/mqtt/ https://home-assistant.io/components/mqtt/
""" """
import json
import logging import logging
import os import os
import socket import socket
@ -33,7 +32,7 @@ DEFAULT_RETAIN = False
SERVICE_PUBLISH = 'publish' SERVICE_PUBLISH = 'publish'
EVENT_MQTT_MESSAGE_RECEIVED = 'MQTT_MESSAGE_RECEIVED' EVENT_MQTT_MESSAGE_RECEIVED = 'MQTT_MESSAGE_RECEIVED'
REQUIREMENTS = ['paho-mqtt==1.1', 'jsonpath-rw==1.4.0'] REQUIREMENTS = ['paho-mqtt==1.1']
CONF_BROKER = 'broker' CONF_BROKER = 'broker'
CONF_PORT = 'port' CONF_PORT = 'port'
@ -136,33 +135,6 @@ def setup(hass, config):
return True return True
# pylint: disable=too-few-public-methods
class _JsonFmtParser(object):
""" Implements a JSON parser on xpath. """
def __init__(self, jsonpath):
import jsonpath_rw
self._expr = jsonpath_rw.parse(jsonpath)
def __call__(self, payload):
match = self._expr.find(json.loads(payload))
return match[0].value if len(match) > 0 else payload
# pylint: disable=too-few-public-methods
class FmtParser(object):
""" Wrapper for all supported formats. """
def __init__(self, fmt):
self._parse = lambda x: x
if fmt:
if fmt.startswith('json:'):
self._parse = _JsonFmtParser(fmt[5:])
def __call__(self, payload):
return self._parse(payload)
# This is based on one of the paho-mqtt examples:
# http://git.eclipse.org/c/paho/org.eclipse.paho.mqtt.python.git/tree/examples/sub-class.py
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
class MQTT(object): class MQTT(object):
""" Implements messaging service for MQTT. """ """ Implements messaging service for MQTT. """

View File

@ -13,6 +13,7 @@ import os
import homeassistant.bootstrap as bootstrap import homeassistant.bootstrap as bootstrap
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.helpers import config_per_platform from homeassistant.helpers import config_per_platform
from homeassistant.util import template
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
@ -33,9 +34,16 @@ SERVICE_NOTIFY = "notify"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def send_message(hass, message): def send_message(hass, message, title=None):
""" Send a notification message. """ """ Send a notification message. """
hass.services.call(DOMAIN, SERVICE_NOTIFY, {ATTR_MESSAGE: message}) data = {
ATTR_MESSAGE: message
}
if title is not None:
data[ATTR_TITLE] = title
hass.services.call(DOMAIN, SERVICE_NOTIFY, data)
def setup(hass, config): def setup(hass, config):
@ -70,8 +78,10 @@ def setup(hass, config):
if message is None: if message is None:
return return
title = call.data.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) title = template.render(
hass, call.data.get(ATTR_TITLE, ATTR_TITLE_DEFAULT))
target = call.data.get(ATTR_TARGET) target = call.data.get(ATTR_TARGET)
message = template.render(hass, message)
notify_service.send_message(message, title=title, target=target) notify_service.send_message(message, title=title, target=target)

View File

@ -1,7 +1,7 @@
""" """
homeassistant.components.rollershutter.demo homeassistant.components.rollershutter.demo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Demo platform for rollorshutter component. Demo platform for the rollorshutter component.
""" """
from homeassistant.const import EVENT_TIME_CHANGED from homeassistant.const import EVENT_TIME_CHANGED
from homeassistant.helpers.event import track_utc_time_change from homeassistant.helpers.event import track_utc_time_change
@ -9,7 +9,7 @@ from homeassistant.components.rollershutter import RollershutterDevice
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the Demo binary sensors. """ """ Sets up the Demo rollershutters. """
add_devices([ add_devices([
DemoRollershutter(hass, 'Kitchen Window', 0), DemoRollershutter(hass, 'Kitchen Window', 0),
DemoRollershutter(hass, 'Living Room Window', 100), DemoRollershutter(hass, 'Living Room Window', 100),
@ -17,7 +17,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class DemoRollershutter(RollershutterDevice): class DemoRollershutter(RollershutterDevice):
""" Represents a rollershutter within Home Assistant. """ """ Represents a rollershutter.. """
# pylint: disable=no-self-use # pylint: disable=no-self-use
def __init__(self, hass, name, position): def __init__(self, hass, name, position):
@ -29,14 +29,17 @@ class DemoRollershutter(RollershutterDevice):
@property @property
def name(self): def name(self):
""" Returns the name of the rollershutter. """
return self._name return self._name
@property @property
def should_poll(self): def should_poll(self):
""" No polling needed for a demo rollershutter. """
return False return False
@property @property
def current_position(self): def current_position(self):
""" Returns the current position of the rollershutter. """
return self._position return self._position
def move_up(self, **kwargs): def move_up(self, **kwargs):
@ -62,11 +65,13 @@ class DemoRollershutter(RollershutterDevice):
self._listener = None self._listener = None
def _listen(self): def _listen(self):
""" Listens for changes. """
if self._listener is None: if self._listener is None:
self._listener = track_utc_time_change(self.hass, self._listener = track_utc_time_change(self.hass,
self._time_changed) self._time_changed)
def _time_changed(self, now): def _time_changed(self, now):
""" Track time changes. """
if self._moving_up: if self._moving_up:
self._position -= 10 self._position -= 10
else: else:

View File

@ -8,7 +8,10 @@ https://home-assistant.io/components/rollershutter.mqtt/
""" """
import logging import logging
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.const import CONF_VALUE_TEMPLATE
from homeassistant.components.rollershutter import RollershutterDevice from homeassistant.components.rollershutter import RollershutterDevice
from homeassistant.util import template
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['mqtt'] DEPENDENCIES = ['mqtt']
@ -36,14 +39,14 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
config.get('payload_up', DEFAULT_PAYLOAD_UP), config.get('payload_up', DEFAULT_PAYLOAD_UP),
config.get('payload_down', DEFAULT_PAYLOAD_DOWN), config.get('payload_down', DEFAULT_PAYLOAD_DOWN),
config.get('payload_stop', DEFAULT_PAYLOAD_STOP), config.get('payload_stop', DEFAULT_PAYLOAD_STOP),
config.get('state_format'))]) config.get(CONF_VALUE_TEMPLATE))])
# pylint: disable=too-many-arguments, too-many-instance-attributes # pylint: disable=too-many-arguments, too-many-instance-attributes
class MqttRollershutter(RollershutterDevice): class MqttRollershutter(RollershutterDevice):
""" Represents a rollershutter that can be controlled using MQTT. """ """ Represents a rollershutter that can be controlled using MQTT. """
def __init__(self, hass, name, state_topic, command_topic, qos, def __init__(self, hass, name, state_topic, command_topic, qos,
payload_up, payload_down, payload_stop, state_format): payload_up, payload_down, payload_stop, value_template):
self._state = None self._state = None
self._hass = hass self._hass = hass
self._name = name self._name = name
@ -53,16 +56,17 @@ class MqttRollershutter(RollershutterDevice):
self._payload_up = payload_up self._payload_up = payload_up
self._payload_down = payload_down self._payload_down = payload_down
self._payload_stop = payload_stop self._payload_stop = payload_stop
self._parse = mqtt.FmtParser(state_format)
if self._state_topic is None: if self._state_topic is None:
return return
def message_received(topic, payload, qos): def message_received(topic, payload, qos):
""" A new MQTT message has been received. """ """ A new MQTT message has been received. """
value = self._parse(payload) if value_template is not None:
if value.isnumeric() and 0 <= int(value) <= 100: payload = template.render_with_possible_json_value(
self._state = int(value) hass, value_template, payload)
if payload.isnumeric() and 0 <= int(payload) <= 100:
self._state = int(payload)
self.update_ha_state() self.update_ha_state()
else: else:
_LOGGER.warning( _LOGGER.warning(

View File

@ -11,9 +11,11 @@ import logging
import requests import requests
from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, \
DEVICE_DEFAULT_NAME
from homeassistant.exceptions import TemplateError
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle from homeassistant.util import template, Throttle
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -50,36 +52,50 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
arest = ArestData(resource) arest = ArestData(resource)
def make_renderer(value_template):
""" Creates renderer based on variable_template value """
if value_template is None:
return lambda value: value
def _render(value):
try:
return template.render(hass, value_template, {'value': value})
except TemplateError:
_LOGGER.exception('Error parsing value')
return value
return _render
dev = [] dev = []
if var_conf is not None: if var_conf is not None:
for variable in config['monitored_variables']: for variable in var_conf:
if variable['name'] not in response['variables']: if variable['name'] not in response['variables']:
_LOGGER.error('Variable: "%s" does not exist', _LOGGER.error('Variable: "%s" does not exist',
variable['name']) variable['name'])
continue continue
renderer = make_renderer(variable.get(CONF_VALUE_TEMPLATE))
dev.append(ArestSensor(arest, dev.append(ArestSensor(arest,
resource, resource,
config.get('name', response['name']), config.get('name', response['name']),
variable['name'], variable['name'],
variable=variable['name'], variable=variable['name'],
unit_of_measurement=variable.get( unit_of_measurement=variable.get(
'unit_of_measurement'))) ATTR_UNIT_OF_MEASUREMENT),
renderer=renderer))
if pins is not None: if pins is not None:
for pinnum, pin in pins.items(): for pinnum, pin in pins.items():
renderer = make_renderer(pin.get(CONF_VALUE_TEMPLATE))
dev.append(ArestSensor(ArestData(resource, pinnum), dev.append(ArestSensor(ArestData(resource, pinnum),
resource, resource,
config.get('name', response['name']), config.get('name', response['name']),
pin.get('name'), pin.get('name'),
pin=pinnum, pin=pinnum,
unit_of_measurement=pin.get( unit_of_measurement=pin.get(
'unit_of_measurement'), ATTR_UNIT_OF_MEASUREMENT),
corr_factor=pin.get('correction_factor', renderer=renderer))
None),
decimal_places=pin.get('decimal_places',
None)))
add_devices(dev) add_devices(dev)
@ -89,8 +105,7 @@ class ArestSensor(Entity):
""" Implements an aREST sensor for exposed variables. """ """ Implements an aREST sensor for exposed variables. """
def __init__(self, arest, resource, location, name, variable=None, def __init__(self, arest, resource, location, name, variable=None,
pin=None, unit_of_measurement=None, corr_factor=None, pin=None, unit_of_measurement=None, renderer=None):
decimal_places=None):
self.arest = arest self.arest = arest
self._resource = resource self._resource = resource
self._name = '{} {}'.format(location.title(), name.title()) \ self._name = '{} {}'.format(location.title(), name.title()) \
@ -99,8 +114,7 @@ class ArestSensor(Entity):
self._pin = pin self._pin = pin
self._state = 'n/a' self._state = 'n/a'
self._unit_of_measurement = unit_of_measurement self._unit_of_measurement = unit_of_measurement
self._corr_factor = corr_factor self._renderer = renderer
self._decimal_places = decimal_places
self.update() self.update()
if self._pin is not None: if self._pin is not None:
@ -126,18 +140,11 @@ class ArestSensor(Entity):
if 'error' in values: if 'error' in values:
return values['error'] return values['error']
elif 'value' in values:
if self._corr_factor is not None \ value = self._renderer(values.get('value',
and self._decimal_places is not None: values.get(self._variable,
return round((float(values['value']) * 'N/A')))
float(self._corr_factor)), self._decimal_places) return value
elif self._corr_factor is not None \
and self._decimal_places is None:
return round(float(values['value']) * float(self._corr_factor))
else:
return values['value']
else:
return values.get(self._variable, 'n/a')
def update(self): def update(self):
""" Gets the latest data from aREST API. """ """ Gets the latest data from aREST API. """

View File

@ -10,8 +10,9 @@ import logging
import subprocess import subprocess
from datetime import timedelta from datetime import timedelta
from homeassistant.const import CONF_VALUE_TEMPLATE
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle from homeassistant.util import template, Throttle
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -32,25 +33,24 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
data = CommandSensorData(config.get('command')) data = CommandSensorData(config.get('command'))
add_devices_callback([CommandSensor( add_devices_callback([CommandSensor(
hass,
data, data,
config.get('name', DEFAULT_NAME), config.get('name', DEFAULT_NAME),
config.get('unit_of_measurement'), config.get('unit_of_measurement'),
config.get('correction_factor', 1.0), config.get(CONF_VALUE_TEMPLATE)
config.get('decimal_places', 0)
)]) )])
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
class CommandSensor(Entity): class CommandSensor(Entity):
""" Represents a sensor that is returning a value of a shell commands. """ """ Represents a sensor that is returning a value of a shell commands. """
def __init__(self, data, name, unit_of_measurement, corr_factor, def __init__(self, hass, data, name, unit_of_measurement, value_template):
decimal_places): self._hass = hass
self.data = data self.data = data
self._name = name self._name = name
self._state = False self._state = False
self._unit_of_measurement = unit_of_measurement self._unit_of_measurement = unit_of_measurement
self._corr_factor = float(corr_factor) self._value_template = value_template
self._decimal_places = decimal_places
self.update() self.update()
@property @property
@ -73,14 +73,10 @@ class CommandSensor(Entity):
self.data.update() self.data.update()
value = self.data.value value = self.data.value
try: if self._value_template is not None:
if value is not None: self._state = template.render_with_possible_json_value(
if self._corr_factor is not None: self._hass, self._value_template, value, 'N/A')
self._state = round((float(value) * self._corr_factor), else:
self._decimal_places)
else:
self._state = value
except ValueError:
self._state = value self._state = value

View File

@ -0,0 +1,118 @@
"""
homeassistant.components.sensor.dweet
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Displays values from Dweet.io.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.dweet/
"""
from datetime import timedelta
import logging
import json
from homeassistant.util import Throttle
from homeassistant.util import template
from homeassistant.helpers.entity import Entity
from homeassistant.const import (STATE_UNKNOWN, CONF_VALUE_TEMPLATE)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['dweepy==0.2.0']
DEFAULT_NAME = 'Dweet.io Sensor'
CONF_DEVICE = 'device'
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
# pylint: disable=unused-variable, too-many-function-args
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Setup the Dweet sensor. """
import dweepy
device = config.get('device')
value_template = config.get(CONF_VALUE_TEMPLATE)
if None in (device, value_template):
_LOGGER.error('Not all required config keys present: %s',
', '.join(CONF_DEVICE, CONF_VALUE_TEMPLATE))
return False
try:
content = json.dumps(dweepy.get_latest_dweet_for(device)[0]['content'])
except dweepy.DweepyError:
_LOGGER.error("Device/thing '%s' could not be found", device)
return False
if template.render_with_possible_json_value(hass,
value_template,
content) is '':
_LOGGER.error("'%s' was not found", value_template)
return False
dweet = DweetData(device)
add_devices([DweetSensor(hass,
dweet,
config.get('name', DEFAULT_NAME),
value_template,
config.get('unit_of_measurement'))])
# pylint: disable=too-many-arguments
class DweetSensor(Entity):
""" Implements a Dweet sensor. """
def __init__(self, hass, dweet, name, value_template, unit_of_measurement):
self.hass = hass
self.dweet = dweet
self._name = name
self._value_template = value_template
self._state = STATE_UNKNOWN
self._unit_of_measurement = unit_of_measurement
self.update()
@property
def name(self):
""" The name of the sensor. """
return self._name
@property
def unit_of_measurement(self):
""" Unit the value is expressed in. """
return self._unit_of_measurement
@property
def state(self):
""" Returns the state. """
if self.dweet.data is None:
return STATE_UNKNOWN
else:
values = json.dumps(self.dweet.data[0]['content'])
value = template.render_with_possible_json_value(
self.hass, self._value_template, values)
return value
def update(self):
""" Gets the latest data from REST API. """
self.dweet.update()
# pylint: disable=too-few-public-methods
class DweetData(object):
""" Class for handling the data retrieval. """
def __init__(self, device):
self._device = device
self.data = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
""" Gets the latest data from Dweet.io. """
import dweepy
try:
self.data = dweepy.get_latest_dweet_for(self._device)
except dweepy.DweepyError:
_LOGGER.error("Device '%s' could not be found", self._device)
self.data = None

View File

@ -0,0 +1,69 @@
"""
homeassistant.components.sensor.eliqonline
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Monitors home energy use for the eliq online service.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.eliqonline/
"""
import logging
from homeassistant.helpers.entity import Entity
from homeassistant.const import (STATE_UNKNOWN, CONF_ACCESS_TOKEN, CONF_NAME)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['eliqonline==1.0.11']
DEFAULT_NAME = "ELIQ Energy Usage"
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Set up the Eliq sensor. """
import eliqonline
access_token = config.get(CONF_ACCESS_TOKEN)
name = config.get(CONF_NAME, DEFAULT_NAME)
channel_id = config.get("channel_id")
if access_token is None:
_LOGGER.error(
"Configuration Error: "
"Please make sure you have configured your access token "
"that can be aquired from https://my.eliq.se/user/settings/api")
return False
api = eliqonline.API(access_token)
add_devices([EliqSensor(api, channel_id, name)])
class EliqSensor(Entity):
""" Implements a Eliq sensor. """
def __init__(self, api, channel_id, name):
self._name = name
self._unit_of_measurement = "W"
self._state = STATE_UNKNOWN
self.api = api
self.channel_id = channel_id
self.update()
@property
def name(self):
""" Returns the name. """
return self._name
@property
def unit_of_measurement(self):
""" Unit of measurement of this entity, if any. """
return self._unit_of_measurement
@property
def state(self):
""" Returns the state of the device. """
return self._state
def update(self):
""" Gets the latest data. """
response = self.api.get_data_now(channelid=self.channel_id)
self._state = int(response.power)

View File

@ -147,17 +147,8 @@ class ForeCastSensor(Entity):
self._state = data.nearestStormBearing self._state = data.nearestStormBearing
elif self.type == 'precip_intensity': elif self.type == 'precip_intensity':
self._state = data.precipIntensity self._state = data.precipIntensity
if data.precipIntensity == 0:
self._state = 'None'
self._unit_of_measurement = ''
else:
self._state = data.precipIntensity
elif self.type == 'precip_type': elif self.type == 'precip_type':
if data.precipType is None: self._state = data.precipType
self._state = 'None'
self._unit_of_measurement = ''
else:
self._state = data.precipType
elif self.type == 'precip_probability': elif self.type == 'precip_probability':
self._state = round(data.precipProbability * 100, 1) self._state = round(data.precipProbability * 100, 1)
elif self.type == 'dew_point': elif self.type == 'dew_point':

View File

@ -79,7 +79,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if resource not in SENSOR_TYPES: if resource not in SENSOR_TYPES:
_LOGGER.error('Sensor type: "%s" does not exist', resource) _LOGGER.error('Sensor type: "%s" does not exist', resource)
else: else:
dev.append(GlancesSensor(rest, resource)) dev.append(GlancesSensor(rest, config.get('name'), resource))
add_devices(dev) add_devices(dev)
@ -87,9 +87,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class GlancesSensor(Entity): class GlancesSensor(Entity):
""" Implements a Glances sensor. """ """ Implements a Glances sensor. """
def __init__(self, rest, sensor_type): def __init__(self, rest, name, sensor_type):
self.rest = rest self.rest = rest
self._name = SENSOR_TYPES[sensor_type][0] self._name = name
self.type = sensor_type self.type = sensor_type
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
@ -98,7 +98,10 @@ class GlancesSensor(Entity):
@property @property
def name(self): def name(self):
""" The name of the sensor. """ """ The name of the sensor. """
return self._name if self._name is None:
return SENSOR_TYPES[self.type][0]
else:
return '{} {}'.format(self._name, SENSOR_TYPES[self.type][0])
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):

View File

@ -7,7 +7,9 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.mqtt/ https://home-assistant.io/components/sensor.mqtt/
""" """
import logging import logging
from homeassistant.const import CONF_VALUE_TEMPLATE
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util import template
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -32,25 +34,27 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
config.get('state_topic'), config.get('state_topic'),
config.get('qos', DEFAULT_QOS), config.get('qos', DEFAULT_QOS),
config.get('unit_of_measurement'), config.get('unit_of_measurement'),
config.get('state_format'))]) config.get(CONF_VALUE_TEMPLATE))])
# pylint: disable=too-many-arguments, too-many-instance-attributes # pylint: disable=too-many-arguments, too-many-instance-attributes
class MqttSensor(Entity): class MqttSensor(Entity):
""" Represents a sensor that can be updated using MQTT. """ """ Represents a sensor that can be updated using MQTT. """
def __init__(self, hass, name, state_topic, qos, unit_of_measurement, def __init__(self, hass, name, state_topic, qos, unit_of_measurement,
state_format): value_template):
self._state = "-" self._state = "-"
self._hass = hass self._hass = hass
self._name = name self._name = name
self._state_topic = state_topic self._state_topic = state_topic
self._qos = qos self._qos = qos
self._unit_of_measurement = unit_of_measurement self._unit_of_measurement = unit_of_measurement
self._parse = mqtt.FmtParser(state_format)
def message_received(topic, payload, qos): def message_received(topic, payload, qos):
""" A new MQTT message has been received. """ """ A new MQTT message has been received. """
self._state = self._parse(payload) if value_template is not None:
payload = template.render_with_possible_json_value(
hass, value_template, payload)
self._state = payload
self.update_ha_state() self.update_ha_state()
mqtt.subscribe(hass, self._state_topic, message_received, self._qos) mqtt.subscribe(hass, self._state_topic, message_received, self._qos)

View File

@ -76,7 +76,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return False return False
persistence = config.get(CONF_PERSISTENCE, True) persistence = config.get(CONF_PERSISTENCE, True)
persistence_file = config.get(CONF_PERSISTENCE_FILE, 'mysensors.pickle') persistence_file = config.get(CONF_PERSISTENCE_FILE,
hass.config.path('mysensors.pickle'))
version = config.get(CONF_VERSION, '1.4') version = config.get(CONF_VERSION, '1.4')
gateway = mysensors.SerialGateway(port, sensor_update, gateway = mysensors.SerialGateway(port, sensor_update,

View File

@ -1,18 +1,17 @@
""" """
homeassistant.components.sensor.rest homeassistant.components.sensor.rest
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The rest sensor will consume JSON responses sent by an exposed REST API. The rest sensor will consume responses sent by an exposed REST API.
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/sensor.rest/ https://home-assistant.io/components/sensor.rest/
""" """
from datetime import timedelta from datetime import timedelta
from json import loads
import logging import logging
import requests import requests
from homeassistant.util import Throttle from homeassistant.const import CONF_VALUE_TEMPLATE
from homeassistant.util import template, Throttle
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -59,57 +58,31 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
'Please check the URL in the configuration file.') 'Please check the URL in the configuration file.')
return False return False
try:
data = loads(response.text)
except ValueError:
_LOGGER.error('No valid JSON in the response in: %s', data)
return False
try:
RestSensor.extract_value(data, config.get('variable'))
except KeyError:
_LOGGER.error('Variable "%s" not found in response: "%s"',
config.get('variable'), data)
return False
if use_get: if use_get:
rest = RestDataGet(resource, verify_ssl) rest = RestDataGet(resource, verify_ssl)
elif use_post: elif use_post:
rest = RestDataPost(resource, payload, verify_ssl) rest = RestDataPost(resource, payload, verify_ssl)
add_devices([RestSensor(rest, add_devices([RestSensor(hass,
rest,
config.get('name', DEFAULT_NAME), config.get('name', DEFAULT_NAME),
config.get('variable'),
config.get('unit_of_measurement'), config.get('unit_of_measurement'),
config.get('correction_factor', None), config.get(CONF_VALUE_TEMPLATE))])
config.get('decimal_places', None))])
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
class RestSensor(Entity): class RestSensor(Entity):
""" Implements a REST sensor. """ """ Implements a REST sensor. """
def __init__(self, rest, name, variable, unit_of_measurement, corr_factor, def __init__(self, hass, rest, name, unit_of_measurement, value_template):
decimal_places): self._hass = hass
self.rest = rest self.rest = rest
self._name = name self._name = name
self._variable = variable
self._state = 'n/a' self._state = 'n/a'
self._unit_of_measurement = unit_of_measurement self._unit_of_measurement = unit_of_measurement
self._corr_factor = corr_factor self._value_template = value_template
self._decimal_places = decimal_places
self.update() self.update()
@classmethod
def extract_value(cls, data, variable):
""" Extracts the value using a key name or a path. """
if isinstance(variable, list):
for variable_item in variable:
data = data[variable_item]
return data
else:
return data[variable]
@property @property
def name(self): def name(self):
""" The name of the sensor. """ """ The name of the sensor. """
@ -133,23 +106,10 @@ class RestSensor(Entity):
if 'error' in value: if 'error' in value:
self._state = value['error'] self._state = value['error']
else: else:
try: if self._value_template is not None:
if value is not None: value = template.render_with_possible_json_value(
value = RestSensor.extract_value(value, self._variable) self._hass, self._value_template, value, 'N/A')
if self._corr_factor is not None \ self._state = value
and self._decimal_places is not None:
self._state = round(
(float(value) *
float(self._corr_factor)),
self._decimal_places)
elif self._corr_factor is not None \
and self._decimal_places is None:
self._state = round(float(value) *
float(self._corr_factor))
else:
self._state = value
except ValueError:
self._state = RestSensor.extract_value(value, self._variable)
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
@ -169,7 +129,7 @@ class RestDataGet(object):
verify=self._verify_ssl) verify=self._verify_ssl)
if 'error' in self.data: if 'error' in self.data:
del self.data['error'] del self.data['error']
self.data = response.json() self.data = response.text
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
_LOGGER.error("No route to resource/endpoint.") _LOGGER.error("No route to resource/endpoint.")
self.data['error'] = 'N/A' self.data['error'] = 'N/A'
@ -193,7 +153,7 @@ class RestDataPost(object):
timeout=10, verify=self._verify_ssl) timeout=10, verify=self._verify_ssl)
if 'error' in self.data: if 'error' in self.data:
del self.data['error'] del self.data['error']
self.data = response.json() self.data = response.text
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
_LOGGER.error("No route to resource/endpoint.") _LOGGER.error("No route to resource/endpoint.")
self.data['error'] = 'N/A' self.data['error'] = 'N/A'

View File

@ -0,0 +1,117 @@
"""
homeassistant.components.sensor.torque
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Get data from the Torque OBD application.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.torque/
"""
import re
from homeassistant.const import HTTP_OK
from homeassistant.helpers.entity import Entity
DOMAIN = 'torque'
DEPENDENCIES = ['http']
SENSOR_EMAIL_FIELD = 'eml'
DEFAULT_NAME = 'vehicle'
ENTITY_NAME_FORMAT = '{0} {1}'
API_PATH = '/api/torque'
SENSOR_NAME_KEY = r'userFullName(\w+)'
SENSOR_UNIT_KEY = r'userUnit(\w+)'
SENSOR_VALUE_KEY = r'k(\w+)'
NAME_KEY = re.compile(SENSOR_NAME_KEY)
UNIT_KEY = re.compile(SENSOR_UNIT_KEY)
VALUE_KEY = re.compile(SENSOR_VALUE_KEY)
def decode(value):
""" Double-decode required. """
return value.encode('raw_unicode_escape').decode('utf-8')
def convert_pid(value):
""" Convert pid from hex string to integer. """
return int(value, 16)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Set up Torque platform. """
vehicle = config.get('name', DEFAULT_NAME)
email = config.get('email', None)
sensors = {}
def _receive_data(handler, path_match, data):
""" Received data from Torque. """
handler.send_response(HTTP_OK)
handler.end_headers()
if email is not None and email != data[SENSOR_EMAIL_FIELD]:
return
names = {}
units = {}
for key in data:
is_name = NAME_KEY.match(key)
is_unit = UNIT_KEY.match(key)
is_value = VALUE_KEY.match(key)
if is_name:
pid = convert_pid(is_name.group(1))
names[pid] = decode(data[key])
elif is_unit:
pid = convert_pid(is_unit.group(1))
units[pid] = decode(data[key])
elif is_value:
pid = convert_pid(is_value.group(1))
if pid in sensors:
sensors[pid].on_update(data[key])
for pid in names:
if pid not in sensors:
sensors[pid] = TorqueSensor(
ENTITY_NAME_FORMAT.format(vehicle, names[pid]),
units.get(pid, None))
add_devices([sensors[pid]])
hass.http.register_path('GET', API_PATH, _receive_data)
return True
class TorqueSensor(Entity):
""" Represents a Torque sensor. """
def __init__(self, name, unit):
self._name = name
self._unit = unit
self._state = None
@property
def name(self):
""" Returns the name of the sensor. """
return self._name
@property
def unit_of_measurement(self):
""" Returns the unit of measurement. """
return self._unit
@property
def state(self):
""" State of the sensor. """
return self._state
@property
def icon(self):
""" Sensor default icon. """
return 'mdi:car'
def on_update(self, value):
""" Receive an update. """
self._state = value
self.update_ha_state()

View File

@ -0,0 +1,81 @@
"""
homeassistant.components.sensor.twitch
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A sensor for the Twitch stream status.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.twitch/
"""
from homeassistant.helpers.entity import Entity
from homeassistant.const import ATTR_ENTITY_PICTURE
STATE_STREAMING = 'streaming'
STATE_OFFLINE = 'offline'
ATTR_GAME = 'game'
ATTR_TITLE = 'title'
ICON = 'mdi:twitch'
REQUIREMENTS = ['python-twitch==1.2.0']
DOMAIN = 'twitch'
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the Twitch platform. """
add_devices(
[TwitchSensor(channel) for channel in config.get('channels', [])])
class TwitchSensor(Entity):
""" Represents an Twitch channel. """
# pylint: disable=abstract-method
def __init__(self, channel):
self._channel = channel
self._state = STATE_OFFLINE
self._preview = None
self._game = None
self._title = None
self.update()
@property
def should_poll(self):
""" Device should be polled. """
return True
@property
def name(self):
""" Returns the name of the sensor. """
return self._channel
@property
def state(self):
""" State of the sensor. """
return self._state
# pylint: disable=no-member
def update(self):
""" Update device state. """
from twitch.api import v3 as twitch
stream = twitch.streams.by_channel(self._channel).get('stream')
if stream:
self._game = stream.get('channel').get('game')
self._title = stream.get('channel').get('status')
self._preview = stream.get('preview').get('small')
self._state = STATE_STREAMING
else:
self._state = STATE_OFFLINE
@property
def state_attributes(self):
""" Returns the state attributes. """
if self._state == STATE_STREAMING:
return {
ATTR_GAME: self._game,
ATTR_TITLE: self._title,
ATTR_ENTITY_PICTURE: self._preview
}
@property
def icon(self):
return ICON

View File

@ -11,9 +11,7 @@ import logging
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.const import CONF_ACCESS_TOKEN, STATE_OPEN, STATE_CLOSED from homeassistant.const import CONF_ACCESS_TOKEN, STATE_OPEN, STATE_CLOSED
REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' REQUIREMENTS = ['python-wink==0.3.1']
'42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip'
'#python-wink==0.2']
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
@ -32,6 +30,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
pywink.set_bearer_token(token) pywink.set_bearer_token(token)
add_devices(WinkSensorDevice(sensor) for sensor in pywink.get_sensors()) add_devices(WinkSensorDevice(sensor) for sensor in pywink.get_sensors())
add_devices(WinkEggMinder(eggtray) for eggtray in pywink.get_eggtrays())
class WinkSensorDevice(Entity): class WinkSensorDevice(Entity):
@ -48,7 +47,7 @@ class WinkSensorDevice(Entity):
@property @property
def unique_id(self): def unique_id(self):
""" Returns the id of this wink sensor """ """ Returns the id of this wink sensor """
return "{}.{}".format(self.__class__, self.wink.deviceId()) return "{}.{}".format(self.__class__, self.wink.device_id())
@property @property
def name(self): def name(self):
@ -57,9 +56,35 @@ class WinkSensorDevice(Entity):
def update(self): def update(self):
""" Update state of the sensor. """ """ Update state of the sensor. """
self.wink.updateState() self.wink.update_state()
@property @property
def is_open(self): def is_open(self):
""" True if door is open. """ """ True if door is open. """
return self.wink.state() return self.wink.state()
class WinkEggMinder(Entity):
""" Represents a Wink Egg Minder. """
def __init__(self, wink):
self.wink = wink
@property
def state(self):
""" Returns the state. """
return self.wink.state()
@property
def unique_id(self):
""" Returns the id of this wink Egg Minder """
return "{}.{}".format(self.__class__, self.wink.device_id())
@property
def name(self):
""" Returns the name of the Egg Minder if any. """
return self.wink.name()
def update(self):
""" Update state of the Egg Minder. """
self.wink.update_state()

View File

@ -8,7 +8,9 @@ https://home-assistant.io/components/switch.mqtt/
""" """
import logging import logging
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.const import CONF_VALUE_TEMPLATE
from homeassistant.components.switch import SwitchDevice from homeassistant.components.switch import SwitchDevice
from homeassistant.util import template
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -40,14 +42,14 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
config.get('payload_on', DEFAULT_PAYLOAD_ON), config.get('payload_on', DEFAULT_PAYLOAD_ON),
config.get('payload_off', DEFAULT_PAYLOAD_OFF), config.get('payload_off', DEFAULT_PAYLOAD_OFF),
config.get('optimistic', DEFAULT_OPTIMISTIC), config.get('optimistic', DEFAULT_OPTIMISTIC),
config.get('state_format'))]) config.get(CONF_VALUE_TEMPLATE))])
# pylint: disable=too-many-arguments, too-many-instance-attributes # pylint: disable=too-many-arguments, too-many-instance-attributes
class MqttSwitch(SwitchDevice): class MqttSwitch(SwitchDevice):
""" Represents a switch that can be toggled using MQTT. """ """ Represents a switch that can be toggled using MQTT. """
def __init__(self, hass, name, state_topic, command_topic, qos, retain, def __init__(self, hass, name, state_topic, command_topic, qos, retain,
payload_on, payload_off, optimistic, state_format): payload_on, payload_off, optimistic, value_template):
self._state = False self._state = False
self._hass = hass self._hass = hass
self._name = name self._name = name
@ -58,11 +60,12 @@ class MqttSwitch(SwitchDevice):
self._payload_on = payload_on self._payload_on = payload_on
self._payload_off = payload_off self._payload_off = payload_off
self._optimistic = optimistic self._optimistic = optimistic
self._parse = mqtt.FmtParser(state_format)
def message_received(topic, payload, qos): def message_received(topic, payload, qos):
""" A new MQTT message has been received. """ """ A new MQTT message has been received. """
payload = self._parse(payload) if value_template is not None:
payload = template.render_with_possible_json_value(
hass, value_template, payload)
if payload == self._payload_on: if payload == self._payload_on:
self._state = True self._state = True
self.update_ha_state() self.update_ha_state()

View File

@ -11,7 +11,7 @@ import logging
from homeassistant.components.switch import SwitchDevice from homeassistant.components.switch import SwitchDevice
DEFAULT_NAME = "Orvibo S20 Switch" DEFAULT_NAME = "Orvibo S20 Switch"
REQUIREMENTS = ['orvibo==1.0.1'] REQUIREMENTS = ['orvibo==1.1.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -11,9 +11,7 @@ import logging
from homeassistant.components.wink import WinkToggleDevice from homeassistant.components.wink import WinkToggleDevice
from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.const import CONF_ACCESS_TOKEN
REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' REQUIREMENTS = ['python-wink==0.3.1']
'42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip'
'#python-wink==0.2']
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):

View File

@ -87,7 +87,13 @@ class Thermostat(ThermostatDevice):
@property @property
def target_temperature(self): def target_temperature(self):
""" Returns the temperature we try to reach. """ """ Returns the temperature we try to reach. """
return (self.target_temperature_low + self.target_temperature_high) / 2 if self.hvac_mode == 'heat' or self.hvac_mode == 'auxHeatOnly':
return self.target_temperature_low
elif self.hvac_mode == 'cool':
return self.target_temperature_high
else:
return (self.target_temperature_low +
self.target_temperature_high) / 2
@property @property
def target_temperature_low(self): def target_temperature_low(self):

View File

@ -0,0 +1,117 @@
"""
homeassistant.components.thermostat.heatmiser
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Adds support for the PRT Heatmiser themostats using the V3 protocol.
See https://github.com/andylockran/heatmiserV3 for more info on the
heatmiserV3 module dependency.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/thermostat.heatmiser/
"""
import logging
from homeassistant.components.thermostat import ThermostatDevice
from homeassistant.const import TEMP_CELCIUS
CONF_IPADDRESS = 'ipaddress'
CONF_PORT = 'port'
CONF_TSTATS = 'tstats'
REQUIREMENTS = ["heatmiserV3==0.9.1"]
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the heatmiser thermostat. """
from heatmiserV3 import heatmiser, connection
ipaddress = str(config[CONF_IPADDRESS])
port = str(config[CONF_PORT])
if ipaddress is None or port is None:
_LOGGER.error("Missing required configuration items %s or %s",
CONF_IPADDRESS, CONF_PORT)
return False
serport = connection.connection(ipaddress, port)
serport.open()
tstats = []
if CONF_TSTATS in config:
tstats = config[CONF_TSTATS]
if tstats is None:
_LOGGER.error("No thermostats configured.")
return False
for tstat in tstats:
add_devices([
HeatmiserV3Thermostat(
heatmiser,
tstat.get("id"),
tstat.get("name"),
serport)
])
return
class HeatmiserV3Thermostat(ThermostatDevice):
""" Represents a HeatmiserV3 thermostat. """
# pylint: disable=too-many-instance-attributes
def __init__(self, heatmiser, device, name, serport):
self.heatmiser = heatmiser
self.device = device
self.serport = serport
self._current_temperature = None
self._name = name
self._id = device
self.dcb = None
self.update()
self._target_temperature = int(self.dcb.get("roomset"))
@property
def name(self):
""" Returns the name of the honeywell, if any. """
return self._name
@property
def unit_of_measurement(self):
""" Unit of measurement this thermostat uses."""
return TEMP_CELCIUS
@property
def current_temperature(self):
""" Returns the current temperature. """
if self.dcb is not None:
low = self.dcb.get("floortemplow ")
high = self.dcb.get("floortemphigh")
temp = (high*256 + low)/10.0
self._current_temperature = temp
else:
self._current_temperature = None
return self._current_temperature
@property
def target_temperature(self):
""" Returns the temperature we try to reach. """
return self._target_temperature
def set_temperature(self, temperature):
""" Set new target temperature """
temperature = int(temperature)
self.heatmiser.hmSendAddress(
self._id,
18,
temperature,
1,
self.serport)
self._target_temperature = int(temperature)
def update(self):
self.dcb = self.heatmiser.hmReadAddress(
self._id,
'prt',
self.serport)

View File

@ -1,8 +1,7 @@
""" """
homeassistant.components.wink homeassistant.components.wink
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Connects to a Wink hub and loads relevant components to control its devices. Connects to a Wink hub and loads relevant components to control its devices.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/wink/ https://home-assistant.io/components/wink/
""" """
@ -17,9 +16,7 @@ from homeassistant.const import (
ATTR_SERVICE, ATTR_DISCOVERED, ATTR_FRIENDLY_NAME) ATTR_SERVICE, ATTR_DISCOVERED, ATTR_FRIENDLY_NAME)
DOMAIN = "wink" DOMAIN = "wink"
REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/' REQUIREMENTS = ['python-wink==0.3.1']
'42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip'
'#python-wink==0.2']
DISCOVER_LIGHTS = "wink.lights" DISCOVER_LIGHTS = "wink.lights"
DISCOVER_SWITCHES = "wink.switches" DISCOVER_SWITCHES = "wink.switches"
@ -41,7 +38,8 @@ def setup(hass, config):
for component_name, func_exists, discovery_type in ( for component_name, func_exists, discovery_type in (
('light', pywink.get_bulbs, DISCOVER_LIGHTS), ('light', pywink.get_bulbs, DISCOVER_LIGHTS),
('switch', pywink.get_switches, DISCOVER_SWITCHES), ('switch', pywink.get_switches, DISCOVER_SWITCHES),
('sensor', pywink.get_sensors, DISCOVER_SENSORS), ('sensor', lambda: pywink.get_sensors or pywink.get_eggtrays,
DISCOVER_SENSORS),
('lock', pywink.get_locks, DISCOVER_LOCKS)): ('lock', pywink.get_locks, DISCOVER_LOCKS)):
if func_exists(): if func_exists():
@ -68,7 +66,7 @@ class WinkToggleDevice(ToggleEntity):
@property @property
def unique_id(self): def unique_id(self):
""" Returns the id of this Wink switch. """ """ Returns the id of this Wink switch. """
return "{}.{}".format(self.__class__, self.wink.deviceId()) return "{}.{}".format(self.__class__, self.wink.device_id())
@property @property
def name(self): def name(self):
@ -89,12 +87,12 @@ class WinkToggleDevice(ToggleEntity):
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
""" Turns the switch on. """ """ Turns the switch on. """
self.wink.setState(True) self.wink.set_state(True)
def turn_off(self): def turn_off(self):
""" Turns the switch off. """ """ Turns the switch off. """
self.wink.setState(False) self.wink.set_state(False)
def update(self): def update(self):
""" Update state of the light. """ """ Update state of the light. """
self.wink.updateState() self.wink.update_state()

View File

@ -1,7 +1,7 @@
# coding: utf-8 # coding: utf-8
""" Constants used by Home Assistant components. """ """ Constants used by Home Assistant components. """
__version__ = "0.9.1" __version__ = "0.10.0.dev0"
# Can be used to specify a catch all when registering state or event listeners. # Can be used to specify a catch all when registering state or event listeners.
MATCH_ALL = '*' MATCH_ALL = '*'
@ -25,6 +25,8 @@ CONF_PASSWORD = "password"
CONF_API_KEY = "api_key" CONF_API_KEY = "api_key"
CONF_ACCESS_TOKEN = "access_token" CONF_ACCESS_TOKEN = "access_token"
CONF_VALUE_TEMPLATE = "value_template"
# #### EVENTS #### # #### EVENTS ####
EVENT_HOMEASSISTANT_START = "homeassistant_start" EVENT_HOMEASSISTANT_START = "homeassistant_start"
EVENT_HOMEASSISTANT_STOP = "homeassistant_stop" EVENT_HOMEASSISTANT_STOP = "homeassistant_stop"
@ -165,6 +167,7 @@ URL_API_COMPONENTS = "/api/components"
URL_API_BOOTSTRAP = "/api/bootstrap" URL_API_BOOTSTRAP = "/api/bootstrap"
URL_API_ERROR_LOG = "/api/error_log" URL_API_ERROR_LOG = "/api/error_log"
URL_API_LOG_OUT = "/api/log_out" URL_API_LOG_OUT = "/api/log_out"
URL_API_TEMPLATE = "/api/template"
HTTP_OK = 200 HTTP_OK = 200
HTTP_CREATED = 201 HTTP_CREATED = 201

View File

@ -14,3 +14,10 @@ class InvalidEntityFormatError(HomeAssistantError):
class NoEntitySpecifiedError(HomeAssistantError): class NoEntitySpecifiedError(HomeAssistantError):
""" When no entity is specified. """ """ When no entity is specified. """
pass pass
class TemplateError(HomeAssistantError):
""" Error during template rendering. """
def __init__(self, exception):
super().__init__('{}: {}'.format(exception.__class__.__name__,
exception))

View File

@ -0,0 +1,113 @@
"""
homeassistant.util.template
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Template utility methods for rendering strings with HA data.
"""
# pylint: disable=too-few-public-methods
import json
import logging
import jinja2
from jinja2.sandbox import ImmutableSandboxedEnvironment
from homeassistant.const import STATE_UNKNOWN
from homeassistant.exceptions import TemplateError
_LOGGER = logging.getLogger(__name__)
_SENTINEL = object()
def render_with_possible_json_value(hass, template, value,
error_value=_SENTINEL):
""" Renders template with value exposed.
If valid JSON will expose value_json too. """
variables = {
'value': value
}
try:
variables['value_json'] = json.loads(value)
except ValueError:
pass
try:
return render(hass, template, variables)
except TemplateError:
_LOGGER.exception('Error parsing value')
return value if error_value is _SENTINEL else error_value
def render(hass, template, variables=None, **kwargs):
""" Render given template. """
if variables is not None:
kwargs.update(variables)
try:
return ENV.from_string(template, {
'states': AllStates(hass),
'is_state': hass.states.is_state
}).render(kwargs).strip()
except jinja2.TemplateError as err:
raise TemplateError(err)
class AllStates(object):
""" Class to expose all HA states as attributes. """
def __init__(self, hass):
self._hass = hass
def __getattr__(self, name):
return DomainStates(self._hass, name)
def __iter__(self):
return iter(sorted(self._hass.states.all(),
key=lambda state: state.entity_id))
def __call__(self, entity_id):
state = self._hass.states.get(entity_id)
return STATE_UNKNOWN if state is None else state.state
class DomainStates(object):
""" Class to expose a specific HA domain as attributes. """
def __init__(self, hass, domain):
self._hass = hass
self._domain = domain
def __getattr__(self, name):
return self._hass.states.get('{}.{}'.format(self._domain, name))
def __iter__(self):
return iter(sorted(
(state for state in self._hass.states.all()
if state.domain == self._domain),
key=lambda state: state.entity_id))
def forgiving_round(value, precision=0):
""" Rounding method that accepts strings. """
try:
value = round(float(value), precision)
return int(value) if precision == 0 else value
except ValueError:
# If value can't be converted to float
return value
def multiply(value, amount):
""" Converts to float and multiplies value. """
try:
return float(value) * amount
except ValueError:
# If value can't be converted to float
return value
class TemplateEnvironment(ImmutableSandboxedEnvironment):
""" Home Assistant template environment. """
def is_safe_callable(self, obj):
return isinstance(obj, AllStates) or super().is_safe_callable(obj)
ENV = TemplateEnvironment()
ENV.filters['round'] = forgiving_round
ENV.filters['multiply'] = multiply

View File

@ -2,4 +2,4 @@ requests>=2,<3
pyyaml>=3.11,<4 pyyaml>=3.11,<4
pytz>=2015.4 pytz>=2015.4
pip>=7.0.0 pip>=7.0.0
vincenty==0.1.3 vincenty==0.1.3

View File

@ -4,10 +4,17 @@ pyyaml>=3.11,<4
pytz>=2015.4 pytz>=2015.4
pip>=7.0.0 pip>=7.0.0
vincenty==0.1.3 vincenty==0.1.3
jinja2>=2.8
# homeassistant.components.arduino # homeassistant.components.arduino
PyMata==2.07a PyMata==2.07a
# homeassistant.components.conversation
fuzzywuzzy==0.8.0
# homeassistant.components.device_tracker.icloud
pyicloud==0.7.2
# homeassistant.components.device_tracker.netgear # homeassistant.components.device_tracker.netgear
pynetgear==0.3 pynetgear==0.3
@ -59,10 +66,10 @@ https://github.com/pavoni/home-assistant-vera-api/archive/efdba4e63d58a30bc9b36d
# homeassistant.components.lock.wink # homeassistant.components.lock.wink
# homeassistant.components.sensor.wink # homeassistant.components.sensor.wink
# homeassistant.components.switch.wink # homeassistant.components.switch.wink
https://github.com/balloob/python-wink/archive/42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip#python-wink==0.2 python-wink==0.3.1
# homeassistant.components.media_player.cast # homeassistant.components.media_player.cast
pychromecast==0.6.12 pychromecast==0.6.13
# homeassistant.components.media_player.kodi # homeassistant.components.media_player.kodi
jsonrpc-requests==0.1 jsonrpc-requests==0.1
@ -82,9 +89,6 @@ https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b6
# homeassistant.components.mqtt # homeassistant.components.mqtt
paho-mqtt==1.1 paho-mqtt==1.1
# homeassistant.components.mqtt
jsonpath-rw==1.4.0
# homeassistant.components.notify.pushbullet # homeassistant.components.notify.pushbullet
pushbullet.py==0.9.0 pushbullet.py==0.9.0
@ -118,6 +122,12 @@ py-cpuinfo==0.1.6
# homeassistant.components.sensor.dht # homeassistant.components.sensor.dht
# http://github.com/mala-zaba/Adafruit_Python_DHT/archive/4101340de8d2457dd194bca1e8d11cbfc237e919.zip#Adafruit_DHT==1.1.0 # http://github.com/mala-zaba/Adafruit_Python_DHT/archive/4101340de8d2457dd194bca1e8d11cbfc237e919.zip#Adafruit_DHT==1.1.0
# homeassistant.components.sensor.dweet
dweepy==0.2.0
# homeassistant.components.sensor.eliqonline
eliqonline==1.0.11
# homeassistant.components.sensor.forecast # homeassistant.components.sensor.forecast
python-forecastio==1.3.3 python-forecastio==1.3.3
@ -144,6 +154,9 @@ https://github.com/rkabadi/temper-python/archive/3dbdaf2d87b8db9a3cd6e5585fc7045
# homeassistant.components.switch.transmission # homeassistant.components.switch.transmission
transmissionrpc==0.11 transmissionrpc==0.11
# homeassistant.components.sensor.twitch
python-twitch==1.2.0
# homeassistant.components.sun # homeassistant.components.sun
astral==0.8.1 astral==0.8.1
@ -154,11 +167,14 @@ https://github.com/rkabadi/pyedimax/archive/365301ce3ff26129a7910c501ead09ea625f
hikvision==0.4 hikvision==0.4
# homeassistant.components.switch.orvibo # homeassistant.components.switch.orvibo
orvibo==1.0.1 orvibo==1.1.0
# homeassistant.components.switch.wemo # homeassistant.components.switch.wemo
pywemo==0.3.3 pywemo==0.3.3
# homeassistant.components.thermostat.heatmiser
heatmiserV3==0.9.1
# homeassistant.components.thermostat.honeywell # homeassistant.components.thermostat.honeywell
evohomeclient==0.2.4 evohomeclient==0.2.4

View File

@ -1,5 +1,7 @@
echo "Bootstrapping frontend..." echo "Bootstrapping frontend..."
git submodule update
cd homeassistant/components/frontend/www_static/home-assistant-polymer cd homeassistant/components/frontend/www_static/home-assistant-polymer
npm install npm install
bower install
npm run setup_js_dev npm run setup_js_dev
cd ../../../../.. cd ../../../../..

View File

@ -1,10 +1,18 @@
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
echo "Update the submodule to latest version..."
git submodule update
echo "Installing dependencies..." echo "Installing dependencies..."
python3 -m pip install --upgrade -r requirements_all.txt python3 -m pip install -r requirements_all.txt
REQ_STATUS=$?
echo "Installing development dependencies.." echo "Installing development dependencies.."
python3 -m pip install --upgrade flake8 pylint coveralls pytest pytest-cov python3 -m pip install flake8 pylint coveralls pytest pytest-cov
REQ_DEV_STATUS=$?
if [ $REQ_DEV_STATUS -eq 0 ]
then
exit $REQ_STATUS
else
exit $REQ_DEV_STATUS
fi

View File

@ -7,6 +7,7 @@ npm run frontend_prod
cp bower_components/webcomponentsjs/webcomponents-lite.min.js .. cp bower_components/webcomponentsjs/webcomponents-lite.min.js ..
cp build/frontend.html .. cp build/frontend.html ..
cp build/service_worker.js ..
# Generate the MD5 hash of the new frontend # Generate the MD5 hash of the new frontend
cd ../.. cd ../..

View File

@ -5,6 +5,12 @@
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
if [ "$TRAVIS_PYTHON_VERSION" != "3.4" ]; then
NO_LINT=1
fi
export NO_LINT
script/test coverage script/test coverage
STATUS=$? STATUS=$?

View File

@ -8,10 +8,11 @@ import importlib
import os import os
import pkgutil import pkgutil
import re import re
import sys
COMMENT_REQUIREMENTS = [ COMMENT_REQUIREMENTS = [
'RPi.GPIO', 'RPi.GPIO',
'Adafruit_Python_DHT' 'Adafruit_Python_DHT',
] ]
@ -67,8 +68,9 @@ def gather_modules():
reqs.setdefault(req, []).append(package) reqs.setdefault(req, []).append(package)
if errors: if errors:
print("Found errors") print("******* ERROR")
print('\n'.join(errors)) print("Errors while importing: ", ', '.join(errors))
print("Make sure you import 3rd party libraries inside methods.")
return None return None
output.append('# Home Assistant core') output.append('# Home Assistant core')
@ -94,6 +96,12 @@ def write_file(data):
req_file.write(data) req_file.write(data)
def validate_file(data):
""" Validates if requirements_all.txt is up to date. """
with open('requirements_all.txt', 'r') as req_file:
return data == ''.join(req_file)
def main(): def main():
""" Main """ """ Main """
if not os.path.isfile('requirements_all.txt'): if not os.path.isfile('requirements_all.txt'):
@ -103,7 +111,16 @@ def main():
data = gather_modules() data = gather_modules()
if data is None: if data is None:
return sys.exit(1)
if sys.argv[-1] == 'validate':
if validate_file(data):
print("requirements_all.txt is up to date.")
sys.exit(0)
print("******* ERROR")
print("requirements_all.txt is not up to date")
print("Please run script/gen_requirements_all.py")
sys.exit(1)
write_file(data) write_file(data)

View File

@ -5,9 +5,12 @@
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
script/lint if [ "$NO_LINT" = "1" ]; then
LINT_STATUS=0
LINT_STATUS=$? else
script/lint
LINT_STATUS=$?
fi
echo "Running tests..." echo "Running tests..."

View File

@ -1,3 +1,4 @@
#!/usr/bin/env python3
import os import os
from setuptools import setup, find_packages from setuptools import setup, find_packages
from homeassistant.const import __version__ from homeassistant.const import __version__
@ -14,7 +15,8 @@ REQUIRES = [
'pyyaml>=3.11,<4', 'pyyaml>=3.11,<4',
'pytz>=2015.4', 'pytz>=2015.4',
'pip>=7.0.0', 'pip>=7.0.0',
'vincenty==0.1.3' 'vincenty==0.1.3',
'jinja2>=2.8'
] ]
setup( setup(

View File

@ -295,7 +295,7 @@ class TestAutomationNumericState(unittest.TestCase):
'trigger': { 'trigger': {
'platform': 'numeric_state', 'platform': 'numeric_state',
'entity_id': 'test.entity', 'entity_id': 'test.entity',
'attribute': 'test_attribute', 'value_template': '{{ state.attributes.test_attribute }}',
'below': 10, 'below': 10,
}, },
'action': { 'action': {
@ -314,7 +314,7 @@ class TestAutomationNumericState(unittest.TestCase):
'trigger': { 'trigger': {
'platform': 'numeric_state', 'platform': 'numeric_state',
'entity_id': 'test.entity', 'entity_id': 'test.entity',
'attribute': 'test_attribute', 'value_template': '{{ state.attributes.test_attribute }}',
'below': 10, 'below': 10,
}, },
'action': { 'action': {
@ -333,7 +333,7 @@ class TestAutomationNumericState(unittest.TestCase):
'trigger': { 'trigger': {
'platform': 'numeric_state', 'platform': 'numeric_state',
'entity_id': 'test.entity', 'entity_id': 'test.entity',
'attribute': 'test_attribute', 'value_template': '{{ state.attributes.test_attribute }}',
'below': 10, 'below': 10,
}, },
'action': { 'action': {
@ -352,7 +352,7 @@ class TestAutomationNumericState(unittest.TestCase):
'trigger': { 'trigger': {
'platform': 'numeric_state', 'platform': 'numeric_state',
'entity_id': 'test.entity', 'entity_id': 'test.entity',
'attribute': 'test_attribute', 'value_template': '{{ state.attributes.test_attribute }}',
'below': 10, 'below': 10,
}, },
'action': { 'action': {
@ -371,7 +371,7 @@ class TestAutomationNumericState(unittest.TestCase):
'trigger': { 'trigger': {
'platform': 'numeric_state', 'platform': 'numeric_state',
'entity_id': 'test.entity', 'entity_id': 'test.entity',
'attribute': 'test_attribute', 'value_template': '{{ state.attributes.test_attribute }}',
'below': 10, 'below': 10,
}, },
'action': { 'action': {
@ -384,13 +384,51 @@ class TestAutomationNumericState(unittest.TestCase):
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls)) self.assertEqual(1, len(self.calls))
def test_template_list(self):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'numeric_state',
'entity_id': 'test.entity',
'value_template': '{{ state.attributes.test_attribute[2] }}',
'below': 10,
},
'action': {
'service': 'test.automation'
}
}
}))
# 3 is below 10
self.hass.states.set('test.entity', 'entity', { 'test_attribute': [11, 15, 3] })
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_template_string(self):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'numeric_state',
'entity_id': 'test.entity',
'value_template': '{{ state.attributes.test_attribute | multiply(10) }}',
'below': 10,
},
'action': {
'service': 'test.automation'
}
}
}))
# 9 is below 10
self.hass.states.set('test.entity', 'entity', { 'test_attribute': '0.9' })
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_not_fires_on_attribute_change_with_attribute_not_below_multiple_attributes(self): def test_if_not_fires_on_attribute_change_with_attribute_not_below_multiple_attributes(self):
self.assertTrue(automation.setup(self.hass, { self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: { automation.DOMAIN: {
'trigger': { 'trigger': {
'platform': 'numeric_state', 'platform': 'numeric_state',
'entity_id': 'test.entity', 'entity_id': 'test.entity',
'attribute': 'test_attribute', 'value_template': '{{ state.attributes.test_attribute }}',
'below': 10, 'below': 10,
}, },
'action': { 'action': {

View File

@ -0,0 +1,355 @@
"""
tests.components.automation.test_template
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests template automation.
"""
import unittest
import homeassistant.core as ha
import homeassistant.components.automation as automation
class TestAutomationTemplate(unittest.TestCase):
""" Test the event automation. """
def setUp(self): # pylint: disable=invalid-name
self.hass = ha.HomeAssistant()
self.hass.states.set('test.entity', 'hello')
self.calls = []
def record_call(service):
self.calls.append(service)
self.hass.services.register('test', 'automation', record_call)
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
self.hass.stop()
def test_if_fires_on_change_bool(self):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'template',
'value_template': '{{ true }}',
},
'action': {
'service': 'test.automation'
}
}
}))
self.hass.states.set('test.entity', 'world')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_fires_on_change_str(self):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'template',
'value_template': 'true',
},
'action': {
'service': 'test.automation'
}
}
}))
self.hass.states.set('test.entity', 'world')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_fires_on_change_str_crazy(self):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'template',
'value_template': 'TrUE',
},
'action': {
'service': 'test.automation'
}
}
}))
self.hass.states.set('test.entity', 'world')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_not_fires_on_change_bool(self):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'template',
'value_template': '{{ false }}',
},
'action': {
'service': 'test.automation'
}
}
}))
self.hass.states.set('test.entity', 'world')
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))
def test_if_not_fires_on_change_str(self):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'template',
'value_template': 'False',
},
'action': {
'service': 'test.automation'
}
}
}))
self.hass.states.set('test.entity', 'world')
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))
def test_if_not_fires_on_change_str_crazy(self):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'template',
'value_template': 'Anything other than "true" is false.',
},
'action': {
'service': 'test.automation'
}
}
}))
self.hass.states.set('test.entity', 'world')
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))
def test_if_fires_on_no_change(self):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'template',
'value_template': '{{ true }}',
},
'action': {
'service': 'test.automation'
}
}
}))
self.hass.states.set('test.entity', 'hello')
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))
def test_if_fires_on_two_change(self):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'template',
'value_template': '{{ true }}',
},
'action': {
'service': 'test.automation'
}
}
}))
# Trigger once
self.hass.states.set('test.entity', 'world')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
# Trigger again
self.hass.states.set('test.entity', 'world')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_fires_on_change_with_template(self):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'template',
'value_template': '{{ is_state("test.entity", "world") }}',
},
'action': {
'service': 'test.automation'
}
}
}))
self.hass.states.set('test.entity', 'world')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_not_fires_on_change_with_template(self):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'template',
'value_template': '{{ is_state("test.entity", "hello") }}',
},
'action': {
'service': 'test.automation'
}
}
}))
self.hass.states.set('test.entity', 'world')
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))
def test_if_fires_on_change_with_template_advanced(self):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'template',
'value_template': '''{%- if is_state("test.entity", "world") -%}
true
{%- else -%}
false
{%- endif -%}''',
},
'action': {
'service': 'test.automation'
}
}
}))
self.hass.states.set('test.entity', 'world')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_fires_on_no_change_with_template_advanced(self):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'template',
'value_template': '''{%- if is_state("test.entity", "world") -%}
true
{%- else -%}
false
{%- endif -%}''',
},
'action': {
'service': 'test.automation'
}
}
}))
# Different state
self.hass.states.set('test.entity', 'worldz')
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))
# Different state
self.hass.states.set('test.entity', 'hello')
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))
def test_if_fires_on_change_with_template_2(self):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'template',
'value_template': '{{ not is_state("test.entity", "world") }}',
},
'action': {
'service': 'test.automation'
}
}
}))
self.hass.states.set('test.entity', 'world')
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))
self.hass.states.set('test.entity', 'home')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
self.hass.states.set('test.entity', 'work')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
self.hass.states.set('test.entity', 'not_home')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
self.hass.states.set('test.entity', 'world')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
self.hass.states.set('test.entity', 'home')
self.hass.pool.block_till_done()
self.assertEqual(2, len(self.calls))
def test_if_action(self):
automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'event',
'event_type': 'test_event',
},
'condition': [{
'platform': 'template',
'value_template': '{{ is_state("test.entity", "world") }}'
}],
'action': {
'service': 'test.automation'
}
}
})
# Condition is not true yet
self.hass.bus.fire('test_event')
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))
# Change condition to true, but it shouldn't be triggered yet
self.hass.states.set('test.entity', 'world')
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))
# Condition is true and event is triggered
self.hass.bus.fire('test_event')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_fires_on_change_with_bad_template(self):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'template',
'value_template': '{{ ',
},
'action': {
'service': 'test.automation'
}
}
}))
self.hass.states.set('test.entity', 'world')
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))
def test_if_fires_on_change_with_bad_template_2(self):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'template',
'value_template': '{{ xyz | round(0) }}',
},
'action': {
'service': 'test.automation'
}
}
}))
self.hass.states.set('test.entity', 'world')
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))

View File

@ -62,6 +62,15 @@ class TestLightMQTT(unittest.TestCase):
""" Stop down stuff we started. """ """ Stop down stuff we started. """
self.hass.stop() self.hass.stop()
def test_fail_setup_if_no_command_topic(self):
self.assertTrue(light.setup(self.hass, {
'light': {
'platform': 'mqtt',
'name': 'test',
}
}))
self.assertIsNone(self.hass.states.get('light.test'))
def test_no_color_or_brightness_if_no_topics(self): def test_no_color_or_brightness_if_no_topics(self):
self.assertTrue(light.setup(self.hass, { self.assertTrue(light.setup(self.hass, {
'light': { 'light': {
@ -77,7 +86,7 @@ class TestLightMQTT(unittest.TestCase):
self.assertIsNone(state.attributes.get('rgb_color')) self.assertIsNone(state.attributes.get('rgb_color'))
self.assertIsNone(state.attributes.get('brightness')) self.assertIsNone(state.attributes.get('brightness'))
fire_mqtt_message(self.hass, 'test_light_rgb/status', 'on') fire_mqtt_message(self.hass, 'test_light_rgb/status', 'ON')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
state = self.hass.states.get('light.test') state = self.hass.states.get('light.test')
@ -143,6 +152,39 @@ class TestLightMQTT(unittest.TestCase):
self.assertEqual([125, 125, 125], self.assertEqual([125, 125, 125],
light_state.attributes.get('rgb_color')) light_state.attributes.get('rgb_color'))
def test_controlling_state_via_topic_with_templates(self):
self.assertTrue(light.setup(self.hass, {
'light': {
'platform': 'mqtt',
'name': 'test',
'state_topic': 'test_light_rgb/status',
'command_topic': 'test_light_rgb/set',
'brightness_state_topic': 'test_light_rgb/brightness/status',
'rgb_state_topic': 'test_light_rgb/rgb/status',
'state_value_template': '{{ value_json.hello }}',
'brightness_value_template': '{{ value_json.hello }}',
'rgb_value_template': '{{ value_json.hello | join(",") }}',
}
}))
state = self.hass.states.get('light.test')
self.assertEqual(STATE_OFF, state.state)
self.assertIsNone(state.attributes.get('brightness'))
self.assertIsNone(state.attributes.get('rgb_color'))
fire_mqtt_message(self.hass, 'test_light_rgb/status',
'{"hello": "ON"}')
fire_mqtt_message(self.hass, 'test_light_rgb/brightness/status',
'{"hello": "50"}')
fire_mqtt_message(self.hass, 'test_light_rgb/rgb/status',
'{"hello": [1, 2, 3]}')
self.hass.pool.block_till_done()
state = self.hass.states.get('light.test')
self.assertEqual(STATE_ON, state.state)
self.assertEqual(50, state.attributes.get('brightness'))
self.assertEqual([1, 2, 3], state.attributes.get('rgb_color'))
def test_sending_mqtt_commands_and_optimistic(self): def test_sending_mqtt_commands_and_optimistic(self):
self.assertTrue(light.setup(self.hass, { self.assertTrue(light.setup(self.hass, {
'light': { 'light': {

View File

View File

@ -0,0 +1,41 @@
"""
tests.components.notify.test_demo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests notify demo component
"""
import unittest
import homeassistant.core as ha
import homeassistant.components.notify as notify
from homeassistant.components.notify import demo
class TestNotifyDemo(unittest.TestCase):
""" Test the demo notify. """
def setUp(self): # pylint: disable=invalid-name
self.hass = ha.HomeAssistant()
self.assertTrue(notify.setup(self.hass, {
'notify': {
'platform': 'demo'
}
}))
self.events = []
def record_event(event):
self.events.append(event)
self.hass.bus.listen(demo.EVENT_NOTIFY, record_event)
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
self.hass.stop()
def test_sending_templated_message(self):
self.hass.states.set('sensor.temperature', 10)
notify.send_message(self.hass, '{{ states.sensor.temperature.state }}',
'{{ states.sensor.temperature.name }}')
self.hass.pool.block_till_done()
last_event = self.events[-1]
self.assertEqual(last_event.data[notify.ATTR_TITLE], 'temperature')
self.assertEqual(last_event.data[notify.ATTR_MESSAGE], '10')

View File

@ -47,7 +47,7 @@ class TestSensorMQTT(unittest.TestCase):
'name': 'test', 'name': 'test',
'state_topic': 'test-topic', 'state_topic': 'test-topic',
'unit_of_measurement': 'fav unit', 'unit_of_measurement': 'fav unit',
'state_format': 'json:val' 'value_template': '{{ value_json.val }}'
} }
})) }))
@ -56,4 +56,3 @@ class TestSensorMQTT(unittest.TestCase):
state = self.hass.states.get('sensor.test') state = self.hass.states.get('sensor.test')
self.assertEqual('100', state.state) self.assertEqual('100', state.state)

View File

@ -90,7 +90,7 @@ class TestSensorMQTT(unittest.TestCase):
'command_topic': 'command-topic', 'command_topic': 'command-topic',
'payload_on': 'beer on', 'payload_on': 'beer on',
'payload_off': 'beer off', 'payload_off': 'beer off',
'state_format': 'json:val' 'value_template': '{{ value_json.val }}'
} }
})) }))

View File

@ -0,0 +1,225 @@
"""
tests.test_component_alexa
~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests Home Assistant Alexa component does what it should do.
"""
# pylint: disable=protected-access,too-many-public-methods
import unittest
import json
from unittest.mock import patch
import requests
from homeassistant import bootstrap, const
import homeassistant.core as ha
from homeassistant.components import alexa, http
API_PASSWORD = "test1234"
# Somehow the socket that holds the default port does not get released
# when we close down HA in a different test case. Until I have figured
# out what is going on, let's run this test on a different port.
SERVER_PORT = 8119
API_URL = "http://127.0.0.1:{}{}".format(SERVER_PORT, alexa.API_ENDPOINT)
HA_HEADERS = {const.HTTP_HEADER_HA_AUTH: API_PASSWORD}
hass = None
@patch('homeassistant.components.http.util.get_local_ip',
return_value='127.0.0.1')
def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name
""" Initalizes a Home Assistant server. """
global hass
hass = ha.HomeAssistant()
bootstrap.setup_component(
hass, http.DOMAIN,
{http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD,
http.CONF_SERVER_PORT: SERVER_PORT}})
bootstrap.setup_component(hass, alexa.DOMAIN, {
'alexa': {
'intents': {
'WhereAreWeIntent': {
'speech': {
'type': 'plaintext',
'text':
"""
{%- if is_state('device_tracker.paulus', 'home') and is_state('device_tracker.anne_therese', 'home') -%}
You are both home, you silly
{%- else -%}
Anne Therese is at {{ states("device_tracker.anne_therese") }} and Paulus is at {{ states("device_tracker.paulus") }}
{% endif %}
""",
}
},
'GetZodiacHoroscopeIntent': {
'speech': {
'type': 'plaintext',
'text': 'You told us your sign is {{ ZodiacSign }}.'
}
}
}
}
})
hass.start()
def tearDownModule(): # pylint: disable=invalid-name
""" Stops the Home Assistant server. """
hass.stop()
def _req(data={}):
return requests.post(API_URL, data=json.dumps(data), timeout=5,
headers=HA_HEADERS)
class TestAlexa(unittest.TestCase):
""" Test Alexa. """
def test_launch_request(self):
data = {
'version': '1.0',
'session': {
'new': True,
'sessionId': 'amzn1.echo-api.session.0000000-0000-0000-0000-00000000000',
'application': {
'applicationId': 'amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe'
},
'attributes': {},
'user': {
'userId': 'amzn1.account.AM3B00000000000000000000000'
}
},
'request': {
'type': 'LaunchRequest',
'requestId': 'amzn1.echo-api.request.0000000-0000-0000-0000-00000000000',
'timestamp': '2015-05-13T12:34:56Z'
}
}
req = _req(data)
self.assertEqual(200, req.status_code)
resp = req.json()
self.assertIn('outputSpeech', resp['response'])
def test_intent_request_with_slots(self):
data = {
'version': '1.0',
'session': {
'new': False,
'sessionId': 'amzn1.echo-api.session.0000000-0000-0000-0000-00000000000',
'application': {
'applicationId': 'amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe'
},
'attributes': {
'supportedHoroscopePeriods': {
'daily': True,
'weekly': False,
'monthly': False
}
},
'user': {
'userId': 'amzn1.account.AM3B00000000000000000000000'
}
},
'request': {
'type': 'IntentRequest',
'requestId': ' amzn1.echo-api.request.0000000-0000-0000-0000-00000000000',
'timestamp': '2015-05-13T12:34:56Z',
'intent': {
'name': 'GetZodiacHoroscopeIntent',
'slots': {
'ZodiacSign': {
'name': 'ZodiacSign',
'value': 'virgo'
}
}
}
}
}
req = _req(data)
self.assertEqual(200, req.status_code)
text = req.json().get('response', {}).get('outputSpeech', {}).get('text')
self.assertEqual('You told us your sign is virgo.', text)
def test_intent_request_without_slots(self):
data = {
'version': '1.0',
'session': {
'new': False,
'sessionId': 'amzn1.echo-api.session.0000000-0000-0000-0000-00000000000',
'application': {
'applicationId': 'amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe'
},
'attributes': {
'supportedHoroscopePeriods': {
'daily': True,
'weekly': False,
'monthly': False
}
},
'user': {
'userId': 'amzn1.account.AM3B00000000000000000000000'
}
},
'request': {
'type': 'IntentRequest',
'requestId': ' amzn1.echo-api.request.0000000-0000-0000-0000-00000000000',
'timestamp': '2015-05-13T12:34:56Z',
'intent': {
'name': 'WhereAreWeIntent',
}
}
}
req = _req(data)
self.assertEqual(200, req.status_code)
text = req.json().get('response', {}).get('outputSpeech', {}).get('text')
self.assertEqual('Anne Therese is at unknown and Paulus is at unknown', text)
hass.states.set('device_tracker.paulus', 'home')
hass.states.set('device_tracker.anne_therese', 'home')
req = _req(data)
self.assertEqual(200, req.status_code)
text = req.json().get('response', {}).get('outputSpeech', {}).get('text')
self.assertEqual('You are both home, you silly', text)
def test_session_ended_request(self):
data = {
'version': '1.0',
'session': {
'new': False,
'sessionId': 'amzn1.echo-api.session.0000000-0000-0000-0000-00000000000',
'application': {
'applicationId': 'amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe'
},
'attributes': {
'supportedHoroscopePeriods': {
'daily': True,
'weekly': False,
'monthly': False
}
},
'user': {
'userId': 'amzn1.account.AM3B00000000000000000000000'
}
},
'request': {
'type': 'SessionEndedRequest',
'requestId': 'amzn1.echo-api.request.0000000-0000-0000-0000-00000000000',
'timestamp': '2015-05-13T12:34:56Z',
'reason': 'USER_INITIATED'
}
}
req = _req(data)
self.assertEqual(200, req.status_code)
self.assertEqual('', req.text)

View File

@ -5,10 +5,11 @@ tests.test_component_http
Tests Home Assistant HTTP component does what it should do. Tests Home Assistant HTTP component does what it should do.
""" """
# pylint: disable=protected-access,too-many-public-methods # pylint: disable=protected-access,too-many-public-methods
import unittest from contextlib import closing
import json import json
from unittest.mock import patch
import tempfile import tempfile
import unittest
from unittest.mock import patch
import requests import requests
@ -326,6 +327,30 @@ class TestAPI(unittest.TestCase):
self.assertEqual(1, len(test_value)) self.assertEqual(1, len(test_value))
def test_api_template(self):
""" Test template API. """
hass.states.set('sensor.temperature', 10)
req = requests.post(
_url(const.URL_API_TEMPLATE),
data=json.dumps({"template":
'{{ states.sensor.temperature.state }}'}),
headers=HA_HEADERS)
self.assertEqual('10', req.text)
def test_api_template_error(self):
""" Test template API. """
hass.states.set('sensor.temperature', 10)
req = requests.post(
_url(const.URL_API_TEMPLATE),
data=json.dumps({"template":
'{{ states.sensor.temperature.state'}),
headers=HA_HEADERS)
self.assertEqual(422, req.status_code)
def test_api_event_forward(self): def test_api_event_forward(self):
""" Test setting up event forwarding. """ """ Test setting up event forwarding. """
@ -401,3 +426,61 @@ class TestAPI(unittest.TestCase):
}), }),
headers=HA_HEADERS) headers=HA_HEADERS)
self.assertEqual(200, req.status_code) self.assertEqual(200, req.status_code)
def test_stream(self):
listen_count = self._listen_count()
with closing(requests.get(_url(const.URL_API_STREAM),
stream=True, headers=HA_HEADERS)) as req:
data = self._stream_next_event(req)
self.assertEqual('ping', data)
self.assertEqual(listen_count + 1, self._listen_count())
hass.bus.fire('test_event')
hass.pool.block_till_done()
data = self._stream_next_event(req)
self.assertEqual('test_event', data['event_type'])
def test_stream_with_restricted(self):
listen_count = self._listen_count()
with closing(requests.get(_url(const.URL_API_STREAM),
data=json.dumps({
'restrict': 'test_event1,test_event3'}),
stream=True, headers=HA_HEADERS)) as req:
data = self._stream_next_event(req)
self.assertEqual('ping', data)
self.assertEqual(listen_count + 2, self._listen_count())
hass.bus.fire('test_event1')
hass.pool.block_till_done()
hass.bus.fire('test_event2')
hass.pool.block_till_done()
hass.bus.fire('test_event3')
hass.pool.block_till_done()
data = self._stream_next_event(req)
self.assertEqual('test_event1', data['event_type'])
data = self._stream_next_event(req)
self.assertEqual('test_event3', data['event_type'])
def _stream_next_event(self, stream):
data = b''
last_new_line = False
for dat in stream.iter_content(1):
if dat == b'\n' and last_new_line:
break
data += dat
last_new_line = dat == b'\n'
conv = data.decode('utf-8').strip()[6:]
return conv if conv == 'ping' else json.loads(conv)
def _listen_count(self):
""" Return number of event listeners. """
return sum(hass.bus.listeners.values())

127
tests/util/test_template.py Normal file
View File

@ -0,0 +1,127 @@
"""
tests.test_util
~~~~~~~~~~~~~~~~~
Tests Home Assistant util methods.
"""
# pylint: disable=too-many-public-methods
import unittest
import homeassistant.core as ha
from homeassistant.exceptions import TemplateError
from homeassistant.util import template
class TestUtilTemplate(unittest.TestCase):
def setUp(self): # pylint: disable=invalid-name
self.hass = ha.HomeAssistant()
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
self.hass.stop()
def test_referring_states_by_entity_id(self):
self.hass.states.set('test.object', 'happy')
self.assertEqual(
'happy',
template.render(self.hass, '{{ states.test.object.state }}'))
def test_iterating_all_states(self):
self.hass.states.set('test.object', 'happy')
self.hass.states.set('sensor.temperature', 10)
self.assertEqual(
'10happy',
template.render(
self.hass,
'{% for state in states %}{{ state.state }}{% endfor %}'))
def test_iterating_domain_states(self):
self.hass.states.set('test.object', 'happy')
self.hass.states.set('sensor.back_door', 'open')
self.hass.states.set('sensor.temperature', 10)
self.assertEqual(
'open10',
template.render(
self.hass,
'{% for state in states.sensor %}{{ state.state }}{% endfor %}'))
def test_rounding_value(self):
self.hass.states.set('sensor.temperature', 12.78)
self.assertEqual(
'12.8',
template.render(
self.hass,
'{{ states.sensor.temperature.state | round(1) }}'))
def test_rounding_value2(self):
self.hass.states.set('sensor.temperature', 12.78)
self.assertEqual(
'128',
template.render(
self.hass,
'{{ states.sensor.temperature.state | multiply(10) | round }}'))
def test_passing_vars_as_keywords(self):
self.assertEqual(
'127', template.render(self.hass, '{{ hello }}', hello=127))
def test_passing_vars_as_vars(self):
self.assertEqual(
'127', template.render(self.hass, '{{ hello }}', {'hello': 127}))
def test_render_with_possible_json_value_with_valid_json(self):
self.assertEqual(
'world',
template.render_with_possible_json_value(
self.hass, '{{ value_json.hello }}', '{"hello": "world"}'))
def test_render_with_possible_json_value_with_invalid_json(self):
self.assertEqual(
'',
template.render_with_possible_json_value(
self.hass, '{{ value_json }}', '{ I AM NOT JSON }'))
def test_render_with_possible_json_value_with_template_error(self):
self.assertEqual(
'hello',
template.render_with_possible_json_value(
self.hass, '{{ value_json', 'hello'))
def test_render_with_possible_json_value_with_template_error_error_value(self):
self.assertEqual(
'-',
template.render_with_possible_json_value(
self.hass, '{{ value_json', 'hello', '-'))
def test_raise_exception_on_error(self):
with self.assertRaises(TemplateError):
template.render(self.hass, '{{ invalid_syntax')
def test_if_state_exists(self):
self.hass.states.set('test.object', 'available')
self.assertEqual(
'exists',
template.render(
self.hass,
'{% if states.test.object %}exists{% else %}not exists{% endif %}'))
def test_is_state(self):
self.hass.states.set('test.object', 'available')
self.assertEqual(
'yes',
template.render(
self.hass,
'{% if is_state("test.object", "available") %}yes{% else %}no{% endif %}'))
def test_states_function(self):
self.hass.states.set('test.object', 'available')
self.assertEqual(
'available',
template.render(self.hass, '{{ states("test.object") }}'))
self.assertEqual(
'unknown',
template.render(self.hass, '{{ states("test.object2") }}'))