Merge pull request #2183 from home-assistant/dev

0.21
This commit is contained in:
Paulus Schoutsen 2016-06-07 19:27:55 -07:00
commit d7b0929a32
135 changed files with 4391 additions and 1553 deletions

View File

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

View File

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

@ -85,3 +85,8 @@ venv
*.swo *.swo
ctags.tmp ctags.tmp
# vagrant stuff
virtualization/vagrant/setup_done
virtualization/vagrant/.vagrant
virtualization/vagrant/config

View File

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

View File

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

View File

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

View File

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

View 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})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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)

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit 4a667eb77e28a27dc766ca6f7bbd04e3866124d9 Subproject commit 612a876199d8ecdc778182ea93fff034a4d15ef4

View File

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

View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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()

View File

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

View 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)

View 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')

View File

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

View File

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

View File

@ -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)),
}) })

View File

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

View File

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

View File

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

View File

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

View File

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

View 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")

View File

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

View File

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

View 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])

View 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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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()

View 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

View File

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

View File

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

View File

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