Pulled from dev to get up-to-date
This commit is contained in:
commit
cceb79a378
34 changed files with 1194 additions and 176 deletions
|
@ -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
|
||||
|
|
|
@ -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`.
|
||||
|
|
186
homeassistant/components/alexa.py
Normal file
186
homeassistant/components/alexa.py
Normal 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)
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
122
homeassistant/components/device_tracker/fritz.py
Normal file
122
homeassistant/components/device_tracker/fritz.py
Normal 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
|
85
homeassistant/components/device_tracker/icloud.py
Normal file
85
homeassistant/components/device_tracker/icloud.py
Normal 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
|
||||
)
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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. """
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
118
homeassistant/components/sensor/dweet.py
Normal file
118
homeassistant/components/sensor/dweet.py
Normal 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
|
|
@ -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):
|
||||
|
|
|
@ -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'
|
||||
|
|
82
homeassistant/components/sensor/twitch.py
Normal file
82
homeassistant/components/sensor/twitch.py
Normal 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
|
|
@ -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__)
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ../../../../..
|
||||
|
|
1
setup.py
1
setup.py
|
@ -1,3 +1,4 @@
|
|||
#!/usr/bin/env python3
|
||||
import os
|
||||
from setuptools import setup, find_packages
|
||||
from homeassistant.const import __version__
|
||||
|
|
|
@ -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': {
|
||||
|
|
225
tests/components/test_alexa.py
Normal file
225
tests/components/test_alexa.py
Normal 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)
|
|
@ -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())
|
||||
|
|
|
@ -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") }}'))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue