Pulled from dev to get up-to-date

This commit is contained in:
William Scanlon 2015-12-16 19:00:06 -05:00
commit cceb79a378
34 changed files with 1194 additions and 176 deletions

View file

@ -39,7 +39,9 @@ omit =
homeassistant/components/device_tracker/aruba.py
homeassistant/components/device_tracker/asuswrt.py
homeassistant/components/device_tracker/ddwrt.py
homeassistant/components/device_tracker/fritz.py
homeassistant/components/device_tracker/geofancy.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
@ -84,6 +86,7 @@ omit =
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/forecast.py
homeassistant/components/sensor/glances.py
@ -97,6 +100,7 @@ omit =
homeassistant/components/sensor/temper.py
homeassistant/components/sensor/time_date.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

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

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

View file

@ -125,22 +125,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)
@ -148,7 +149,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)
@ -162,7 +167,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):

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,18 +79,23 @@ 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:

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,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,85 @@
"""
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
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
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
)

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

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

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit e51b8add369f9e81d22b25b4be2400675361afdb
Subproject commit e19f3c5e34bc2f5e5bd2dcc1444bb569fb1c0c68

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

@ -42,7 +42,7 @@ class WinkLight(WinkToggleDevice):
brightness = kwargs.get(ATTR_BRIGHTNESS)
if brightness is not None:
self.wink.set_state(True, brightness / 255)
self.wink.set_state(True, brightness=brightness / 255)
else:
self.wink.set_state(True)

View file

@ -15,7 +15,7 @@ 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)
@ -24,7 +24,7 @@ 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 +261,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

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

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

@ -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
from homeassistant.util import template, Throttle
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@ -59,57 +58,31 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
'Please check the URL in the configuration file.')
return False
try:
data = loads(response.text)
except ValueError:
_LOGGER.error('No valid JSON in the response in: %s', data)
return False
try:
RestSensor.extract_value(data, config.get('variable'))
except KeyError:
_LOGGER.error('Variable "%s" not found in response: "%s"',
config.get('variable'), data)
return False
if use_get:
rest = RestDataGet(resource, verify_ssl)
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._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 +106,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, 'N/A')
self._state = value
# pylint: disable=too-few-public-methods
@ -164,7 +129,7 @@ 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'
@ -188,7 +153,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'

View file

@ -0,0 +1,82 @@
"""
homeassistant.components.sensor.twitch
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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 device. """
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,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

@ -10,7 +10,6 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/thermostat.heatmiser/
"""
import logging
import heatmiserV3
from homeassistant.components.thermostat import ThermostatDevice
from homeassistant.const import TEMP_CELCIUS
@ -26,6 +25,8 @@ _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])
@ -34,7 +35,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
CONF_IPADDRESS, CONF_PORT)
return False
serport = heatmiserV3.connection.connection(ipaddress, port)
serport = connection.connection(ipaddress, port)
serport.open()
tstats = []
@ -48,6 +49,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for tstat in tstats:
add_devices([
HeatmiserV3Thermostat(
heatmiser,
tstat.get("id"),
tstat.get("name"),
serport)
@ -58,7 +60,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class HeatmiserV3Thermostat(ThermostatDevice):
""" Represents a HeatmiserV3 thermostat. """
def __init__(self, device, name, serport):
# 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
@ -98,7 +102,7 @@ class HeatmiserV3Thermostat(ThermostatDevice):
def set_temperature(self, temperature):
""" Set new target temperature """
temperature = int(temperature)
heatmiserV3.heatmiser.hmSendAddress(
self.heatmiser.hmSendAddress(
self._id,
18,
temperature,
@ -107,7 +111,7 @@ class HeatmiserV3Thermostat(ThermostatDevice):
self._target_temperature = int(temperature)
def update(self):
self.dcb = heatmiserV3.heatmiser.hmReadAddress(
self.dcb = self.heatmiser.hmReadAddress(
self._id,
'prt',
self.serport)

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

@ -6,10 +6,18 @@ 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):
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 = {
@ -20,7 +28,11 @@ def render_with_possible_json_value(hass, template, value):
except ValueError:
pass
return render(hass, template, variables)
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):
@ -28,9 +40,13 @@ def render(hass, template, variables=None, **kwargs):
if variables is not None:
kwargs.update(variables)
return ENV.from_string(template, {
'states': AllStates(hass)
}).render(kwargs)
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):
@ -45,6 +61,10 @@ class AllStates(object):
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. """
@ -66,8 +86,8 @@ class DomainStates(object):
def forgiving_round(value, precision=0):
""" Rounding method that accepts strings. """
try:
return int(float(value)) if precision == 0 else round(float(value),
precision)
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
@ -81,6 +101,13 @@ def multiply(value, amount):
# If value can't be converted to float
return value
ENV = ImmutableSandboxedEnvironment()
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

@ -63,7 +63,11 @@ https://github.com/pavoni/home-assistant-vera-api/archive/efdba4e63d58a30bc9b36d
# homeassistant.components.lock.wink
# homeassistant.components.sensor.wink
# homeassistant.components.switch.wink
<<<<<<< HEAD
https://github.com/bradsk88/python-wink/archive/d3fcce7528bd031a2c05363a108628acc4eb03aa.zip#python-wink==0.3.1
=======
https://github.com/bradsk88/python-wink/archive/91c8e9a5df24c8dd1a5267dc29a00a40c11d826a.zip#python-wink==0.3
>>>>>>> 7fb5927ac80273d9b5e087defe72765f2ce3227a
# homeassistant.components.media_player.cast
pychromecast==0.6.12

View file

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

View file

@ -1,3 +1,4 @@
#!/usr/bin/env python3
import os
from setuptools import setup, find_packages
from homeassistant.const import __version__

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

View file

@ -5,10 +5,11 @@ tests.test_component_http
Tests Home Assistant HTTP component does what it should do.
"""
# 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
@ -415,3 +416,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())

View file

@ -7,7 +7,7 @@ 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
@ -57,10 +57,10 @@ class TestUtilTemplate(unittest.TestCase):
'{{ states.sensor.temperature.state | round(1) }}'))
def test_rounding_value2(self):
self.hass.states.set('sensor.temperature', 12.72)
self.hass.states.set('sensor.temperature', 12.78)
self.assertEqual(
'127',
'128',
template.render(
self.hass,
'{{ states.sensor.temperature.state | multiply(10) | round }}'))
@ -84,3 +84,44 @@ class TestUtilTemplate(unittest.TestCase):
'',
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") }}'))