Merge branch 'dev' into mysensors-component-switch

Conflicts:
	homeassistant/components/sensor/mysensors.py
	requirements_all.txt
This commit is contained in:
MartinHjelmare 2015-12-23 23:45:32 +01:00
commit 77959341a3
72 changed files with 3158 additions and 497 deletions

View File

@ -36,32 +36,34 @@ omit =
homeassistant/components/*/mysensors.py
homeassistant/components/binary_sensor/arest.py
homeassistant/components/binary_sensor/rest.py
homeassistant/components/browser.py
homeassistant/components/camera/*
homeassistant/components/device_tracker/actiontec.py
homeassistant/components/device_tracker/aruba.py
homeassistant/components/device_tracker/asuswrt.py
homeassistant/components/device_tracker/ddwrt.py
homeassistant/components/device_tracker/geofancy.py
homeassistant/components/device_tracker/fritz.py
homeassistant/components/device_tracker/locative.py
homeassistant/components/device_tracker/icloud.py
homeassistant/components/device_tracker/luci.py
homeassistant/components/device_tracker/ubus.py
homeassistant/components/device_tracker/netgear.py
homeassistant/components/device_tracker/nmap_tracker.py
homeassistant/components/device_tracker/owntracks.py
homeassistant/components/device_tracker/snmp.py
homeassistant/components/device_tracker/thomson.py
homeassistant/components/device_tracker/tomato.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/downloader.py
homeassistant/components/ifttt.py
homeassistant/components/influxdb.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/hue.py
homeassistant/components/light/hyperion.py
homeassistant/components/light/limitlessled.py
homeassistant/components/media_player/cast.py
homeassistant/components/media_player/denon.py
homeassistant/components/media_player/firetv.py
@ -69,9 +71,8 @@ omit =
homeassistant/components/media_player/kodi.py
homeassistant/components/media_player/mpd.py
homeassistant/components/media_player/plex.py
homeassistant/components/media_player/squeezebox.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/nma.py
homeassistant/components/notify/pushbullet.py
@ -84,10 +85,11 @@ omit =
homeassistant/components/notify/xmpp.py
homeassistant/components/sensor/arest.py
homeassistant/components/sensor/bitcoin.py
homeassistant/components/sensor/command_sensor.py
homeassistant/components/sensor/cpuspeed.py
homeassistant/components/sensor/dht.py
homeassistant/components/sensor/dweet.py
homeassistant/components/sensor/efergy.py
homeassistant/components/sensor/eliqonline.py
homeassistant/components/sensor/forecast.py
homeassistant/components/sensor/glances.py
homeassistant/components/sensor/openweathermap.py
@ -98,10 +100,11 @@ omit =
homeassistant/components/sensor/systemmonitor.py
homeassistant/components/sensor/temper.py
homeassistant/components/sensor/time_date.py
homeassistant/components/sensor/torque.py
homeassistant/components/sensor/transmission.py
homeassistant/components/sensor/twitch.py
homeassistant/components/sensor/worldclock.py
homeassistant/components/switch/arest.py
homeassistant/components/switch/command_switch.py
homeassistant/components/switch/edimax.py
homeassistant/components/switch/hikvisioncam.py
homeassistant/components/switch/mystrom.py
@ -110,6 +113,7 @@ omit =
homeassistant/components/switch/rpi_gpio.py
homeassistant/components/switch/transmission.py
homeassistant/components/switch/wemo.py
homeassistant/components/thermostat/heatmiser.py
homeassistant/components/thermostat/homematic.py
homeassistant/components/thermostat/honeywell.py
homeassistant/components/thermostat/nest.py

View File

@ -2,11 +2,16 @@ sudo: false
language: python
cache:
directories:
- $HOME/virtualenv/python$TRAVIS_PYTHON_VERSION/
- $HOME/.cache/pip
# - "$HOME/virtualenv/python$TRAVIS_PYTHON_VERSION"
python:
- 3.4.2
- 3.5.0
- 3.4
- 3.5
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:
- 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:
- 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. There is no ordering right now, so just add it to the end of the file.
- Add any new dependencies to `requirements_all.txt` if needed. Use `script/gen_requirements_all.py`.
- 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).
- 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'))
logger = logging.getLogger('')
logger.addHandler(err_handler)
logger.setLevel(logging.INFO) # this sets the minimum log level
logger.setLevel(logging.INFO)
else:
_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() if 'value' in value}
else:
self.variables = {}
def add_card(self, card_type, title, content):
""" Add a card to the response. """
assert self.card is None
card = {
"type": card_type.value
}
if card_type == CardType.link_account:
self.card = card
return
card["title"] = self._render(title),
card["content"] = self._render(content)
self.card = card
def add_speech(self, speech_type, text):
""" Add speech to the response. """
assert self.speech is None
key = 'ssml' if speech_type == SpeechType.ssml else 'text'
self.speech = {
'type': speech_type.value,
key: self._render(text)
}
def add_reprompt(self, speech_type, text):
""" Add repromopt if user does not answer. """
assert self.reprompt is None
key = 'ssml' if speech_type == SpeechType.ssml else 'text'
self.reprompt = {
'type': speech_type.value,
key: self._render(text)
}
def as_dict(self):
""" Returns response in an Alexa valid dict. """
response = {
'shouldEndSession': self.should_end_session
}
if self.card is not None:
response['card'] = self.card
if self.speech is not None:
response['outputSpeech'] = self.speech
if self.reprompt is not None:
response['reprompt'] = {
'outputSpeech': self.reprompt
}
return {
'version': '1.0',
'sessionAttributes': self.session_attributes,
'response': response,
}
def _render(self, template_string):
""" Render a response, adding data from intent if available. """
return template.render(self.hass, template_string, self.variables)

View File

@ -12,16 +12,19 @@ import threading
import json
import homeassistant.core as ha
from homeassistant.exceptions import TemplateError
from homeassistant.helpers.state import TrackStates
import homeassistant.remote as rem
from homeassistant.util import template
from homeassistant.bootstrap import ERROR_LOG_FILENAME
from homeassistant.const import (
URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES, URL_API_STREAM,
URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, URL_API_COMPONENTS,
URL_API_CONFIG, URL_API_BOOTSTRAP, 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_UNPROCESSABLE_ENTITY)
HTTP_UNPROCESSABLE_ENTITY, HTTP_HEADER_CONTENT_TYPE,
CONTENT_TYPE_TEXT_PLAIN)
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_TEMPLATE,
_handle_post_api_template)
return True
@ -120,22 +126,23 @@ def _handle_get_api_stream(handler, path_match, data):
try:
wfile.write(msg.encode("UTF-8"))
wfile.flush()
handler.server.sessions.extend_validation(session_id)
except IOError:
except (IOError, ValueError):
# IOError: socket errors
# ValueError: raised when 'I/O operation on closed file'
block.set()
def forward_events(event):
""" Forwards events to the open request. """
nonlocal gracefully_closed
if block.is_set() or event.event_type == EVENT_TIME_CHANGED or \
restrict and event.event_type not in restrict:
if block.is_set() or event.event_type == EVENT_TIME_CHANGED:
return
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
gracefully_closed = True
block.set()
return
handler.server.sessions.extend_validation(session_id)
write_message(json.dumps(event, cls=rem.JSONEncoder))
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()
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:
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",
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):
@ -359,6 +374,22 @@ def _handle_post_api_log_out(handler, path_match, data):
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):
""" Generate services data to JSONify. """
return [{"domain": key, "services": value}

View File

@ -8,13 +8,14 @@ at https://home-assistant.io/components/automation/#numeric-state-trigger
"""
import logging
from homeassistant.const import CONF_VALUE_TEMPLATE
from homeassistant.helpers.event import track_state_change
from homeassistant.util import template
CONF_ENTITY_ID = "entity_id"
CONF_BELOW = "below"
CONF_ABOVE = "above"
CONF_ATTRIBUTE = "attribute"
_LOGGER = logging.getLogger(__name__)
@ -29,7 +30,7 @@ def trigger(hass, config, action):
below = config.get(CONF_BELOW)
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:
_LOGGER.error("Missing configuration key."
@ -37,13 +38,20 @@ def trigger(hass, config, action):
CONF_BELOW, CONF_ABOVE)
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
def state_automation_listener(entity, from_s, to_s):
""" Listens for state changes and calls action. """
# Fire action if we go from outside range into range
if _in_range(to_s, above, below, attribute) and \
(from_s is None or not _in_range(from_s, above, below, attribute)):
if _in_range(above, below, renderer(to_s)) and \
(from_s is None or not _in_range(above, below, renderer(from_s))):
action()
track_state_change(
@ -63,7 +71,7 @@ def if_action(hass, config):
below = config.get(CONF_BELOW)
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:
_LOGGER.error("Missing configuration key."
@ -71,22 +79,28 @@ def if_action(hass, config):
CONF_BELOW, CONF_ABOVE)
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():
""" Test numeric state condition. """
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
def _in_range(state, range_start, range_end, attribute):
def _in_range(range_start, range_end, value):
""" Checks if value is inside the range """
value = (state.state if attribute is None
else state.attributes.get(attribute))
try:
value = float(value)
except ValueError:
_LOGGER.warning("Missing value in numeric check")
_LOGGER.warning("Value returned from template is not a number: %s",
value)
return False
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/
"""
import logging
from homeassistant.const import CONF_VALUE_TEMPLATE
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.util import template
import homeassistant.components.mqtt as mqtt
_LOGGER = logging.getLogger(__name__)
@ -34,13 +37,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
config.get('state_topic', None),
config.get('qos', DEFAULT_QOS),
config.get('payload_on', DEFAULT_PAYLOAD_ON),
config.get('payload_off', DEFAULT_PAYLOAD_OFF))])
config.get('payload_off', DEFAULT_PAYLOAD_OFF),
config.get(CONF_VALUE_TEMPLATE))])
# pylint: disable=too-many-arguments, too-many-instance-attributes
class MqttBinarySensor(BinarySensorDevice):
""" Represents a binary sensor that is updated by MQTT. """
def __init__(self, hass, name, state_topic, qos, payload_on, payload_off):
def __init__(self, hass, name, state_topic, qos, payload_on, payload_off,
value_template):
self._hass = hass
self._name = name
self._state = False
@ -51,6 +56,9 @@ class MqttBinarySensor(BinarySensorDevice):
def message_received(topic, payload, qos):
""" A new MQTT message has been received. """
if value_template is not None:
payload = template.render_with_possible_json_value(
hass, value_template, payload)
if payload == self._payload_on:
self._state = True
self.update_ha_state()

View File

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

View File

@ -9,6 +9,7 @@ https://home-assistant.io/components/conversation/
import logging
import re
from homeassistant import core
from homeassistant.const import (
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+)')
REQUIREMENTS = ['fuzzywuzzy==0.8.0']
def setup(hass, config):
""" Registers the process service. """
from fuzzywuzzy import process as fuzzyExtract
logger = logging.getLogger(__name__)
def process(service):
@ -42,9 +47,11 @@ def setup(hass, config):
name, command = match.groups()
entity_ids = [
state.entity_id for state in hass.states.all()
if state.name.lower() == name]
entities = {state.entity_id: state.name for state in hass.states.all()}
entity_ids = fuzzyExtract.extractOne(name,
entities,
score_cutoff=65)[2]
if not entity_ids:
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

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

View File

@ -11,7 +11,6 @@ import logging
from . import version, mdi_version
import homeassistant.util as util
from homeassistant.const import URL_ROOT, HTTP_OK
from homeassistant.config import get_default_config_dir
DOMAIN = 'frontend'
DEPENDENCIES = ['api']
@ -22,7 +21,7 @@ _LOGGER = logging.getLogger(__name__)
FRONTEND_URLS = [
URL_ROOT, '/logbook', '/history', '/map', '/devService', '/devState',
'/devEvent', '/devInfo', '/states']
'/devEvent', '/devInfo', '/devTemplate', '/states']
_FINGERPRINT = re.compile(r'^(\w+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
@ -109,8 +108,6 @@ def _handle_get_local(handler, path_match, data):
"""
req_file = util.sanitize_path(path_match.group('file'))
path = os.path.join(get_default_config_dir(), 'www', req_file)
if not os.path.isfile(path):
return False
path = handler.server.hass.config.path('www', req_file)
handler.write_file(path)

View File

@ -13,7 +13,7 @@
<meta name='viewport' content='width=device-width, user-scalable=no'>
<meta name='theme-color' content='#03a9f4'>
<style>
#init {
#ha-init-skeleton {
display: -webkit-flex;
display: flex;
-webkit-flex-direction: column;
@ -34,7 +34,7 @@
<link rel='import' href='/static/{{ app_url }}' async>
</head>
<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>
var webComponentsSupported = ('registerElement' in document &&
'import' in document.createElement('link') &&

View File

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

File diff suppressed because one or more lines are too long

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

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. """
url = urlparse(self.path)
# Read query input
data = parse_qs(url.query)
# parse_qs gives a list for each value, take the latest element
for key in data:
data[key] = data[key][-1]
# Read query input. parse_qs gives a list for each value, we want last
data = {key: data[-1] for key, data in parse_qs(url.query).items()}
# Did we get post input ?
content_length = int(self.headers.get(HTTP_HEADER_CONTENT_LENGTH, 0))
@ -363,13 +359,13 @@ class RequestHandler(SimpleHTTPRequestHandler):
def set_session_cookie_header(self):
""" Add the header for the session cookie and return session id. """
if not self.authenticated:
return
return None
session_id = self.get_cookie_session_id()
if session_id is not None:
self.server.sessions.extend_validation(session_id)
return
return session_id
self.send_header(
'Set-Cookie',
@ -426,10 +422,10 @@ def session_valid_time():
class SessionStore(object):
""" Responsible for storing and retrieving http sessions """
def __init__(self, enabled=True):
def __init__(self):
""" Set up the session store """
self._sessions = {}
self.lock = threading.RLock()
self._lock = threading.RLock()
@util.Throttle(SESSION_CLEAR_INTERVAL)
def _remove_expired(self):
@ -441,7 +437,7 @@ class SessionStore(object):
def is_valid(self, key):
""" Return True if a valid session is given. """
with self.lock:
with self._lock:
self._remove_expired()
return (key in self._sessions and
@ -449,17 +445,19 @@ class SessionStore(object):
def extend_validation(self, key):
""" Extend a session validation time. """
with self.lock:
with self._lock:
if key not in self._sessions:
return
self._sessions[key] = session_valid_time()
def destroy(self, key):
""" Destroy a session by key. """
with self.lock:
with self._lock:
self._sessions.pop(key, None)
def create(self):
""" Creates a new session. """
with self.lock:
with self._lock:
session_id = util.get_random_string(20)
while session_id in self._sessions:

View File

@ -6,24 +6,24 @@ Allows to configure a MQTT light.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.mqtt/
"""
from functools import partial
import logging
import homeassistant.components.mqtt as mqtt
from homeassistant.components.light import (Light,
ATTR_BRIGHTNESS, ATTR_RGB_COLOR)
from homeassistant.util.template import render_with_possible_json_value
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "MQTT Light"
DEFAULT_NAME = 'MQTT Light'
DEFAULT_QOS = 0
DEFAULT_PAYLOAD_ON = "on"
DEFAULT_PAYLOAD_OFF = "off"
DEFAULT_PAYLOAD_ON = 'ON'
DEFAULT_PAYLOAD_OFF = 'OFF'
DEFAULT_OPTIMISTIC = False
DEPENDENCIES = ['mqtt']
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Add MQTT Light. """
@ -35,18 +35,16 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
add_devices_callback([MqttLight(
hass,
config.get('name', DEFAULT_NAME),
{
"state_topic": config.get('state_topic'),
"command_topic": config.get('command_topic'),
"brightness_state_topic": config.get('brightness_state_topic'),
"brightness_command_topic": config.get('brightness_command_topic'),
"rgb_state_topic": config.get('rgb_state_topic'),
"rgb_command_topic": config.get('rgb_command_topic')
},
{key: config.get(key) for key in
(typ + topic
for typ in ('', 'brightness_', 'rgb_')
for topic in ('state_topic', 'command_topic'))},
{key: config.get(key + '_value_template')
for key in ('state', 'brightness', 'rgb')},
config.get('qos', DEFAULT_QOS),
{
"on": config.get('payload_on', DEFAULT_PAYLOAD_ON),
"off": config.get('payload_off', DEFAULT_PAYLOAD_OFF)
'on': config.get('payload_on', DEFAULT_PAYLOAD_ON),
'off': config.get('payload_off', DEFAULT_PAYLOAD_OFF)
},
config.get('optimistic', DEFAULT_OPTIMISTIC))])
@ -55,7 +53,7 @@ class MqttLight(Light):
""" Provides a MQTT light. """
# 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._name = name
@ -68,8 +66,13 @@ class MqttLight(Light):
topic["brightness_state_topic"] is None)
self._state = False
templates = {key: ((lambda value: value) if tpl is None else
partial(render_with_possible_json_value, hass, tpl))
for key, tpl in templates.items()}
def state_received(topic, payload, qos):
""" A new MQTT message has been received. """
payload = templates['state'](payload)
if payload == self._payload["on"]:
self._state = True
elif payload == self._payload["off"]:
@ -83,7 +86,7 @@ class MqttLight(Light):
def brightness_received(topic, payload, qos):
""" A new MQTT message for the brightness has been received. """
self._brightness = int(payload)
self._brightness = int(templates['brightness'](payload))
self.update_ha_state()
if self._topic["brightness_state_topic"] is not None:
@ -95,7 +98,8 @@ class MqttLight(Light):
def rgb_received(topic, payload, qos):
""" 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()
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.const import CONF_ACCESS_TOKEN
REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/'
'42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip'
'#python-wink==0.2']
REQUIREMENTS = ['python-wink==0.3.1']
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
@ -45,10 +43,10 @@ class WinkLight(WinkToggleDevice):
brightness = kwargs.get(ATTR_BRIGHTNESS)
if brightness is not None:
self.wink.setState(True, brightness / 255)
self.wink.set_state(True, brightness=brightness / 255)
else:
self.wink.setState(True)
self.wink.set_state(True)
@property
def state_attributes(self):

View File

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

View File

@ -28,7 +28,7 @@ QUERY_EVENTS_BETWEEN = """
SELECT * FROM events WHERE time_fired > ? AND time_fired < ?
"""
EVENT_LOGBOOK_ENTRY = 'LOGBOOK_ENTRY'
EVENT_LOGBOOK_ENTRY = 'logbook_entry'
GROUP_BY_MINUTES = 15
@ -204,7 +204,7 @@ def humanify(events):
event.time_fired, "Home Assistant", action,
domain=HA_DOMAIN)
elif event.event_type == EVENT_LOGBOOK_ENTRY:
elif event.event_type.lower() == EVENT_LOGBOOK_ENTRY:
domain = event.data.get(ATTR_DOMAIN)
entity_id = event.data.get(ATTR_ENTITY_ID)
if domain is None and entity_id is not None:

View File

@ -76,8 +76,12 @@ def setup(hass, config=None):
logfilter[LOGGER_LOGS] = logs
logger = logging.getLogger('')
logger.setLevel(logging.NOTSET)
# Set log filter for all log handler
for handler in logging.root.handlers:
handler.setLevel(logging.NOTSET)
handler.addFilter(HomeAssistantLogFilter(logfilter))
return True

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

View File

@ -21,14 +21,14 @@ from homeassistant.const import (
from homeassistant.components.media_player import (
MediaPlayerDevice,
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)
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['python-mpd2==0.5.4']
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
@ -141,7 +141,13 @@ class MpdDevice(MediaPlayerDevice):
@property
def media_title(self):
""" 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
def media_artist(self):
@ -163,9 +169,13 @@ class MpdDevice(MediaPlayerDevice):
return SUPPORT_MPD
def turn_off(self):
""" Service to exit the running MPD. """
""" Service to send the MPD the command to stop playing. """
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):
""" Sets volume """
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
https://home-assistant.io/components/mqtt/
"""
import json
import logging
import os
import socket
@ -33,7 +32,7 @@ DEFAULT_RETAIN = False
SERVICE_PUBLISH = 'publish'
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_PORT = 'port'
@ -136,33 +135,6 @@ def setup(hass, config):
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
class MQTT(object):
""" Implements messaging service for MQTT. """

View File

@ -13,6 +13,7 @@ import os
import homeassistant.bootstrap as bootstrap
from homeassistant.config import load_yaml_config_file
from homeassistant.helpers import config_per_platform
from homeassistant.util import template
from homeassistant.const import CONF_NAME
@ -33,9 +34,16 @@ SERVICE_NOTIFY = "notify"
_LOGGER = logging.getLogger(__name__)
def send_message(hass, message):
def send_message(hass, message, title=None):
""" 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):
@ -70,8 +78,10 @@ def setup(hass, config):
if message is None:
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)
message = template.render(hass, message)
notify_service.send_message(message, title=title, target=target)

View File

@ -8,7 +8,10 @@ https://home-assistant.io/components/rollershutter.mqtt/
"""
import logging
import homeassistant.components.mqtt as mqtt
from homeassistant.const import CONF_VALUE_TEMPLATE
from homeassistant.components.rollershutter import RollershutterDevice
from homeassistant.util import template
_LOGGER = logging.getLogger(__name__)
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_down', DEFAULT_PAYLOAD_DOWN),
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
class MqttRollershutter(RollershutterDevice):
""" Represents a rollershutter that can be controlled using MQTT. """
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._hass = hass
self._name = name
@ -53,16 +56,17 @@ class MqttRollershutter(RollershutterDevice):
self._payload_up = payload_up
self._payload_down = payload_down
self._payload_stop = payload_stop
self._parse = mqtt.FmtParser(state_format)
if self._state_topic is None:
return
def message_received(topic, payload, qos):
""" A new MQTT message has been received. """
value = self._parse(payload)
if value.isnumeric() and 0 <= int(value) <= 100:
self._state = int(value)
if value_template is not None:
payload = template.render_with_possible_json_value(
hass, value_template, payload)
if payload.isnumeric() and 0 <= int(payload) <= 100:
self._state = int(payload)
self.update_ha_state()
else:
_LOGGER.warning(

View File

@ -11,9 +11,11 @@ import logging
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.util import Throttle
from homeassistant.util import template, Throttle
_LOGGER = logging.getLogger(__name__)
@ -50,36 +52,50 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
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 = []
if var_conf is not None:
for variable in config['monitored_variables']:
for variable in var_conf:
if variable['name'] not in response['variables']:
_LOGGER.error('Variable: "%s" does not exist',
variable['name'])
continue
renderer = make_renderer(variable.get(CONF_VALUE_TEMPLATE))
dev.append(ArestSensor(arest,
resource,
config.get('name', response['name']),
variable['name'],
variable=variable['name'],
unit_of_measurement=variable.get(
'unit_of_measurement')))
ATTR_UNIT_OF_MEASUREMENT),
renderer=renderer))
if pins is not None:
for pinnum, pin in pins.items():
renderer = make_renderer(pin.get(CONF_VALUE_TEMPLATE))
dev.append(ArestSensor(ArestData(resource, pinnum),
resource,
config.get('name', response['name']),
pin.get('name'),
pin=pinnum,
unit_of_measurement=pin.get(
'unit_of_measurement'),
corr_factor=pin.get('correction_factor',
None),
decimal_places=pin.get('decimal_places',
None)))
ATTR_UNIT_OF_MEASUREMENT),
renderer=renderer))
add_devices(dev)
@ -89,8 +105,7 @@ class ArestSensor(Entity):
""" Implements an aREST sensor for exposed variables. """
def __init__(self, arest, resource, location, name, variable=None,
pin=None, unit_of_measurement=None, corr_factor=None,
decimal_places=None):
pin=None, unit_of_measurement=None, renderer=None):
self.arest = arest
self._resource = resource
self._name = '{} {}'.format(location.title(), name.title()) \
@ -99,8 +114,7 @@ class ArestSensor(Entity):
self._pin = pin
self._state = 'n/a'
self._unit_of_measurement = unit_of_measurement
self._corr_factor = corr_factor
self._decimal_places = decimal_places
self._renderer = renderer
self.update()
if self._pin is not None:
@ -126,17 +140,11 @@ class ArestSensor(Entity):
if 'error' in values:
return values['error']
elif 'value' in values:
value = values['value']
if self._corr_factor is not None:
value = float(value) * float(self._corr_factor)
if self._decimal_places is not None:
value = round(value, self._decimal_places)
if self._decimal_places == 0:
value = int(value)
return value
else:
return values.get(self._variable, 'n/a')
value = self._renderer(values.get('value',
values.get(self._variable,
'N/A')))
return value
def update(self):
""" Gets the latest data from aREST API. """

View File

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

@ -79,7 +79,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if resource not in SENSOR_TYPES:
_LOGGER.error('Sensor type: "%s" does not exist', resource)
else:
dev.append(GlancesSensor(rest, resource))
dev.append(GlancesSensor(rest, config.get('name'), resource))
add_devices(dev)
@ -87,9 +87,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class GlancesSensor(Entity):
""" Implements a Glances sensor. """
def __init__(self, rest, sensor_type):
def __init__(self, rest, name, sensor_type):
self.rest = rest
self._name = SENSOR_TYPES[sensor_type][0]
self._name = name
self.type = sensor_type
self._state = STATE_UNKNOWN
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
@ -98,7 +98,10 @@ class GlancesSensor(Entity):
@property
def name(self):
""" 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
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/
"""
import logging
from homeassistant.const import CONF_VALUE_TEMPLATE
from homeassistant.helpers.entity import Entity
from homeassistant.util import template
import homeassistant.components.mqtt as mqtt
_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('qos', DEFAULT_QOS),
config.get('unit_of_measurement'),
config.get('state_format'))])
config.get(CONF_VALUE_TEMPLATE))])
# pylint: disable=too-many-arguments, too-many-instance-attributes
class MqttSensor(Entity):
""" Represents a sensor that can be updated using MQTT. """
def __init__(self, hass, name, state_topic, qos, unit_of_measurement,
state_format):
value_template):
self._state = "-"
self._hass = hass
self._name = name
self._state_topic = state_topic
self._qos = qos
self._unit_of_measurement = unit_of_measurement
self._parse = mqtt.FmtParser(state_format)
def message_received(topic, payload, qos):
""" 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()
mqtt.subscribe(hass, self._state_topic, message_received, self._qos)

View File

@ -1,18 +1,17 @@
"""
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
https://home-assistant.io/components/sensor.rest/
"""
from datetime import timedelta
from json import loads
import logging
import requests
from homeassistant.util import Throttle
from homeassistant.const import (CONF_VALUE_TEMPLATE, STATE_UNKNOWN)
from homeassistant.util import template, Throttle
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@ -48,28 +47,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
response = requests.post(resource, data=payload, timeout=10,
verify=verify_ssl)
if not response.ok:
_LOGGER.error('Response status is "%s"', response.status_code)
_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.')
_LOGGER.error("Missing resource or schema in configuration. "
"Add http:// or https:// to your URL")
return False
except requests.exceptions.ConnectionError:
_LOGGER.error('No route to resource/endpoint. '
'Please check the URL in the configuration file.')
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)
_LOGGER.error("No route to resource/endpoint: %s", resource)
return False
if use_get:
@ -77,39 +62,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
elif use_post:
rest = RestDataPost(resource, payload, verify_ssl)
add_devices([RestSensor(rest,
add_devices([RestSensor(hass,
rest,
config.get('name', DEFAULT_NAME),
config.get('variable'),
config.get('unit_of_measurement'),
config.get('correction_factor', None),
config.get('decimal_places', None))])
config.get(CONF_VALUE_TEMPLATE))])
# pylint: disable=too-many-arguments
class RestSensor(Entity):
""" Implements a REST sensor. """
def __init__(self, rest, name, variable, unit_of_measurement, corr_factor,
decimal_places):
def __init__(self, hass, rest, name, unit_of_measurement, value_template):
self._hass = hass
self.rest = rest
self._name = name
self._variable = variable
self._state = 'n/a'
self._state = STATE_UNKNOWN
self._unit_of_measurement = unit_of_measurement
self._corr_factor = corr_factor
self._decimal_places = decimal_places
self._value_template = value_template
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
def name(self):
""" The name of the sensor. """
@ -133,18 +105,10 @@ class RestSensor(Entity):
if 'error' in value:
self._state = value['error']
else:
try:
if value is not None:
value = RestSensor.extract_value(value, self._variable)
if self._corr_factor is not None:
value = float(value) * float(self._corr_factor)
if self._decimal_places is not None:
value = round(value, self._decimal_places)
if self._decimal_places == 0:
value = int(value)
self._state = value
except ValueError:
self._state = RestSensor.extract_value(value, self._variable)
if self._value_template is not None:
value = template.render_with_possible_json_value(
self._hass, self._value_template, value, STATE_UNKNOWN)
self._state = value
# pylint: disable=too-few-public-methods
@ -164,10 +128,10 @@ class RestDataGet(object):
verify=self._verify_ssl)
if 'error' in self.data:
del self.data['error']
self.data = response.json()
self.data = response.text
except requests.exceptions.ConnectionError:
_LOGGER.error("No route to resource/endpoint.")
self.data['error'] = 'N/A'
_LOGGER.error("No route to resource/endpoint: %s", self._resource)
self.data['error'] = STATE_UNKNOWN
# pylint: disable=too-few-public-methods
@ -188,7 +152,7 @@ class RestDataPost(object):
timeout=10, verify=self._verify_ssl)
if 'error' in self.data:
del self.data['error']
self.data = response.json()
self.data = response.text
except requests.exceptions.ConnectionError:
_LOGGER.error("No route to resource/endpoint.")
self.data['error'] = 'N/A'
_LOGGER.error("No route to resource/endpoint: %s", self._resource)
self.data['error'] = STATE_UNKNOWN

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.const import CONF_ACCESS_TOKEN, STATE_OPEN, STATE_CLOSED
REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/'
'42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip'
'#python-wink==0.2']
REQUIREMENTS = ['python-wink==0.3.1']
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)
add_devices(WinkSensorDevice(sensor) for sensor in pywink.get_sensors())
add_devices(WinkEggMinder(eggtray) for eggtray in pywink.get_eggtrays())
class WinkSensorDevice(Entity):
@ -48,7 +47,7 @@ class WinkSensorDevice(Entity):
@property
def unique_id(self):
""" Returns the id of this wink sensor """
return "{}.{}".format(self.__class__, self.wink.deviceId())
return "{}.{}".format(self.__class__, self.wink.device_id())
@property
def name(self):
@ -57,9 +56,35 @@ class WinkSensorDevice(Entity):
def update(self):
""" Update state of the sensor. """
self.wink.updateState()
self.wink.update_state()
@property
def is_open(self):
""" True if door is open. """
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

@ -0,0 +1,256 @@
"""
homeassistant.components.sensor.yr
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Yr.no weather service.
Configuration:
Will show a symbol for the current weather as default:
sensor:
platform: yr
Will show temperatue and wind direction:
sensor:
platform: yr
monitored_conditions:
- temperature
- windDirection
Will show all available sensors:
sensor:
platform: yr
monitored_conditions:
- temperature
- symbol
- precipitation
- windSpeed
- pressure
- windDirection
- humidity
- fog
- cloudiness
- lowClouds
- mediumClouds
- highClouds
- dewpointTemperature
"""
import logging
import datetime
import urllib.request
import requests
from homeassistant.const import ATTR_ENTITY_PICTURE
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['xmltodict', 'astral==0.8.1']
# Sensor types are defined like so:
SENSOR_TYPES = {
'symbol': ['Symbol', ''],
'precipitation': ['Condition', 'mm'],
'temperature': ['Temperature', '°C'],
'windSpeed': ['Wind speed', 'm/s'],
'pressure': ['Pressure', 'hPa'],
'windDirection': ['Wind direction', '°'],
'humidity': ['Humidity', '%'],
'fog': ['Fog', '%'],
'cloudiness': ['Cloudiness', '%'],
'lowClouds': ['Low clouds', '%'],
'mediumClouds': ['Medium clouds', '%'],
'highClouds': ['High clouds', '%'],
'dewpointTemperature': ['Dewpoint temperature', '°C'],
}
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Get the yr.no sensor. """
if None in (hass.config.latitude, hass.config.longitude):
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
return False
from astral import Location, GoogleGeocoder
location = Location(('', '', hass.config.latitude, hass.config.longitude,
hass.config.time_zone, 0))
google = GoogleGeocoder()
try:
google._get_elevation(location) # pylint: disable=protected-access
_LOGGER.info(
'Retrieved elevation from Google: %s', location.elevation)
elevation = location.elevation
except urllib.error.URLError:
# If no internet connection available etc.
elevation = 0
coordinates = dict(lat=hass.config.latitude,
lon=hass.config.longitude, msl=elevation)
weather = YrData(coordinates)
dev = []
if 'monitored_conditions' in config:
for variable in config['monitored_conditions']:
if variable not in SENSOR_TYPES:
_LOGGER.error('Sensor type: "%s" does not exist', variable)
else:
dev.append(YrSensor(variable, weather))
# add symbol as default sensor
if len(dev) == 0:
dev.append(YrSensor("symbol", weather))
add_devices(dev)
# pylint: disable=too-many-instance-attributes
class YrSensor(Entity):
""" Implements an Yr.no sensor. """
def __init__(self, sensor_type, weather):
self.client_name = 'yr'
self._name = SENSOR_TYPES[sensor_type][0]
self.type = sensor_type
self._state = None
self._weather = weather
self._info = ''
self._unit_of_measurement = SENSOR_TYPES[self.type][1]
self._update = datetime.datetime.fromtimestamp(0)
self.update()
@property
def name(self):
return '{} {}'.format(self.client_name, self._name)
@property
def state(self):
""" Returns the state of the device. """
return self._state
@property
def state_attributes(self):
""" Returns state attributes. """
data = {}
data[''] = "Weather forecast from yr.no, delivered by the"\
" Norwegian Meteorological Institute and the NRK"
if self.type == 'symbol':
symbol_nr = self._state
data[ATTR_ENTITY_PICTURE] = "http://api.met.no/weatherapi/weathericon/1.1/" \
"?symbol=" + str(symbol_nr) + \
";content_type=image/png"
return data
@property
def unit_of_measurement(self):
""" Unit of measurement of this entity, if any. """
return self._unit_of_measurement
@property
def should_poll(self):
""" Return True if entity has to be polled for state. """
return True
# pylint: disable=too-many-branches, too-many-return-statements
def update(self):
""" Gets the latest data from yr.no and updates the states. """
self._weather.update()
now = datetime.datetime.now()
# check if data should be updated
if now <= self._update:
return
time_data = self._weather.data['product']['time']
# pylint: disable=consider-using-enumerate
# find sensor
for k in range(len(time_data)):
valid_from = datetime.datetime.strptime(time_data[k]['@from'],
"%Y-%m-%dT%H:%M:%SZ")
valid_to = datetime.datetime.strptime(time_data[k]['@to'],
"%Y-%m-%dT%H:%M:%SZ")
self._update = valid_to
self._info = "Forecast between " + time_data[k]['@from'] \
+ " and " + time_data[k]['@to'] + ". "
temp_data = time_data[k]['location']
if self.type not in temp_data and now >= valid_to:
continue
if self.type == 'precipitation' and valid_from < now:
self._state = temp_data[self.type]['@value']
return
elif self.type == 'symbol' and valid_from < now:
self._state = temp_data[self.type]['@number']
return
elif self.type == 'temperature':
self._state = temp_data[self.type]['@value']
return
elif self.type == 'windSpeed':
self._state = temp_data[self.type]['@mps']
return
elif self.type == 'pressure':
self._state = temp_data[self.type]['@value']
return
elif self.type == 'windDirection':
self._state = float(temp_data[self.type]['@deg'])
return
elif self.type == 'humidity':
self._state = temp_data[self.type]['@value']
return
elif self.type == 'fog':
self._state = temp_data[self.type]['@percent']
return
elif self.type == 'cloudiness':
self._state = temp_data[self.type]['@percent']
return
elif self.type == 'lowClouds':
self._state = temp_data[self.type]['@percent']
return
elif self.type == 'mediumClouds':
self._state = temp_data[self.type]['@percent']
return
elif self.type == 'highClouds':
self._state = temp_data[self.type]['@percent']
return
elif self.type == 'dewpointTemperature':
self._state = temp_data[self.type]['@value']
return
# pylint: disable=too-few-public-methods
class YrData(object):
""" Gets the latest data and updates the states. """
def __init__(self, coordinates):
self._url = 'http://api.yr.no/weatherapi/locationforecast/1.9/?' \
'lat={lat};lon={lon};msl={msl}'.format(**coordinates)
self._nextrun = datetime.datetime.fromtimestamp(0)
self.update()
def update(self):
""" Gets the latest data from yr.no """
now = datetime.datetime.now()
# check if new will be available
if now <= self._nextrun:
return
try:
response = requests.get(self._url)
except requests.RequestException:
return
if response.status_code != 200:
return
data = response.text
import xmltodict
self.data = xmltodict.parse(data)['weatherdata']
model = self.data['meta']['model']
if '@nextrun' not in model:
model = model[0]
self._nextrun = datetime.datetime.strptime(model['@nextrun'],
"%Y-%m-%dT%H:%M:%SZ")

View File

@ -8,7 +8,9 @@ https://home-assistant.io/components/switch.mqtt/
"""
import logging
import homeassistant.components.mqtt as mqtt
from homeassistant.const import CONF_VALUE_TEMPLATE
from homeassistant.components.switch import SwitchDevice
from homeassistant.util import template
_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_off', DEFAULT_PAYLOAD_OFF),
config.get('optimistic', DEFAULT_OPTIMISTIC),
config.get('state_format'))])
config.get(CONF_VALUE_TEMPLATE))])
# pylint: disable=too-many-arguments, too-many-instance-attributes
class MqttSwitch(SwitchDevice):
""" Represents a switch that can be toggled using MQTT. """
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._hass = hass
self._name = name
@ -58,11 +60,12 @@ class MqttSwitch(SwitchDevice):
self._payload_on = payload_on
self._payload_off = payload_off
self._optimistic = optimistic
self._parse = mqtt.FmtParser(state_format)
def message_received(topic, payload, qos):
""" 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:
self._state = True
self.update_ha_state()

View File

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

View File

@ -18,7 +18,7 @@ DEFAULT_BODY_ON = "ON"
DEFAULT_BODY_OFF = "OFF"
# pylint: disable=unused-argument
# pylint: disable=unused-argument,
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Get REST switch. """
@ -32,11 +32,10 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
requests.get(resource, timeout=10)
except requests.exceptions.MissingSchema:
_LOGGER.error("Missing resource or schema in configuration. "
"Add http:// to your URL.")
"Add http:// or https:// to your URL")
return False
except requests.exceptions.ConnectionError:
_LOGGER.error("No route to resource/endpoint. "
"Please check the IP address in the configuration file.")
_LOGGER.error("No route to resource/endpoint: %s", resource)
return False
add_devices_callback([RestSwitch(

View File

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

View File

@ -87,7 +87,13 @@ class Thermostat(ThermostatDevice):
@property
def target_temperature(self):
""" 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
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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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
https://home-assistant.io/components/wink/
"""
@ -17,9 +16,7 @@ from homeassistant.const import (
ATTR_SERVICE, ATTR_DISCOVERED, ATTR_FRIENDLY_NAME)
DOMAIN = "wink"
REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/'
'42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip'
'#python-wink==0.2']
REQUIREMENTS = ['python-wink==0.3.1']
DISCOVER_LIGHTS = "wink.lights"
DISCOVER_SWITCHES = "wink.switches"
@ -41,7 +38,8 @@ def setup(hass, config):
for component_name, func_exists, discovery_type in (
('light', pywink.get_bulbs, DISCOVER_LIGHTS),
('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)):
if func_exists():
@ -68,7 +66,7 @@ class WinkToggleDevice(ToggleEntity):
@property
def unique_id(self):
""" Returns the id of this Wink switch. """
return "{}.{}".format(self.__class__, self.wink.deviceId())
return "{}.{}".format(self.__class__, self.wink.device_id())
@property
def name(self):
@ -89,12 +87,12 @@ class WinkToggleDevice(ToggleEntity):
def turn_on(self, **kwargs):
""" Turns the switch on. """
self.wink.setState(True)
self.wink.set_state(True)
def turn_off(self):
""" Turns the switch off. """
self.wink.setState(False)
self.wink.set_state(False)
def update(self):
""" Update state of the light. """
self.wink.updateState()
self.wink.update_state()

View File

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

View File

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

View File

@ -113,12 +113,16 @@ class EntityComponent(object):
def _update_entity_states(self, now):
""" Update the states of all the entities. """
with self.lock:
# We copy the entities because new entities might be detected
# during state update causing deadlocks.
entities = list(entity for entity in self.entities.values()
if entity.should_poll)
self.logger.info("Updating %s entities", self.domain)
with self.lock:
for entity in self.entities.values():
if entity.should_poll:
entity.update_ha_state(True)
for entity in entities:
entity.update_ha_state(True)
def _entity_discovered(self, service, info):
""" Called when a entity is discovered. """

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
pytz>=2015.4
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
pip>=7.0.0
vincenty==0.1.3
jinja2>=2.8
# homeassistant.components.arduino
PyMata==2.07a
# homeassistant.components.conversation
fuzzywuzzy==0.8.0
# homeassistant.components.device_tracker.icloud
pyicloud==0.7.2
# homeassistant.components.device_tracker.netgear
pynetgear==0.3
@ -59,10 +66,10 @@ https://github.com/pavoni/home-assistant-vera-api/archive/efdba4e63d58a30bc9b36d
# homeassistant.components.lock.wink
# homeassistant.components.sensor.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
pychromecast==0.6.12
pychromecast==0.6.13
# homeassistant.components.media_player.kodi
jsonrpc-requests==0.1
@ -82,9 +89,6 @@ https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b6
# homeassistant.components.mqtt
paho-mqtt==1.1
# homeassistant.components.mqtt
jsonpath-rw==1.4.0
# homeassistant.components.mysensors
https://github.com/theolind/pymysensors/archive/2aa8f32908e8c5bb3e5c77c5851db778f8635792.zip#pymysensors==0.3
@ -121,6 +125,12 @@ py-cpuinfo==0.1.6
# homeassistant.components.sensor.dht
# 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
python-forecastio==1.3.3
@ -144,7 +154,14 @@ https://github.com/rkabadi/temper-python/archive/3dbdaf2d87b8db9a3cd6e5585fc7045
# homeassistant.components.switch.transmission
transmissionrpc==0.11
# homeassistant.components.sensor.twitch
python-twitch==1.2.0
# homeassistant.components.sensor.yr
xmltodict
# homeassistant.components.sun
# homeassistant.components.sensor.yr
astral==0.8.1
# homeassistant.components.switch.edimax
@ -154,11 +171,14 @@ https://github.com/rkabadi/pyedimax/archive/365301ce3ff26129a7910c501ead09ea625f
hikvision==0.4
# homeassistant.components.switch.orvibo
orvibo==1.0.1
orvibo==1.1.0
# homeassistant.components.switch.wemo
pywemo==0.3.3
# homeassistant.components.thermostat.heatmiser
heatmiserV3==0.9.1
# homeassistant.components.thermostat.honeywell
evohomeclient==0.2.4

View File

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

View File

@ -1,10 +1,18 @@
cd "$(dirname "$0")/.."
echo "Update the submodule to latest version..."
git submodule update
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.."
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

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

View File

@ -8,10 +8,11 @@ import importlib
import os
import pkgutil
import re
import sys
COMMENT_REQUIREMENTS = [
'RPi.GPIO',
'Adafruit_Python_DHT'
'Adafruit_Python_DHT',
]
@ -67,8 +68,9 @@ def gather_modules():
reqs.setdefault(req, []).append(package)
if errors:
print("Found errors")
print('\n'.join(errors))
print("******* ERROR")
print("Errors while importing: ", ', '.join(errors))
print("Make sure you import 3rd party libraries inside methods.")
return None
output.append('# Home Assistant core')
@ -94,6 +96,12 @@ def write_file(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():
""" Main """
if not os.path.isfile('requirements_all.txt'):
@ -103,7 +111,16 @@ def main():
data = gather_modules()
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)

View File

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

View File

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

View File

@ -295,7 +295,7 @@ class TestAutomationNumericState(unittest.TestCase):
'trigger': {
'platform': 'numeric_state',
'entity_id': 'test.entity',
'attribute': 'test_attribute',
'value_template': '{{ state.attributes.test_attribute }}',
'below': 10,
},
'action': {
@ -314,7 +314,7 @@ class TestAutomationNumericState(unittest.TestCase):
'trigger': {
'platform': 'numeric_state',
'entity_id': 'test.entity',
'attribute': 'test_attribute',
'value_template': '{{ state.attributes.test_attribute }}',
'below': 10,
},
'action': {
@ -333,7 +333,7 @@ class TestAutomationNumericState(unittest.TestCase):
'trigger': {
'platform': 'numeric_state',
'entity_id': 'test.entity',
'attribute': 'test_attribute',
'value_template': '{{ state.attributes.test_attribute }}',
'below': 10,
},
'action': {
@ -352,7 +352,7 @@ class TestAutomationNumericState(unittest.TestCase):
'trigger': {
'platform': 'numeric_state',
'entity_id': 'test.entity',
'attribute': 'test_attribute',
'value_template': '{{ state.attributes.test_attribute }}',
'below': 10,
},
'action': {
@ -371,7 +371,7 @@ class TestAutomationNumericState(unittest.TestCase):
'trigger': {
'platform': 'numeric_state',
'entity_id': 'test.entity',
'attribute': 'test_attribute',
'value_template': '{{ state.attributes.test_attribute }}',
'below': 10,
},
'action': {
@ -384,13 +384,51 @@ class TestAutomationNumericState(unittest.TestCase):
self.hass.pool.block_till_done()
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):
self.assertTrue(automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'numeric_state',
'entity_id': 'test.entity',
'attribute': 'test_attribute',
'value_template': '{{ state.attributes.test_attribute }}',
'below': 10,
},
'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. """
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):
self.assertTrue(light.setup(self.hass, {
'light': {
@ -77,7 +86,7 @@ class TestLightMQTT(unittest.TestCase):
self.assertIsNone(state.attributes.get('rgb_color'))
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()
state = self.hass.states.get('light.test')
@ -143,6 +152,39 @@ class TestLightMQTT(unittest.TestCase):
self.assertEqual([125, 125, 125],
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):
self.assertTrue(light.setup(self.hass, {
'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',
'state_topic': 'test-topic',
'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')
self.assertEqual('100', state.state)

View File

@ -0,0 +1,69 @@
"""
tests.components.sensor.test_yr
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests Yr sensor.
"""
import unittest
import homeassistant.core as ha
import homeassistant.components.sensor as sensor
class TestSensorYr(unittest.TestCase):
""" Test the Yr sensor. """
def setUp(self): # pylint: disable=invalid-name
self.hass = ha.HomeAssistant()
latitude = 32.87336
longitude = 117.22743
# Compare it with the real data
self.hass.config.latitude = latitude
self.hass.config.longitude = longitude
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
self.hass.stop()
def test_default_setup(self):
self.assertTrue(sensor.setup(self.hass, {
'sensor': {
'platform': 'yr',
}
}))
state = self.hass.states.get('sensor.yr_symbol')
self.assertTrue(state.state.isnumeric())
self.assertEqual(None,
state.attributes.get('unit_of_measurement'))
def test_custom_setup(self):
self.assertTrue(sensor.setup(self.hass, {
'sensor': {
'platform': 'yr',
'monitored_conditions': {'pressure', 'windDirection', 'humidity', 'fog', 'windSpeed'}
}
}))
state = self.hass.states.get('sensor.yr_symbol')
self.assertEqual(None, state)
state = self.hass.states.get('sensor.yr_pressure')
self.assertEqual('hPa',
state.attributes.get('unit_of_measurement'))
state = self.hass.states.get('sensor.yr_wind_direction')
self.assertEqual('°',
state.attributes.get('unit_of_measurement'))
state = self.hass.states.get('sensor.yr_humidity')
self.assertEqual('%',
state.attributes.get('unit_of_measurement'))
state = self.hass.states.get('sensor.yr_fog')
self.assertEqual('%',
state.attributes.get('unit_of_measurement'))
state = self.hass.states.get('sensor.yr_wind_speed')
self.assertEqual('m/s',
state.attributes.get('unit_of_measurement'))

View File

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

View File

@ -0,0 +1,264 @@
"""
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_with_slots_but_no_value(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',
}
}
}
}
}
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 .', 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.
"""
# pylint: disable=protected-access,too-many-public-methods
import unittest
from contextlib import closing
import json
from unittest.mock import patch
import tempfile
import unittest
from unittest.mock import patch
import requests
@ -326,6 +327,30 @@ class TestAPI(unittest.TestCase):
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):
""" Test setting up event forwarding. """
@ -401,3 +426,61 @@ class TestAPI(unittest.TestCase):
}),
headers=HA_HEADERS)
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") }}'))