mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
commit
d7b0929a32
@ -75,6 +75,9 @@ omit =
|
||||
homeassistant/components/zwave.py
|
||||
homeassistant/components/*/zwave.py
|
||||
|
||||
homeassistant/components/enocean.py
|
||||
homeassistant/components/*/enocean.py
|
||||
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
homeassistant/components/binary_sensor/arest.py
|
||||
@ -111,6 +114,8 @@ omit =
|
||||
homeassistant/components/light/hyperion.py
|
||||
homeassistant/components/light/lifx.py
|
||||
homeassistant/components/light/limitlessled.py
|
||||
homeassistant/components/light/osramlightify.py
|
||||
homeassistant/components/lirc.py
|
||||
homeassistant/components/media_player/cast.py
|
||||
homeassistant/components/media_player/denon.py
|
||||
homeassistant/components/media_player/firetv.py
|
||||
@ -156,6 +161,7 @@ omit =
|
||||
homeassistant/components/sensor/cpuspeed.py
|
||||
homeassistant/components/sensor/deutsche_bahn.py
|
||||
homeassistant/components/sensor/dht.py
|
||||
homeassistant/components/sensor/dte_energy_bridge.py
|
||||
homeassistant/components/sensor/efergy.py
|
||||
homeassistant/components/sensor/eliqonline.py
|
||||
homeassistant/components/sensor/fitbit.py
|
||||
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,7 +1,7 @@
|
||||
**Description:**
|
||||
|
||||
|
||||
**Related issue (if applicable):** #
|
||||
**Related issue (if applicable):** fixes #
|
||||
|
||||
**Pull request in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) with documentation (if applicable):** home-assistant/home-assistant.io#
|
||||
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -85,3 +85,8 @@ venv
|
||||
*.swo
|
||||
|
||||
ctags.tmp
|
||||
|
||||
# vagrant stuff
|
||||
virtualization/vagrant/setup_done
|
||||
virtualization/vagrant/.vagrant
|
||||
virtualization/vagrant/config
|
||||
|
11
Dockerfile
11
Dockerfile
@ -19,15 +19,8 @@ RUN script/build_python_openzwave && \
|
||||
ln -sf /usr/src/app/build/python-openzwave/openzwave/config /usr/local/share/python-openzwave/config
|
||||
|
||||
COPY requirements_all.txt requirements_all.txt
|
||||
RUN pip3 install --no-cache-dir -r requirements_all.txt
|
||||
|
||||
RUN wget http://www.openssl.org/source/openssl-1.0.2h.tar.gz && \
|
||||
tar -xvzf openssl-1.0.2h.tar.gz && \
|
||||
cd openssl-1.0.2h && \
|
||||
./config --prefix=/usr/ && \
|
||||
make && \
|
||||
make install && \
|
||||
rm -rf openssl-1.0.2h*
|
||||
# certifi breaks Debian based installs
|
||||
RUN pip3 install --no-cache-dir -r requirements_all.txt && pip3 uninstall -y certifi
|
||||
|
||||
# Copy source
|
||||
COPY . .
|
||||
|
@ -304,7 +304,6 @@ def setup_and_run_hass(config_dir, args):
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser)
|
||||
|
||||
print('Starting Home-Assistant')
|
||||
hass.start()
|
||||
exit_code = int(hass.block_till_stopped())
|
||||
|
||||
|
@ -7,14 +7,14 @@ https://home-assistant.io/components/alexa/
|
||||
import enum
|
||||
import logging
|
||||
|
||||
from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY
|
||||
from homeassistant.const import HTTP_BAD_REQUEST
|
||||
from homeassistant.helpers import template, script
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
DOMAIN = 'alexa'
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_CONFIG = {}
|
||||
|
||||
API_ENDPOINT = '/api/alexa'
|
||||
|
||||
@ -26,80 +26,88 @@ CONF_ACTION = 'action'
|
||||
|
||||
def setup(hass, config):
|
||||
"""Activate Alexa component."""
|
||||
intents = config[DOMAIN].get(CONF_INTENTS, {})
|
||||
|
||||
for name, intent in intents.items():
|
||||
if CONF_ACTION in intent:
|
||||
intent[CONF_ACTION] = script.Script(hass, intent[CONF_ACTION],
|
||||
"Alexa intent {}".format(name))
|
||||
|
||||
_CONFIG.update(intents)
|
||||
|
||||
hass.http.register_path('POST', API_ENDPOINT, _handle_alexa, True)
|
||||
hass.wsgi.register_view(AlexaView(hass,
|
||||
config[DOMAIN].get(CONF_INTENTS, {})))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_alexa(handler, path_match, data):
|
||||
"""Handle Alexa."""
|
||||
_LOGGER.debug('Received Alexa request: %s', data)
|
||||
class AlexaView(HomeAssistantView):
|
||||
"""Handle Alexa requests."""
|
||||
|
||||
req = data.get('request')
|
||||
url = API_ENDPOINT
|
||||
name = 'api:alexa'
|
||||
|
||||
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
|
||||
def __init__(self, hass, intents):
|
||||
"""Initialize Alexa view."""
|
||||
super().__init__(hass)
|
||||
|
||||
req_type = req['type']
|
||||
for name, intent in intents.items():
|
||||
if CONF_ACTION in intent:
|
||||
intent[CONF_ACTION] = script.Script(
|
||||
hass, intent[CONF_ACTION], "Alexa intent {}".format(name))
|
||||
|
||||
if req_type == 'SessionEndedRequest':
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.end_headers()
|
||||
return
|
||||
self.intents = intents
|
||||
|
||||
intent = req.get('intent')
|
||||
response = AlexaResponse(handler.server.hass, intent)
|
||||
def post(self, request):
|
||||
"""Handle Alexa."""
|
||||
data = request.json
|
||||
|
||||
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
|
||||
_LOGGER.debug('Received Alexa request: %s', data)
|
||||
|
||||
if req_type != 'IntentRequest':
|
||||
_LOGGER.warning('Received unsupported request: %s', req_type)
|
||||
return
|
||||
req = data.get('request')
|
||||
|
||||
intent_name = intent['name']
|
||||
config = _CONFIG.get(intent_name)
|
||||
if req is None:
|
||||
_LOGGER.error('Received invalid data from Alexa: %s', data)
|
||||
return self.json_message('Expected request value not received',
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
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
|
||||
req_type = req['type']
|
||||
|
||||
speech = config.get(CONF_SPEECH)
|
||||
card = config.get(CONF_CARD)
|
||||
action = config.get(CONF_ACTION)
|
||||
if req_type == 'SessionEndedRequest':
|
||||
return None
|
||||
|
||||
# pylint: disable=unsubscriptable-object
|
||||
if speech is not None:
|
||||
response.add_speech(SpeechType[speech['type']], speech['text'])
|
||||
intent = req.get('intent')
|
||||
response = AlexaResponse(self.hass, intent)
|
||||
|
||||
if card is not None:
|
||||
response.add_card(CardType[card['type']], card['title'],
|
||||
card['content'])
|
||||
if req_type == 'LaunchRequest':
|
||||
response.add_speech(
|
||||
SpeechType.plaintext,
|
||||
"Hello, and welcome to the future. How may I help?")
|
||||
return self.json(response)
|
||||
|
||||
if action is not None:
|
||||
action.run(response.variables)
|
||||
if req_type != 'IntentRequest':
|
||||
_LOGGER.warning('Received unsupported request: %s', req_type)
|
||||
return self.json_message(
|
||||
'Received unsupported request: {}'.format(req_type),
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
handler.write_json(response.as_dict())
|
||||
intent_name = intent['name']
|
||||
config = self.intents.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.")
|
||||
return self.json(response)
|
||||
|
||||
speech = config.get(CONF_SPEECH)
|
||||
card = config.get(CONF_CARD)
|
||||
action = config.get(CONF_ACTION)
|
||||
|
||||
# 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'])
|
||||
|
||||
if action is not None:
|
||||
action.run(response.variables)
|
||||
|
||||
return self.json(response)
|
||||
|
||||
|
||||
class SpeechType(enum.Enum):
|
||||
|
@ -6,23 +6,23 @@ https://home-assistant.io/developers/api/
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import threading
|
||||
from time import time
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.remote as rem
|
||||
from homeassistant.bootstrap import ERROR_LOG_FILENAME
|
||||
from homeassistant.const import (
|
||||
CONTENT_TYPE_TEXT_PLAIN, EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED,
|
||||
HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_HEADER_CONTENT_TYPE, HTTP_NOT_FOUND,
|
||||
HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, MATCH_ALL, URL_API, URL_API_COMPONENTS,
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED,
|
||||
HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND,
|
||||
HTTP_UNPROCESSABLE_ENTITY, MATCH_ALL, URL_API, URL_API_COMPONENTS,
|
||||
URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG,
|
||||
URL_API_EVENT_FORWARD, URL_API_EVENTS, URL_API_LOG_OUT, URL_API_SERVICES,
|
||||
URL_API_EVENT_FORWARD, URL_API_EVENTS, URL_API_SERVICES,
|
||||
URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE,
|
||||
__version__)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.state import TrackStates
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
DOMAIN = 'api'
|
||||
DEPENDENCIES = ['http']
|
||||
@ -35,372 +35,365 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def setup(hass, config):
|
||||
"""Register the API with the HTTP interface."""
|
||||
# /api - for validation purposes
|
||||
hass.http.register_path('GET', URL_API, _handle_get_api)
|
||||
|
||||
# /api/config
|
||||
hass.http.register_path('GET', URL_API_CONFIG, _handle_get_api_config)
|
||||
|
||||
# /api/discovery_info
|
||||
hass.http.register_path('GET', URL_API_DISCOVERY_INFO,
|
||||
_handle_get_api_discovery_info,
|
||||
require_auth=False)
|
||||
|
||||
# /api/stream
|
||||
hass.http.register_path('GET', URL_API_STREAM, _handle_get_api_stream)
|
||||
|
||||
# /api/states
|
||||
hass.http.register_path('GET', URL_API_STATES, _handle_get_api_states)
|
||||
hass.http.register_path(
|
||||
'GET', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
_handle_get_api_states_entity)
|
||||
hass.http.register_path(
|
||||
'POST', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
_handle_post_state_entity)
|
||||
hass.http.register_path(
|
||||
'PUT', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
_handle_post_state_entity)
|
||||
hass.http.register_path(
|
||||
'DELETE', re.compile(r'/api/states/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
_handle_delete_state_entity)
|
||||
|
||||
# /api/events
|
||||
hass.http.register_path('GET', URL_API_EVENTS, _handle_get_api_events)
|
||||
hass.http.register_path(
|
||||
'POST', re.compile(r'/api/events/(?P<event_type>[a-zA-Z\._0-9]+)'),
|
||||
_handle_api_post_events_event)
|
||||
|
||||
# /api/services
|
||||
hass.http.register_path('GET', URL_API_SERVICES, _handle_get_api_services)
|
||||
hass.http.register_path(
|
||||
'POST',
|
||||
re.compile((r'/api/services/'
|
||||
r'(?P<domain>[a-zA-Z\._0-9]+)/'
|
||||
r'(?P<service>[a-zA-Z\._0-9]+)')),
|
||||
_handle_post_api_services_domain_service)
|
||||
|
||||
# /api/event_forwarding
|
||||
hass.http.register_path(
|
||||
'POST', URL_API_EVENT_FORWARD, _handle_post_api_event_forward)
|
||||
hass.http.register_path(
|
||||
'DELETE', URL_API_EVENT_FORWARD, _handle_delete_api_event_forward)
|
||||
|
||||
# /api/components
|
||||
hass.http.register_path(
|
||||
'GET', URL_API_COMPONENTS, _handle_get_api_components)
|
||||
|
||||
# /api/error_log
|
||||
hass.http.register_path('GET', URL_API_ERROR_LOG,
|
||||
_handle_get_api_error_log)
|
||||
|
||||
hass.http.register_path('POST', URL_API_LOG_OUT, _handle_post_api_log_out)
|
||||
|
||||
# /api/template
|
||||
hass.http.register_path('POST', URL_API_TEMPLATE,
|
||||
_handle_post_api_template)
|
||||
hass.wsgi.register_view(APIStatusView)
|
||||
hass.wsgi.register_view(APIEventStream)
|
||||
hass.wsgi.register_view(APIConfigView)
|
||||
hass.wsgi.register_view(APIDiscoveryView)
|
||||
hass.wsgi.register_view(APIStatesView)
|
||||
hass.wsgi.register_view(APIEntityStateView)
|
||||
hass.wsgi.register_view(APIEventListenersView)
|
||||
hass.wsgi.register_view(APIEventView)
|
||||
hass.wsgi.register_view(APIServicesView)
|
||||
hass.wsgi.register_view(APIDomainServicesView)
|
||||
hass.wsgi.register_view(APIEventForwardingView)
|
||||
hass.wsgi.register_view(APIComponentsView)
|
||||
hass.wsgi.register_view(APIErrorLogView)
|
||||
hass.wsgi.register_view(APITemplateView)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_get_api(handler, path_match, data):
|
||||
"""Render the debug interface."""
|
||||
handler.write_json_message("API running.")
|
||||
|
||||
|
||||
def _handle_get_api_stream(handler, path_match, data):
|
||||
"""Provide a streaming interface for the event bus."""
|
||||
gracefully_closed = False
|
||||
hass = handler.server.hass
|
||||
wfile = handler.wfile
|
||||
write_lock = threading.Lock()
|
||||
block = threading.Event()
|
||||
session_id = None
|
||||
class APIStatusView(HomeAssistantView):
|
||||
"""View to handle Status requests."""
|
||||
|
||||
restrict = data.get('restrict')
|
||||
if restrict:
|
||||
restrict = restrict.split(',')
|
||||
url = URL_API
|
||||
name = "api:status"
|
||||
|
||||
def write_message(payload):
|
||||
"""Write a message to the output."""
|
||||
with write_lock:
|
||||
msg = "data: {}\n\n".format(payload)
|
||||
def get(self, request):
|
||||
"""Retrieve if API is running."""
|
||||
return self.json_message('API running.')
|
||||
|
||||
try:
|
||||
wfile.write(msg.encode("UTF-8"))
|
||||
wfile.flush()
|
||||
except (IOError, ValueError):
|
||||
# IOError: socket errors
|
||||
# ValueError: raised when 'I/O operation on closed file'
|
||||
block.set()
|
||||
|
||||
def forward_events(event):
|
||||
"""Forward events to the open request."""
|
||||
nonlocal gracefully_closed
|
||||
class APIEventStream(HomeAssistantView):
|
||||
"""View to handle EventStream requests."""
|
||||
|
||||
if block.is_set() or event.event_type == EVENT_TIME_CHANGED:
|
||||
return
|
||||
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
|
||||
gracefully_closed = True
|
||||
block.set()
|
||||
return
|
||||
url = URL_API_STREAM
|
||||
name = "api:stream"
|
||||
|
||||
handler.server.sessions.extend_validation(session_id)
|
||||
write_message(json.dumps(event, cls=rem.JSONEncoder))
|
||||
def get(self, request):
|
||||
"""Provide a streaming interface for the event bus."""
|
||||
from eventlet.queue import LightQueue, Empty
|
||||
import eventlet
|
||||
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.send_header('Content-type', 'text/event-stream')
|
||||
session_id = handler.set_session_cookie_header()
|
||||
handler.end_headers()
|
||||
cur_hub = eventlet.hubs.get_hub()
|
||||
request.environ['eventlet.minimum_write_chunk_size'] = 0
|
||||
to_write = LightQueue()
|
||||
stop_obj = object()
|
||||
|
||||
if restrict:
|
||||
for event in restrict:
|
||||
hass.bus.listen(event, forward_events)
|
||||
else:
|
||||
hass.bus.listen(MATCH_ALL, forward_events)
|
||||
restrict = request.args.get('restrict')
|
||||
if restrict:
|
||||
restrict = restrict.split(',')
|
||||
|
||||
while True:
|
||||
write_message(STREAM_PING_PAYLOAD)
|
||||
def thread_forward_events(event):
|
||||
"""Forward events to the open request."""
|
||||
if event.event_type == EVENT_TIME_CHANGED:
|
||||
return
|
||||
|
||||
block.wait(STREAM_PING_INTERVAL)
|
||||
if restrict and event.event_type not in restrict:
|
||||
return
|
||||
|
||||
if block.is_set():
|
||||
break
|
||||
_LOGGER.debug('STREAM %s FORWARDING %s', id(stop_obj), event)
|
||||
|
||||
if not gracefully_closed:
|
||||
_LOGGER.info("Found broken event stream to %s, cleaning up",
|
||||
handler.client_address[0])
|
||||
if event.event_type == EVENT_HOMEASSISTANT_STOP:
|
||||
data = stop_obj
|
||||
else:
|
||||
data = json.dumps(event, cls=rem.JSONEncoder)
|
||||
|
||||
if restrict:
|
||||
for event in restrict:
|
||||
hass.bus.remove_listener(event, forward_events)
|
||||
else:
|
||||
hass.bus.remove_listener(MATCH_ALL, forward_events)
|
||||
cur_hub.schedule_call_global(0, lambda: to_write.put(data))
|
||||
|
||||
def stream():
|
||||
"""Stream events to response."""
|
||||
self.hass.bus.listen(MATCH_ALL, thread_forward_events)
|
||||
|
||||
def _handle_get_api_config(handler, path_match, data):
|
||||
"""Return the Home Assistant configuration."""
|
||||
handler.write_json(handler.server.hass.config.as_dict())
|
||||
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
|
||||
|
||||
last_msg = time()
|
||||
# Fire off one message right away to have browsers fire open event
|
||||
to_write.put(STREAM_PING_PAYLOAD)
|
||||
|
||||
def _handle_get_api_discovery_info(handler, path_match, data):
|
||||
needs_auth = (handler.server.hass.config.api.api_password is not None)
|
||||
params = {
|
||||
'base_url': handler.server.hass.config.api.base_url,
|
||||
'location_name': handler.server.hass.config.location_name,
|
||||
'requires_api_password': needs_auth,
|
||||
'version': __version__
|
||||
}
|
||||
handler.write_json(params)
|
||||
while True:
|
||||
try:
|
||||
# Somehow our queue.get sometimes takes too long to
|
||||
# be notified of arrival of data. Probably
|
||||
# because of our spawning on hub in other thread
|
||||
# hack. Because current goal is to get this out,
|
||||
# We just timeout every second because it will
|
||||
# return right away if qsize() > 0.
|
||||
# So yes, we're basically polling :(
|
||||
payload = to_write.get(timeout=1)
|
||||
|
||||
if payload is stop_obj:
|
||||
break
|
||||
|
||||
def _handle_get_api_states(handler, path_match, data):
|
||||
"""Return a dict containing all entity ids and their state."""
|
||||
handler.write_json(handler.server.hass.states.all())
|
||||
msg = "data: {}\n\n".format(payload)
|
||||
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
|
||||
msg.strip())
|
||||
yield msg.encode("UTF-8")
|
||||
last_msg = time()
|
||||
except Empty:
|
||||
if time() - last_msg > 50:
|
||||
to_write.put(STREAM_PING_PAYLOAD)
|
||||
except GeneratorExit:
|
||||
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
|
||||
break
|
||||
|
||||
self.hass.bus.remove_listener(MATCH_ALL, thread_forward_events)
|
||||
|
||||
def _handle_get_api_states_entity(handler, path_match, data):
|
||||
"""Return the state of a specific entity."""
|
||||
entity_id = path_match.group('entity_id')
|
||||
return self.Response(stream(), mimetype='text/event-stream')
|
||||
|
||||
state = handler.server.hass.states.get(entity_id)
|
||||
|
||||
if state:
|
||||
handler.write_json(state)
|
||||
else:
|
||||
handler.write_json_message("State does not exist.", HTTP_NOT_FOUND)
|
||||
class APIConfigView(HomeAssistantView):
|
||||
"""View to handle Config requests."""
|
||||
|
||||
url = URL_API_CONFIG
|
||||
name = "api:config"
|
||||
|
||||
def _handle_post_state_entity(handler, path_match, data):
|
||||
"""Handle updating the state of an entity.
|
||||
def get(self, request):
|
||||
"""Get current configuration."""
|
||||
return self.json(self.hass.config.as_dict())
|
||||
|
||||
This handles the following paths:
|
||||
/api/states/<entity_id>
|
||||
"""
|
||||
entity_id = path_match.group('entity_id')
|
||||
|
||||
try:
|
||||
new_state = data['state']
|
||||
except KeyError:
|
||||
handler.write_json_message("state not specified", HTTP_BAD_REQUEST)
|
||||
return
|
||||
class APIDiscoveryView(HomeAssistantView):
|
||||
"""View to provide discovery info."""
|
||||
|
||||
attributes = data['attributes'] if 'attributes' in data else None
|
||||
requires_auth = False
|
||||
url = URL_API_DISCOVERY_INFO
|
||||
name = "api:discovery"
|
||||
|
||||
is_new_state = handler.server.hass.states.get(entity_id) is None
|
||||
def get(self, request):
|
||||
"""Get discovery info."""
|
||||
needs_auth = self.hass.config.api.api_password is not None
|
||||
return self.json({
|
||||
'base_url': self.hass.config.api.base_url,
|
||||
'location_name': self.hass.config.location_name,
|
||||
'requires_api_password': needs_auth,
|
||||
'version': __version__
|
||||
})
|
||||
|
||||
# Write state
|
||||
handler.server.hass.states.set(entity_id, new_state, attributes)
|
||||
|
||||
state = handler.server.hass.states.get(entity_id)
|
||||
class APIStatesView(HomeAssistantView):
|
||||
"""View to handle States requests."""
|
||||
|
||||
status_code = HTTP_CREATED if is_new_state else HTTP_OK
|
||||
url = URL_API_STATES
|
||||
name = "api:states"
|
||||
|
||||
handler.write_json(
|
||||
state.as_dict(),
|
||||
status_code=status_code,
|
||||
location=URL_API_STATES_ENTITY.format(entity_id))
|
||||
def get(self, request):
|
||||
"""Get current states."""
|
||||
return self.json(self.hass.states.all())
|
||||
|
||||
|
||||
def _handle_delete_state_entity(handler, path_match, data):
|
||||
"""Handle request to delete an entity from state machine.
|
||||
class APIEntityStateView(HomeAssistantView):
|
||||
"""View to handle EntityState requests."""
|
||||
|
||||
This handles the following paths:
|
||||
/api/states/<entity_id>
|
||||
"""
|
||||
entity_id = path_match.group('entity_id')
|
||||
url = "/api/states/<entity(exist=False):entity_id>"
|
||||
name = "api:entity-state"
|
||||
|
||||
if handler.server.hass.states.remove(entity_id):
|
||||
handler.write_json_message(
|
||||
"Entity not found", HTTP_NOT_FOUND)
|
||||
else:
|
||||
handler.write_json_message(
|
||||
"Entity removed", HTTP_OK)
|
||||
def get(self, request, entity_id):
|
||||
"""Retrieve state of entity."""
|
||||
state = self.hass.states.get(entity_id)
|
||||
if state:
|
||||
return self.json(state)
|
||||
else:
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
|
||||
def post(self, request, entity_id):
|
||||
"""Update state of entity."""
|
||||
try:
|
||||
new_state = request.json['state']
|
||||
except KeyError:
|
||||
return self.json_message('No state specified', HTTP_BAD_REQUEST)
|
||||
|
||||
def _handle_get_api_events(handler, path_match, data):
|
||||
"""Handle getting overview of event listeners."""
|
||||
handler.write_json(events_json(handler.server.hass))
|
||||
attributes = request.json.get('attributes')
|
||||
|
||||
is_new_state = self.hass.states.get(entity_id) is None
|
||||
|
||||
def _handle_api_post_events_event(handler, path_match, event_data):
|
||||
"""Handle firing of an event.
|
||||
# Write state
|
||||
self.hass.states.set(entity_id, new_state, attributes)
|
||||
|
||||
This handles the following paths: /api/events/<event_type>
|
||||
# Read the state back for our response
|
||||
resp = self.json(self.hass.states.get(entity_id))
|
||||
|
||||
Events from /api are threated as remote events.
|
||||
"""
|
||||
event_type = path_match.group('event_type')
|
||||
if is_new_state:
|
||||
resp.status_code = HTTP_CREATED
|
||||
|
||||
if event_data is not None and not isinstance(event_data, dict):
|
||||
handler.write_json_message(
|
||||
"event_data should be an object", HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id))
|
||||
|
||||
event_origin = ha.EventOrigin.remote
|
||||
return resp
|
||||
|
||||
# Special case handling for event STATE_CHANGED
|
||||
# We will try to convert state dicts back to State objects
|
||||
if event_type == ha.EVENT_STATE_CHANGED and event_data:
|
||||
for key in ('old_state', 'new_state'):
|
||||
state = ha.State.from_dict(event_data.get(key))
|
||||
def delete(self, request, entity_id):
|
||||
"""Remove entity."""
|
||||
if self.hass.states.remove(entity_id):
|
||||
return self.json_message('Entity removed')
|
||||
else:
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
|
||||
if state:
|
||||
event_data[key] = state
|
||||
|
||||
handler.server.hass.bus.fire(event_type, event_data, event_origin)
|
||||
class APIEventListenersView(HomeAssistantView):
|
||||
"""View to handle EventListeners requests."""
|
||||
|
||||
handler.write_json_message("Event {} fired.".format(event_type))
|
||||
url = URL_API_EVENTS
|
||||
name = "api:event-listeners"
|
||||
|
||||
def get(self, request):
|
||||
"""Get event listeners."""
|
||||
return self.json(events_json(self.hass))
|
||||
|
||||
def _handle_get_api_services(handler, path_match, data):
|
||||
"""Handle getting overview of services."""
|
||||
handler.write_json(services_json(handler.server.hass))
|
||||
|
||||
class APIEventView(HomeAssistantView):
|
||||
"""View to handle Event requests."""
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def _handle_post_api_services_domain_service(handler, path_match, data):
|
||||
"""Handle calling a service.
|
||||
url = '/api/events/<event_type>'
|
||||
name = "api:event"
|
||||
|
||||
This handles the following paths: /api/services/<domain>/<service>
|
||||
"""
|
||||
domain = path_match.group('domain')
|
||||
service = path_match.group('service')
|
||||
def post(self, request, event_type):
|
||||
"""Fire events."""
|
||||
event_data = request.json
|
||||
|
||||
with TrackStates(handler.server.hass) as changed_states:
|
||||
handler.server.hass.services.call(domain, service, data, True)
|
||||
if event_data is not None and not isinstance(event_data, dict):
|
||||
return self.json_message('Event data should be a JSON object',
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
handler.write_json(changed_states)
|
||||
# Special case handling for event STATE_CHANGED
|
||||
# We will try to convert state dicts back to State objects
|
||||
if event_type == ha.EVENT_STATE_CHANGED and event_data:
|
||||
for key in ('old_state', 'new_state'):
|
||||
state = ha.State.from_dict(event_data.get(key))
|
||||
|
||||
if state:
|
||||
event_data[key] = state
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def _handle_post_api_event_forward(handler, path_match, data):
|
||||
"""Handle adding an event forwarding target."""
|
||||
try:
|
||||
host = data['host']
|
||||
api_password = data['api_password']
|
||||
except KeyError:
|
||||
handler.write_json_message(
|
||||
"No host or api_password received.", HTTP_BAD_REQUEST)
|
||||
return
|
||||
self.hass.bus.fire(event_type, event_data, ha.EventOrigin.remote)
|
||||
|
||||
try:
|
||||
port = int(data['port']) if 'port' in data else None
|
||||
except ValueError:
|
||||
handler.write_json_message(
|
||||
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
return self.json_message("Event {} fired.".format(event_type))
|
||||
|
||||
api = rem.API(host, api_password, port)
|
||||
|
||||
if not api.validate_api():
|
||||
handler.write_json_message(
|
||||
"Unable to validate API", HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
class APIServicesView(HomeAssistantView):
|
||||
"""View to handle Services requests."""
|
||||
|
||||
if handler.server.event_forwarder is None:
|
||||
handler.server.event_forwarder = \
|
||||
rem.EventForwarder(handler.server.hass)
|
||||
url = URL_API_SERVICES
|
||||
name = "api:services"
|
||||
|
||||
handler.server.event_forwarder.connect(api)
|
||||
def get(self, request):
|
||||
"""Get registered services."""
|
||||
return self.json(services_json(self.hass))
|
||||
|
||||
handler.write_json_message("Event forwarding setup.")
|
||||
|
||||
class APIDomainServicesView(HomeAssistantView):
|
||||
"""View to handle DomainServices requests."""
|
||||
|
||||
def _handle_delete_api_event_forward(handler, path_match, data):
|
||||
"""Handle deleting an event forwarding target."""
|
||||
try:
|
||||
host = data['host']
|
||||
except KeyError:
|
||||
handler.write_json_message("No host received.", HTTP_BAD_REQUEST)
|
||||
return
|
||||
url = "/api/services/<domain>/<service>"
|
||||
name = "api:domain-services"
|
||||
|
||||
try:
|
||||
port = int(data['port']) if 'port' in data else None
|
||||
except ValueError:
|
||||
handler.write_json_message(
|
||||
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
def post(self, request, domain, service):
|
||||
"""Call a service.
|
||||
|
||||
if handler.server.event_forwarder is not None:
|
||||
api = rem.API(host, None, port)
|
||||
Returns a list of changed states.
|
||||
"""
|
||||
with TrackStates(self.hass) as changed_states:
|
||||
self.hass.services.call(domain, service, request.json, True)
|
||||
|
||||
handler.server.event_forwarder.disconnect(api)
|
||||
return self.json(changed_states)
|
||||
|
||||
handler.write_json_message("Event forwarding cancelled.")
|
||||
|
||||
class APIEventForwardingView(HomeAssistantView):
|
||||
"""View to handle EventForwarding requests."""
|
||||
|
||||
def _handle_get_api_components(handler, path_match, data):
|
||||
"""Return all the loaded components."""
|
||||
handler.write_json(handler.server.hass.config.components)
|
||||
url = URL_API_EVENT_FORWARD
|
||||
name = "api:event-forward"
|
||||
event_forwarder = None
|
||||
|
||||
def post(self, request):
|
||||
"""Setup an event forwarder."""
|
||||
data = request.json
|
||||
if data is None:
|
||||
return self.json_message("No data received.", HTTP_BAD_REQUEST)
|
||||
try:
|
||||
host = data['host']
|
||||
api_password = data['api_password']
|
||||
except KeyError:
|
||||
return self.json_message("No host or api_password received.",
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
def _handle_get_api_error_log(handler, path_match, data):
|
||||
"""Return the logged errors for this session."""
|
||||
handler.write_file(handler.server.hass.config.path(ERROR_LOG_FILENAME),
|
||||
False)
|
||||
try:
|
||||
port = int(data['port']) if 'port' in data else None
|
||||
except ValueError:
|
||||
return self.json_message("Invalid value received for port.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
api = rem.API(host, api_password, port)
|
||||
|
||||
def _handle_post_api_log_out(handler, path_match, data):
|
||||
"""Log user out."""
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.destroy_session()
|
||||
handler.end_headers()
|
||||
if not api.validate_api():
|
||||
return self.json_message("Unable to validate API.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
if self.event_forwarder is None:
|
||||
self.event_forwarder = rem.EventForwarder(self.hass)
|
||||
|
||||
def _handle_post_api_template(handler, path_match, data):
|
||||
"""Log user out."""
|
||||
template_string = data.get('template', '')
|
||||
self.event_forwarder.connect(api)
|
||||
|
||||
try:
|
||||
rendered = template.render(handler.server.hass, template_string)
|
||||
return self.json_message("Event forwarding setup.")
|
||||
|
||||
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 delete(self, request):
|
||||
"""Remove event forwarer."""
|
||||
data = request.json
|
||||
if data is None:
|
||||
return self.json_message("No data received.", HTTP_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
host = data['host']
|
||||
except KeyError:
|
||||
return self.json_message("No host received.", HTTP_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
port = int(data['port']) if 'port' in data else None
|
||||
except ValueError:
|
||||
return self.json_message("Invalid value received for port.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
if self.event_forwarder is not None:
|
||||
api = rem.API(host, None, port)
|
||||
|
||||
self.event_forwarder.disconnect(api)
|
||||
|
||||
return self.json_message("Event forwarding cancelled.")
|
||||
|
||||
|
||||
class APIComponentsView(HomeAssistantView):
|
||||
"""View to handle Components requests."""
|
||||
|
||||
url = URL_API_COMPONENTS
|
||||
name = "api:components"
|
||||
|
||||
def get(self, request):
|
||||
"""Get current loaded components."""
|
||||
return self.json(self.hass.config.components)
|
||||
|
||||
|
||||
class APIErrorLogView(HomeAssistantView):
|
||||
"""View to handle ErrorLog requests."""
|
||||
|
||||
url = URL_API_ERROR_LOG
|
||||
name = "api:error-log"
|
||||
|
||||
def get(self, request):
|
||||
"""Serve error log."""
|
||||
return self.file(request, self.hass.config.path(ERROR_LOG_FILENAME))
|
||||
|
||||
|
||||
class APITemplateView(HomeAssistantView):
|
||||
"""View to handle requests."""
|
||||
|
||||
url = URL_API_TEMPLATE
|
||||
name = "api:template"
|
||||
|
||||
def post(self, request):
|
||||
"""Render a template."""
|
||||
try:
|
||||
return template.render(self.hass, request.json['template'],
|
||||
request.json.get('variables'))
|
||||
except TemplateError as ex:
|
||||
return self.json_message('Error rendering template: {}'.format(ex),
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
|
||||
def services_json(hass):
|
||||
|
63
homeassistant/components/binary_sensor/enocean.py
Normal file
63
homeassistant/components/binary_sensor/enocean.py
Normal file
@ -0,0 +1,63 @@
|
||||
"""
|
||||
Support for EnOcean binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.enocean/
|
||||
"""
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components import enocean
|
||||
from homeassistant.const import CONF_NAME
|
||||
|
||||
DEPENDENCIES = ["enocean"]
|
||||
|
||||
CONF_ID = "id"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Binary Sensor platform fo EnOcean."""
|
||||
dev_id = config.get(CONF_ID, None)
|
||||
devname = config.get(CONF_NAME, "EnOcean binary sensor")
|
||||
add_devices([EnOceanBinarySensor(dev_id, devname)])
|
||||
|
||||
|
||||
class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
|
||||
"""Representation of EnOcean binary sensors such as wall switches."""
|
||||
|
||||
def __init__(self, dev_id, devname):
|
||||
"""Initialize the EnOcean binary sensor."""
|
||||
enocean.EnOceanDevice.__init__(self)
|
||||
self.stype = "listener"
|
||||
self.dev_id = dev_id
|
||||
self.which = -1
|
||||
self.onoff = -1
|
||||
self.devname = devname
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The default name for the binary sensor."""
|
||||
return self.devname
|
||||
|
||||
def value_changed(self, value, value2):
|
||||
"""Fire an event with the data that have changed.
|
||||
|
||||
This method is called when there is an incoming packet associated
|
||||
with this platform.
|
||||
"""
|
||||
self.update_ha_state()
|
||||
if value2 == 0x70:
|
||||
self.which = 0
|
||||
self.onoff = 0
|
||||
elif value2 == 0x50:
|
||||
self.which = 0
|
||||
self.onoff = 1
|
||||
elif value2 == 0x30:
|
||||
self.which = 1
|
||||
self.onoff = 0
|
||||
elif value2 == 0x10:
|
||||
self.which = 1
|
||||
self.onoff = 1
|
||||
self.hass.bus.fire('button_pressed', {"id": self.dev_id,
|
||||
'pushed': value,
|
||||
'which': self.which,
|
||||
'onoff': self.onoff})
|
@ -9,11 +9,12 @@ import logging
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDevice,
|
||||
ENTITY_ID_FORMAT,
|
||||
SENSOR_CLASSES)
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_VALUE_TEMPLATE
|
||||
from homeassistant.core import EVENT_STATE_CHANGED
|
||||
from homeassistant.const import (ATTR_FRIENDLY_NAME, CONF_VALUE_TEMPLATE,
|
||||
ATTR_ENTITY_ID, MATCH_ALL)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.util import slugify
|
||||
|
||||
CONF_SENSORS = 'sensors'
|
||||
@ -52,13 +53,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
'Missing %s for sensor %s', CONF_VALUE_TEMPLATE, device)
|
||||
continue
|
||||
|
||||
entity_ids = device_config.get(ATTR_ENTITY_ID, MATCH_ALL)
|
||||
|
||||
sensors.append(
|
||||
BinarySensorTemplate(
|
||||
hass,
|
||||
device,
|
||||
friendly_name,
|
||||
sensor_class,
|
||||
value_template)
|
||||
value_template,
|
||||
entity_ids)
|
||||
)
|
||||
if not sensors:
|
||||
_LOGGER.error('No sensors added')
|
||||
@ -73,7 +77,7 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, device, friendly_name, sensor_class,
|
||||
value_template):
|
||||
value_template, entity_ids):
|
||||
"""Initialize the Template binary sensor."""
|
||||
self.hass = hass
|
||||
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, device,
|
||||
@ -85,12 +89,12 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
|
||||
self.update()
|
||||
|
||||
def template_bsensor_event_listener(event):
|
||||
def template_bsensor_state_listener(entity, old_state, new_state):
|
||||
"""Called when the target device changes state."""
|
||||
self.update_ha_state(True)
|
||||
|
||||
hass.bus.listen(EVENT_STATE_CHANGED,
|
||||
template_bsensor_event_listener)
|
||||
track_state_change(hass, entity_ids,
|
||||
template_bsensor_state_listener)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -6,17 +6,12 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/camera/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.components import bloomsky
|
||||
from homeassistant.const import HTTP_OK, HTTP_NOT_FOUND, ATTR_ENTITY_ID
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
DOMAIN = 'camera'
|
||||
DEPENDENCIES = ['http']
|
||||
@ -32,10 +27,7 @@ STATE_RECORDING = 'recording'
|
||||
STATE_STREAMING = 'streaming'
|
||||
STATE_IDLE = 'idle'
|
||||
|
||||
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}'
|
||||
|
||||
MULTIPART_BOUNDARY = '--jpgboundary'
|
||||
MJPEG_START_HEADER = 'Content-type: {0}\r\n\r\n'
|
||||
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
|
||||
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
@ -45,57 +37,11 @@ def setup(hass, config):
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
|
||||
DISCOVERY_PLATFORMS)
|
||||
|
||||
hass.wsgi.register_view(CameraImageView(hass, component.entities))
|
||||
hass.wsgi.register_view(CameraMjpegStream(hass, component.entities))
|
||||
|
||||
component.setup(config)
|
||||
|
||||
def _proxy_camera_image(handler, path_match, data):
|
||||
"""Serve the camera image via the HA server."""
|
||||
entity_id = path_match.group(ATTR_ENTITY_ID)
|
||||
camera = component.entities.get(entity_id)
|
||||
|
||||
if camera is None:
|
||||
handler.send_response(HTTP_NOT_FOUND)
|
||||
handler.end_headers()
|
||||
return
|
||||
|
||||
response = camera.camera_image()
|
||||
|
||||
if response is None:
|
||||
handler.send_response(HTTP_NOT_FOUND)
|
||||
handler.end_headers()
|
||||
return
|
||||
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.write_content(response)
|
||||
|
||||
hass.http.register_path(
|
||||
'GET',
|
||||
re.compile(r'/api/camera_proxy/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
_proxy_camera_image)
|
||||
|
||||
def _proxy_camera_mjpeg_stream(handler, path_match, data):
|
||||
"""Proxy the camera image as an mjpeg stream via the HA server."""
|
||||
entity_id = path_match.group(ATTR_ENTITY_ID)
|
||||
camera = component.entities.get(entity_id)
|
||||
|
||||
if camera is None:
|
||||
handler.send_response(HTTP_NOT_FOUND)
|
||||
handler.end_headers()
|
||||
return
|
||||
|
||||
try:
|
||||
camera.is_streaming = True
|
||||
camera.update_ha_state()
|
||||
camera.mjpeg_stream(handler)
|
||||
|
||||
except (requests.RequestException, IOError):
|
||||
camera.is_streaming = False
|
||||
camera.update_ha_state()
|
||||
|
||||
hass.http.register_path(
|
||||
'GET',
|
||||
re.compile(r'/api/camera_proxy_stream/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||
_proxy_camera_mjpeg_stream)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@ -106,6 +52,11 @@ class Camera(Entity):
|
||||
"""Initialize a camera."""
|
||||
self.is_streaming = False
|
||||
|
||||
@property
|
||||
def access_token(self):
|
||||
"""Access token for this camera."""
|
||||
return str(id(self))
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No need to poll cameras."""
|
||||
@ -114,7 +65,7 @@ class Camera(Entity):
|
||||
@property
|
||||
def entity_picture(self):
|
||||
"""Return a link to the camera feed as entity picture."""
|
||||
return ENTITY_IMAGE_URL.format(self.entity_id)
|
||||
return ENTITY_IMAGE_URL.format(self.entity_id, self.access_token)
|
||||
|
||||
@property
|
||||
def is_recording(self):
|
||||
@ -135,32 +86,35 @@ class Camera(Entity):
|
||||
"""Return bytes of camera image."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def mjpeg_stream(self, handler):
|
||||
def mjpeg_stream(self, response):
|
||||
"""Generate an HTTP MJPEG stream from camera images."""
|
||||
def write_string(text):
|
||||
"""Helper method to write a string to the stream."""
|
||||
handler.request.sendall(bytes(text + '\r\n', 'utf-8'))
|
||||
import eventlet
|
||||
response.content_type = ('multipart/x-mixed-replace; '
|
||||
'boundary=--jpegboundary')
|
||||
|
||||
write_string('HTTP/1.1 200 OK')
|
||||
write_string('Content-type: multipart/x-mixed-replace; '
|
||||
'boundary={}'.format(MULTIPART_BOUNDARY))
|
||||
write_string('')
|
||||
write_string(MULTIPART_BOUNDARY)
|
||||
def stream():
|
||||
"""Stream images as mjpeg stream."""
|
||||
try:
|
||||
last_image = None
|
||||
while True:
|
||||
img_bytes = self.camera_image()
|
||||
|
||||
while True:
|
||||
img_bytes = self.camera_image()
|
||||
if img_bytes is not None and img_bytes != last_image:
|
||||
yield bytes(
|
||||
'--jpegboundary\r\n'
|
||||
'Content-Type: image/jpeg\r\n'
|
||||
'Content-Length: {}\r\n\r\n'.format(
|
||||
len(img_bytes)), 'utf-8') + img_bytes + b'\r\n'
|
||||
|
||||
if img_bytes is None:
|
||||
continue
|
||||
last_image = img_bytes
|
||||
|
||||
write_string('Content-length: {}'.format(len(img_bytes)))
|
||||
write_string('Content-type: image/jpeg')
|
||||
write_string('')
|
||||
handler.request.sendall(img_bytes)
|
||||
write_string('')
|
||||
write_string(MULTIPART_BOUNDARY)
|
||||
eventlet.sleep(0.5)
|
||||
except GeneratorExit:
|
||||
pass
|
||||
|
||||
time.sleep(0.5)
|
||||
response.response = stream()
|
||||
|
||||
return response
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
@ -175,7 +129,9 @@ class Camera(Entity):
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Camera state attributes."""
|
||||
attr = {}
|
||||
attr = {
|
||||
'access_token': self.access_token,
|
||||
}
|
||||
|
||||
if self.model:
|
||||
attr['model_name'] = self.model
|
||||
@ -184,3 +140,60 @@ class Camera(Entity):
|
||||
attr['brand'] = self.brand
|
||||
|
||||
return attr
|
||||
|
||||
|
||||
class CameraView(HomeAssistantView):
|
||||
"""Base CameraView."""
|
||||
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, hass, entities):
|
||||
"""Initialize a basic camera view."""
|
||||
super().__init__(hass)
|
||||
self.entities = entities
|
||||
|
||||
def get(self, request, entity_id):
|
||||
"""Start a get request."""
|
||||
camera = self.entities.get(entity_id)
|
||||
|
||||
if camera is None:
|
||||
return self.Response(status=404)
|
||||
|
||||
authenticated = (request.authenticated or
|
||||
request.args.get('token') == camera.access_token)
|
||||
|
||||
if not authenticated:
|
||||
return self.Response(status=401)
|
||||
|
||||
return self.handle(camera)
|
||||
|
||||
def handle(self, camera):
|
||||
"""Hanlde the camera request."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class CameraImageView(CameraView):
|
||||
"""Camera view to serve an image."""
|
||||
|
||||
url = "/api/camera_proxy/<entity(domain=camera):entity_id>"
|
||||
name = "api:camera:image"
|
||||
|
||||
def handle(self, camera):
|
||||
"""Serve camera image."""
|
||||
response = camera.camera_image()
|
||||
|
||||
if response is None:
|
||||
return self.Response(status=500)
|
||||
|
||||
return self.Response(response)
|
||||
|
||||
|
||||
class CameraMjpegStream(CameraView):
|
||||
"""Camera View to serve an MJPEG stream."""
|
||||
|
||||
url = "/api/camera_proxy_stream/<entity(domain=camera):entity_id>"
|
||||
name = "api:camera:stream"
|
||||
|
||||
def handle(self, camera):
|
||||
"""Serve camera image."""
|
||||
return camera.mjpeg_stream(self.Response())
|
||||
|
@ -11,7 +11,6 @@ import requests
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
from homeassistant.components.camera import DOMAIN, Camera
|
||||
from homeassistant.const import HTTP_OK
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
CONTENT_TYPE_HEADER = 'Content-Type'
|
||||
@ -68,19 +67,12 @@ class MjpegCamera(Camera):
|
||||
with closing(self.camera_stream()) as response:
|
||||
return process_response(response)
|
||||
|
||||
def mjpeg_stream(self, handler):
|
||||
def mjpeg_stream(self, response):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
response = self.camera_stream()
|
||||
content_type = response.headers[CONTENT_TYPE_HEADER]
|
||||
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.send_header(CONTENT_TYPE_HEADER, content_type)
|
||||
handler.end_headers()
|
||||
|
||||
for chunk in response.iter_content(chunk_size=1024):
|
||||
if not chunk:
|
||||
break
|
||||
handler.wfile.write(chunk)
|
||||
stream = self.camera_stream()
|
||||
response.mimetype = stream.headers[CONTENT_TYPE_HEADER]
|
||||
response.response = stream.iter_content(chunk_size=1024)
|
||||
return response
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -12,7 +12,7 @@ import requests
|
||||
from homeassistant.components.camera import DOMAIN, Camera
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
REQUIREMENTS = ['uvcclient==0.8']
|
||||
REQUIREMENTS = ['uvcclient==0.9.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -45,13 +45,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
_LOGGER.error('Unable to connect to NVR: %s', str(ex))
|
||||
return False
|
||||
|
||||
identifier = nvrconn.server_version >= (3, 2, 0) and 'id' or 'uuid'
|
||||
# Filter out airCam models, which are not supported in the latest
|
||||
# version of UnifiVideo and which are EOL by Ubiquiti
|
||||
cameras = [camera for camera in cameras
|
||||
if 'airCam' not in nvrconn.get_camera(camera['uuid'])['model']]
|
||||
cameras = [
|
||||
camera for camera in cameras
|
||||
if 'airCam' not in nvrconn.get_camera(camera[identifier])['model']]
|
||||
|
||||
add_devices([UnifiVideoCamera(nvrconn,
|
||||
camera['uuid'],
|
||||
camera[identifier],
|
||||
camera['name'])
|
||||
for camera in cameras])
|
||||
return True
|
||||
@ -110,12 +112,17 @@ class UnifiVideoCamera(Camera):
|
||||
dict(name=self._name))
|
||||
password = 'ubnt'
|
||||
|
||||
if self._nvr.server_version >= (3, 2, 0):
|
||||
client_cls = uvc_camera.UVCCameraClientV320
|
||||
else:
|
||||
client_cls = uvc_camera.UVCCameraClient
|
||||
|
||||
camera = None
|
||||
for addr in addrs:
|
||||
try:
|
||||
camera = uvc_camera.UVCCameraClient(addr,
|
||||
caminfo['username'],
|
||||
password)
|
||||
camera = client_cls(addr,
|
||||
caminfo['username'],
|
||||
password)
|
||||
camera.login()
|
||||
_LOGGER.debug('Logged into UVC camera %(name)s via %(addr)s',
|
||||
dict(name=self._name, addr=addr))
|
||||
|
@ -27,7 +27,7 @@ SERVICE_PROCESS_SCHEMA = vol.Schema({
|
||||
|
||||
REGEX_TURN_COMMAND = re.compile(r'turn (?P<name>(?: |\w)+) (?P<command>\w+)')
|
||||
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.8.0']
|
||||
REQUIREMENTS = ['fuzzywuzzy==0.10.0']
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
|
@ -17,7 +17,7 @@ from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_per_platform
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util as util
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
@ -26,6 +26,7 @@ from homeassistant.const import (
|
||||
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE,
|
||||
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME)
|
||||
|
||||
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
|
||||
DOMAIN = "device_tracker"
|
||||
DEPENDENCIES = ['zone']
|
||||
|
||||
@ -193,7 +194,7 @@ class DeviceTracker(object):
|
||||
if not device:
|
||||
dev_id = util.slugify(host_name or '') or util.slugify(mac)
|
||||
else:
|
||||
dev_id = str(dev_id).lower()
|
||||
dev_id = cv.slug(str(dev_id).lower())
|
||||
device = self.devices.get(dev_id)
|
||||
|
||||
if device:
|
||||
|
@ -5,95 +5,92 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.locative/
|
||||
"""
|
||||
import logging
|
||||
from functools import partial
|
||||
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
URL_API_LOCATIVE_ENDPOINT = "/api/locative"
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
"""Setup an endpoint for the Locative application."""
|
||||
# POST would be semantically better, but that currently does not work
|
||||
# since Locative sends the data as key1=value1&key2=value2
|
||||
# in the request body, while Home Assistant expects json there.
|
||||
hass.http.register_path(
|
||||
'GET', URL_API_LOCATIVE_ENDPOINT,
|
||||
partial(_handle_get_api_locative, hass, see))
|
||||
hass.wsgi.register_view(LocativeView(hass, see))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_get_api_locative(hass, see, handler, path_match, data):
|
||||
"""Locative message received."""
|
||||
if not _check_data(handler, data):
|
||||
return
|
||||
class LocativeView(HomeAssistantView):
|
||||
"""View to handle locative requests."""
|
||||
|
||||
device = data['device'].replace('-', '')
|
||||
location_name = data['id'].lower()
|
||||
direction = data['trigger']
|
||||
url = "/api/locative"
|
||||
name = "api:bootstrap"
|
||||
|
||||
if direction == 'enter':
|
||||
see(dev_id=device, location_name=location_name)
|
||||
handler.write_text("Setting location to {}".format(location_name))
|
||||
def __init__(self, hass, see):
|
||||
"""Initialize Locative url endpoints."""
|
||||
super().__init__(hass)
|
||||
self.see = see
|
||||
|
||||
elif direction == 'exit':
|
||||
current_state = hass.states.get("{}.{}".format(DOMAIN, device))
|
||||
def get(self, request):
|
||||
"""Locative message received as GET."""
|
||||
return self.post(request)
|
||||
|
||||
def post(self, request):
|
||||
"""Locative message received."""
|
||||
# pylint: disable=too-many-return-statements
|
||||
data = request.values
|
||||
|
||||
if 'latitude' not in data or 'longitude' not in data:
|
||||
return ("Latitude and longitude not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
if 'device' not in data:
|
||||
_LOGGER.error("Device id not specified.")
|
||||
return ("Device id not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
if 'id' not in data:
|
||||
_LOGGER.error("Location id not specified.")
|
||||
return ("Location id not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
if 'trigger' not in data:
|
||||
_LOGGER.error("Trigger is not specified.")
|
||||
return ("Trigger is not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
device = data['device'].replace('-', '')
|
||||
location_name = data['id'].lower()
|
||||
direction = data['trigger']
|
||||
|
||||
if direction == 'enter':
|
||||
self.see(dev_id=device, location_name=location_name)
|
||||
return "Setting location to {}".format(location_name)
|
||||
|
||||
elif direction == 'exit':
|
||||
current_state = self.hass.states.get(
|
||||
"{}.{}".format(DOMAIN, device))
|
||||
|
||||
if current_state is None or current_state.state == location_name:
|
||||
self.see(dev_id=device, location_name=STATE_NOT_HOME)
|
||||
return "Setting location to not home"
|
||||
else:
|
||||
# Ignore the message if it is telling us to exit a zone that we
|
||||
# aren't currently in. This occurs when a zone is entered
|
||||
# before the previous zone was exited. The enter message will
|
||||
# be sent first, then the exit message will be sent second.
|
||||
return 'Ignoring exit from {} (already in {})'.format(
|
||||
location_name, current_state)
|
||||
|
||||
elif direction == 'test':
|
||||
# In the app, a test message can be sent. Just return something to
|
||||
# the user to let them know that it works.
|
||||
return "Received test message."
|
||||
|
||||
if current_state is None or current_state.state == location_name:
|
||||
see(dev_id=device, location_name=STATE_NOT_HOME)
|
||||
handler.write_text("Setting location to not home")
|
||||
else:
|
||||
# Ignore the message if it is telling us to exit a zone that we
|
||||
# aren't currently in. This occurs when a zone is entered before
|
||||
# the previous zone was exited. The enter message will be sent
|
||||
# first, then the exit message will be sent second.
|
||||
handler.write_text(
|
||||
'Ignoring exit from {} (already in {})'.format(
|
||||
location_name, current_state))
|
||||
|
||||
elif direction == 'test':
|
||||
# In the app, a test message can be sent. Just return something to
|
||||
# the user to let them know that it works.
|
||||
handler.write_text("Received test message.")
|
||||
|
||||
else:
|
||||
handler.write_text(
|
||||
"Received unidentified message: {}".format(direction),
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error("Received unidentified message from Locative: %s",
|
||||
direction)
|
||||
|
||||
|
||||
def _check_data(handler, data):
|
||||
"""Check the data."""
|
||||
if 'latitude' not in data or 'longitude' not in data:
|
||||
handler.write_text("Latitude and longitude not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error("Latitude and longitude not specified.")
|
||||
return False
|
||||
|
||||
if 'device' not in data:
|
||||
handler.write_text("Device id not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error("Device id not specified.")
|
||||
return False
|
||||
|
||||
if 'id' not in data:
|
||||
handler.write_text("Location id not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error("Location id not specified.")
|
||||
return False
|
||||
|
||||
if 'trigger' not in data:
|
||||
handler.write_text("Trigger is not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error("Trigger is not specified.")
|
||||
return False
|
||||
|
||||
return True
|
||||
_LOGGER.error("Received unidentified message from Locative: %s",
|
||||
direction)
|
||||
return ("Received unidentified message: {}".format(direction),
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
@ -186,7 +186,7 @@ def setup_scanner(hass, config, see):
|
||||
def _parse_see_args(topic, data):
|
||||
"""Parse the OwnTracks location parameters, into the format see expects."""
|
||||
parts = topic.split('/')
|
||||
dev_id = '{}_{}'.format(parts[1], parts[2])
|
||||
dev_id = slugify('{}_{}'.format(parts[1], parts[2]))
|
||||
host_name = parts[1]
|
||||
kwargs = {
|
||||
'dev_id': dev_id,
|
||||
|
@ -18,7 +18,7 @@ from homeassistant.util import Throttle
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['pysnmp==4.2.5']
|
||||
REQUIREMENTS = ['pysnmp==4.3.2']
|
||||
|
||||
CONF_COMMUNITY = "community"
|
||||
CONF_BASEOID = "baseoid"
|
||||
@ -72,7 +72,7 @@ class SnmpScanner(object):
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self):
|
||||
"""Ensure the information from the WAP is up to date.
|
||||
"""Ensure the information from the device is up to date.
|
||||
|
||||
Return boolean if scanning successful.
|
||||
"""
|
||||
@ -88,7 +88,7 @@ class SnmpScanner(object):
|
||||
return True
|
||||
|
||||
def get_snmp_data(self):
|
||||
"""Fetch MAC addresses from WAP via SNMP."""
|
||||
"""Fetch MAC addresses from access point via SNMP."""
|
||||
devices = []
|
||||
|
||||
errindication, errstatus, errindex, restable = self.snmp.nextCmd(
|
||||
@ -97,9 +97,10 @@ class SnmpScanner(object):
|
||||
if errindication:
|
||||
_LOGGER.error("SNMPLIB error: %s", errindication)
|
||||
return
|
||||
# pylint: disable=no-member
|
||||
if errstatus:
|
||||
_LOGGER.error('SNMP error: %s at %s', errstatus.prettyPrint(),
|
||||
errindex and restable[-1][int(errindex)-1] or '?')
|
||||
errindex and restable[int(errindex) - 1][0] or '?')
|
||||
return
|
||||
|
||||
for resrow in restable:
|
||||
|
117
homeassistant/components/enocean.py
Normal file
117
homeassistant/components/enocean.py
Normal file
@ -0,0 +1,117 @@
|
||||
"""
|
||||
EnOcean Component.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/EnOcean/
|
||||
"""
|
||||
|
||||
DOMAIN = "enocean"
|
||||
|
||||
REQUIREMENTS = ['enocean==0.31']
|
||||
|
||||
CONF_DEVICE = "device"
|
||||
|
||||
ENOCEAN_DONGLE = None
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the EnOcean component."""
|
||||
global ENOCEAN_DONGLE
|
||||
|
||||
serial_dev = config[DOMAIN].get(CONF_DEVICE, "/dev/ttyUSB0")
|
||||
|
||||
ENOCEAN_DONGLE = EnOceanDongle(hass, serial_dev)
|
||||
return True
|
||||
|
||||
|
||||
class EnOceanDongle:
|
||||
"""Representation of an EnOcean dongle."""
|
||||
|
||||
def __init__(self, hass, ser):
|
||||
"""Initialize the EnOcean dongle."""
|
||||
from enocean.communicators.serialcommunicator import SerialCommunicator
|
||||
self.__communicator = SerialCommunicator(port=ser,
|
||||
callback=self.callback)
|
||||
self.__communicator.start()
|
||||
self.__devices = []
|
||||
|
||||
def register_device(self, dev):
|
||||
"""Register another device."""
|
||||
self.__devices.append(dev)
|
||||
|
||||
def send_command(self, command):
|
||||
"""Send a command from the EnOcean dongle."""
|
||||
self.__communicator.send(command)
|
||||
|
||||
def _combine_hex(self, data): # pylint: disable=no-self-use
|
||||
"""Combine list of integer values to one big integer."""
|
||||
output = 0x00
|
||||
for i, j in enumerate(reversed(data)):
|
||||
output |= (j << i * 8)
|
||||
return output
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def callback(self, temp):
|
||||
"""Callback function for EnOcean Device.
|
||||
|
||||
This is the callback function called by
|
||||
python-enocan whenever there is an incoming
|
||||
packet.
|
||||
"""
|
||||
from enocean.protocol.packet import RadioPacket
|
||||
if isinstance(temp, RadioPacket):
|
||||
rxtype = None
|
||||
value = None
|
||||
if temp.data[6] == 0x30:
|
||||
rxtype = "wallswitch"
|
||||
value = 1
|
||||
elif temp.data[6] == 0x20:
|
||||
rxtype = "wallswitch"
|
||||
value = 0
|
||||
elif temp.data[4] == 0x0c:
|
||||
rxtype = "power"
|
||||
value = temp.data[3] + (temp.data[2] << 8)
|
||||
elif temp.data[2] == 0x60:
|
||||
rxtype = "switch_status"
|
||||
if temp.data[3] == 0xe4:
|
||||
value = 1
|
||||
elif temp.data[3] == 0x80:
|
||||
value = 0
|
||||
elif temp.data[0] == 0xa5 and temp.data[1] == 0x02:
|
||||
rxtype = "dimmerstatus"
|
||||
value = temp.data[2]
|
||||
for device in self.__devices:
|
||||
if rxtype == "wallswitch" and device.stype == "listener":
|
||||
if temp.sender == self._combine_hex(device.dev_id):
|
||||
device.value_changed(value, temp.data[1])
|
||||
if rxtype == "power" and device.stype == "powersensor":
|
||||
if temp.sender == self._combine_hex(device.dev_id):
|
||||
device.value_changed(value)
|
||||
if rxtype == "power" and device.stype == "switch":
|
||||
if temp.sender == self._combine_hex(device.dev_id):
|
||||
if value > 10:
|
||||
device.value_changed(1)
|
||||
if rxtype == "switch_status" and device.stype == "switch":
|
||||
if temp.sender == self._combine_hex(device.dev_id):
|
||||
device.value_changed(value)
|
||||
if rxtype == "dimmerstatus" and device.stype == "dimmer":
|
||||
if temp.sender == self._combine_hex(device.dev_id):
|
||||
device.value_changed(value)
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class EnOceanDevice():
|
||||
"""Parent class for all devices associated with the EnOcean component."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the device."""
|
||||
ENOCEAN_DONGLE.register_device(self)
|
||||
self.stype = ""
|
||||
self.sensorid = [0x00, 0x00, 0x00, 0x00]
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def send_command(self, data, optional, packet_type):
|
||||
"""Send a command via the EnOcean dongle."""
|
||||
from enocean.protocol.packet import Packet
|
||||
packet = Packet(packet_type, data=data, optional=optional)
|
||||
ENOCEAN_DONGLE.send_command(packet)
|
@ -6,7 +6,11 @@ https://home-assistant.io/components/feedreader/
|
||||
"""
|
||||
from datetime import datetime
|
||||
from logging import getLogger
|
||||
from os.path import exists
|
||||
from threading import Lock
|
||||
import pickle
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
|
||||
@ -27,14 +31,15 @@ MAX_ENTRIES = 20
|
||||
class FeedManager(object):
|
||||
"""Abstraction over feedparser module."""
|
||||
|
||||
def __init__(self, url, hass):
|
||||
def __init__(self, url, hass, storage):
|
||||
"""Initialize the FeedManager object, poll every hour."""
|
||||
self._url = url
|
||||
self._feed = None
|
||||
self._hass = hass
|
||||
self._firstrun = True
|
||||
# Initialize last entry timestamp as epoch time
|
||||
self._last_entry_timestamp = datetime.utcfromtimestamp(0).timetuple()
|
||||
self._storage = storage
|
||||
self._last_entry_timestamp = None
|
||||
self._has_published_parsed = False
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START,
|
||||
lambda _: self._update())
|
||||
track_utc_time_change(hass, lambda now: self._update(),
|
||||
@ -42,7 +47,7 @@ class FeedManager(object):
|
||||
|
||||
def _log_no_entries(self):
|
||||
"""Send no entries log at debug level."""
|
||||
_LOGGER.debug('No new entries in feed "%s"', self._url)
|
||||
_LOGGER.debug('No new entries to be published in feed "%s"', self._url)
|
||||
|
||||
def _update(self):
|
||||
"""Update the feed and publish new entries to the event bus."""
|
||||
@ -65,10 +70,13 @@ class FeedManager(object):
|
||||
len(self._feed.entries),
|
||||
self._url)
|
||||
if len(self._feed.entries) > MAX_ENTRIES:
|
||||
_LOGGER.debug('Publishing only the first %s entries '
|
||||
_LOGGER.debug('Processing only the first %s entries '
|
||||
'in feed "%s"', MAX_ENTRIES, self._url)
|
||||
self._feed.entries = self._feed.entries[0:MAX_ENTRIES]
|
||||
self._publish_new_entries()
|
||||
if self._has_published_parsed:
|
||||
self._storage.put_timestamp(self._url,
|
||||
self._last_entry_timestamp)
|
||||
else:
|
||||
self._log_no_entries()
|
||||
_LOGGER.info('Fetch from feed "%s" completed', self._url)
|
||||
@ -79,9 +87,11 @@ class FeedManager(object):
|
||||
# let's make use of it to publish only new available
|
||||
# entries since the last run
|
||||
if 'published_parsed' in entry.keys():
|
||||
self._has_published_parsed = True
|
||||
self._last_entry_timestamp = max(entry.published_parsed,
|
||||
self._last_entry_timestamp)
|
||||
else:
|
||||
self._has_published_parsed = False
|
||||
_LOGGER.debug('No `published_parsed` info available '
|
||||
'for entry "%s"', entry.title)
|
||||
entry.update({'feed_url': self._url})
|
||||
@ -90,6 +100,13 @@ class FeedManager(object):
|
||||
def _publish_new_entries(self):
|
||||
"""Publish new entries to the event bus."""
|
||||
new_entries = False
|
||||
self._last_entry_timestamp = self._storage.get_timestamp(self._url)
|
||||
if self._last_entry_timestamp:
|
||||
self._firstrun = False
|
||||
else:
|
||||
# Set last entry timestamp as epoch time if not available
|
||||
self._last_entry_timestamp = \
|
||||
datetime.utcfromtimestamp(0).timetuple()
|
||||
for entry in self._feed.entries:
|
||||
if self._firstrun or (
|
||||
'published_parsed' in entry.keys() and
|
||||
@ -103,8 +120,55 @@ class FeedManager(object):
|
||||
self._firstrun = False
|
||||
|
||||
|
||||
class StoredData(object):
|
||||
"""Abstraction over pickle data storage."""
|
||||
|
||||
def __init__(self, data_file):
|
||||
"""Initialize pickle data storage."""
|
||||
self._data_file = data_file
|
||||
self._lock = Lock()
|
||||
self._cache_outdated = True
|
||||
self._data = {}
|
||||
self._fetch_data()
|
||||
|
||||
def _fetch_data(self):
|
||||
"""Fetch data stored into pickle file."""
|
||||
if self._cache_outdated and exists(self._data_file):
|
||||
try:
|
||||
_LOGGER.debug('Fetching data from file %s', self._data_file)
|
||||
with self._lock, open(self._data_file, 'rb') as myfile:
|
||||
self._data = pickle.load(myfile) or {}
|
||||
self._cache_outdated = False
|
||||
# pylint: disable=bare-except
|
||||
except:
|
||||
_LOGGER.error('Error loading data from pickled file %s',
|
||||
self._data_file)
|
||||
|
||||
def get_timestamp(self, url):
|
||||
"""Return stored timestamp for given url."""
|
||||
self._fetch_data()
|
||||
return self._data.get(url)
|
||||
|
||||
def put_timestamp(self, url, timestamp):
|
||||
"""Update timestamp for given url."""
|
||||
self._fetch_data()
|
||||
with self._lock, open(self._data_file, 'wb') as myfile:
|
||||
self._data.update({url: timestamp})
|
||||
_LOGGER.debug('Overwriting feed "%s" timestamp in storage file %s',
|
||||
url, self._data_file)
|
||||
try:
|
||||
pickle.dump(self._data, myfile)
|
||||
# pylint: disable=bare-except
|
||||
except:
|
||||
_LOGGER.error('Error saving pickled data to %s',
|
||||
self._data_file)
|
||||
self._cache_outdated = True
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup the feedreader component."""
|
||||
urls = config.get(DOMAIN)['urls']
|
||||
feeds = [FeedManager(url, hass) for url in urls]
|
||||
data_file = hass.config.path("{}.pickle".format(DOMAIN))
|
||||
storage = StoredData(data_file)
|
||||
feeds = [FeedManager(url, hass, storage) for url in urls]
|
||||
return len(feeds) > 0
|
||||
|
@ -1,121 +1,101 @@
|
||||
"""Handle the frontend for Home Assistant."""
|
||||
import re
|
||||
import os
|
||||
import logging
|
||||
|
||||
from . import version, mdi_version
|
||||
import homeassistant.util as util
|
||||
from homeassistant.const import URL_ROOT, HTTP_OK
|
||||
from homeassistant.components import api
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api']
|
||||
|
||||
INDEX_PATH = os.path.join(os.path.dirname(__file__), 'index.html.template')
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
FRONTEND_URLS = [
|
||||
URL_ROOT, '/logbook', '/history', '/map', '/devService', '/devState',
|
||||
'/devEvent', '/devInfo', '/devTemplate',
|
||||
re.compile(r'/states(/([a-zA-Z\._\-0-9/]+)|)'),
|
||||
]
|
||||
|
||||
URL_API_BOOTSTRAP = "/api/bootstrap"
|
||||
|
||||
_FINGERPRINT = re.compile(r'^(\w+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup serving the frontend."""
|
||||
for url in FRONTEND_URLS:
|
||||
hass.http.register_path('GET', url, _handle_get_root, False)
|
||||
hass.wsgi.register_view(IndexView)
|
||||
hass.wsgi.register_view(BootstrapView)
|
||||
|
||||
hass.http.register_path('GET', '/service_worker.js',
|
||||
_handle_get_service_worker, False)
|
||||
|
||||
# Bootstrap API
|
||||
hass.http.register_path(
|
||||
'GET', URL_API_BOOTSTRAP, _handle_get_api_bootstrap)
|
||||
|
||||
# Static files
|
||||
hass.http.register_path(
|
||||
'GET', re.compile(r'/static/(?P<file>[a-zA-Z\._\-0-9/]+)'),
|
||||
_handle_get_static, False)
|
||||
hass.http.register_path(
|
||||
'HEAD', re.compile(r'/static/(?P<file>[a-zA-Z\._\-0-9/]+)'),
|
||||
_handle_get_static, False)
|
||||
hass.http.register_path(
|
||||
'GET', re.compile(r'/local/(?P<file>[a-zA-Z\._\-0-9/]+)'),
|
||||
_handle_get_local, False)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_get_api_bootstrap(handler, path_match, data):
|
||||
"""Return all data needed to bootstrap Home Assistant."""
|
||||
hass = handler.server.hass
|
||||
|
||||
handler.write_json({
|
||||
'config': hass.config.as_dict(),
|
||||
'states': hass.states.all(),
|
||||
'events': api.events_json(hass),
|
||||
'services': api.services_json(hass),
|
||||
})
|
||||
|
||||
|
||||
def _handle_get_root(handler, path_match, data):
|
||||
"""Render the frontend."""
|
||||
if handler.server.development:
|
||||
app_url = "home-assistant-polymer/src/home-assistant.html"
|
||||
else:
|
||||
app_url = "frontend-{}.html".format(version.VERSION)
|
||||
|
||||
# auto login if no password was set, else check api_password param
|
||||
auth = ('no_password_set' if handler.server.api_password is None
|
||||
else data.get('api_password', ''))
|
||||
|
||||
with open(INDEX_PATH) as template_file:
|
||||
template_html = template_file.read()
|
||||
|
||||
template_html = template_html.replace('{{ app_url }}', app_url)
|
||||
template_html = template_html.replace('{{ auth }}', auth)
|
||||
template_html = template_html.replace('{{ icons }}', mdi_version.VERSION)
|
||||
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.write_content(template_html.encode("UTF-8"),
|
||||
'text/html; charset=utf-8')
|
||||
|
||||
|
||||
def _handle_get_service_worker(handler, path_match, data):
|
||||
"""Return service worker for the frontend."""
|
||||
if handler.server.development:
|
||||
www_static_path = os.path.join(os.path.dirname(__file__), 'www_static')
|
||||
if hass.wsgi.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))
|
||||
hass.wsgi.register_static_path(
|
||||
"/service_worker.js",
|
||||
os.path.join(www_static_path, sw_path),
|
||||
0
|
||||
)
|
||||
hass.wsgi.register_static_path(
|
||||
"/robots.txt",
|
||||
os.path.join(www_static_path, "robots.txt")
|
||||
)
|
||||
hass.wsgi.register_static_path("/static", www_static_path)
|
||||
hass.wsgi.register_static_path("/local", hass.config.path('www'))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_get_static(handler, path_match, data):
|
||||
"""Return a static file for the frontend."""
|
||||
req_file = util.sanitize_path(path_match.group('file'))
|
||||
class BootstrapView(HomeAssistantView):
|
||||
"""View to bootstrap frontend with all needed data."""
|
||||
|
||||
# Strip md5 hash out
|
||||
fingerprinted = _FINGERPRINT.match(req_file)
|
||||
if fingerprinted:
|
||||
req_file = "{}.{}".format(*fingerprinted.groups())
|
||||
url = "/api/bootstrap"
|
||||
name = "api:bootstrap"
|
||||
|
||||
path = os.path.join(os.path.dirname(__file__), 'www_static', req_file)
|
||||
|
||||
handler.write_file(path)
|
||||
def get(self, request):
|
||||
"""Return all data needed to bootstrap Home Assistant."""
|
||||
return self.json({
|
||||
'config': self.hass.config.as_dict(),
|
||||
'states': self.hass.states.all(),
|
||||
'events': api.events_json(self.hass),
|
||||
'services': api.services_json(self.hass),
|
||||
})
|
||||
|
||||
|
||||
def _handle_get_local(handler, path_match, data):
|
||||
"""Return a static file from the hass.config.path/www for the frontend."""
|
||||
req_file = util.sanitize_path(path_match.group('file'))
|
||||
class IndexView(HomeAssistantView):
|
||||
"""Serve the frontend."""
|
||||
|
||||
path = handler.server.hass.config.path('www', req_file)
|
||||
url = '/'
|
||||
name = "frontend:index"
|
||||
requires_auth = False
|
||||
extra_urls = ['/logbook', '/history', '/map', '/devService', '/devState',
|
||||
'/devEvent', '/devInfo', '/devTemplate',
|
||||
'/states', '/states/<entity:entity_id>']
|
||||
|
||||
handler.write_file(path)
|
||||
def __init__(self, hass):
|
||||
"""Initialize the frontend view."""
|
||||
super().__init__(hass)
|
||||
|
||||
from jinja2 import FileSystemLoader, Environment
|
||||
|
||||
self.templates = Environment(
|
||||
loader=FileSystemLoader(
|
||||
os.path.join(os.path.dirname(__file__), 'templates/')
|
||||
)
|
||||
)
|
||||
|
||||
def get(self, request, entity_id=None):
|
||||
"""Serve the index view."""
|
||||
if self.hass.wsgi.development:
|
||||
core_url = 'home-assistant-polymer/build/_core_compiled.js'
|
||||
ui_url = 'home-assistant-polymer/src/home-assistant.html'
|
||||
else:
|
||||
core_url = 'core-{}.js'.format(version.CORE)
|
||||
ui_url = 'frontend-{}.html'.format(version.UI)
|
||||
|
||||
# auto login if no password was set
|
||||
if self.hass.config.api.api_password is None:
|
||||
auth = 'true'
|
||||
else:
|
||||
auth = 'false'
|
||||
|
||||
icons_url = 'mdi-{}.html'.format(mdi_version.VERSION)
|
||||
|
||||
template = self.templates.get_template('index.html')
|
||||
|
||||
# pylint is wrong
|
||||
# pylint: disable=no-member
|
||||
resp = template.render(
|
||||
core_url=core_url, ui_url=ui_url, auth=auth,
|
||||
icons_url=icons_url, icons=mdi_version.VERSION)
|
||||
|
||||
return self.Response(resp, mimetype='text/html')
|
||||
|
@ -1,2 +1,2 @@
|
||||
"""DO NOT MODIFY. Auto-generated by update_mdi script."""
|
||||
VERSION = "1baebe8155deb447230866d7ae854bd9"
|
||||
VERSION = "9ee3d4466a65bef35c2c8974e91b37c0"
|
||||
|
@ -9,6 +9,11 @@
|
||||
<link rel='apple-touch-icon' sizes='180x180'
|
||||
href='/static/favicon-apple-180x180.png'>
|
||||
<meta name='apple-mobile-web-app-capable' content='yes'>
|
||||
<meta name="msapplication-square70x70logo" content="/static/tile-win-70x70.png"/>
|
||||
<meta name="msapplication-square150x150logo" content="/static/tile-win-150x150.png"/>
|
||||
<meta name="msapplication-wide310x150logo" content="/static/tile-win-310x150.png"/>
|
||||
<meta name="msapplication-square310x310logo" content="/static/tile-win-310x310.png"/>
|
||||
<meta name="msapplication-TileColor" content="#3fbbf4ff"/>
|
||||
<meta name='mobile-web-app-capable' content='yes'>
|
||||
<meta name='viewport' content='width=device-width, user-scalable=no'>
|
||||
<meta name='theme-color' content='#03a9f4'>
|
||||
@ -28,7 +33,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin-bottom: 97px;
|
||||
margin-bottom: 83px;
|
||||
font-family: Roboto, sans-serif;
|
||||
font-size: 0pt;
|
||||
transition: font-size 2s;
|
||||
@ -36,6 +41,7 @@
|
||||
|
||||
#ha-init-skeleton paper-spinner {
|
||||
height: 28px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
#ha-init-skeleton a {
|
||||
@ -59,8 +65,8 @@
|
||||
.getElementById('ha-init-skeleton')
|
||||
.classList.add('error');
|
||||
}
|
||||
window.noAuth = {{ auth }}
|
||||
</script>
|
||||
<link rel='import' href='/static/{{ app_url }}' onerror='initError()' async>
|
||||
</head>
|
||||
<body fullbleed>
|
||||
<div id='ha-init-skeleton'>
|
||||
@ -68,6 +74,10 @@
|
||||
<paper-spinner active></paper-spinner>
|
||||
Home Assistant had trouble<br>connecting to the server.<br><br><a href='/'>TRY AGAIN</a>
|
||||
</div>
|
||||
<home-assistant icons='{{ icons }}'></home-assistant>
|
||||
<script src='/static/{{ core_url }}'></script>
|
||||
<link rel='import' href='/static/{{ ui_url }}' onerror='initError()' async>
|
||||
<link rel='import' href='/static/{{ icons_url }}' async>
|
||||
<script>
|
||||
var webComponentsSupported = (
|
||||
'registerElement' in document &&
|
||||
@ -81,6 +91,5 @@
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
</script>
|
||||
<home-assistant auth='{{ auth }}' icons='{{ icons }}'></home-assistant>
|
||||
</body>
|
||||
</html>
|
@ -1,2 +1,3 @@
|
||||
"""DO NOT MODIFY. Auto-generated by build_frontend script."""
|
||||
VERSION = "0a226e905af198b2dabf1ce154844568"
|
||||
CORE = "d0b415dac66c8056d81380b258af5767"
|
||||
UI = "b0ea2672fff86b1ab86dd86135d4b43a"
|
||||
|
5
homeassistant/components/frontend/www_static/core.js
Normal file
5
homeassistant/components/frontend/www_static/core.js
Normal file
File diff suppressed because one or more lines are too long
BIN
homeassistant/components/frontend/www_static/core.js.gz
Normal file
BIN
homeassistant/components/frontend/www_static/core.js.gz
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
homeassistant/components/frontend/www_static/frontend.html.gz
Normal file
BIN
homeassistant/components/frontend/www_static/frontend.html.gz
Normal file
Binary file not shown.
@ -1 +1 @@
|
||||
Subproject commit 4a667eb77e28a27dc766ca6f7bbd04e3866124d9
|
||||
Subproject commit 612a876199d8ecdc778182ea93fff034a4d15ef4
|
@ -8,12 +8,12 @@
|
||||
{
|
||||
"src": "/static/favicon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/favicon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
BIN
homeassistant/components/frontend/www_static/mdi.html.gz
Normal file
BIN
homeassistant/components/frontend/www_static/mdi.html.gz
Normal file
Binary file not shown.
2
homeassistant/components/frontend/www_static/robots.txt
Normal file
2
homeassistant/components/frontend/www_static/robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
@ -1 +1,258 @@
|
||||
!function(e){function t(r){if(n[r])return n[r].exports;var s=n[r]={i:r,l:!1,exports:{}};return e[r].call(s.exports,s,s.exports,t),s.l=!0,s.exports}var n={};return t.m=e,t.c=n,t.p="",t(t.s=194)}({194:function(e,t,n){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){return n||fetch(e.request).then(function(e){return t.put(s,e.clone()),e})})}))})}});
|
||||
/**
|
||||
* Copyright 2016 Google Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// This generated service worker JavaScript will precache your site's resources.
|
||||
// The code needs to be saved in a .js file at the top-level of your site, and registered
|
||||
// from your pages in order to be used. See
|
||||
// https://github.com/googlechrome/sw-precache/blob/master/demo/app/js/service-worker-registration.js
|
||||
// for an example of how you can register this script and handle various service worker events.
|
||||
|
||||
/* eslint-env worker, serviceworker */
|
||||
/* eslint-disable indent, no-unused-vars, no-multiple-empty-lines, max-nested-callbacks, space-before-function-paren */
|
||||
'use strict';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* eslint-disable quotes, comma-spacing */
|
||||
var PrecacheConfig = [["/","69818e2c5b6f4ca764c46ac78d2fea04"],["/devEvent","69818e2c5b6f4ca764c46ac78d2fea04"],["/devInfo","69818e2c5b6f4ca764c46ac78d2fea04"],["/devService","69818e2c5b6f4ca764c46ac78d2fea04"],["/devState","69818e2c5b6f4ca764c46ac78d2fea04"],["/devTemplate","69818e2c5b6f4ca764c46ac78d2fea04"],["/history","69818e2c5b6f4ca764c46ac78d2fea04"],["/logbook","69818e2c5b6f4ca764c46ac78d2fea04"],["/map","69818e2c5b6f4ca764c46ac78d2fea04"],["/states","69818e2c5b6f4ca764c46ac78d2fea04"],["/static/core-d0b415dac66c8056d81380b258af5767.js","dfafa8e9e34f53e8c36dd8b3f7299b2a"],["/static/frontend-b0ea2672fff86b1ab86dd86135d4b43a.html","69818e2c5b6f4ca764c46ac78d2fea04"],["/static/mdi-9ee3d4466a65bef35c2c8974e91b37c0.html","9a6846935116cd29279c91e0ee0a26d0"],["static/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","b0f32ad3c7749c40d486603f31c9d8b1"]];
|
||||
/* eslint-enable quotes, comma-spacing */
|
||||
var CacheNamePrefix = 'sw-precache-v1--' + (self.registration ? self.registration.scope : '') + '-';
|
||||
|
||||
|
||||
var IgnoreUrlParametersMatching = [/^utm_/];
|
||||
|
||||
|
||||
|
||||
var addDirectoryIndex = function (originalUrl, index) {
|
||||
var url = new URL(originalUrl);
|
||||
if (url.pathname.slice(-1) === '/') {
|
||||
url.pathname += index;
|
||||
}
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
var getCacheBustedUrl = function (url, param) {
|
||||
param = param || Date.now();
|
||||
|
||||
var urlWithCacheBusting = new URL(url);
|
||||
urlWithCacheBusting.search += (urlWithCacheBusting.search ? '&' : '') +
|
||||
'sw-precache=' + param;
|
||||
|
||||
return urlWithCacheBusting.toString();
|
||||
};
|
||||
|
||||
var isPathWhitelisted = function (whitelist, absoluteUrlString) {
|
||||
// If the whitelist is empty, then consider all URLs to be whitelisted.
|
||||
if (whitelist.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise compare each path regex to the path of the URL passed in.
|
||||
var path = (new URL(absoluteUrlString)).pathname;
|
||||
return whitelist.some(function(whitelistedPathRegex) {
|
||||
return path.match(whitelistedPathRegex);
|
||||
});
|
||||
};
|
||||
|
||||
var populateCurrentCacheNames = function (precacheConfig,
|
||||
cacheNamePrefix, baseUrl) {
|
||||
var absoluteUrlToCacheName = {};
|
||||
var currentCacheNamesToAbsoluteUrl = {};
|
||||
|
||||
precacheConfig.forEach(function(cacheOption) {
|
||||
var absoluteUrl = new URL(cacheOption[0], baseUrl).toString();
|
||||
var cacheName = cacheNamePrefix + absoluteUrl + '-' + cacheOption[1];
|
||||
currentCacheNamesToAbsoluteUrl[cacheName] = absoluteUrl;
|
||||
absoluteUrlToCacheName[absoluteUrl] = cacheName;
|
||||
});
|
||||
|
||||
return {
|
||||
absoluteUrlToCacheName: absoluteUrlToCacheName,
|
||||
currentCacheNamesToAbsoluteUrl: currentCacheNamesToAbsoluteUrl
|
||||
};
|
||||
};
|
||||
|
||||
var stripIgnoredUrlParameters = function (originalUrl,
|
||||
ignoreUrlParametersMatching) {
|
||||
var url = new URL(originalUrl);
|
||||
|
||||
url.search = url.search.slice(1) // Exclude initial '?'
|
||||
.split('&') // Split into an array of 'key=value' strings
|
||||
.map(function(kv) {
|
||||
return kv.split('='); // Split each 'key=value' string into a [key, value] array
|
||||
})
|
||||
.filter(function(kv) {
|
||||
return ignoreUrlParametersMatching.every(function(ignoredRegex) {
|
||||
return !ignoredRegex.test(kv[0]); // Return true iff the key doesn't match any of the regexes.
|
||||
});
|
||||
})
|
||||
.map(function(kv) {
|
||||
return kv.join('='); // Join each [key, value] array into a 'key=value' string
|
||||
})
|
||||
.join('&'); // Join the array of 'key=value' strings into a string with '&' in between each
|
||||
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
|
||||
var mappings = populateCurrentCacheNames(PrecacheConfig, CacheNamePrefix, self.location);
|
||||
var AbsoluteUrlToCacheName = mappings.absoluteUrlToCacheName;
|
||||
var CurrentCacheNamesToAbsoluteUrl = mappings.currentCacheNamesToAbsoluteUrl;
|
||||
|
||||
function deleteAllCaches() {
|
||||
return caches.keys().then(function(cacheNames) {
|
||||
return Promise.all(
|
||||
cacheNames.map(function(cacheName) {
|
||||
return caches.delete(cacheName);
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
self.addEventListener('install', function(event) {
|
||||
event.waitUntil(
|
||||
// Take a look at each of the cache names we expect for this version.
|
||||
Promise.all(Object.keys(CurrentCacheNamesToAbsoluteUrl).map(function(cacheName) {
|
||||
return caches.open(cacheName).then(function(cache) {
|
||||
// Get a list of all the entries in the specific named cache.
|
||||
// For caches that are already populated for a given version of a
|
||||
// resource, there should be 1 entry.
|
||||
return cache.keys().then(function(keys) {
|
||||
// If there are 0 entries, either because this is a brand new version
|
||||
// of a resource or because the install step was interrupted the
|
||||
// last time it ran, then we need to populate the cache.
|
||||
if (keys.length === 0) {
|
||||
// Use the last bit of the cache name, which contains the hash,
|
||||
// as the cache-busting parameter.
|
||||
// See https://github.com/GoogleChrome/sw-precache/issues/100
|
||||
var cacheBustParam = cacheName.split('-').pop();
|
||||
var urlWithCacheBusting = getCacheBustedUrl(
|
||||
CurrentCacheNamesToAbsoluteUrl[cacheName], cacheBustParam);
|
||||
|
||||
var request = new Request(urlWithCacheBusting,
|
||||
{credentials: 'same-origin'});
|
||||
return fetch(request).then(function(response) {
|
||||
if (response.ok) {
|
||||
return cache.put(CurrentCacheNamesToAbsoluteUrl[cacheName],
|
||||
response);
|
||||
}
|
||||
|
||||
console.error('Request for %s returned a response status %d, ' +
|
||||
'so not attempting to cache it.',
|
||||
urlWithCacheBusting, response.status);
|
||||
// Get rid of the empty cache if we can't add a successful response to it.
|
||||
return caches.delete(cacheName);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
})).then(function() {
|
||||
return caches.keys().then(function(allCacheNames) {
|
||||
return Promise.all(allCacheNames.filter(function(cacheName) {
|
||||
return cacheName.indexOf(CacheNamePrefix) === 0 &&
|
||||
!(cacheName in CurrentCacheNamesToAbsoluteUrl);
|
||||
}).map(function(cacheName) {
|
||||
return caches.delete(cacheName);
|
||||
})
|
||||
);
|
||||
});
|
||||
}).then(function() {
|
||||
if (typeof self.skipWaiting === 'function') {
|
||||
// Force the SW to transition from installing -> active state
|
||||
self.skipWaiting();
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
if (self.clients && (typeof self.clients.claim === 'function')) {
|
||||
self.addEventListener('activate', function(event) {
|
||||
event.waitUntil(self.clients.claim());
|
||||
});
|
||||
}
|
||||
|
||||
self.addEventListener('message', function(event) {
|
||||
if (event.data.command === 'delete_all') {
|
||||
console.log('About to delete all caches...');
|
||||
deleteAllCaches().then(function() {
|
||||
console.log('Caches deleted.');
|
||||
event.ports[0].postMessage({
|
||||
error: null
|
||||
});
|
||||
}).catch(function(error) {
|
||||
console.log('Caches not deleted:', error);
|
||||
event.ports[0].postMessage({
|
||||
error: error
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
self.addEventListener('fetch', function(event) {
|
||||
if (event.request.method === 'GET') {
|
||||
var urlWithoutIgnoredParameters = stripIgnoredUrlParameters(event.request.url,
|
||||
IgnoreUrlParametersMatching);
|
||||
|
||||
var cacheName = AbsoluteUrlToCacheName[urlWithoutIgnoredParameters];
|
||||
var directoryIndex = 'index.html';
|
||||
if (!cacheName && directoryIndex) {
|
||||
urlWithoutIgnoredParameters = addDirectoryIndex(urlWithoutIgnoredParameters, directoryIndex);
|
||||
cacheName = AbsoluteUrlToCacheName[urlWithoutIgnoredParameters];
|
||||
}
|
||||
|
||||
var navigateFallback = '';
|
||||
// Ideally, this would check for event.request.mode === 'navigate', but that is not widely
|
||||
// supported yet:
|
||||
// https://code.google.com/p/chromium/issues/detail?id=540967
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1209081
|
||||
if (!cacheName && navigateFallback && event.request.headers.has('accept') &&
|
||||
event.request.headers.get('accept').includes('text/html') &&
|
||||
/* eslint-disable quotes, comma-spacing */
|
||||
isPathWhitelisted([], event.request.url)) {
|
||||
/* eslint-enable quotes, comma-spacing */
|
||||
var navigateFallbackUrl = new URL(navigateFallback, self.location);
|
||||
cacheName = AbsoluteUrlToCacheName[navigateFallbackUrl.toString()];
|
||||
}
|
||||
|
||||
if (cacheName) {
|
||||
event.respondWith(
|
||||
// Rely on the fact that each cache we manage should only have one entry, and return that.
|
||||
caches.open(cacheName).then(function(cache) {
|
||||
return cache.keys().then(function(keys) {
|
||||
return cache.match(keys[0]).then(function(response) {
|
||||
if (response) {
|
||||
return response;
|
||||
}
|
||||
// If for some reason the response was deleted from the cache,
|
||||
// raise and exception and fall back to the fetch() triggered in the catch().
|
||||
throw Error('The cache ' + cacheName + ' is empty.');
|
||||
});
|
||||
});
|
||||
}).catch(function(e) {
|
||||
console.warn('Couldn\'t serve response for "%s" from cache: %O', event.request.url, e);
|
||||
return fetch(event.request);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
BIN
homeassistant/components/frontend/www_static/tile-win-70x70.png
Normal file
BIN
homeassistant/components/frontend/www_static/tile-win-70x70.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.9 KiB |
@ -11,7 +11,7 @@ from itertools import groupby
|
||||
|
||||
from homeassistant.components import recorder, script
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import HTTP_BAD_REQUEST
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
DOMAIN = 'history'
|
||||
DEPENDENCIES = ['recorder', 'http']
|
||||
@ -155,49 +155,44 @@ def get_state(utc_point_in_time, entity_id, run=None):
|
||||
# pylint: disable=unused-argument
|
||||
def setup(hass, config):
|
||||
"""Setup the history hooks."""
|
||||
hass.http.register_path(
|
||||
'GET',
|
||||
re.compile(
|
||||
r'/api/history/entity/(?P<entity_id>[a-zA-Z\._0-9]+)/'
|
||||
r'recent_states'),
|
||||
_api_last_5_states)
|
||||
|
||||
hass.http.register_path('GET', URL_HISTORY_PERIOD, _api_history_period)
|
||||
hass.wsgi.register_view(Last5StatesView)
|
||||
hass.wsgi.register_view(HistoryPeriodView)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
# pylint: disable=invalid-name
|
||||
def _api_last_5_states(handler, path_match, data):
|
||||
"""Return the last 5 states for an entity id as JSON."""
|
||||
entity_id = path_match.group('entity_id')
|
||||
class Last5StatesView(HomeAssistantView):
|
||||
"""Handle last 5 state view requests."""
|
||||
|
||||
handler.write_json(last_5_states(entity_id))
|
||||
url = '/api/history/entity/<entity:entity_id>/recent_states'
|
||||
name = 'api:history:entity-recent-states'
|
||||
|
||||
def get(self, request, entity_id):
|
||||
"""Retrieve last 5 states of entity."""
|
||||
return self.json(last_5_states(entity_id))
|
||||
|
||||
|
||||
def _api_history_period(handler, path_match, data):
|
||||
"""Return history over a period of time."""
|
||||
date_str = path_match.group('date')
|
||||
one_day = timedelta(seconds=86400)
|
||||
class HistoryPeriodView(HomeAssistantView):
|
||||
"""Handle history period requests."""
|
||||
|
||||
if date_str:
|
||||
start_date = dt_util.parse_date(date_str)
|
||||
url = '/api/history/period'
|
||||
name = 'api:history:view-period'
|
||||
extra_urls = ['/api/history/period/<date:date>']
|
||||
|
||||
if start_date is None:
|
||||
handler.write_json_message("Error parsing JSON", HTTP_BAD_REQUEST)
|
||||
return
|
||||
def get(self, request, date=None):
|
||||
"""Return history over a period of time."""
|
||||
one_day = timedelta(days=1)
|
||||
|
||||
start_time = dt_util.as_utc(dt_util.start_of_local_day(start_date))
|
||||
else:
|
||||
start_time = dt_util.utcnow() - one_day
|
||||
if date:
|
||||
start_time = dt_util.as_utc(dt_util.start_of_local_day(date))
|
||||
else:
|
||||
start_time = dt_util.utcnow() - one_day
|
||||
|
||||
end_time = start_time + one_day
|
||||
end_time = start_time + one_day
|
||||
entity_id = request.args.get('filter_entity_id')
|
||||
|
||||
entity_id = data.get('filter_entity_id')
|
||||
|
||||
handler.write_json(
|
||||
get_significant_states(start_time, end_time, entity_id).values())
|
||||
return self.json(
|
||||
get_significant_states(start_time, end_time, entity_id).values())
|
||||
|
||||
|
||||
def _is_significant(state):
|
||||
|
@ -1,41 +1,25 @@
|
||||
"""
|
||||
This module provides an API and a HTTP interface for debug purposes.
|
||||
|
||||
For more details about the RESTful API, please refer to the documentation at
|
||||
https://home-assistant.io/developers/api/
|
||||
"""
|
||||
import gzip
|
||||
"""This module provides WSGI application to serve the Home Assistant API."""
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import ssl
|
||||
import mimetypes
|
||||
import threading
|
||||
import time
|
||||
from datetime import timedelta
|
||||
from http import cookies
|
||||
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||
from socketserver import ThreadingMixIn
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
import re
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.bootstrap as bootstrap
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.remote as rem
|
||||
import homeassistant.util as util
|
||||
import homeassistant.util.dt as date_util
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant import util
|
||||
from homeassistant.const import (
|
||||
CONTENT_TYPE_JSON, CONTENT_TYPE_TEXT_PLAIN, HTTP_HEADER_ACCEPT_ENCODING,
|
||||
HTTP_HEADER_CACHE_CONTROL, HTTP_HEADER_CONTENT_ENCODING,
|
||||
HTTP_HEADER_CONTENT_LENGTH, HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_EXPIRES,
|
||||
HTTP_HEADER_HA_AUTH, HTTP_HEADER_VARY,
|
||||
SERVER_PORT, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CACHE_CONTROL,
|
||||
HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
|
||||
HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, HTTP_METHOD_NOT_ALLOWED,
|
||||
HTTP_NOT_FOUND, HTTP_OK, HTTP_UNAUTHORIZED, HTTP_UNPROCESSABLE_ENTITY,
|
||||
ALLOWED_CORS_HEADERS,
|
||||
SERVER_PORT, URL_ROOT, URL_API_EVENT_FORWARD)
|
||||
HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS)
|
||||
from homeassistant.helpers.entity import split_entity_id
|
||||
import homeassistant.util.dt as dt_util
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DOMAIN = "http"
|
||||
REQUIREMENTS = ("eventlet==0.19.0", "static3==0.7.0", "Werkzeug==0.11.5",)
|
||||
|
||||
CONF_API_PASSWORD = "api_password"
|
||||
CONF_SERVER_HOST = "server_host"
|
||||
@ -47,10 +31,7 @@ CONF_CORS_ORIGINS = 'cors_allowed_origins'
|
||||
|
||||
DATA_API_PASSWORD = 'api_password'
|
||||
|
||||
# Throttling time in seconds for expired sessions check
|
||||
SESSION_CLEAR_INTERVAL = timedelta(seconds=20)
|
||||
SESSION_TIMEOUT_SECONDS = 1800
|
||||
SESSION_KEY = 'sessionId'
|
||||
_FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -68,13 +49,32 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
class HideSensitiveFilter(logging.Filter):
|
||||
"""Filter API password calls."""
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
def __init__(self, hass):
|
||||
"""Initialize sensitive data filter."""
|
||||
super().__init__()
|
||||
self.hass = hass
|
||||
|
||||
def filter(self, record):
|
||||
"""Hide sensitive data in messages."""
|
||||
if self.hass.wsgi.api_password is None:
|
||||
return True
|
||||
|
||||
record.msg = record.msg.replace(self.hass.wsgi.api_password, '*******')
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the HTTP API and debug interface."""
|
||||
_LOGGER.addFilter(HideSensitiveFilter(hass))
|
||||
|
||||
conf = config.get(DOMAIN, {})
|
||||
|
||||
api_password = util.convert(conf.get(CONF_API_PASSWORD), str)
|
||||
|
||||
# If no server host is given, accept all incoming requests
|
||||
server_host = conf.get(CONF_SERVER_HOST, '0.0.0.0')
|
||||
server_port = conf.get(CONF_SERVER_PORT, SERVER_PORT)
|
||||
development = str(conf.get(CONF_DEVELOPMENT, "")) == "1"
|
||||
@ -82,22 +82,24 @@ def setup(hass, config):
|
||||
ssl_key = conf.get(CONF_SSL_KEY)
|
||||
cors_origins = conf.get(CONF_CORS_ORIGINS, [])
|
||||
|
||||
try:
|
||||
server = HomeAssistantHTTPServer(
|
||||
(server_host, server_port), RequestHandler, hass, api_password,
|
||||
development, ssl_certificate, ssl_key, cors_origins)
|
||||
except OSError:
|
||||
# If address already in use
|
||||
_LOGGER.exception("Error setting up HTTP server")
|
||||
return False
|
||||
server = HomeAssistantWSGI(
|
||||
hass,
|
||||
development=development,
|
||||
server_host=server_host,
|
||||
server_port=server_port,
|
||||
api_password=api_password,
|
||||
ssl_certificate=ssl_certificate,
|
||||
ssl_key=ssl_key,
|
||||
cors_origins=cors_origins
|
||||
)
|
||||
|
||||
hass.bus.listen_once(
|
||||
ha.EVENT_HOMEASSISTANT_START,
|
||||
lambda event:
|
||||
threading.Thread(target=server.start, daemon=True,
|
||||
name='HTTP-server').start())
|
||||
name='WSGI-server').start())
|
||||
|
||||
hass.http = server
|
||||
hass.wsgi = server
|
||||
hass.config.api = rem.API(server_host if server_host != '0.0.0.0'
|
||||
else util.get_local_ip(),
|
||||
api_password, server_port,
|
||||
@ -106,413 +108,338 @@ def setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
|
||||
"""Handle HTTP requests in a threaded fashion."""
|
||||
def request_class():
|
||||
"""Generate request class.
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
allow_reuse_address = True
|
||||
daemon_threads = True
|
||||
Done in method because of imports.
|
||||
"""
|
||||
from werkzeug.exceptions import BadRequest
|
||||
from werkzeug.wrappers import BaseRequest, AcceptMixin
|
||||
from werkzeug.utils import cached_property
|
||||
|
||||
class Request(BaseRequest, AcceptMixin):
|
||||
"""Base class for incoming requests."""
|
||||
|
||||
@cached_property
|
||||
def json(self):
|
||||
"""Get the result of json.loads if possible."""
|
||||
if not self.data:
|
||||
return None
|
||||
# elif 'json' not in self.environ.get('CONTENT_TYPE', ''):
|
||||
# raise BadRequest('Not a JSON request')
|
||||
try:
|
||||
return json.loads(self.data.decode(
|
||||
self.charset, self.encoding_errors))
|
||||
except (TypeError, ValueError):
|
||||
raise BadRequest('Unable to read JSON request')
|
||||
|
||||
return Request
|
||||
|
||||
|
||||
def routing_map(hass):
|
||||
"""Generate empty routing map with HA validators."""
|
||||
from werkzeug.routing import Map, BaseConverter, ValidationError
|
||||
|
||||
class EntityValidator(BaseConverter):
|
||||
"""Validate entity_id in urls."""
|
||||
|
||||
regex = r"(\w+)\.(\w+)"
|
||||
|
||||
def __init__(self, url_map, exist=True, domain=None):
|
||||
"""Initilalize entity validator."""
|
||||
super().__init__(url_map)
|
||||
self._exist = exist
|
||||
self._domain = domain
|
||||
|
||||
def to_python(self, value):
|
||||
"""Validate entity id."""
|
||||
if self._exist and hass.states.get(value) is None:
|
||||
raise ValidationError()
|
||||
if self._domain is not None and \
|
||||
split_entity_id(value)[0] != self._domain:
|
||||
raise ValidationError()
|
||||
|
||||
return value
|
||||
|
||||
def to_url(self, value):
|
||||
"""Convert entity_id for a url."""
|
||||
return value
|
||||
|
||||
class DateValidator(BaseConverter):
|
||||
"""Validate dates in urls."""
|
||||
|
||||
regex = r'\d{4}-\d{1,2}-\d{1,2}'
|
||||
|
||||
def to_python(self, value):
|
||||
"""Validate and convert date."""
|
||||
parsed = dt_util.parse_date(value)
|
||||
|
||||
if parsed is None:
|
||||
raise ValidationError()
|
||||
|
||||
return parsed
|
||||
|
||||
def to_url(self, value):
|
||||
"""Convert date to url value."""
|
||||
return value.isoformat()
|
||||
|
||||
return Map(converters={
|
||||
'entity': EntityValidator,
|
||||
'date': DateValidator,
|
||||
})
|
||||
|
||||
|
||||
class HomeAssistantWSGI(object):
|
||||
"""WSGI server for Home Assistant."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes, too-many-locals
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, server_address, request_handler_class,
|
||||
hass, api_password, development, ssl_certificate, ssl_key,
|
||||
cors_origins):
|
||||
"""Initialize the server."""
|
||||
super().__init__(server_address, request_handler_class)
|
||||
|
||||
self.server_address = server_address
|
||||
def __init__(self, hass, development, api_password, ssl_certificate,
|
||||
ssl_key, server_host, server_port, cors_origins):
|
||||
"""Initilalize the WSGI Home Assistant server."""
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
Response.mimetype = 'text/html'
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
self.Request = request_class()
|
||||
self.url_map = routing_map(hass)
|
||||
self.views = {}
|
||||
self.hass = hass
|
||||
self.api_password = api_password
|
||||
self.extra_apps = {}
|
||||
self.development = development
|
||||
self.paths = []
|
||||
self.sessions = SessionStore()
|
||||
self.use_ssl = ssl_certificate is not None
|
||||
self.api_password = api_password
|
||||
self.ssl_certificate = ssl_certificate
|
||||
self.ssl_key = ssl_key
|
||||
self.server_host = server_host
|
||||
self.server_port = server_port
|
||||
self.cors_origins = cors_origins
|
||||
|
||||
# We will lazy init this one if needed
|
||||
self.event_forwarder = None
|
||||
|
||||
if development:
|
||||
_LOGGER.info("running http in development mode")
|
||||
def register_view(self, view):
|
||||
"""Register a view with the WSGI server.
|
||||
|
||||
if ssl_certificate is not None:
|
||||
context = ssl.create_default_context(
|
||||
purpose=ssl.Purpose.CLIENT_AUTH)
|
||||
context.load_cert_chain(ssl_certificate, keyfile=ssl_key)
|
||||
self.socket = context.wrap_socket(self.socket, server_side=True)
|
||||
The view argument must be a class that inherits from HomeAssistantView.
|
||||
It is optional to instantiate it before registering; this method will
|
||||
handle it either way.
|
||||
"""
|
||||
from werkzeug.routing import Rule
|
||||
|
||||
if view.name in self.views:
|
||||
_LOGGER.warning("View '%s' is being overwritten", view.name)
|
||||
if isinstance(view, type):
|
||||
# Instantiate the view, if needed
|
||||
view = view(self.hass)
|
||||
|
||||
self.views[view.name] = view
|
||||
|
||||
rule = Rule(view.url, endpoint=view.name)
|
||||
self.url_map.add(rule)
|
||||
for url in view.extra_urls:
|
||||
rule = Rule(url, endpoint=view.name)
|
||||
self.url_map.add(rule)
|
||||
|
||||
def register_redirect(self, url, redirect_to):
|
||||
"""Register a redirect with the server.
|
||||
|
||||
If given this must be either a string or callable. In case of a
|
||||
callable it's called with the url adapter that triggered the match and
|
||||
the values of the URL as keyword arguments and has to return the target
|
||||
for the redirect, otherwise it has to be a string with placeholders in
|
||||
rule syntax.
|
||||
"""
|
||||
from werkzeug.routing import Rule
|
||||
|
||||
self.url_map.add(Rule(url, redirect_to=redirect_to))
|
||||
|
||||
def register_static_path(self, url_root, path, cache_length=31):
|
||||
"""Register a folder to serve as a static path.
|
||||
|
||||
Specify optional cache length of asset in days.
|
||||
"""
|
||||
from static import Cling
|
||||
|
||||
headers = []
|
||||
|
||||
if cache_length and not self.development:
|
||||
# 1 year in seconds
|
||||
cache_time = cache_length * 86400
|
||||
|
||||
headers.append({
|
||||
'prefix': '',
|
||||
HTTP_HEADER_CACHE_CONTROL:
|
||||
"public, max-age={}".format(cache_time)
|
||||
})
|
||||
|
||||
self.register_wsgi_app(url_root, Cling(path, headers=headers))
|
||||
|
||||
def register_wsgi_app(self, url_root, app):
|
||||
"""Register a path to serve a WSGI app."""
|
||||
if url_root in self.extra_apps:
|
||||
_LOGGER.warning("Url root '%s' is being overwritten", url_root)
|
||||
|
||||
self.extra_apps[url_root] = app
|
||||
|
||||
def start(self):
|
||||
"""Start the HTTP server."""
|
||||
def stop_http(event):
|
||||
"""Stop the HTTP server."""
|
||||
self.shutdown()
|
||||
"""Start the wsgi server."""
|
||||
from eventlet import wsgi
|
||||
import eventlet
|
||||
|
||||
self.hass.bus.listen_once(ha.EVENT_HOMEASSISTANT_STOP, stop_http)
|
||||
sock = eventlet.listen((self.server_host, self.server_port))
|
||||
if self.ssl_certificate:
|
||||
sock = eventlet.wrap_ssl(sock, certfile=self.ssl_certificate,
|
||||
keyfile=self.ssl_key, server_side=True)
|
||||
wsgi.server(sock, self, log=_LOGGER)
|
||||
|
||||
protocol = 'https' if self.use_ssl else 'http'
|
||||
def dispatch_request(self, request):
|
||||
"""Handle incoming request."""
|
||||
from werkzeug.exceptions import (
|
||||
MethodNotAllowed, NotFound, BadRequest, Unauthorized,
|
||||
)
|
||||
from werkzeug.routing import RequestRedirect
|
||||
|
||||
_LOGGER.info(
|
||||
"Starting web interface at %s://%s:%d",
|
||||
protocol, self.server_address[0], self.server_address[1])
|
||||
with request:
|
||||
adapter = self.url_map.bind_to_environ(request.environ)
|
||||
try:
|
||||
endpoint, values = adapter.match()
|
||||
return self.views[endpoint].handle_request(request, **values)
|
||||
except RequestRedirect as ex:
|
||||
return ex
|
||||
except (BadRequest, NotFound, MethodNotAllowed,
|
||||
Unauthorized) as ex:
|
||||
resp = ex.get_response(request.environ)
|
||||
if request.accept_mimetypes.accept_json:
|
||||
resp.data = json.dumps({
|
||||
"result": "error",
|
||||
"message": str(ex),
|
||||
})
|
||||
resp.mimetype = "application/json"
|
||||
return resp
|
||||
|
||||
# 31-1-2015: Refactored frontend/api components out of this component
|
||||
# To prevent stuff from breaking, load the two extracted components
|
||||
bootstrap.setup_component(self.hass, 'api')
|
||||
bootstrap.setup_component(self.hass, 'frontend')
|
||||
def base_app(self, environ, start_response):
|
||||
"""WSGI Handler of requests to base app."""
|
||||
request = self.Request(environ)
|
||||
response = self.dispatch_request(request)
|
||||
|
||||
self.serve_forever()
|
||||
if self.cors_origins:
|
||||
cors_check = (environ.get("HTTP_ORIGIN") in self.cors_origins)
|
||||
cors_headers = ", ".join(ALLOWED_CORS_HEADERS)
|
||||
if cors_check:
|
||||
response.headers[HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN] = \
|
||||
environ.get("HTTP_ORIGIN")
|
||||
response.headers[HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS] = \
|
||||
cors_headers
|
||||
|
||||
def register_path(self, method, url, callback, require_auth=True):
|
||||
"""Register a path with the server."""
|
||||
self.paths.append((method, url, callback, require_auth))
|
||||
return response(environ, start_response)
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
"""Redirect built-in log to HA logging."""
|
||||
# pylint: disable=no-self-use
|
||||
_LOGGER.info(fmt, *args)
|
||||
def __call__(self, environ, start_response):
|
||||
"""Handle a request for base app + extra apps."""
|
||||
from werkzeug.wsgi import DispatcherMiddleware
|
||||
|
||||
app = DispatcherMiddleware(self.base_app, self.extra_apps)
|
||||
# Strip out any cachebusting MD5 fingerprints
|
||||
fingerprinted = _FINGERPRINT.match(environ.get('PATH_INFO', ''))
|
||||
if fingerprinted:
|
||||
environ['PATH_INFO'] = "{}.{}".format(*fingerprinted.groups())
|
||||
return app(environ, start_response)
|
||||
|
||||
|
||||
# pylint: disable=too-many-public-methods,too-many-locals
|
||||
class RequestHandler(SimpleHTTPRequestHandler):
|
||||
"""Handle incoming HTTP requests.
|
||||
class HomeAssistantView(object):
|
||||
"""Base view for all views."""
|
||||
|
||||
We extend from SimpleHTTPRequestHandler instead of Base so we
|
||||
can use the guess content type methods.
|
||||
"""
|
||||
extra_urls = []
|
||||
requires_auth = True # Views inheriting from this class can override this
|
||||
|
||||
server_version = "HomeAssistant/1.0"
|
||||
def __init__(self, hass):
|
||||
"""Initilalize the base view."""
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
def __init__(self, req, client_addr, server):
|
||||
"""Constructor, call the base constructor and set up session."""
|
||||
# Track if this was an authenticated request
|
||||
self.authenticated = False
|
||||
SimpleHTTPRequestHandler.__init__(self, req, client_addr, server)
|
||||
self.protocol_version = 'HTTP/1.1'
|
||||
if not hasattr(self, 'url'):
|
||||
class_name = self.__class__.__name__
|
||||
raise AttributeError(
|
||||
'{0} missing required attribute "url"'.format(class_name)
|
||||
)
|
||||
|
||||
def log_message(self, fmt, *arguments):
|
||||
"""Redirect built-in log to HA logging."""
|
||||
if self.server.api_password is None:
|
||||
_LOGGER.info(fmt, *arguments)
|
||||
else:
|
||||
_LOGGER.info(
|
||||
fmt, *(arg.replace(self.server.api_password, '*******')
|
||||
if isinstance(arg, str) else arg for arg in arguments))
|
||||
if not hasattr(self, 'name'):
|
||||
class_name = self.__class__.__name__
|
||||
raise AttributeError(
|
||||
'{0} missing required attribute "name"'.format(class_name)
|
||||
)
|
||||
|
||||
def _handle_request(self, method): # pylint: disable=too-many-branches
|
||||
"""Perform some common checks and call appropriate method."""
|
||||
url = urlparse(self.path)
|
||||
self.hass = hass
|
||||
# pylint: disable=invalid-name
|
||||
self.Response = Response
|
||||
|
||||
# Read query input. parse_qs gives a list for each value, we want last
|
||||
data = {key: data[-1] for key, data in parse_qs(url.query).items()}
|
||||
def handle_request(self, request, **values):
|
||||
"""Handle request to url."""
|
||||
from werkzeug.exceptions import MethodNotAllowed, Unauthorized
|
||||
|
||||
# Did we get post input ?
|
||||
content_length = int(self.headers.get(HTTP_HEADER_CONTENT_LENGTH, 0))
|
||||
try:
|
||||
handler = getattr(self, request.method.lower())
|
||||
except AttributeError:
|
||||
raise MethodNotAllowed
|
||||
|
||||
if content_length:
|
||||
body_content = self.rfile.read(content_length).decode("UTF-8")
|
||||
# Auth code verbose on purpose
|
||||
authenticated = False
|
||||
|
||||
if self.hass.wsgi.api_password is None:
|
||||
authenticated = True
|
||||
|
||||
elif hmac.compare_digest(request.headers.get(HTTP_HEADER_HA_AUTH, ''),
|
||||
self.hass.wsgi.api_password):
|
||||
# A valid auth header has been set
|
||||
authenticated = True
|
||||
|
||||
elif hmac.compare_digest(request.args.get(DATA_API_PASSWORD, ''),
|
||||
self.hass.wsgi.api_password):
|
||||
authenticated = True
|
||||
|
||||
if self.requires_auth and not authenticated:
|
||||
raise Unauthorized()
|
||||
|
||||
request.authenticated = authenticated
|
||||
|
||||
result = handler(request, **values)
|
||||
|
||||
if isinstance(result, self.Response):
|
||||
# The method handler returned a ready-made Response, how nice of it
|
||||
return result
|
||||
|
||||
status_code = 200
|
||||
|
||||
if isinstance(result, tuple):
|
||||
result, status_code = result
|
||||
|
||||
return self.Response(result, status=status_code)
|
||||
|
||||
def json(self, result, status_code=200):
|
||||
"""Return a JSON response."""
|
||||
msg = json.dumps(
|
||||
result,
|
||||
sort_keys=True,
|
||||
cls=rem.JSONEncoder
|
||||
).encode('UTF-8')
|
||||
return self.Response(msg, mimetype="application/json",
|
||||
status=status_code)
|
||||
|
||||
def json_message(self, error, status_code=200):
|
||||
"""Return a JSON message response."""
|
||||
return self.json({'message': error}, status_code)
|
||||
|
||||
def file(self, request, fil, mimetype=None):
|
||||
"""Return a file."""
|
||||
from werkzeug.wsgi import wrap_file
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
if isinstance(fil, str):
|
||||
if mimetype is None:
|
||||
mimetype = mimetypes.guess_type(fil)[0]
|
||||
|
||||
try:
|
||||
data.update(json.loads(body_content))
|
||||
except (TypeError, ValueError):
|
||||
# TypeError if JSON object is not a dict
|
||||
# ValueError if we could not parse JSON
|
||||
_LOGGER.exception(
|
||||
"Exception parsing JSON: %s", body_content)
|
||||
self.write_json_message(
|
||||
"Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
fil = open(fil)
|
||||
except IOError:
|
||||
raise NotFound()
|
||||
|
||||
if self.verify_session():
|
||||
# The user has a valid session already
|
||||
self.authenticated = True
|
||||
elif self.server.api_password is None:
|
||||
# No password is set, so everyone is authenticated
|
||||
self.authenticated = True
|
||||
elif hmac.compare_digest(self.headers.get(HTTP_HEADER_HA_AUTH, ''),
|
||||
self.server.api_password):
|
||||
# A valid auth header has been set
|
||||
self.authenticated = True
|
||||
elif hmac.compare_digest(data.get(DATA_API_PASSWORD, ''),
|
||||
self.server.api_password):
|
||||
# A valid password has been specified
|
||||
self.authenticated = True
|
||||
else:
|
||||
self.authenticated = False
|
||||
|
||||
# we really shouldn't need to forward the password from here
|
||||
if url.path not in [URL_ROOT, URL_API_EVENT_FORWARD]:
|
||||
data.pop(DATA_API_PASSWORD, None)
|
||||
|
||||
if '_METHOD' in data:
|
||||
method = data.pop('_METHOD')
|
||||
|
||||
# Var to keep track if we found a path that matched a handler but
|
||||
# the method was different
|
||||
path_matched_but_not_method = False
|
||||
|
||||
# Var to hold the handler for this path and method if found
|
||||
handle_request_method = False
|
||||
require_auth = True
|
||||
|
||||
# Check every handler to find matching result
|
||||
for t_method, t_path, t_handler, t_auth in self.server.paths:
|
||||
# we either do string-comparison or regular expression matching
|
||||
# pylint: disable=maybe-no-member
|
||||
if isinstance(t_path, str):
|
||||
path_match = url.path == t_path
|
||||
else:
|
||||
path_match = t_path.match(url.path)
|
||||
|
||||
if path_match and method == t_method:
|
||||
# Call the method
|
||||
handle_request_method = t_handler
|
||||
require_auth = t_auth
|
||||
break
|
||||
|
||||
elif path_match:
|
||||
path_matched_but_not_method = True
|
||||
|
||||
# Did we find a handler for the incoming request?
|
||||
if handle_request_method:
|
||||
# For some calls we need a valid password
|
||||
msg = "API password missing or incorrect."
|
||||
if require_auth and not self.authenticated:
|
||||
self.write_json_message(msg, HTTP_UNAUTHORIZED)
|
||||
_LOGGER.warning('%s Source IP: %s',
|
||||
msg,
|
||||
self.client_address[0])
|
||||
return
|
||||
|
||||
handle_request_method(self, path_match, data)
|
||||
|
||||
elif path_matched_but_not_method:
|
||||
self.send_response(HTTP_METHOD_NOT_ALLOWED)
|
||||
self.end_headers()
|
||||
|
||||
else:
|
||||
self.send_response(HTTP_NOT_FOUND)
|
||||
self.end_headers()
|
||||
|
||||
def do_HEAD(self): # pylint: disable=invalid-name
|
||||
"""HEAD request handler."""
|
||||
self._handle_request('HEAD')
|
||||
|
||||
def do_GET(self): # pylint: disable=invalid-name
|
||||
"""GET request handler."""
|
||||
self._handle_request('GET')
|
||||
|
||||
def do_POST(self): # pylint: disable=invalid-name
|
||||
"""POST request handler."""
|
||||
self._handle_request('POST')
|
||||
|
||||
def do_PUT(self): # pylint: disable=invalid-name
|
||||
"""PUT request handler."""
|
||||
self._handle_request('PUT')
|
||||
|
||||
def do_DELETE(self): # pylint: disable=invalid-name
|
||||
"""DELETE request handler."""
|
||||
self._handle_request('DELETE')
|
||||
|
||||
def write_json_message(self, message, status_code=HTTP_OK):
|
||||
"""Helper method to return a message to the caller."""
|
||||
self.write_json({'message': message}, status_code=status_code)
|
||||
|
||||
def write_json(self, data=None, status_code=HTTP_OK, location=None):
|
||||
"""Helper method to return JSON to the caller."""
|
||||
json_data = json.dumps(data, indent=4, sort_keys=True,
|
||||
cls=rem.JSONEncoder).encode('UTF-8')
|
||||
self.send_response(status_code)
|
||||
|
||||
if location:
|
||||
self.send_header('Location', location)
|
||||
|
||||
self.set_session_cookie_header()
|
||||
|
||||
self.write_content(json_data, CONTENT_TYPE_JSON)
|
||||
|
||||
def write_text(self, message, status_code=HTTP_OK):
|
||||
"""Helper method to return a text message to the caller."""
|
||||
msg_data = message.encode('UTF-8')
|
||||
self.send_response(status_code)
|
||||
self.set_session_cookie_header()
|
||||
|
||||
self.write_content(msg_data, CONTENT_TYPE_TEXT_PLAIN)
|
||||
|
||||
def write_file(self, path, cache_headers=True):
|
||||
"""Return a file to the user."""
|
||||
try:
|
||||
with open(path, 'rb') as inp:
|
||||
self.write_file_pointer(self.guess_type(path), inp,
|
||||
cache_headers)
|
||||
|
||||
except IOError:
|
||||
self.send_response(HTTP_NOT_FOUND)
|
||||
self.end_headers()
|
||||
_LOGGER.exception("Unable to serve %s", path)
|
||||
|
||||
def write_file_pointer(self, content_type, inp, cache_headers=True):
|
||||
"""Helper function to write a file pointer to the user."""
|
||||
self.send_response(HTTP_OK)
|
||||
|
||||
if cache_headers:
|
||||
self.set_cache_header()
|
||||
self.set_session_cookie_header()
|
||||
|
||||
self.write_content(inp.read(), content_type)
|
||||
|
||||
def write_content(self, content, content_type=None):
|
||||
"""Helper method to write content bytes to output stream."""
|
||||
if content_type is not None:
|
||||
self.send_header(HTTP_HEADER_CONTENT_TYPE, content_type)
|
||||
|
||||
if 'gzip' in self.headers.get(HTTP_HEADER_ACCEPT_ENCODING, ''):
|
||||
content = gzip.compress(content)
|
||||
|
||||
self.send_header(HTTP_HEADER_CONTENT_ENCODING, "gzip")
|
||||
self.send_header(HTTP_HEADER_VARY, HTTP_HEADER_ACCEPT_ENCODING)
|
||||
|
||||
self.send_header(HTTP_HEADER_CONTENT_LENGTH, str(len(content)))
|
||||
|
||||
cors_check = (self.headers.get("Origin") in self.server.cors_origins)
|
||||
|
||||
cors_headers = ", ".join(ALLOWED_CORS_HEADERS)
|
||||
|
||||
if self.server.cors_origins and cors_check:
|
||||
self.send_header(HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
|
||||
self.headers.get("Origin"))
|
||||
self.send_header(HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS,
|
||||
cors_headers)
|
||||
self.end_headers()
|
||||
|
||||
if self.command == 'HEAD':
|
||||
return
|
||||
|
||||
self.wfile.write(content)
|
||||
|
||||
def set_cache_header(self):
|
||||
"""Add cache headers if not in development."""
|
||||
if self.server.development:
|
||||
return
|
||||
|
||||
# 1 year in seconds
|
||||
cache_time = 365 * 86400
|
||||
|
||||
self.send_header(
|
||||
HTTP_HEADER_CACHE_CONTROL,
|
||||
"public, max-age={}".format(cache_time))
|
||||
self.send_header(
|
||||
HTTP_HEADER_EXPIRES,
|
||||
self.date_time_string(time.time()+cache_time))
|
||||
|
||||
def set_session_cookie_header(self):
|
||||
"""Add the header for the session cookie and return session ID."""
|
||||
if not self.authenticated:
|
||||
return None
|
||||
|
||||
session_id = self.get_cookie_session_id()
|
||||
|
||||
if session_id is not None:
|
||||
self.server.sessions.extend_validation(session_id)
|
||||
return session_id
|
||||
|
||||
self.send_header(
|
||||
'Set-Cookie',
|
||||
'{}={}'.format(SESSION_KEY, self.server.sessions.create())
|
||||
)
|
||||
|
||||
return session_id
|
||||
|
||||
def verify_session(self):
|
||||
"""Verify that we are in a valid session."""
|
||||
return self.get_cookie_session_id() is not None
|
||||
|
||||
def get_cookie_session_id(self):
|
||||
"""Extract the current session ID from the cookie.
|
||||
|
||||
Return None if not set or invalid.
|
||||
"""
|
||||
if 'Cookie' not in self.headers:
|
||||
return None
|
||||
|
||||
cookie = cookies.SimpleCookie()
|
||||
try:
|
||||
cookie.load(self.headers["Cookie"])
|
||||
except cookies.CookieError:
|
||||
return None
|
||||
|
||||
morsel = cookie.get(SESSION_KEY)
|
||||
|
||||
if morsel is None:
|
||||
return None
|
||||
|
||||
session_id = cookie[SESSION_KEY].value
|
||||
|
||||
if self.server.sessions.is_valid(session_id):
|
||||
return session_id
|
||||
|
||||
return None
|
||||
|
||||
def destroy_session(self):
|
||||
"""Destroy the session."""
|
||||
session_id = self.get_cookie_session_id()
|
||||
|
||||
if session_id is None:
|
||||
return
|
||||
|
||||
self.send_header('Set-Cookie', '')
|
||||
self.server.sessions.destroy(session_id)
|
||||
|
||||
|
||||
def session_valid_time():
|
||||
"""Time till when a session will be valid."""
|
||||
return date_util.utcnow() + timedelta(seconds=SESSION_TIMEOUT_SECONDS)
|
||||
|
||||
|
||||
class SessionStore(object):
|
||||
"""Responsible for storing and retrieving HTTP sessions."""
|
||||
|
||||
def __init__(self):
|
||||
"""Setup the session store."""
|
||||
self._sessions = {}
|
||||
self._lock = threading.RLock()
|
||||
|
||||
@util.Throttle(SESSION_CLEAR_INTERVAL)
|
||||
def _remove_expired(self):
|
||||
"""Remove any expired sessions."""
|
||||
now = date_util.utcnow()
|
||||
for key in [key for key, valid_time in self._sessions.items()
|
||||
if valid_time < now]:
|
||||
self._sessions.pop(key)
|
||||
|
||||
def is_valid(self, key):
|
||||
"""Return True if a valid session is given."""
|
||||
with self._lock:
|
||||
self._remove_expired()
|
||||
|
||||
return (key in self._sessions and
|
||||
self._sessions[key] > date_util.utcnow())
|
||||
|
||||
def extend_validation(self, key):
|
||||
"""Extend a session validation time."""
|
||||
with self._lock:
|
||||
if key not in self._sessions:
|
||||
return
|
||||
self._sessions[key] = session_valid_time()
|
||||
|
||||
def destroy(self, key):
|
||||
"""Destroy a session by key."""
|
||||
with self._lock:
|
||||
self._sessions.pop(key, None)
|
||||
|
||||
def create(self):
|
||||
"""Create a new session."""
|
||||
with self._lock:
|
||||
session_id = util.get_random_string(20)
|
||||
|
||||
while session_id in self._sessions:
|
||||
session_id = util.get_random_string(20)
|
||||
|
||||
self._sessions[session_id] = session_valid_time()
|
||||
|
||||
return session_id
|
||||
return self.Response(wrap_file(request.environ, fil),
|
||||
mimetype=mimetype, direct_passthrough=True)
|
||||
|
@ -468,12 +468,12 @@ class HvacDevice(Entity):
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return convert(7, TEMP_CELCIUS, self.unit_of_measurement)
|
||||
return convert(19, TEMP_CELCIUS, self.unit_of_measurement)
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return convert(35, TEMP_CELCIUS, self.unit_of_measurement)
|
||||
return convert(30, TEMP_CELCIUS, self.unit_of_measurement)
|
||||
|
||||
@property
|
||||
def min_humidity(self):
|
||||
|
@ -233,13 +233,3 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
||||
class_id=COMMAND_CLASS_CONFIGURATION).values():
|
||||
if value.command_class == 112 and value.index == 33:
|
||||
value.data = int(swing_mode)
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return self._convert_for_display(19)
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return self._convert_for_display(30)
|
||||
|
@ -11,7 +11,6 @@ from homeassistant.const import (
|
||||
ATTR_DISCOVERED, ATTR_SERVICE, CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME,
|
||||
EVENT_PLATFORM_DISCOVERED)
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DOMAIN = "insteon_hub"
|
||||
@ -53,43 +52,3 @@ def setup(hass, config):
|
||||
EVENT_PLATFORM_DISCOVERED,
|
||||
{ATTR_SERVICE: discovery, ATTR_DISCOVERED: {}})
|
||||
return True
|
||||
|
||||
|
||||
class InsteonToggleDevice(ToggleEntity):
|
||||
"""An abstract Class for an Insteon node."""
|
||||
|
||||
def __init__(self, node):
|
||||
"""Initialize the device."""
|
||||
self.node = node
|
||||
self._value = 0
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the the name of the node."""
|
||||
return self.node.DeviceName
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this insteon node."""
|
||||
return self.node.DeviceID
|
||||
|
||||
def update(self):
|
||||
"""Update state of the sensor."""
|
||||
resp = self.node.send_command('get_status', wait=True)
|
||||
try:
|
||||
self._value = resp['response']['level']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the boolean response if the node is on."""
|
||||
return self._value != 0
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn device on."""
|
||||
self.node.send_command('on')
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn device off."""
|
||||
self.node.send_command('off')
|
||||
|
@ -16,7 +16,7 @@ from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DOMAIN = "isy994"
|
||||
REQUIREMENTS = ['PyISY==1.0.5']
|
||||
REQUIREMENTS = ['PyISY==1.0.6']
|
||||
DISCOVER_LIGHTS = "isy994.lights"
|
||||
DISCOVER_SWITCHES = "isy994.switches"
|
||||
DISCOVER_SENSORS = "isy994.sensors"
|
||||
|
92
homeassistant/components/light/enocean.py
Normal file
92
homeassistant/components/light/enocean.py
Normal file
@ -0,0 +1,92 @@
|
||||
"""
|
||||
Support for EnOcean light sources.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.enocean/
|
||||
"""
|
||||
|
||||
import logging
|
||||
import math
|
||||
|
||||
from homeassistant.components.light import Light, ATTR_BRIGHTNESS
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.components import enocean
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ["enocean"]
|
||||
|
||||
CONF_ID = "id"
|
||||
CONF_SENDER_ID = "sender_id"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the EnOcean light platform."""
|
||||
sender_id = config.get(CONF_SENDER_ID, None)
|
||||
devname = config.get(CONF_NAME, "Enocean actuator")
|
||||
dev_id = config.get(CONF_ID, [0x00, 0x00, 0x00, 0x00])
|
||||
|
||||
add_devices([EnOceanLight(sender_id, devname, dev_id)])
|
||||
|
||||
|
||||
class EnOceanLight(enocean.EnOceanDevice, Light):
|
||||
"""Representation of an EnOcean light source."""
|
||||
|
||||
def __init__(self, sender_id, devname, dev_id):
|
||||
"""Initialize the EnOcean light source."""
|
||||
enocean.EnOceanDevice.__init__(self)
|
||||
self._on_state = False
|
||||
self._brightness = 50
|
||||
self._sender_id = sender_id
|
||||
self.dev_id = dev_id
|
||||
self._devname = devname
|
||||
self.stype = "dimmer"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device if any."""
|
||||
return self._devname
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Brightness of the light.
|
||||
|
||||
This method is optional. Removing it indicates to Home Assistant
|
||||
that brightness is not supported for this light.
|
||||
"""
|
||||
return self._brightness
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""If light is on."""
|
||||
return self._on_state
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the light source on or sets a specific dimmer value."""
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||
if brightness is not None:
|
||||
self._brightness = brightness
|
||||
|
||||
bval = math.floor(self._brightness / 256.0 * 100.0)
|
||||
if bval == 0:
|
||||
bval = 1
|
||||
command = [0xa5, 0x02, bval, 0x01, 0x09]
|
||||
command.extend(self._sender_id)
|
||||
command.extend([0x00])
|
||||
self.send_command(command, [], 0x01)
|
||||
self._on_state = True
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the light source off."""
|
||||
command = [0xa5, 0x02, 0x00, 0x01, 0x09]
|
||||
command.extend(self._sender_id)
|
||||
command.extend([0x00])
|
||||
self.send_command(command, [], 0x01)
|
||||
self._on_state = False
|
||||
|
||||
def value_changed(self, val):
|
||||
"""Update the internal state of this device in HA."""
|
||||
self._brightness = math.floor(val / 100.0 * 256.0)
|
||||
self._on_state = bool(val != 0)
|
||||
self.update_ha_state()
|
@ -4,7 +4,8 @@ Support for Insteon Hub lights.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/insteon_hub/
|
||||
"""
|
||||
from homeassistant.components.insteon_hub import INSTEON, InsteonToggleDevice
|
||||
from homeassistant.components.insteon_hub import INSTEON
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, Light
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@ -16,3 +17,53 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if device.DeviceCategory == "Dimmable Lighting Control":
|
||||
devs.append(InsteonToggleDevice(device))
|
||||
add_devices(devs)
|
||||
|
||||
|
||||
class InsteonToggleDevice(Light):
|
||||
"""An abstract Class for an Insteon node."""
|
||||
|
||||
def __init__(self, node):
|
||||
"""Initialize the device."""
|
||||
self.node = node
|
||||
self._value = 0
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the the name of the node."""
|
||||
return self.node.DeviceName
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this insteon node."""
|
||||
return self.node.DeviceID
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
return self._value / 100 * 255
|
||||
|
||||
def update(self):
|
||||
"""Update state of the sensor."""
|
||||
resp = self.node.send_command('get_status', wait=True)
|
||||
try:
|
||||
self._value = resp['response']['level']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the boolean response if the node is on."""
|
||||
return self._value != 0
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn device on."""
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
self._value = kwargs[ATTR_BRIGHTNESS] / 255 * 100
|
||||
self.node.send_command('on', self._value)
|
||||
else:
|
||||
self._value = 100
|
||||
self.node.send_command('on')
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn device off."""
|
||||
self.node.send_command('off')
|
||||
|
165
homeassistant/components/light/osramlightify.py
Normal file
165
homeassistant/components/light/osramlightify.py
Normal file
@ -0,0 +1,165 @@
|
||||
"""
|
||||
Support for Osram Lightify.
|
||||
|
||||
Uses: https://github.com/aneumeier/python-lightify for the Osram light
|
||||
interface.
|
||||
|
||||
In order to use the platform just add the following to the configuration.yaml:
|
||||
|
||||
light:
|
||||
platform: osramlightify
|
||||
host: <hostname_or_ip>
|
||||
|
||||
Todo:
|
||||
Add support for Non RGBW lights.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import socket
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant import util
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.components.light import (
|
||||
Light,
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP,
|
||||
ATTR_RGB_COLOR,
|
||||
ATTR_TRANSITION
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['lightify==1.0.3']
|
||||
|
||||
TEMP_MIN = 2000 # lightify minimum temperature
|
||||
TEMP_MAX = 6500 # lightify maximum temperature
|
||||
TEMP_MIN_HASS = 154 # home assistant minimum temperature
|
||||
TEMP_MAX_HASS = 500 # home assistant maximum temperature
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Find and return lights."""
|
||||
import lightify
|
||||
host = config.get(CONF_HOST)
|
||||
if host:
|
||||
try:
|
||||
bridge = lightify.Lightify(host)
|
||||
except socket.error as err:
|
||||
msg = 'Error connecting to bridge: {} due to: {}'.format(host,
|
||||
str(err))
|
||||
_LOGGER.exception(msg)
|
||||
return False
|
||||
setup_bridge(bridge, add_devices_callback)
|
||||
else:
|
||||
_LOGGER.error('No host found in configuration')
|
||||
return False
|
||||
|
||||
|
||||
def setup_bridge(bridge, add_devices_callback):
|
||||
"""Setup the Lightify bridge."""
|
||||
lights = {}
|
||||
|
||||
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
||||
def update_lights():
|
||||
"""Update the lights objects with latest info from bridge."""
|
||||
bridge.update_all_light_status()
|
||||
|
||||
new_lights = []
|
||||
|
||||
for (light_id, light) in bridge.lights().items():
|
||||
if light_id not in lights:
|
||||
osram_light = OsramLightifyLight(light_id, light,
|
||||
update_lights)
|
||||
|
||||
lights[light_id] = osram_light
|
||||
new_lights.append(osram_light)
|
||||
else:
|
||||
lights[light_id].light = light
|
||||
|
||||
if new_lights:
|
||||
add_devices_callback(new_lights)
|
||||
|
||||
update_lights()
|
||||
|
||||
|
||||
class OsramLightifyLight(Light):
|
||||
"""Defines an Osram Lightify Light."""
|
||||
|
||||
def __init__(self, light_id, light, update_lights):
|
||||
"""Initialize the light."""
|
||||
self._light = light
|
||||
self._light_id = light_id
|
||||
self.update_lights = update_lights
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device if any."""
|
||||
return self._light.name()
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
"""Last RGB color value set."""
|
||||
return self._light.rgb()
|
||||
|
||||
@property
|
||||
def color_temp(self):
|
||||
"""Return the color temperature."""
|
||||
o_temp = self._light.temp()
|
||||
temperature = int(TEMP_MIN_HASS + (TEMP_MAX_HASS - TEMP_MIN_HASS) *
|
||||
(o_temp - TEMP_MIN) / (TEMP_MAX - TEMP_MIN))
|
||||
return temperature
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Brightness of this light between 0..255."""
|
||||
return int(self._light.lum() * 2.55)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Update Status to True if device is on."""
|
||||
self.update_lights()
|
||||
_LOGGER.debug("is_on light state for light: %s is: %s",
|
||||
self._light.name(), self._light.on())
|
||||
return self._light.on()
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
brightness = 100
|
||||
if self.brightness:
|
||||
brightness = int(self.brightness / 2.55)
|
||||
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
fade = kwargs[ATTR_TRANSITION] * 10
|
||||
else:
|
||||
fade = 0
|
||||
|
||||
if ATTR_RGB_COLOR in kwargs:
|
||||
red, green, blue = kwargs[ATTR_RGB_COLOR]
|
||||
self._light.set_rgb(red, green, blue, fade)
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
brightness = int(kwargs[ATTR_BRIGHTNESS] / 2.55)
|
||||
|
||||
if ATTR_COLOR_TEMP in kwargs:
|
||||
color_t = kwargs[ATTR_COLOR_TEMP]
|
||||
kelvin = int(((TEMP_MAX - TEMP_MIN) * (color_t - TEMP_MIN_HASS) /
|
||||
(TEMP_MAX_HASS - TEMP_MIN_HASS)) + TEMP_MIN)
|
||||
self._light.set_temperature(kelvin, fade)
|
||||
|
||||
self._light.set_luminance(brightness, fade)
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the device off."""
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
fade = kwargs[ATTR_TRANSITION] * 10
|
||||
else:
|
||||
fade = 0
|
||||
self._light.set_luminance(0, fade)
|
||||
self.update_ha_state()
|
||||
|
||||
def update(self):
|
||||
"""Synchronize state with bridge."""
|
||||
self.update_lights(no_throttle=True)
|
78
homeassistant/components/lirc.py
Normal file
78
homeassistant/components/lirc.py
Normal file
@ -0,0 +1,78 @@
|
||||
"""
|
||||
LIRC interface to receive signals from a infrared remote control.
|
||||
|
||||
This sensor will momentarily set state to various values as defined
|
||||
in the .lintrc file which can be interpreted in home-assistant to
|
||||
trigger various actions.
|
||||
|
||||
Sending signals to other IR receivers can be accomplished with the
|
||||
shell_command component and the irsend command for now.
|
||||
"""
|
||||
# pylint: disable=import-error
|
||||
import threading
|
||||
import time
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP,
|
||||
EVENT_HOMEASSISTANT_START)
|
||||
|
||||
DOMAIN = "lirc"
|
||||
REQUIREMENTS = ['python-lirc==1.2.1']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
ICON = 'mdi:remote'
|
||||
EVENT_IR_COMMAND_RECEIVED = 'ir_command_received'
|
||||
BUTTON_NAME = 'button_name'
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup LIRC capability."""
|
||||
import lirc
|
||||
|
||||
# blocking=True gives unexpected behavior (multiple responses for 1 press)
|
||||
# also by not blocking, we allow hass to shut down the thread gracefully
|
||||
# on exit.
|
||||
lirc.init('home-assistant', blocking=False)
|
||||
lirc_interface = LircInterface(hass)
|
||||
|
||||
def _start_lirc(_event):
|
||||
lirc_interface.start()
|
||||
|
||||
def _stop_lirc(_event):
|
||||
lirc_interface.stopped.set()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_lirc)
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _stop_lirc)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class LircInterface(threading.Thread):
|
||||
"""
|
||||
This interfaces with the lirc daemon to read IR commands.
|
||||
|
||||
When using lirc in blocking mode, sometimes repeated commands get produced
|
||||
in the next read of a command so we use a thread here to just wait
|
||||
around until a non-empty response is obtained from lirc.
|
||||
"""
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Construct a LIRC interface object."""
|
||||
threading.Thread.__init__(self)
|
||||
self.daemon = True
|
||||
self.stopped = threading.Event()
|
||||
self.hass = hass
|
||||
|
||||
def run(self):
|
||||
"""Main loop of LIRC interface thread."""
|
||||
import lirc
|
||||
while not self.stopped.isSet():
|
||||
code = lirc.nextcode() # list; empty if no buttons pressed
|
||||
# interpret result from python-lirc
|
||||
if code:
|
||||
code = code[0]
|
||||
_LOGGER.info('Got new LIRC code %s', code)
|
||||
self.hass.bus.fire(EVENT_IR_COMMAND_RECEIVED,
|
||||
{BUTTON_NAME: code})
|
||||
else:
|
||||
time.sleep(0.2)
|
||||
_LOGGER.info('LIRC interface thread stopped')
|
@ -15,12 +15,13 @@ import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components import recorder, sun
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
|
||||
HTTP_BAD_REQUEST, STATE_NOT_HOME, STATE_OFF, STATE_ON)
|
||||
STATE_NOT_HOME, STATE_OFF, STATE_ON)
|
||||
from homeassistant.core import DOMAIN as HA_DOMAIN
|
||||
from homeassistant.core import State
|
||||
from homeassistant.helpers.entity import split_entity_id
|
||||
from homeassistant.helpers import template
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
DOMAIN = "logbook"
|
||||
DEPENDENCIES = ['recorder', 'http']
|
||||
@ -76,34 +77,34 @@ def setup(hass, config):
|
||||
message = template.render(hass, message)
|
||||
log_entry(hass, name, message, domain, entity_id)
|
||||
|
||||
hass.http.register_path('GET', URL_LOGBOOK, _handle_get_logbook)
|
||||
hass.wsgi.register_view(LogbookView)
|
||||
|
||||
hass.services.register(DOMAIN, 'log', log_message,
|
||||
schema=LOG_MESSAGE_SCHEMA)
|
||||
return True
|
||||
|
||||
|
||||
def _handle_get_logbook(handler, path_match, data):
|
||||
"""Return logbook entries."""
|
||||
date_str = path_match.group('date')
|
||||
class LogbookView(HomeAssistantView):
|
||||
"""Handle logbook view requests."""
|
||||
|
||||
if date_str:
|
||||
start_date = dt_util.parse_date(date_str)
|
||||
url = '/api/logbook'
|
||||
name = 'api:logbook'
|
||||
extra_urls = ['/api/logbook/<date:date>']
|
||||
|
||||
if start_date is None:
|
||||
handler.write_json_message("Error parsing JSON", HTTP_BAD_REQUEST)
|
||||
return
|
||||
def get(self, request, date=None):
|
||||
"""Retrieve logbook entries."""
|
||||
if date:
|
||||
start_day = dt_util.start_of_local_day(date)
|
||||
else:
|
||||
start_day = dt_util.start_of_local_day()
|
||||
|
||||
start_day = dt_util.start_of_local_day(start_date)
|
||||
else:
|
||||
start_day = dt_util.start_of_local_day()
|
||||
end_day = start_day + timedelta(days=1)
|
||||
|
||||
end_day = start_day + timedelta(days=1)
|
||||
events = recorder.query_events(
|
||||
QUERY_EVENTS_BETWEEN,
|
||||
(dt_util.as_utc(start_day), dt_util.as_utc(end_day)))
|
||||
|
||||
events = recorder.query_events(
|
||||
QUERY_EVENTS_BETWEEN,
|
||||
(dt_util.as_utc(start_day), dt_util.as_utc(end_day)))
|
||||
|
||||
handler.write_json(humanify(events))
|
||||
return self.json(humanify(events))
|
||||
|
||||
|
||||
class Entry(object):
|
||||
|
@ -10,7 +10,7 @@ import urllib
|
||||
from homeassistant.components.media_player import (
|
||||
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
|
||||
SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP,
|
||||
MediaPlayerDevice)
|
||||
SUPPORT_TURN_OFF, MediaPlayerDevice)
|
||||
from homeassistant.const import (
|
||||
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING)
|
||||
|
||||
@ -36,7 +36,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
url,
|
||||
auth=(
|
||||
config.get('user', ''),
|
||||
config.get('password', ''))),
|
||||
config.get('password', '')),
|
||||
turn_off_action=config.get('turn_off_action', 'none')),
|
||||
])
|
||||
|
||||
|
||||
@ -44,7 +45,8 @@ class KodiDevice(MediaPlayerDevice):
|
||||
"""Representation of a XBMC/Kodi device."""
|
||||
|
||||
# pylint: disable=too-many-public-methods, abstract-method
|
||||
def __init__(self, name, url, auth=None):
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
def __init__(self, name, url, auth=None, turn_off_action=None):
|
||||
"""Initialize the Kodi device."""
|
||||
import jsonrpc_requests
|
||||
self._name = name
|
||||
@ -52,6 +54,7 @@ class KodiDevice(MediaPlayerDevice):
|
||||
self._server = jsonrpc_requests.Server(
|
||||
'{}/jsonrpc'.format(self._url),
|
||||
auth=auth)
|
||||
self._turn_off_action = turn_off_action
|
||||
self._players = list()
|
||||
self._properties = None
|
||||
self._item = None
|
||||
@ -181,11 +184,29 @@ class KodiDevice(MediaPlayerDevice):
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
"""Flag of media commands that are supported."""
|
||||
return SUPPORT_KODI
|
||||
supported_media_commands = SUPPORT_KODI
|
||||
|
||||
if self._turn_off_action in [
|
||||
'quit', 'hibernate', 'suspend', 'reboot', 'shutdown']:
|
||||
supported_media_commands |= SUPPORT_TURN_OFF
|
||||
|
||||
return supported_media_commands
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn off media player."""
|
||||
self._server.System.Shutdown()
|
||||
"""Execute turn_off_action to turn off media player."""
|
||||
if self._turn_off_action == 'quit':
|
||||
self._server.Application.Quit()
|
||||
elif self._turn_off_action == 'hibernate':
|
||||
self._server.System.Hibernate()
|
||||
elif self._turn_off_action == 'suspend':
|
||||
self._server.System.Suspend()
|
||||
elif self._turn_off_action == 'reboot':
|
||||
self._server.System.Reboot()
|
||||
elif self._turn_off_action == 'shutdown':
|
||||
self._server.System.Shutdown()
|
||||
else:
|
||||
_LOGGER.warning('turn_off requested but turn_off_action is none')
|
||||
|
||||
self.update_ha_state()
|
||||
|
||||
def volume_up(self):
|
||||
|
@ -37,7 +37,8 @@ PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): "lg_netcast",
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_ACCESS_TOKEN): vol.All(cv.string, vol.Length(max=6)),
|
||||
vol.Optional(CONF_ACCESS_TOKEN, default=None):
|
||||
vol.All(cv.string, vol.Length(max=6)),
|
||||
})
|
||||
|
||||
|
||||
|
@ -66,13 +66,18 @@ class RokuDevice(MediaPlayerDevice):
|
||||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
self.roku_name = "roku_" + self.roku.device_info.sernum
|
||||
self.ip_address = self.roku.host
|
||||
self.channels = self.get_source_list()
|
||||
import requests.exceptions
|
||||
|
||||
if self.roku.current_app is not None:
|
||||
self.current_app = self.roku.current_app
|
||||
else:
|
||||
try:
|
||||
self.roku_name = "roku_" + self.roku.device_info.sernum
|
||||
self.ip_address = self.roku.host
|
||||
self.channels = self.get_source_list()
|
||||
|
||||
if self.roku.current_app is not None:
|
||||
self.current_app = self.roku.current_app
|
||||
else:
|
||||
self.current_app = None
|
||||
except requests.exceptions.ConnectionError:
|
||||
self.current_app = None
|
||||
|
||||
def get_source_list(self):
|
||||
@ -92,6 +97,9 @@ class RokuDevice(MediaPlayerDevice):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self.current_app is None:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
if self.current_app.name in ["Power Saver", "Default screensaver"]:
|
||||
return STATE_IDLE
|
||||
elif self.current_app.name == "Roku":
|
||||
@ -137,17 +145,20 @@ class RokuDevice(MediaPlayerDevice):
|
||||
@property
|
||||
def app_name(self):
|
||||
"""Name of the current running app."""
|
||||
return self.current_app.name
|
||||
if self.current_app is not None:
|
||||
return self.current_app.name
|
||||
|
||||
@property
|
||||
def app_id(self):
|
||||
"""Return the ID of the current running app."""
|
||||
return self.current_app.id
|
||||
if self.current_app is not None:
|
||||
return self.current_app.id
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
"""Return the current input source."""
|
||||
return self.current_app.name
|
||||
if self.current_app is not None:
|
||||
return self.current_app.name
|
||||
|
||||
@property
|
||||
def source_list(self):
|
||||
@ -156,32 +167,39 @@ class RokuDevice(MediaPlayerDevice):
|
||||
|
||||
def media_play_pause(self):
|
||||
"""Send play/pause command."""
|
||||
self.roku.play()
|
||||
if self.current_app is not None:
|
||||
self.roku.play()
|
||||
|
||||
def media_previous_track(self):
|
||||
"""Send previous track command."""
|
||||
self.roku.reverse()
|
||||
if self.current_app is not None:
|
||||
self.roku.reverse()
|
||||
|
||||
def media_next_track(self):
|
||||
"""Send next track command."""
|
||||
self.roku.forward()
|
||||
if self.current_app is not None:
|
||||
self.roku.forward()
|
||||
|
||||
def mute_volume(self, mute):
|
||||
"""Mute the volume."""
|
||||
self.roku.volume_mute()
|
||||
if self.current_app is not None:
|
||||
self.roku.volume_mute()
|
||||
|
||||
def volume_up(self):
|
||||
"""Volume up media player."""
|
||||
self.roku.volume_up()
|
||||
if self.current_app is not None:
|
||||
self.roku.volume_up()
|
||||
|
||||
def volume_down(self):
|
||||
"""Volume down media player."""
|
||||
self.roku.volume_down()
|
||||
if self.current_app is not None:
|
||||
self.roku.volume_down()
|
||||
|
||||
def select_source(self, source):
|
||||
"""Select input source."""
|
||||
if source == "Home":
|
||||
self.roku.home()
|
||||
else:
|
||||
channel = self.roku[source]
|
||||
channel.launch()
|
||||
if self.current_app is not None:
|
||||
if source == "Home":
|
||||
self.roku.home()
|
||||
else:
|
||||
channel = self.roku[source]
|
||||
channel.launch()
|
||||
|
@ -10,7 +10,7 @@ from homeassistant.components.notify import DOMAIN, BaseNotificationService
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.helpers import validate_config
|
||||
|
||||
REQUIREMENTS = ['slacker==0.9.10']
|
||||
REQUIREMENTS = ['slacker==0.9.16']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.helpers import validate_config
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['python-telegram-bot==4.1.1']
|
||||
REQUIREMENTS = ['python-telegram-bot==4.2.0']
|
||||
|
||||
|
||||
def get_service(hass, config):
|
||||
|
@ -9,8 +9,8 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS
|
||||
from homeassistant.components.discovery import load_platform
|
||||
|
||||
REQUIREMENTS = ['https://github.com/kellerza/pyqwikswitch/archive/v0.3.zip'
|
||||
'#pyqwikswitch==0.3']
|
||||
REQUIREMENTS = ['https://github.com/kellerza/pyqwikswitch/archive/v0.4.zip'
|
||||
'#pyqwikswitch==0.4']
|
||||
DEPENDENCIES = []
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import (ATTR_ENTITY_ID, TEMP_CELSIUS)
|
||||
|
||||
REQUIREMENTS = ['pyRFXtrx==0.6.5']
|
||||
REQUIREMENTS = ['pyRFXtrx==0.8.0']
|
||||
|
||||
DOMAIN = "rfxtrx"
|
||||
|
||||
@ -310,6 +310,7 @@ class RfxtrxDevice(Entity):
|
||||
self.update_ha_state()
|
||||
|
||||
def _send_command(self, command, brightness=0):
|
||||
# pylint: disable=too-many-return-statements,too-many-branches
|
||||
if not self._event:
|
||||
return
|
||||
|
||||
@ -330,4 +331,16 @@ class RfxtrxDevice(Entity):
|
||||
self._state = False
|
||||
self._brightness = 0
|
||||
|
||||
elif command == "roll_up":
|
||||
for _ in range(self.signal_repetitions):
|
||||
self._event.device.send_open(RFXOBJECT.transport)
|
||||
|
||||
elif command == "roll_down":
|
||||
for _ in range(self.signal_repetitions):
|
||||
self._event.device.send_close(RFXOBJECT.transport)
|
||||
|
||||
elif command == "stop_roll":
|
||||
for _ in range(self.signal_repetitions):
|
||||
self._event.device.send_stop(RFXOBJECT.transport)
|
||||
|
||||
self.update_ha_state()
|
||||
|
66
homeassistant/components/rollershutter/rfxtrx.py
Normal file
66
homeassistant/components/rollershutter/rfxtrx.py
Normal file
@ -0,0 +1,66 @@
|
||||
"""
|
||||
Support for RFXtrx roller shutter components.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/rollershutter.rfxtrx/
|
||||
"""
|
||||
|
||||
import homeassistant.components.rfxtrx as rfxtrx
|
||||
from homeassistant.components.rollershutter import RollershutterDevice
|
||||
|
||||
DEPENDENCIES = ['rfxtrx']
|
||||
|
||||
PLATFORM_SCHEMA = rfxtrx.DEFAULT_SCHEMA
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Setup the Demo roller shutters."""
|
||||
import RFXtrx as rfxtrxmod
|
||||
|
||||
# Add rollershutter from config file
|
||||
rollershutters = rfxtrx.get_devices_from_config(config,
|
||||
RfxtrxRollershutter)
|
||||
add_devices_callback(rollershutters)
|
||||
|
||||
def rollershutter_update(event):
|
||||
"""Callback for roller shutter updates from the RFXtrx gateway."""
|
||||
if not isinstance(event.device, rfxtrxmod.LightingDevice) or \
|
||||
event.device.known_to_be_dimmable or \
|
||||
not event.device.known_to_be_rollershutter:
|
||||
return
|
||||
|
||||
new_device = rfxtrx.get_new_device(event, config, RfxtrxRollershutter)
|
||||
if new_device:
|
||||
add_devices_callback([new_device])
|
||||
|
||||
rfxtrx.apply_received_command(event)
|
||||
|
||||
# Subscribe to main rfxtrx events
|
||||
if rollershutter_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS:
|
||||
rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(rollershutter_update)
|
||||
|
||||
|
||||
class RfxtrxRollershutter(rfxtrx.RfxtrxDevice, RollershutterDevice):
|
||||
"""Representation of an rfxtrx roller shutter."""
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling available in rfxtrx roller shutter."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def current_position(self):
|
||||
"""No position available in rfxtrx roller shutter."""
|
||||
return None
|
||||
|
||||
def move_up(self, **kwargs):
|
||||
"""Move the roller shutter up."""
|
||||
self._send_command("roll_up")
|
||||
|
||||
def move_down(self, **kwargs):
|
||||
"""Move the roller shutter down."""
|
||||
self._send_command("roll_down")
|
||||
|
||||
def stop(self, **kwargs):
|
||||
"""Stop the roller shutter."""
|
||||
self._send_command("stop_roll")
|
@ -10,7 +10,7 @@ from datetime import timedelta
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['blockchain==1.3.1']
|
||||
REQUIREMENTS = ['blockchain==1.3.3']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
OPTION_TYPES = {
|
||||
'exchangerate': ['Exchange rate (1 BTC)', None],
|
||||
|
@ -10,7 +10,7 @@ from homeassistant.util import Throttle
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['schiene==0.15']
|
||||
REQUIREMENTS = ['schiene==0.17']
|
||||
ICON = 'mdi:train'
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
|
80
homeassistant/components/sensor/dte_energy_bridge.py
Normal file
80
homeassistant/components/sensor/dte_energy_bridge.py
Normal file
@ -0,0 +1,80 @@
|
||||
"""Support for monitoring energy usage using the DTE energy bridge."""
|
||||
import logging
|
||||
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ICON = 'mdi:flash'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the DTE energy bridge sensor."""
|
||||
ip_address = config.get('ip')
|
||||
if not ip_address:
|
||||
_LOGGER.error(
|
||||
"Configuration Error"
|
||||
"'ip' of the DTE energy bridge is required")
|
||||
return None
|
||||
dev = [DteEnergyBridgeSensor(ip_address)]
|
||||
add_devices(dev)
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class DteEnergyBridgeSensor(Entity):
|
||||
"""Implementation of an DTE Energy Bridge sensor."""
|
||||
|
||||
def __init__(self, ip_address):
|
||||
"""Initialize the sensor."""
|
||||
self._url = "http://{}/instantaneousdemand".format(ip_address)
|
||||
self._name = "Current Energy Usage"
|
||||
self._unit_of_measurement = "kW"
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of th sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement of this entity, if any."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend, if any."""
|
||||
return ICON
|
||||
|
||||
def update(self):
|
||||
"""Get the energy usage data from the DTE energy bridge."""
|
||||
import requests
|
||||
|
||||
try:
|
||||
response = requests.get(self._url, timeout=5)
|
||||
except (requests.exceptions.RequestException, ValueError):
|
||||
_LOGGER.warning(
|
||||
'Could not update status for DTE Energy Bridge (%s)',
|
||||
self._name)
|
||||
return
|
||||
|
||||
if response.status_code != 200:
|
||||
_LOGGER.warning(
|
||||
'Invalid status_code from DTE Energy Bridge: %s (%s)',
|
||||
response.status_code, self._name)
|
||||
return
|
||||
|
||||
response_split = response.text.split()
|
||||
|
||||
if len(response_split) != 2:
|
||||
_LOGGER.warning(
|
||||
'Invalid response from DTE Energy Bridge: "%s" (%s)',
|
||||
response.text, self._name)
|
||||
return
|
||||
|
||||
self._state = float(response_split[0])
|
55
homeassistant/components/sensor/enocean.py
Normal file
55
homeassistant/components/sensor/enocean.py
Normal file
@ -0,0 +1,55 @@
|
||||
"""
|
||||
Support for EnOcean sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.enocean/
|
||||
"""
|
||||
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.components import enocean
|
||||
|
||||
DEPENDENCIES = ["enocean"]
|
||||
|
||||
CONF_ID = "id"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup an EnOcean sensor device."""
|
||||
dev_id = config.get(CONF_ID, None)
|
||||
devname = config.get(CONF_NAME, None)
|
||||
add_devices([EnOceanSensor(dev_id, devname)])
|
||||
|
||||
|
||||
class EnOceanSensor(enocean.EnOceanDevice, Entity):
|
||||
"""Representation of an EnOcean sensor device such as a power meter."""
|
||||
|
||||
def __init__(self, dev_id, devname):
|
||||
"""Initialize the EnOcean sensor device."""
|
||||
enocean.EnOceanDevice.__init__(self)
|
||||
self.stype = "powersensor"
|
||||
self.power = None
|
||||
self.dev_id = dev_id
|
||||
self.which = -1
|
||||
self.onoff = -1
|
||||
self.devname = devname
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return 'Power %s' % self.devname
|
||||
|
||||
def value_changed(self, value):
|
||||
"""Update the internal state of the device."""
|
||||
self.power = value
|
||||
self.update_ha_state()
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self.power
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return "W"
|
@ -10,10 +10,11 @@ import logging
|
||||
import datetime
|
||||
import time
|
||||
|
||||
from homeassistant.const import HTTP_OK, TEMP_CELSIUS
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ["fitbit==0.2.2"]
|
||||
@ -248,70 +249,83 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
redirect_uri = "{}{}".format(hass.config.api.base_url,
|
||||
FITBIT_AUTH_CALLBACK_PATH)
|
||||
|
||||
def _start_fitbit_auth(handler, path_match, data):
|
||||
"""Start Fitbit OAuth2 flow."""
|
||||
url, _ = oauth.authorize_token_url(redirect_uri=redirect_uri,
|
||||
scope=["activity", "heartrate",
|
||||
"nutrition", "profile",
|
||||
"settings", "sleep",
|
||||
"weight"])
|
||||
handler.send_response(301)
|
||||
handler.send_header("Location", url)
|
||||
handler.end_headers()
|
||||
fitbit_auth_start_url, _ = oauth.authorize_token_url(
|
||||
redirect_uri=redirect_uri,
|
||||
scope=["activity", "heartrate", "nutrition", "profile",
|
||||
"settings", "sleep", "weight"])
|
||||
|
||||
def _finish_fitbit_auth(handler, path_match, data):
|
||||
"""Finish Fitbit OAuth2 flow."""
|
||||
response_message = """Fitbit has been successfully authorized!
|
||||
You can close this window now!"""
|
||||
from oauthlib.oauth2.rfc6749.errors import MismatchingStateError
|
||||
from oauthlib.oauth2.rfc6749.errors import MissingTokenError
|
||||
if data.get("code") is not None:
|
||||
try:
|
||||
oauth.fetch_access_token(data.get("code"), redirect_uri)
|
||||
except MissingTokenError as error:
|
||||
_LOGGER.error("Missing token: %s", error)
|
||||
response_message = """Something went wrong when
|
||||
attempting authenticating with Fitbit. The error
|
||||
encountered was {}. Please try again!""".format(error)
|
||||
except MismatchingStateError as error:
|
||||
_LOGGER.error("Mismatched state, CSRF error: %s", error)
|
||||
response_message = """Something went wrong when
|
||||
attempting authenticating with Fitbit. The error
|
||||
encountered was {}. Please try again!""".format(error)
|
||||
else:
|
||||
_LOGGER.error("Unknown error when authing")
|
||||
response_message = """Something went wrong when
|
||||
attempting authenticating with Fitbit.
|
||||
An unknown error occurred. Please try again!
|
||||
"""
|
||||
|
||||
html_response = """<html><head><title>Fitbit Auth</title></head>
|
||||
<body><h1>{}</h1></body></html>""".format(response_message)
|
||||
|
||||
html_response = html_response.encode("utf-8")
|
||||
|
||||
handler.send_response(HTTP_OK)
|
||||
handler.write_content(html_response, content_type="text/html")
|
||||
|
||||
config_contents = {
|
||||
"access_token": oauth.token["access_token"],
|
||||
"refresh_token": oauth.token["refresh_token"],
|
||||
"client_id": oauth.client_id,
|
||||
"client_secret": oauth.client_secret
|
||||
}
|
||||
if not config_from_file(config_path, config_contents):
|
||||
_LOGGER.error("failed to save config file")
|
||||
|
||||
setup_platform(hass, config, add_devices, discovery_info=None)
|
||||
|
||||
hass.http.register_path("GET", FITBIT_AUTH_START, _start_fitbit_auth,
|
||||
require_auth=False)
|
||||
hass.http.register_path("GET", FITBIT_AUTH_CALLBACK_PATH,
|
||||
_finish_fitbit_auth, require_auth=False)
|
||||
hass.wsgi.register_redirect(FITBIT_AUTH_START, fitbit_auth_start_url)
|
||||
hass.wsgi.register_view(FitbitAuthCallbackView(hass, config,
|
||||
add_devices, oauth))
|
||||
|
||||
request_oauth_completion(hass)
|
||||
|
||||
|
||||
class FitbitAuthCallbackView(HomeAssistantView):
|
||||
"""Handle OAuth finish callback requests."""
|
||||
|
||||
requires_auth = False
|
||||
url = '/auth/fitbit/callback'
|
||||
name = 'auth:fitbit:callback'
|
||||
|
||||
def __init__(self, hass, config, add_devices, oauth):
|
||||
"""Initialize the OAuth callback view."""
|
||||
super().__init__(hass)
|
||||
self.config = config
|
||||
self.add_devices = add_devices
|
||||
self.oauth = oauth
|
||||
|
||||
def get(self, request):
|
||||
"""Finish OAuth callback request."""
|
||||
from oauthlib.oauth2.rfc6749.errors import MismatchingStateError
|
||||
from oauthlib.oauth2.rfc6749.errors import MissingTokenError
|
||||
|
||||
data = request.args
|
||||
|
||||
response_message = """Fitbit has been successfully authorized!
|
||||
You can close this window now!"""
|
||||
|
||||
if data.get("code") is not None:
|
||||
redirect_uri = "{}{}".format(self.hass.config.api.base_url,
|
||||
FITBIT_AUTH_CALLBACK_PATH)
|
||||
|
||||
try:
|
||||
self.oauth.fetch_access_token(data.get("code"), redirect_uri)
|
||||
except MissingTokenError as error:
|
||||
_LOGGER.error("Missing token: %s", error)
|
||||
response_message = """Something went wrong when
|
||||
attempting authenticating with Fitbit. The error
|
||||
encountered was {}. Please try again!""".format(error)
|
||||
except MismatchingStateError as error:
|
||||
_LOGGER.error("Mismatched state, CSRF error: %s", error)
|
||||
response_message = """Something went wrong when
|
||||
attempting authenticating with Fitbit. The error
|
||||
encountered was {}. Please try again!""".format(error)
|
||||
else:
|
||||
_LOGGER.error("Unknown error when authing")
|
||||
response_message = """Something went wrong when
|
||||
attempting authenticating with Fitbit.
|
||||
An unknown error occurred. Please try again!
|
||||
"""
|
||||
|
||||
html_response = """<html><head><title>Fitbit Auth</title></head>
|
||||
<body><h1>{}</h1></body></html>""".format(response_message)
|
||||
|
||||
config_contents = {
|
||||
"access_token": self.oauth.token["access_token"],
|
||||
"refresh_token": self.oauth.token["refresh_token"],
|
||||
"client_id": self.oauth.client_id,
|
||||
"client_secret": self.oauth.client_secret
|
||||
}
|
||||
if not config_from_file(self.hass.config.path(FITBIT_CONFIG_FILE),
|
||||
config_contents):
|
||||
_LOGGER.error("failed to save config file")
|
||||
|
||||
setup_platform(self.hass, self.config, self.add_devices)
|
||||
|
||||
return html_response
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class FitbitSensor(Entity):
|
||||
"""Implementation of a Fitbit sensor."""
|
||||
|
@ -37,7 +37,7 @@ SENSOR_TYPES = {
|
||||
'wind_bearing': ['Wind Bearing', '°', '°', '°', '°', '°'],
|
||||
'cloud_cover': ['Cloud Coverage', '%', '%', '%', '%', '%'],
|
||||
'humidity': ['Humidity', '%', '%', '%', '%', '%'],
|
||||
'pressure': ['Pressure', 'mBar', 'mBar', 'mBar', 'mBar', 'mBar'],
|
||||
'pressure': ['Pressure', 'mbar', 'mbar', 'mbar', 'mbar', 'mbar'],
|
||||
'visibility': ['Visibility', 'km', 'm', 'km', 'km', 'm'],
|
||||
'ozone': ['Ozone', 'DU', 'DU', 'DU', 'DU', 'DU'],
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_DESTINATION): vol.Coerce(str),
|
||||
vol.Optional(CONF_TRAVEL_MODE):
|
||||
vol.In(["driving", "walking", "bicycling", "transit"]),
|
||||
vol.Optional(CONF_OPTIONS, default=dict()): vol.All(
|
||||
vol.Optional(CONF_OPTIONS, default={CONF_MODE: 'driving'}): vol.All(
|
||||
dict, vol.Schema({
|
||||
vol.Optional(CONF_MODE, default='driving'):
|
||||
vol.In(["driving", "walking", "bicycling", "transit"]),
|
||||
@ -178,7 +178,7 @@ class GoogleTravelTimeSensor(Entity):
|
||||
options_copy['departure_time'] = convert_time_to_utc(dtime)
|
||||
elif dtime is not None:
|
||||
options_copy['departure_time'] = dtime
|
||||
else:
|
||||
elif atime is None:
|
||||
options_copy['departure_time'] = 'now'
|
||||
|
||||
if atime is not None and ':' in atime:
|
||||
|
@ -14,7 +14,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "loopenergy"
|
||||
|
||||
REQUIREMENTS = ['pyloopenergy==0.0.12']
|
||||
REQUIREMENTS = ['pyloopenergy==0.0.13']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
@ -33,6 +33,7 @@ SENSOR_TYPES = {
|
||||
}
|
||||
|
||||
CONF_SECRET_KEY = 'secret_key'
|
||||
CONF_STATION = 'station'
|
||||
ATTR_MODULE = 'modules'
|
||||
|
||||
# Return cached results if last scan was less then this time ago
|
||||
@ -64,7 +65,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"Please check your settings for NatAtmo API.")
|
||||
return False
|
||||
|
||||
data = NetAtmoData(authorization)
|
||||
data = NetAtmoData(authorization, config.get(CONF_STATION, None))
|
||||
|
||||
dev = []
|
||||
try:
|
||||
@ -149,10 +150,11 @@ class NetAtmoSensor(Entity):
|
||||
class NetAtmoData(object):
|
||||
"""Get the latest data from NetAtmo."""
|
||||
|
||||
def __init__(self, auth):
|
||||
def __init__(self, auth, station):
|
||||
"""Initialize the data object."""
|
||||
self.auth = auth
|
||||
self.data = None
|
||||
self.station = station
|
||||
|
||||
def get_module_names(self):
|
||||
"""Return all module available on the API as a list."""
|
||||
@ -164,4 +166,8 @@ class NetAtmoData(object):
|
||||
"""Call the NetAtmo API to update the data."""
|
||||
import lnetatmo
|
||||
dev_list = lnetatmo.DeviceList(self.auth)
|
||||
self.data = dev_list.lastData(exclude=3600)
|
||||
|
||||
if self.station is not None:
|
||||
self.data = dev_list.lastData(station=self.station, exclude=3600)
|
||||
else:
|
||||
self.data = dev_list.lastData(exclude=3600)
|
||||
|
@ -93,7 +93,11 @@ class OctoPrintSensor(Entity):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
sensor_unit = self.unit_of_measurement
|
||||
if sensor_unit == TEMP_CELSIUS or sensor_unit == "%":
|
||||
return round(self._state, 2)
|
||||
else:
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
|
@ -7,7 +7,12 @@ https://home-assistant.io/components/sensor.openweathermap/
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.const import CONF_API_KEY, TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (CONF_API_KEY, TEMP_CELSIUS, TEMP_FAHRENHEIT,
|
||||
CONF_PLATFORM, CONF_LATITUDE, CONF_LONGITUDE,
|
||||
CONF_MONITORED_CONDITIONS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
@ -24,6 +29,15 @@ SENSOR_TYPES = {
|
||||
'snow': ['Snow', 'mm']
|
||||
}
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'openweathermap',
|
||||
vol.Required(CONF_API_KEY): vol.Coerce(str),
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=[]):
|
||||
[vol.In(SENSOR_TYPES.keys())],
|
||||
vol.Optional(CONF_LATITUDE): cv.latitude,
|
||||
vol.Optional(CONF_LONGITUDE): cv.longitude
|
||||
})
|
||||
|
||||
# Return cached results if last scan was less then this time ago.
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
|
||||
|
||||
|
@ -8,11 +8,12 @@ import logging
|
||||
|
||||
from homeassistant.components.sensor import ENTITY_ID_FORMAT
|
||||
from homeassistant.const import (
|
||||
ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE)
|
||||
from homeassistant.core import EVENT_STATE_CHANGED
|
||||
ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE,
|
||||
ATTR_ENTITY_ID, MATCH_ALL)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.entity import Entity, generate_entity_id
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.util import slugify
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -45,13 +46,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"Missing %s for sensor %s", CONF_VALUE_TEMPLATE, device)
|
||||
continue
|
||||
|
||||
entity_ids = device_config.get(ATTR_ENTITY_ID, MATCH_ALL)
|
||||
|
||||
sensors.append(
|
||||
SensorTemplate(
|
||||
hass,
|
||||
device,
|
||||
friendly_name,
|
||||
unit_of_measurement,
|
||||
state_template)
|
||||
state_template,
|
||||
entity_ids)
|
||||
)
|
||||
if not sensors:
|
||||
_LOGGER.error("No sensors added")
|
||||
@ -65,7 +69,7 @@ class SensorTemplate(Entity):
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, device_id, friendly_name, unit_of_measurement,
|
||||
state_template):
|
||||
state_template, entity_ids):
|
||||
"""Initialize the sensor."""
|
||||
self.hass = hass
|
||||
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id,
|
||||
@ -77,11 +81,12 @@ class SensorTemplate(Entity):
|
||||
|
||||
self.update()
|
||||
|
||||
def template_sensor_event_listener(event):
|
||||
def template_sensor_state_listener(entity, old_state, new_state):
|
||||
"""Called when the target device changes state."""
|
||||
self.update_ha_state(True)
|
||||
|
||||
hass.bus.listen(EVENT_STATE_CHANGED, template_sensor_event_listener)
|
||||
track_state_change(hass, entity_ids,
|
||||
template_sensor_state_listener)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -6,6 +6,7 @@ https://home-assistant.io/components/sensor.time_date/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from datetime import timedelta
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
@ -15,7 +16,7 @@ OPTION_TYPES = {
|
||||
'date': 'Date',
|
||||
'date_time': 'Date & Time',
|
||||
'time_date': 'Time & Date',
|
||||
'beat': 'Time (beat)',
|
||||
'beat': 'Internet Time',
|
||||
'time_utc': 'Time (UTC)',
|
||||
}
|
||||
|
||||
@ -76,10 +77,13 @@ class TimeDateSensor(Entity):
|
||||
time_utc = time_date.strftime(TIME_STR_FORMAT)
|
||||
date = dt_util.as_local(time_date).date().isoformat()
|
||||
|
||||
# Calculate the beat (Swatch Internet Time) time without date.
|
||||
hours, minutes, seconds = time_date.strftime('%H:%M:%S').split(':')
|
||||
beat = ((int(seconds) + (int(minutes) * 60) + ((int(hours) + 1) *
|
||||
3600)) / 86.4)
|
||||
# Calculate Swatch Internet Time.
|
||||
time_bmt = time_date + timedelta(hours=1)
|
||||
delta = timedelta(hours=time_bmt.hour,
|
||||
minutes=time_bmt.minute,
|
||||
seconds=time_bmt.second,
|
||||
microseconds=time_bmt.microsecond)
|
||||
beat = int((delta.seconds + delta.microseconds / 1000000.0) / 86.4)
|
||||
|
||||
if self.type == 'time':
|
||||
self._state = time
|
||||
@ -92,4 +96,4 @@ class TimeDateSensor(Entity):
|
||||
elif self.type == 'time_utc':
|
||||
self._state = time_utc
|
||||
elif self.type == 'beat':
|
||||
self._state = '{0:.2f}'.format(beat)
|
||||
self._state = '@{0:03d}'.format(beat)
|
||||
|
@ -7,8 +7,8 @@ https://home-assistant.io/components/sensor.torque/
|
||||
|
||||
import re
|
||||
|
||||
from homeassistant.const import HTTP_OK
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
DOMAIN = 'torque'
|
||||
DEPENDENCIES = ['http']
|
||||
@ -43,12 +43,31 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
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()
|
||||
hass.wsgi.register_view(TorqueReceiveDataView(hass, email, vehicle,
|
||||
sensors, add_devices))
|
||||
return True
|
||||
|
||||
if email is not None and email != data[SENSOR_EMAIL_FIELD]:
|
||||
|
||||
class TorqueReceiveDataView(HomeAssistantView):
|
||||
"""Handle data from Torque requests."""
|
||||
|
||||
url = API_PATH
|
||||
name = 'api:torque'
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, email, vehicle, sensors, add_devices):
|
||||
"""Initialize a Torque view."""
|
||||
super().__init__(hass)
|
||||
self.email = email
|
||||
self.vehicle = vehicle
|
||||
self.sensors = sensors
|
||||
self.add_devices = add_devices
|
||||
|
||||
def get(self, request):
|
||||
"""Handle Torque data request."""
|
||||
data = request.args
|
||||
|
||||
if self.email is not None and self.email != data[SENSOR_EMAIL_FIELD]:
|
||||
return
|
||||
|
||||
names = {}
|
||||
@ -66,18 +85,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
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])
|
||||
if pid in self.sensors:
|
||||
self.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]),
|
||||
if pid not in self.sensors:
|
||||
self.sensors[pid] = TorqueSensor(
|
||||
ENTITY_NAME_FORMAT.format(self.vehicle, names[pid]),
|
||||
units.get(pid, None))
|
||||
add_devices([sensors[pid]])
|
||||
self.add_devices([self.sensors[pid]])
|
||||
|
||||
hass.http.register_path('GET', API_PATH, _receive_data)
|
||||
return True
|
||||
return None
|
||||
|
||||
|
||||
class TorqueSensor(Entity):
|
||||
|
@ -15,7 +15,7 @@ from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util import location as location_util
|
||||
from homeassistant.const import CONF_ELEVATION
|
||||
|
||||
REQUIREMENTS = ['astral==1.0']
|
||||
REQUIREMENTS = ['astral==1.1']
|
||||
DOMAIN = "sun"
|
||||
ENTITY_ID = "sun.sun"
|
||||
|
||||
@ -25,6 +25,7 @@ STATE_BELOW_HORIZON = "below_horizon"
|
||||
STATE_ATTR_NEXT_RISING = "next_rising"
|
||||
STATE_ATTR_NEXT_SETTING = "next_setting"
|
||||
STATE_ATTR_ELEVATION = "elevation"
|
||||
STATE_ATTR_AZIMUTH = "azimuth"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -80,7 +81,7 @@ def next_rising_utc(hass, entity_id=None):
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Track the state of the sun in HA."""
|
||||
"""Track the state of the sun."""
|
||||
if None in (hass.config.latitude, hass.config.longitude):
|
||||
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
|
||||
return False
|
||||
@ -126,10 +127,12 @@ class Sun(Entity):
|
||||
entity_id = ENTITY_ID
|
||||
|
||||
def __init__(self, hass, location):
|
||||
"""Initialize the Sun."""
|
||||
"""Initialize the sun."""
|
||||
self.hass = hass
|
||||
self.location = location
|
||||
self._state = self.next_rising = self.next_setting = None
|
||||
self.solar_elevation = self.solar_azimuth = 0
|
||||
|
||||
track_utc_time_change(hass, self.timer_update, second=30)
|
||||
|
||||
@property
|
||||
@ -151,7 +154,8 @@ class Sun(Entity):
|
||||
return {
|
||||
STATE_ATTR_NEXT_RISING: self.next_rising.isoformat(),
|
||||
STATE_ATTR_NEXT_SETTING: self.next_setting.isoformat(),
|
||||
STATE_ATTR_ELEVATION: round(self.solar_elevation, 2)
|
||||
STATE_ATTR_ELEVATION: round(self.solar_elevation, 2),
|
||||
STATE_ATTR_AZIMUTH: round(self.solar_azimuth, 2)
|
||||
}
|
||||
|
||||
@property
|
||||
@ -159,36 +163,49 @@ class Sun(Entity):
|
||||
"""Datetime when the next change to the state is."""
|
||||
return min(self.next_rising, self.next_setting)
|
||||
|
||||
@property
|
||||
def solar_elevation(self):
|
||||
"""Angle the sun is above the horizon."""
|
||||
from astral import Astral
|
||||
return Astral().solar_elevation(
|
||||
dt_util.utcnow(),
|
||||
self.location.latitude,
|
||||
self.location.longitude)
|
||||
|
||||
def update_as_of(self, utc_point_in_time):
|
||||
"""Calculate sun state at a point in UTC time."""
|
||||
import astral
|
||||
|
||||
mod = -1
|
||||
while True:
|
||||
next_rising_dt = self.location.sunrise(
|
||||
utc_point_in_time + timedelta(days=mod), local=False)
|
||||
if next_rising_dt > utc_point_in_time:
|
||||
break
|
||||
try:
|
||||
next_rising_dt = self.location.sunrise(
|
||||
utc_point_in_time + timedelta(days=mod), local=False)
|
||||
if next_rising_dt > utc_point_in_time:
|
||||
break
|
||||
except astral.AstralError:
|
||||
pass
|
||||
mod += 1
|
||||
|
||||
mod = -1
|
||||
while True:
|
||||
next_setting_dt = (self.location.sunset(
|
||||
utc_point_in_time + timedelta(days=mod), local=False))
|
||||
if next_setting_dt > utc_point_in_time:
|
||||
break
|
||||
try:
|
||||
next_setting_dt = (self.location.sunset(
|
||||
utc_point_in_time + timedelta(days=mod), local=False))
|
||||
if next_setting_dt > utc_point_in_time:
|
||||
break
|
||||
except astral.AstralError:
|
||||
pass
|
||||
mod += 1
|
||||
|
||||
self.next_rising = next_rising_dt
|
||||
self.next_setting = next_setting_dt
|
||||
|
||||
def update_sun_position(self, utc_point_in_time):
|
||||
"""Calculate the position of the sun."""
|
||||
from astral import Astral
|
||||
|
||||
self.solar_azimuth = Astral().solar_azimuth(
|
||||
utc_point_in_time,
|
||||
self.location.latitude,
|
||||
self.location.longitude)
|
||||
|
||||
self.solar_elevation = Astral().solar_elevation(
|
||||
utc_point_in_time,
|
||||
self.location.latitude,
|
||||
self.location.longitude)
|
||||
|
||||
def point_in_time_listener(self, now):
|
||||
"""Called when the state of the sun has changed."""
|
||||
self.update_as_of(now)
|
||||
@ -200,5 +217,6 @@ class Sun(Entity):
|
||||
self.next_change + timedelta(seconds=1))
|
||||
|
||||
def timer_update(self, time):
|
||||
"""Needed to update solar elevation."""
|
||||
"""Needed to update solar elevation and azimuth."""
|
||||
self.update_sun_position(time)
|
||||
self.update_ha_state()
|
||||
|
@ -61,6 +61,8 @@ class SmartPlugSwitch(SwitchDevice):
|
||||
return float(self.smartplug.now_power) / 1000000.0
|
||||
except ValueError:
|
||||
return None
|
||||
except TypeError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def today_power_mw(self):
|
||||
@ -69,6 +71,8 @@ class SmartPlugSwitch(SwitchDevice):
|
||||
return float(self.smartplug.now_energy_day) / 1000.0
|
||||
except ValueError:
|
||||
return None
|
||||
except TypeError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
76
homeassistant/components/switch/enocean.py
Normal file
76
homeassistant/components/switch/enocean.py
Normal file
@ -0,0 +1,76 @@
|
||||
"""
|
||||
Support for EnOcean switches.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/switch.enocean/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.components import enocean
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ["enocean"]
|
||||
|
||||
CONF_ID = "id"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the EnOcean switch platform."""
|
||||
dev_id = config.get(CONF_ID, None)
|
||||
devname = config.get(CONF_NAME, "Enocean actuator")
|
||||
|
||||
add_devices([EnOceanSwitch(dev_id, devname)])
|
||||
|
||||
|
||||
class EnOceanSwitch(enocean.EnOceanDevice, ToggleEntity):
|
||||
"""Representation of an EnOcean switch device."""
|
||||
|
||||
def __init__(self, dev_id, devname):
|
||||
"""Initialize the EnOcean switch device."""
|
||||
enocean.EnOceanDevice.__init__(self)
|
||||
self.dev_id = dev_id
|
||||
self._devname = devname
|
||||
self._light = None
|
||||
self._on_state = False
|
||||
self._on_state2 = False
|
||||
self.stype = "switch"
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return whether the switch is on or off."""
|
||||
return self._on_state
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the device name."""
|
||||
return self._devname
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn on the switch."""
|
||||
optional = [0x03, ]
|
||||
optional.extend(self.dev_id)
|
||||
optional.extend([0xff, 0x00])
|
||||
self.send_command(data=[0xD2, 0x01, 0x00, 0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00], optional=optional,
|
||||
packet_type=0x01)
|
||||
self._on_state = True
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn off the switch."""
|
||||
optional = [0x03, ]
|
||||
optional.extend(self.dev_id)
|
||||
optional.extend([0xff, 0x00])
|
||||
self.send_command(data=[0xD2, 0x01, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00], optional=optional,
|
||||
packet_type=0x01)
|
||||
self._on_state = False
|
||||
|
||||
def value_changed(self, val):
|
||||
"""Update the internal state of the switch."""
|
||||
self._on_state = val
|
||||
self.update_ha_state()
|
196
homeassistant/components/switch/flux.py
Normal file
196
homeassistant/components/switch/flux.py
Normal file
@ -0,0 +1,196 @@
|
||||
"""
|
||||
Flux for Home-Assistant.
|
||||
|
||||
The idea was taken from https://github.com/KpaBap/hue-flux/
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/switch/flux/
|
||||
"""
|
||||
from datetime import time
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import is_on, turn_on
|
||||
from homeassistant.components.sun import next_setting, next_rising
|
||||
from homeassistant.components.switch import DOMAIN, SwitchDevice
|
||||
from homeassistant.const import CONF_NAME, CONF_PLATFORM, EVENT_TIME_CHANGED
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.util.color import color_temperature_to_rgb as temp_to_rgb
|
||||
from homeassistant.util.color import color_RGB_to_xy
|
||||
from homeassistant.util.dt import now as dt_now
|
||||
from homeassistant.util.dt import as_local
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['sun', 'light']
|
||||
SUN = "sun.sun"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_LIGHTS = 'lights'
|
||||
CONF_START_TIME = 'start_time'
|
||||
CONF_STOP_TIME = 'stop_time'
|
||||
CONF_START_CT = 'start_colortemp'
|
||||
CONF_SUNSET_CT = 'sunset_colortemp'
|
||||
CONF_STOP_CT = 'stop_colortemp'
|
||||
CONF_BRIGHTNESS = 'brightness'
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'flux',
|
||||
vol.Required(CONF_LIGHTS): cv.entity_ids,
|
||||
vol.Optional(CONF_NAME, default="Flux"): cv.string,
|
||||
vol.Optional(CONF_START_TIME): cv.time,
|
||||
vol.Optional(CONF_STOP_TIME, default=time(22, 0)): cv.time,
|
||||
vol.Optional(CONF_START_CT, default=4000):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1000, max=40000)),
|
||||
vol.Optional(CONF_SUNSET_CT, default=3000):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1000, max=40000)),
|
||||
vol.Optional(CONF_STOP_CT, default=1900):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1000, max=40000)),
|
||||
vol.Optional(CONF_BRIGHTNESS):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0, max=255))
|
||||
})
|
||||
|
||||
|
||||
def set_lights_xy(hass, lights, x_val, y_val, brightness):
|
||||
"""Set color of array of lights."""
|
||||
for light in lights:
|
||||
if is_on(hass, light):
|
||||
turn_on(hass, light,
|
||||
xy_color=[x_val, y_val],
|
||||
brightness=brightness,
|
||||
transition=30)
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the demo switches."""
|
||||
name = config.get(CONF_NAME)
|
||||
lights = config.get(CONF_LIGHTS)
|
||||
start_time = config.get(CONF_START_TIME)
|
||||
stop_time = config.get(CONF_STOP_TIME)
|
||||
start_colortemp = config.get(CONF_START_CT)
|
||||
sunset_colortemp = config.get(CONF_SUNSET_CT)
|
||||
stop_colortemp = config.get(CONF_STOP_CT)
|
||||
brightness = config.get(CONF_BRIGHTNESS)
|
||||
flux = FluxSwitch(name, hass, False, lights, start_time, stop_time,
|
||||
start_colortemp, sunset_colortemp, stop_colortemp,
|
||||
brightness)
|
||||
add_devices([flux])
|
||||
|
||||
def update(call=None):
|
||||
"""Update lights."""
|
||||
flux.flux_update()
|
||||
|
||||
hass.services.register(DOMAIN, 'flux_update', update)
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class FluxSwitch(SwitchDevice):
|
||||
"""Flux switch."""
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, name, hass, state, lights, start_time, stop_time,
|
||||
start_colortemp, sunset_colortemp, stop_colortemp,
|
||||
brightness):
|
||||
"""Initialize the Flux switch."""
|
||||
self._name = name
|
||||
self.hass = hass
|
||||
self._state = state
|
||||
self._lights = lights
|
||||
self._start_time = start_time
|
||||
self._stop_time = stop_time
|
||||
self._start_colortemp = start_colortemp
|
||||
self._sunset_colortemp = sunset_colortemp
|
||||
self._stop_colortemp = stop_colortemp
|
||||
self._brightness = brightness
|
||||
self.tracker = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device if any."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if switch is on."""
|
||||
return self._state
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn on flux."""
|
||||
self._state = True
|
||||
self.tracker = track_utc_time_change(self.hass,
|
||||
self.flux_update,
|
||||
second=[0, 30])
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn off flux."""
|
||||
self._state = False
|
||||
self.hass.bus.remove_listener(EVENT_TIME_CHANGED, self.tracker)
|
||||
self.update_ha_state()
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
def flux_update(self, now=dt_now()):
|
||||
"""Update all the lights using flux."""
|
||||
sunset = next_setting(self.hass, SUN).replace(day=now.day,
|
||||
month=now.month,
|
||||
year=now.year)
|
||||
start_time = self.find_start_time(now)
|
||||
stop_time = now.replace(hour=self._stop_time.hour,
|
||||
minute=self._stop_time.minute,
|
||||
second=0)
|
||||
|
||||
if start_time < now < sunset:
|
||||
# Daytime
|
||||
temp_range = abs(self._start_colortemp - self._sunset_colortemp)
|
||||
day_length = int(sunset.timestamp() - start_time.timestamp())
|
||||
seconds_from_start = int(now.timestamp() - start_time.timestamp())
|
||||
percentage_of_day_complete = seconds_from_start / day_length
|
||||
temp_offset = temp_range * percentage_of_day_complete
|
||||
if self._start_colortemp > self._sunset_colortemp:
|
||||
temp = self._start_colortemp - temp_offset
|
||||
else:
|
||||
temp = self._start_colortemp + temp_offset
|
||||
x_val, y_val, b_val = color_RGB_to_xy(*temp_to_rgb(temp))
|
||||
brightness = self._brightness if self._brightness else b_val
|
||||
set_lights_xy(self.hass, self._lights, x_val,
|
||||
y_val, brightness)
|
||||
_LOGGER.info("Lights updated to x:%s y:%s brightness:%s, %s%%"
|
||||
" of day cycle complete at %s", x_val, y_val,
|
||||
brightness, round(percentage_of_day_complete*100),
|
||||
as_local(now))
|
||||
else:
|
||||
# Nightime
|
||||
if now < stop_time and now > start_time:
|
||||
now_time = now
|
||||
else:
|
||||
now_time = stop_time
|
||||
temp_range = abs(self._sunset_colortemp - self._stop_colortemp)
|
||||
night_length = int(stop_time.timestamp() - sunset.timestamp())
|
||||
seconds_from_sunset = int(now_time.timestamp() -
|
||||
sunset.timestamp())
|
||||
percentage_of_night_complete = seconds_from_sunset / night_length
|
||||
temp_offset = temp_range * percentage_of_night_complete
|
||||
if self._sunset_colortemp > self._stop_colortemp:
|
||||
temp = self._sunset_colortemp - temp_offset
|
||||
else:
|
||||
temp = self._sunset_colortemp + temp_offset
|
||||
x_val, y_val, b_val = color_RGB_to_xy(*temp_to_rgb(temp))
|
||||
brightness = self._brightness if self._brightness else b_val
|
||||
set_lights_xy(self.hass, self._lights, x_val,
|
||||
y_val, brightness)
|
||||
_LOGGER.info("Lights updated to x:%s y:%s brightness:%s, %s%%"
|
||||
" of night cycle complete at %s", x_val, y_val,
|
||||
brightness, round(percentage_of_night_complete*100),
|
||||
as_local(now))
|
||||
|
||||
def find_start_time(self, now):
|
||||
"""Return sunrise or start_time if given."""
|
||||
if self._start_time:
|
||||
sunrise = now.replace(hour=self._start_time.hour,
|
||||
minute=self._start_time.minute,
|
||||
second=0)
|
||||
else:
|
||||
sunrise = next_rising(self.hass, SUN).replace(day=now.day,
|
||||
month=now.month,
|
||||
year=now.year)
|
||||
return sunrise
|
@ -28,7 +28,8 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
def switch_update(event):
|
||||
"""Callback for sensor updates from the RFXtrx gateway."""
|
||||
if not isinstance(event.device, rfxtrxmod.LightingDevice) or \
|
||||
event.device.known_to_be_dimmable:
|
||||
event.device.known_to_be_dimmable or \
|
||||
event.device.known_to_be_rollershutter:
|
||||
return
|
||||
|
||||
new_device = rfxtrx.get_new_device(event, config, RfxtrxSwitch)
|
||||
|
@ -8,12 +8,13 @@ import logging
|
||||
|
||||
from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchDevice
|
||||
from homeassistant.const import (
|
||||
ATTR_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, STATE_OFF, STATE_ON)
|
||||
from homeassistant.core import EVENT_STATE_CHANGED
|
||||
ATTR_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, STATE_OFF, STATE_ON,
|
||||
ATTR_ENTITY_ID, MATCH_ALL)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers.service import call_from_config
|
||||
from homeassistant.helpers.script import Script
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
from homeassistant.util import slugify
|
||||
|
||||
CONF_SWITCHES = 'switches'
|
||||
@ -58,6 +59,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"Missing action for switch %s", device)
|
||||
continue
|
||||
|
||||
entity_ids = device_config.get(ATTR_ENTITY_ID, MATCH_ALL)
|
||||
|
||||
switches.append(
|
||||
SwitchTemplate(
|
||||
hass,
|
||||
@ -65,7 +68,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
friendly_name,
|
||||
state_template,
|
||||
on_action,
|
||||
off_action)
|
||||
off_action,
|
||||
entity_ids)
|
||||
)
|
||||
if not switches:
|
||||
_LOGGER.error("No switches added")
|
||||
@ -79,25 +83,25 @@ class SwitchTemplate(SwitchDevice):
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, device_id, friendly_name, state_template,
|
||||
on_action, off_action):
|
||||
on_action, off_action, entity_ids):
|
||||
"""Initialize the Template switch."""
|
||||
self.hass = hass
|
||||
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id,
|
||||
hass=hass)
|
||||
self._name = friendly_name
|
||||
self._template = state_template
|
||||
self._on_action = on_action
|
||||
self._off_action = off_action
|
||||
self._on_script = Script(hass, on_action)
|
||||
self._off_script = Script(hass, off_action)
|
||||
self._state = False
|
||||
|
||||
self.update()
|
||||
|
||||
def template_switch_event_listener(event):
|
||||
def template_switch_state_listener(entity, old_state, new_state):
|
||||
"""Called when the target device changes state."""
|
||||
self.update_ha_state(True)
|
||||
|
||||
hass.bus.listen(EVENT_STATE_CHANGED,
|
||||
template_switch_event_listener)
|
||||
track_state_change(hass, entity_ids,
|
||||
template_switch_state_listener)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -121,11 +125,11 @@ class SwitchTemplate(SwitchDevice):
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Fire the on action."""
|
||||
call_from_config(self.hass, self._on_action, True)
|
||||
self._on_script.run()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Fire the off action."""
|
||||
call_from_config(self.hass, self._off_action, True)
|
||||
self._off_script.run()
|
||||
|
||||
def update(self):
|
||||
"""Update the state from the template."""
|
||||
|
@ -9,7 +9,7 @@ import logging
|
||||
from homeassistant.components import discovery
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
|
||||
REQUIREMENTS = ['pywemo==0.4.2']
|
||||
REQUIREMENTS = ['pywemo==0.4.3']
|
||||
|
||||
DOMAIN = 'wemo'
|
||||
DISCOVER_LIGHTS = 'wemo.light'
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user