mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
commit
7ee71b1831
22
.coveragerc
22
.coveragerc
@ -33,32 +33,34 @@ omit =
|
|||||||
homeassistant/components/*/rfxtrx.py
|
homeassistant/components/*/rfxtrx.py
|
||||||
|
|
||||||
homeassistant/components/binary_sensor/arest.py
|
homeassistant/components/binary_sensor/arest.py
|
||||||
|
homeassistant/components/binary_sensor/rest.py
|
||||||
homeassistant/components/browser.py
|
homeassistant/components/browser.py
|
||||||
homeassistant/components/camera/*
|
homeassistant/components/camera/*
|
||||||
homeassistant/components/device_tracker/actiontec.py
|
homeassistant/components/device_tracker/actiontec.py
|
||||||
homeassistant/components/device_tracker/aruba.py
|
homeassistant/components/device_tracker/aruba.py
|
||||||
homeassistant/components/device_tracker/asuswrt.py
|
homeassistant/components/device_tracker/asuswrt.py
|
||||||
homeassistant/components/device_tracker/ddwrt.py
|
homeassistant/components/device_tracker/ddwrt.py
|
||||||
|
homeassistant/components/device_tracker/fritz.py
|
||||||
homeassistant/components/device_tracker/geofancy.py
|
homeassistant/components/device_tracker/geofancy.py
|
||||||
|
homeassistant/components/device_tracker/icloud.py
|
||||||
homeassistant/components/device_tracker/luci.py
|
homeassistant/components/device_tracker/luci.py
|
||||||
homeassistant/components/device_tracker/ubus.py
|
|
||||||
homeassistant/components/device_tracker/netgear.py
|
homeassistant/components/device_tracker/netgear.py
|
||||||
homeassistant/components/device_tracker/nmap_tracker.py
|
homeassistant/components/device_tracker/nmap_tracker.py
|
||||||
homeassistant/components/device_tracker/owntracks.py
|
homeassistant/components/device_tracker/owntracks.py
|
||||||
|
homeassistant/components/device_tracker/snmp.py
|
||||||
homeassistant/components/device_tracker/thomson.py
|
homeassistant/components/device_tracker/thomson.py
|
||||||
homeassistant/components/device_tracker/tomato.py
|
homeassistant/components/device_tracker/tomato.py
|
||||||
homeassistant/components/device_tracker/tplink.py
|
homeassistant/components/device_tracker/tplink.py
|
||||||
homeassistant/components/device_tracker/snmp.py
|
homeassistant/components/device_tracker/ubus.py
|
||||||
homeassistant/components/discovery.py
|
homeassistant/components/discovery.py
|
||||||
homeassistant/components/downloader.py
|
homeassistant/components/downloader.py
|
||||||
homeassistant/components/ifttt.py
|
homeassistant/components/ifttt.py
|
||||||
homeassistant/components/influxdb.py
|
homeassistant/components/influxdb.py
|
||||||
homeassistant/components/keyboard.py
|
homeassistant/components/keyboard.py
|
||||||
homeassistant/components/light/hue.py
|
|
||||||
homeassistant/components/light/mqtt.py
|
|
||||||
homeassistant/components/light/limitlessled.py
|
|
||||||
homeassistant/components/light/blinksticklight.py
|
homeassistant/components/light/blinksticklight.py
|
||||||
|
homeassistant/components/light/hue.py
|
||||||
homeassistant/components/light/hyperion.py
|
homeassistant/components/light/hyperion.py
|
||||||
|
homeassistant/components/light/limitlessled.py
|
||||||
homeassistant/components/media_player/cast.py
|
homeassistant/components/media_player/cast.py
|
||||||
homeassistant/components/media_player/denon.py
|
homeassistant/components/media_player/denon.py
|
||||||
homeassistant/components/media_player/firetv.py
|
homeassistant/components/media_player/firetv.py
|
||||||
@ -66,9 +68,8 @@ omit =
|
|||||||
homeassistant/components/media_player/kodi.py
|
homeassistant/components/media_player/kodi.py
|
||||||
homeassistant/components/media_player/mpd.py
|
homeassistant/components/media_player/mpd.py
|
||||||
homeassistant/components/media_player/plex.py
|
homeassistant/components/media_player/plex.py
|
||||||
homeassistant/components/media_player/squeezebox.py
|
|
||||||
homeassistant/components/media_player/sonos.py
|
homeassistant/components/media_player/sonos.py
|
||||||
homeassistant/components/notify/file.py
|
homeassistant/components/media_player/squeezebox.py
|
||||||
homeassistant/components/notify/instapush.py
|
homeassistant/components/notify/instapush.py
|
||||||
homeassistant/components/notify/nma.py
|
homeassistant/components/notify/nma.py
|
||||||
homeassistant/components/notify/pushbullet.py
|
homeassistant/components/notify/pushbullet.py
|
||||||
@ -81,10 +82,11 @@ omit =
|
|||||||
homeassistant/components/notify/xmpp.py
|
homeassistant/components/notify/xmpp.py
|
||||||
homeassistant/components/sensor/arest.py
|
homeassistant/components/sensor/arest.py
|
||||||
homeassistant/components/sensor/bitcoin.py
|
homeassistant/components/sensor/bitcoin.py
|
||||||
homeassistant/components/sensor/command_sensor.py
|
|
||||||
homeassistant/components/sensor/cpuspeed.py
|
homeassistant/components/sensor/cpuspeed.py
|
||||||
homeassistant/components/sensor/dht.py
|
homeassistant/components/sensor/dht.py
|
||||||
|
homeassistant/components/sensor/dweet.py
|
||||||
homeassistant/components/sensor/efergy.py
|
homeassistant/components/sensor/efergy.py
|
||||||
|
homeassistant/components/sensor/eliqonline.py
|
||||||
homeassistant/components/sensor/forecast.py
|
homeassistant/components/sensor/forecast.py
|
||||||
homeassistant/components/sensor/glances.py
|
homeassistant/components/sensor/glances.py
|
||||||
homeassistant/components/sensor/mysensors.py
|
homeassistant/components/sensor/mysensors.py
|
||||||
@ -96,10 +98,11 @@ omit =
|
|||||||
homeassistant/components/sensor/systemmonitor.py
|
homeassistant/components/sensor/systemmonitor.py
|
||||||
homeassistant/components/sensor/temper.py
|
homeassistant/components/sensor/temper.py
|
||||||
homeassistant/components/sensor/time_date.py
|
homeassistant/components/sensor/time_date.py
|
||||||
|
homeassistant/components/sensor/torque.py
|
||||||
homeassistant/components/sensor/transmission.py
|
homeassistant/components/sensor/transmission.py
|
||||||
|
homeassistant/components/sensor/twitch.py
|
||||||
homeassistant/components/sensor/worldclock.py
|
homeassistant/components/sensor/worldclock.py
|
||||||
homeassistant/components/switch/arest.py
|
homeassistant/components/switch/arest.py
|
||||||
homeassistant/components/switch/command_switch.py
|
|
||||||
homeassistant/components/switch/edimax.py
|
homeassistant/components/switch/edimax.py
|
||||||
homeassistant/components/switch/hikvisioncam.py
|
homeassistant/components/switch/hikvisioncam.py
|
||||||
homeassistant/components/switch/mystrom.py
|
homeassistant/components/switch/mystrom.py
|
||||||
@ -108,6 +111,7 @@ omit =
|
|||||||
homeassistant/components/switch/rpi_gpio.py
|
homeassistant/components/switch/rpi_gpio.py
|
||||||
homeassistant/components/switch/transmission.py
|
homeassistant/components/switch/transmission.py
|
||||||
homeassistant/components/switch/wemo.py
|
homeassistant/components/switch/wemo.py
|
||||||
|
homeassistant/components/thermostat/heatmiser.py
|
||||||
homeassistant/components/thermostat/homematic.py
|
homeassistant/components/thermostat/homematic.py
|
||||||
homeassistant/components/thermostat/honeywell.py
|
homeassistant/components/thermostat/honeywell.py
|
||||||
homeassistant/components/thermostat/nest.py
|
homeassistant/components/thermostat/nest.py
|
||||||
|
11
.travis.yml
11
.travis.yml
@ -2,11 +2,16 @@ sudo: false
|
|||||||
language: python
|
language: python
|
||||||
cache:
|
cache:
|
||||||
directories:
|
directories:
|
||||||
- $HOME/virtualenv/python$TRAVIS_PYTHON_VERSION/
|
- $HOME/.cache/pip
|
||||||
|
# - "$HOME/virtualenv/python$TRAVIS_PYTHON_VERSION"
|
||||||
python:
|
python:
|
||||||
- 3.4.2
|
- 3.4
|
||||||
- 3.5.0
|
- 3.5
|
||||||
install:
|
install:
|
||||||
|
# Validate requirements_all.txt on Python 3.5
|
||||||
|
- if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then python3 setup.py develop; script/gen_requirements_all.py validate; fi
|
||||||
- script/bootstrap_server
|
- script/bootstrap_server
|
||||||
script:
|
script:
|
||||||
- script/cibuild
|
- script/cibuild
|
||||||
|
matrix:
|
||||||
|
fast_finish: true
|
||||||
|
@ -17,8 +17,7 @@ For help on building your component, please see the [developer documentation](ht
|
|||||||
|
|
||||||
After you finish adding support for your device:
|
After you finish adding support for your device:
|
||||||
|
|
||||||
- Add a link to the website of your device/service/component in the "examples" listing of the `README.md` file.
|
- Add any new dependencies to `requirements_all.txt` if needed. Use `script/gen_requirements_all.py`.
|
||||||
- Add any new dependencies to `requirements_all.txt` if needed. There is no ordering right now, so just add it to the end of the file.
|
|
||||||
- Update the `.coveragerc` file to exclude your platform if there are no tests available.
|
- Update the `.coveragerc` file to exclude your platform if there are no tests available.
|
||||||
- Provide some documentation for [home-assistant.io](https://home-assistant.io/). It's OK to just add a docstring with configuration details (sample entry for `configuration.yaml` file and alike) to the file header as a start. Visit the [website documentation](https://home-assistant.io/developers/website/) for further information on contributing to [home-assistant.io](https://github.com/balloob/home-assistant.io).
|
- Provide some documentation for [home-assistant.io](https://home-assistant.io/). It's OK to just add a docstring with configuration details (sample entry for `configuration.yaml` file and alike) to the file header as a start. Visit the [website documentation](https://home-assistant.io/developers/website/) for further information on contributing to [home-assistant.io](https://github.com/balloob/home-assistant.io).
|
||||||
- Make sure all your code passes ``pylint`` and ``flake8`` (PEP8 and some more) validation. To check your repository, run `./script/lint`.
|
- Make sure all your code passes ``pylint`` and ``flake8`` (PEP8 and some more) validation. To check your repository, run `./script/lint`.
|
||||||
|
@ -275,7 +275,7 @@ def enable_logging(hass, verbose=False, daemon=False, log_rotate_days=None):
|
|||||||
datefmt='%y-%m-%d %H:%M:%S'))
|
datefmt='%y-%m-%d %H:%M:%S'))
|
||||||
logger = logging.getLogger('')
|
logger = logging.getLogger('')
|
||||||
logger.addHandler(err_handler)
|
logger.addHandler(err_handler)
|
||||||
logger.setLevel(logging.INFO) # this sets the minimum log level
|
logger.setLevel(logging.NOTSET) # this sets the minimum log level
|
||||||
|
|
||||||
else:
|
else:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
|
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)
|
@ -12,16 +12,19 @@ import threading
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
import homeassistant.core as ha
|
import homeassistant.core as ha
|
||||||
|
from homeassistant.exceptions import TemplateError
|
||||||
from homeassistant.helpers.state import TrackStates
|
from homeassistant.helpers.state import TrackStates
|
||||||
import homeassistant.remote as rem
|
import homeassistant.remote as rem
|
||||||
|
from homeassistant.util import template
|
||||||
from homeassistant.bootstrap import ERROR_LOG_FILENAME
|
from homeassistant.bootstrap import ERROR_LOG_FILENAME
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES, URL_API_STREAM,
|
URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES, URL_API_STREAM,
|
||||||
URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, URL_API_COMPONENTS,
|
URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, URL_API_COMPONENTS,
|
||||||
URL_API_CONFIG, URL_API_BOOTSTRAP, URL_API_ERROR_LOG, URL_API_LOG_OUT,
|
URL_API_CONFIG, URL_API_BOOTSTRAP, URL_API_ERROR_LOG, URL_API_LOG_OUT,
|
||||||
EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, MATCH_ALL,
|
URL_API_TEMPLATE, EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, MATCH_ALL,
|
||||||
HTTP_OK, HTTP_CREATED, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
|
HTTP_OK, HTTP_CREATED, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
|
||||||
HTTP_UNPROCESSABLE_ENTITY)
|
HTTP_UNPROCESSABLE_ENTITY, HTTP_HEADER_CONTENT_TYPE,
|
||||||
|
CONTENT_TYPE_TEXT_PLAIN)
|
||||||
|
|
||||||
|
|
||||||
DOMAIN = 'api'
|
DOMAIN = 'api'
|
||||||
@ -91,6 +94,9 @@ def setup(hass, config):
|
|||||||
|
|
||||||
hass.http.register_path('POST', URL_API_LOG_OUT, _handle_post_api_log_out)
|
hass.http.register_path('POST', URL_API_LOG_OUT, _handle_post_api_log_out)
|
||||||
|
|
||||||
|
hass.http.register_path('POST', URL_API_TEMPLATE,
|
||||||
|
_handle_post_api_template)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -120,22 +126,23 @@ def _handle_get_api_stream(handler, path_match, data):
|
|||||||
try:
|
try:
|
||||||
wfile.write(msg.encode("UTF-8"))
|
wfile.write(msg.encode("UTF-8"))
|
||||||
wfile.flush()
|
wfile.flush()
|
||||||
handler.server.sessions.extend_validation(session_id)
|
except (IOError, ValueError):
|
||||||
except IOError:
|
# IOError: socket errors
|
||||||
|
# ValueError: raised when 'I/O operation on closed file'
|
||||||
block.set()
|
block.set()
|
||||||
|
|
||||||
def forward_events(event):
|
def forward_events(event):
|
||||||
""" Forwards events to the open request. """
|
""" Forwards events to the open request. """
|
||||||
nonlocal gracefully_closed
|
nonlocal gracefully_closed
|
||||||
|
|
||||||
if block.is_set() or event.event_type == EVENT_TIME_CHANGED or \
|
if block.is_set() or event.event_type == EVENT_TIME_CHANGED:
|
||||||
restrict and event.event_type not in restrict:
|
|
||||||
return
|
return
|
||||||
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
|
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
|
||||||
gracefully_closed = True
|
gracefully_closed = True
|
||||||
block.set()
|
block.set()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
handler.server.sessions.extend_validation(session_id)
|
||||||
write_message(json.dumps(event, cls=rem.JSONEncoder))
|
write_message(json.dumps(event, cls=rem.JSONEncoder))
|
||||||
|
|
||||||
handler.send_response(HTTP_OK)
|
handler.send_response(HTTP_OK)
|
||||||
@ -143,7 +150,11 @@ def _handle_get_api_stream(handler, path_match, data):
|
|||||||
session_id = handler.set_session_cookie_header()
|
session_id = handler.set_session_cookie_header()
|
||||||
handler.end_headers()
|
handler.end_headers()
|
||||||
|
|
||||||
hass.bus.listen(MATCH_ALL, forward_events)
|
if restrict:
|
||||||
|
for event in restrict:
|
||||||
|
hass.bus.listen(event, forward_events)
|
||||||
|
else:
|
||||||
|
hass.bus.listen(MATCH_ALL, forward_events)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
write_message(STREAM_PING_PAYLOAD)
|
write_message(STREAM_PING_PAYLOAD)
|
||||||
@ -157,7 +168,11 @@ def _handle_get_api_stream(handler, path_match, data):
|
|||||||
_LOGGER.info("Found broken event stream to %s, cleaning up",
|
_LOGGER.info("Found broken event stream to %s, cleaning up",
|
||||||
handler.client_address[0])
|
handler.client_address[0])
|
||||||
|
|
||||||
hass.bus.remove_listener(MATCH_ALL, forward_events)
|
if restrict:
|
||||||
|
for event in restrict:
|
||||||
|
hass.bus.remove_listener(event, forward_events)
|
||||||
|
else:
|
||||||
|
hass.bus.remove_listener(MATCH_ALL, forward_events)
|
||||||
|
|
||||||
|
|
||||||
def _handle_get_api_config(handler, path_match, data):
|
def _handle_get_api_config(handler, path_match, data):
|
||||||
@ -359,6 +374,22 @@ def _handle_post_api_log_out(handler, path_match, data):
|
|||||||
handler.end_headers()
|
handler.end_headers()
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_post_api_template(handler, path_match, data):
|
||||||
|
""" Log user out. """
|
||||||
|
template_string = data.get('template', '')
|
||||||
|
|
||||||
|
try:
|
||||||
|
rendered = template.render(handler.server.hass, template_string)
|
||||||
|
|
||||||
|
handler.send_response(HTTP_OK)
|
||||||
|
handler.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN)
|
||||||
|
handler.end_headers()
|
||||||
|
handler.wfile.write(rendered.encode('utf-8'))
|
||||||
|
except TemplateError as e:
|
||||||
|
handler.write_json_message(str(e), HTTP_UNPROCESSABLE_ENTITY)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def _services_json(hass):
|
def _services_json(hass):
|
||||||
""" Generate services data to JSONify. """
|
""" Generate services data to JSONify. """
|
||||||
return [{"domain": key, "services": value}
|
return [{"domain": key, "services": value}
|
||||||
|
@ -8,13 +8,14 @@ at https://home-assistant.io/components/automation/#numeric-state-trigger
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||||
from homeassistant.helpers.event import track_state_change
|
from homeassistant.helpers.event import track_state_change
|
||||||
|
from homeassistant.util import template
|
||||||
|
|
||||||
|
|
||||||
CONF_ENTITY_ID = "entity_id"
|
CONF_ENTITY_ID = "entity_id"
|
||||||
CONF_BELOW = "below"
|
CONF_BELOW = "below"
|
||||||
CONF_ABOVE = "above"
|
CONF_ABOVE = "above"
|
||||||
CONF_ATTRIBUTE = "attribute"
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -29,7 +30,7 @@ def trigger(hass, config, action):
|
|||||||
|
|
||||||
below = config.get(CONF_BELOW)
|
below = config.get(CONF_BELOW)
|
||||||
above = config.get(CONF_ABOVE)
|
above = config.get(CONF_ABOVE)
|
||||||
attribute = config.get(CONF_ATTRIBUTE)
|
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||||
|
|
||||||
if below is None and above is None:
|
if below is None and above is None:
|
||||||
_LOGGER.error("Missing configuration key."
|
_LOGGER.error("Missing configuration key."
|
||||||
@ -37,13 +38,20 @@ def trigger(hass, config, action):
|
|||||||
CONF_BELOW, CONF_ABOVE)
|
CONF_BELOW, CONF_ABOVE)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if value_template is not None:
|
||||||
|
renderer = lambda value: template.render(hass,
|
||||||
|
value_template,
|
||||||
|
{'state': value})
|
||||||
|
else:
|
||||||
|
renderer = lambda value: value.state
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def state_automation_listener(entity, from_s, to_s):
|
def state_automation_listener(entity, from_s, to_s):
|
||||||
""" Listens for state changes and calls action. """
|
""" Listens for state changes and calls action. """
|
||||||
|
|
||||||
# Fire action if we go from outside range into range
|
# Fire action if we go from outside range into range
|
||||||
if _in_range(to_s, above, below, attribute) and \
|
if _in_range(above, below, renderer(to_s)) and \
|
||||||
(from_s is None or not _in_range(from_s, above, below, attribute)):
|
(from_s is None or not _in_range(above, below, renderer(from_s))):
|
||||||
action()
|
action()
|
||||||
|
|
||||||
track_state_change(
|
track_state_change(
|
||||||
@ -63,7 +71,7 @@ def if_action(hass, config):
|
|||||||
|
|
||||||
below = config.get(CONF_BELOW)
|
below = config.get(CONF_BELOW)
|
||||||
above = config.get(CONF_ABOVE)
|
above = config.get(CONF_ABOVE)
|
||||||
attribute = config.get(CONF_ATTRIBUTE)
|
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||||
|
|
||||||
if below is None and above is None:
|
if below is None and above is None:
|
||||||
_LOGGER.error("Missing configuration key."
|
_LOGGER.error("Missing configuration key."
|
||||||
@ -71,22 +79,28 @@ def if_action(hass, config):
|
|||||||
CONF_BELOW, CONF_ABOVE)
|
CONF_BELOW, CONF_ABOVE)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if value_template is not None:
|
||||||
|
renderer = lambda value: template.render(hass,
|
||||||
|
value_template,
|
||||||
|
{'state': value})
|
||||||
|
else:
|
||||||
|
renderer = lambda value: value.state
|
||||||
|
|
||||||
def if_numeric_state():
|
def if_numeric_state():
|
||||||
""" Test numeric state condition. """
|
""" Test numeric state condition. """
|
||||||
state = hass.states.get(entity_id)
|
state = hass.states.get(entity_id)
|
||||||
return state is not None and _in_range(state, above, below, attribute)
|
return state is not None and _in_range(above, below, renderer(state))
|
||||||
|
|
||||||
return if_numeric_state
|
return if_numeric_state
|
||||||
|
|
||||||
|
|
||||||
def _in_range(state, range_start, range_end, attribute):
|
def _in_range(range_start, range_end, value):
|
||||||
""" Checks if value is inside the range """
|
""" Checks if value is inside the range """
|
||||||
value = (state.state if attribute is None
|
|
||||||
else state.attributes.get(attribute))
|
|
||||||
try:
|
try:
|
||||||
value = float(value)
|
value = float(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
_LOGGER.warning("Missing value in numeric check")
|
_LOGGER.warning("Value returned from template is not a number: %s",
|
||||||
|
value)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if range_start is not None and range_end is not None:
|
if range_start is not None and range_end is not None:
|
||||||
|
65
homeassistant/components/automation/template.py
Normal file
65
homeassistant/components/automation/template.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.automation.template
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Offers template automation rules.
|
||||||
|
|
||||||
|
For more details about this automation rule, please refer to the documentation
|
||||||
|
at https://home-assistant.io/components/automation/#template-trigger
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_VALUE_TEMPLATE, EVENT_STATE_CHANGED
|
||||||
|
from homeassistant.exceptions import TemplateError
|
||||||
|
from homeassistant.util import template
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def trigger(hass, config, action):
|
||||||
|
""" Listen for state changes based on `config`. """
|
||||||
|
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||||
|
|
||||||
|
if value_template is None:
|
||||||
|
_LOGGER.error("Missing configuration key %s", CONF_VALUE_TEMPLATE)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Local variable to keep track of if the action has already been triggered
|
||||||
|
already_triggered = False
|
||||||
|
|
||||||
|
def event_listener(event):
|
||||||
|
""" Listens for state changes and calls action. """
|
||||||
|
nonlocal already_triggered
|
||||||
|
template_result = _check_template(hass, value_template)
|
||||||
|
|
||||||
|
# Check to see if template returns true
|
||||||
|
if template_result and not already_triggered:
|
||||||
|
already_triggered = True
|
||||||
|
action()
|
||||||
|
elif not template_result:
|
||||||
|
already_triggered = False
|
||||||
|
|
||||||
|
hass.bus.listen(EVENT_STATE_CHANGED, event_listener)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def if_action(hass, config):
|
||||||
|
""" Wraps action method with state based condition. """
|
||||||
|
|
||||||
|
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||||
|
|
||||||
|
if value_template is None:
|
||||||
|
_LOGGER.error("Missing configuration key %s", CONF_VALUE_TEMPLATE)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return lambda: _check_template(hass, value_template)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_template(hass, value_template):
|
||||||
|
""" Checks if result of template is true """
|
||||||
|
try:
|
||||||
|
value = template.render(hass, value_template, {})
|
||||||
|
except TemplateError:
|
||||||
|
_LOGGER.exception('Error parsing template')
|
||||||
|
return False
|
||||||
|
|
||||||
|
return value.lower() == 'true'
|
@ -7,7 +7,10 @@ For more details about this platform, please refer to the documentation at
|
|||||||
https://home-assistant.io/components/binary_sensor.mqtt/
|
https://home-assistant.io/components/binary_sensor.mqtt/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
|
from homeassistant.util import template
|
||||||
import homeassistant.components.mqtt as mqtt
|
import homeassistant.components.mqtt as mqtt
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -34,13 +37,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
config.get('state_topic', None),
|
config.get('state_topic', None),
|
||||||
config.get('qos', DEFAULT_QOS),
|
config.get('qos', DEFAULT_QOS),
|
||||||
config.get('payload_on', DEFAULT_PAYLOAD_ON),
|
config.get('payload_on', DEFAULT_PAYLOAD_ON),
|
||||||
config.get('payload_off', DEFAULT_PAYLOAD_OFF))])
|
config.get('payload_off', DEFAULT_PAYLOAD_OFF),
|
||||||
|
config.get(CONF_VALUE_TEMPLATE))])
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||||
class MqttBinarySensor(BinarySensorDevice):
|
class MqttBinarySensor(BinarySensorDevice):
|
||||||
""" Represents a binary sensor that is updated by MQTT. """
|
""" Represents a binary sensor that is updated by MQTT. """
|
||||||
def __init__(self, hass, name, state_topic, qos, payload_on, payload_off):
|
def __init__(self, hass, name, state_topic, qos, payload_on, payload_off,
|
||||||
|
value_template):
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._name = name
|
self._name = name
|
||||||
self._state = False
|
self._state = False
|
||||||
@ -51,6 +56,9 @@ class MqttBinarySensor(BinarySensorDevice):
|
|||||||
|
|
||||||
def message_received(topic, payload, qos):
|
def message_received(topic, payload, qos):
|
||||||
""" A new MQTT message has been received. """
|
""" A new MQTT message has been received. """
|
||||||
|
if value_template is not None:
|
||||||
|
payload = template.render_with_possible_json_value(
|
||||||
|
hass, value_template, payload)
|
||||||
if payload == self._payload_on:
|
if payload == self._payload_on:
|
||||||
self._state = True
|
self._state = True
|
||||||
self.update_ha_state()
|
self.update_ha_state()
|
||||||
|
145
homeassistant/components/binary_sensor/rest.py
Normal file
145
homeassistant/components/binary_sensor/rest.py
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.binary_sensor.rest
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
The rest binary sensor will consume responses sent by an exposed REST API.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/binary_sensor.rest/
|
||||||
|
"""
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||||
|
from homeassistant.util import template, Throttle
|
||||||
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEFAULT_NAME = 'REST Binary Sensor'
|
||||||
|
DEFAULT_METHOD = 'GET'
|
||||||
|
|
||||||
|
# Return cached results if last scan was less then this time ago
|
||||||
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-variable
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
""" Get the REST binary sensor. """
|
||||||
|
|
||||||
|
use_get = False
|
||||||
|
use_post = False
|
||||||
|
|
||||||
|
resource = config.get('resource', None)
|
||||||
|
method = config.get('method', DEFAULT_METHOD)
|
||||||
|
payload = config.get('payload', None)
|
||||||
|
verify_ssl = config.get('verify_ssl', True)
|
||||||
|
|
||||||
|
if method == 'GET':
|
||||||
|
use_get = True
|
||||||
|
elif method == 'POST':
|
||||||
|
use_post = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
if use_get:
|
||||||
|
response = requests.get(resource, timeout=10, verify=verify_ssl)
|
||||||
|
elif use_post:
|
||||||
|
response = requests.post(resource, data=payload, timeout=10,
|
||||||
|
verify=verify_ssl)
|
||||||
|
if not response.ok:
|
||||||
|
_LOGGER.error('Response status is "%s"', response.status_code)
|
||||||
|
return False
|
||||||
|
except requests.exceptions.MissingSchema:
|
||||||
|
_LOGGER.error('Missing resource or schema in configuration. '
|
||||||
|
'Add http:// to your URL.')
|
||||||
|
return False
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
_LOGGER.error('No route to resource/endpoint: %s',
|
||||||
|
resource)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if use_get:
|
||||||
|
rest = RestDataGet(resource, verify_ssl)
|
||||||
|
elif use_post:
|
||||||
|
rest = RestDataPost(resource, payload, verify_ssl)
|
||||||
|
|
||||||
|
add_devices([RestBinarySensor(hass,
|
||||||
|
rest,
|
||||||
|
config.get('name', DEFAULT_NAME),
|
||||||
|
config.get(CONF_VALUE_TEMPLATE))])
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
|
class RestBinarySensor(BinarySensorDevice):
|
||||||
|
""" Implements a REST binary sensor. """
|
||||||
|
|
||||||
|
def __init__(self, hass, rest, name, value_template):
|
||||||
|
self._hass = hass
|
||||||
|
self.rest = rest
|
||||||
|
self._name = name
|
||||||
|
self._state = False
|
||||||
|
self._value_template = value_template
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
""" The name of the binary sensor. """
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
""" True if the binary sensor is on. """
|
||||||
|
if self.rest.data is False:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
if self._value_template is not None:
|
||||||
|
self.rest.data = template.render_with_possible_json_value(
|
||||||
|
self._hass, self._value_template, self.rest.data, False)
|
||||||
|
return bool(int(self.rest.data))
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
""" Gets the latest data from REST API and updates the state. """
|
||||||
|
self.rest.update()
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-few-public-methods
|
||||||
|
class RestDataGet(object):
|
||||||
|
""" Class for handling the data retrieval with GET method. """
|
||||||
|
|
||||||
|
def __init__(self, resource, verify_ssl):
|
||||||
|
self._resource = resource
|
||||||
|
self._verify_ssl = verify_ssl
|
||||||
|
self.data = False
|
||||||
|
|
||||||
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
|
def update(self):
|
||||||
|
""" Gets the latest data from REST service with GET method. """
|
||||||
|
try:
|
||||||
|
response = requests.get(self._resource, timeout=10,
|
||||||
|
verify=self._verify_ssl)
|
||||||
|
self.data = response.text
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
_LOGGER.error("No route to resource/endpoint: %s", self._resource)
|
||||||
|
self.data = False
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-few-public-methods
|
||||||
|
class RestDataPost(object):
|
||||||
|
""" Class for handling the data retrieval with POST method. """
|
||||||
|
|
||||||
|
def __init__(self, resource, payload, verify_ssl):
|
||||||
|
self._resource = resource
|
||||||
|
self._payload = payload
|
||||||
|
self._verify_ssl = verify_ssl
|
||||||
|
self.data = False
|
||||||
|
|
||||||
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
|
def update(self):
|
||||||
|
""" Gets the latest data from REST service with POST method. """
|
||||||
|
try:
|
||||||
|
response = requests.post(self._resource, data=self._payload,
|
||||||
|
timeout=10, verify=self._verify_ssl)
|
||||||
|
self.data = response.text
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
_LOGGER.error("No route to resource/endpoint: %s", self._resource)
|
||||||
|
self.data = False
|
@ -9,6 +9,7 @@ https://home-assistant.io/components/conversation/
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
from homeassistant import core
|
from homeassistant import core
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
|
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
|
||||||
@ -21,9 +22,13 @@ ATTR_TEXT = "text"
|
|||||||
|
|
||||||
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
|
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
|
||||||
|
|
||||||
|
REQUIREMENTS = ['fuzzywuzzy==0.8.0']
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
""" Registers the process service. """
|
""" Registers the process service. """
|
||||||
|
from fuzzywuzzy import process as fuzzyExtract
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def process(service):
|
def process(service):
|
||||||
@ -42,9 +47,11 @@ def setup(hass, config):
|
|||||||
|
|
||||||
name, command = match.groups()
|
name, command = match.groups()
|
||||||
|
|
||||||
entity_ids = [
|
entities = {state.entity_id: state.name for state in hass.states.all()}
|
||||||
state.entity_id for state in hass.states.all()
|
|
||||||
if state.name.lower() == name]
|
entity_ids = fuzzyExtract.extractOne(name,
|
||||||
|
entities,
|
||||||
|
score_cutoff=65)[2]
|
||||||
|
|
||||||
if not entity_ids:
|
if not entity_ids:
|
||||||
logger.error(
|
logger.error(
|
||||||
|
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
|
87
homeassistant/components/device_tracker/icloud.py
Normal file
87
homeassistant/components/device_tracker/icloud.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.device_tracker.icloud
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Device tracker platform that supports scanning iCloud devices.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/device_tracker.icloud/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import re
|
||||||
|
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||||
|
from homeassistant.helpers.event import track_utc_time_change
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
REQUIREMENTS = ['pyicloud==0.7.2']
|
||||||
|
|
||||||
|
CONF_INTERVAL = 'interval'
|
||||||
|
DEFAULT_INTERVAL = 8
|
||||||
|
|
||||||
|
|
||||||
|
def setup_scanner(hass, config, see):
|
||||||
|
""" Set up the iCloud Scanner. """
|
||||||
|
from pyicloud import PyiCloudService
|
||||||
|
from pyicloud.exceptions import PyiCloudFailedLoginException
|
||||||
|
from pyicloud.exceptions import PyiCloudNoDevicesException
|
||||||
|
|
||||||
|
# Get the username and password from the configuration
|
||||||
|
username = config.get(CONF_USERNAME)
|
||||||
|
password = config.get(CONF_PASSWORD)
|
||||||
|
|
||||||
|
if username is None or password is None:
|
||||||
|
_LOGGER.error('Must specify a username and password')
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
_LOGGER.info('Logging into iCloud Account')
|
||||||
|
# Attempt the login to iCloud
|
||||||
|
api = PyiCloudService(username,
|
||||||
|
password,
|
||||||
|
verify=True)
|
||||||
|
except PyiCloudFailedLoginException as error:
|
||||||
|
_LOGGER.exception('Error logging into iCloud Service: %s', error)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def keep_alive(now):
|
||||||
|
""" Keeps authenticating iCloud connection. """
|
||||||
|
api.authenticate()
|
||||||
|
_LOGGER.info("Authenticate against iCloud")
|
||||||
|
|
||||||
|
track_utc_time_change(hass, keep_alive, second=0)
|
||||||
|
|
||||||
|
def update_icloud(now):
|
||||||
|
""" Authenticate against iCloud and scan for devices. """
|
||||||
|
try:
|
||||||
|
# The session timeouts if we are not using it so we
|
||||||
|
# have to re-authenticate. This will send an email.
|
||||||
|
api.authenticate()
|
||||||
|
# Loop through every device registered with the iCloud account
|
||||||
|
for device in api.devices:
|
||||||
|
status = device.status()
|
||||||
|
location = device.location()
|
||||||
|
# If the device has a location add it. If not do nothing
|
||||||
|
if location:
|
||||||
|
see(
|
||||||
|
dev_id=re.sub(r"(\s|\W|')",
|
||||||
|
'',
|
||||||
|
status['name']),
|
||||||
|
host_name=status['name'],
|
||||||
|
gps=(location['latitude'], location['longitude']),
|
||||||
|
battery=status['batteryLevel']*100,
|
||||||
|
gps_accuracy=location['horizontalAccuracy']
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# No location found for the device so continue
|
||||||
|
continue
|
||||||
|
except PyiCloudNoDevicesException:
|
||||||
|
_LOGGER.info('No iCloud Devices found!')
|
||||||
|
|
||||||
|
track_utc_time_change(
|
||||||
|
hass, update_icloud,
|
||||||
|
minute=range(0, 60, config.get(CONF_INTERVAL, DEFAULT_INTERVAL)),
|
||||||
|
second=0
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
@ -11,7 +11,6 @@ import logging
|
|||||||
from . import version, mdi_version
|
from . import version, mdi_version
|
||||||
import homeassistant.util as util
|
import homeassistant.util as util
|
||||||
from homeassistant.const import URL_ROOT, HTTP_OK
|
from homeassistant.const import URL_ROOT, HTTP_OK
|
||||||
from homeassistant.config import get_default_config_dir
|
|
||||||
|
|
||||||
DOMAIN = 'frontend'
|
DOMAIN = 'frontend'
|
||||||
DEPENDENCIES = ['api']
|
DEPENDENCIES = ['api']
|
||||||
@ -22,8 +21,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
FRONTEND_URLS = [
|
FRONTEND_URLS = [
|
||||||
URL_ROOT, '/logbook', '/history', '/map', '/devService', '/devState',
|
URL_ROOT, '/logbook', '/history', '/map', '/devService', '/devState',
|
||||||
'/devEvent', '/devInfo']
|
'/devEvent', '/devInfo', '/devTemplate', '/states']
|
||||||
STATES_URL = re.compile(r'/states(/([a-zA-Z\._\-0-9/]+)|)')
|
|
||||||
|
|
||||||
_FINGERPRINT = re.compile(r'^(\w+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
|
_FINGERPRINT = re.compile(r'^(\w+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
|
||||||
|
|
||||||
@ -37,7 +35,8 @@ def setup(hass, config):
|
|||||||
for url in FRONTEND_URLS:
|
for url in FRONTEND_URLS:
|
||||||
hass.http.register_path('GET', url, _handle_get_root, False)
|
hass.http.register_path('GET', url, _handle_get_root, False)
|
||||||
|
|
||||||
hass.http.register_path('GET', STATES_URL, _handle_get_root, False)
|
hass.http.register_path('GET', '/service_worker.js',
|
||||||
|
_handle_get_service_worker, False)
|
||||||
|
|
||||||
# Static files
|
# Static files
|
||||||
hass.http.register_path(
|
hass.http.register_path(
|
||||||
@ -78,6 +77,17 @@ def _handle_get_root(handler, path_match, data):
|
|||||||
handler.wfile.write(template_html.encode("UTF-8"))
|
handler.wfile.write(template_html.encode("UTF-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_get_service_worker(handler, path_match, data):
|
||||||
|
""" Returns service worker for the frontend. """
|
||||||
|
if handler.server.development:
|
||||||
|
sw_path = "home-assistant-polymer/build/service_worker.js"
|
||||||
|
else:
|
||||||
|
sw_path = "service_worker.js"
|
||||||
|
|
||||||
|
handler.write_file(os.path.join(os.path.dirname(__file__), 'www_static',
|
||||||
|
sw_path))
|
||||||
|
|
||||||
|
|
||||||
def _handle_get_static(handler, path_match, data):
|
def _handle_get_static(handler, path_match, data):
|
||||||
""" Returns a static file for the frontend. """
|
""" Returns a static file for the frontend. """
|
||||||
req_file = util.sanitize_path(path_match.group('file'))
|
req_file = util.sanitize_path(path_match.group('file'))
|
||||||
@ -98,8 +108,6 @@ def _handle_get_local(handler, path_match, data):
|
|||||||
"""
|
"""
|
||||||
req_file = util.sanitize_path(path_match.group('file'))
|
req_file = util.sanitize_path(path_match.group('file'))
|
||||||
|
|
||||||
path = os.path.join(get_default_config_dir(), 'www', req_file)
|
path = handler.server.hass.config.path('www', req_file)
|
||||||
if not os.path.isfile(path):
|
|
||||||
return False
|
|
||||||
|
|
||||||
handler.write_file(path)
|
handler.write_file(path)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Home Assistant</title>
|
<title>Home Assistant</title>
|
||||||
@ -13,7 +13,7 @@
|
|||||||
<meta name='viewport' content='width=device-width, user-scalable=no'>
|
<meta name='viewport' content='width=device-width, user-scalable=no'>
|
||||||
<meta name='theme-color' content='#03a9f4'>
|
<meta name='theme-color' content='#03a9f4'>
|
||||||
<style>
|
<style>
|
||||||
#init {
|
#ha-init-skeleton {
|
||||||
display: -webkit-flex;
|
display: -webkit-flex;
|
||||||
display: flex;
|
display: flex;
|
||||||
-webkit-flex-direction: column;
|
-webkit-flex-direction: column;
|
||||||
@ -31,11 +31,21 @@
|
|||||||
margin-bottom: 123px;
|
margin-bottom: 123px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<link rel='import' href='/static/{{ app_url }}' async>
|
||||||
</head>
|
</head>
|
||||||
<body fullbleed>
|
<body fullbleed>
|
||||||
<div id='init'><img src='/static/favicon-192x192.png' height='192'></div>
|
<div id='ha-init-skeleton'><img src='/static/favicon-192x192.png' height='192'></div>
|
||||||
<script src='/static/webcomponents-lite.min.js'></script>
|
<script>
|
||||||
<link rel='import' href='/static/{{ app_url }}' />
|
var webComponentsSupported = ('registerElement' in document &&
|
||||||
|
'import' in document.createElement('link') &&
|
||||||
|
'content' in document.createElement('template'))
|
||||||
|
if (!webComponentsSupported) {
|
||||||
|
var script = document.createElement('script')
|
||||||
|
script.async = true
|
||||||
|
script.src = '/static/webcomponents-lite.min.js'
|
||||||
|
document.head.appendChild(script)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<home-assistant auth='{{ auth }}' icons='{{ icons }}'></home-assistant>
|
<home-assistant auth='{{ auth }}' icons='{{ icons }}'></home-assistant>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
""" DO NOT MODIFY. Auto-generated by build_frontend script """
|
""" DO NOT MODIFY. Auto-generated by build_frontend script """
|
||||||
VERSION = "d07b7ed1734ae3f2472f9ae88e0c3dea"
|
VERSION = "be08c5a3ce12040bbdba2db83cb1a568"
|
||||||
|
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
|||||||
Subproject commit 044a6d9810b6aa662b82af0f67deb662725ad4cb
|
Subproject commit 50aadaf880a9cb36bf144540171ff5fa029e9eaf
|
@ -0,0 +1,5 @@
|
|||||||
|
!function(e){function t(r){if(n[r])return n[r].exports;var s=n[r]={exports:{},id:r,loaded:!1};return e[r].call(s.exports,s,s.exports,t),s.loaded=!0,s.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([/*!*************************************!*\
|
||||||
|
!*** ./src/service-worker/index.js ***!
|
||||||
|
\*************************************/
|
||||||
|
function(e,t,n){"use strict";var r="0.10",s="/",c=["/","/logbook","/history","/map","/devService","/devState","/devEvent","/devInfo","/states"],i=["/static/favicon-192x192.png"];self.addEventListener("install",function(e){e.waitUntil(caches.open(r).then(function(e){return e.addAll(i.concat(s))}))}),self.addEventListener("activate",function(e){}),self.addEventListener("message",function(e){}),self.addEventListener("fetch",function(e){var t=e.request.url.substr(e.request.url.indexOf("/",8));i.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(e.request)})),c.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(s).then(function(n){var r=fetch(e.request).then(function(e){return t.put(s,e.clone()),e});return n||r})}))})}]);
|
||||||
|
//# sourceMappingURL=service_worker.js.map
|
File diff suppressed because one or more lines are too long
@ -178,12 +178,8 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
|||||||
""" Does some common checks and calls appropriate method. """
|
""" Does some common checks and calls appropriate method. """
|
||||||
url = urlparse(self.path)
|
url = urlparse(self.path)
|
||||||
|
|
||||||
# Read query input
|
# Read query input. parse_qs gives a list for each value, we want last
|
||||||
data = parse_qs(url.query)
|
data = {key: data[-1] for key, data in parse_qs(url.query).items()}
|
||||||
|
|
||||||
# parse_qs gives a list for each value, take the latest element
|
|
||||||
for key in data:
|
|
||||||
data[key] = data[key][-1]
|
|
||||||
|
|
||||||
# Did we get post input ?
|
# Did we get post input ?
|
||||||
content_length = int(self.headers.get(HTTP_HEADER_CONTENT_LENGTH, 0))
|
content_length = int(self.headers.get(HTTP_HEADER_CONTENT_LENGTH, 0))
|
||||||
@ -363,13 +359,13 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
|||||||
def set_session_cookie_header(self):
|
def set_session_cookie_header(self):
|
||||||
""" Add the header for the session cookie and return session id. """
|
""" Add the header for the session cookie and return session id. """
|
||||||
if not self.authenticated:
|
if not self.authenticated:
|
||||||
return
|
return None
|
||||||
|
|
||||||
session_id = self.get_cookie_session_id()
|
session_id = self.get_cookie_session_id()
|
||||||
|
|
||||||
if session_id is not None:
|
if session_id is not None:
|
||||||
self.server.sessions.extend_validation(session_id)
|
self.server.sessions.extend_validation(session_id)
|
||||||
return
|
return session_id
|
||||||
|
|
||||||
self.send_header(
|
self.send_header(
|
||||||
'Set-Cookie',
|
'Set-Cookie',
|
||||||
@ -426,10 +422,10 @@ def session_valid_time():
|
|||||||
|
|
||||||
class SessionStore(object):
|
class SessionStore(object):
|
||||||
""" Responsible for storing and retrieving http sessions """
|
""" Responsible for storing and retrieving http sessions """
|
||||||
def __init__(self, enabled=True):
|
def __init__(self):
|
||||||
""" Set up the session store """
|
""" Set up the session store """
|
||||||
self._sessions = {}
|
self._sessions = {}
|
||||||
self.lock = threading.RLock()
|
self._lock = threading.RLock()
|
||||||
|
|
||||||
@util.Throttle(SESSION_CLEAR_INTERVAL)
|
@util.Throttle(SESSION_CLEAR_INTERVAL)
|
||||||
def _remove_expired(self):
|
def _remove_expired(self):
|
||||||
@ -441,7 +437,7 @@ class SessionStore(object):
|
|||||||
|
|
||||||
def is_valid(self, key):
|
def is_valid(self, key):
|
||||||
""" Return True if a valid session is given. """
|
""" Return True if a valid session is given. """
|
||||||
with self.lock:
|
with self._lock:
|
||||||
self._remove_expired()
|
self._remove_expired()
|
||||||
|
|
||||||
return (key in self._sessions and
|
return (key in self._sessions and
|
||||||
@ -449,17 +445,19 @@ class SessionStore(object):
|
|||||||
|
|
||||||
def extend_validation(self, key):
|
def extend_validation(self, key):
|
||||||
""" Extend a session validation time. """
|
""" Extend a session validation time. """
|
||||||
with self.lock:
|
with self._lock:
|
||||||
|
if key not in self._sessions:
|
||||||
|
return
|
||||||
self._sessions[key] = session_valid_time()
|
self._sessions[key] = session_valid_time()
|
||||||
|
|
||||||
def destroy(self, key):
|
def destroy(self, key):
|
||||||
""" Destroy a session by key. """
|
""" Destroy a session by key. """
|
||||||
with self.lock:
|
with self._lock:
|
||||||
self._sessions.pop(key, None)
|
self._sessions.pop(key, None)
|
||||||
|
|
||||||
def create(self):
|
def create(self):
|
||||||
""" Creates a new session. """
|
""" Creates a new session. """
|
||||||
with self.lock:
|
with self._lock:
|
||||||
session_id = util.get_random_string(20)
|
session_id = util.get_random_string(20)
|
||||||
|
|
||||||
while session_id in self._sessions:
|
while session_id in self._sessions:
|
||||||
|
@ -1,19 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
homeassistant.components.influx
|
homeassistant.components.influxdb
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
InfluxDB component which allows you to send data to an Influx database.
|
InfluxDB component which allows you to send data to an Influx database.
|
||||||
|
|
||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/influx/
|
https://home-assistant.io/components/influxdb/
|
||||||
|
|
||||||
Configuration:
|
|
||||||
|
|
||||||
influx:
|
|
||||||
host: localhost
|
|
||||||
port: 8086
|
|
||||||
dbname: home_assistant
|
|
||||||
dbuser: DB_USER
|
|
||||||
dbuser_password: DB_USER_PASSWORD
|
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@ -26,7 +17,7 @@ from homeassistant.components.sun import (STATE_ABOVE_HORIZON,
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = "influx"
|
DOMAIN = "influxdb"
|
||||||
DEPENDENCIES = []
|
DEPENDENCIES = []
|
||||||
|
|
||||||
DEFAULT_HOST = 'localhost'
|
DEFAULT_HOST = 'localhost'
|
||||||
@ -43,7 +34,7 @@ CONF_PASSWORD = 'password'
|
|||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
""" Setup the Influx component. """
|
""" Setup the InfluxDB component. """
|
||||||
|
|
||||||
from influxdb import InfluxDBClient, exceptions
|
from influxdb import InfluxDBClient, exceptions
|
||||||
|
|
||||||
@ -54,21 +45,21 @@ def setup(hass, config):
|
|||||||
|
|
||||||
host = conf[CONF_HOST]
|
host = conf[CONF_HOST]
|
||||||
port = util.convert(conf.get(CONF_PORT), int, DEFAULT_PORT)
|
port = util.convert(conf.get(CONF_PORT), int, DEFAULT_PORT)
|
||||||
dbname = util.convert(conf.get(CONF_DB_NAME), str, DEFAULT_DATABASE)
|
database = util.convert(conf.get(CONF_DB_NAME), str, DEFAULT_DATABASE)
|
||||||
username = util.convert(conf.get(CONF_USERNAME), str)
|
username = util.convert(conf.get(CONF_USERNAME), str)
|
||||||
password = util.convert(conf.get(CONF_PASSWORD), str)
|
password = util.convert(conf.get(CONF_PASSWORD), str)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
influx = InfluxDBClient(host=host, port=port, username=username,
|
influx = InfluxDBClient(host=host, port=port, username=username,
|
||||||
password=password, database=dbname)
|
password=password, database=database)
|
||||||
databases = [i['name'] for i in influx.get_list_database()]
|
databases = [i['name'] for i in influx.get_list_database()]
|
||||||
except exceptions.InfluxDBClientError:
|
except exceptions.InfluxDBClientError:
|
||||||
_LOGGER.error("Database host is not accessible. "
|
_LOGGER.error("Database host is not accessible. "
|
||||||
"Please check your entries in the configuration file.")
|
"Please check your entries in the configuration file.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if dbname not in databases:
|
if database not in databases:
|
||||||
_LOGGER.error("Database %s doesn't exist", dbname)
|
_LOGGER.error("Database %s doesn't exist", database)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def influx_event_listener(event):
|
def influx_event_listener(event):
|
||||||
@ -106,7 +97,7 @@ def setup(hass, config):
|
|||||||
try:
|
try:
|
||||||
influx.write_points(json_body)
|
influx.write_points(json_body)
|
||||||
except exceptions.InfluxDBClientError:
|
except exceptions.InfluxDBClientError:
|
||||||
_LOGGER.exception('Error saving event to Influx')
|
_LOGGER.exception('Error saving event to InfluxDB')
|
||||||
|
|
||||||
hass.bus.listen(EVENT_STATE_CHANGED, influx_event_listener)
|
hass.bus.listen(EVENT_STATE_CHANGED, influx_event_listener)
|
||||||
|
|
||||||
|
@ -6,24 +6,24 @@ Allows to configure a MQTT light.
|
|||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/light.mqtt/
|
https://home-assistant.io/components/light.mqtt/
|
||||||
"""
|
"""
|
||||||
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import homeassistant.components.mqtt as mqtt
|
import homeassistant.components.mqtt as mqtt
|
||||||
from homeassistant.components.light import (Light,
|
from homeassistant.components.light import (Light,
|
||||||
ATTR_BRIGHTNESS, ATTR_RGB_COLOR)
|
ATTR_BRIGHTNESS, ATTR_RGB_COLOR)
|
||||||
|
from homeassistant.util.template import render_with_possible_json_value
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_NAME = "MQTT Light"
|
DEFAULT_NAME = 'MQTT Light'
|
||||||
DEFAULT_QOS = 0
|
DEFAULT_QOS = 0
|
||||||
DEFAULT_PAYLOAD_ON = "on"
|
DEFAULT_PAYLOAD_ON = 'ON'
|
||||||
DEFAULT_PAYLOAD_OFF = "off"
|
DEFAULT_PAYLOAD_OFF = 'OFF'
|
||||||
DEFAULT_OPTIMISTIC = False
|
DEFAULT_OPTIMISTIC = False
|
||||||
|
|
||||||
DEPENDENCIES = ['mqtt']
|
DEPENDENCIES = ['mqtt']
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||||
""" Add MQTT Light. """
|
""" Add MQTT Light. """
|
||||||
@ -35,18 +35,16 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||||||
add_devices_callback([MqttLight(
|
add_devices_callback([MqttLight(
|
||||||
hass,
|
hass,
|
||||||
config.get('name', DEFAULT_NAME),
|
config.get('name', DEFAULT_NAME),
|
||||||
{
|
{key: config.get(key) for key in
|
||||||
"state_topic": config.get('state_topic'),
|
(typ + topic
|
||||||
"command_topic": config.get('command_topic'),
|
for typ in ('', 'brightness_', 'rgb_')
|
||||||
"brightness_state_topic": config.get('brightness_state_topic'),
|
for topic in ('state_topic', 'command_topic'))},
|
||||||
"brightness_command_topic": config.get('brightness_command_topic'),
|
{key: config.get(key + '_value_template')
|
||||||
"rgb_state_topic": config.get('rgb_state_topic'),
|
for key in ('state', 'brightness', 'rgb')},
|
||||||
"rgb_command_topic": config.get('rgb_command_topic')
|
|
||||||
},
|
|
||||||
config.get('qos', DEFAULT_QOS),
|
config.get('qos', DEFAULT_QOS),
|
||||||
{
|
{
|
||||||
"on": config.get('payload_on', DEFAULT_PAYLOAD_ON),
|
'on': config.get('payload_on', DEFAULT_PAYLOAD_ON),
|
||||||
"off": config.get('payload_off', DEFAULT_PAYLOAD_OFF)
|
'off': config.get('payload_off', DEFAULT_PAYLOAD_OFF)
|
||||||
},
|
},
|
||||||
config.get('optimistic', DEFAULT_OPTIMISTIC))])
|
config.get('optimistic', DEFAULT_OPTIMISTIC))])
|
||||||
|
|
||||||
@ -55,7 +53,7 @@ class MqttLight(Light):
|
|||||||
""" Provides a MQTT light. """
|
""" Provides a MQTT light. """
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments,too-many-instance-attributes
|
# pylint: disable=too-many-arguments,too-many-instance-attributes
|
||||||
def __init__(self, hass, name, topic, qos, payload, optimistic):
|
def __init__(self, hass, name, topic, templates, qos, payload, optimistic):
|
||||||
|
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._name = name
|
self._name = name
|
||||||
@ -68,8 +66,13 @@ class MqttLight(Light):
|
|||||||
topic["brightness_state_topic"] is None)
|
topic["brightness_state_topic"] is None)
|
||||||
self._state = False
|
self._state = False
|
||||||
|
|
||||||
|
templates = {key: ((lambda value: value) if tpl is None else
|
||||||
|
partial(render_with_possible_json_value, hass, tpl))
|
||||||
|
for key, tpl in templates.items()}
|
||||||
|
|
||||||
def state_received(topic, payload, qos):
|
def state_received(topic, payload, qos):
|
||||||
""" A new MQTT message has been received. """
|
""" A new MQTT message has been received. """
|
||||||
|
payload = templates['state'](payload)
|
||||||
if payload == self._payload["on"]:
|
if payload == self._payload["on"]:
|
||||||
self._state = True
|
self._state = True
|
||||||
elif payload == self._payload["off"]:
|
elif payload == self._payload["off"]:
|
||||||
@ -83,7 +86,7 @@ class MqttLight(Light):
|
|||||||
|
|
||||||
def brightness_received(topic, payload, qos):
|
def brightness_received(topic, payload, qos):
|
||||||
""" A new MQTT message for the brightness has been received. """
|
""" A new MQTT message for the brightness has been received. """
|
||||||
self._brightness = int(payload)
|
self._brightness = int(templates['brightness'](payload))
|
||||||
self.update_ha_state()
|
self.update_ha_state()
|
||||||
|
|
||||||
if self._topic["brightness_state_topic"] is not None:
|
if self._topic["brightness_state_topic"] is not None:
|
||||||
@ -95,7 +98,8 @@ class MqttLight(Light):
|
|||||||
|
|
||||||
def rgb_received(topic, payload, qos):
|
def rgb_received(topic, payload, qos):
|
||||||
""" A new MQTT message has been received. """
|
""" A new MQTT message has been received. """
|
||||||
self._rgb = [int(val) for val in payload.split(',')]
|
self._rgb = [int(val) for val in
|
||||||
|
templates['rgb'](payload).split(',')]
|
||||||
self.update_ha_state()
|
self.update_ha_state()
|
||||||
|
|
||||||
if self._topic["rgb_state_topic"] is not None:
|
if self._topic["rgb_state_topic"] is not None:
|
||||||
|
@ -12,9 +12,7 @@ from homeassistant.components.light import ATTR_BRIGHTNESS
|
|||||||
from homeassistant.components.wink import WinkToggleDevice
|
from homeassistant.components.wink import WinkToggleDevice
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||||
|
|
||||||
REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/'
|
REQUIREMENTS = ['python-wink==0.3.1']
|
||||||
'42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip'
|
|
||||||
'#python-wink==0.2']
|
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||||
@ -45,10 +43,10 @@ class WinkLight(WinkToggleDevice):
|
|||||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||||
|
|
||||||
if brightness is not None:
|
if brightness is not None:
|
||||||
self.wink.setState(True, brightness / 255)
|
self.wink.set_state(True, brightness=brightness / 255)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.wink.setState(True)
|
self.wink.set_state(True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def state_attributes(self):
|
||||||
|
@ -11,9 +11,7 @@ import logging
|
|||||||
from homeassistant.components.lock import LockDevice
|
from homeassistant.components.lock import LockDevice
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||||
|
|
||||||
REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/'
|
REQUIREMENTS = ['python-wink==0.3.1']
|
||||||
'42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip'
|
|
||||||
'#python-wink==0.2']
|
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
@ -43,7 +41,7 @@ class WinkLockDevice(LockDevice):
|
|||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
""" Returns the id of this wink lock """
|
""" Returns the id of this wink lock """
|
||||||
return "{}.{}".format(self.__class__, self.wink.deviceId())
|
return "{}.{}".format(self.__class__, self.wink.device_id())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -52,7 +50,7 @@ class WinkLockDevice(LockDevice):
|
|||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
""" Update the state of the lock. """
|
""" Update the state of the lock. """
|
||||||
self.wink.updateState()
|
self.wink.update_state()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_locked(self):
|
def is_locked(self):
|
||||||
@ -61,8 +59,8 @@ class WinkLockDevice(LockDevice):
|
|||||||
|
|
||||||
def lock(self):
|
def lock(self):
|
||||||
""" Lock the device. """
|
""" Lock the device. """
|
||||||
self.wink.setState(True)
|
self.wink.set_state(True)
|
||||||
|
|
||||||
def unlock(self):
|
def unlock(self):
|
||||||
""" Unlock the device. """
|
""" Unlock the device. """
|
||||||
self.wink.setState(False)
|
self.wink.set_state(False)
|
||||||
|
@ -6,6 +6,7 @@ Provides functionality to interact with Cast devices on the network.
|
|||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/media_player.cast/
|
https://home-assistant.io/components/media_player.cast/
|
||||||
"""
|
"""
|
||||||
|
# pylint: disable=import-error
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -15,16 +16,16 @@ from homeassistant.const import (
|
|||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
MediaPlayerDevice,
|
MediaPlayerDevice,
|
||||||
SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
|
SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
|
||||||
SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_YOUTUBE,
|
SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_YOUTUBE, SUPPORT_PLAY_MEDIA,
|
||||||
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
|
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
|
||||||
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO)
|
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO)
|
||||||
|
|
||||||
REQUIREMENTS = ['pychromecast==0.6.12']
|
REQUIREMENTS = ['pychromecast==0.6.13']
|
||||||
CONF_IGNORE_CEC = 'ignore_cec'
|
CONF_IGNORE_CEC = 'ignore_cec'
|
||||||
CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png'
|
CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png'
|
||||||
SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \
|
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \
|
||||||
SUPPORT_NEXT_TRACK | SUPPORT_YOUTUBE
|
SUPPORT_NEXT_TRACK | SUPPORT_YOUTUBE | SUPPORT_PLAY_MEDIA
|
||||||
KNOWN_HOSTS = []
|
KNOWN_HOSTS = []
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
@ -261,6 +262,10 @@ class CastDevice(MediaPlayerDevice):
|
|||||||
""" Seek the media to a specific location. """
|
""" Seek the media to a specific location. """
|
||||||
self.cast.media_controller.seek(position)
|
self.cast.media_controller.seek(position)
|
||||||
|
|
||||||
|
def play_media(self, media_type, media_id):
|
||||||
|
""" Plays media from a URL """
|
||||||
|
self.cast.media_controller.play_media(media_id, media_type)
|
||||||
|
|
||||||
def play_youtube(self, media_id):
|
def play_youtube(self, media_id):
|
||||||
""" Plays a YouTube media. """
|
""" Plays a YouTube media. """
|
||||||
self.youtube.play_video(media_id)
|
self.youtube.play_video(media_id)
|
||||||
|
@ -21,14 +21,14 @@ from homeassistant.const import (
|
|||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
MediaPlayerDevice,
|
MediaPlayerDevice,
|
||||||
SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_TURN_OFF,
|
SUPPORT_PAUSE, SUPPORT_VOLUME_SET, SUPPORT_TURN_OFF,
|
||||||
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
|
SUPPORT_TURN_ON, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
|
||||||
MEDIA_TYPE_MUSIC)
|
MEDIA_TYPE_MUSIC)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
REQUIREMENTS = ['python-mpd2==0.5.4']
|
REQUIREMENTS = ['python-mpd2==0.5.4']
|
||||||
|
|
||||||
SUPPORT_MPD = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \
|
SUPPORT_MPD = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \
|
||||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
|
SUPPORT_TURN_ON | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
@ -141,7 +141,13 @@ class MpdDevice(MediaPlayerDevice):
|
|||||||
@property
|
@property
|
||||||
def media_title(self):
|
def media_title(self):
|
||||||
""" Title of current playing media. """
|
""" Title of current playing media. """
|
||||||
return self.currentsong['title']
|
name = self.currentsong.get('name', None)
|
||||||
|
title = self.currentsong['title']
|
||||||
|
|
||||||
|
if name is None:
|
||||||
|
return title
|
||||||
|
else:
|
||||||
|
return '{}: {}'.format(name, title)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_artist(self):
|
def media_artist(self):
|
||||||
@ -163,9 +169,13 @@ class MpdDevice(MediaPlayerDevice):
|
|||||||
return SUPPORT_MPD
|
return SUPPORT_MPD
|
||||||
|
|
||||||
def turn_off(self):
|
def turn_off(self):
|
||||||
""" Service to exit the running MPD. """
|
""" Service to send the MPD the command to stop playing. """
|
||||||
self.client.stop()
|
self.client.stop()
|
||||||
|
|
||||||
|
def turn_on(self):
|
||||||
|
""" Service to send the MPD the command to start playing. """
|
||||||
|
self.client.play()
|
||||||
|
|
||||||
def set_volume_level(self, volume):
|
def set_volume_level(self, volume):
|
||||||
""" Sets volume """
|
""" Sets volume """
|
||||||
self.client.setvol(int(volume * 100))
|
self.client.setvol(int(volume * 100))
|
||||||
|
@ -6,7 +6,6 @@ MQTT component, using paho-mqtt.
|
|||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/mqtt/
|
https://home-assistant.io/components/mqtt/
|
||||||
"""
|
"""
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
@ -33,7 +32,7 @@ DEFAULT_RETAIN = False
|
|||||||
SERVICE_PUBLISH = 'publish'
|
SERVICE_PUBLISH = 'publish'
|
||||||
EVENT_MQTT_MESSAGE_RECEIVED = 'MQTT_MESSAGE_RECEIVED'
|
EVENT_MQTT_MESSAGE_RECEIVED = 'MQTT_MESSAGE_RECEIVED'
|
||||||
|
|
||||||
REQUIREMENTS = ['paho-mqtt==1.1', 'jsonpath-rw==1.4.0']
|
REQUIREMENTS = ['paho-mqtt==1.1']
|
||||||
|
|
||||||
CONF_BROKER = 'broker'
|
CONF_BROKER = 'broker'
|
||||||
CONF_PORT = 'port'
|
CONF_PORT = 'port'
|
||||||
@ -136,33 +135,6 @@ def setup(hass, config):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
|
||||||
class _JsonFmtParser(object):
|
|
||||||
""" Implements a JSON parser on xpath. """
|
|
||||||
def __init__(self, jsonpath):
|
|
||||||
import jsonpath_rw
|
|
||||||
self._expr = jsonpath_rw.parse(jsonpath)
|
|
||||||
|
|
||||||
def __call__(self, payload):
|
|
||||||
match = self._expr.find(json.loads(payload))
|
|
||||||
return match[0].value if len(match) > 0 else payload
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
|
||||||
class FmtParser(object):
|
|
||||||
""" Wrapper for all supported formats. """
|
|
||||||
def __init__(self, fmt):
|
|
||||||
self._parse = lambda x: x
|
|
||||||
if fmt:
|
|
||||||
if fmt.startswith('json:'):
|
|
||||||
self._parse = _JsonFmtParser(fmt[5:])
|
|
||||||
|
|
||||||
def __call__(self, payload):
|
|
||||||
return self._parse(payload)
|
|
||||||
|
|
||||||
|
|
||||||
# This is based on one of the paho-mqtt examples:
|
|
||||||
# http://git.eclipse.org/c/paho/org.eclipse.paho.mqtt.python.git/tree/examples/sub-class.py
|
|
||||||
# pylint: disable=too-many-arguments
|
# pylint: disable=too-many-arguments
|
||||||
class MQTT(object):
|
class MQTT(object):
|
||||||
""" Implements messaging service for MQTT. """
|
""" Implements messaging service for MQTT. """
|
||||||
|
@ -13,6 +13,7 @@ import os
|
|||||||
import homeassistant.bootstrap as bootstrap
|
import homeassistant.bootstrap as bootstrap
|
||||||
from homeassistant.config import load_yaml_config_file
|
from homeassistant.config import load_yaml_config_file
|
||||||
from homeassistant.helpers import config_per_platform
|
from homeassistant.helpers import config_per_platform
|
||||||
|
from homeassistant.util import template
|
||||||
|
|
||||||
from homeassistant.const import CONF_NAME
|
from homeassistant.const import CONF_NAME
|
||||||
|
|
||||||
@ -33,9 +34,16 @@ SERVICE_NOTIFY = "notify"
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def send_message(hass, message):
|
def send_message(hass, message, title=None):
|
||||||
""" Send a notification message. """
|
""" Send a notification message. """
|
||||||
hass.services.call(DOMAIN, SERVICE_NOTIFY, {ATTR_MESSAGE: message})
|
data = {
|
||||||
|
ATTR_MESSAGE: message
|
||||||
|
}
|
||||||
|
|
||||||
|
if title is not None:
|
||||||
|
data[ATTR_TITLE] = title
|
||||||
|
|
||||||
|
hass.services.call(DOMAIN, SERVICE_NOTIFY, data)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
@ -70,8 +78,10 @@ def setup(hass, config):
|
|||||||
if message is None:
|
if message is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
title = call.data.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
|
title = template.render(
|
||||||
|
hass, call.data.get(ATTR_TITLE, ATTR_TITLE_DEFAULT))
|
||||||
target = call.data.get(ATTR_TARGET)
|
target = call.data.get(ATTR_TARGET)
|
||||||
|
message = template.render(hass, message)
|
||||||
|
|
||||||
notify_service.send_message(message, title=title, target=target)
|
notify_service.send_message(message, title=title, target=target)
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
homeassistant.components.rollershutter.demo
|
homeassistant.components.rollershutter.demo
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Demo platform for rollorshutter component.
|
Demo platform for the rollorshutter component.
|
||||||
"""
|
"""
|
||||||
from homeassistant.const import EVENT_TIME_CHANGED
|
from homeassistant.const import EVENT_TIME_CHANGED
|
||||||
from homeassistant.helpers.event import track_utc_time_change
|
from homeassistant.helpers.event import track_utc_time_change
|
||||||
@ -9,7 +9,7 @@ from homeassistant.components.rollershutter import RollershutterDevice
|
|||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
""" Sets up the Demo binary sensors. """
|
""" Sets up the Demo rollershutters. """
|
||||||
add_devices([
|
add_devices([
|
||||||
DemoRollershutter(hass, 'Kitchen Window', 0),
|
DemoRollershutter(hass, 'Kitchen Window', 0),
|
||||||
DemoRollershutter(hass, 'Living Room Window', 100),
|
DemoRollershutter(hass, 'Living Room Window', 100),
|
||||||
@ -17,7 +17,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
|
|
||||||
|
|
||||||
class DemoRollershutter(RollershutterDevice):
|
class DemoRollershutter(RollershutterDevice):
|
||||||
""" Represents a rollershutter within Home Assistant. """
|
""" Represents a rollershutter.. """
|
||||||
# pylint: disable=no-self-use
|
# pylint: disable=no-self-use
|
||||||
|
|
||||||
def __init__(self, hass, name, position):
|
def __init__(self, hass, name, position):
|
||||||
@ -29,14 +29,17 @@ class DemoRollershutter(RollershutterDevice):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
""" Returns the name of the rollershutter. """
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
|
""" No polling needed for a demo rollershutter. """
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_position(self):
|
def current_position(self):
|
||||||
|
""" Returns the current position of the rollershutter. """
|
||||||
return self._position
|
return self._position
|
||||||
|
|
||||||
def move_up(self, **kwargs):
|
def move_up(self, **kwargs):
|
||||||
@ -62,11 +65,13 @@ class DemoRollershutter(RollershutterDevice):
|
|||||||
self._listener = None
|
self._listener = None
|
||||||
|
|
||||||
def _listen(self):
|
def _listen(self):
|
||||||
|
""" Listens for changes. """
|
||||||
if self._listener is None:
|
if self._listener is None:
|
||||||
self._listener = track_utc_time_change(self.hass,
|
self._listener = track_utc_time_change(self.hass,
|
||||||
self._time_changed)
|
self._time_changed)
|
||||||
|
|
||||||
def _time_changed(self, now):
|
def _time_changed(self, now):
|
||||||
|
""" Track time changes. """
|
||||||
if self._moving_up:
|
if self._moving_up:
|
||||||
self._position -= 10
|
self._position -= 10
|
||||||
else:
|
else:
|
||||||
|
@ -8,7 +8,10 @@ https://home-assistant.io/components/rollershutter.mqtt/
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import homeassistant.components.mqtt as mqtt
|
import homeassistant.components.mqtt as mqtt
|
||||||
|
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||||
from homeassistant.components.rollershutter import RollershutterDevice
|
from homeassistant.components.rollershutter import RollershutterDevice
|
||||||
|
from homeassistant.util import template
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEPENDENCIES = ['mqtt']
|
DEPENDENCIES = ['mqtt']
|
||||||
@ -36,14 +39,14 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||||||
config.get('payload_up', DEFAULT_PAYLOAD_UP),
|
config.get('payload_up', DEFAULT_PAYLOAD_UP),
|
||||||
config.get('payload_down', DEFAULT_PAYLOAD_DOWN),
|
config.get('payload_down', DEFAULT_PAYLOAD_DOWN),
|
||||||
config.get('payload_stop', DEFAULT_PAYLOAD_STOP),
|
config.get('payload_stop', DEFAULT_PAYLOAD_STOP),
|
||||||
config.get('state_format'))])
|
config.get(CONF_VALUE_TEMPLATE))])
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||||
class MqttRollershutter(RollershutterDevice):
|
class MqttRollershutter(RollershutterDevice):
|
||||||
""" Represents a rollershutter that can be controlled using MQTT. """
|
""" Represents a rollershutter that can be controlled using MQTT. """
|
||||||
def __init__(self, hass, name, state_topic, command_topic, qos,
|
def __init__(self, hass, name, state_topic, command_topic, qos,
|
||||||
payload_up, payload_down, payload_stop, state_format):
|
payload_up, payload_down, payload_stop, value_template):
|
||||||
self._state = None
|
self._state = None
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._name = name
|
self._name = name
|
||||||
@ -53,16 +56,17 @@ class MqttRollershutter(RollershutterDevice):
|
|||||||
self._payload_up = payload_up
|
self._payload_up = payload_up
|
||||||
self._payload_down = payload_down
|
self._payload_down = payload_down
|
||||||
self._payload_stop = payload_stop
|
self._payload_stop = payload_stop
|
||||||
self._parse = mqtt.FmtParser(state_format)
|
|
||||||
|
|
||||||
if self._state_topic is None:
|
if self._state_topic is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
def message_received(topic, payload, qos):
|
def message_received(topic, payload, qos):
|
||||||
""" A new MQTT message has been received. """
|
""" A new MQTT message has been received. """
|
||||||
value = self._parse(payload)
|
if value_template is not None:
|
||||||
if value.isnumeric() and 0 <= int(value) <= 100:
|
payload = template.render_with_possible_json_value(
|
||||||
self._state = int(value)
|
hass, value_template, payload)
|
||||||
|
if payload.isnumeric() and 0 <= int(payload) <= 100:
|
||||||
|
self._state = int(payload)
|
||||||
self.update_ha_state()
|
self.update_ha_state()
|
||||||
else:
|
else:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
|
@ -11,9 +11,11 @@ import logging
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from homeassistant.const import DEVICE_DEFAULT_NAME
|
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, \
|
||||||
|
DEVICE_DEFAULT_NAME
|
||||||
|
from homeassistant.exceptions import TemplateError
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import template, Throttle
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -50,36 +52,50 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
|
|
||||||
arest = ArestData(resource)
|
arest = ArestData(resource)
|
||||||
|
|
||||||
|
def make_renderer(value_template):
|
||||||
|
""" Creates renderer based on variable_template value """
|
||||||
|
if value_template is None:
|
||||||
|
return lambda value: value
|
||||||
|
|
||||||
|
def _render(value):
|
||||||
|
try:
|
||||||
|
return template.render(hass, value_template, {'value': value})
|
||||||
|
except TemplateError:
|
||||||
|
_LOGGER.exception('Error parsing value')
|
||||||
|
return value
|
||||||
|
|
||||||
|
return _render
|
||||||
|
|
||||||
dev = []
|
dev = []
|
||||||
|
|
||||||
if var_conf is not None:
|
if var_conf is not None:
|
||||||
for variable in config['monitored_variables']:
|
for variable in var_conf:
|
||||||
if variable['name'] not in response['variables']:
|
if variable['name'] not in response['variables']:
|
||||||
_LOGGER.error('Variable: "%s" does not exist',
|
_LOGGER.error('Variable: "%s" does not exist',
|
||||||
variable['name'])
|
variable['name'])
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
renderer = make_renderer(variable.get(CONF_VALUE_TEMPLATE))
|
||||||
dev.append(ArestSensor(arest,
|
dev.append(ArestSensor(arest,
|
||||||
resource,
|
resource,
|
||||||
config.get('name', response['name']),
|
config.get('name', response['name']),
|
||||||
variable['name'],
|
variable['name'],
|
||||||
variable=variable['name'],
|
variable=variable['name'],
|
||||||
unit_of_measurement=variable.get(
|
unit_of_measurement=variable.get(
|
||||||
'unit_of_measurement')))
|
ATTR_UNIT_OF_MEASUREMENT),
|
||||||
|
renderer=renderer))
|
||||||
|
|
||||||
if pins is not None:
|
if pins is not None:
|
||||||
for pinnum, pin in pins.items():
|
for pinnum, pin in pins.items():
|
||||||
|
renderer = make_renderer(pin.get(CONF_VALUE_TEMPLATE))
|
||||||
dev.append(ArestSensor(ArestData(resource, pinnum),
|
dev.append(ArestSensor(ArestData(resource, pinnum),
|
||||||
resource,
|
resource,
|
||||||
config.get('name', response['name']),
|
config.get('name', response['name']),
|
||||||
pin.get('name'),
|
pin.get('name'),
|
||||||
pin=pinnum,
|
pin=pinnum,
|
||||||
unit_of_measurement=pin.get(
|
unit_of_measurement=pin.get(
|
||||||
'unit_of_measurement'),
|
ATTR_UNIT_OF_MEASUREMENT),
|
||||||
corr_factor=pin.get('correction_factor',
|
renderer=renderer))
|
||||||
None),
|
|
||||||
decimal_places=pin.get('decimal_places',
|
|
||||||
None)))
|
|
||||||
|
|
||||||
add_devices(dev)
|
add_devices(dev)
|
||||||
|
|
||||||
@ -89,8 +105,7 @@ class ArestSensor(Entity):
|
|||||||
""" Implements an aREST sensor for exposed variables. """
|
""" Implements an aREST sensor for exposed variables. """
|
||||||
|
|
||||||
def __init__(self, arest, resource, location, name, variable=None,
|
def __init__(self, arest, resource, location, name, variable=None,
|
||||||
pin=None, unit_of_measurement=None, corr_factor=None,
|
pin=None, unit_of_measurement=None, renderer=None):
|
||||||
decimal_places=None):
|
|
||||||
self.arest = arest
|
self.arest = arest
|
||||||
self._resource = resource
|
self._resource = resource
|
||||||
self._name = '{} {}'.format(location.title(), name.title()) \
|
self._name = '{} {}'.format(location.title(), name.title()) \
|
||||||
@ -99,8 +114,7 @@ class ArestSensor(Entity):
|
|||||||
self._pin = pin
|
self._pin = pin
|
||||||
self._state = 'n/a'
|
self._state = 'n/a'
|
||||||
self._unit_of_measurement = unit_of_measurement
|
self._unit_of_measurement = unit_of_measurement
|
||||||
self._corr_factor = corr_factor
|
self._renderer = renderer
|
||||||
self._decimal_places = decimal_places
|
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
if self._pin is not None:
|
if self._pin is not None:
|
||||||
@ -126,18 +140,11 @@ class ArestSensor(Entity):
|
|||||||
|
|
||||||
if 'error' in values:
|
if 'error' in values:
|
||||||
return values['error']
|
return values['error']
|
||||||
elif 'value' in values:
|
|
||||||
if self._corr_factor is not None \
|
value = self._renderer(values.get('value',
|
||||||
and self._decimal_places is not None:
|
values.get(self._variable,
|
||||||
return round((float(values['value']) *
|
'N/A')))
|
||||||
float(self._corr_factor)), self._decimal_places)
|
return value
|
||||||
elif self._corr_factor is not None \
|
|
||||||
and self._decimal_places is None:
|
|
||||||
return round(float(values['value']) * float(self._corr_factor))
|
|
||||||
else:
|
|
||||||
return values['value']
|
|
||||||
else:
|
|
||||||
return values.get(self._variable, 'n/a')
|
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
""" Gets the latest data from aREST API. """
|
""" Gets the latest data from aREST API. """
|
||||||
|
@ -10,8 +10,9 @@ import logging
|
|||||||
import subprocess
|
import subprocess
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import template, Throttle
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -32,25 +33,24 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||||||
data = CommandSensorData(config.get('command'))
|
data = CommandSensorData(config.get('command'))
|
||||||
|
|
||||||
add_devices_callback([CommandSensor(
|
add_devices_callback([CommandSensor(
|
||||||
|
hass,
|
||||||
data,
|
data,
|
||||||
config.get('name', DEFAULT_NAME),
|
config.get('name', DEFAULT_NAME),
|
||||||
config.get('unit_of_measurement'),
|
config.get('unit_of_measurement'),
|
||||||
config.get('correction_factor', 1.0),
|
config.get(CONF_VALUE_TEMPLATE)
|
||||||
config.get('decimal_places', 0)
|
|
||||||
)])
|
)])
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
# pylint: disable=too-many-arguments
|
||||||
class CommandSensor(Entity):
|
class CommandSensor(Entity):
|
||||||
""" Represents a sensor that is returning a value of a shell commands. """
|
""" Represents a sensor that is returning a value of a shell commands. """
|
||||||
def __init__(self, data, name, unit_of_measurement, corr_factor,
|
def __init__(self, hass, data, name, unit_of_measurement, value_template):
|
||||||
decimal_places):
|
self._hass = hass
|
||||||
self.data = data
|
self.data = data
|
||||||
self._name = name
|
self._name = name
|
||||||
self._state = False
|
self._state = False
|
||||||
self._unit_of_measurement = unit_of_measurement
|
self._unit_of_measurement = unit_of_measurement
|
||||||
self._corr_factor = float(corr_factor)
|
self._value_template = value_template
|
||||||
self._decimal_places = decimal_places
|
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -73,14 +73,10 @@ class CommandSensor(Entity):
|
|||||||
self.data.update()
|
self.data.update()
|
||||||
value = self.data.value
|
value = self.data.value
|
||||||
|
|
||||||
try:
|
if self._value_template is not None:
|
||||||
if value is not None:
|
self._state = template.render_with_possible_json_value(
|
||||||
if self._corr_factor is not None:
|
self._hass, self._value_template, value, 'N/A')
|
||||||
self._state = round((float(value) * self._corr_factor),
|
else:
|
||||||
self._decimal_places)
|
|
||||||
else:
|
|
||||||
self._state = value
|
|
||||||
except ValueError:
|
|
||||||
self._state = value
|
self._state = value
|
||||||
|
|
||||||
|
|
||||||
|
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
|
69
homeassistant/components/sensor/eliqonline.py
Normal file
69
homeassistant/components/sensor/eliqonline.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.sensor.eliqonline
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Monitors home energy use for the eliq online service.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/sensor.eliqonline/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.const import (STATE_UNKNOWN, CONF_ACCESS_TOKEN, CONF_NAME)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
REQUIREMENTS = ['eliqonline==1.0.11']
|
||||||
|
DEFAULT_NAME = "ELIQ Energy Usage"
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
""" Set up the Eliq sensor. """
|
||||||
|
|
||||||
|
import eliqonline
|
||||||
|
|
||||||
|
access_token = config.get(CONF_ACCESS_TOKEN)
|
||||||
|
name = config.get(CONF_NAME, DEFAULT_NAME)
|
||||||
|
channel_id = config.get("channel_id")
|
||||||
|
|
||||||
|
if access_token is None:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Configuration Error: "
|
||||||
|
"Please make sure you have configured your access token "
|
||||||
|
"that can be aquired from https://my.eliq.se/user/settings/api")
|
||||||
|
return False
|
||||||
|
|
||||||
|
api = eliqonline.API(access_token)
|
||||||
|
add_devices([EliqSensor(api, channel_id, name)])
|
||||||
|
|
||||||
|
|
||||||
|
class EliqSensor(Entity):
|
||||||
|
""" Implements a Eliq sensor. """
|
||||||
|
|
||||||
|
def __init__(self, api, channel_id, name):
|
||||||
|
self._name = name
|
||||||
|
self._unit_of_measurement = "W"
|
||||||
|
self._state = STATE_UNKNOWN
|
||||||
|
|
||||||
|
self.api = api
|
||||||
|
self.channel_id = channel_id
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
""" Returns the name. """
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
""" Unit of measurement of this entity, if any. """
|
||||||
|
return self._unit_of_measurement
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
""" Returns the state of the device. """
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
""" Gets the latest data. """
|
||||||
|
response = self.api.get_data_now(channelid=self.channel_id)
|
||||||
|
self._state = int(response.power)
|
@ -147,17 +147,8 @@ class ForeCastSensor(Entity):
|
|||||||
self._state = data.nearestStormBearing
|
self._state = data.nearestStormBearing
|
||||||
elif self.type == 'precip_intensity':
|
elif self.type == 'precip_intensity':
|
||||||
self._state = data.precipIntensity
|
self._state = data.precipIntensity
|
||||||
if data.precipIntensity == 0:
|
|
||||||
self._state = 'None'
|
|
||||||
self._unit_of_measurement = ''
|
|
||||||
else:
|
|
||||||
self._state = data.precipIntensity
|
|
||||||
elif self.type == 'precip_type':
|
elif self.type == 'precip_type':
|
||||||
if data.precipType is None:
|
self._state = data.precipType
|
||||||
self._state = 'None'
|
|
||||||
self._unit_of_measurement = ''
|
|
||||||
else:
|
|
||||||
self._state = data.precipType
|
|
||||||
elif self.type == 'precip_probability':
|
elif self.type == 'precip_probability':
|
||||||
self._state = round(data.precipProbability * 100, 1)
|
self._state = round(data.precipProbability * 100, 1)
|
||||||
elif self.type == 'dew_point':
|
elif self.type == 'dew_point':
|
||||||
|
@ -79,7 +79,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
if resource not in SENSOR_TYPES:
|
if resource not in SENSOR_TYPES:
|
||||||
_LOGGER.error('Sensor type: "%s" does not exist', resource)
|
_LOGGER.error('Sensor type: "%s" does not exist', resource)
|
||||||
else:
|
else:
|
||||||
dev.append(GlancesSensor(rest, resource))
|
dev.append(GlancesSensor(rest, config.get('name'), resource))
|
||||||
|
|
||||||
add_devices(dev)
|
add_devices(dev)
|
||||||
|
|
||||||
@ -87,9 +87,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
class GlancesSensor(Entity):
|
class GlancesSensor(Entity):
|
||||||
""" Implements a Glances sensor. """
|
""" Implements a Glances sensor. """
|
||||||
|
|
||||||
def __init__(self, rest, sensor_type):
|
def __init__(self, rest, name, sensor_type):
|
||||||
self.rest = rest
|
self.rest = rest
|
||||||
self._name = SENSOR_TYPES[sensor_type][0]
|
self._name = name
|
||||||
self.type = sensor_type
|
self.type = sensor_type
|
||||||
self._state = STATE_UNKNOWN
|
self._state = STATE_UNKNOWN
|
||||||
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
|
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
|
||||||
@ -98,7 +98,10 @@ class GlancesSensor(Entity):
|
|||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
""" The name of the sensor. """
|
""" The name of the sensor. """
|
||||||
return self._name
|
if self._name is None:
|
||||||
|
return SENSOR_TYPES[self.type][0]
|
||||||
|
else:
|
||||||
|
return '{} {}'.format(self._name, SENSOR_TYPES[self.type][0])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit_of_measurement(self):
|
def unit_of_measurement(self):
|
||||||
|
@ -7,7 +7,9 @@ For more details about this platform, please refer to the documentation at
|
|||||||
https://home-assistant.io/components/sensor.mqtt/
|
https://home-assistant.io/components/sensor.mqtt/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.util import template
|
||||||
import homeassistant.components.mqtt as mqtt
|
import homeassistant.components.mqtt as mqtt
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -32,25 +34,27 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||||||
config.get('state_topic'),
|
config.get('state_topic'),
|
||||||
config.get('qos', DEFAULT_QOS),
|
config.get('qos', DEFAULT_QOS),
|
||||||
config.get('unit_of_measurement'),
|
config.get('unit_of_measurement'),
|
||||||
config.get('state_format'))])
|
config.get(CONF_VALUE_TEMPLATE))])
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||||
class MqttSensor(Entity):
|
class MqttSensor(Entity):
|
||||||
""" Represents a sensor that can be updated using MQTT. """
|
""" Represents a sensor that can be updated using MQTT. """
|
||||||
def __init__(self, hass, name, state_topic, qos, unit_of_measurement,
|
def __init__(self, hass, name, state_topic, qos, unit_of_measurement,
|
||||||
state_format):
|
value_template):
|
||||||
self._state = "-"
|
self._state = "-"
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._name = name
|
self._name = name
|
||||||
self._state_topic = state_topic
|
self._state_topic = state_topic
|
||||||
self._qos = qos
|
self._qos = qos
|
||||||
self._unit_of_measurement = unit_of_measurement
|
self._unit_of_measurement = unit_of_measurement
|
||||||
self._parse = mqtt.FmtParser(state_format)
|
|
||||||
|
|
||||||
def message_received(topic, payload, qos):
|
def message_received(topic, payload, qos):
|
||||||
""" A new MQTT message has been received. """
|
""" A new MQTT message has been received. """
|
||||||
self._state = self._parse(payload)
|
if value_template is not None:
|
||||||
|
payload = template.render_with_possible_json_value(
|
||||||
|
hass, value_template, payload)
|
||||||
|
self._state = payload
|
||||||
self.update_ha_state()
|
self.update_ha_state()
|
||||||
|
|
||||||
mqtt.subscribe(hass, self._state_topic, message_received, self._qos)
|
mqtt.subscribe(hass, self._state_topic, message_received, self._qos)
|
||||||
|
@ -76,7 +76,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
persistence = config.get(CONF_PERSISTENCE, True)
|
persistence = config.get(CONF_PERSISTENCE, True)
|
||||||
persistence_file = config.get(CONF_PERSISTENCE_FILE, 'mysensors.pickle')
|
persistence_file = config.get(CONF_PERSISTENCE_FILE,
|
||||||
|
hass.config.path('mysensors.pickle'))
|
||||||
version = config.get(CONF_VERSION, '1.4')
|
version = config.get(CONF_VERSION, '1.4')
|
||||||
|
|
||||||
gateway = mysensors.SerialGateway(port, sensor_update,
|
gateway = mysensors.SerialGateway(port, sensor_update,
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
"""
|
"""
|
||||||
homeassistant.components.sensor.rest
|
homeassistant.components.sensor.rest
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
The rest sensor will consume JSON responses sent by an exposed REST API.
|
The rest sensor will consume responses sent by an exposed REST API.
|
||||||
|
|
||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/sensor.rest/
|
https://home-assistant.io/components/sensor.rest/
|
||||||
"""
|
"""
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from json import loads
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||||
|
from homeassistant.util import template, Throttle
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -59,57 +58,31 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
'Please check the URL in the configuration file.')
|
'Please check the URL in the configuration file.')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
|
||||||
data = loads(response.text)
|
|
||||||
except ValueError:
|
|
||||||
_LOGGER.error('No valid JSON in the response in: %s', data)
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
RestSensor.extract_value(data, config.get('variable'))
|
|
||||||
except KeyError:
|
|
||||||
_LOGGER.error('Variable "%s" not found in response: "%s"',
|
|
||||||
config.get('variable'), data)
|
|
||||||
return False
|
|
||||||
|
|
||||||
if use_get:
|
if use_get:
|
||||||
rest = RestDataGet(resource, verify_ssl)
|
rest = RestDataGet(resource, verify_ssl)
|
||||||
elif use_post:
|
elif use_post:
|
||||||
rest = RestDataPost(resource, payload, verify_ssl)
|
rest = RestDataPost(resource, payload, verify_ssl)
|
||||||
|
|
||||||
add_devices([RestSensor(rest,
|
add_devices([RestSensor(hass,
|
||||||
|
rest,
|
||||||
config.get('name', DEFAULT_NAME),
|
config.get('name', DEFAULT_NAME),
|
||||||
config.get('variable'),
|
|
||||||
config.get('unit_of_measurement'),
|
config.get('unit_of_measurement'),
|
||||||
config.get('correction_factor', None),
|
config.get(CONF_VALUE_TEMPLATE))])
|
||||||
config.get('decimal_places', None))])
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
# pylint: disable=too-many-arguments
|
||||||
class RestSensor(Entity):
|
class RestSensor(Entity):
|
||||||
""" Implements a REST sensor. """
|
""" Implements a REST sensor. """
|
||||||
|
|
||||||
def __init__(self, rest, name, variable, unit_of_measurement, corr_factor,
|
def __init__(self, hass, rest, name, unit_of_measurement, value_template):
|
||||||
decimal_places):
|
self._hass = hass
|
||||||
self.rest = rest
|
self.rest = rest
|
||||||
self._name = name
|
self._name = name
|
||||||
self._variable = variable
|
|
||||||
self._state = 'n/a'
|
self._state = 'n/a'
|
||||||
self._unit_of_measurement = unit_of_measurement
|
self._unit_of_measurement = unit_of_measurement
|
||||||
self._corr_factor = corr_factor
|
self._value_template = value_template
|
||||||
self._decimal_places = decimal_places
|
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def extract_value(cls, data, variable):
|
|
||||||
""" Extracts the value using a key name or a path. """
|
|
||||||
if isinstance(variable, list):
|
|
||||||
for variable_item in variable:
|
|
||||||
data = data[variable_item]
|
|
||||||
return data
|
|
||||||
else:
|
|
||||||
return data[variable]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
""" The name of the sensor. """
|
""" The name of the sensor. """
|
||||||
@ -133,23 +106,10 @@ class RestSensor(Entity):
|
|||||||
if 'error' in value:
|
if 'error' in value:
|
||||||
self._state = value['error']
|
self._state = value['error']
|
||||||
else:
|
else:
|
||||||
try:
|
if self._value_template is not None:
|
||||||
if value is not None:
|
value = template.render_with_possible_json_value(
|
||||||
value = RestSensor.extract_value(value, self._variable)
|
self._hass, self._value_template, value, 'N/A')
|
||||||
if self._corr_factor is not None \
|
self._state = value
|
||||||
and self._decimal_places is not None:
|
|
||||||
self._state = round(
|
|
||||||
(float(value) *
|
|
||||||
float(self._corr_factor)),
|
|
||||||
self._decimal_places)
|
|
||||||
elif self._corr_factor is not None \
|
|
||||||
and self._decimal_places is None:
|
|
||||||
self._state = round(float(value) *
|
|
||||||
float(self._corr_factor))
|
|
||||||
else:
|
|
||||||
self._state = value
|
|
||||||
except ValueError:
|
|
||||||
self._state = RestSensor.extract_value(value, self._variable)
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
@ -169,7 +129,7 @@ class RestDataGet(object):
|
|||||||
verify=self._verify_ssl)
|
verify=self._verify_ssl)
|
||||||
if 'error' in self.data:
|
if 'error' in self.data:
|
||||||
del self.data['error']
|
del self.data['error']
|
||||||
self.data = response.json()
|
self.data = response.text
|
||||||
except requests.exceptions.ConnectionError:
|
except requests.exceptions.ConnectionError:
|
||||||
_LOGGER.error("No route to resource/endpoint.")
|
_LOGGER.error("No route to resource/endpoint.")
|
||||||
self.data['error'] = 'N/A'
|
self.data['error'] = 'N/A'
|
||||||
@ -193,7 +153,7 @@ class RestDataPost(object):
|
|||||||
timeout=10, verify=self._verify_ssl)
|
timeout=10, verify=self._verify_ssl)
|
||||||
if 'error' in self.data:
|
if 'error' in self.data:
|
||||||
del self.data['error']
|
del self.data['error']
|
||||||
self.data = response.json()
|
self.data = response.text
|
||||||
except requests.exceptions.ConnectionError:
|
except requests.exceptions.ConnectionError:
|
||||||
_LOGGER.error("No route to resource/endpoint.")
|
_LOGGER.error("No route to resource/endpoint.")
|
||||||
self.data['error'] = 'N/A'
|
self.data['error'] = 'N/A'
|
||||||
|
117
homeassistant/components/sensor/torque.py
Normal file
117
homeassistant/components/sensor/torque.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.sensor.torque
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Get data from the Torque OBD application.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/sensor.torque/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from homeassistant.const import HTTP_OK
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
|
|
||||||
|
DOMAIN = 'torque'
|
||||||
|
DEPENDENCIES = ['http']
|
||||||
|
SENSOR_EMAIL_FIELD = 'eml'
|
||||||
|
DEFAULT_NAME = 'vehicle'
|
||||||
|
ENTITY_NAME_FORMAT = '{0} {1}'
|
||||||
|
|
||||||
|
API_PATH = '/api/torque'
|
||||||
|
SENSOR_NAME_KEY = r'userFullName(\w+)'
|
||||||
|
SENSOR_UNIT_KEY = r'userUnit(\w+)'
|
||||||
|
SENSOR_VALUE_KEY = r'k(\w+)'
|
||||||
|
|
||||||
|
NAME_KEY = re.compile(SENSOR_NAME_KEY)
|
||||||
|
UNIT_KEY = re.compile(SENSOR_UNIT_KEY)
|
||||||
|
VALUE_KEY = re.compile(SENSOR_VALUE_KEY)
|
||||||
|
|
||||||
|
|
||||||
|
def decode(value):
|
||||||
|
""" Double-decode required. """
|
||||||
|
return value.encode('raw_unicode_escape').decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def convert_pid(value):
|
||||||
|
""" Convert pid from hex string to integer. """
|
||||||
|
return int(value, 16)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
""" Set up Torque platform. """
|
||||||
|
|
||||||
|
vehicle = config.get('name', DEFAULT_NAME)
|
||||||
|
email = config.get('email', None)
|
||||||
|
sensors = {}
|
||||||
|
|
||||||
|
def _receive_data(handler, path_match, data):
|
||||||
|
""" Received data from Torque. """
|
||||||
|
handler.send_response(HTTP_OK)
|
||||||
|
handler.end_headers()
|
||||||
|
|
||||||
|
if email is not None and email != data[SENSOR_EMAIL_FIELD]:
|
||||||
|
return
|
||||||
|
|
||||||
|
names = {}
|
||||||
|
units = {}
|
||||||
|
for key in data:
|
||||||
|
is_name = NAME_KEY.match(key)
|
||||||
|
is_unit = UNIT_KEY.match(key)
|
||||||
|
is_value = VALUE_KEY.match(key)
|
||||||
|
|
||||||
|
if is_name:
|
||||||
|
pid = convert_pid(is_name.group(1))
|
||||||
|
names[pid] = decode(data[key])
|
||||||
|
elif is_unit:
|
||||||
|
pid = convert_pid(is_unit.group(1))
|
||||||
|
units[pid] = decode(data[key])
|
||||||
|
elif is_value:
|
||||||
|
pid = convert_pid(is_value.group(1))
|
||||||
|
if pid in sensors:
|
||||||
|
sensors[pid].on_update(data[key])
|
||||||
|
|
||||||
|
for pid in names:
|
||||||
|
if pid not in sensors:
|
||||||
|
sensors[pid] = TorqueSensor(
|
||||||
|
ENTITY_NAME_FORMAT.format(vehicle, names[pid]),
|
||||||
|
units.get(pid, None))
|
||||||
|
add_devices([sensors[pid]])
|
||||||
|
|
||||||
|
hass.http.register_path('GET', API_PATH, _receive_data)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class TorqueSensor(Entity):
|
||||||
|
""" Represents a Torque sensor. """
|
||||||
|
|
||||||
|
def __init__(self, name, unit):
|
||||||
|
self._name = name
|
||||||
|
self._unit = unit
|
||||||
|
self._state = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
""" Returns the name of the sensor. """
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
""" Returns the unit of measurement. """
|
||||||
|
return self._unit
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
""" State of the sensor. """
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
""" Sensor default icon. """
|
||||||
|
return 'mdi:car'
|
||||||
|
|
||||||
|
def on_update(self, value):
|
||||||
|
""" Receive an update. """
|
||||||
|
self._state = value
|
||||||
|
self.update_ha_state()
|
81
homeassistant/components/sensor/twitch.py
Normal file
81
homeassistant/components/sensor/twitch.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.sensor.twitch
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
A sensor for the Twitch stream status.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/sensor.twitch/
|
||||||
|
"""
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.const import ATTR_ENTITY_PICTURE
|
||||||
|
|
||||||
|
STATE_STREAMING = 'streaming'
|
||||||
|
STATE_OFFLINE = 'offline'
|
||||||
|
ATTR_GAME = 'game'
|
||||||
|
ATTR_TITLE = 'title'
|
||||||
|
ICON = 'mdi:twitch'
|
||||||
|
|
||||||
|
REQUIREMENTS = ['python-twitch==1.2.0']
|
||||||
|
DOMAIN = 'twitch'
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
""" Sets up the Twitch platform. """
|
||||||
|
add_devices(
|
||||||
|
[TwitchSensor(channel) for channel in config.get('channels', [])])
|
||||||
|
|
||||||
|
|
||||||
|
class TwitchSensor(Entity):
|
||||||
|
""" Represents an Twitch channel. """
|
||||||
|
|
||||||
|
# pylint: disable=abstract-method
|
||||||
|
def __init__(self, channel):
|
||||||
|
self._channel = channel
|
||||||
|
self._state = STATE_OFFLINE
|
||||||
|
self._preview = None
|
||||||
|
self._game = None
|
||||||
|
self._title = None
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
""" Device should be polled. """
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
""" Returns the name of the sensor. """
|
||||||
|
return self._channel
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
""" State of the sensor. """
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
# pylint: disable=no-member
|
||||||
|
def update(self):
|
||||||
|
""" Update device state. """
|
||||||
|
from twitch.api import v3 as twitch
|
||||||
|
stream = twitch.streams.by_channel(self._channel).get('stream')
|
||||||
|
if stream:
|
||||||
|
self._game = stream.get('channel').get('game')
|
||||||
|
self._title = stream.get('channel').get('status')
|
||||||
|
self._preview = stream.get('preview').get('small')
|
||||||
|
self._state = STATE_STREAMING
|
||||||
|
else:
|
||||||
|
self._state = STATE_OFFLINE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state_attributes(self):
|
||||||
|
""" Returns the state attributes. """
|
||||||
|
if self._state == STATE_STREAMING:
|
||||||
|
return {
|
||||||
|
ATTR_GAME: self._game,
|
||||||
|
ATTR_TITLE: self._title,
|
||||||
|
ATTR_ENTITY_PICTURE: self._preview
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
return ICON
|
@ -11,9 +11,7 @@ import logging
|
|||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN, STATE_OPEN, STATE_CLOSED
|
from homeassistant.const import CONF_ACCESS_TOKEN, STATE_OPEN, STATE_CLOSED
|
||||||
|
|
||||||
REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/'
|
REQUIREMENTS = ['python-wink==0.3.1']
|
||||||
'42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip'
|
|
||||||
'#python-wink==0.2']
|
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
@ -32,6 +30,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
pywink.set_bearer_token(token)
|
pywink.set_bearer_token(token)
|
||||||
|
|
||||||
add_devices(WinkSensorDevice(sensor) for sensor in pywink.get_sensors())
|
add_devices(WinkSensorDevice(sensor) for sensor in pywink.get_sensors())
|
||||||
|
add_devices(WinkEggMinder(eggtray) for eggtray in pywink.get_eggtrays())
|
||||||
|
|
||||||
|
|
||||||
class WinkSensorDevice(Entity):
|
class WinkSensorDevice(Entity):
|
||||||
@ -48,7 +47,7 @@ class WinkSensorDevice(Entity):
|
|||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
""" Returns the id of this wink sensor """
|
""" Returns the id of this wink sensor """
|
||||||
return "{}.{}".format(self.__class__, self.wink.deviceId())
|
return "{}.{}".format(self.__class__, self.wink.device_id())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -57,9 +56,35 @@ class WinkSensorDevice(Entity):
|
|||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
""" Update state of the sensor. """
|
""" Update state of the sensor. """
|
||||||
self.wink.updateState()
|
self.wink.update_state()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_open(self):
|
def is_open(self):
|
||||||
""" True if door is open. """
|
""" True if door is open. """
|
||||||
return self.wink.state()
|
return self.wink.state()
|
||||||
|
|
||||||
|
|
||||||
|
class WinkEggMinder(Entity):
|
||||||
|
""" Represents a Wink Egg Minder. """
|
||||||
|
|
||||||
|
def __init__(self, wink):
|
||||||
|
self.wink = wink
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
""" Returns the state. """
|
||||||
|
return self.wink.state()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
""" Returns the id of this wink Egg Minder """
|
||||||
|
return "{}.{}".format(self.__class__, self.wink.device_id())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
""" Returns the name of the Egg Minder if any. """
|
||||||
|
return self.wink.name()
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
""" Update state of the Egg Minder. """
|
||||||
|
self.wink.update_state()
|
||||||
|
@ -8,7 +8,9 @@ https://home-assistant.io/components/switch.mqtt/
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import homeassistant.components.mqtt as mqtt
|
import homeassistant.components.mqtt as mqtt
|
||||||
|
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||||
from homeassistant.components.switch import SwitchDevice
|
from homeassistant.components.switch import SwitchDevice
|
||||||
|
from homeassistant.util import template
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -40,14 +42,14 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||||||
config.get('payload_on', DEFAULT_PAYLOAD_ON),
|
config.get('payload_on', DEFAULT_PAYLOAD_ON),
|
||||||
config.get('payload_off', DEFAULT_PAYLOAD_OFF),
|
config.get('payload_off', DEFAULT_PAYLOAD_OFF),
|
||||||
config.get('optimistic', DEFAULT_OPTIMISTIC),
|
config.get('optimistic', DEFAULT_OPTIMISTIC),
|
||||||
config.get('state_format'))])
|
config.get(CONF_VALUE_TEMPLATE))])
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
# pylint: disable=too-many-arguments, too-many-instance-attributes
|
||||||
class MqttSwitch(SwitchDevice):
|
class MqttSwitch(SwitchDevice):
|
||||||
""" Represents a switch that can be toggled using MQTT. """
|
""" Represents a switch that can be toggled using MQTT. """
|
||||||
def __init__(self, hass, name, state_topic, command_topic, qos, retain,
|
def __init__(self, hass, name, state_topic, command_topic, qos, retain,
|
||||||
payload_on, payload_off, optimistic, state_format):
|
payload_on, payload_off, optimistic, value_template):
|
||||||
self._state = False
|
self._state = False
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._name = name
|
self._name = name
|
||||||
@ -58,11 +60,12 @@ class MqttSwitch(SwitchDevice):
|
|||||||
self._payload_on = payload_on
|
self._payload_on = payload_on
|
||||||
self._payload_off = payload_off
|
self._payload_off = payload_off
|
||||||
self._optimistic = optimistic
|
self._optimistic = optimistic
|
||||||
self._parse = mqtt.FmtParser(state_format)
|
|
||||||
|
|
||||||
def message_received(topic, payload, qos):
|
def message_received(topic, payload, qos):
|
||||||
""" A new MQTT message has been received. """
|
""" A new MQTT message has been received. """
|
||||||
payload = self._parse(payload)
|
if value_template is not None:
|
||||||
|
payload = template.render_with_possible_json_value(
|
||||||
|
hass, value_template, payload)
|
||||||
if payload == self._payload_on:
|
if payload == self._payload_on:
|
||||||
self._state = True
|
self._state = True
|
||||||
self.update_ha_state()
|
self.update_ha_state()
|
||||||
|
@ -11,7 +11,7 @@ import logging
|
|||||||
from homeassistant.components.switch import SwitchDevice
|
from homeassistant.components.switch import SwitchDevice
|
||||||
|
|
||||||
DEFAULT_NAME = "Orvibo S20 Switch"
|
DEFAULT_NAME = "Orvibo S20 Switch"
|
||||||
REQUIREMENTS = ['orvibo==1.0.1']
|
REQUIREMENTS = ['orvibo==1.1.0']
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,9 +11,7 @@ import logging
|
|||||||
from homeassistant.components.wink import WinkToggleDevice
|
from homeassistant.components.wink import WinkToggleDevice
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||||
|
|
||||||
REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/'
|
REQUIREMENTS = ['python-wink==0.3.1']
|
||||||
'42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip'
|
|
||||||
'#python-wink==0.2']
|
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
@ -87,7 +87,13 @@ class Thermostat(ThermostatDevice):
|
|||||||
@property
|
@property
|
||||||
def target_temperature(self):
|
def target_temperature(self):
|
||||||
""" Returns the temperature we try to reach. """
|
""" Returns the temperature we try to reach. """
|
||||||
return (self.target_temperature_low + self.target_temperature_high) / 2
|
if self.hvac_mode == 'heat' or self.hvac_mode == 'auxHeatOnly':
|
||||||
|
return self.target_temperature_low
|
||||||
|
elif self.hvac_mode == 'cool':
|
||||||
|
return self.target_temperature_high
|
||||||
|
else:
|
||||||
|
return (self.target_temperature_low +
|
||||||
|
self.target_temperature_high) / 2
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_temperature_low(self):
|
def target_temperature_low(self):
|
||||||
|
117
homeassistant/components/thermostat/heatmiser.py
Normal file
117
homeassistant/components/thermostat/heatmiser.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.components.thermostat.heatmiser
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Adds support for the PRT Heatmiser themostats using the V3 protocol.
|
||||||
|
|
||||||
|
See https://github.com/andylockran/heatmiserV3 for more info on the
|
||||||
|
heatmiserV3 module dependency.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/thermostat.heatmiser/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from homeassistant.components.thermostat import ThermostatDevice
|
||||||
|
from homeassistant.const import TEMP_CELCIUS
|
||||||
|
|
||||||
|
CONF_IPADDRESS = 'ipaddress'
|
||||||
|
CONF_PORT = 'port'
|
||||||
|
CONF_TSTATS = 'tstats'
|
||||||
|
|
||||||
|
REQUIREMENTS = ["heatmiserV3==0.9.1"]
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
""" Sets up the heatmiser thermostat. """
|
||||||
|
|
||||||
|
from heatmiserV3 import heatmiser, connection
|
||||||
|
|
||||||
|
ipaddress = str(config[CONF_IPADDRESS])
|
||||||
|
port = str(config[CONF_PORT])
|
||||||
|
|
||||||
|
if ipaddress is None or port is None:
|
||||||
|
_LOGGER.error("Missing required configuration items %s or %s",
|
||||||
|
CONF_IPADDRESS, CONF_PORT)
|
||||||
|
return False
|
||||||
|
|
||||||
|
serport = connection.connection(ipaddress, port)
|
||||||
|
serport.open()
|
||||||
|
|
||||||
|
tstats = []
|
||||||
|
if CONF_TSTATS in config:
|
||||||
|
tstats = config[CONF_TSTATS]
|
||||||
|
|
||||||
|
if tstats is None:
|
||||||
|
_LOGGER.error("No thermostats configured.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
for tstat in tstats:
|
||||||
|
add_devices([
|
||||||
|
HeatmiserV3Thermostat(
|
||||||
|
heatmiser,
|
||||||
|
tstat.get("id"),
|
||||||
|
tstat.get("name"),
|
||||||
|
serport)
|
||||||
|
])
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
class HeatmiserV3Thermostat(ThermostatDevice):
|
||||||
|
""" Represents a HeatmiserV3 thermostat. """
|
||||||
|
|
||||||
|
# pylint: disable=too-many-instance-attributes
|
||||||
|
def __init__(self, heatmiser, device, name, serport):
|
||||||
|
self.heatmiser = heatmiser
|
||||||
|
self.device = device
|
||||||
|
self.serport = serport
|
||||||
|
self._current_temperature = None
|
||||||
|
self._name = name
|
||||||
|
self._id = device
|
||||||
|
self.dcb = None
|
||||||
|
self.update()
|
||||||
|
self._target_temperature = int(self.dcb.get("roomset"))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
""" Returns the name of the honeywell, if any. """
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self):
|
||||||
|
""" Unit of measurement this thermostat uses."""
|
||||||
|
return TEMP_CELCIUS
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_temperature(self):
|
||||||
|
""" Returns the current temperature. """
|
||||||
|
if self.dcb is not None:
|
||||||
|
low = self.dcb.get("floortemplow ")
|
||||||
|
high = self.dcb.get("floortemphigh")
|
||||||
|
temp = (high*256 + low)/10.0
|
||||||
|
self._current_temperature = temp
|
||||||
|
else:
|
||||||
|
self._current_temperature = None
|
||||||
|
return self._current_temperature
|
||||||
|
|
||||||
|
@property
|
||||||
|
def target_temperature(self):
|
||||||
|
""" Returns the temperature we try to reach. """
|
||||||
|
return self._target_temperature
|
||||||
|
|
||||||
|
def set_temperature(self, temperature):
|
||||||
|
""" Set new target temperature """
|
||||||
|
temperature = int(temperature)
|
||||||
|
self.heatmiser.hmSendAddress(
|
||||||
|
self._id,
|
||||||
|
18,
|
||||||
|
temperature,
|
||||||
|
1,
|
||||||
|
self.serport)
|
||||||
|
self._target_temperature = int(temperature)
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
self.dcb = self.heatmiser.hmReadAddress(
|
||||||
|
self._id,
|
||||||
|
'prt',
|
||||||
|
self.serport)
|
@ -1,8 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
homeassistant.components.wink
|
homeassistant.components.wink
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
Connects to a Wink hub and loads relevant components to control its devices.
|
Connects to a Wink hub and loads relevant components to control its devices.
|
||||||
|
|
||||||
For more details about this component, please refer to the documentation at
|
For more details about this component, please refer to the documentation at
|
||||||
https://home-assistant.io/components/wink/
|
https://home-assistant.io/components/wink/
|
||||||
"""
|
"""
|
||||||
@ -17,9 +16,7 @@ from homeassistant.const import (
|
|||||||
ATTR_SERVICE, ATTR_DISCOVERED, ATTR_FRIENDLY_NAME)
|
ATTR_SERVICE, ATTR_DISCOVERED, ATTR_FRIENDLY_NAME)
|
||||||
|
|
||||||
DOMAIN = "wink"
|
DOMAIN = "wink"
|
||||||
REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/'
|
REQUIREMENTS = ['python-wink==0.3.1']
|
||||||
'42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip'
|
|
||||||
'#python-wink==0.2']
|
|
||||||
|
|
||||||
DISCOVER_LIGHTS = "wink.lights"
|
DISCOVER_LIGHTS = "wink.lights"
|
||||||
DISCOVER_SWITCHES = "wink.switches"
|
DISCOVER_SWITCHES = "wink.switches"
|
||||||
@ -41,7 +38,8 @@ def setup(hass, config):
|
|||||||
for component_name, func_exists, discovery_type in (
|
for component_name, func_exists, discovery_type in (
|
||||||
('light', pywink.get_bulbs, DISCOVER_LIGHTS),
|
('light', pywink.get_bulbs, DISCOVER_LIGHTS),
|
||||||
('switch', pywink.get_switches, DISCOVER_SWITCHES),
|
('switch', pywink.get_switches, DISCOVER_SWITCHES),
|
||||||
('sensor', pywink.get_sensors, DISCOVER_SENSORS),
|
('sensor', lambda: pywink.get_sensors or pywink.get_eggtrays,
|
||||||
|
DISCOVER_SENSORS),
|
||||||
('lock', pywink.get_locks, DISCOVER_LOCKS)):
|
('lock', pywink.get_locks, DISCOVER_LOCKS)):
|
||||||
|
|
||||||
if func_exists():
|
if func_exists():
|
||||||
@ -68,7 +66,7 @@ class WinkToggleDevice(ToggleEntity):
|
|||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
""" Returns the id of this Wink switch. """
|
""" Returns the id of this Wink switch. """
|
||||||
return "{}.{}".format(self.__class__, self.wink.deviceId())
|
return "{}.{}".format(self.__class__, self.wink.device_id())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -89,12 +87,12 @@ class WinkToggleDevice(ToggleEntity):
|
|||||||
|
|
||||||
def turn_on(self, **kwargs):
|
def turn_on(self, **kwargs):
|
||||||
""" Turns the switch on. """
|
""" Turns the switch on. """
|
||||||
self.wink.setState(True)
|
self.wink.set_state(True)
|
||||||
|
|
||||||
def turn_off(self):
|
def turn_off(self):
|
||||||
""" Turns the switch off. """
|
""" Turns the switch off. """
|
||||||
self.wink.setState(False)
|
self.wink.set_state(False)
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
""" Update state of the light. """
|
""" Update state of the light. """
|
||||||
self.wink.updateState()
|
self.wink.update_state()
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
""" Constants used by Home Assistant components. """
|
""" Constants used by Home Assistant components. """
|
||||||
|
|
||||||
__version__ = "0.9.1"
|
__version__ = "0.10.0.dev0"
|
||||||
|
|
||||||
# Can be used to specify a catch all when registering state or event listeners.
|
# Can be used to specify a catch all when registering state or event listeners.
|
||||||
MATCH_ALL = '*'
|
MATCH_ALL = '*'
|
||||||
@ -25,6 +25,8 @@ CONF_PASSWORD = "password"
|
|||||||
CONF_API_KEY = "api_key"
|
CONF_API_KEY = "api_key"
|
||||||
CONF_ACCESS_TOKEN = "access_token"
|
CONF_ACCESS_TOKEN = "access_token"
|
||||||
|
|
||||||
|
CONF_VALUE_TEMPLATE = "value_template"
|
||||||
|
|
||||||
# #### EVENTS ####
|
# #### EVENTS ####
|
||||||
EVENT_HOMEASSISTANT_START = "homeassistant_start"
|
EVENT_HOMEASSISTANT_START = "homeassistant_start"
|
||||||
EVENT_HOMEASSISTANT_STOP = "homeassistant_stop"
|
EVENT_HOMEASSISTANT_STOP = "homeassistant_stop"
|
||||||
@ -165,6 +167,7 @@ URL_API_COMPONENTS = "/api/components"
|
|||||||
URL_API_BOOTSTRAP = "/api/bootstrap"
|
URL_API_BOOTSTRAP = "/api/bootstrap"
|
||||||
URL_API_ERROR_LOG = "/api/error_log"
|
URL_API_ERROR_LOG = "/api/error_log"
|
||||||
URL_API_LOG_OUT = "/api/log_out"
|
URL_API_LOG_OUT = "/api/log_out"
|
||||||
|
URL_API_TEMPLATE = "/api/template"
|
||||||
|
|
||||||
HTTP_OK = 200
|
HTTP_OK = 200
|
||||||
HTTP_CREATED = 201
|
HTTP_CREATED = 201
|
||||||
|
@ -14,3 +14,10 @@ class InvalidEntityFormatError(HomeAssistantError):
|
|||||||
class NoEntitySpecifiedError(HomeAssistantError):
|
class NoEntitySpecifiedError(HomeAssistantError):
|
||||||
""" When no entity is specified. """
|
""" When no entity is specified. """
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateError(HomeAssistantError):
|
||||||
|
""" Error during template rendering. """
|
||||||
|
def __init__(self, exception):
|
||||||
|
super().__init__('{}: {}'.format(exception.__class__.__name__,
|
||||||
|
exception))
|
||||||
|
113
homeassistant/util/template.py
Normal file
113
homeassistant/util/template.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
"""
|
||||||
|
homeassistant.util.template
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Template utility methods for rendering strings with HA data.
|
||||||
|
"""
|
||||||
|
# pylint: disable=too-few-public-methods
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import jinja2
|
||||||
|
from jinja2.sandbox import ImmutableSandboxedEnvironment
|
||||||
|
from homeassistant.const import STATE_UNKNOWN
|
||||||
|
from homeassistant.exceptions import TemplateError
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
_SENTINEL = object()
|
||||||
|
|
||||||
|
|
||||||
|
def render_with_possible_json_value(hass, template, value,
|
||||||
|
error_value=_SENTINEL):
|
||||||
|
""" Renders template with value exposed.
|
||||||
|
If valid JSON will expose value_json too. """
|
||||||
|
variables = {
|
||||||
|
'value': value
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
variables['value_json'] = json.loads(value)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
return render(hass, template, variables)
|
||||||
|
except TemplateError:
|
||||||
|
_LOGGER.exception('Error parsing value')
|
||||||
|
return value if error_value is _SENTINEL else error_value
|
||||||
|
|
||||||
|
|
||||||
|
def render(hass, template, variables=None, **kwargs):
|
||||||
|
""" Render given template. """
|
||||||
|
if variables is not None:
|
||||||
|
kwargs.update(variables)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return ENV.from_string(template, {
|
||||||
|
'states': AllStates(hass),
|
||||||
|
'is_state': hass.states.is_state
|
||||||
|
}).render(kwargs).strip()
|
||||||
|
except jinja2.TemplateError as err:
|
||||||
|
raise TemplateError(err)
|
||||||
|
|
||||||
|
|
||||||
|
class AllStates(object):
|
||||||
|
""" Class to expose all HA states as attributes. """
|
||||||
|
def __init__(self, hass):
|
||||||
|
self._hass = hass
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return DomainStates(self._hass, name)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(sorted(self._hass.states.all(),
|
||||||
|
key=lambda state: state.entity_id))
|
||||||
|
|
||||||
|
def __call__(self, entity_id):
|
||||||
|
state = self._hass.states.get(entity_id)
|
||||||
|
return STATE_UNKNOWN if state is None else state.state
|
||||||
|
|
||||||
|
|
||||||
|
class DomainStates(object):
|
||||||
|
""" Class to expose a specific HA domain as attributes. """
|
||||||
|
|
||||||
|
def __init__(self, hass, domain):
|
||||||
|
self._hass = hass
|
||||||
|
self._domain = domain
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return self._hass.states.get('{}.{}'.format(self._domain, name))
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(sorted(
|
||||||
|
(state for state in self._hass.states.all()
|
||||||
|
if state.domain == self._domain),
|
||||||
|
key=lambda state: state.entity_id))
|
||||||
|
|
||||||
|
|
||||||
|
def forgiving_round(value, precision=0):
|
||||||
|
""" Rounding method that accepts strings. """
|
||||||
|
try:
|
||||||
|
value = round(float(value), precision)
|
||||||
|
return int(value) if precision == 0 else value
|
||||||
|
except ValueError:
|
||||||
|
# If value can't be converted to float
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def multiply(value, amount):
|
||||||
|
""" Converts to float and multiplies value. """
|
||||||
|
try:
|
||||||
|
return float(value) * amount
|
||||||
|
except ValueError:
|
||||||
|
# If value can't be converted to float
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
||||||
|
""" Home Assistant template environment. """
|
||||||
|
|
||||||
|
def is_safe_callable(self, obj):
|
||||||
|
return isinstance(obj, AllStates) or super().is_safe_callable(obj)
|
||||||
|
|
||||||
|
ENV = TemplateEnvironment()
|
||||||
|
ENV.filters['round'] = forgiving_round
|
||||||
|
ENV.filters['multiply'] = multiply
|
@ -2,4 +2,4 @@ requests>=2,<3
|
|||||||
pyyaml>=3.11,<4
|
pyyaml>=3.11,<4
|
||||||
pytz>=2015.4
|
pytz>=2015.4
|
||||||
pip>=7.0.0
|
pip>=7.0.0
|
||||||
vincenty==0.1.3
|
vincenty==0.1.3
|
@ -4,10 +4,17 @@ pyyaml>=3.11,<4
|
|||||||
pytz>=2015.4
|
pytz>=2015.4
|
||||||
pip>=7.0.0
|
pip>=7.0.0
|
||||||
vincenty==0.1.3
|
vincenty==0.1.3
|
||||||
|
jinja2>=2.8
|
||||||
|
|
||||||
# homeassistant.components.arduino
|
# homeassistant.components.arduino
|
||||||
PyMata==2.07a
|
PyMata==2.07a
|
||||||
|
|
||||||
|
# homeassistant.components.conversation
|
||||||
|
fuzzywuzzy==0.8.0
|
||||||
|
|
||||||
|
# homeassistant.components.device_tracker.icloud
|
||||||
|
pyicloud==0.7.2
|
||||||
|
|
||||||
# homeassistant.components.device_tracker.netgear
|
# homeassistant.components.device_tracker.netgear
|
||||||
pynetgear==0.3
|
pynetgear==0.3
|
||||||
|
|
||||||
@ -59,10 +66,10 @@ https://github.com/pavoni/home-assistant-vera-api/archive/efdba4e63d58a30bc9b36d
|
|||||||
# homeassistant.components.lock.wink
|
# homeassistant.components.lock.wink
|
||||||
# homeassistant.components.sensor.wink
|
# homeassistant.components.sensor.wink
|
||||||
# homeassistant.components.switch.wink
|
# homeassistant.components.switch.wink
|
||||||
https://github.com/balloob/python-wink/archive/42fdcfa721b1bc583688e3592d8427f4c13ba6d9.zip#python-wink==0.2
|
python-wink==0.3.1
|
||||||
|
|
||||||
# homeassistant.components.media_player.cast
|
# homeassistant.components.media_player.cast
|
||||||
pychromecast==0.6.12
|
pychromecast==0.6.13
|
||||||
|
|
||||||
# homeassistant.components.media_player.kodi
|
# homeassistant.components.media_player.kodi
|
||||||
jsonrpc-requests==0.1
|
jsonrpc-requests==0.1
|
||||||
@ -82,9 +89,6 @@ https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b6
|
|||||||
# homeassistant.components.mqtt
|
# homeassistant.components.mqtt
|
||||||
paho-mqtt==1.1
|
paho-mqtt==1.1
|
||||||
|
|
||||||
# homeassistant.components.mqtt
|
|
||||||
jsonpath-rw==1.4.0
|
|
||||||
|
|
||||||
# homeassistant.components.notify.pushbullet
|
# homeassistant.components.notify.pushbullet
|
||||||
pushbullet.py==0.9.0
|
pushbullet.py==0.9.0
|
||||||
|
|
||||||
@ -118,6 +122,12 @@ py-cpuinfo==0.1.6
|
|||||||
# homeassistant.components.sensor.dht
|
# homeassistant.components.sensor.dht
|
||||||
# http://github.com/mala-zaba/Adafruit_Python_DHT/archive/4101340de8d2457dd194bca1e8d11cbfc237e919.zip#Adafruit_DHT==1.1.0
|
# http://github.com/mala-zaba/Adafruit_Python_DHT/archive/4101340de8d2457dd194bca1e8d11cbfc237e919.zip#Adafruit_DHT==1.1.0
|
||||||
|
|
||||||
|
# homeassistant.components.sensor.dweet
|
||||||
|
dweepy==0.2.0
|
||||||
|
|
||||||
|
# homeassistant.components.sensor.eliqonline
|
||||||
|
eliqonline==1.0.11
|
||||||
|
|
||||||
# homeassistant.components.sensor.forecast
|
# homeassistant.components.sensor.forecast
|
||||||
python-forecastio==1.3.3
|
python-forecastio==1.3.3
|
||||||
|
|
||||||
@ -144,6 +154,9 @@ https://github.com/rkabadi/temper-python/archive/3dbdaf2d87b8db9a3cd6e5585fc7045
|
|||||||
# homeassistant.components.switch.transmission
|
# homeassistant.components.switch.transmission
|
||||||
transmissionrpc==0.11
|
transmissionrpc==0.11
|
||||||
|
|
||||||
|
# homeassistant.components.sensor.twitch
|
||||||
|
python-twitch==1.2.0
|
||||||
|
|
||||||
# homeassistant.components.sun
|
# homeassistant.components.sun
|
||||||
astral==0.8.1
|
astral==0.8.1
|
||||||
|
|
||||||
@ -154,11 +167,14 @@ https://github.com/rkabadi/pyedimax/archive/365301ce3ff26129a7910c501ead09ea625f
|
|||||||
hikvision==0.4
|
hikvision==0.4
|
||||||
|
|
||||||
# homeassistant.components.switch.orvibo
|
# homeassistant.components.switch.orvibo
|
||||||
orvibo==1.0.1
|
orvibo==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.switch.wemo
|
# homeassistant.components.switch.wemo
|
||||||
pywemo==0.3.3
|
pywemo==0.3.3
|
||||||
|
|
||||||
|
# homeassistant.components.thermostat.heatmiser
|
||||||
|
heatmiserV3==0.9.1
|
||||||
|
|
||||||
# homeassistant.components.thermostat.honeywell
|
# homeassistant.components.thermostat.honeywell
|
||||||
evohomeclient==0.2.4
|
evohomeclient==0.2.4
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
echo "Bootstrapping frontend..."
|
echo "Bootstrapping frontend..."
|
||||||
|
git submodule update
|
||||||
cd homeassistant/components/frontend/www_static/home-assistant-polymer
|
cd homeassistant/components/frontend/www_static/home-assistant-polymer
|
||||||
npm install
|
npm install
|
||||||
|
bower install
|
||||||
npm run setup_js_dev
|
npm run setup_js_dev
|
||||||
cd ../../../../..
|
cd ../../../../..
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
echo "Update the submodule to latest version..."
|
|
||||||
git submodule update
|
|
||||||
|
|
||||||
echo "Installing dependencies..."
|
echo "Installing dependencies..."
|
||||||
python3 -m pip install --upgrade -r requirements_all.txt
|
python3 -m pip install -r requirements_all.txt
|
||||||
|
|
||||||
|
REQ_STATUS=$?
|
||||||
|
|
||||||
echo "Installing development dependencies.."
|
echo "Installing development dependencies.."
|
||||||
python3 -m pip install --upgrade flake8 pylint coveralls pytest pytest-cov
|
python3 -m pip install flake8 pylint coveralls pytest pytest-cov
|
||||||
|
|
||||||
|
REQ_DEV_STATUS=$?
|
||||||
|
|
||||||
|
if [ $REQ_DEV_STATUS -eq 0 ]
|
||||||
|
then
|
||||||
|
exit $REQ_STATUS
|
||||||
|
else
|
||||||
|
exit $REQ_DEV_STATUS
|
||||||
|
fi
|
||||||
|
@ -7,6 +7,7 @@ npm run frontend_prod
|
|||||||
|
|
||||||
cp bower_components/webcomponentsjs/webcomponents-lite.min.js ..
|
cp bower_components/webcomponentsjs/webcomponents-lite.min.js ..
|
||||||
cp build/frontend.html ..
|
cp build/frontend.html ..
|
||||||
|
cp build/service_worker.js ..
|
||||||
|
|
||||||
# Generate the MD5 hash of the new frontend
|
# Generate the MD5 hash of the new frontend
|
||||||
cd ../..
|
cd ../..
|
||||||
|
@ -5,6 +5,12 @@
|
|||||||
|
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
if [ "$TRAVIS_PYTHON_VERSION" != "3.4" ]; then
|
||||||
|
NO_LINT=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export NO_LINT
|
||||||
|
|
||||||
script/test coverage
|
script/test coverage
|
||||||
|
|
||||||
STATUS=$?
|
STATUS=$?
|
||||||
|
@ -8,10 +8,11 @@ import importlib
|
|||||||
import os
|
import os
|
||||||
import pkgutil
|
import pkgutil
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
COMMENT_REQUIREMENTS = [
|
COMMENT_REQUIREMENTS = [
|
||||||
'RPi.GPIO',
|
'RPi.GPIO',
|
||||||
'Adafruit_Python_DHT'
|
'Adafruit_Python_DHT',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -67,8 +68,9 @@ def gather_modules():
|
|||||||
reqs.setdefault(req, []).append(package)
|
reqs.setdefault(req, []).append(package)
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
print("Found errors")
|
print("******* ERROR")
|
||||||
print('\n'.join(errors))
|
print("Errors while importing: ", ', '.join(errors))
|
||||||
|
print("Make sure you import 3rd party libraries inside methods.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
output.append('# Home Assistant core')
|
output.append('# Home Assistant core')
|
||||||
@ -94,6 +96,12 @@ def write_file(data):
|
|||||||
req_file.write(data)
|
req_file.write(data)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_file(data):
|
||||||
|
""" Validates if requirements_all.txt is up to date. """
|
||||||
|
with open('requirements_all.txt', 'r') as req_file:
|
||||||
|
return data == ''.join(req_file)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
""" Main """
|
""" Main """
|
||||||
if not os.path.isfile('requirements_all.txt'):
|
if not os.path.isfile('requirements_all.txt'):
|
||||||
@ -103,7 +111,16 @@ def main():
|
|||||||
data = gather_modules()
|
data = gather_modules()
|
||||||
|
|
||||||
if data is None:
|
if data is None:
|
||||||
return
|
sys.exit(1)
|
||||||
|
|
||||||
|
if sys.argv[-1] == 'validate':
|
||||||
|
if validate_file(data):
|
||||||
|
print("requirements_all.txt is up to date.")
|
||||||
|
sys.exit(0)
|
||||||
|
print("******* ERROR")
|
||||||
|
print("requirements_all.txt is not up to date")
|
||||||
|
print("Please run script/gen_requirements_all.py")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
write_file(data)
|
write_file(data)
|
||||||
|
|
||||||
|
@ -5,9 +5,12 @@
|
|||||||
|
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
script/lint
|
if [ "$NO_LINT" = "1" ]; then
|
||||||
|
LINT_STATUS=0
|
||||||
LINT_STATUS=$?
|
else
|
||||||
|
script/lint
|
||||||
|
LINT_STATUS=$?
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Running tests..."
|
echo "Running tests..."
|
||||||
|
|
||||||
|
4
setup.py
4
setup.py
@ -1,3 +1,4 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
import os
|
import os
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
from homeassistant.const import __version__
|
from homeassistant.const import __version__
|
||||||
@ -14,7 +15,8 @@ REQUIRES = [
|
|||||||
'pyyaml>=3.11,<4',
|
'pyyaml>=3.11,<4',
|
||||||
'pytz>=2015.4',
|
'pytz>=2015.4',
|
||||||
'pip>=7.0.0',
|
'pip>=7.0.0',
|
||||||
'vincenty==0.1.3'
|
'vincenty==0.1.3',
|
||||||
|
'jinja2>=2.8'
|
||||||
]
|
]
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
|
@ -295,7 +295,7 @@ class TestAutomationNumericState(unittest.TestCase):
|
|||||||
'trigger': {
|
'trigger': {
|
||||||
'platform': 'numeric_state',
|
'platform': 'numeric_state',
|
||||||
'entity_id': 'test.entity',
|
'entity_id': 'test.entity',
|
||||||
'attribute': 'test_attribute',
|
'value_template': '{{ state.attributes.test_attribute }}',
|
||||||
'below': 10,
|
'below': 10,
|
||||||
},
|
},
|
||||||
'action': {
|
'action': {
|
||||||
@ -314,7 +314,7 @@ class TestAutomationNumericState(unittest.TestCase):
|
|||||||
'trigger': {
|
'trigger': {
|
||||||
'platform': 'numeric_state',
|
'platform': 'numeric_state',
|
||||||
'entity_id': 'test.entity',
|
'entity_id': 'test.entity',
|
||||||
'attribute': 'test_attribute',
|
'value_template': '{{ state.attributes.test_attribute }}',
|
||||||
'below': 10,
|
'below': 10,
|
||||||
},
|
},
|
||||||
'action': {
|
'action': {
|
||||||
@ -333,7 +333,7 @@ class TestAutomationNumericState(unittest.TestCase):
|
|||||||
'trigger': {
|
'trigger': {
|
||||||
'platform': 'numeric_state',
|
'platform': 'numeric_state',
|
||||||
'entity_id': 'test.entity',
|
'entity_id': 'test.entity',
|
||||||
'attribute': 'test_attribute',
|
'value_template': '{{ state.attributes.test_attribute }}',
|
||||||
'below': 10,
|
'below': 10,
|
||||||
},
|
},
|
||||||
'action': {
|
'action': {
|
||||||
@ -352,7 +352,7 @@ class TestAutomationNumericState(unittest.TestCase):
|
|||||||
'trigger': {
|
'trigger': {
|
||||||
'platform': 'numeric_state',
|
'platform': 'numeric_state',
|
||||||
'entity_id': 'test.entity',
|
'entity_id': 'test.entity',
|
||||||
'attribute': 'test_attribute',
|
'value_template': '{{ state.attributes.test_attribute }}',
|
||||||
'below': 10,
|
'below': 10,
|
||||||
},
|
},
|
||||||
'action': {
|
'action': {
|
||||||
@ -371,7 +371,7 @@ class TestAutomationNumericState(unittest.TestCase):
|
|||||||
'trigger': {
|
'trigger': {
|
||||||
'platform': 'numeric_state',
|
'platform': 'numeric_state',
|
||||||
'entity_id': 'test.entity',
|
'entity_id': 'test.entity',
|
||||||
'attribute': 'test_attribute',
|
'value_template': '{{ state.attributes.test_attribute }}',
|
||||||
'below': 10,
|
'below': 10,
|
||||||
},
|
},
|
||||||
'action': {
|
'action': {
|
||||||
@ -384,13 +384,51 @@ class TestAutomationNumericState(unittest.TestCase):
|
|||||||
self.hass.pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
self.assertEqual(1, len(self.calls))
|
self.assertEqual(1, len(self.calls))
|
||||||
|
|
||||||
|
def test_template_list(self):
|
||||||
|
self.assertTrue(automation.setup(self.hass, {
|
||||||
|
automation.DOMAIN: {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'numeric_state',
|
||||||
|
'entity_id': 'test.entity',
|
||||||
|
'value_template': '{{ state.attributes.test_attribute[2] }}',
|
||||||
|
'below': 10,
|
||||||
|
},
|
||||||
|
'action': {
|
||||||
|
'service': 'test.automation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
# 3 is below 10
|
||||||
|
self.hass.states.set('test.entity', 'entity', { 'test_attribute': [11, 15, 3] })
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(1, len(self.calls))
|
||||||
|
|
||||||
|
def test_template_string(self):
|
||||||
|
self.assertTrue(automation.setup(self.hass, {
|
||||||
|
automation.DOMAIN: {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'numeric_state',
|
||||||
|
'entity_id': 'test.entity',
|
||||||
|
'value_template': '{{ state.attributes.test_attribute | multiply(10) }}',
|
||||||
|
'below': 10,
|
||||||
|
},
|
||||||
|
'action': {
|
||||||
|
'service': 'test.automation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
# 9 is below 10
|
||||||
|
self.hass.states.set('test.entity', 'entity', { 'test_attribute': '0.9' })
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(1, len(self.calls))
|
||||||
|
|
||||||
def test_if_not_fires_on_attribute_change_with_attribute_not_below_multiple_attributes(self):
|
def test_if_not_fires_on_attribute_change_with_attribute_not_below_multiple_attributes(self):
|
||||||
self.assertTrue(automation.setup(self.hass, {
|
self.assertTrue(automation.setup(self.hass, {
|
||||||
automation.DOMAIN: {
|
automation.DOMAIN: {
|
||||||
'trigger': {
|
'trigger': {
|
||||||
'platform': 'numeric_state',
|
'platform': 'numeric_state',
|
||||||
'entity_id': 'test.entity',
|
'entity_id': 'test.entity',
|
||||||
'attribute': 'test_attribute',
|
'value_template': '{{ state.attributes.test_attribute }}',
|
||||||
'below': 10,
|
'below': 10,
|
||||||
},
|
},
|
||||||
'action': {
|
'action': {
|
||||||
|
355
tests/components/automation/test_template.py
Normal file
355
tests/components/automation/test_template.py
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
"""
|
||||||
|
tests.components.automation.test_template
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Tests template automation.
|
||||||
|
"""
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import homeassistant.core as ha
|
||||||
|
import homeassistant.components.automation as automation
|
||||||
|
|
||||||
|
|
||||||
|
class TestAutomationTemplate(unittest.TestCase):
|
||||||
|
""" Test the event automation. """
|
||||||
|
|
||||||
|
def setUp(self): # pylint: disable=invalid-name
|
||||||
|
self.hass = ha.HomeAssistant()
|
||||||
|
self.hass.states.set('test.entity', 'hello')
|
||||||
|
self.calls = []
|
||||||
|
|
||||||
|
def record_call(service):
|
||||||
|
self.calls.append(service)
|
||||||
|
|
||||||
|
self.hass.services.register('test', 'automation', record_call)
|
||||||
|
|
||||||
|
def tearDown(self): # pylint: disable=invalid-name
|
||||||
|
""" Stop down stuff we started. """
|
||||||
|
self.hass.stop()
|
||||||
|
|
||||||
|
def test_if_fires_on_change_bool(self):
|
||||||
|
self.assertTrue(automation.setup(self.hass, {
|
||||||
|
automation.DOMAIN: {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'template',
|
||||||
|
'value_template': '{{ true }}',
|
||||||
|
},
|
||||||
|
'action': {
|
||||||
|
'service': 'test.automation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
self.hass.states.set('test.entity', 'world')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(1, len(self.calls))
|
||||||
|
|
||||||
|
def test_if_fires_on_change_str(self):
|
||||||
|
self.assertTrue(automation.setup(self.hass, {
|
||||||
|
automation.DOMAIN: {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'template',
|
||||||
|
'value_template': 'true',
|
||||||
|
},
|
||||||
|
'action': {
|
||||||
|
'service': 'test.automation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
self.hass.states.set('test.entity', 'world')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(1, len(self.calls))
|
||||||
|
|
||||||
|
def test_if_fires_on_change_str_crazy(self):
|
||||||
|
self.assertTrue(automation.setup(self.hass, {
|
||||||
|
automation.DOMAIN: {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'template',
|
||||||
|
'value_template': 'TrUE',
|
||||||
|
},
|
||||||
|
'action': {
|
||||||
|
'service': 'test.automation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
self.hass.states.set('test.entity', 'world')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(1, len(self.calls))
|
||||||
|
|
||||||
|
def test_if_not_fires_on_change_bool(self):
|
||||||
|
self.assertTrue(automation.setup(self.hass, {
|
||||||
|
automation.DOMAIN: {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'template',
|
||||||
|
'value_template': '{{ false }}',
|
||||||
|
},
|
||||||
|
'action': {
|
||||||
|
'service': 'test.automation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
self.hass.states.set('test.entity', 'world')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(0, len(self.calls))
|
||||||
|
|
||||||
|
def test_if_not_fires_on_change_str(self):
|
||||||
|
self.assertTrue(automation.setup(self.hass, {
|
||||||
|
automation.DOMAIN: {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'template',
|
||||||
|
'value_template': 'False',
|
||||||
|
},
|
||||||
|
'action': {
|
||||||
|
'service': 'test.automation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
self.hass.states.set('test.entity', 'world')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(0, len(self.calls))
|
||||||
|
|
||||||
|
def test_if_not_fires_on_change_str_crazy(self):
|
||||||
|
self.assertTrue(automation.setup(self.hass, {
|
||||||
|
automation.DOMAIN: {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'template',
|
||||||
|
'value_template': 'Anything other than "true" is false.',
|
||||||
|
},
|
||||||
|
'action': {
|
||||||
|
'service': 'test.automation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
self.hass.states.set('test.entity', 'world')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(0, len(self.calls))
|
||||||
|
|
||||||
|
def test_if_fires_on_no_change(self):
|
||||||
|
self.assertTrue(automation.setup(self.hass, {
|
||||||
|
automation.DOMAIN: {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'template',
|
||||||
|
'value_template': '{{ true }}',
|
||||||
|
},
|
||||||
|
'action': {
|
||||||
|
'service': 'test.automation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
self.hass.states.set('test.entity', 'hello')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(0, len(self.calls))
|
||||||
|
|
||||||
|
def test_if_fires_on_two_change(self):
|
||||||
|
self.assertTrue(automation.setup(self.hass, {
|
||||||
|
automation.DOMAIN: {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'template',
|
||||||
|
'value_template': '{{ true }}',
|
||||||
|
},
|
||||||
|
'action': {
|
||||||
|
'service': 'test.automation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
# Trigger once
|
||||||
|
self.hass.states.set('test.entity', 'world')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(1, len(self.calls))
|
||||||
|
|
||||||
|
# Trigger again
|
||||||
|
self.hass.states.set('test.entity', 'world')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(1, len(self.calls))
|
||||||
|
|
||||||
|
def test_if_fires_on_change_with_template(self):
|
||||||
|
self.assertTrue(automation.setup(self.hass, {
|
||||||
|
automation.DOMAIN: {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'template',
|
||||||
|
'value_template': '{{ is_state("test.entity", "world") }}',
|
||||||
|
},
|
||||||
|
'action': {
|
||||||
|
'service': 'test.automation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
self.hass.states.set('test.entity', 'world')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(1, len(self.calls))
|
||||||
|
|
||||||
|
def test_if_not_fires_on_change_with_template(self):
|
||||||
|
self.assertTrue(automation.setup(self.hass, {
|
||||||
|
automation.DOMAIN: {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'template',
|
||||||
|
'value_template': '{{ is_state("test.entity", "hello") }}',
|
||||||
|
},
|
||||||
|
'action': {
|
||||||
|
'service': 'test.automation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
self.hass.states.set('test.entity', 'world')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(0, len(self.calls))
|
||||||
|
|
||||||
|
def test_if_fires_on_change_with_template_advanced(self):
|
||||||
|
self.assertTrue(automation.setup(self.hass, {
|
||||||
|
automation.DOMAIN: {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'template',
|
||||||
|
'value_template': '''{%- if is_state("test.entity", "world") -%}
|
||||||
|
true
|
||||||
|
{%- else -%}
|
||||||
|
false
|
||||||
|
{%- endif -%}''',
|
||||||
|
},
|
||||||
|
'action': {
|
||||||
|
'service': 'test.automation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
self.hass.states.set('test.entity', 'world')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(1, len(self.calls))
|
||||||
|
|
||||||
|
def test_if_fires_on_no_change_with_template_advanced(self):
|
||||||
|
self.assertTrue(automation.setup(self.hass, {
|
||||||
|
automation.DOMAIN: {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'template',
|
||||||
|
'value_template': '''{%- if is_state("test.entity", "world") -%}
|
||||||
|
true
|
||||||
|
{%- else -%}
|
||||||
|
false
|
||||||
|
{%- endif -%}''',
|
||||||
|
},
|
||||||
|
'action': {
|
||||||
|
'service': 'test.automation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
# Different state
|
||||||
|
self.hass.states.set('test.entity', 'worldz')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(0, len(self.calls))
|
||||||
|
|
||||||
|
# Different state
|
||||||
|
self.hass.states.set('test.entity', 'hello')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(0, len(self.calls))
|
||||||
|
|
||||||
|
def test_if_fires_on_change_with_template_2(self):
|
||||||
|
self.assertTrue(automation.setup(self.hass, {
|
||||||
|
automation.DOMAIN: {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'template',
|
||||||
|
'value_template': '{{ not is_state("test.entity", "world") }}',
|
||||||
|
},
|
||||||
|
'action': {
|
||||||
|
'service': 'test.automation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
self.hass.states.set('test.entity', 'world')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(0, len(self.calls))
|
||||||
|
|
||||||
|
self.hass.states.set('test.entity', 'home')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(1, len(self.calls))
|
||||||
|
|
||||||
|
self.hass.states.set('test.entity', 'work')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(1, len(self.calls))
|
||||||
|
|
||||||
|
self.hass.states.set('test.entity', 'not_home')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(1, len(self.calls))
|
||||||
|
|
||||||
|
self.hass.states.set('test.entity', 'world')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(1, len(self.calls))
|
||||||
|
|
||||||
|
self.hass.states.set('test.entity', 'home')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(2, len(self.calls))
|
||||||
|
|
||||||
|
def test_if_action(self):
|
||||||
|
automation.setup(self.hass, {
|
||||||
|
automation.DOMAIN: {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'event',
|
||||||
|
'event_type': 'test_event',
|
||||||
|
},
|
||||||
|
'condition': [{
|
||||||
|
'platform': 'template',
|
||||||
|
'value_template': '{{ is_state("test.entity", "world") }}'
|
||||||
|
}],
|
||||||
|
'action': {
|
||||||
|
'service': 'test.automation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Condition is not true yet
|
||||||
|
self.hass.bus.fire('test_event')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(0, len(self.calls))
|
||||||
|
|
||||||
|
# Change condition to true, but it shouldn't be triggered yet
|
||||||
|
self.hass.states.set('test.entity', 'world')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(0, len(self.calls))
|
||||||
|
|
||||||
|
# Condition is true and event is triggered
|
||||||
|
self.hass.bus.fire('test_event')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(1, len(self.calls))
|
||||||
|
|
||||||
|
def test_if_fires_on_change_with_bad_template(self):
|
||||||
|
self.assertTrue(automation.setup(self.hass, {
|
||||||
|
automation.DOMAIN: {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'template',
|
||||||
|
'value_template': '{{ ',
|
||||||
|
},
|
||||||
|
'action': {
|
||||||
|
'service': 'test.automation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
self.hass.states.set('test.entity', 'world')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(0, len(self.calls))
|
||||||
|
|
||||||
|
def test_if_fires_on_change_with_bad_template_2(self):
|
||||||
|
self.assertTrue(automation.setup(self.hass, {
|
||||||
|
automation.DOMAIN: {
|
||||||
|
'trigger': {
|
||||||
|
'platform': 'template',
|
||||||
|
'value_template': '{{ xyz | round(0) }}',
|
||||||
|
},
|
||||||
|
'action': {
|
||||||
|
'service': 'test.automation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
self.hass.states.set('test.entity', 'world')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
self.assertEqual(0, len(self.calls))
|
@ -62,6 +62,15 @@ class TestLightMQTT(unittest.TestCase):
|
|||||||
""" Stop down stuff we started. """
|
""" Stop down stuff we started. """
|
||||||
self.hass.stop()
|
self.hass.stop()
|
||||||
|
|
||||||
|
def test_fail_setup_if_no_command_topic(self):
|
||||||
|
self.assertTrue(light.setup(self.hass, {
|
||||||
|
'light': {
|
||||||
|
'platform': 'mqtt',
|
||||||
|
'name': 'test',
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
self.assertIsNone(self.hass.states.get('light.test'))
|
||||||
|
|
||||||
def test_no_color_or_brightness_if_no_topics(self):
|
def test_no_color_or_brightness_if_no_topics(self):
|
||||||
self.assertTrue(light.setup(self.hass, {
|
self.assertTrue(light.setup(self.hass, {
|
||||||
'light': {
|
'light': {
|
||||||
@ -77,7 +86,7 @@ class TestLightMQTT(unittest.TestCase):
|
|||||||
self.assertIsNone(state.attributes.get('rgb_color'))
|
self.assertIsNone(state.attributes.get('rgb_color'))
|
||||||
self.assertIsNone(state.attributes.get('brightness'))
|
self.assertIsNone(state.attributes.get('brightness'))
|
||||||
|
|
||||||
fire_mqtt_message(self.hass, 'test_light_rgb/status', 'on')
|
fire_mqtt_message(self.hass, 'test_light_rgb/status', 'ON')
|
||||||
self.hass.pool.block_till_done()
|
self.hass.pool.block_till_done()
|
||||||
|
|
||||||
state = self.hass.states.get('light.test')
|
state = self.hass.states.get('light.test')
|
||||||
@ -143,6 +152,39 @@ class TestLightMQTT(unittest.TestCase):
|
|||||||
self.assertEqual([125, 125, 125],
|
self.assertEqual([125, 125, 125],
|
||||||
light_state.attributes.get('rgb_color'))
|
light_state.attributes.get('rgb_color'))
|
||||||
|
|
||||||
|
def test_controlling_state_via_topic_with_templates(self):
|
||||||
|
self.assertTrue(light.setup(self.hass, {
|
||||||
|
'light': {
|
||||||
|
'platform': 'mqtt',
|
||||||
|
'name': 'test',
|
||||||
|
'state_topic': 'test_light_rgb/status',
|
||||||
|
'command_topic': 'test_light_rgb/set',
|
||||||
|
'brightness_state_topic': 'test_light_rgb/brightness/status',
|
||||||
|
'rgb_state_topic': 'test_light_rgb/rgb/status',
|
||||||
|
'state_value_template': '{{ value_json.hello }}',
|
||||||
|
'brightness_value_template': '{{ value_json.hello }}',
|
||||||
|
'rgb_value_template': '{{ value_json.hello | join(",") }}',
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
state = self.hass.states.get('light.test')
|
||||||
|
self.assertEqual(STATE_OFF, state.state)
|
||||||
|
self.assertIsNone(state.attributes.get('brightness'))
|
||||||
|
self.assertIsNone(state.attributes.get('rgb_color'))
|
||||||
|
|
||||||
|
fire_mqtt_message(self.hass, 'test_light_rgb/status',
|
||||||
|
'{"hello": "ON"}')
|
||||||
|
fire_mqtt_message(self.hass, 'test_light_rgb/brightness/status',
|
||||||
|
'{"hello": "50"}')
|
||||||
|
fire_mqtt_message(self.hass, 'test_light_rgb/rgb/status',
|
||||||
|
'{"hello": [1, 2, 3]}')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
|
||||||
|
state = self.hass.states.get('light.test')
|
||||||
|
self.assertEqual(STATE_ON, state.state)
|
||||||
|
self.assertEqual(50, state.attributes.get('brightness'))
|
||||||
|
self.assertEqual([1, 2, 3], state.attributes.get('rgb_color'))
|
||||||
|
|
||||||
def test_sending_mqtt_commands_and_optimistic(self):
|
def test_sending_mqtt_commands_and_optimistic(self):
|
||||||
self.assertTrue(light.setup(self.hass, {
|
self.assertTrue(light.setup(self.hass, {
|
||||||
'light': {
|
'light': {
|
||||||
|
0
tests/components/notify/__init__.py
Normal file
0
tests/components/notify/__init__.py
Normal file
41
tests/components/notify/test_demo.py
Normal file
41
tests/components/notify/test_demo.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
"""
|
||||||
|
tests.components.notify.test_demo
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Tests notify demo component
|
||||||
|
"""
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import homeassistant.core as ha
|
||||||
|
import homeassistant.components.notify as notify
|
||||||
|
from homeassistant.components.notify import demo
|
||||||
|
|
||||||
|
|
||||||
|
class TestNotifyDemo(unittest.TestCase):
|
||||||
|
""" Test the demo notify. """
|
||||||
|
|
||||||
|
def setUp(self): # pylint: disable=invalid-name
|
||||||
|
self.hass = ha.HomeAssistant()
|
||||||
|
self.assertTrue(notify.setup(self.hass, {
|
||||||
|
'notify': {
|
||||||
|
'platform': 'demo'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
self.events = []
|
||||||
|
|
||||||
|
def record_event(event):
|
||||||
|
self.events.append(event)
|
||||||
|
|
||||||
|
self.hass.bus.listen(demo.EVENT_NOTIFY, record_event)
|
||||||
|
|
||||||
|
def tearDown(self): # pylint: disable=invalid-name
|
||||||
|
""" Stop down stuff we started. """
|
||||||
|
self.hass.stop()
|
||||||
|
|
||||||
|
def test_sending_templated_message(self):
|
||||||
|
self.hass.states.set('sensor.temperature', 10)
|
||||||
|
notify.send_message(self.hass, '{{ states.sensor.temperature.state }}',
|
||||||
|
'{{ states.sensor.temperature.name }}')
|
||||||
|
self.hass.pool.block_till_done()
|
||||||
|
last_event = self.events[-1]
|
||||||
|
self.assertEqual(last_event.data[notify.ATTR_TITLE], 'temperature')
|
||||||
|
self.assertEqual(last_event.data[notify.ATTR_MESSAGE], '10')
|
@ -47,7 +47,7 @@ class TestSensorMQTT(unittest.TestCase):
|
|||||||
'name': 'test',
|
'name': 'test',
|
||||||
'state_topic': 'test-topic',
|
'state_topic': 'test-topic',
|
||||||
'unit_of_measurement': 'fav unit',
|
'unit_of_measurement': 'fav unit',
|
||||||
'state_format': 'json:val'
|
'value_template': '{{ value_json.val }}'
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -56,4 +56,3 @@ class TestSensorMQTT(unittest.TestCase):
|
|||||||
state = self.hass.states.get('sensor.test')
|
state = self.hass.states.get('sensor.test')
|
||||||
|
|
||||||
self.assertEqual('100', state.state)
|
self.assertEqual('100', state.state)
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ class TestSensorMQTT(unittest.TestCase):
|
|||||||
'command_topic': 'command-topic',
|
'command_topic': 'command-topic',
|
||||||
'payload_on': 'beer on',
|
'payload_on': 'beer on',
|
||||||
'payload_off': 'beer off',
|
'payload_off': 'beer off',
|
||||||
'state_format': 'json:val'
|
'value_template': '{{ value_json.val }}'
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
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.
|
Tests Home Assistant HTTP component does what it should do.
|
||||||
"""
|
"""
|
||||||
# pylint: disable=protected-access,too-many-public-methods
|
# pylint: disable=protected-access,too-many-public-methods
|
||||||
import unittest
|
from contextlib import closing
|
||||||
import json
|
import json
|
||||||
from unittest.mock import patch
|
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
@ -326,6 +327,30 @@ class TestAPI(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(1, len(test_value))
|
self.assertEqual(1, len(test_value))
|
||||||
|
|
||||||
|
def test_api_template(self):
|
||||||
|
""" Test template API. """
|
||||||
|
hass.states.set('sensor.temperature', 10)
|
||||||
|
|
||||||
|
req = requests.post(
|
||||||
|
_url(const.URL_API_TEMPLATE),
|
||||||
|
data=json.dumps({"template":
|
||||||
|
'{{ states.sensor.temperature.state }}'}),
|
||||||
|
headers=HA_HEADERS)
|
||||||
|
|
||||||
|
self.assertEqual('10', req.text)
|
||||||
|
|
||||||
|
def test_api_template_error(self):
|
||||||
|
""" Test template API. """
|
||||||
|
hass.states.set('sensor.temperature', 10)
|
||||||
|
|
||||||
|
req = requests.post(
|
||||||
|
_url(const.URL_API_TEMPLATE),
|
||||||
|
data=json.dumps({"template":
|
||||||
|
'{{ states.sensor.temperature.state'}),
|
||||||
|
headers=HA_HEADERS)
|
||||||
|
|
||||||
|
self.assertEqual(422, req.status_code)
|
||||||
|
|
||||||
def test_api_event_forward(self):
|
def test_api_event_forward(self):
|
||||||
""" Test setting up event forwarding. """
|
""" Test setting up event forwarding. """
|
||||||
|
|
||||||
@ -401,3 +426,61 @@ class TestAPI(unittest.TestCase):
|
|||||||
}),
|
}),
|
||||||
headers=HA_HEADERS)
|
headers=HA_HEADERS)
|
||||||
self.assertEqual(200, req.status_code)
|
self.assertEqual(200, req.status_code)
|
||||||
|
|
||||||
|
def test_stream(self):
|
||||||
|
listen_count = self._listen_count()
|
||||||
|
with closing(requests.get(_url(const.URL_API_STREAM),
|
||||||
|
stream=True, headers=HA_HEADERS)) as req:
|
||||||
|
|
||||||
|
data = self._stream_next_event(req)
|
||||||
|
self.assertEqual('ping', data)
|
||||||
|
|
||||||
|
self.assertEqual(listen_count + 1, self._listen_count())
|
||||||
|
|
||||||
|
hass.bus.fire('test_event')
|
||||||
|
hass.pool.block_till_done()
|
||||||
|
|
||||||
|
data = self._stream_next_event(req)
|
||||||
|
|
||||||
|
self.assertEqual('test_event', data['event_type'])
|
||||||
|
|
||||||
|
def test_stream_with_restricted(self):
|
||||||
|
listen_count = self._listen_count()
|
||||||
|
with closing(requests.get(_url(const.URL_API_STREAM),
|
||||||
|
data=json.dumps({
|
||||||
|
'restrict': 'test_event1,test_event3'}),
|
||||||
|
stream=True, headers=HA_HEADERS)) as req:
|
||||||
|
|
||||||
|
data = self._stream_next_event(req)
|
||||||
|
self.assertEqual('ping', data)
|
||||||
|
|
||||||
|
self.assertEqual(listen_count + 2, self._listen_count())
|
||||||
|
|
||||||
|
hass.bus.fire('test_event1')
|
||||||
|
hass.pool.block_till_done()
|
||||||
|
hass.bus.fire('test_event2')
|
||||||
|
hass.pool.block_till_done()
|
||||||
|
hass.bus.fire('test_event3')
|
||||||
|
hass.pool.block_till_done()
|
||||||
|
|
||||||
|
data = self._stream_next_event(req)
|
||||||
|
self.assertEqual('test_event1', data['event_type'])
|
||||||
|
data = self._stream_next_event(req)
|
||||||
|
self.assertEqual('test_event3', data['event_type'])
|
||||||
|
|
||||||
|
def _stream_next_event(self, stream):
|
||||||
|
data = b''
|
||||||
|
last_new_line = False
|
||||||
|
for dat in stream.iter_content(1):
|
||||||
|
if dat == b'\n' and last_new_line:
|
||||||
|
break
|
||||||
|
data += dat
|
||||||
|
last_new_line = dat == b'\n'
|
||||||
|
|
||||||
|
conv = data.decode('utf-8').strip()[6:]
|
||||||
|
|
||||||
|
return conv if conv == 'ping' else json.loads(conv)
|
||||||
|
|
||||||
|
def _listen_count(self):
|
||||||
|
""" Return number of event listeners. """
|
||||||
|
return sum(hass.bus.listeners.values())
|
||||||
|
127
tests/util/test_template.py
Normal file
127
tests/util/test_template.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
"""
|
||||||
|
tests.test_util
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Tests Home Assistant util methods.
|
||||||
|
"""
|
||||||
|
# pylint: disable=too-many-public-methods
|
||||||
|
import unittest
|
||||||
|
import homeassistant.core as ha
|
||||||
|
from homeassistant.exceptions import TemplateError
|
||||||
|
from homeassistant.util import template
|
||||||
|
|
||||||
|
|
||||||
|
class TestUtilTemplate(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self): # pylint: disable=invalid-name
|
||||||
|
self.hass = ha.HomeAssistant()
|
||||||
|
|
||||||
|
def tearDown(self): # pylint: disable=invalid-name
|
||||||
|
""" Stop down stuff we started. """
|
||||||
|
self.hass.stop()
|
||||||
|
|
||||||
|
def test_referring_states_by_entity_id(self):
|
||||||
|
self.hass.states.set('test.object', 'happy')
|
||||||
|
self.assertEqual(
|
||||||
|
'happy',
|
||||||
|
template.render(self.hass, '{{ states.test.object.state }}'))
|
||||||
|
|
||||||
|
def test_iterating_all_states(self):
|
||||||
|
self.hass.states.set('test.object', 'happy')
|
||||||
|
self.hass.states.set('sensor.temperature', 10)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
'10happy',
|
||||||
|
template.render(
|
||||||
|
self.hass,
|
||||||
|
'{% for state in states %}{{ state.state }}{% endfor %}'))
|
||||||
|
|
||||||
|
def test_iterating_domain_states(self):
|
||||||
|
self.hass.states.set('test.object', 'happy')
|
||||||
|
self.hass.states.set('sensor.back_door', 'open')
|
||||||
|
self.hass.states.set('sensor.temperature', 10)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
'open10',
|
||||||
|
template.render(
|
||||||
|
self.hass,
|
||||||
|
'{% for state in states.sensor %}{{ state.state }}{% endfor %}'))
|
||||||
|
|
||||||
|
def test_rounding_value(self):
|
||||||
|
self.hass.states.set('sensor.temperature', 12.78)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
'12.8',
|
||||||
|
template.render(
|
||||||
|
self.hass,
|
||||||
|
'{{ states.sensor.temperature.state | round(1) }}'))
|
||||||
|
|
||||||
|
def test_rounding_value2(self):
|
||||||
|
self.hass.states.set('sensor.temperature', 12.78)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
'128',
|
||||||
|
template.render(
|
||||||
|
self.hass,
|
||||||
|
'{{ states.sensor.temperature.state | multiply(10) | round }}'))
|
||||||
|
|
||||||
|
def test_passing_vars_as_keywords(self):
|
||||||
|
self.assertEqual(
|
||||||
|
'127', template.render(self.hass, '{{ hello }}', hello=127))
|
||||||
|
|
||||||
|
def test_passing_vars_as_vars(self):
|
||||||
|
self.assertEqual(
|
||||||
|
'127', template.render(self.hass, '{{ hello }}', {'hello': 127}))
|
||||||
|
|
||||||
|
def test_render_with_possible_json_value_with_valid_json(self):
|
||||||
|
self.assertEqual(
|
||||||
|
'world',
|
||||||
|
template.render_with_possible_json_value(
|
||||||
|
self.hass, '{{ value_json.hello }}', '{"hello": "world"}'))
|
||||||
|
|
||||||
|
def test_render_with_possible_json_value_with_invalid_json(self):
|
||||||
|
self.assertEqual(
|
||||||
|
'',
|
||||||
|
template.render_with_possible_json_value(
|
||||||
|
self.hass, '{{ value_json }}', '{ I AM NOT JSON }'))
|
||||||
|
|
||||||
|
def test_render_with_possible_json_value_with_template_error(self):
|
||||||
|
self.assertEqual(
|
||||||
|
'hello',
|
||||||
|
template.render_with_possible_json_value(
|
||||||
|
self.hass, '{{ value_json', 'hello'))
|
||||||
|
|
||||||
|
def test_render_with_possible_json_value_with_template_error_error_value(self):
|
||||||
|
self.assertEqual(
|
||||||
|
'-',
|
||||||
|
template.render_with_possible_json_value(
|
||||||
|
self.hass, '{{ value_json', 'hello', '-'))
|
||||||
|
|
||||||
|
def test_raise_exception_on_error(self):
|
||||||
|
with self.assertRaises(TemplateError):
|
||||||
|
template.render(self.hass, '{{ invalid_syntax')
|
||||||
|
|
||||||
|
def test_if_state_exists(self):
|
||||||
|
self.hass.states.set('test.object', 'available')
|
||||||
|
self.assertEqual(
|
||||||
|
'exists',
|
||||||
|
template.render(
|
||||||
|
self.hass,
|
||||||
|
'{% if states.test.object %}exists{% else %}not exists{% endif %}'))
|
||||||
|
|
||||||
|
def test_is_state(self):
|
||||||
|
self.hass.states.set('test.object', 'available')
|
||||||
|
self.assertEqual(
|
||||||
|
'yes',
|
||||||
|
template.render(
|
||||||
|
self.hass,
|
||||||
|
'{% if is_state("test.object", "available") %}yes{% else %}no{% endif %}'))
|
||||||
|
|
||||||
|
def test_states_function(self):
|
||||||
|
self.hass.states.set('test.object', 'available')
|
||||||
|
self.assertEqual(
|
||||||
|
'available',
|
||||||
|
template.render(self.hass, '{{ states("test.object") }}'))
|
||||||
|
self.assertEqual(
|
||||||
|
'unknown',
|
||||||
|
template.render(self.hass, '{{ states("test.object2") }}'))
|
Loading…
x
Reference in New Issue
Block a user