mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
commit
d7b0929a32
@ -75,6 +75,9 @@ omit =
|
|||||||
homeassistant/components/zwave.py
|
homeassistant/components/zwave.py
|
||||||
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/alarmdotcom.py
|
||||||
homeassistant/components/alarm_control_panel/nx584.py
|
homeassistant/components/alarm_control_panel/nx584.py
|
||||||
homeassistant/components/binary_sensor/arest.py
|
homeassistant/components/binary_sensor/arest.py
|
||||||
@ -111,6 +114,8 @@ omit =
|
|||||||
homeassistant/components/light/hyperion.py
|
homeassistant/components/light/hyperion.py
|
||||||
homeassistant/components/light/lifx.py
|
homeassistant/components/light/lifx.py
|
||||||
homeassistant/components/light/limitlessled.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/cast.py
|
||||||
homeassistant/components/media_player/denon.py
|
homeassistant/components/media_player/denon.py
|
||||||
homeassistant/components/media_player/firetv.py
|
homeassistant/components/media_player/firetv.py
|
||||||
@ -156,6 +161,7 @@ omit =
|
|||||||
homeassistant/components/sensor/cpuspeed.py
|
homeassistant/components/sensor/cpuspeed.py
|
||||||
homeassistant/components/sensor/deutsche_bahn.py
|
homeassistant/components/sensor/deutsche_bahn.py
|
||||||
homeassistant/components/sensor/dht.py
|
homeassistant/components/sensor/dht.py
|
||||||
|
homeassistant/components/sensor/dte_energy_bridge.py
|
||||||
homeassistant/components/sensor/efergy.py
|
homeassistant/components/sensor/efergy.py
|
||||||
homeassistant/components/sensor/eliqonline.py
|
homeassistant/components/sensor/eliqonline.py
|
||||||
homeassistant/components/sensor/fitbit.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:**
|
**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#
|
**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
|
*.swo
|
||||||
|
|
||||||
ctags.tmp
|
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
|
ln -sf /usr/src/app/build/python-openzwave/openzwave/config /usr/local/share/python-openzwave/config
|
||||||
|
|
||||||
COPY requirements_all.txt requirements_all.txt
|
COPY requirements_all.txt requirements_all.txt
|
||||||
RUN pip3 install --no-cache-dir -r requirements_all.txt
|
# certifi breaks Debian based installs
|
||||||
|
RUN pip3 install --no-cache-dir -r requirements_all.txt && pip3 uninstall -y certifi
|
||||||
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*
|
|
||||||
|
|
||||||
# Copy source
|
# Copy source
|
||||||
COPY . .
|
COPY . .
|
||||||
|
@ -304,7 +304,6 @@ def setup_and_run_hass(config_dir, args):
|
|||||||
|
|
||||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser)
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, open_browser)
|
||||||
|
|
||||||
print('Starting Home-Assistant')
|
|
||||||
hass.start()
|
hass.start()
|
||||||
exit_code = int(hass.block_till_stopped())
|
exit_code = int(hass.block_till_stopped())
|
||||||
|
|
||||||
|
@ -7,14 +7,14 @@ https://home-assistant.io/components/alexa/
|
|||||||
import enum
|
import enum
|
||||||
import logging
|
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.helpers import template, script
|
||||||
|
from homeassistant.components.http import HomeAssistantView
|
||||||
|
|
||||||
DOMAIN = 'alexa'
|
DOMAIN = 'alexa'
|
||||||
DEPENDENCIES = ['http']
|
DEPENDENCIES = ['http']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
_CONFIG = {}
|
|
||||||
|
|
||||||
API_ENDPOINT = '/api/alexa'
|
API_ENDPOINT = '/api/alexa'
|
||||||
|
|
||||||
@ -26,80 +26,88 @@ CONF_ACTION = 'action'
|
|||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Activate Alexa component."""
|
"""Activate Alexa component."""
|
||||||
intents = config[DOMAIN].get(CONF_INTENTS, {})
|
hass.wsgi.register_view(AlexaView(hass,
|
||||||
|
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)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _handle_alexa(handler, path_match, data):
|
class AlexaView(HomeAssistantView):
|
||||||
"""Handle Alexa."""
|
"""Handle Alexa requests."""
|
||||||
_LOGGER.debug('Received Alexa request: %s', data)
|
|
||||||
|
|
||||||
req = data.get('request')
|
url = API_ENDPOINT
|
||||||
|
name = 'api:alexa'
|
||||||
|
|
||||||
if req is None:
|
def __init__(self, hass, intents):
|
||||||
_LOGGER.error('Received invalid data from Alexa: %s', data)
|
"""Initialize Alexa view."""
|
||||||
handler.write_json_message(
|
super().__init__(hass)
|
||||||
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
|
|
||||||
return
|
|
||||||
|
|
||||||
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':
|
self.intents = intents
|
||||||
handler.send_response(HTTP_OK)
|
|
||||||
handler.end_headers()
|
|
||||||
return
|
|
||||||
|
|
||||||
intent = req.get('intent')
|
def post(self, request):
|
||||||
response = AlexaResponse(handler.server.hass, intent)
|
"""Handle Alexa."""
|
||||||
|
data = request.json
|
||||||
|
|
||||||
if req_type == 'LaunchRequest':
|
_LOGGER.debug('Received Alexa request: %s', data)
|
||||||
response.add_speech(
|
|
||||||
SpeechType.plaintext,
|
|
||||||
"Hello, and welcome to the future. How may I help?")
|
|
||||||
handler.write_json(response.as_dict())
|
|
||||||
return
|
|
||||||
|
|
||||||
if req_type != 'IntentRequest':
|
req = data.get('request')
|
||||||
_LOGGER.warning('Received unsupported request: %s', req_type)
|
|
||||||
return
|
|
||||||
|
|
||||||
intent_name = intent['name']
|
if req is None:
|
||||||
config = _CONFIG.get(intent_name)
|
_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:
|
req_type = req['type']
|
||||||
_LOGGER.warning('Received unknown intent %s', intent_name)
|
|
||||||
response.add_speech(
|
|
||||||
SpeechType.plaintext,
|
|
||||||
"This intent is not yet configured within Home Assistant.")
|
|
||||||
handler.write_json(response.as_dict())
|
|
||||||
return
|
|
||||||
|
|
||||||
speech = config.get(CONF_SPEECH)
|
if req_type == 'SessionEndedRequest':
|
||||||
card = config.get(CONF_CARD)
|
return None
|
||||||
action = config.get(CONF_ACTION)
|
|
||||||
|
|
||||||
# pylint: disable=unsubscriptable-object
|
intent = req.get('intent')
|
||||||
if speech is not None:
|
response = AlexaResponse(self.hass, intent)
|
||||||
response.add_speech(SpeechType[speech['type']], speech['text'])
|
|
||||||
|
|
||||||
if card is not None:
|
if req_type == 'LaunchRequest':
|
||||||
response.add_card(CardType[card['type']], card['title'],
|
response.add_speech(
|
||||||
card['content'])
|
SpeechType.plaintext,
|
||||||
|
"Hello, and welcome to the future. How may I help?")
|
||||||
|
return self.json(response)
|
||||||
|
|
||||||
if action is not None:
|
if req_type != 'IntentRequest':
|
||||||
action.run(response.variables)
|
_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):
|
class SpeechType(enum.Enum):
|
||||||
|
@ -6,23 +6,23 @@ https://home-assistant.io/developers/api/
|
|||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
from time import time
|
||||||
import threading
|
|
||||||
|
|
||||||
import homeassistant.core as ha
|
import homeassistant.core as ha
|
||||||
import homeassistant.remote as rem
|
import homeassistant.remote as rem
|
||||||
from homeassistant.bootstrap import ERROR_LOG_FILENAME
|
from homeassistant.bootstrap import ERROR_LOG_FILENAME
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONTENT_TYPE_TEXT_PLAIN, EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED,
|
EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED,
|
||||||
HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_HEADER_CONTENT_TYPE, HTTP_NOT_FOUND,
|
HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND,
|
||||||
HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, MATCH_ALL, URL_API, URL_API_COMPONENTS,
|
HTTP_UNPROCESSABLE_ENTITY, MATCH_ALL, URL_API, URL_API_COMPONENTS,
|
||||||
URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG,
|
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,
|
URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE,
|
||||||
__version__)
|
__version__)
|
||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.exceptions import TemplateError
|
||||||
from homeassistant.helpers.state import TrackStates
|
from homeassistant.helpers.state import TrackStates
|
||||||
from homeassistant.helpers import template
|
from homeassistant.helpers import template
|
||||||
|
from homeassistant.components.http import HomeAssistantView
|
||||||
|
|
||||||
DOMAIN = 'api'
|
DOMAIN = 'api'
|
||||||
DEPENDENCIES = ['http']
|
DEPENDENCIES = ['http']
|
||||||
@ -35,372 +35,365 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Register the API with the HTTP interface."""
|
"""Register the API with the HTTP interface."""
|
||||||
# /api - for validation purposes
|
hass.wsgi.register_view(APIStatusView)
|
||||||
hass.http.register_path('GET', URL_API, _handle_get_api)
|
hass.wsgi.register_view(APIEventStream)
|
||||||
|
hass.wsgi.register_view(APIConfigView)
|
||||||
# /api/config
|
hass.wsgi.register_view(APIDiscoveryView)
|
||||||
hass.http.register_path('GET', URL_API_CONFIG, _handle_get_api_config)
|
hass.wsgi.register_view(APIStatesView)
|
||||||
|
hass.wsgi.register_view(APIEntityStateView)
|
||||||
# /api/discovery_info
|
hass.wsgi.register_view(APIEventListenersView)
|
||||||
hass.http.register_path('GET', URL_API_DISCOVERY_INFO,
|
hass.wsgi.register_view(APIEventView)
|
||||||
_handle_get_api_discovery_info,
|
hass.wsgi.register_view(APIServicesView)
|
||||||
require_auth=False)
|
hass.wsgi.register_view(APIDomainServicesView)
|
||||||
|
hass.wsgi.register_view(APIEventForwardingView)
|
||||||
# /api/stream
|
hass.wsgi.register_view(APIComponentsView)
|
||||||
hass.http.register_path('GET', URL_API_STREAM, _handle_get_api_stream)
|
hass.wsgi.register_view(APIErrorLogView)
|
||||||
|
hass.wsgi.register_view(APITemplateView)
|
||||||
# /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)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _handle_get_api(handler, path_match, data):
|
class APIStatusView(HomeAssistantView):
|
||||||
"""Render the debug interface."""
|
"""View to handle Status requests."""
|
||||||
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
|
|
||||||
|
|
||||||
restrict = data.get('restrict')
|
url = URL_API
|
||||||
if restrict:
|
name = "api:status"
|
||||||
restrict = restrict.split(',')
|
|
||||||
|
|
||||||
def write_message(payload):
|
def get(self, request):
|
||||||
"""Write a message to the output."""
|
"""Retrieve if API is running."""
|
||||||
with write_lock:
|
return self.json_message('API running.')
|
||||||
msg = "data: {}\n\n".format(payload)
|
|
||||||
|
|
||||||
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):
|
class APIEventStream(HomeAssistantView):
|
||||||
"""Forward events to the open request."""
|
"""View to handle EventStream requests."""
|
||||||
nonlocal gracefully_closed
|
|
||||||
|
|
||||||
if block.is_set() or event.event_type == EVENT_TIME_CHANGED:
|
url = URL_API_STREAM
|
||||||
return
|
name = "api:stream"
|
||||||
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
|
|
||||||
gracefully_closed = True
|
|
||||||
block.set()
|
|
||||||
return
|
|
||||||
|
|
||||||
handler.server.sessions.extend_validation(session_id)
|
def get(self, request):
|
||||||
write_message(json.dumps(event, cls=rem.JSONEncoder))
|
"""Provide a streaming interface for the event bus."""
|
||||||
|
from eventlet.queue import LightQueue, Empty
|
||||||
|
import eventlet
|
||||||
|
|
||||||
handler.send_response(HTTP_OK)
|
cur_hub = eventlet.hubs.get_hub()
|
||||||
handler.send_header('Content-type', 'text/event-stream')
|
request.environ['eventlet.minimum_write_chunk_size'] = 0
|
||||||
session_id = handler.set_session_cookie_header()
|
to_write = LightQueue()
|
||||||
handler.end_headers()
|
stop_obj = object()
|
||||||
|
|
||||||
if restrict:
|
restrict = request.args.get('restrict')
|
||||||
for event in restrict:
|
if restrict:
|
||||||
hass.bus.listen(event, forward_events)
|
restrict = restrict.split(',')
|
||||||
else:
|
|
||||||
hass.bus.listen(MATCH_ALL, forward_events)
|
|
||||||
|
|
||||||
while True:
|
def thread_forward_events(event):
|
||||||
write_message(STREAM_PING_PAYLOAD)
|
"""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():
|
_LOGGER.debug('STREAM %s FORWARDING %s', id(stop_obj), event)
|
||||||
break
|
|
||||||
|
|
||||||
if not gracefully_closed:
|
if event.event_type == EVENT_HOMEASSISTANT_STOP:
|
||||||
_LOGGER.info("Found broken event stream to %s, cleaning up",
|
data = stop_obj
|
||||||
handler.client_address[0])
|
else:
|
||||||
|
data = json.dumps(event, cls=rem.JSONEncoder)
|
||||||
|
|
||||||
if restrict:
|
cur_hub.schedule_call_global(0, lambda: to_write.put(data))
|
||||||
for event in restrict:
|
|
||||||
hass.bus.remove_listener(event, forward_events)
|
|
||||||
else:
|
|
||||||
hass.bus.remove_listener(MATCH_ALL, forward_events)
|
|
||||||
|
|
||||||
|
def stream():
|
||||||
|
"""Stream events to response."""
|
||||||
|
self.hass.bus.listen(MATCH_ALL, thread_forward_events)
|
||||||
|
|
||||||
def _handle_get_api_config(handler, path_match, data):
|
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
|
||||||
"""Return the Home Assistant configuration."""
|
|
||||||
handler.write_json(handler.server.hass.config.as_dict())
|
|
||||||
|
|
||||||
|
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):
|
while True:
|
||||||
needs_auth = (handler.server.hass.config.api.api_password is not None)
|
try:
|
||||||
params = {
|
# Somehow our queue.get sometimes takes too long to
|
||||||
'base_url': handler.server.hass.config.api.base_url,
|
# be notified of arrival of data. Probably
|
||||||
'location_name': handler.server.hass.config.location_name,
|
# because of our spawning on hub in other thread
|
||||||
'requires_api_password': needs_auth,
|
# hack. Because current goal is to get this out,
|
||||||
'version': __version__
|
# We just timeout every second because it will
|
||||||
}
|
# return right away if qsize() > 0.
|
||||||
handler.write_json(params)
|
# 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):
|
msg = "data: {}\n\n".format(payload)
|
||||||
"""Return a dict containing all entity ids and their state."""
|
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
|
||||||
handler.write_json(handler.server.hass.states.all())
|
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 self.Response(stream(), mimetype='text/event-stream')
|
||||||
"""Return the state of a specific entity."""
|
|
||||||
entity_id = path_match.group('entity_id')
|
|
||||||
|
|
||||||
state = handler.server.hass.states.get(entity_id)
|
|
||||||
|
|
||||||
if state:
|
class APIConfigView(HomeAssistantView):
|
||||||
handler.write_json(state)
|
"""View to handle Config requests."""
|
||||||
else:
|
|
||||||
handler.write_json_message("State does not exist.", HTTP_NOT_FOUND)
|
|
||||||
|
|
||||||
|
url = URL_API_CONFIG
|
||||||
|
name = "api:config"
|
||||||
|
|
||||||
def _handle_post_state_entity(handler, path_match, data):
|
def get(self, request):
|
||||||
"""Handle updating the state of an entity.
|
"""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:
|
class APIDiscoveryView(HomeAssistantView):
|
||||||
new_state = data['state']
|
"""View to provide discovery info."""
|
||||||
except KeyError:
|
|
||||||
handler.write_json_message("state not specified", HTTP_BAD_REQUEST)
|
|
||||||
return
|
|
||||||
|
|
||||||
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(
|
def get(self, request):
|
||||||
state.as_dict(),
|
"""Get current states."""
|
||||||
status_code=status_code,
|
return self.json(self.hass.states.all())
|
||||||
location=URL_API_STATES_ENTITY.format(entity_id))
|
|
||||||
|
|
||||||
|
|
||||||
def _handle_delete_state_entity(handler, path_match, data):
|
class APIEntityStateView(HomeAssistantView):
|
||||||
"""Handle request to delete an entity from state machine.
|
"""View to handle EntityState requests."""
|
||||||
|
|
||||||
This handles the following paths:
|
url = "/api/states/<entity(exist=False):entity_id>"
|
||||||
/api/states/<entity_id>
|
name = "api:entity-state"
|
||||||
"""
|
|
||||||
entity_id = path_match.group('entity_id')
|
|
||||||
|
|
||||||
if handler.server.hass.states.remove(entity_id):
|
def get(self, request, entity_id):
|
||||||
handler.write_json_message(
|
"""Retrieve state of entity."""
|
||||||
"Entity not found", HTTP_NOT_FOUND)
|
state = self.hass.states.get(entity_id)
|
||||||
else:
|
if state:
|
||||||
handler.write_json_message(
|
return self.json(state)
|
||||||
"Entity removed", HTTP_OK)
|
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):
|
attributes = request.json.get('attributes')
|
||||||
"""Handle getting overview of event listeners."""
|
|
||||||
handler.write_json(events_json(handler.server.hass))
|
|
||||||
|
|
||||||
|
is_new_state = self.hass.states.get(entity_id) is None
|
||||||
|
|
||||||
def _handle_api_post_events_event(handler, path_match, event_data):
|
# Write state
|
||||||
"""Handle firing of an event.
|
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.
|
if is_new_state:
|
||||||
"""
|
resp.status_code = HTTP_CREATED
|
||||||
event_type = path_match.group('event_type')
|
|
||||||
|
|
||||||
if event_data is not None and not isinstance(event_data, dict):
|
resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id))
|
||||||
handler.write_json_message(
|
|
||||||
"event_data should be an object", HTTP_UNPROCESSABLE_ENTITY)
|
|
||||||
return
|
|
||||||
|
|
||||||
event_origin = ha.EventOrigin.remote
|
return resp
|
||||||
|
|
||||||
# Special case handling for event STATE_CHANGED
|
def delete(self, request, entity_id):
|
||||||
# We will try to convert state dicts back to State objects
|
"""Remove entity."""
|
||||||
if event_type == ha.EVENT_STATE_CHANGED and event_data:
|
if self.hass.states.remove(entity_id):
|
||||||
for key in ('old_state', 'new_state'):
|
return self.json_message('Entity removed')
|
||||||
state = ha.State.from_dict(event_data.get(key))
|
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
|
url = '/api/events/<event_type>'
|
||||||
def _handle_post_api_services_domain_service(handler, path_match, data):
|
name = "api:event"
|
||||||
"""Handle calling a service.
|
|
||||||
|
|
||||||
This handles the following paths: /api/services/<domain>/<service>
|
def post(self, request, event_type):
|
||||||
"""
|
"""Fire events."""
|
||||||
domain = path_match.group('domain')
|
event_data = request.json
|
||||||
service = path_match.group('service')
|
|
||||||
|
|
||||||
with TrackStates(handler.server.hass) as changed_states:
|
if event_data is not None and not isinstance(event_data, dict):
|
||||||
handler.server.hass.services.call(domain, service, data, True)
|
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
|
self.hass.bus.fire(event_type, event_data, ha.EventOrigin.remote)
|
||||||
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
|
|
||||||
|
|
||||||
try:
|
return self.json_message("Event {} fired.".format(event_type))
|
||||||
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
|
|
||||||
|
|
||||||
api = rem.API(host, api_password, port)
|
|
||||||
|
|
||||||
if not api.validate_api():
|
class APIServicesView(HomeAssistantView):
|
||||||
handler.write_json_message(
|
"""View to handle Services requests."""
|
||||||
"Unable to validate API", HTTP_UNPROCESSABLE_ENTITY)
|
|
||||||
return
|
|
||||||
|
|
||||||
if handler.server.event_forwarder is None:
|
url = URL_API_SERVICES
|
||||||
handler.server.event_forwarder = \
|
name = "api:services"
|
||||||
rem.EventForwarder(handler.server.hass)
|
|
||||||
|
|
||||||
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):
|
url = "/api/services/<domain>/<service>"
|
||||||
"""Handle deleting an event forwarding target."""
|
name = "api:domain-services"
|
||||||
try:
|
|
||||||
host = data['host']
|
|
||||||
except KeyError:
|
|
||||||
handler.write_json_message("No host received.", HTTP_BAD_REQUEST)
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
def post(self, request, domain, service):
|
||||||
port = int(data['port']) if 'port' in data else None
|
"""Call a service.
|
||||||
except ValueError:
|
|
||||||
handler.write_json_message(
|
|
||||||
"Invalid value received for port", HTTP_UNPROCESSABLE_ENTITY)
|
|
||||||
return
|
|
||||||
|
|
||||||
if handler.server.event_forwarder is not None:
|
Returns a list of changed states.
|
||||||
api = rem.API(host, None, port)
|
"""
|
||||||
|
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):
|
url = URL_API_EVENT_FORWARD
|
||||||
"""Return all the loaded components."""
|
name = "api:event-forward"
|
||||||
handler.write_json(handler.server.hass.config.components)
|
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):
|
try:
|
||||||
"""Return the logged errors for this session."""
|
port = int(data['port']) if 'port' in data else None
|
||||||
handler.write_file(handler.server.hass.config.path(ERROR_LOG_FILENAME),
|
except ValueError:
|
||||||
False)
|
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):
|
if not api.validate_api():
|
||||||
"""Log user out."""
|
return self.json_message("Unable to validate API.",
|
||||||
handler.send_response(HTTP_OK)
|
HTTP_UNPROCESSABLE_ENTITY)
|
||||||
handler.destroy_session()
|
|
||||||
handler.end_headers()
|
|
||||||
|
|
||||||
|
if self.event_forwarder is None:
|
||||||
|
self.event_forwarder = rem.EventForwarder(self.hass)
|
||||||
|
|
||||||
def _handle_post_api_template(handler, path_match, data):
|
self.event_forwarder.connect(api)
|
||||||
"""Log user out."""
|
|
||||||
template_string = data.get('template', '')
|
|
||||||
|
|
||||||
try:
|
return self.json_message("Event forwarding setup.")
|
||||||
rendered = template.render(handler.server.hass, template_string)
|
|
||||||
|
|
||||||
handler.send_response(HTTP_OK)
|
def delete(self, request):
|
||||||
handler.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN)
|
"""Remove event forwarer."""
|
||||||
handler.end_headers()
|
data = request.json
|
||||||
handler.wfile.write(rendered.encode('utf-8'))
|
if data is None:
|
||||||
except TemplateError as e:
|
return self.json_message("No data received.", HTTP_BAD_REQUEST)
|
||||||
handler.write_json_message(str(e), HTTP_UNPROCESSABLE_ENTITY)
|
|
||||||
return
|
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):
|
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,
|
from homeassistant.components.binary_sensor import (BinarySensorDevice,
|
||||||
ENTITY_ID_FORMAT,
|
ENTITY_ID_FORMAT,
|
||||||
SENSOR_CLASSES)
|
SENSOR_CLASSES)
|
||||||
from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_VALUE_TEMPLATE
|
from homeassistant.const import (ATTR_FRIENDLY_NAME, CONF_VALUE_TEMPLATE,
|
||||||
from homeassistant.core import EVENT_STATE_CHANGED
|
ATTR_ENTITY_ID, MATCH_ALL)
|
||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.exceptions import TemplateError
|
||||||
from homeassistant.helpers.entity import generate_entity_id
|
from homeassistant.helpers.entity import generate_entity_id
|
||||||
from homeassistant.helpers import template
|
from homeassistant.helpers import template
|
||||||
|
from homeassistant.helpers.event import track_state_change
|
||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
CONF_SENSORS = 'sensors'
|
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)
|
'Missing %s for sensor %s', CONF_VALUE_TEMPLATE, device)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
entity_ids = device_config.get(ATTR_ENTITY_ID, MATCH_ALL)
|
||||||
|
|
||||||
sensors.append(
|
sensors.append(
|
||||||
BinarySensorTemplate(
|
BinarySensorTemplate(
|
||||||
hass,
|
hass,
|
||||||
device,
|
device,
|
||||||
friendly_name,
|
friendly_name,
|
||||||
sensor_class,
|
sensor_class,
|
||||||
value_template)
|
value_template,
|
||||||
|
entity_ids)
|
||||||
)
|
)
|
||||||
if not sensors:
|
if not sensors:
|
||||||
_LOGGER.error('No sensors added')
|
_LOGGER.error('No sensors added')
|
||||||
@ -73,7 +77,7 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
# pylint: disable=too-many-arguments
|
||||||
def __init__(self, hass, device, friendly_name, sensor_class,
|
def __init__(self, hass, device, friendly_name, sensor_class,
|
||||||
value_template):
|
value_template, entity_ids):
|
||||||
"""Initialize the Template binary sensor."""
|
"""Initialize the Template binary sensor."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, device,
|
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, device,
|
||||||
@ -85,12 +89,12 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||||||
|
|
||||||
self.update()
|
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."""
|
"""Called when the target device changes state."""
|
||||||
self.update_ha_state(True)
|
self.update_ha_state(True)
|
||||||
|
|
||||||
hass.bus.listen(EVENT_STATE_CHANGED,
|
track_state_change(hass, entity_ids,
|
||||||
template_bsensor_event_listener)
|
template_bsensor_state_listener)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
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/
|
https://home-assistant.io/components/camera/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
import time
|
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
from homeassistant.components import bloomsky
|
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.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||||
|
from homeassistant.components.http import HomeAssistantView
|
||||||
|
|
||||||
DOMAIN = 'camera'
|
DOMAIN = 'camera'
|
||||||
DEPENDENCIES = ['http']
|
DEPENDENCIES = ['http']
|
||||||
@ -32,10 +27,7 @@ STATE_RECORDING = 'recording'
|
|||||||
STATE_STREAMING = 'streaming'
|
STATE_STREAMING = 'streaming'
|
||||||
STATE_IDLE = 'idle'
|
STATE_IDLE = 'idle'
|
||||||
|
|
||||||
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}'
|
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
|
||||||
|
|
||||||
MULTIPART_BOUNDARY = '--jpgboundary'
|
|
||||||
MJPEG_START_HEADER = 'Content-type: {0}\r\n\r\n'
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-branches
|
# pylint: disable=too-many-branches
|
||||||
@ -45,57 +37,11 @@ def setup(hass, config):
|
|||||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
|
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
|
||||||
DISCOVERY_PLATFORMS)
|
DISCOVERY_PLATFORMS)
|
||||||
|
|
||||||
|
hass.wsgi.register_view(CameraImageView(hass, component.entities))
|
||||||
|
hass.wsgi.register_view(CameraMjpegStream(hass, component.entities))
|
||||||
|
|
||||||
component.setup(config)
|
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
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -106,6 +52,11 @@ class Camera(Entity):
|
|||||||
"""Initialize a camera."""
|
"""Initialize a camera."""
|
||||||
self.is_streaming = False
|
self.is_streaming = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def access_token(self):
|
||||||
|
"""Access token for this camera."""
|
||||||
|
return str(id(self))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
"""No need to poll cameras."""
|
"""No need to poll cameras."""
|
||||||
@ -114,7 +65,7 @@ class Camera(Entity):
|
|||||||
@property
|
@property
|
||||||
def entity_picture(self):
|
def entity_picture(self):
|
||||||
"""Return a link to the camera feed as entity picture."""
|
"""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
|
@property
|
||||||
def is_recording(self):
|
def is_recording(self):
|
||||||
@ -135,32 +86,35 @@ class Camera(Entity):
|
|||||||
"""Return bytes of camera image."""
|
"""Return bytes of camera image."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def mjpeg_stream(self, handler):
|
def mjpeg_stream(self, response):
|
||||||
"""Generate an HTTP MJPEG stream from camera images."""
|
"""Generate an HTTP MJPEG stream from camera images."""
|
||||||
def write_string(text):
|
import eventlet
|
||||||
"""Helper method to write a string to the stream."""
|
response.content_type = ('multipart/x-mixed-replace; '
|
||||||
handler.request.sendall(bytes(text + '\r\n', 'utf-8'))
|
'boundary=--jpegboundary')
|
||||||
|
|
||||||
write_string('HTTP/1.1 200 OK')
|
def stream():
|
||||||
write_string('Content-type: multipart/x-mixed-replace; '
|
"""Stream images as mjpeg stream."""
|
||||||
'boundary={}'.format(MULTIPART_BOUNDARY))
|
try:
|
||||||
write_string('')
|
last_image = None
|
||||||
write_string(MULTIPART_BOUNDARY)
|
while True:
|
||||||
|
img_bytes = self.camera_image()
|
||||||
|
|
||||||
while True:
|
if img_bytes is not None and img_bytes != last_image:
|
||||||
img_bytes = self.camera_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:
|
last_image = img_bytes
|
||||||
continue
|
|
||||||
|
|
||||||
write_string('Content-length: {}'.format(len(img_bytes)))
|
eventlet.sleep(0.5)
|
||||||
write_string('Content-type: image/jpeg')
|
except GeneratorExit:
|
||||||
write_string('')
|
pass
|
||||||
handler.request.sendall(img_bytes)
|
|
||||||
write_string('')
|
|
||||||
write_string(MULTIPART_BOUNDARY)
|
|
||||||
|
|
||||||
time.sleep(0.5)
|
response.response = stream()
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
@ -175,7 +129,9 @@ class Camera(Entity):
|
|||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def state_attributes(self):
|
||||||
"""Camera state attributes."""
|
"""Camera state attributes."""
|
||||||
attr = {}
|
attr = {
|
||||||
|
'access_token': self.access_token,
|
||||||
|
}
|
||||||
|
|
||||||
if self.model:
|
if self.model:
|
||||||
attr['model_name'] = self.model
|
attr['model_name'] = self.model
|
||||||
@ -184,3 +140,60 @@ class Camera(Entity):
|
|||||||
attr['brand'] = self.brand
|
attr['brand'] = self.brand
|
||||||
|
|
||||||
return attr
|
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 requests.auth import HTTPBasicAuth
|
||||||
|
|
||||||
from homeassistant.components.camera import DOMAIN, Camera
|
from homeassistant.components.camera import DOMAIN, Camera
|
||||||
from homeassistant.const import HTTP_OK
|
|
||||||
from homeassistant.helpers import validate_config
|
from homeassistant.helpers import validate_config
|
||||||
|
|
||||||
CONTENT_TYPE_HEADER = 'Content-Type'
|
CONTENT_TYPE_HEADER = 'Content-Type'
|
||||||
@ -68,19 +67,12 @@ class MjpegCamera(Camera):
|
|||||||
with closing(self.camera_stream()) as response:
|
with closing(self.camera_stream()) as response:
|
||||||
return process_response(response)
|
return process_response(response)
|
||||||
|
|
||||||
def mjpeg_stream(self, handler):
|
def mjpeg_stream(self, response):
|
||||||
"""Generate an HTTP MJPEG stream from the camera."""
|
"""Generate an HTTP MJPEG stream from the camera."""
|
||||||
response = self.camera_stream()
|
stream = self.camera_stream()
|
||||||
content_type = response.headers[CONTENT_TYPE_HEADER]
|
response.mimetype = stream.headers[CONTENT_TYPE_HEADER]
|
||||||
|
response.response = stream.iter_content(chunk_size=1024)
|
||||||
handler.send_response(HTTP_OK)
|
return response
|
||||||
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)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
@ -12,7 +12,7 @@ import requests
|
|||||||
from homeassistant.components.camera import DOMAIN, Camera
|
from homeassistant.components.camera import DOMAIN, Camera
|
||||||
from homeassistant.helpers import validate_config
|
from homeassistant.helpers import validate_config
|
||||||
|
|
||||||
REQUIREMENTS = ['uvcclient==0.8']
|
REQUIREMENTS = ['uvcclient==0.9.0']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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))
|
_LOGGER.error('Unable to connect to NVR: %s', str(ex))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
identifier = nvrconn.server_version >= (3, 2, 0) and 'id' or 'uuid'
|
||||||
# Filter out airCam models, which are not supported in the latest
|
# Filter out airCam models, which are not supported in the latest
|
||||||
# version of UnifiVideo and which are EOL by Ubiquiti
|
# version of UnifiVideo and which are EOL by Ubiquiti
|
||||||
cameras = [camera for camera in cameras
|
cameras = [
|
||||||
if 'airCam' not in nvrconn.get_camera(camera['uuid'])['model']]
|
camera for camera in cameras
|
||||||
|
if 'airCam' not in nvrconn.get_camera(camera[identifier])['model']]
|
||||||
|
|
||||||
add_devices([UnifiVideoCamera(nvrconn,
|
add_devices([UnifiVideoCamera(nvrconn,
|
||||||
camera['uuid'],
|
camera[identifier],
|
||||||
camera['name'])
|
camera['name'])
|
||||||
for camera in cameras])
|
for camera in cameras])
|
||||||
return True
|
return True
|
||||||
@ -110,12 +112,17 @@ class UnifiVideoCamera(Camera):
|
|||||||
dict(name=self._name))
|
dict(name=self._name))
|
||||||
password = 'ubnt'
|
password = 'ubnt'
|
||||||
|
|
||||||
|
if self._nvr.server_version >= (3, 2, 0):
|
||||||
|
client_cls = uvc_camera.UVCCameraClientV320
|
||||||
|
else:
|
||||||
|
client_cls = uvc_camera.UVCCameraClient
|
||||||
|
|
||||||
camera = None
|
camera = None
|
||||||
for addr in addrs:
|
for addr in addrs:
|
||||||
try:
|
try:
|
||||||
camera = uvc_camera.UVCCameraClient(addr,
|
camera = client_cls(addr,
|
||||||
caminfo['username'],
|
caminfo['username'],
|
||||||
password)
|
password)
|
||||||
camera.login()
|
camera.login()
|
||||||
_LOGGER.debug('Logged into UVC camera %(name)s via %(addr)s',
|
_LOGGER.debug('Logged into UVC camera %(name)s via %(addr)s',
|
||||||
dict(name=self._name, addr=addr))
|
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+)')
|
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):
|
def setup(hass, config):
|
||||||
|
@ -17,7 +17,7 @@ from homeassistant.config import load_yaml_config_file
|
|||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import config_per_platform
|
from homeassistant.helpers import config_per_platform
|
||||||
from homeassistant.helpers.entity import Entity
|
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 as util
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
@ -26,6 +26,7 @@ from homeassistant.const import (
|
|||||||
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE,
|
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE,
|
||||||
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME)
|
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME)
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
|
||||||
DOMAIN = "device_tracker"
|
DOMAIN = "device_tracker"
|
||||||
DEPENDENCIES = ['zone']
|
DEPENDENCIES = ['zone']
|
||||||
|
|
||||||
@ -193,7 +194,7 @@ class DeviceTracker(object):
|
|||||||
if not device:
|
if not device:
|
||||||
dev_id = util.slugify(host_name or '') or util.slugify(mac)
|
dev_id = util.slugify(host_name or '') or util.slugify(mac)
|
||||||
else:
|
else:
|
||||||
dev_id = str(dev_id).lower()
|
dev_id = cv.slug(str(dev_id).lower())
|
||||||
device = self.devices.get(dev_id)
|
device = self.devices.get(dev_id)
|
||||||
|
|
||||||
if device:
|
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/
|
https://home-assistant.io/components/device_tracker.locative/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from functools import partial
|
|
||||||
|
|
||||||
from homeassistant.components.device_tracker import DOMAIN
|
from homeassistant.components.device_tracker import DOMAIN
|
||||||
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME
|
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME
|
||||||
|
from homeassistant.components.http import HomeAssistantView
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEPENDENCIES = ['http']
|
DEPENDENCIES = ['http']
|
||||||
|
|
||||||
URL_API_LOCATIVE_ENDPOINT = "/api/locative"
|
|
||||||
|
|
||||||
|
|
||||||
def setup_scanner(hass, config, see):
|
def setup_scanner(hass, config, see):
|
||||||
"""Setup an endpoint for the Locative application."""
|
"""Setup an endpoint for the Locative application."""
|
||||||
# POST would be semantically better, but that currently does not work
|
hass.wsgi.register_view(LocativeView(hass, see))
|
||||||
# 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))
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _handle_get_api_locative(hass, see, handler, path_match, data):
|
class LocativeView(HomeAssistantView):
|
||||||
"""Locative message received."""
|
"""View to handle locative requests."""
|
||||||
if not _check_data(handler, data):
|
|
||||||
return
|
|
||||||
|
|
||||||
device = data['device'].replace('-', '')
|
url = "/api/locative"
|
||||||
location_name = data['id'].lower()
|
name = "api:bootstrap"
|
||||||
direction = data['trigger']
|
|
||||||
|
|
||||||
if direction == 'enter':
|
def __init__(self, hass, see):
|
||||||
see(dev_id=device, location_name=location_name)
|
"""Initialize Locative url endpoints."""
|
||||||
handler.write_text("Setting location to {}".format(location_name))
|
super().__init__(hass)
|
||||||
|
self.see = see
|
||||||
|
|
||||||
elif direction == 'exit':
|
def get(self, request):
|
||||||
current_state = hass.states.get("{}.{}".format(DOMAIN, device))
|
"""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:
|
else:
|
||||||
# Ignore the message if it is telling us to exit a zone that we
|
_LOGGER.error("Received unidentified message from Locative: %s",
|
||||||
# aren't currently in. This occurs when a zone is entered before
|
direction)
|
||||||
# the previous zone was exited. The enter message will be sent
|
return ("Received unidentified message: {}".format(direction),
|
||||||
# first, then the exit message will be sent second.
|
HTTP_UNPROCESSABLE_ENTITY)
|
||||||
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
|
|
||||||
|
@ -186,7 +186,7 @@ def setup_scanner(hass, config, see):
|
|||||||
def _parse_see_args(topic, data):
|
def _parse_see_args(topic, data):
|
||||||
"""Parse the OwnTracks location parameters, into the format see expects."""
|
"""Parse the OwnTracks location parameters, into the format see expects."""
|
||||||
parts = topic.split('/')
|
parts = topic.split('/')
|
||||||
dev_id = '{}_{}'.format(parts[1], parts[2])
|
dev_id = slugify('{}_{}'.format(parts[1], parts[2]))
|
||||||
host_name = parts[1]
|
host_name = parts[1]
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'dev_id': dev_id,
|
'dev_id': dev_id,
|
||||||
|
@ -18,7 +18,7 @@ from homeassistant.util import Throttle
|
|||||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
REQUIREMENTS = ['pysnmp==4.2.5']
|
REQUIREMENTS = ['pysnmp==4.3.2']
|
||||||
|
|
||||||
CONF_COMMUNITY = "community"
|
CONF_COMMUNITY = "community"
|
||||||
CONF_BASEOID = "baseoid"
|
CONF_BASEOID = "baseoid"
|
||||||
@ -72,7 +72,7 @@ class SnmpScanner(object):
|
|||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||||
def _update_info(self):
|
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.
|
Return boolean if scanning successful.
|
||||||
"""
|
"""
|
||||||
@ -88,7 +88,7 @@ class SnmpScanner(object):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def get_snmp_data(self):
|
def get_snmp_data(self):
|
||||||
"""Fetch MAC addresses from WAP via SNMP."""
|
"""Fetch MAC addresses from access point via SNMP."""
|
||||||
devices = []
|
devices = []
|
||||||
|
|
||||||
errindication, errstatus, errindex, restable = self.snmp.nextCmd(
|
errindication, errstatus, errindex, restable = self.snmp.nextCmd(
|
||||||
@ -97,9 +97,10 @@ class SnmpScanner(object):
|
|||||||
if errindication:
|
if errindication:
|
||||||
_LOGGER.error("SNMPLIB error: %s", errindication)
|
_LOGGER.error("SNMPLIB error: %s", errindication)
|
||||||
return
|
return
|
||||||
|
# pylint: disable=no-member
|
||||||
if errstatus:
|
if errstatus:
|
||||||
_LOGGER.error('SNMP error: %s at %s', errstatus.prettyPrint(),
|
_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
|
return
|
||||||
|
|
||||||
for resrow in restable:
|
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 datetime import datetime
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
from os.path import exists
|
||||||
|
from threading import Lock
|
||||||
|
import pickle
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||||
from homeassistant.helpers.event import track_utc_time_change
|
from homeassistant.helpers.event import track_utc_time_change
|
||||||
|
|
||||||
@ -27,14 +31,15 @@ MAX_ENTRIES = 20
|
|||||||
class FeedManager(object):
|
class FeedManager(object):
|
||||||
"""Abstraction over feedparser module."""
|
"""Abstraction over feedparser module."""
|
||||||
|
|
||||||
def __init__(self, url, hass):
|
def __init__(self, url, hass, storage):
|
||||||
"""Initialize the FeedManager object, poll every hour."""
|
"""Initialize the FeedManager object, poll every hour."""
|
||||||
self._url = url
|
self._url = url
|
||||||
self._feed = None
|
self._feed = None
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._firstrun = True
|
self._firstrun = True
|
||||||
# Initialize last entry timestamp as epoch time
|
self._storage = storage
|
||||||
self._last_entry_timestamp = datetime.utcfromtimestamp(0).timetuple()
|
self._last_entry_timestamp = None
|
||||||
|
self._has_published_parsed = False
|
||||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START,
|
hass.bus.listen_once(EVENT_HOMEASSISTANT_START,
|
||||||
lambda _: self._update())
|
lambda _: self._update())
|
||||||
track_utc_time_change(hass, lambda now: self._update(),
|
track_utc_time_change(hass, lambda now: self._update(),
|
||||||
@ -42,7 +47,7 @@ class FeedManager(object):
|
|||||||
|
|
||||||
def _log_no_entries(self):
|
def _log_no_entries(self):
|
||||||
"""Send no entries log at debug level."""
|
"""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):
|
def _update(self):
|
||||||
"""Update the feed and publish new entries to the event bus."""
|
"""Update the feed and publish new entries to the event bus."""
|
||||||
@ -65,10 +70,13 @@ class FeedManager(object):
|
|||||||
len(self._feed.entries),
|
len(self._feed.entries),
|
||||||
self._url)
|
self._url)
|
||||||
if len(self._feed.entries) > MAX_ENTRIES:
|
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)
|
'in feed "%s"', MAX_ENTRIES, self._url)
|
||||||
self._feed.entries = self._feed.entries[0:MAX_ENTRIES]
|
self._feed.entries = self._feed.entries[0:MAX_ENTRIES]
|
||||||
self._publish_new_entries()
|
self._publish_new_entries()
|
||||||
|
if self._has_published_parsed:
|
||||||
|
self._storage.put_timestamp(self._url,
|
||||||
|
self._last_entry_timestamp)
|
||||||
else:
|
else:
|
||||||
self._log_no_entries()
|
self._log_no_entries()
|
||||||
_LOGGER.info('Fetch from feed "%s" completed', self._url)
|
_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
|
# let's make use of it to publish only new available
|
||||||
# entries since the last run
|
# entries since the last run
|
||||||
if 'published_parsed' in entry.keys():
|
if 'published_parsed' in entry.keys():
|
||||||
|
self._has_published_parsed = True
|
||||||
self._last_entry_timestamp = max(entry.published_parsed,
|
self._last_entry_timestamp = max(entry.published_parsed,
|
||||||
self._last_entry_timestamp)
|
self._last_entry_timestamp)
|
||||||
else:
|
else:
|
||||||
|
self._has_published_parsed = False
|
||||||
_LOGGER.debug('No `published_parsed` info available '
|
_LOGGER.debug('No `published_parsed` info available '
|
||||||
'for entry "%s"', entry.title)
|
'for entry "%s"', entry.title)
|
||||||
entry.update({'feed_url': self._url})
|
entry.update({'feed_url': self._url})
|
||||||
@ -90,6 +100,13 @@ class FeedManager(object):
|
|||||||
def _publish_new_entries(self):
|
def _publish_new_entries(self):
|
||||||
"""Publish new entries to the event bus."""
|
"""Publish new entries to the event bus."""
|
||||||
new_entries = False
|
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:
|
for entry in self._feed.entries:
|
||||||
if self._firstrun or (
|
if self._firstrun or (
|
||||||
'published_parsed' in entry.keys() and
|
'published_parsed' in entry.keys() and
|
||||||
@ -103,8 +120,55 @@ class FeedManager(object):
|
|||||||
self._firstrun = False
|
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):
|
def setup(hass, config):
|
||||||
"""Setup the feedreader component."""
|
"""Setup the feedreader component."""
|
||||||
urls = config.get(DOMAIN)['urls']
|
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
|
return len(feeds) > 0
|
||||||
|
@ -1,121 +1,101 @@
|
|||||||
"""Handle the frontend for Home Assistant."""
|
"""Handle the frontend for Home Assistant."""
|
||||||
import re
|
|
||||||
import os
|
import os
|
||||||
import logging
|
|
||||||
|
|
||||||
from . import version, mdi_version
|
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 import api
|
||||||
|
from homeassistant.components.http import HomeAssistantView
|
||||||
|
|
||||||
DOMAIN = 'frontend'
|
DOMAIN = 'frontend'
|
||||||
DEPENDENCIES = ['api']
|
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):
|
def setup(hass, config):
|
||||||
"""Setup serving the frontend."""
|
"""Setup serving the frontend."""
|
||||||
for url in FRONTEND_URLS:
|
hass.wsgi.register_view(IndexView)
|
||||||
hass.http.register_path('GET', url, _handle_get_root, False)
|
hass.wsgi.register_view(BootstrapView)
|
||||||
|
|
||||||
hass.http.register_path('GET', '/service_worker.js',
|
www_static_path = os.path.join(os.path.dirname(__file__), 'www_static')
|
||||||
_handle_get_service_worker, False)
|
if hass.wsgi.development:
|
||||||
|
|
||||||
# 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:
|
|
||||||
sw_path = "home-assistant-polymer/build/service_worker.js"
|
sw_path = "home-assistant-polymer/build/service_worker.js"
|
||||||
else:
|
else:
|
||||||
sw_path = "service_worker.js"
|
sw_path = "service_worker.js"
|
||||||
|
|
||||||
handler.write_file(os.path.join(os.path.dirname(__file__), 'www_static',
|
hass.wsgi.register_static_path(
|
||||||
sw_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):
|
class BootstrapView(HomeAssistantView):
|
||||||
"""Return a static file for the frontend."""
|
"""View to bootstrap frontend with all needed data."""
|
||||||
req_file = util.sanitize_path(path_match.group('file'))
|
|
||||||
|
|
||||||
# Strip md5 hash out
|
url = "/api/bootstrap"
|
||||||
fingerprinted = _FINGERPRINT.match(req_file)
|
name = "api:bootstrap"
|
||||||
if fingerprinted:
|
|
||||||
req_file = "{}.{}".format(*fingerprinted.groups())
|
|
||||||
|
|
||||||
path = os.path.join(os.path.dirname(__file__), 'www_static', req_file)
|
def get(self, request):
|
||||||
|
"""Return all data needed to bootstrap Home Assistant."""
|
||||||
handler.write_file(path)
|
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):
|
class IndexView(HomeAssistantView):
|
||||||
"""Return a static file from the hass.config.path/www for the frontend."""
|
"""Serve the frontend."""
|
||||||
req_file = util.sanitize_path(path_match.group('file'))
|
|
||||||
|
|
||||||
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."""
|
"""DO NOT MODIFY. Auto-generated by update_mdi script."""
|
||||||
VERSION = "1baebe8155deb447230866d7ae854bd9"
|
VERSION = "9ee3d4466a65bef35c2c8974e91b37c0"
|
||||||
|
@ -9,6 +9,11 @@
|
|||||||
<link rel='apple-touch-icon' sizes='180x180'
|
<link rel='apple-touch-icon' sizes='180x180'
|
||||||
href='/static/favicon-apple-180x180.png'>
|
href='/static/favicon-apple-180x180.png'>
|
||||||
<meta name='apple-mobile-web-app-capable' content='yes'>
|
<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='mobile-web-app-capable' content='yes'>
|
||||||
<meta name='viewport' content='width=device-width, user-scalable=no'>
|
<meta name='viewport' content='width=device-width, user-scalable=no'>
|
||||||
<meta name='theme-color' content='#03a9f4'>
|
<meta name='theme-color' content='#03a9f4'>
|
||||||
@ -28,7 +33,7 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
margin-bottom: 97px;
|
margin-bottom: 83px;
|
||||||
font-family: Roboto, sans-serif;
|
font-family: Roboto, sans-serif;
|
||||||
font-size: 0pt;
|
font-size: 0pt;
|
||||||
transition: font-size 2s;
|
transition: font-size 2s;
|
||||||
@ -36,6 +41,7 @@
|
|||||||
|
|
||||||
#ha-init-skeleton paper-spinner {
|
#ha-init-skeleton paper-spinner {
|
||||||
height: 28px;
|
height: 28px;
|
||||||
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ha-init-skeleton a {
|
#ha-init-skeleton a {
|
||||||
@ -59,8 +65,8 @@
|
|||||||
.getElementById('ha-init-skeleton')
|
.getElementById('ha-init-skeleton')
|
||||||
.classList.add('error');
|
.classList.add('error');
|
||||||
}
|
}
|
||||||
|
window.noAuth = {{ auth }}
|
||||||
</script>
|
</script>
|
||||||
<link rel='import' href='/static/{{ app_url }}' onerror='initError()' async>
|
|
||||||
</head>
|
</head>
|
||||||
<body fullbleed>
|
<body fullbleed>
|
||||||
<div id='ha-init-skeleton'>
|
<div id='ha-init-skeleton'>
|
||||||
@ -68,6 +74,10 @@
|
|||||||
<paper-spinner active></paper-spinner>
|
<paper-spinner active></paper-spinner>
|
||||||
Home Assistant had trouble<br>connecting to the server.<br><br><a href='/'>TRY AGAIN</a>
|
Home Assistant had trouble<br>connecting to the server.<br><br><a href='/'>TRY AGAIN</a>
|
||||||
</div>
|
</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>
|
<script>
|
||||||
var webComponentsSupported = (
|
var webComponentsSupported = (
|
||||||
'registerElement' in document &&
|
'registerElement' in document &&
|
||||||
@ -81,6 +91,5 @@
|
|||||||
document.head.appendChild(script)
|
document.head.appendChild(script)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<home-assistant auth='{{ auth }}' icons='{{ icons }}'></home-assistant>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -1,2 +1,3 @@
|
|||||||
"""DO NOT MODIFY. Auto-generated by build_frontend script."""
|
"""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",
|
"src": "/static/favicon-192x192.png",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png",
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/static/favicon-384x384.png",
|
"src": "/static/favicon-384x384.png",
|
||||||
"sizes": "384x384",
|
"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
|
from homeassistant.components import recorder, script
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.const import HTTP_BAD_REQUEST
|
from homeassistant.components.http import HomeAssistantView
|
||||||
|
|
||||||
DOMAIN = 'history'
|
DOMAIN = 'history'
|
||||||
DEPENDENCIES = ['recorder', 'http']
|
DEPENDENCIES = ['recorder', 'http']
|
||||||
@ -155,49 +155,44 @@ def get_state(utc_point_in_time, entity_id, run=None):
|
|||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Setup the history hooks."""
|
"""Setup the history hooks."""
|
||||||
hass.http.register_path(
|
hass.wsgi.register_view(Last5StatesView)
|
||||||
'GET',
|
hass.wsgi.register_view(HistoryPeriodView)
|
||||||
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)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
class Last5StatesView(HomeAssistantView):
|
||||||
# pylint: disable=invalid-name
|
"""Handle last 5 state view requests."""
|
||||||
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')
|
|
||||||
|
|
||||||
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):
|
class HistoryPeriodView(HomeAssistantView):
|
||||||
"""Return history over a period of time."""
|
"""Handle history period requests."""
|
||||||
date_str = path_match.group('date')
|
|
||||||
one_day = timedelta(seconds=86400)
|
|
||||||
|
|
||||||
if date_str:
|
url = '/api/history/period'
|
||||||
start_date = dt_util.parse_date(date_str)
|
name = 'api:history:view-period'
|
||||||
|
extra_urls = ['/api/history/period/<date:date>']
|
||||||
|
|
||||||
if start_date is None:
|
def get(self, request, date=None):
|
||||||
handler.write_json_message("Error parsing JSON", HTTP_BAD_REQUEST)
|
"""Return history over a period of time."""
|
||||||
return
|
one_day = timedelta(days=1)
|
||||||
|
|
||||||
start_time = dt_util.as_utc(dt_util.start_of_local_day(start_date))
|
if date:
|
||||||
else:
|
start_time = dt_util.as_utc(dt_util.start_of_local_day(date))
|
||||||
start_time = dt_util.utcnow() - one_day
|
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')
|
return self.json(
|
||||||
|
get_significant_states(start_time, end_time, entity_id).values())
|
||||||
handler.write_json(
|
|
||||||
get_significant_states(start_time, end_time, entity_id).values())
|
|
||||||
|
|
||||||
|
|
||||||
def _is_significant(state):
|
def _is_significant(state):
|
||||||
|
@ -1,41 +1,25 @@
|
|||||||
"""
|
"""This module provides WSGI application to serve the Home Assistant API."""
|
||||||
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
|
|
||||||
import hmac
|
import hmac
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import ssl
|
import mimetypes
|
||||||
import threading
|
import threading
|
||||||
import time
|
import re
|
||||||
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 voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.bootstrap as bootstrap
|
|
||||||
import homeassistant.core as ha
|
import homeassistant.core as ha
|
||||||
import homeassistant.remote as rem
|
import homeassistant.remote as rem
|
||||||
import homeassistant.util as util
|
from homeassistant import util
|
||||||
import homeassistant.util.dt as date_util
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONTENT_TYPE_JSON, CONTENT_TYPE_TEXT_PLAIN, HTTP_HEADER_ACCEPT_ENCODING,
|
SERVER_PORT, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CACHE_CONTROL,
|
||||||
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,
|
|
||||||
HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
|
HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
|
||||||
HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, HTTP_METHOD_NOT_ALLOWED,
|
HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS)
|
||||||
HTTP_NOT_FOUND, HTTP_OK, HTTP_UNAUTHORIZED, HTTP_UNPROCESSABLE_ENTITY,
|
from homeassistant.helpers.entity import split_entity_id
|
||||||
ALLOWED_CORS_HEADERS,
|
import homeassistant.util.dt as dt_util
|
||||||
SERVER_PORT, URL_ROOT, URL_API_EVENT_FORWARD)
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
DOMAIN = "http"
|
DOMAIN = "http"
|
||||||
|
REQUIREMENTS = ("eventlet==0.19.0", "static3==0.7.0", "Werkzeug==0.11.5",)
|
||||||
|
|
||||||
CONF_API_PASSWORD = "api_password"
|
CONF_API_PASSWORD = "api_password"
|
||||||
CONF_SERVER_HOST = "server_host"
|
CONF_SERVER_HOST = "server_host"
|
||||||
@ -47,10 +31,7 @@ CONF_CORS_ORIGINS = 'cors_allowed_origins'
|
|||||||
|
|
||||||
DATA_API_PASSWORD = 'api_password'
|
DATA_API_PASSWORD = 'api_password'
|
||||||
|
|
||||||
# Throttling time in seconds for expired sessions check
|
_FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
|
||||||
SESSION_CLEAR_INTERVAL = timedelta(seconds=20)
|
|
||||||
SESSION_TIMEOUT_SECONDS = 1800
|
|
||||||
SESSION_KEY = 'sessionId'
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -68,13 +49,32 @@ CONFIG_SCHEMA = vol.Schema({
|
|||||||
}, extra=vol.ALLOW_EXTRA)
|
}, 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):
|
def setup(hass, config):
|
||||||
"""Set up the HTTP API and debug interface."""
|
"""Set up the HTTP API and debug interface."""
|
||||||
|
_LOGGER.addFilter(HideSensitiveFilter(hass))
|
||||||
|
|
||||||
conf = config.get(DOMAIN, {})
|
conf = config.get(DOMAIN, {})
|
||||||
|
|
||||||
api_password = util.convert(conf.get(CONF_API_PASSWORD), str)
|
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_host = conf.get(CONF_SERVER_HOST, '0.0.0.0')
|
||||||
server_port = conf.get(CONF_SERVER_PORT, SERVER_PORT)
|
server_port = conf.get(CONF_SERVER_PORT, SERVER_PORT)
|
||||||
development = str(conf.get(CONF_DEVELOPMENT, "")) == "1"
|
development = str(conf.get(CONF_DEVELOPMENT, "")) == "1"
|
||||||
@ -82,22 +82,24 @@ def setup(hass, config):
|
|||||||
ssl_key = conf.get(CONF_SSL_KEY)
|
ssl_key = conf.get(CONF_SSL_KEY)
|
||||||
cors_origins = conf.get(CONF_CORS_ORIGINS, [])
|
cors_origins = conf.get(CONF_CORS_ORIGINS, [])
|
||||||
|
|
||||||
try:
|
server = HomeAssistantWSGI(
|
||||||
server = HomeAssistantHTTPServer(
|
hass,
|
||||||
(server_host, server_port), RequestHandler, hass, api_password,
|
development=development,
|
||||||
development, ssl_certificate, ssl_key, cors_origins)
|
server_host=server_host,
|
||||||
except OSError:
|
server_port=server_port,
|
||||||
# If address already in use
|
api_password=api_password,
|
||||||
_LOGGER.exception("Error setting up HTTP server")
|
ssl_certificate=ssl_certificate,
|
||||||
return False
|
ssl_key=ssl_key,
|
||||||
|
cors_origins=cors_origins
|
||||||
|
)
|
||||||
|
|
||||||
hass.bus.listen_once(
|
hass.bus.listen_once(
|
||||||
ha.EVENT_HOMEASSISTANT_START,
|
ha.EVENT_HOMEASSISTANT_START,
|
||||||
lambda event:
|
lambda event:
|
||||||
threading.Thread(target=server.start, daemon=True,
|
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'
|
hass.config.api = rem.API(server_host if server_host != '0.0.0.0'
|
||||||
else util.get_local_ip(),
|
else util.get_local_ip(),
|
||||||
api_password, server_port,
|
api_password, server_port,
|
||||||
@ -106,413 +108,338 @@ def setup(hass, config):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-instance-attributes
|
def request_class():
|
||||||
class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer):
|
"""Generate request class.
|
||||||
"""Handle HTTP requests in a threaded fashion."""
|
|
||||||
|
|
||||||
# pylint: disable=too-few-public-methods
|
Done in method because of imports.
|
||||||
allow_reuse_address = True
|
"""
|
||||||
daemon_threads = True
|
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
|
# 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.hass = hass
|
||||||
self.api_password = api_password
|
self.extra_apps = {}
|
||||||
self.development = development
|
self.development = development
|
||||||
self.paths = []
|
self.api_password = api_password
|
||||||
self.sessions = SessionStore()
|
self.ssl_certificate = ssl_certificate
|
||||||
self.use_ssl = ssl_certificate is not None
|
self.ssl_key = ssl_key
|
||||||
|
self.server_host = server_host
|
||||||
|
self.server_port = server_port
|
||||||
self.cors_origins = cors_origins
|
self.cors_origins = cors_origins
|
||||||
|
|
||||||
# We will lazy init this one if needed
|
|
||||||
self.event_forwarder = None
|
self.event_forwarder = None
|
||||||
|
|
||||||
if development:
|
def register_view(self, view):
|
||||||
_LOGGER.info("running http in development mode")
|
"""Register a view with the WSGI server.
|
||||||
|
|
||||||
if ssl_certificate is not None:
|
The view argument must be a class that inherits from HomeAssistantView.
|
||||||
context = ssl.create_default_context(
|
It is optional to instantiate it before registering; this method will
|
||||||
purpose=ssl.Purpose.CLIENT_AUTH)
|
handle it either way.
|
||||||
context.load_cert_chain(ssl_certificate, keyfile=ssl_key)
|
"""
|
||||||
self.socket = context.wrap_socket(self.socket, server_side=True)
|
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):
|
def start(self):
|
||||||
"""Start the HTTP server."""
|
"""Start the wsgi server."""
|
||||||
def stop_http(event):
|
from eventlet import wsgi
|
||||||
"""Stop the HTTP server."""
|
import eventlet
|
||||||
self.shutdown()
|
|
||||||
|
|
||||||
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(
|
with request:
|
||||||
"Starting web interface at %s://%s:%d",
|
adapter = self.url_map.bind_to_environ(request.environ)
|
||||||
protocol, self.server_address[0], self.server_address[1])
|
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
|
def base_app(self, environ, start_response):
|
||||||
# To prevent stuff from breaking, load the two extracted components
|
"""WSGI Handler of requests to base app."""
|
||||||
bootstrap.setup_component(self.hass, 'api')
|
request = self.Request(environ)
|
||||||
bootstrap.setup_component(self.hass, 'frontend')
|
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):
|
return response(environ, start_response)
|
||||||
"""Register a path with the server."""
|
|
||||||
self.paths.append((method, url, callback, require_auth))
|
|
||||||
|
|
||||||
def log_message(self, fmt, *args):
|
def __call__(self, environ, start_response):
|
||||||
"""Redirect built-in log to HA logging."""
|
"""Handle a request for base app + extra apps."""
|
||||||
# pylint: disable=no-self-use
|
from werkzeug.wsgi import DispatcherMiddleware
|
||||||
_LOGGER.info(fmt, *args)
|
|
||||||
|
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 HomeAssistantView(object):
|
||||||
class RequestHandler(SimpleHTTPRequestHandler):
|
"""Base view for all views."""
|
||||||
"""Handle incoming HTTP requests.
|
|
||||||
|
|
||||||
We extend from SimpleHTTPRequestHandler instead of Base so we
|
extra_urls = []
|
||||||
can use the guess content type methods.
|
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):
|
if not hasattr(self, 'url'):
|
||||||
"""Constructor, call the base constructor and set up session."""
|
class_name = self.__class__.__name__
|
||||||
# Track if this was an authenticated request
|
raise AttributeError(
|
||||||
self.authenticated = False
|
'{0} missing required attribute "url"'.format(class_name)
|
||||||
SimpleHTTPRequestHandler.__init__(self, req, client_addr, server)
|
)
|
||||||
self.protocol_version = 'HTTP/1.1'
|
|
||||||
|
|
||||||
def log_message(self, fmt, *arguments):
|
if not hasattr(self, 'name'):
|
||||||
"""Redirect built-in log to HA logging."""
|
class_name = self.__class__.__name__
|
||||||
if self.server.api_password is None:
|
raise AttributeError(
|
||||||
_LOGGER.info(fmt, *arguments)
|
'{0} missing required attribute "name"'.format(class_name)
|
||||||
else:
|
)
|
||||||
_LOGGER.info(
|
|
||||||
fmt, *(arg.replace(self.server.api_password, '*******')
|
|
||||||
if isinstance(arg, str) else arg for arg in arguments))
|
|
||||||
|
|
||||||
def _handle_request(self, method): # pylint: disable=too-many-branches
|
self.hass = hass
|
||||||
"""Perform some common checks and call appropriate method."""
|
# pylint: disable=invalid-name
|
||||||
url = urlparse(self.path)
|
self.Response = Response
|
||||||
|
|
||||||
# Read query input. parse_qs gives a list for each value, we want last
|
def handle_request(self, request, **values):
|
||||||
data = {key: data[-1] for key, data in parse_qs(url.query).items()}
|
"""Handle request to url."""
|
||||||
|
from werkzeug.exceptions import MethodNotAllowed, Unauthorized
|
||||||
|
|
||||||
# Did we get post input ?
|
try:
|
||||||
content_length = int(self.headers.get(HTTP_HEADER_CONTENT_LENGTH, 0))
|
handler = getattr(self, request.method.lower())
|
||||||
|
except AttributeError:
|
||||||
|
raise MethodNotAllowed
|
||||||
|
|
||||||
if content_length:
|
# Auth code verbose on purpose
|
||||||
body_content = self.rfile.read(content_length).decode("UTF-8")
|
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:
|
try:
|
||||||
data.update(json.loads(body_content))
|
fil = open(fil)
|
||||||
except (TypeError, ValueError):
|
except IOError:
|
||||||
# TypeError if JSON object is not a dict
|
raise NotFound()
|
||||||
# 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
|
|
||||||
|
|
||||||
if self.verify_session():
|
return self.Response(wrap_file(request.environ, fil),
|
||||||
# The user has a valid session already
|
mimetype=mimetype, direct_passthrough=True)
|
||||||
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
|
|
||||||
|
@ -468,12 +468,12 @@ class HvacDevice(Entity):
|
|||||||
@property
|
@property
|
||||||
def min_temp(self):
|
def min_temp(self):
|
||||||
"""Return the minimum temperature."""
|
"""Return the minimum temperature."""
|
||||||
return convert(7, TEMP_CELCIUS, self.unit_of_measurement)
|
return convert(19, TEMP_CELCIUS, self.unit_of_measurement)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def max_temp(self):
|
def max_temp(self):
|
||||||
"""Return the maximum temperature."""
|
"""Return the maximum temperature."""
|
||||||
return convert(35, TEMP_CELCIUS, self.unit_of_measurement)
|
return convert(30, TEMP_CELCIUS, self.unit_of_measurement)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def min_humidity(self):
|
def min_humidity(self):
|
||||||
|
@ -233,13 +233,3 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice):
|
|||||||
class_id=COMMAND_CLASS_CONFIGURATION).values():
|
class_id=COMMAND_CLASS_CONFIGURATION).values():
|
||||||
if value.command_class == 112 and value.index == 33:
|
if value.command_class == 112 and value.index == 33:
|
||||||
value.data = int(swing_mode)
|
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,
|
ATTR_DISCOVERED, ATTR_SERVICE, CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME,
|
||||||
EVENT_PLATFORM_DISCOVERED)
|
EVENT_PLATFORM_DISCOVERED)
|
||||||
from homeassistant.helpers import validate_config
|
from homeassistant.helpers import validate_config
|
||||||
from homeassistant.helpers.entity import ToggleEntity
|
|
||||||
from homeassistant.loader import get_component
|
from homeassistant.loader import get_component
|
||||||
|
|
||||||
DOMAIN = "insteon_hub"
|
DOMAIN = "insteon_hub"
|
||||||
@ -53,43 +52,3 @@ def setup(hass, config):
|
|||||||
EVENT_PLATFORM_DISCOVERED,
|
EVENT_PLATFORM_DISCOVERED,
|
||||||
{ATTR_SERVICE: discovery, ATTR_DISCOVERED: {}})
|
{ATTR_SERVICE: discovery, ATTR_DISCOVERED: {}})
|
||||||
return True
|
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
|
from homeassistant.loader import get_component
|
||||||
|
|
||||||
DOMAIN = "isy994"
|
DOMAIN = "isy994"
|
||||||
REQUIREMENTS = ['PyISY==1.0.5']
|
REQUIREMENTS = ['PyISY==1.0.6']
|
||||||
DISCOVER_LIGHTS = "isy994.lights"
|
DISCOVER_LIGHTS = "isy994.lights"
|
||||||
DISCOVER_SWITCHES = "isy994.switches"
|
DISCOVER_SWITCHES = "isy994.switches"
|
||||||
DISCOVER_SENSORS = "isy994.sensors"
|
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
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/insteon_hub/
|
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):
|
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":
|
if device.DeviceCategory == "Dimmable Lighting Control":
|
||||||
devs.append(InsteonToggleDevice(device))
|
devs.append(InsteonToggleDevice(device))
|
||||||
add_devices(devs)
|
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.components import recorder, sun
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
|
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 DOMAIN as HA_DOMAIN
|
||||||
from homeassistant.core import State
|
from homeassistant.core import State
|
||||||
from homeassistant.helpers.entity import split_entity_id
|
from homeassistant.helpers.entity import split_entity_id
|
||||||
from homeassistant.helpers import template
|
from homeassistant.helpers import template
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.components.http import HomeAssistantView
|
||||||
|
|
||||||
DOMAIN = "logbook"
|
DOMAIN = "logbook"
|
||||||
DEPENDENCIES = ['recorder', 'http']
|
DEPENDENCIES = ['recorder', 'http']
|
||||||
@ -76,34 +77,34 @@ def setup(hass, config):
|
|||||||
message = template.render(hass, message)
|
message = template.render(hass, message)
|
||||||
log_entry(hass, name, message, domain, entity_id)
|
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,
|
hass.services.register(DOMAIN, 'log', log_message,
|
||||||
schema=LOG_MESSAGE_SCHEMA)
|
schema=LOG_MESSAGE_SCHEMA)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _handle_get_logbook(handler, path_match, data):
|
class LogbookView(HomeAssistantView):
|
||||||
"""Return logbook entries."""
|
"""Handle logbook view requests."""
|
||||||
date_str = path_match.group('date')
|
|
||||||
|
|
||||||
if date_str:
|
url = '/api/logbook'
|
||||||
start_date = dt_util.parse_date(date_str)
|
name = 'api:logbook'
|
||||||
|
extra_urls = ['/api/logbook/<date:date>']
|
||||||
|
|
||||||
if start_date is None:
|
def get(self, request, date=None):
|
||||||
handler.write_json_message("Error parsing JSON", HTTP_BAD_REQUEST)
|
"""Retrieve logbook entries."""
|
||||||
return
|
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)
|
end_day = start_day + timedelta(days=1)
|
||||||
else:
|
|
||||||
start_day = dt_util.start_of_local_day()
|
|
||||||
|
|
||||||
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(
|
return self.json(humanify(events))
|
||||||
QUERY_EVENTS_BETWEEN,
|
|
||||||
(dt_util.as_utc(start_day), dt_util.as_utc(end_day)))
|
|
||||||
|
|
||||||
handler.write_json(humanify(events))
|
|
||||||
|
|
||||||
|
|
||||||
class Entry(object):
|
class Entry(object):
|
||||||
|
@ -10,7 +10,7 @@ import urllib
|
|||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
|
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
|
||||||
SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP,
|
SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP,
|
||||||
MediaPlayerDevice)
|
SUPPORT_TURN_OFF, MediaPlayerDevice)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING)
|
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING)
|
||||||
|
|
||||||
@ -36,7 +36,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
url,
|
url,
|
||||||
auth=(
|
auth=(
|
||||||
config.get('user', ''),
|
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."""
|
"""Representation of a XBMC/Kodi device."""
|
||||||
|
|
||||||
# pylint: disable=too-many-public-methods, abstract-method
|
# 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."""
|
"""Initialize the Kodi device."""
|
||||||
import jsonrpc_requests
|
import jsonrpc_requests
|
||||||
self._name = name
|
self._name = name
|
||||||
@ -52,6 +54,7 @@ class KodiDevice(MediaPlayerDevice):
|
|||||||
self._server = jsonrpc_requests.Server(
|
self._server = jsonrpc_requests.Server(
|
||||||
'{}/jsonrpc'.format(self._url),
|
'{}/jsonrpc'.format(self._url),
|
||||||
auth=auth)
|
auth=auth)
|
||||||
|
self._turn_off_action = turn_off_action
|
||||||
self._players = list()
|
self._players = list()
|
||||||
self._properties = None
|
self._properties = None
|
||||||
self._item = None
|
self._item = None
|
||||||
@ -181,11 +184,29 @@ class KodiDevice(MediaPlayerDevice):
|
|||||||
@property
|
@property
|
||||||
def supported_media_commands(self):
|
def supported_media_commands(self):
|
||||||
"""Flag of media commands that are supported."""
|
"""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):
|
def turn_off(self):
|
||||||
"""Turn off media player."""
|
"""Execute turn_off_action to turn off media player."""
|
||||||
self._server.System.Shutdown()
|
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()
|
self.update_ha_state()
|
||||||
|
|
||||||
def volume_up(self):
|
def volume_up(self):
|
||||||
|
@ -37,7 +37,8 @@ PLATFORM_SCHEMA = vol.Schema({
|
|||||||
vol.Required(CONF_PLATFORM): "lg_netcast",
|
vol.Required(CONF_PLATFORM): "lg_netcast",
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
vol.Required(CONF_HOST): 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):
|
def update(self):
|
||||||
"""Retrieve latest state."""
|
"""Retrieve latest state."""
|
||||||
self.roku_name = "roku_" + self.roku.device_info.sernum
|
import requests.exceptions
|
||||||
self.ip_address = self.roku.host
|
|
||||||
self.channels = self.get_source_list()
|
|
||||||
|
|
||||||
if self.roku.current_app is not None:
|
try:
|
||||||
self.current_app = self.roku.current_app
|
self.roku_name = "roku_" + self.roku.device_info.sernum
|
||||||
else:
|
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
|
self.current_app = None
|
||||||
|
|
||||||
def get_source_list(self):
|
def get_source_list(self):
|
||||||
@ -92,6 +97,9 @@ class RokuDevice(MediaPlayerDevice):
|
|||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the device."""
|
"""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"]:
|
if self.current_app.name in ["Power Saver", "Default screensaver"]:
|
||||||
return STATE_IDLE
|
return STATE_IDLE
|
||||||
elif self.current_app.name == "Roku":
|
elif self.current_app.name == "Roku":
|
||||||
@ -137,17 +145,20 @@ class RokuDevice(MediaPlayerDevice):
|
|||||||
@property
|
@property
|
||||||
def app_name(self):
|
def app_name(self):
|
||||||
"""Name of the current running app."""
|
"""Name of the current running app."""
|
||||||
return self.current_app.name
|
if self.current_app is not None:
|
||||||
|
return self.current_app.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def app_id(self):
|
def app_id(self):
|
||||||
"""Return the ID of the current running app."""
|
"""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
|
@property
|
||||||
def source(self):
|
def source(self):
|
||||||
"""Return the current input source."""
|
"""Return the current input source."""
|
||||||
return self.current_app.name
|
if self.current_app is not None:
|
||||||
|
return self.current_app.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def source_list(self):
|
def source_list(self):
|
||||||
@ -156,32 +167,39 @@ class RokuDevice(MediaPlayerDevice):
|
|||||||
|
|
||||||
def media_play_pause(self):
|
def media_play_pause(self):
|
||||||
"""Send play/pause command."""
|
"""Send play/pause command."""
|
||||||
self.roku.play()
|
if self.current_app is not None:
|
||||||
|
self.roku.play()
|
||||||
|
|
||||||
def media_previous_track(self):
|
def media_previous_track(self):
|
||||||
"""Send previous track command."""
|
"""Send previous track command."""
|
||||||
self.roku.reverse()
|
if self.current_app is not None:
|
||||||
|
self.roku.reverse()
|
||||||
|
|
||||||
def media_next_track(self):
|
def media_next_track(self):
|
||||||
"""Send next track command."""
|
"""Send next track command."""
|
||||||
self.roku.forward()
|
if self.current_app is not None:
|
||||||
|
self.roku.forward()
|
||||||
|
|
||||||
def mute_volume(self, mute):
|
def mute_volume(self, mute):
|
||||||
"""Mute the volume."""
|
"""Mute the volume."""
|
||||||
self.roku.volume_mute()
|
if self.current_app is not None:
|
||||||
|
self.roku.volume_mute()
|
||||||
|
|
||||||
def volume_up(self):
|
def volume_up(self):
|
||||||
"""Volume up media player."""
|
"""Volume up media player."""
|
||||||
self.roku.volume_up()
|
if self.current_app is not None:
|
||||||
|
self.roku.volume_up()
|
||||||
|
|
||||||
def volume_down(self):
|
def volume_down(self):
|
||||||
"""Volume down media player."""
|
"""Volume down media player."""
|
||||||
self.roku.volume_down()
|
if self.current_app is not None:
|
||||||
|
self.roku.volume_down()
|
||||||
|
|
||||||
def select_source(self, source):
|
def select_source(self, source):
|
||||||
"""Select input source."""
|
"""Select input source."""
|
||||||
if source == "Home":
|
if self.current_app is not None:
|
||||||
self.roku.home()
|
if source == "Home":
|
||||||
else:
|
self.roku.home()
|
||||||
channel = self.roku[source]
|
else:
|
||||||
channel.launch()
|
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.const import CONF_API_KEY
|
||||||
from homeassistant.helpers import validate_config
|
from homeassistant.helpers import validate_config
|
||||||
|
|
||||||
REQUIREMENTS = ['slacker==0.9.10']
|
REQUIREMENTS = ['slacker==0.9.16']
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ from homeassistant.helpers import validate_config
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REQUIREMENTS = ['python-telegram-bot==4.1.1']
|
REQUIREMENTS = ['python-telegram-bot==4.2.0']
|
||||||
|
|
||||||
|
|
||||||
def get_service(hass, config):
|
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.light import ATTR_BRIGHTNESS
|
||||||
from homeassistant.components.discovery import load_platform
|
from homeassistant.components.discovery import load_platform
|
||||||
|
|
||||||
REQUIREMENTS = ['https://github.com/kellerza/pyqwikswitch/archive/v0.3.zip'
|
REQUIREMENTS = ['https://github.com/kellerza/pyqwikswitch/archive/v0.4.zip'
|
||||||
'#pyqwikswitch==0.3']
|
'#pyqwikswitch==0.4']
|
||||||
DEPENDENCIES = []
|
DEPENDENCIES = []
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -14,7 +14,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
|||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.const import (ATTR_ENTITY_ID, TEMP_CELSIUS)
|
from homeassistant.const import (ATTR_ENTITY_ID, TEMP_CELSIUS)
|
||||||
|
|
||||||
REQUIREMENTS = ['pyRFXtrx==0.6.5']
|
REQUIREMENTS = ['pyRFXtrx==0.8.0']
|
||||||
|
|
||||||
DOMAIN = "rfxtrx"
|
DOMAIN = "rfxtrx"
|
||||||
|
|
||||||
@ -310,6 +310,7 @@ class RfxtrxDevice(Entity):
|
|||||||
self.update_ha_state()
|
self.update_ha_state()
|
||||||
|
|
||||||
def _send_command(self, command, brightness=0):
|
def _send_command(self, command, brightness=0):
|
||||||
|
# pylint: disable=too-many-return-statements,too-many-branches
|
||||||
if not self._event:
|
if not self._event:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -330,4 +331,16 @@ class RfxtrxDevice(Entity):
|
|||||||
self._state = False
|
self._state = False
|
||||||
self._brightness = 0
|
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()
|
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.helpers.entity import Entity
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
REQUIREMENTS = ['blockchain==1.3.1']
|
REQUIREMENTS = ['blockchain==1.3.3']
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
OPTION_TYPES = {
|
OPTION_TYPES = {
|
||||||
'exchangerate': ['Exchange rate (1 BTC)', None],
|
'exchangerate': ['Exchange rate (1 BTC)', None],
|
||||||
|
@ -10,7 +10,7 @@ from homeassistant.util import Throttle
|
|||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
REQUIREMENTS = ['schiene==0.15']
|
REQUIREMENTS = ['schiene==0.17']
|
||||||
ICON = 'mdi:train'
|
ICON = 'mdi:train'
|
||||||
|
|
||||||
# Return cached results if last scan was less then this time ago.
|
# 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 datetime
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from homeassistant.const import HTTP_OK, TEMP_CELSIUS
|
from homeassistant.const import TEMP_CELSIUS
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.loader import get_component
|
from homeassistant.loader import get_component
|
||||||
|
from homeassistant.components.http import HomeAssistantView
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
REQUIREMENTS = ["fitbit==0.2.2"]
|
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,
|
redirect_uri = "{}{}".format(hass.config.api.base_url,
|
||||||
FITBIT_AUTH_CALLBACK_PATH)
|
FITBIT_AUTH_CALLBACK_PATH)
|
||||||
|
|
||||||
def _start_fitbit_auth(handler, path_match, data):
|
fitbit_auth_start_url, _ = oauth.authorize_token_url(
|
||||||
"""Start Fitbit OAuth2 flow."""
|
redirect_uri=redirect_uri,
|
||||||
url, _ = oauth.authorize_token_url(redirect_uri=redirect_uri,
|
scope=["activity", "heartrate", "nutrition", "profile",
|
||||||
scope=["activity", "heartrate",
|
"settings", "sleep", "weight"])
|
||||||
"nutrition", "profile",
|
|
||||||
"settings", "sleep",
|
|
||||||
"weight"])
|
|
||||||
handler.send_response(301)
|
|
||||||
handler.send_header("Location", url)
|
|
||||||
handler.end_headers()
|
|
||||||
|
|
||||||
def _finish_fitbit_auth(handler, path_match, data):
|
hass.wsgi.register_redirect(FITBIT_AUTH_START, fitbit_auth_start_url)
|
||||||
"""Finish Fitbit OAuth2 flow."""
|
hass.wsgi.register_view(FitbitAuthCallbackView(hass, config,
|
||||||
response_message = """Fitbit has been successfully authorized!
|
add_devices, oauth))
|
||||||
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)
|
|
||||||
|
|
||||||
request_oauth_completion(hass)
|
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
|
# pylint: disable=too-few-public-methods
|
||||||
class FitbitSensor(Entity):
|
class FitbitSensor(Entity):
|
||||||
"""Implementation of a Fitbit sensor."""
|
"""Implementation of a Fitbit sensor."""
|
||||||
|
@ -37,7 +37,7 @@ SENSOR_TYPES = {
|
|||||||
'wind_bearing': ['Wind Bearing', '°', '°', '°', '°', '°'],
|
'wind_bearing': ['Wind Bearing', '°', '°', '°', '°', '°'],
|
||||||
'cloud_cover': ['Cloud Coverage', '%', '%', '%', '%', '%'],
|
'cloud_cover': ['Cloud Coverage', '%', '%', '%', '%', '%'],
|
||||||
'humidity': ['Humidity', '%', '%', '%', '%', '%'],
|
'humidity': ['Humidity', '%', '%', '%', '%', '%'],
|
||||||
'pressure': ['Pressure', 'mBar', 'mBar', 'mBar', 'mBar', 'mBar'],
|
'pressure': ['Pressure', 'mbar', 'mbar', 'mbar', 'mbar', 'mbar'],
|
||||||
'visibility': ['Visibility', 'km', 'm', 'km', 'km', 'm'],
|
'visibility': ['Visibility', 'km', 'm', 'km', 'km', 'm'],
|
||||||
'ozone': ['Ozone', 'DU', 'DU', 'DU', 'DU', 'DU'],
|
'ozone': ['Ozone', 'DU', 'DU', 'DU', 'DU', 'DU'],
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ PLATFORM_SCHEMA = vol.Schema({
|
|||||||
vol.Required(CONF_DESTINATION): vol.Coerce(str),
|
vol.Required(CONF_DESTINATION): vol.Coerce(str),
|
||||||
vol.Optional(CONF_TRAVEL_MODE):
|
vol.Optional(CONF_TRAVEL_MODE):
|
||||||
vol.In(["driving", "walking", "bicycling", "transit"]),
|
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({
|
dict, vol.Schema({
|
||||||
vol.Optional(CONF_MODE, default='driving'):
|
vol.Optional(CONF_MODE, default='driving'):
|
||||||
vol.In(["driving", "walking", "bicycling", "transit"]),
|
vol.In(["driving", "walking", "bicycling", "transit"]),
|
||||||
@ -178,7 +178,7 @@ class GoogleTravelTimeSensor(Entity):
|
|||||||
options_copy['departure_time'] = convert_time_to_utc(dtime)
|
options_copy['departure_time'] = convert_time_to_utc(dtime)
|
||||||
elif dtime is not None:
|
elif dtime is not None:
|
||||||
options_copy['departure_time'] = dtime
|
options_copy['departure_time'] = dtime
|
||||||
else:
|
elif atime is None:
|
||||||
options_copy['departure_time'] = 'now'
|
options_copy['departure_time'] = 'now'
|
||||||
|
|
||||||
if atime is not None and ':' in atime:
|
if atime is not None and ':' in atime:
|
||||||
|
@ -14,7 +14,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
DOMAIN = "loopenergy"
|
DOMAIN = "loopenergy"
|
||||||
|
|
||||||
REQUIREMENTS = ['pyloopenergy==0.0.12']
|
REQUIREMENTS = ['pyloopenergy==0.0.13']
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
|
@ -33,6 +33,7 @@ SENSOR_TYPES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CONF_SECRET_KEY = 'secret_key'
|
CONF_SECRET_KEY = 'secret_key'
|
||||||
|
CONF_STATION = 'station'
|
||||||
ATTR_MODULE = 'modules'
|
ATTR_MODULE = 'modules'
|
||||||
|
|
||||||
# Return cached results if last scan was less then this time ago
|
# 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.")
|
"Please check your settings for NatAtmo API.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
data = NetAtmoData(authorization)
|
data = NetAtmoData(authorization, config.get(CONF_STATION, None))
|
||||||
|
|
||||||
dev = []
|
dev = []
|
||||||
try:
|
try:
|
||||||
@ -149,10 +150,11 @@ class NetAtmoSensor(Entity):
|
|||||||
class NetAtmoData(object):
|
class NetAtmoData(object):
|
||||||
"""Get the latest data from NetAtmo."""
|
"""Get the latest data from NetAtmo."""
|
||||||
|
|
||||||
def __init__(self, auth):
|
def __init__(self, auth, station):
|
||||||
"""Initialize the data object."""
|
"""Initialize the data object."""
|
||||||
self.auth = auth
|
self.auth = auth
|
||||||
self.data = None
|
self.data = None
|
||||||
|
self.station = station
|
||||||
|
|
||||||
def get_module_names(self):
|
def get_module_names(self):
|
||||||
"""Return all module available on the API as a list."""
|
"""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."""
|
"""Call the NetAtmo API to update the data."""
|
||||||
import lnetatmo
|
import lnetatmo
|
||||||
dev_list = lnetatmo.DeviceList(self.auth)
|
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
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the sensor."""
|
"""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
|
@property
|
||||||
def unit_of_measurement(self):
|
def unit_of_measurement(self):
|
||||||
|
@ -7,7 +7,12 @@ https://home-assistant.io/components/sensor.openweathermap/
|
|||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
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.helpers.entity import Entity
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
@ -24,6 +29,15 @@ SENSOR_TYPES = {
|
|||||||
'snow': ['Snow', 'mm']
|
'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.
|
# Return cached results if last scan was less then this time ago.
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
|
||||||
|
|
||||||
|
@ -8,11 +8,12 @@ import logging
|
|||||||
|
|
||||||
from homeassistant.components.sensor import ENTITY_ID_FORMAT
|
from homeassistant.components.sensor import ENTITY_ID_FORMAT
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE)
|
ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE,
|
||||||
from homeassistant.core import EVENT_STATE_CHANGED
|
ATTR_ENTITY_ID, MATCH_ALL)
|
||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.exceptions import TemplateError
|
||||||
from homeassistant.helpers.entity import Entity, generate_entity_id
|
from homeassistant.helpers.entity import Entity, generate_entity_id
|
||||||
from homeassistant.helpers import template
|
from homeassistant.helpers import template
|
||||||
|
from homeassistant.helpers.event import track_state_change
|
||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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)
|
"Missing %s for sensor %s", CONF_VALUE_TEMPLATE, device)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
entity_ids = device_config.get(ATTR_ENTITY_ID, MATCH_ALL)
|
||||||
|
|
||||||
sensors.append(
|
sensors.append(
|
||||||
SensorTemplate(
|
SensorTemplate(
|
||||||
hass,
|
hass,
|
||||||
device,
|
device,
|
||||||
friendly_name,
|
friendly_name,
|
||||||
unit_of_measurement,
|
unit_of_measurement,
|
||||||
state_template)
|
state_template,
|
||||||
|
entity_ids)
|
||||||
)
|
)
|
||||||
if not sensors:
|
if not sensors:
|
||||||
_LOGGER.error("No sensors added")
|
_LOGGER.error("No sensors added")
|
||||||
@ -65,7 +69,7 @@ class SensorTemplate(Entity):
|
|||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
# pylint: disable=too-many-arguments
|
||||||
def __init__(self, hass, device_id, friendly_name, unit_of_measurement,
|
def __init__(self, hass, device_id, friendly_name, unit_of_measurement,
|
||||||
state_template):
|
state_template, entity_ids):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id,
|
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id,
|
||||||
@ -77,11 +81,12 @@ class SensorTemplate(Entity):
|
|||||||
|
|
||||||
self.update()
|
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."""
|
"""Called when the target device changes state."""
|
||||||
self.update_ha_state(True)
|
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
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
@ -6,6 +6,7 @@ https://home-assistant.io/components/sensor.time_date/
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
@ -15,7 +16,7 @@ OPTION_TYPES = {
|
|||||||
'date': 'Date',
|
'date': 'Date',
|
||||||
'date_time': 'Date & Time',
|
'date_time': 'Date & Time',
|
||||||
'time_date': 'Time & Date',
|
'time_date': 'Time & Date',
|
||||||
'beat': 'Time (beat)',
|
'beat': 'Internet Time',
|
||||||
'time_utc': 'Time (UTC)',
|
'time_utc': 'Time (UTC)',
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,10 +77,13 @@ class TimeDateSensor(Entity):
|
|||||||
time_utc = time_date.strftime(TIME_STR_FORMAT)
|
time_utc = time_date.strftime(TIME_STR_FORMAT)
|
||||||
date = dt_util.as_local(time_date).date().isoformat()
|
date = dt_util.as_local(time_date).date().isoformat()
|
||||||
|
|
||||||
# Calculate the beat (Swatch Internet Time) time without date.
|
# Calculate Swatch Internet Time.
|
||||||
hours, minutes, seconds = time_date.strftime('%H:%M:%S').split(':')
|
time_bmt = time_date + timedelta(hours=1)
|
||||||
beat = ((int(seconds) + (int(minutes) * 60) + ((int(hours) + 1) *
|
delta = timedelta(hours=time_bmt.hour,
|
||||||
3600)) / 86.4)
|
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':
|
if self.type == 'time':
|
||||||
self._state = time
|
self._state = time
|
||||||
@ -92,4 +96,4 @@ class TimeDateSensor(Entity):
|
|||||||
elif self.type == 'time_utc':
|
elif self.type == 'time_utc':
|
||||||
self._state = time_utc
|
self._state = time_utc
|
||||||
elif self.type == 'beat':
|
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
|
import re
|
||||||
|
|
||||||
from homeassistant.const import HTTP_OK
|
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.components.http import HomeAssistantView
|
||||||
|
|
||||||
DOMAIN = 'torque'
|
DOMAIN = 'torque'
|
||||||
DEPENDENCIES = ['http']
|
DEPENDENCIES = ['http']
|
||||||
@ -43,12 +43,31 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
email = config.get('email', None)
|
email = config.get('email', None)
|
||||||
sensors = {}
|
sensors = {}
|
||||||
|
|
||||||
def _receive_data(handler, path_match, data):
|
hass.wsgi.register_view(TorqueReceiveDataView(hass, email, vehicle,
|
||||||
"""Received data from Torque."""
|
sensors, add_devices))
|
||||||
handler.send_response(HTTP_OK)
|
return True
|
||||||
handler.end_headers()
|
|
||||||
|
|
||||||
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
|
return
|
||||||
|
|
||||||
names = {}
|
names = {}
|
||||||
@ -66,18 +85,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
units[pid] = decode(data[key])
|
units[pid] = decode(data[key])
|
||||||
elif is_value:
|
elif is_value:
|
||||||
pid = convert_pid(is_value.group(1))
|
pid = convert_pid(is_value.group(1))
|
||||||
if pid in sensors:
|
if pid in self.sensors:
|
||||||
sensors[pid].on_update(data[key])
|
self.sensors[pid].on_update(data[key])
|
||||||
|
|
||||||
for pid in names:
|
for pid in names:
|
||||||
if pid not in sensors:
|
if pid not in self.sensors:
|
||||||
sensors[pid] = TorqueSensor(
|
self.sensors[pid] = TorqueSensor(
|
||||||
ENTITY_NAME_FORMAT.format(vehicle, names[pid]),
|
ENTITY_NAME_FORMAT.format(self.vehicle, names[pid]),
|
||||||
units.get(pid, None))
|
units.get(pid, None))
|
||||||
add_devices([sensors[pid]])
|
self.add_devices([self.sensors[pid]])
|
||||||
|
|
||||||
hass.http.register_path('GET', API_PATH, _receive_data)
|
return None
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class TorqueSensor(Entity):
|
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.util import location as location_util
|
||||||
from homeassistant.const import CONF_ELEVATION
|
from homeassistant.const import CONF_ELEVATION
|
||||||
|
|
||||||
REQUIREMENTS = ['astral==1.0']
|
REQUIREMENTS = ['astral==1.1']
|
||||||
DOMAIN = "sun"
|
DOMAIN = "sun"
|
||||||
ENTITY_ID = "sun.sun"
|
ENTITY_ID = "sun.sun"
|
||||||
|
|
||||||
@ -25,6 +25,7 @@ STATE_BELOW_HORIZON = "below_horizon"
|
|||||||
STATE_ATTR_NEXT_RISING = "next_rising"
|
STATE_ATTR_NEXT_RISING = "next_rising"
|
||||||
STATE_ATTR_NEXT_SETTING = "next_setting"
|
STATE_ATTR_NEXT_SETTING = "next_setting"
|
||||||
STATE_ATTR_ELEVATION = "elevation"
|
STATE_ATTR_ELEVATION = "elevation"
|
||||||
|
STATE_ATTR_AZIMUTH = "azimuth"
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -80,7 +81,7 @@ def next_rising_utc(hass, entity_id=None):
|
|||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
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):
|
if None in (hass.config.latitude, hass.config.longitude):
|
||||||
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
|
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
|
||||||
return False
|
return False
|
||||||
@ -126,10 +127,12 @@ class Sun(Entity):
|
|||||||
entity_id = ENTITY_ID
|
entity_id = ENTITY_ID
|
||||||
|
|
||||||
def __init__(self, hass, location):
|
def __init__(self, hass, location):
|
||||||
"""Initialize the Sun."""
|
"""Initialize the sun."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.location = location
|
self.location = location
|
||||||
self._state = self.next_rising = self.next_setting = None
|
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)
|
track_utc_time_change(hass, self.timer_update, second=30)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -151,7 +154,8 @@ class Sun(Entity):
|
|||||||
return {
|
return {
|
||||||
STATE_ATTR_NEXT_RISING: self.next_rising.isoformat(),
|
STATE_ATTR_NEXT_RISING: self.next_rising.isoformat(),
|
||||||
STATE_ATTR_NEXT_SETTING: self.next_setting.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
|
@property
|
||||||
@ -159,36 +163,49 @@ class Sun(Entity):
|
|||||||
"""Datetime when the next change to the state is."""
|
"""Datetime when the next change to the state is."""
|
||||||
return min(self.next_rising, self.next_setting)
|
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):
|
def update_as_of(self, utc_point_in_time):
|
||||||
"""Calculate sun state at a point in UTC time."""
|
"""Calculate sun state at a point in UTC time."""
|
||||||
|
import astral
|
||||||
|
|
||||||
mod = -1
|
mod = -1
|
||||||
while True:
|
while True:
|
||||||
next_rising_dt = self.location.sunrise(
|
try:
|
||||||
utc_point_in_time + timedelta(days=mod), local=False)
|
next_rising_dt = self.location.sunrise(
|
||||||
if next_rising_dt > utc_point_in_time:
|
utc_point_in_time + timedelta(days=mod), local=False)
|
||||||
break
|
if next_rising_dt > utc_point_in_time:
|
||||||
|
break
|
||||||
|
except astral.AstralError:
|
||||||
|
pass
|
||||||
mod += 1
|
mod += 1
|
||||||
|
|
||||||
mod = -1
|
mod = -1
|
||||||
while True:
|
while True:
|
||||||
next_setting_dt = (self.location.sunset(
|
try:
|
||||||
utc_point_in_time + timedelta(days=mod), local=False))
|
next_setting_dt = (self.location.sunset(
|
||||||
if next_setting_dt > utc_point_in_time:
|
utc_point_in_time + timedelta(days=mod), local=False))
|
||||||
break
|
if next_setting_dt > utc_point_in_time:
|
||||||
|
break
|
||||||
|
except astral.AstralError:
|
||||||
|
pass
|
||||||
mod += 1
|
mod += 1
|
||||||
|
|
||||||
self.next_rising = next_rising_dt
|
self.next_rising = next_rising_dt
|
||||||
self.next_setting = next_setting_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):
|
def point_in_time_listener(self, now):
|
||||||
"""Called when the state of the sun has changed."""
|
"""Called when the state of the sun has changed."""
|
||||||
self.update_as_of(now)
|
self.update_as_of(now)
|
||||||
@ -200,5 +217,6 @@ class Sun(Entity):
|
|||||||
self.next_change + timedelta(seconds=1))
|
self.next_change + timedelta(seconds=1))
|
||||||
|
|
||||||
def timer_update(self, time):
|
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()
|
self.update_ha_state()
|
||||||
|
@ -61,6 +61,8 @@ class SmartPlugSwitch(SwitchDevice):
|
|||||||
return float(self.smartplug.now_power) / 1000000.0
|
return float(self.smartplug.now_power) / 1000000.0
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
|
except TypeError:
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def today_power_mw(self):
|
def today_power_mw(self):
|
||||||
@ -69,6 +71,8 @@ class SmartPlugSwitch(SwitchDevice):
|
|||||||
return float(self.smartplug.now_energy_day) / 1000.0
|
return float(self.smartplug.now_energy_day) / 1000.0
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
|
except TypeError:
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
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):
|
def switch_update(event):
|
||||||
"""Callback for sensor updates from the RFXtrx gateway."""
|
"""Callback for sensor updates from the RFXtrx gateway."""
|
||||||
if not isinstance(event.device, rfxtrxmod.LightingDevice) or \
|
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
|
return
|
||||||
|
|
||||||
new_device = rfxtrx.get_new_device(event, config, RfxtrxSwitch)
|
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.components.switch import ENTITY_ID_FORMAT, SwitchDevice
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, STATE_OFF, STATE_ON)
|
ATTR_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, STATE_OFF, STATE_ON,
|
||||||
from homeassistant.core import EVENT_STATE_CHANGED
|
ATTR_ENTITY_ID, MATCH_ALL)
|
||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.exceptions import TemplateError
|
||||||
from homeassistant.helpers.entity import generate_entity_id
|
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 import template
|
||||||
|
from homeassistant.helpers.event import track_state_change
|
||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
CONF_SWITCHES = 'switches'
|
CONF_SWITCHES = 'switches'
|
||||||
@ -58,6 +59,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
"Missing action for switch %s", device)
|
"Missing action for switch %s", device)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
entity_ids = device_config.get(ATTR_ENTITY_ID, MATCH_ALL)
|
||||||
|
|
||||||
switches.append(
|
switches.append(
|
||||||
SwitchTemplate(
|
SwitchTemplate(
|
||||||
hass,
|
hass,
|
||||||
@ -65,7 +68,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
friendly_name,
|
friendly_name,
|
||||||
state_template,
|
state_template,
|
||||||
on_action,
|
on_action,
|
||||||
off_action)
|
off_action,
|
||||||
|
entity_ids)
|
||||||
)
|
)
|
||||||
if not switches:
|
if not switches:
|
||||||
_LOGGER.error("No switches added")
|
_LOGGER.error("No switches added")
|
||||||
@ -79,25 +83,25 @@ class SwitchTemplate(SwitchDevice):
|
|||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
# pylint: disable=too-many-arguments
|
||||||
def __init__(self, hass, device_id, friendly_name, state_template,
|
def __init__(self, hass, device_id, friendly_name, state_template,
|
||||||
on_action, off_action):
|
on_action, off_action, entity_ids):
|
||||||
"""Initialize the Template switch."""
|
"""Initialize the Template switch."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id,
|
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id,
|
||||||
hass=hass)
|
hass=hass)
|
||||||
self._name = friendly_name
|
self._name = friendly_name
|
||||||
self._template = state_template
|
self._template = state_template
|
||||||
self._on_action = on_action
|
self._on_script = Script(hass, on_action)
|
||||||
self._off_action = off_action
|
self._off_script = Script(hass, off_action)
|
||||||
self._state = False
|
self._state = False
|
||||||
|
|
||||||
self.update()
|
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."""
|
"""Called when the target device changes state."""
|
||||||
self.update_ha_state(True)
|
self.update_ha_state(True)
|
||||||
|
|
||||||
hass.bus.listen(EVENT_STATE_CHANGED,
|
track_state_change(hass, entity_ids,
|
||||||
template_switch_event_listener)
|
template_switch_state_listener)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -121,11 +125,11 @@ class SwitchTemplate(SwitchDevice):
|
|||||||
|
|
||||||
def turn_on(self, **kwargs):
|
def turn_on(self, **kwargs):
|
||||||
"""Fire the on action."""
|
"""Fire the on action."""
|
||||||
call_from_config(self.hass, self._on_action, True)
|
self._on_script.run()
|
||||||
|
|
||||||
def turn_off(self, **kwargs):
|
def turn_off(self, **kwargs):
|
||||||
"""Fire the off action."""
|
"""Fire the off action."""
|
||||||
call_from_config(self.hass, self._off_action, True)
|
self._off_script.run()
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Update the state from the template."""
|
"""Update the state from the template."""
|
||||||
|
@ -9,7 +9,7 @@ import logging
|
|||||||
from homeassistant.components import discovery
|
from homeassistant.components import discovery
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||||
|
|
||||||
REQUIREMENTS = ['pywemo==0.4.2']
|
REQUIREMENTS = ['pywemo==0.4.3']
|
||||||
|
|
||||||
DOMAIN = 'wemo'
|
DOMAIN = 'wemo'
|
||||||
DISCOVER_LIGHTS = 'wemo.light'
|
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