Merge pull request #4626 from home-assistant/dev

0.34
This commit is contained in:
Paulus Schoutsen 2016-12-03 12:17:02 -08:00 committed by GitHub
commit 94b719e150
239 changed files with 10393 additions and 2204 deletions

View file

@ -40,9 +40,6 @@ omit =
homeassistant/components/isy994.py homeassistant/components/isy994.py
homeassistant/components/*/isy994.py homeassistant/components/*/isy994.py
homeassistant/components/litejet.py
homeassistant/components/*/litejet.py
homeassistant/components/modbus.py homeassistant/components/modbus.py
homeassistant/components/*/modbus.py homeassistant/components/*/modbus.py
@ -127,6 +124,7 @@ omit =
homeassistant/components/binary_sensor/concord232.py homeassistant/components/binary_sensor/concord232.py
homeassistant/components/binary_sensor/rest.py homeassistant/components/binary_sensor/rest.py
homeassistant/components/browser.py homeassistant/components/browser.py
homeassistant/components/camera/amcrest.py
homeassistant/components/camera/bloomsky.py homeassistant/components/camera/bloomsky.py
homeassistant/components/camera/foscam.py homeassistant/components/camera/foscam.py
homeassistant/components/camera/mjpeg.py homeassistant/components/camera/mjpeg.py
@ -152,6 +150,7 @@ omit =
homeassistant/components/device_tracker/bt_home_hub_5.py homeassistant/components/device_tracker/bt_home_hub_5.py
homeassistant/components/device_tracker/cisco_ios.py homeassistant/components/device_tracker/cisco_ios.py
homeassistant/components/device_tracker/fritz.py homeassistant/components/device_tracker/fritz.py
homeassistant/components/device_tracker/gpslogger.py
homeassistant/components/device_tracker/icloud.py homeassistant/components/device_tracker/icloud.py
homeassistant/components/device_tracker/luci.py homeassistant/components/device_tracker/luci.py
homeassistant/components/device_tracker/netgear.py homeassistant/components/device_tracker/netgear.py
@ -188,7 +187,9 @@ omit =
homeassistant/components/media_player/cast.py homeassistant/components/media_player/cast.py
homeassistant/components/media_player/cmus.py homeassistant/components/media_player/cmus.py
homeassistant/components/media_player/denon.py homeassistant/components/media_player/denon.py
homeassistant/components/media_player/denonavr.py
homeassistant/components/media_player/directv.py homeassistant/components/media_player/directv.py
homeassistant/components/media_player/dunehd.py
homeassistant/components/media_player/emby.py homeassistant/components/media_player/emby.py
homeassistant/components/media_player/firetv.py homeassistant/components/media_player/firetv.py
homeassistant/components/media_player/gpmdp.py homeassistant/components/media_player/gpmdp.py
@ -240,6 +241,7 @@ omit =
homeassistant/components/notify/xmpp.py homeassistant/components/notify/xmpp.py
homeassistant/components/nuimo_controller.py homeassistant/components/nuimo_controller.py
homeassistant/components/openalpr.py homeassistant/components/openalpr.py
homeassistant/components/remote/harmony.py
homeassistant/components/scene/hunterdouglas_powerview.py homeassistant/components/scene/hunterdouglas_powerview.py
homeassistant/components/sensor/arest.py homeassistant/components/sensor/arest.py
homeassistant/components/sensor/arwn.py homeassistant/components/sensor/arwn.py
@ -279,6 +281,7 @@ omit =
homeassistant/components/sensor/miflora.py homeassistant/components/sensor/miflora.py
homeassistant/components/sensor/mqtt_room.py homeassistant/components/sensor/mqtt_room.py
homeassistant/components/sensor/neurio_energy.py homeassistant/components/sensor/neurio_energy.py
homeassistant/components/sensor/nut.py
homeassistant/components/sensor/nzbget.py homeassistant/components/sensor/nzbget.py
homeassistant/components/sensor/ohmconnect.py homeassistant/components/sensor/ohmconnect.py
homeassistant/components/sensor/onewire.py homeassistant/components/sensor/onewire.py
@ -291,6 +294,7 @@ omit =
homeassistant/components/sensor/scrape.py homeassistant/components/sensor/scrape.py
homeassistant/components/sensor/serial_pm.py homeassistant/components/sensor/serial_pm.py
homeassistant/components/sensor/snmp.py homeassistant/components/sensor/snmp.py
homeassistant/components/sensor/sonarr.py
homeassistant/components/sensor/speedtest.py homeassistant/components/sensor/speedtest.py
homeassistant/components/sensor/steam_online.py homeassistant/components/sensor/steam_online.py
homeassistant/components/sensor/supervisord.py homeassistant/components/sensor/supervisord.py
@ -306,14 +310,17 @@ omit =
homeassistant/components/sensor/twitch.py homeassistant/components/sensor/twitch.py
homeassistant/components/sensor/uber.py homeassistant/components/sensor/uber.py
homeassistant/components/sensor/vasttrafik.py homeassistant/components/sensor/vasttrafik.py
homeassistant/components/sensor/waqi.py
homeassistant/components/sensor/xbox_live.py homeassistant/components/sensor/xbox_live.py
homeassistant/components/sensor/yweather.py homeassistant/components/sensor/yweather.py
homeassistant/components/sensor/waqi.py
homeassistant/components/switch/acer_projector.py homeassistant/components/switch/acer_projector.py
homeassistant/components/switch/anel_pwrctrl.py homeassistant/components/switch/anel_pwrctrl.py
homeassistant/components/switch/arest.py homeassistant/components/switch/arest.py
homeassistant/components/switch/dlink.py homeassistant/components/switch/dlink.py
homeassistant/components/switch/edimax.py homeassistant/components/switch/edimax.py
homeassistant/components/switch/hikvisioncam.py homeassistant/components/switch/hikvisioncam.py
homeassistant/components/switch/hook.py
homeassistant/components/switch/mystrom.py homeassistant/components/switch/mystrom.py
homeassistant/components/switch/netio.py homeassistant/components/switch/netio.py
homeassistant/components/switch/orvibo.py homeassistant/components/switch/orvibo.py

1
.gitignore vendored
View file

@ -62,6 +62,7 @@ pip-log.txt
.coverage .coverage
.tox .tox
nosetests.xml nosetests.xml
htmlcov/
# Translations # Translations
*.mo *.mo

View file

@ -365,6 +365,7 @@ def async_from_config_dict(config: Dict[str, Any],
Dynamically loads required components and its dependencies. Dynamically loads required components and its dependencies.
This method is a coroutine. This method is a coroutine.
""" """
hass.async_track_tasks()
setup_lock = hass.data.get('setup_lock') setup_lock = hass.data.get('setup_lock')
if setup_lock is None: if setup_lock is None:
setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop) setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop)
@ -427,6 +428,8 @@ def async_from_config_dict(config: Dict[str, Any],
setup_lock.release() setup_lock.release()
yield from hass.async_stop_track_tasks()
return hass return hass

View file

@ -117,11 +117,11 @@ class NX584Alarm(alarm.AlarmControlPanel):
def alarm_arm_home(self, code=None): def alarm_arm_home(self, code=None):
"""Send arm home command.""" """Send arm home command."""
self._alarm.arm('home') self._alarm.arm('stay')
def alarm_arm_away(self, code=None): def alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command."""
self._alarm.arm('auto') self._alarm.arm('exit')
def alarm_trigger(self, code=None): def alarm_trigger(self, code=None):
"""Alarm trigger command.""" """Alarm trigger command."""

View file

@ -118,7 +118,7 @@ class AlexaIntentsView(HomeAssistantView):
def __init__(self, hass, intents): def __init__(self, hass, intents):
"""Initialize Alexa view.""" """Initialize Alexa view."""
super().__init__(hass) super().__init__()
intents = copy.deepcopy(intents) intents = copy.deepcopy(intents)
template.attach(hass, intents) template.attach(hass, intents)
@ -150,7 +150,7 @@ class AlexaIntentsView(HomeAssistantView):
return None return None
intent = req.get('intent') intent = req.get('intent')
response = AlexaResponse(self.hass, intent) response = AlexaResponse(request.app['hass'], intent)
if req_type == 'LaunchRequest': if req_type == 'LaunchRequest':
response.add_speech( response.add_speech(
@ -282,7 +282,7 @@ class AlexaFlashBriefingView(HomeAssistantView):
def __init__(self, hass, flash_briefings): def __init__(self, hass, flash_briefings):
"""Initialize Alexa view.""" """Initialize Alexa view."""
super().__init__(hass) super().__init__()
self.flash_briefings = copy.deepcopy(flash_briefings) self.flash_briefings = copy.deepcopy(flash_briefings)
template.attach(hass, self.flash_briefings) template.attach(hass, self.flash_briefings)

View file

@ -77,8 +77,10 @@ class APIEventStream(HomeAssistantView):
@asyncio.coroutine @asyncio.coroutine
def get(self, request): def get(self, request):
"""Provide a streaming interface for the event bus.""" """Provide a streaming interface for the event bus."""
# pylint: disable=no-self-use
hass = request.app['hass']
stop_obj = object() stop_obj = object()
to_write = asyncio.Queue(loop=self.hass.loop) to_write = asyncio.Queue(loop=hass.loop)
restrict = request.GET.get('restrict') restrict = request.GET.get('restrict')
if restrict: if restrict:
@ -106,7 +108,7 @@ class APIEventStream(HomeAssistantView):
response.content_type = 'text/event-stream' response.content_type = 'text/event-stream'
yield from response.prepare(request) yield from response.prepare(request)
unsub_stream = self.hass.bus.async_listen(MATCH_ALL, forward_events) unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events)
try: try:
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj)) _LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
@ -117,7 +119,7 @@ class APIEventStream(HomeAssistantView):
while True: while True:
try: try:
with async_timeout.timeout(STREAM_PING_INTERVAL, with async_timeout.timeout(STREAM_PING_INTERVAL,
loop=self.hass.loop): loop=hass.loop):
payload = yield from to_write.get() payload = yield from to_write.get()
if payload is stop_obj: if payload is stop_obj:
@ -145,7 +147,7 @@ class APIConfigView(HomeAssistantView):
@ha.callback @ha.callback
def get(self, request): def get(self, request):
"""Get current configuration.""" """Get current configuration."""
return self.json(self.hass.config.as_dict()) return self.json(request.app['hass'].config.as_dict())
class APIDiscoveryView(HomeAssistantView): class APIDiscoveryView(HomeAssistantView):
@ -158,10 +160,11 @@ class APIDiscoveryView(HomeAssistantView):
@ha.callback @ha.callback
def get(self, request): def get(self, request):
"""Get discovery info.""" """Get discovery info."""
needs_auth = self.hass.config.api.api_password is not None hass = request.app['hass']
needs_auth = hass.config.api.api_password is not None
return self.json({ return self.json({
'base_url': self.hass.config.api.base_url, 'base_url': hass.config.api.base_url,
'location_name': self.hass.config.location_name, 'location_name': hass.config.location_name,
'requires_api_password': needs_auth, 'requires_api_password': needs_auth,
'version': __version__ 'version': __version__
}) })
@ -176,7 +179,7 @@ class APIStatesView(HomeAssistantView):
@ha.callback @ha.callback
def get(self, request): def get(self, request):
"""Get current states.""" """Get current states."""
return self.json(self.hass.states.async_all()) return self.json(request.app['hass'].states.async_all())
class APIEntityStateView(HomeAssistantView): class APIEntityStateView(HomeAssistantView):
@ -188,7 +191,7 @@ class APIEntityStateView(HomeAssistantView):
@ha.callback @ha.callback
def get(self, request, entity_id): def get(self, request, entity_id):
"""Retrieve state of entity.""" """Retrieve state of entity."""
state = self.hass.states.get(entity_id) state = request.app['hass'].states.get(entity_id)
if state: if state:
return self.json(state) return self.json(state)
else: else:
@ -197,6 +200,7 @@ class APIEntityStateView(HomeAssistantView):
@asyncio.coroutine @asyncio.coroutine
def post(self, request, entity_id): def post(self, request, entity_id):
"""Update state of entity.""" """Update state of entity."""
hass = request.app['hass']
try: try:
data = yield from request.json() data = yield from request.json()
except ValueError: except ValueError:
@ -211,15 +215,14 @@ class APIEntityStateView(HomeAssistantView):
attributes = data.get('attributes') attributes = data.get('attributes')
force_update = data.get('force_update', False) force_update = data.get('force_update', False)
is_new_state = self.hass.states.get(entity_id) is None is_new_state = hass.states.get(entity_id) is None
# Write state # Write state
self.hass.states.async_set(entity_id, new_state, attributes, hass.states.async_set(entity_id, new_state, attributes, force_update)
force_update)
# Read the state back for our response # Read the state back for our response
status_code = HTTP_CREATED if is_new_state else 200 status_code = HTTP_CREATED if is_new_state else 200
resp = self.json(self.hass.states.get(entity_id), status_code) resp = self.json(hass.states.get(entity_id), status_code)
resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id)) resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id))
@ -228,7 +231,7 @@ class APIEntityStateView(HomeAssistantView):
@ha.callback @ha.callback
def delete(self, request, entity_id): def delete(self, request, entity_id):
"""Remove entity.""" """Remove entity."""
if self.hass.states.async_remove(entity_id): if request.app['hass'].states.async_remove(entity_id):
return self.json_message('Entity removed') return self.json_message('Entity removed')
else: else:
return self.json_message('Entity not found', HTTP_NOT_FOUND) return self.json_message('Entity not found', HTTP_NOT_FOUND)
@ -243,7 +246,7 @@ class APIEventListenersView(HomeAssistantView):
@ha.callback @ha.callback
def get(self, request): def get(self, request):
"""Get event listeners.""" """Get event listeners."""
return self.json(async_events_json(self.hass)) return self.json(async_events_json(request.app['hass']))
class APIEventView(HomeAssistantView): class APIEventView(HomeAssistantView):
@ -271,7 +274,8 @@ class APIEventView(HomeAssistantView):
if state: if state:
event_data[key] = state event_data[key] = state
self.hass.bus.async_fire(event_type, event_data, ha.EventOrigin.remote) request.app['hass'].bus.async_fire(event_type, event_data,
ha.EventOrigin.remote)
return self.json_message("Event {} fired.".format(event_type)) return self.json_message("Event {} fired.".format(event_type))
@ -285,7 +289,7 @@ class APIServicesView(HomeAssistantView):
@ha.callback @ha.callback
def get(self, request): def get(self, request):
"""Get registered services.""" """Get registered services."""
return self.json(async_services_json(self.hass)) return self.json(async_services_json(request.app['hass']))
class APIDomainServicesView(HomeAssistantView): class APIDomainServicesView(HomeAssistantView):
@ -300,12 +304,12 @@ class APIDomainServicesView(HomeAssistantView):
Returns a list of changed states. Returns a list of changed states.
""" """
hass = request.app['hass']
body = yield from request.text() body = yield from request.text()
data = json.loads(body) if body else None data = json.loads(body) if body else None
with AsyncTrackStates(self.hass) as changed_states: with AsyncTrackStates(hass) as changed_states:
yield from self.hass.services.async_call(domain, service, data, yield from hass.services.async_call(domain, service, data, True)
True)
return self.json(changed_states) return self.json(changed_states)
@ -320,6 +324,7 @@ class APIEventForwardingView(HomeAssistantView):
@asyncio.coroutine @asyncio.coroutine
def post(self, request): def post(self, request):
"""Setup an event forwarder.""" """Setup an event forwarder."""
hass = request.app['hass']
try: try:
data = yield from request.json() data = yield from request.json()
except ValueError: except ValueError:
@ -340,14 +345,14 @@ class APIEventForwardingView(HomeAssistantView):
api = rem.API(host, api_password, port) api = rem.API(host, api_password, port)
valid = yield from self.hass.loop.run_in_executor( valid = yield from hass.loop.run_in_executor(
None, api.validate_api) None, api.validate_api)
if not valid: if not valid:
return self.json_message("Unable to validate API.", return self.json_message("Unable to validate API.",
HTTP_UNPROCESSABLE_ENTITY) HTTP_UNPROCESSABLE_ENTITY)
if self.event_forwarder is None: if self.event_forwarder is None:
self.event_forwarder = rem.EventForwarder(self.hass) self.event_forwarder = rem.EventForwarder(hass)
self.event_forwarder.async_connect(api) self.event_forwarder.async_connect(api)
@ -389,7 +394,7 @@ class APIComponentsView(HomeAssistantView):
@ha.callback @ha.callback
def get(self, request): def get(self, request):
"""Get current loaded components.""" """Get current loaded components."""
return self.json(self.hass.config.components) return self.json(request.app['hass'].config.components)
class APIErrorLogView(HomeAssistantView): class APIErrorLogView(HomeAssistantView):
@ -402,7 +407,7 @@ class APIErrorLogView(HomeAssistantView):
def get(self, request): def get(self, request):
"""Serve error log.""" """Serve error log."""
resp = yield from self.file( resp = yield from self.file(
request, self.hass.config.path(ERROR_LOG_FILENAME)) request, request.app['hass'].config.path(ERROR_LOG_FILENAME))
return resp return resp
@ -417,7 +422,7 @@ class APITemplateView(HomeAssistantView):
"""Render a template.""" """Render a template."""
try: try:
data = yield from request.json() data = yield from request.json()
tpl = template.Template(data['template'], self.hass) tpl = template.Template(data['template'], request.app['hass'])
return tpl.async_render(data.get('variables')) return tpl.async_render(data.get('variables'))
except (ValueError, TemplateError) as ex: except (ValueError, TemplateError) as ex:
return self.json_message('Error rendering template: {}'.format(ex), return self.json_message('Error rendering template: {}'.format(ex),

View file

@ -11,22 +11,34 @@ import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.const import CONF_PLATFORM from homeassistant.const import CONF_PLATFORM
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
from homeassistant.helpers.event import track_point_in_utc_time
DEPENDENCIES = ['litejet'] DEPENDENCIES = ['litejet']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_NUMBER = 'number' CONF_NUMBER = 'number'
CONF_HELD_MORE_THAN = 'held_more_than'
CONF_HELD_LESS_THAN = 'held_less_than'
TRIGGER_SCHEMA = vol.Schema({ TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'litejet', vol.Required(CONF_PLATFORM): 'litejet',
vol.Required(CONF_NUMBER): cv.positive_int vol.Required(CONF_NUMBER): cv.positive_int,
vol.Optional(CONF_HELD_MORE_THAN):
vol.All(cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_HELD_LESS_THAN):
vol.All(cv.time_period, cv.positive_timedelta)
}) })
def async_trigger(hass, config, action): def async_trigger(hass, config, action):
"""Listen for events based on configuration.""" """Listen for events based on configuration."""
number = config.get(CONF_NUMBER) number = config.get(CONF_NUMBER)
held_more_than = config.get(CONF_HELD_MORE_THAN)
held_less_than = config.get(CONF_HELD_LESS_THAN)
pressed_time = None
cancel_pressed_more_than = None
@callback @callback
def call_action(): def call_action():
@ -34,8 +46,53 @@ def async_trigger(hass, config, action):
hass.async_run_job(action, { hass.async_run_job(action, {
'trigger': { 'trigger': {
CONF_PLATFORM: 'litejet', CONF_PLATFORM: 'litejet',
CONF_NUMBER: number CONF_NUMBER: number,
CONF_HELD_MORE_THAN: held_more_than,
CONF_HELD_LESS_THAN: held_less_than
}, },
}) })
hass.data['litejet_system'].on_switch_released(number, call_action) # held_more_than and held_less_than: trigger on released (if in time range)
# held_more_than: trigger after pressed with calculation
# held_less_than: trigger on released with calculation
# neither: trigger on pressed
@callback
def pressed_more_than_satisfied(now):
"""Handle the LiteJet's switch's button pressed >= held_more_than."""
call_action()
def pressed():
"""Handle the press of the LiteJet switch's button."""
nonlocal cancel_pressed_more_than, pressed_time
nonlocal held_less_than, held_more_than
pressed_time = dt_util.utcnow()
if held_more_than is None and held_less_than is None:
call_action()
if held_more_than is not None and held_less_than is None:
cancel_pressed_more_than = track_point_in_utc_time(
hass,
pressed_more_than_satisfied,
dt_util.utcnow() + held_more_than)
def released():
"""Handle the release of the LiteJet switch's button."""
nonlocal cancel_pressed_more_than, pressed_time
nonlocal held_less_than, held_more_than
# pylint: disable=not-callable
if cancel_pressed_more_than is not None:
cancel_pressed_more_than()
cancel_pressed_more_than = None
held_time = dt_util.utcnow() - pressed_time
if held_less_than is not None and held_time < held_less_than:
if held_more_than is None or held_time > held_more_than:
call_action()
hass.data['litejet_system'].on_switch_pressed(number, pressed)
hass.data['litejet_system'].on_switch_released(number, released)
def async_remove():
"""Remove all subscriptions used for this trigger."""
return
return async_remove

View file

@ -4,6 +4,7 @@ Component to interface with binary sensors.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/binary_sensor/ https://home-assistant.io/components/binary_sensor/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
@ -39,13 +40,13 @@ SENSOR_CLASSES = [
SENSOR_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(SENSOR_CLASSES)) SENSOR_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(SENSOR_CLASSES))
def setup(hass, config): @asyncio.coroutine
def async_setup(hass, config):
"""Track states and offer events for binary sensors.""" """Track states and offer events for binary sensors."""
component = EntityComponent( component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
component.setup(config) yield from component.async_setup(config)
return True return True

View file

@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.command_line/ https://home-assistant.io/components/binary_sensor.command_line/
""" """
import logging import logging
from datetime import timedelta
import voluptuous as vol import voluptuous as vol
@ -23,7 +22,7 @@ DEFAULT_NAME = 'Binary Command Sensor'
DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_PAYLOAD_ON = 'ON'
DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_PAYLOAD_OFF = 'OFF'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) SCAN_INTERVAL = 60
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COMMAND): cv.string, vol.Required(CONF_COMMAND): cv.string,

View file

@ -7,7 +7,8 @@ https://home-assistant.io/components/binary_sensor.homematic/
import logging import logging
from homeassistant.const import STATE_UNKNOWN from homeassistant.const import STATE_UNKNOWN
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
import homeassistant.components.homematic as homematic from homeassistant.components.homematic import HMDevice
from homeassistant.loader import get_component
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -32,14 +33,16 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None):
if discovery_info is None: if discovery_info is None:
return return
homematic = get_component("homematic")
return homematic.setup_hmdevice_discovery_helper( return homematic.setup_hmdevice_discovery_helper(
hass,
HMBinarySensor, HMBinarySensor,
discovery_info, discovery_info,
add_callback_devices add_callback_devices
) )
class HMBinarySensor(homematic.HMDevice, BinarySensorDevice): class HMBinarySensor(HMDevice, BinarySensorDevice):
"""Representation of a binary Homematic device.""" """Representation of a binary Homematic device."""
@property @property

View file

@ -4,17 +4,34 @@ Support for Nest Thermostat Binary Sensors.
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/binary_sensor.nest/ https://home-assistant.io/components/binary_sensor.nest/
""" """
from itertools import chain
import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA) BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.sensor.nest import NestSensor from homeassistant.components.sensor.nest import NestSensor
from homeassistant.const import (CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS) from homeassistant.const import (CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS)
from homeassistant.components.nest import DATA_NEST from homeassistant.components.nest import (
DATA_NEST, is_thermostat, is_camera)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['nest'] DEPENDENCIES = ['nest']
BINARY_TYPES = ['fan',
BINARY_TYPES = ['online']
CLIMATE_BINARY_TYPES = ['fan',
'is_using_emergency_heat',
'is_locked',
'has_leaf']
CAMERA_BINARY_TYPES = [
'motion_detected',
'sound_detected',
'person_detected']
_BINARY_TYPES_DEPRECATED = [
'hvac_ac_state', 'hvac_ac_state',
'hvac_aux_heater_state', 'hvac_aux_heater_state',
'hvac_heater_state', 'hvac_heater_state',
@ -22,28 +39,62 @@ BINARY_TYPES = ['fan',
'hvac_heat_x3_state', 'hvac_heat_x3_state',
'hvac_alt_heat_state', 'hvac_alt_heat_state',
'hvac_alt_heat_x2_state', 'hvac_alt_heat_x2_state',
'hvac_emer_heat_state', 'hvac_emer_heat_state']
'online']
_VALID_BINARY_SENSOR_TYPES = BINARY_TYPES + CLIMATE_BINARY_TYPES \
+ CAMERA_BINARY_TYPES
_VALID_BINARY_SENSOR_TYPES_WITH_DEPRECATED = _VALID_BINARY_SENSOR_TYPES \
+ _BINARY_TYPES_DEPRECATED
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SCAN_INTERVAL): vol.Optional(CONF_SCAN_INTERVAL):
vol.All(vol.Coerce(int), vol.Range(min=1)), vol.All(vol.Coerce(int), vol.Range(min=1)),
vol.Required(CONF_MONITORED_CONDITIONS): vol.Required(CONF_MONITORED_CONDITIONS):
vol.All(cv.ensure_list, [vol.In(BINARY_TYPES)]), vol.All(cv.ensure_list,
[vol.In(_VALID_BINARY_SENSOR_TYPES_WITH_DEPRECATED)])
}) })
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Nest binary sensors.""" """Setup Nest binary sensors."""
nest = hass.data[DATA_NEST] nest = hass.data[DATA_NEST]
conf = config.get(CONF_MONITORED_CONDITIONS, _VALID_BINARY_SENSOR_TYPES)
all_sensors = [] for variable in conf:
for structure, device in nest.devices(): if variable in _BINARY_TYPES_DEPRECATED:
all_sensors.extend( wstr = (variable + " is no a longer supported "
[NestBinarySensor(structure, device, variable) "monitored_conditions. See "
for variable in config[CONF_MONITORED_CONDITIONS]]) "https://home-assistant.io/components/binary_sensor.nest/ "
"for valid options, or remove monitored_conditions "
"entirely to get a reasonable default")
_LOGGER.error(wstr)
add_devices(all_sensors, True) sensors = []
device_chain = chain(nest.devices(),
nest.protect_devices(),
nest.camera_devices())
for structure, device in device_chain:
sensors += [NestBinarySensor(structure, device, variable)
for variable in conf
if variable in BINARY_TYPES]
sensors += [NestBinarySensor(structure, device, variable)
for variable in conf
if variable in CLIMATE_BINARY_TYPES
and is_thermostat(device)]
if is_camera(device):
sensors += [NestBinarySensor(structure, device, variable)
for variable in conf
if variable in CAMERA_BINARY_TYPES]
for activity_zone in device.activity_zones:
sensors += [NestActivityZoneSensor(structure,
device,
activity_zone)]
add_devices(sensors, True)
class NestBinarySensor(NestSensor, BinarySensorDevice): class NestBinarySensor(NestSensor, BinarySensorDevice):
@ -57,3 +108,21 @@ class NestBinarySensor(NestSensor, BinarySensorDevice):
def update(self): def update(self):
"""Retrieve latest state.""" """Retrieve latest state."""
self._state = bool(getattr(self.device, self.variable)) self._state = bool(getattr(self.device, self.variable))
class NestActivityZoneSensor(NestBinarySensor):
"""Represents a Nest binary sensor for activity in a zone."""
def __init__(self, structure, device, zone):
"""Initialize the sensor."""
super(NestActivityZoneSensor, self).__init__(structure, device, None)
self.zone = zone
@property
def name(self):
"""Return the name of the nest, if any."""
return "{} {} activity".format(self._name, self.zone.name)
def update(self):
"""Retrieve latest state."""
self._state = self.device.has_ongoing_motion_in_zone(self.zone.zone_id)

View file

@ -0,0 +1,128 @@
"""
Support for monitoring if a sensor value is below/above a threshold.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.threshold/
"""
import asyncio
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES_SCHEMA)
from homeassistant.const import (
CONF_NAME, CONF_ENTITY_ID, CONF_TYPE, STATE_UNKNOWN, CONF_SENSOR_CLASS,
ATTR_ENTITY_ID)
from homeassistant.core import callback
from homeassistant.helpers.event import async_track_state_change
_LOGGER = logging.getLogger(__name__)
ATTR_SENSOR_VALUE = 'sensor_value'
ATTR_THRESHOLD = 'threshold'
ATTR_TYPE = 'type'
CONF_LOWER = 'lower'
CONF_THRESHOLD = 'threshold'
CONF_UPPER = 'upper'
DEFAULT_NAME = 'Threshold'
SENSOR_TYPES = [CONF_LOWER, CONF_UPPER]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_THRESHOLD): vol.Coerce(float),
vol.Required(CONF_TYPE): vol.In(SENSOR_TYPES),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SENSOR_CLASS, default=None): SENSOR_CLASSES_SCHEMA,
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Threshold sensor."""
entity_id = config.get(CONF_ENTITY_ID)
name = config.get(CONF_NAME)
threshold = config.get(CONF_THRESHOLD)
limit_type = config.get(CONF_TYPE)
sensor_class = config.get(CONF_SENSOR_CLASS)
yield from async_add_devices(
[ThresholdSensor(hass, entity_id, name, threshold, limit_type,
sensor_class)], True)
return True
class ThresholdSensor(BinarySensorDevice):
"""Representation of a Threshold sensor."""
def __init__(self, hass, entity_id, name, threshold, limit_type,
sensor_class):
"""Initialize the Threshold sensor."""
self._hass = hass
self._entity_id = entity_id
self.is_upper = limit_type == 'upper'
self._name = name
self._threshold = threshold
self._sensor_class = sensor_class
self._deviation = False
self.sensor_value = 0
@callback
# pylint: disable=invalid-name
def async_threshold_sensor_state_listener(
entity, old_state, new_state):
"""Called when the sensor changes state."""
if new_state.state == STATE_UNKNOWN:
return
try:
self.sensor_value = float(new_state.state)
except ValueError:
_LOGGER.error("State is not numerical")
hass.async_add_job(self.async_update_ha_state, True)
async_track_state_change(
hass, entity_id, async_threshold_sensor_state_listener)
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def is_on(self):
"""Return true if sensor is on."""
return self._deviation
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def sensor_class(self):
"""Return the sensor class of the sensor."""
return self._sensor_class
@property
def state_attributes(self):
"""Return the state attributes of the sensor."""
return {
ATTR_ENTITY_ID: self._entity_id,
ATTR_SENSOR_VALUE: self.sensor_value,
ATTR_THRESHOLD: self._threshold,
ATTR_TYPE: CONF_UPPER if self.is_upper else CONF_LOWER,
}
@asyncio.coroutine
def async_update(self):
"""Get the latest data and updates the states."""
if self.is_upper:
self._deviation = bool(self.sensor_value > self._threshold)
else:
self._deviation = bool(self.sensor_value < self._threshold)

View file

@ -4,8 +4,6 @@ Support for Wink binary sensors.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
at https://home-assistant.io/components/binary_sensor.wink/ at https://home-assistant.io/components/binary_sensor.wink/
""" """
import json
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.sensor.wink import WinkDevice from homeassistant.components.sensor.wink import WinkDevice
@ -34,38 +32,25 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for sensor in pywink.get_sensors(): for sensor in pywink.get_sensors():
if sensor.capability() in SENSOR_TYPES: if sensor.capability() in SENSOR_TYPES:
add_devices([WinkBinarySensorDevice(sensor)]) add_devices([WinkBinarySensorDevice(sensor, hass)])
for key in pywink.get_keys(): for key in pywink.get_keys():
add_devices([WinkBinarySensorDevice(key)]) add_devices([WinkBinarySensorDevice(key, hass)])
for sensor in pywink.get_smoke_and_co_detectors(): for sensor in pywink.get_smoke_and_co_detectors():
add_devices([WinkBinarySensorDevice(sensor)]) add_devices([WinkBinarySensorDevice(sensor, hass)])
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity): class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
"""Representation of a Wink binary sensor.""" """Representation of a Wink binary sensor."""
def __init__(self, wink): def __init__(self, wink, hass):
"""Initialize the Wink binary sensor.""" """Initialize the Wink binary sensor."""
super().__init__(wink) super().__init__(wink, hass)
wink = get_component('wink') wink = get_component('wink')
self._unit_of_measurement = self.wink.UNIT self._unit_of_measurement = self.wink.UNIT
self.capability = self.wink.capability() self.capability = self.wink.capability()
def _pubnub_update(self, message, channel):
try:
if 'data' in message:
json_data = json.dumps(message.get('data'))
else:
json_data = message
self.wink.pubnub_update(json.loads(json_data))
self.update_ha_state()
except (AttributeError, KeyError):
error = "Pubnub returned invalid json for " + self.name
logging.getLogger(__name__).error(error)
self.update_ha_state(True)
@property @property
def is_on(self): def is_on(self):
"""Return true if the binary sensor is on.""" """Return true if the binary sensor is on."""

View file

@ -13,7 +13,7 @@ from aiohttp import web
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.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
DOMAIN = 'camera' DOMAIN = 'camera'
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
@ -33,8 +33,8 @@ def async_setup(hass, config):
component = EntityComponent( component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
hass.http.register_view(CameraImageView(hass, component.entities)) hass.http.register_view(CameraImageView(component.entities))
hass.http.register_view(CameraMjpegStream(hass, component.entities)) hass.http.register_view(CameraMjpegStream(component.entities))
yield from component.async_setup(config) yield from component.async_setup(config)
return True return True
@ -165,9 +165,8 @@ class CameraView(HomeAssistantView):
requires_auth = False requires_auth = False
def __init__(self, hass, entities): def __init__(self, entities):
"""Initialize a basic camera view.""" """Initialize a basic camera view."""
super().__init__(hass)
self.entities = entities self.entities = entities
@asyncio.coroutine @asyncio.coroutine
@ -178,7 +177,7 @@ class CameraView(HomeAssistantView):
if camera is None: if camera is None:
return web.Response(status=404) return web.Response(status=404)
authenticated = (request.authenticated or authenticated = (request[KEY_AUTHENTICATED] or
request.GET.get('token') == camera.access_token) request.GET.get('token') == camera.access_token)
if not authenticated: if not authenticated:

View file

@ -0,0 +1,79 @@
"""
This component provides basic support for Amcrest IP cameras.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.amcrest/
"""
import logging
import voluptuous as vol
import homeassistant.loader as loader
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA)
from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
from homeassistant.helpers import config_validation as cv
REQUIREMENTS = ['amcrest==1.0.0']
_LOGGER = logging.getLogger(__name__)
DEFAULT_PORT = 80
DEFAULT_NAME = 'Amcrest Camera'
NOTIFICATION_ID = 'amcrest_notification'
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up an Amcrest IP Camera."""
from amcrest import AmcrestCamera
data = AmcrestCamera(
config.get(CONF_HOST), config.get(CONF_PORT),
config.get(CONF_USERNAME), config.get(CONF_PASSWORD))
persistent_notification = loader.get_component('persistent_notification')
try:
data.camera.current_time
# pylint: disable=broad-except
except Exception as ex:
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
persistent_notification.create(
hass, 'Error: {}<br />'
'You will need to restart hass after fixing.'
''.format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
add_devices([AmcrestCam(config, data)])
return True
class AmcrestCam(Camera):
"""An implementation of an Amcrest IP camera."""
def __init__(self, device_info, data):
"""Initialize an Amcrest camera."""
super(AmcrestCam, self).__init__()
self._name = device_info.get(CONF_NAME)
self._data = data
def camera_image(self):
"""Return a still image reponse from the camera."""
# Send the request to snap a picture and return raw jpg data
response = self._data.camera.snapshot()
return response.data
@property
def name(self):
"""Return the name of this camera."""
return self._name

View file

@ -18,6 +18,7 @@ from homeassistant.const import (
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION) HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera) from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe from homeassistant.util.async import run_coroutine_threadsafe
@ -96,8 +97,7 @@ class GenericCamera(Camera):
def fetch(): def fetch():
"""Read image from a URL.""" """Read image from a URL."""
try: try:
kwargs = {'timeout': 10, 'auth': self._auth} response = requests.get(url, timeout=10, auth=self._auth)
response = requests.get(url, **kwargs)
return response.content return response.content
except requests.exceptions.RequestException as error: except requests.exceptions.RequestException as error:
_LOGGER.error('Error getting camera image: %s', error) _LOGGER.error('Error getting camera image: %s', error)
@ -107,12 +107,13 @@ class GenericCamera(Camera):
None, fetch) None, fetch)
# async # async
else: else:
response = None
try: try:
websession = async_get_clientsession(self.hass)
with async_timeout.timeout(10, loop=self.hass.loop): with async_timeout.timeout(10, loop=self.hass.loop):
response = yield from self.hass.websession.get( response = yield from websession.get(
url, auth=self._auth) url, auth=self._auth)
self._last_image = yield from response.read() self._last_image = yield from response.read()
yield from response.release()
except asyncio.TimeoutError: except asyncio.TimeoutError:
_LOGGER.error('Timeout getting camera image') _LOGGER.error('Timeout getting camera image')
return self._last_image return self._last_image
@ -120,6 +121,9 @@ class GenericCamera(Camera):
aiohttp.errors.ClientDisconnectedError) as err: aiohttp.errors.ClientDisconnectedError) as err:
_LOGGER.error('Error getting new camera image: %s', err) _LOGGER.error('Error getting new camera image: %s', err)
return self._last_image return self._last_image
finally:
if response is not None:
self.hass.async_add_job(response.release())
self._last_url = url self._last_url = url
return self._last_image return self._last_image

View file

@ -20,6 +20,7 @@ from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION,
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION) HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera) from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -101,28 +102,32 @@ class MjpegCamera(Camera):
return return
# connect to stream # connect to stream
websession = async_get_clientsession(self.hass)
stream = None
response = None
try: try:
with async_timeout.timeout(10, loop=self.hass.loop): with async_timeout.timeout(10, loop=self.hass.loop):
stream = yield from self.hass.websession.get( stream = yield from websession.get(self._mjpeg_url,
self._mjpeg_url, auth=self._auth)
auth=self._auth
)
except asyncio.TimeoutError:
raise HTTPGatewayTimeout()
response = web.StreamResponse() response = web.StreamResponse()
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER) response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
yield from response.prepare(request) yield from response.prepare(request)
try:
while True: while True:
data = yield from stream.content.read(102400) data = yield from stream.content.read(102400)
if not data: if not data:
break break
response.write(data) response.write(data)
except asyncio.TimeoutError:
raise HTTPGatewayTimeout()
finally: finally:
if stream is not None:
self.hass.async_add_job(stream.release()) self.hass.async_add_job(stream.release())
if response is not None:
yield from response.write_eof() yield from response.write_eof()
@property @property

View file

@ -0,0 +1,108 @@
"""
Support for Nest Cameras.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.nest/
"""
import logging
from datetime import timedelta
import requests
import homeassistant.components.nest as nest
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
from homeassistant.util.dt import utcnow
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['nest']
NEST_BRAND = 'Nest'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up a Nest Cam."""
if discovery_info is None:
return
camera_devices = hass.data[nest.DATA_NEST].camera_devices()
cameras = [NestCamera(structure, device)
for structure, device in camera_devices]
add_devices(cameras, True)
class NestCamera(Camera):
"""Representation of a Nest Camera."""
def __init__(self, structure, device):
"""Initialize a Nest Camera."""
super(NestCamera, self).__init__()
self.structure = structure
self.device = device
self._location = None
self._name = None
self._is_online = None
self._is_streaming = None
self._is_video_history_enabled = False
# Default to non-NestAware subscribed, but will be fixed during update
self._time_between_snapshots = timedelta(seconds=30)
self._last_image = None
self._next_snapshot_at = None
@property
def name(self):
"""Return the name of the nest, if any."""
return self._name
@property
def should_poll(self):
"""Nest camera should poll periodically."""
return True
@property
def is_recording(self):
"""Return true if the device is recording."""
return self._is_streaming
@property
def brand(self):
"""Return the brand of the camera."""
return NEST_BRAND
# This doesn't seem to be getting called regularly, for some reason
def update(self):
"""Cache value from Python-nest."""
self._location = self.device.where
self._name = self.device.name
self._is_online = self.device.is_online
self._is_streaming = self.device.is_streaming
self._is_video_history_enabled = self.device.is_video_history_enabled
if self._is_video_history_enabled:
# NestAware allowed 10/min
self._time_between_snapshots = timedelta(seconds=6)
else:
# Otherwise, 2/min
self._time_between_snapshots = timedelta(seconds=30)
def _ready_for_snapshot(self, now):
return (self._next_snapshot_at is None or
now > self._next_snapshot_at)
def camera_image(self):
"""Return a still image response from the camera."""
now = utcnow()
if self._ready_for_snapshot(now):
url = self.device.snapshot_url
try:
response = requests.get(url)
except requests.exceptions.RequestException as error:
_LOGGER.error("Error getting camera image: %s", error)
return None
self._next_snapshot_at = now + self._time_between_snapshots
self._last_image = response.content
return self._last_image

View file

@ -14,12 +14,13 @@ from aiohttp import web
from aiohttp.web_exceptions import HTTPGatewayTimeout from aiohttp.web_exceptions import HTTPGatewayTimeout
import async_timeout import async_timeout
from homeassistant.core import callback
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP) CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL)
from homeassistant.components.camera import ( from homeassistant.components.camera import (
Camera, PLATFORM_SCHEMA) Camera, PLATFORM_SCHEMA)
from homeassistant.helpers.aiohttp_client import (
async_get_clientsession, async_create_clientsession)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe from homeassistant.util.async import run_coroutine_threadsafe
@ -59,23 +60,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup a Synology IP Camera.""" """Setup a Synology IP Camera."""
if not config.get(CONF_VERIFY_SSL): verify_ssl = config.get(CONF_VERIFY_SSL)
connector = aiohttp.TCPConnector(verify_ssl=False) websession_init = async_get_clientsession(hass, verify_ssl)
@asyncio.coroutine
def _async_close_connector(event):
"""Close websession on shutdown."""
yield from connector.close()
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, _async_close_connector)
else:
connector = hass.websession.connector
websession_init = aiohttp.ClientSession(
loop=hass.loop,
connector=connector
)
# Determine API to use for authentication # Determine API to use for authentication
syno_api_url = SYNO_API_URL.format( syno_api_url = SYNO_API_URL.format(
@ -87,15 +73,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
'version': '1', 'version': '1',
'query': 'SYNO.' 'query': 'SYNO.'
} }
query_req = None
try: try:
with async_timeout.timeout(TIMEOUT, loop=hass.loop): with async_timeout.timeout(TIMEOUT, loop=hass.loop):
query_req = yield from websession_init.get( query_req = yield from websession_init.get(
syno_api_url, syno_api_url,
params=query_payload params=query_payload
) )
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.exception("Error on %s", syno_api_url)
return False
query_resp = yield from query_req.json() query_resp = yield from query_req.json()
auth_path = query_resp['data'][AUTH_API]['path'] auth_path = query_resp['data'][AUTH_API]['path']
@ -103,7 +87,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
camera_path = query_resp['data'][CAMERA_API]['path'] camera_path = query_resp['data'][CAMERA_API]['path']
streaming_path = query_resp['data'][STREAMING_API]['path'] streaming_path = query_resp['data'][STREAMING_API]['path']
# cleanup except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.exception("Error on %s", syno_api_url)
return False
finally:
if query_req is not None:
yield from query_req.release() yield from query_req.release()
# Authticate to NAS to get a session id # Authticate to NAS to get a session id
@ -118,19 +107,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
syno_auth_url syno_auth_url
) )
websession_init.detach()
# init websession # init websession
websession = aiohttp.ClientSession( websession = async_create_clientsession(
loop=hass.loop, connector=connector, cookies={'id': session_id}) hass, verify_ssl, cookies={'id': session_id})
@callback
def _async_close_websession(event):
"""Close websession on shutdown."""
websession.detach()
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, _async_close_websession)
# Use SessionID to get cameras in system # Use SessionID to get cameras in system
syno_camera_url = SYNO_API_URL.format( syno_camera_url = SYNO_API_URL.format(
@ -190,21 +169,24 @@ def get_session_id(hass, websession, username, password, login_url):
'session': 'SurveillanceStation', 'session': 'SurveillanceStation',
'format': 'sid' 'format': 'sid'
} }
auth_req = None
try: try:
with async_timeout.timeout(TIMEOUT, loop=hass.loop): with async_timeout.timeout(TIMEOUT, loop=hass.loop):
auth_req = yield from websession.get( auth_req = yield from websession.get(
login_url, login_url,
params=auth_payload params=auth_payload
) )
auth_resp = yield from auth_req.json()
return auth_resp['data']['sid']
except (asyncio.TimeoutError, aiohttp.errors.ClientError): except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.exception("Error on %s", login_url) _LOGGER.exception("Error on %s", login_url)
return False return False
auth_resp = yield from auth_req.json() finally:
if auth_req is not None:
yield from auth_req.release() yield from auth_req.release()
return auth_resp['data']['sid']
class SynologyCamera(Camera): class SynologyCamera(Camera):
"""An implementation of a Synology NAS based IP camera.""" """An implementation of a Synology NAS based IP camera."""
@ -271,29 +253,33 @@ class SynologyCamera(Camera):
'cameraId': self._camera_id, 'cameraId': self._camera_id,
'format': 'mjpeg' 'format': 'mjpeg'
} }
stream = None
response = None
try: try:
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop): with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
stream = yield from self._websession.get( stream = yield from self._websession.get(
streaming_url, streaming_url,
params=streaming_payload params=streaming_payload
) )
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.exception("Error on %s", streaming_url)
raise HTTPGatewayTimeout()
response = web.StreamResponse() response = web.StreamResponse()
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER) response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
yield from response.prepare(request) yield from response.prepare(request)
try:
while True: while True:
data = yield from stream.content.read(102400) data = yield from stream.content.read(102400)
if not data: if not data:
break break
response.write(data) response.write(data)
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
_LOGGER.exception("Error on %s", streaming_url)
raise HTTPGatewayTimeout()
finally: finally:
if stream is not None:
self.hass.async_add_job(stream.release()) self.hass.async_add_job(stream.release())
if response is not None:
yield from response.write_eof() yield from response.write_eof()
@property @property

View file

@ -58,6 +58,11 @@ ATTR_OPERATION_LIST = "operation_list"
ATTR_SWING_MODE = "swing_mode" ATTR_SWING_MODE = "swing_mode"
ATTR_SWING_LIST = "swing_list" ATTR_SWING_LIST = "swing_list"
# The degree of precision for each platform
PRECISION_WHOLE = 1
PRECISION_HALVES = 0.5
PRECISION_TENTHS = 0.1
CONVERTIBLE_ATTRIBUTE = [ CONVERTIBLE_ATTRIBUTE = [
ATTR_TEMPERATURE, ATTR_TEMPERATURE,
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_LOW,
@ -371,6 +376,14 @@ class ClimateDevice(Entity):
else: else:
return STATE_UNKNOWN return STATE_UNKNOWN
@property
def precision(self):
"""Return the precision of the system."""
if self.unit_of_measurement == TEMP_CELSIUS:
return PRECISION_TENTHS
else:
return PRECISION_WHOLE
@property @property
def state_attributes(self): def state_attributes(self):
"""Return the optional state attributes.""" """Return the optional state attributes."""
@ -562,16 +575,18 @@ class ClimateDevice(Entity):
def _convert_for_display(self, temp): def _convert_for_display(self, temp):
"""Convert temperature into preferred units for display purposes.""" """Convert temperature into preferred units for display purposes."""
if temp is None or not isinstance(temp, Number): if (temp is None or not isinstance(temp, Number) or
self.temperature_unit == self.unit_of_measurement):
return temp return temp
value = convert_temperature(temp, self.temperature_unit, value = convert_temperature(temp, self.temperature_unit,
self.unit_of_measurement) self.unit_of_measurement)
if self.unit_of_measurement is TEMP_CELSIUS: # Round in the units appropriate
decimal_count = 1 if self.precision == PRECISION_HALVES:
return round(value * 2) / 2.0
elif self.precision == PRECISION_TENTHS:
return round(value, 1)
else: else:
# Users of fahrenheit generally expect integer units. # PRECISION_WHOLE as a fall back
decimal_count = 0 return round(value)
return round(value, decimal_count)

View file

@ -21,10 +21,10 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['switch', 'sensor'] DEPENDENCIES = ['switch', 'sensor']
TOL_TEMP = 0.3 DEFAULT_TOLERANCE = 0.3
DEFAULT_NAME = 'Generic Thermostat'
CONF_NAME = 'name' CONF_NAME = 'name'
DEFAULT_NAME = 'Generic Thermostat'
CONF_HEATER = 'heater' CONF_HEATER = 'heater'
CONF_SENSOR = 'target_sensor' CONF_SENSOR = 'target_sensor'
CONF_MIN_TEMP = 'min_temp' CONF_MIN_TEMP = 'min_temp'
@ -32,6 +32,7 @@ CONF_MAX_TEMP = 'max_temp'
CONF_TARGET_TEMP = 'target_temp' CONF_TARGET_TEMP = 'target_temp'
CONF_AC_MODE = 'ac_mode' CONF_AC_MODE = 'ac_mode'
CONF_MIN_DUR = 'min_cycle_duration' CONF_MIN_DUR = 'min_cycle_duration'
CONF_TOLERANCE = 'tolerance'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -42,6 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_MIN_DUR): vol.All(cv.time_period, cv.positive_timedelta), vol.Optional(CONF_MIN_DUR): vol.All(cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_MIN_TEMP): vol.Coerce(float), vol.Optional(CONF_MIN_TEMP): vol.Coerce(float),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce(float),
vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float), vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float),
}) })
@ -56,23 +58,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
target_temp = config.get(CONF_TARGET_TEMP) target_temp = config.get(CONF_TARGET_TEMP)
ac_mode = config.get(CONF_AC_MODE) ac_mode = config.get(CONF_AC_MODE)
min_cycle_duration = config.get(CONF_MIN_DUR) min_cycle_duration = config.get(CONF_MIN_DUR)
tolerance = config.get(CONF_TOLERANCE)
add_devices([GenericThermostat( add_devices([GenericThermostat(
hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp, hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp,
target_temp, ac_mode, min_cycle_duration)]) target_temp, ac_mode, min_cycle_duration, tolerance)])
class GenericThermostat(ClimateDevice): class GenericThermostat(ClimateDevice):
"""Representation of a GenericThermostat device.""" """Representation of a GenericThermostat device."""
def __init__(self, hass, name, heater_entity_id, sensor_entity_id, def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration): min_temp, max_temp, target_temp, ac_mode, min_cycle_duration,
tolerance):
"""Initialize the thermostat.""" """Initialize the thermostat."""
self.hass = hass self.hass = hass
self._name = name self._name = name
self.heater_entity_id = heater_entity_id self.heater_entity_id = heater_entity_id
self.ac_mode = ac_mode self.ac_mode = ac_mode
self.min_cycle_duration = min_cycle_duration self.min_cycle_duration = min_cycle_duration
self._tolerance = tolerance
self._active = False self._active = False
self._cur_temp = None self._cur_temp = None
@ -193,7 +198,7 @@ class GenericThermostat(ClimateDevice):
return return
if self.ac_mode: if self.ac_mode:
too_hot = self._cur_temp - self._target_temp > TOL_TEMP too_hot = self._cur_temp - self._target_temp > self._tolerance
is_cooling = self._is_device_active is_cooling = self._is_device_active
if too_hot and not is_cooling: if too_hot and not is_cooling:
_LOGGER.info('Turning on AC %s', self.heater_entity_id) _LOGGER.info('Turning on AC %s', self.heater_entity_id)
@ -202,7 +207,7 @@ class GenericThermostat(ClimateDevice):
_LOGGER.info('Turning off AC %s', self.heater_entity_id) _LOGGER.info('Turning off AC %s', self.heater_entity_id)
switch.turn_off(self.hass, self.heater_entity_id) switch.turn_off(self.hass, self.heater_entity_id)
else: else:
too_cold = self._target_temp - self._cur_temp > TOL_TEMP too_cold = self._target_temp - self._cur_temp > self._tolerance
is_heating = self._is_device_active is_heating = self._is_device_active
if too_cold and not is_heating: if too_cold and not is_heating:

View file

@ -5,10 +5,11 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.homematic/ https://home-assistant.io/components/climate.homematic/
""" """
import logging import logging
import homeassistant.components.homematic as homematic
from homeassistant.components.climate import ClimateDevice, STATE_AUTO from homeassistant.components.climate import ClimateDevice, STATE_AUTO
from homeassistant.components.homematic import HMDevice
from homeassistant.util.temperature import convert from homeassistant.util.temperature import convert
from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE
from homeassistant.loader import get_component
DEPENDENCIES = ['homematic'] DEPENDENCIES = ['homematic']
@ -29,14 +30,16 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None):
if discovery_info is None: if discovery_info is None:
return return
homematic = get_component("homematic")
return homematic.setup_hmdevice_discovery_helper( return homematic.setup_hmdevice_discovery_helper(
hass,
HMThermostat, HMThermostat,
discovery_info, discovery_info,
add_callback_devices add_callback_devices
) )
class HMThermostat(homematic.HMDevice, ClimateDevice): class HMThermostat(HMDevice, ClimateDevice):
"""Representation of a Homematic thermostat.""" """Representation of a Homematic thermostat."""
@property @property
@ -94,13 +97,9 @@ class HMThermostat(homematic.HMDevice, ClimateDevice):
def set_temperature(self, **kwargs): def set_temperature(self, **kwargs):
"""Set new target temperature.""" """Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE) temperature = kwargs.get(ATTR_TEMPERATURE)
if not self.available: if not self.available or temperature is None:
return None return None
if temperature is None:
return
if self.current_operation == STATE_AUTO:
return self._hmdevice.actionNodeData('MANU_MODE', temperature)
self._hmdevice.set_temperature(temperature) self._hmdevice.set_temperature(temperature)
def set_operation_mode(self, operation_mode): def set_operation_mode(self, operation_mode):

View file

@ -14,7 +14,8 @@ from homeassistant.components.climate import (
PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
ATTR_TEMPERATURE) ATTR_TEMPERATURE)
from homeassistant.const import ( from homeassistant.const import (
TEMP_CELSIUS, CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN) TEMP_CELSIUS, TEMP_FAHRENHEIT,
CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN)
DEPENDENCIES = ['nest'] DEPENDENCIES = ['nest']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -24,10 +25,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.All(vol.Coerce(int), vol.Range(min=1)), vol.All(vol.Coerce(int), vol.Range(min=1)),
}) })
STATE_ECO = 'eco'
STATE_HEAT_COOL = 'heat-cool'
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Nest thermostat.""" """Setup the Nest thermostat."""
_LOGGER.debug("Setting up nest thermostat")
if discovery_info is None:
return
temp_unit = hass.config.units.temperature_unit temp_unit = hass.config.units.temperature_unit
add_devices( add_devices(
[NestThermostat(structure, device, temp_unit) [NestThermostat(structure, device, temp_unit)
for structure, device in hass.data[DATA_NEST].devices()], for structure, device in hass.data[DATA_NEST].devices()],
@ -58,9 +67,9 @@ class NestThermostat(ClimateDevice):
if self.device.can_heat and self.device.can_cool: if self.device.can_heat and self.device.can_cool:
self._operation_list.append(STATE_AUTO) self._operation_list.append(STATE_AUTO)
self._operation_list.append(STATE_ECO)
# feature of device # feature of device
self._has_humidifier = self.device.has_humidifier
self._has_dehumidifier = self.device.has_dehumidifier
self._has_fan = self.device.has_fan self._has_fan = self.device.has_fan
# data attributes # data attributes
@ -68,41 +77,24 @@ class NestThermostat(ClimateDevice):
self._location = None self._location = None
self._name = None self._name = None
self._humidity = None self._humidity = None
self._target_humidity = None
self._target_temperature = None self._target_temperature = None
self._temperature = None self._temperature = None
self._temperature_scale = None
self._mode = None self._mode = None
self._fan = None self._fan = None
self._away_temperature = None self._eco_temperature = None
self._is_locked = None
self._locked_temperature = None
@property @property
def name(self): def name(self):
"""Return the name of the nest, if any.""" """Return the name of the nest, if any."""
if self._location is None:
return self._name return self._name
else:
if self._name == '':
return self._location.capitalize()
else:
return self._location.capitalize() + '(' + self._name + ')'
@property @property
def temperature_unit(self): def temperature_unit(self):
"""Return the unit of measurement.""" """Return the unit of measurement."""
return TEMP_CELSIUS return self._temperature_scale
@property
def device_state_attributes(self):
"""Return the device specific state attributes."""
if self._has_humidifier or self._has_dehumidifier:
# Move these to Thermostat Device and make them global
return {
"humidity": self._humidity,
"target_humidity": self._target_humidity,
}
else:
# No way to control humidity not show setting
return {}
@property @property
def current_temperature(self): def current_temperature(self):
@ -112,21 +104,17 @@ class NestThermostat(ClimateDevice):
@property @property
def current_operation(self): def current_operation(self):
"""Return current operation ie. heat, cool, idle.""" """Return current operation ie. heat, cool, idle."""
if self._mode == 'cool': if self._mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]:
return STATE_COOL return self._mode
elif self._mode == 'heat': elif self._mode == STATE_HEAT_COOL:
return STATE_HEAT
elif self._mode == 'range':
return STATE_AUTO return STATE_AUTO
elif self._mode == 'off':
return STATE_OFF
else: else:
return STATE_UNKNOWN return STATE_UNKNOWN
@property @property
def target_temperature(self): def target_temperature(self):
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
if self._mode != 'range' and not self.is_away_mode_on: if self._mode != STATE_HEAT_COOL and not self.is_away_mode_on:
return self._target_temperature return self._target_temperature
else: else:
return None return None
@ -134,10 +122,11 @@ class NestThermostat(ClimateDevice):
@property @property
def target_temperature_low(self): def target_temperature_low(self):
"""Return the lower bound temperature we try to reach.""" """Return the lower bound temperature we try to reach."""
if self.is_away_mode_on and self._away_temperature[0]: if (self.is_away_mode_on or self._mode == STATE_ECO) and \
# away_temperature is always a low, high tuple self._eco_temperature[0]:
return self._away_temperature[0] # eco_temperature is always a low, high tuple
if self._mode == 'range': return self._eco_temperature[0]
if self._mode == STATE_HEAT_COOL:
return self._target_temperature[0] return self._target_temperature[0]
else: else:
return None return None
@ -145,10 +134,11 @@ class NestThermostat(ClimateDevice):
@property @property
def target_temperature_high(self): def target_temperature_high(self):
"""Return the upper bound temperature we try to reach.""" """Return the upper bound temperature we try to reach."""
if self.is_away_mode_on and self._away_temperature[1]: if (self.is_away_mode_on or self._mode == STATE_ECO) and \
# away_temperature is always a low, high tuple self._eco_temperature[1]:
return self._away_temperature[1] # eco_temperature is always a low, high tuple
if self._mode == 'range': return self._eco_temperature[1]
if self._mode == STATE_HEAT_COOL:
return self._target_temperature[1] return self._target_temperature[1]
else: else:
return None return None
@ -163,8 +153,7 @@ class NestThermostat(ClimateDevice):
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
if target_temp_low is not None and target_temp_high is not None: if target_temp_low is not None and target_temp_high is not None:
if self._mode == STATE_HEAT_COOL:
if self._mode == 'range':
temp = (target_temp_low, target_temp_high) temp = (target_temp_low, target_temp_high)
else: else:
temp = kwargs.get(ATTR_TEMPERATURE) temp = kwargs.get(ATTR_TEMPERATURE)
@ -173,14 +162,11 @@ class NestThermostat(ClimateDevice):
def set_operation_mode(self, operation_mode): def set_operation_mode(self, operation_mode):
"""Set operation mode.""" """Set operation mode."""
if operation_mode == STATE_HEAT: if operation_mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]:
self.device.mode = 'heat' device_mode = operation_mode
elif operation_mode == STATE_COOL:
self.device.mode = 'cool'
elif operation_mode == STATE_AUTO: elif operation_mode == STATE_AUTO:
self.device.mode = 'range' device_mode = STATE_HEAT_COOL
elif operation_mode == STATE_OFF: self.device.mode = device_mode
self.device.mode = 'off'
@property @property
def operation_list(self): def operation_list(self):
@ -217,30 +203,33 @@ class NestThermostat(ClimateDevice):
@property @property
def min_temp(self): def min_temp(self):
"""Identify min_temp in Nest API or defaults if not available.""" """Identify min_temp in Nest API or defaults if not available."""
temp = self._away_temperature[0] if self._is_locked:
if temp is None: return self._locked_temperature[0]
return super().min_temp
else: else:
return temp return None
@property @property
def max_temp(self): def max_temp(self):
"""Identify max_temp in Nest API or defaults if not available.""" """Identify max_temp in Nest API or defaults if not available."""
temp = self._away_temperature[1] if self._is_locked:
if temp is None: return self._locked_temperature[1]
return super().max_temp
else: else:
return temp return None
def update(self): def update(self):
"""Cache value from Python-nest.""" """Cache value from Python-nest."""
self._location = self.device.where self._location = self.device.where
self._name = self.device.name self._name = self.device.name
self._humidity = self.device.humidity, self._humidity = self.device.humidity,
self._target_humidity = self.device.target_humidity,
self._temperature = self.device.temperature self._temperature = self.device.temperature
self._mode = self.device.mode self._mode = self.device.mode
self._target_temperature = self.device.target self._target_temperature = self.device.target
self._fan = self.device.fan self._fan = self.device.fan
self._away = self.structure.away self._away = self.structure.away == 'away'
self._away_temperature = self.device.away_temperature self._eco_temperature = self.device.eco_temperature
self._locked_temperature = self.device.locked_temperature
self._is_locked = self.device.is_locked
if self.device.temperature == 'C':
self._temperature_scale = TEMP_CELSIUS
else:
self._temperature_scale = TEMP_FAHRENHEIT

View file

@ -7,7 +7,8 @@ https://home-assistant.io/components/climate.proliphix/
import voluptuous as vol import voluptuous as vol
from homeassistant.components.climate import ( from homeassistant.components.climate import (
STATE_COOL, STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA) PRECISION_TENTHS, STATE_COOL, STATE_HEAT, STATE_IDLE,
ClimateDevice, PLATFORM_SCHEMA)
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -60,6 +61,15 @@ class ProliphixThermostat(ClimateDevice):
"""Return the name of the thermostat.""" """Return the name of the thermostat."""
return self._name return self._name
@property
def precision(self):
"""Return the precision of the system.
Proliphix temperature values are passed back and forth in the
API as tenths of degrees F (i.e. 690 for 69 degrees).
"""
return PRECISION_TENTHS
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the device specific state attributes.""" """Return the device specific state attributes."""

View file

@ -30,7 +30,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Wink thermostat.""" """Setup the Wink thermostat."""
import pywink import pywink
temp_unit = hass.config.units.temperature_unit temp_unit = hass.config.units.temperature_unit
add_devices(WinkThermostat(thermostat, temp_unit) add_devices(WinkThermostat(thermostat, hass, temp_unit)
for thermostat in pywink.get_thermostats()) for thermostat in pywink.get_thermostats())
@ -38,9 +38,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class WinkThermostat(WinkDevice, ClimateDevice): class WinkThermostat(WinkDevice, ClimateDevice):
"""Representation of a Wink thermostat.""" """Representation of a Wink thermostat."""
def __init__(self, wink, temp_unit): def __init__(self, wink, hass, temp_unit):
"""Initialize the Wink device.""" """Initialize the Wink device."""
super().__init__(wink) super().__init__(wink, hass)
wink = get_component('wink') wink = get_component('wink')
self._config_temp_unit = temp_unit self._config_temp_unit = temp_unit

View file

@ -12,7 +12,8 @@ import logging
from homeassistant.const import STATE_UNKNOWN from homeassistant.const import STATE_UNKNOWN
from homeassistant.components.cover import CoverDevice,\ from homeassistant.components.cover import CoverDevice,\
ATTR_POSITION ATTR_POSITION
import homeassistant.components.homematic as homematic from homeassistant.components.homematic import HMDevice
from homeassistant.loader import get_component
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -24,14 +25,16 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None):
if discovery_info is None: if discovery_info is None:
return return
homematic = get_component("homematic")
return homematic.setup_hmdevice_discovery_helper( return homematic.setup_hmdevice_discovery_helper(
hass,
HMCover, HMCover,
discovery_info, discovery_info,
add_callback_devices add_callback_devices
) )
class HMCover(homematic.HMDevice, CoverDevice): class HMCover(HMDevice, CoverDevice):
"""Represents a Homematic Cover in Home Assistant.""" """Represents a Homematic Cover in Home Assistant."""
@property @property

View file

@ -15,18 +15,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Wink cover platform.""" """Setup the Wink cover platform."""
import pywink import pywink
add_devices(WinkCoverDevice(shade) for shade in add_devices(WinkCoverDevice(shade, hass) for shade in
pywink.get_shades()) pywink.get_shades())
add_devices(WinkCoverDevice(door) for door in add_devices(WinkCoverDevice(door, hass) for door in
pywink.get_garage_doors()) pywink.get_garage_doors())
class WinkCoverDevice(WinkDevice, CoverDevice): class WinkCoverDevice(WinkDevice, CoverDevice):
"""Representation of a Wink cover device.""" """Representation of a Wink cover device."""
def __init__(self, wink): def __init__(self, wink, hass):
"""Initialize the cover.""" """Initialize the cover."""
WinkDevice.__init__(self, wink) WinkDevice.__init__(self, wink, hass)
def close_cover(self): def close_cover(self):
"""Close the shade.""" """Close the shade."""

View file

@ -86,16 +86,11 @@ def setup(hass, config):
group.Group.create_group(hass, 'people', [ group.Group.create_group(hass, 'people', [
'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy', 'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy',
'device_tracker.demo_paulus']) 'device_tracker.demo_paulus'])
group.Group.create_group(hass, 'thermostats', [
'thermostat.nest', 'thermostat.thermostat'])
group.Group.create_group(hass, 'downstairs', [ group.Group.create_group(hass, 'downstairs', [
'group.living_room', 'group.kitchen', 'group.living_room', 'group.kitchen',
'scene.romantic_lights', 'rollershutter.kitchen_window', 'scene.romantic_lights', 'rollershutter.kitchen_window',
'rollershutter.living_room_window', 'group.doors', 'rollershutter.living_room_window', 'group.doors',
'thermostat.nest', 'thermostat.ecobee',
], view=True)
group.Group.create_group(hass, 'Upstairs', [
'thermostat.thermostat', 'group.bedroom',
], view=True) ], view=True)
# Setup scripts # Setup scripts

View file

@ -10,6 +10,8 @@ import logging
import os import os
from typing import Any, Sequence, Callable from typing import Any, Sequence, Callable
import aiohttp
import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.bootstrap import ( from homeassistant.bootstrap import (
@ -19,6 +21,7 @@ from homeassistant.components import group, zone
from homeassistant.components.discovery import SERVICE_NETGEAR from homeassistant.components.discovery import SERVICE_NETGEAR
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers import config_per_platform, discovery
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
@ -278,6 +281,9 @@ class DeviceTracker(object):
yield from self.group.async_update_tracked_entity_ids( yield from self.group.async_update_tracked_entity_ids(
list(self.group.tracking) + [device.entity_id]) list(self.group.tracking) + [device.entity_id])
# lookup mac vendor string to be stored in config
device.set_vendor_for_mac()
# update known_devices.yaml # update known_devices.yaml
self.hass.async_add_job( self.hass.async_add_job(
self.async_update_config(self.hass.config.path(YAML_DEVICES), self.async_update_config(self.hass.config.path(YAML_DEVICES),
@ -291,7 +297,7 @@ class DeviceTracker(object):
This method is a coroutine. This method is a coroutine.
""" """
with (yield from self._is_updating): with (yield from self._is_updating):
self.hass.loop.run_in_executor( yield from self.hass.loop.run_in_executor(
None, update_config, self.hass.config.path(YAML_DEVICES), None, update_config, self.hass.config.path(YAML_DEVICES),
dev_id, device) dev_id, device)
@ -328,6 +334,7 @@ class Device(Entity):
last_seen = None # type: dt_util.dt.datetime last_seen = None # type: dt_util.dt.datetime
battery = None # type: str battery = None # type: str
attributes = None # type: dict attributes = None # type: dict
vendor = None # type: str
# Track if the last update of this device was HOME. # Track if the last update of this device was HOME.
last_update_home = False last_update_home = False
@ -336,7 +343,7 @@ class Device(Entity):
def __init__(self, hass: HomeAssistantType, consider_home: timedelta, def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
track: bool, dev_id: str, mac: str, name: str=None, track: bool, dev_id: str, mac: str, name: str=None,
picture: str=None, gravatar: str=None, picture: str=None, gravatar: str=None,
hide_if_away: bool=False) -> None: hide_if_away: bool=False, vendor: str=None) -> None:
"""Initialize a device.""" """Initialize a device."""
self.hass = hass self.hass = hass
self.entity_id = ENTITY_ID_FORMAT.format(dev_id) self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
@ -362,6 +369,7 @@ class Device(Entity):
self.config_picture = picture self.config_picture = picture
self.away_hide = hide_if_away self.away_hide = hide_if_away
self.vendor = vendor
@property @property
def name(self): def name(self):
@ -460,6 +468,53 @@ class Device(Entity):
self._state = STATE_HOME self._state = STATE_HOME
self.last_update_home = True self.last_update_home = True
@asyncio.coroutine
def set_vendor_for_mac(self):
"""Set vendor string using api.macvendors.com."""
self.vendor = yield from self.get_vendor_for_mac()
@asyncio.coroutine
def get_vendor_for_mac(self):
"""Try to find the vendor string for a given MAC address."""
# can't continue without a mac
if not self.mac:
return None
# prevent lookup of invalid macs
if not len(self.mac.split(':')) == 6:
return 'unknown'
# we only need the first 3 bytes of the mac for a lookup
# this improves somewhat on privacy
oui_bytes = self.mac.split(':')[0:3]
# bytes like 00 get truncates to 0, API needs full bytes
oui = '{:02x}:{:02x}:{:02x}'.format(*[int(b, 16) for b in oui_bytes])
url = 'http://api.macvendors.com/' + oui
resp = None
try:
websession = async_get_clientsession(self.hass)
with async_timeout.timeout(5, loop=self.hass.loop):
resp = yield from websession.get(url)
# mac vendor found, response is the string
if resp.status == 200:
vendor_string = yield from resp.text()
return vendor_string
# if vendor is not known to the API (404) or there
# was a failure during the lookup (500); set vendor
# to something other then None to prevent retry
# as the value is only relevant when it is to be stored
# in the 'known_devices.yaml' file which only happens
# the first time the device is seen.
return 'unknown'
except (asyncio.TimeoutError, aiohttp.errors.ClientError,
aiohttp.errors.ClientDisconnectedError):
# same as above
return 'unknown'
finally:
if resp is not None:
yield from resp.release()
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta): def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
"""Load devices from YAML configuration file.""" """Load devices from YAML configuration file."""
@ -483,7 +538,8 @@ def async_load_config(path: str, hass: HomeAssistantType,
vol.Optional('gravatar', default=None): vol.Any(None, cv.string), vol.Optional('gravatar', default=None): vol.Any(None, cv.string),
vol.Optional('picture', default=None): vol.Any(None, cv.string), vol.Optional('picture', default=None): vol.Any(None, cv.string),
vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All( vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All(
cv.time_period, cv.positive_timedelta) cv.time_period, cv.positive_timedelta),
vol.Optional('vendor', default=None): vol.Any(None, cv.string),
}) })
try: try:
result = [] result = []
@ -546,7 +602,8 @@ def update_config(path: str, dev_id: str, device: Device):
'mac': device.mac, 'mac': device.mac,
'picture': device.config_picture, 'picture': device.config_picture,
'track': device.track, 'track': device.track,
CONF_AWAY_HIDE: device.away_hide CONF_AWAY_HIDE: device.away_hide,
'vendor': device.vendor,
}} }}
out.write('\n') out.write('\n')
out.write(dump(device)) out.write(dump(device))

View file

@ -0,0 +1,72 @@
"""
Support for the GPSLogger platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.gpslogger/
"""
import asyncio
from functools import partial
import logging
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY
from homeassistant.components.http import HomeAssistantView
# pylint: disable=unused-import
from homeassistant.components.device_tracker import ( # NOQA
DOMAIN, PLATFORM_SCHEMA)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http']
def setup_scanner(hass, config, see):
"""Setup an endpoint for the GPSLogger application."""
hass.http.register_view(GPSLoggerView(see))
return True
class GPSLoggerView(HomeAssistantView):
"""View to handle gpslogger requests."""
url = '/api/gpslogger'
name = 'api:gpslogger'
def __init__(self, see):
"""Initialize GPSLogger url endpoints."""
self.see = see
@asyncio.coroutine
def get(self, request):
"""A GPSLogger message received as GET."""
res = yield from self._handle(request.app['hass'], request.GET)
return res
@asyncio.coroutine
def _handle(self, hass, data):
"""Handle gpslogger request."""
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)
device = data['device'].replace('-', '')
gps_location = (data['latitude'], data['longitude'])
accuracy = 200
battery = -1
if 'accuracy' in data:
accuracy = int(float(data['accuracy']))
if 'battery' in data:
battery = float(data['battery'])
yield from hass.loop.run_in_executor(
None, partial(self.see, dev_id=device,
gps=gps_location, battery=battery,
gps_accuracy=accuracy))
return 'Setting location for {}'.format(device)

View file

@ -23,7 +23,7 @@ DEPENDENCIES = ['http']
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."""
hass.http.register_view(LocativeView(hass, see)) hass.http.register_view(LocativeView(see))
return True return True
@ -34,27 +34,26 @@ class LocativeView(HomeAssistantView):
url = '/api/locative' url = '/api/locative'
name = 'api:locative' name = 'api:locative'
def __init__(self, hass, see): def __init__(self, see):
"""Initialize Locative url endpoints.""" """Initialize Locative url endpoints."""
super().__init__(hass)
self.see = see self.see = see
@asyncio.coroutine @asyncio.coroutine
def get(self, request): def get(self, request):
"""Locative message received as GET.""" """Locative message received as GET."""
res = yield from self._handle(request.GET) res = yield from self._handle(request.app['hass'], request.GET)
return res return res
@asyncio.coroutine @asyncio.coroutine
def post(self, request): def post(self, request):
"""Locative message received.""" """Locative message received."""
data = yield from request.post() data = yield from request.post()
res = yield from self._handle(data) res = yield from self._handle(request.app['hass'], data)
return res return res
@asyncio.coroutine @asyncio.coroutine
# pylint: disable=too-many-return-statements # pylint: disable=too-many-return-statements
def _handle(self, data): def _handle(self, hass, data):
"""Handle locative request.""" """Handle locative request."""
if 'latitude' not in data or 'longitude' not in data: if 'latitude' not in data or 'longitude' not in data:
return ('Latitude and longitude not specified.', return ('Latitude and longitude not specified.',
@ -81,19 +80,19 @@ class LocativeView(HomeAssistantView):
gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]) gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE])
if direction == 'enter': if direction == 'enter':
yield from self.hass.loop.run_in_executor( yield from hass.loop.run_in_executor(
None, partial(self.see, dev_id=device, None, partial(self.see, dev_id=device,
location_name=location_name, location_name=location_name,
gps=gps_location)) gps=gps_location))
return 'Setting location to {}'.format(location_name) return 'Setting location to {}'.format(location_name)
elif direction == 'exit': elif direction == 'exit':
current_state = self.hass.states.get( current_state = hass.states.get(
'{}.{}'.format(DOMAIN, device)) '{}.{}'.format(DOMAIN, device))
if current_state is None or current_state.state == location_name: if current_state is None or current_state.state == location_name:
location_name = STATE_NOT_HOME location_name = STATE_NOT_HOME
yield from self.hass.loop.run_in_executor( yield from hass.loop.run_in_executor(
None, partial(self.see, dev_id=device, None, partial(self.see, dev_id=device,
location_name=location_name, location_name=location_name,
gps=gps_location)) gps=gps_location))

View file

@ -75,14 +75,16 @@ def setup(hass, yaml_config):
api_password=None, api_password=None,
ssl_certificate=None, ssl_certificate=None,
ssl_key=None, ssl_key=None,
cors_origins=[], cors_origins=None,
use_x_forwarded_for=False, use_x_forwarded_for=False,
trusted_networks=[] trusted_networks=[],
login_threshold=0,
is_ban_enabled=False
) )
server.register_view(DescriptionXmlView(hass, config)) server.register_view(DescriptionXmlView(config))
server.register_view(HueUsernameView(hass)) server.register_view(HueUsernameView)
server.register_view(HueLightsView(hass, config)) server.register_view(HueLightsView(config))
upnp_listener = UPNPResponderThread( upnp_listener = UPNPResponderThread(
config.host_ip_addr, config.listen_port) config.host_ip_addr, config.listen_port)
@ -154,9 +156,8 @@ class DescriptionXmlView(HomeAssistantView):
name = 'description:xml' name = 'description:xml'
requires_auth = False requires_auth = False
def __init__(self, hass, config): def __init__(self, config):
"""Initialize the instance of the view.""" """Initialize the instance of the view."""
super().__init__(hass)
self.config = config self.config = config
@core.callback @core.callback
@ -198,10 +199,6 @@ class HueUsernameView(HomeAssistantView):
extra_urls = ['/api/'] extra_urls = ['/api/']
requires_auth = False requires_auth = False
def __init__(self, hass):
"""Initialize the instance of the view."""
super().__init__(hass)
@asyncio.coroutine @asyncio.coroutine
def post(self, request): def post(self, request):
"""Handle a POST request.""" """Handle a POST request."""
@ -226,30 +223,33 @@ class HueLightsView(HomeAssistantView):
'/api/{username}/lights/{entity_id}/state'] '/api/{username}/lights/{entity_id}/state']
requires_auth = False requires_auth = False
def __init__(self, hass, config): def __init__(self, config):
"""Initialize the instance of the view.""" """Initialize the instance of the view."""
super().__init__(hass)
self.config = config self.config = config
self.cached_states = {} self.cached_states = {}
@core.callback @core.callback
def get(self, request, username, entity_id=None): def get(self, request, username, entity_id=None):
"""Handle a GET request.""" """Handle a GET request."""
hass = request.app['hass']
if entity_id is None: if entity_id is None:
return self.async_get_lights_list() return self.async_get_lights_list(hass)
if not request.path.endswith('state'): if not request.path.endswith('state'):
return self.async_get_light_state(entity_id) return self.async_get_light_state(hass, entity_id)
return web.Response(text="Method not allowed", status=405) return web.Response(text="Method not allowed", status=405)
@asyncio.coroutine @asyncio.coroutine
def put(self, request, username, entity_id=None): def put(self, request, username, entity_id=None):
"""Handle a PUT request.""" """Handle a PUT request."""
hass = request.app['hass']
if not request.path.endswith('state'): if not request.path.endswith('state'):
return web.Response(text="Method not allowed", status=405) return web.Response(text="Method not allowed", status=405)
if entity_id and self.hass.states.get(entity_id) is None: if entity_id and hass.states.get(entity_id) is None:
return self.json_message('Entity not found', HTTP_NOT_FOUND) return self.json_message('Entity not found', HTTP_NOT_FOUND)
try: try:
@ -257,24 +257,25 @@ class HueLightsView(HomeAssistantView):
except ValueError: except ValueError:
return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
result = yield from self.async_put_light_state(json_data, entity_id) result = yield from self.async_put_light_state(hass, json_data,
entity_id)
return result return result
@core.callback @core.callback
def async_get_lights_list(self): def async_get_lights_list(self, hass):
"""Process a request to get the list of available lights.""" """Process a request to get the list of available lights."""
json_response = {} json_response = {}
for entity in self.hass.states.async_all(): for entity in hass.states.async_all():
if self.is_entity_exposed(entity): if self.is_entity_exposed(entity):
json_response[entity.entity_id] = entity_to_json(entity) json_response[entity.entity_id] = entity_to_json(entity)
return self.json(json_response) return self.json(json_response)
@core.callback @core.callback
def async_get_light_state(self, entity_id): def async_get_light_state(self, hass, entity_id):
"""Process a request to get the state of an individual light.""" """Process a request to get the state of an individual light."""
entity = self.hass.states.get(entity_id) entity = hass.states.get(entity_id)
if entity is None or not self.is_entity_exposed(entity): if entity is None or not self.is_entity_exposed(entity):
return web.Response(text="Entity not found", status=404) return web.Response(text="Entity not found", status=404)
@ -292,12 +293,12 @@ class HueLightsView(HomeAssistantView):
return self.json(json_response) return self.json(json_response)
@asyncio.coroutine @asyncio.coroutine
def async_put_light_state(self, request_json, entity_id): def async_put_light_state(self, hass, request_json, entity_id):
"""Process a request to set the state of an individual light.""" """Process a request to set the state of an individual light."""
config = self.config config = self.config
# Retrieve the entity from the state machine # Retrieve the entity from the state machine
entity = self.hass.states.get(entity_id) entity = hass.states.get(entity_id)
if entity is None: if entity is None:
return web.Response(text="Entity not found", status=404) return web.Response(text="Entity not found", status=404)
@ -342,7 +343,7 @@ class HueLightsView(HomeAssistantView):
self.cached_states[entity_id] = (result, brightness) self.cached_states[entity_id] = (result, brightness)
# Perform the requested action # Perform the requested action
yield from self.hass.services.async_call(core.DOMAIN, service, data, yield from hass.services.async_call(core.DOMAIN, service, data,
blocking=True) blocking=True)
json_response = \ json_response = \

View file

@ -75,8 +75,7 @@ def setup(hass, config):
descriptions[DOMAIN][SERVICE_CHECKIN], descriptions[DOMAIN][SERVICE_CHECKIN],
schema=CHECKIN_SERVICE_SCHEMA) schema=CHECKIN_SERVICE_SCHEMA)
hass.http.register_view(FoursquarePushReceiver( hass.http.register_view(FoursquarePushReceiver(config[CONF_PUSH_SECRET]))
hass, config[CONF_PUSH_SECRET]))
return True return True
@ -88,9 +87,8 @@ class FoursquarePushReceiver(HomeAssistantView):
url = "/api/foursquare" url = "/api/foursquare"
name = "foursquare" name = "foursquare"
def __init__(self, hass, push_secret): def __init__(self, push_secret):
"""Initialize the OAuth callback view.""" """Initialize the OAuth callback view."""
super().__init__(hass)
self.push_secret = push_secret self.push_secret = push_secret
@asyncio.coroutine @asyncio.coroutine
@ -110,4 +108,4 @@ class FoursquarePushReceiver(HomeAssistantView):
"push secret: %s", secret) "push secret: %s", secret)
return self.json_message('Incorrect secret', HTTP_BAD_REQUEST) return self.json_message('Incorrect secret', HTTP_BAD_REQUEST)
self.hass.bus.async_fire(EVENT_PUSH, data) request.app['hass'].bus.async_fire(EVENT_PUSH, data)

View file

@ -8,17 +8,18 @@ import os
from aiohttp import web from aiohttp import web
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.const import EVENT_HOMEASSISTANT_START, HTTP_NOT_FOUND from homeassistant.const import HTTP_NOT_FOUND
from homeassistant.components import api, group from homeassistant.components import api, group
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.auth import is_trusted_ip
from homeassistant.components.http.const import KEY_DEVELOPMENT
from .version import FINGERPRINTS from .version import FINGERPRINTS
DOMAIN = 'frontend' DOMAIN = 'frontend'
DEPENDENCIES = ['api'] DEPENDENCIES = ['api', 'websocket_api']
URL_PANEL_COMPONENT = '/frontend/panels/{}.html' URL_PANEL_COMPONENT = '/frontend/panels/{}.html'
URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html' URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static') STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static')
PANELS = {}
MANIFEST_JSON = { MANIFEST_JSON = {
"background_color": "#FFFFFF", "background_color": "#FFFFFF",
"description": "Open-source home automation platform running on Python 3.", "description": "Open-source home automation platform running on Python 3.",
@ -32,6 +33,16 @@ MANIFEST_JSON = {
"theme_color": "#03A9F4" "theme_color": "#03A9F4"
} }
for size in (192, 384, 512, 1024):
MANIFEST_JSON['icons'].append({
"src": "/static/icons/favicon-{}x{}.png".format(size, size),
"sizes": "{}x{}".format(size, size),
"type": "image/png"
})
DATA_PANELS = 'frontend_panels'
DATA_INDEX_VIEW = 'frontend_index_view'
# To keep track we don't register a component twice (gives a warning) # To keep track we don't register a component twice (gives a warning)
_REGISTERED_COMPONENTS = set() _REGISTERED_COMPONENTS = set()
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -68,10 +79,14 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
Warning: this API will probably change. Use at own risk. Warning: this API will probably change. Use at own risk.
""" """
panels = hass.data.get(DATA_PANELS)
if panels is None:
panels = hass.data[DATA_PANELS] = {}
if url_path is None: if url_path is None:
url_path = component_name url_path = component_name
if url_path in PANELS: if url_path in panels:
_LOGGER.warning('Overwriting component %s', url_path) _LOGGER.warning('Overwriting component %s', url_path)
if not os.path.isfile(path): if not os.path.isfile(path):
_LOGGER.error('Panel %s component does not exist: %s', _LOGGER.error('Panel %s component does not exist: %s',
@ -106,7 +121,15 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
fprinted_url = URL_PANEL_COMPONENT_FP.format(component_name, md5) fprinted_url = URL_PANEL_COMPONENT_FP.format(component_name, md5)
data['url'] = fprinted_url data['url'] = fprinted_url
PANELS[url_path] = data panels[url_path] = data
# Register index view for this route if IndexView already loaded
# Otherwise it will be done during setup.
index_view = hass.data.get(DATA_INDEX_VIEW)
if index_view:
hass.http.app.router.add_route('get', '/{}'.format(url_path),
index_view.get)
def add_manifest_json_key(key, val): def add_manifest_json_key(key, val):
@ -134,29 +157,24 @@ def setup(hass, config):
if os.path.isdir(local): if os.path.isdir(local):
hass.http.register_static_path("/local", local) hass.http.register_static_path("/local", local)
index_view = hass.data[DATA_INDEX_VIEW] = IndexView()
hass.http.register_view(index_view)
# Components have registered panels before frontend got setup.
# Now register their urls.
if DATA_PANELS in hass.data:
for url_path in hass.data[DATA_PANELS]:
hass.http.app.router.add_route('get', '/{}'.format(url_path),
index_view.get)
else:
hass.data[DATA_PANELS] = {}
register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location') register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location')
for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state', for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state',
'dev-template'): 'dev-template'):
register_built_in_panel(hass, panel) register_built_in_panel(hass, panel)
def register_frontend_index(event):
"""Register the frontend index urls.
Done when Home Assistant is started so that all panels are known.
"""
hass.http.register_view(IndexView(
hass, ['/{}'.format(name) for name in PANELS]))
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, register_frontend_index)
for size in (192, 384, 512, 1024):
MANIFEST_JSON['icons'].append({
"src": "/static/icons/favicon-{}x{}.png".format(size, size),
"sizes": "{}x{}".format(size, size),
"type": "image/png"
})
return True return True
@ -169,12 +187,14 @@ class BootstrapView(HomeAssistantView):
@callback @callback
def get(self, request): def get(self, request):
"""Return all data needed to bootstrap Home Assistant.""" """Return all data needed to bootstrap Home Assistant."""
hass = request.app['hass']
return self.json({ return self.json({
'config': self.hass.config.as_dict(), 'config': hass.config.as_dict(),
'states': self.hass.states.async_all(), 'states': hass.states.async_all(),
'events': api.async_events_json(self.hass), 'events': api.async_events_json(hass),
'services': api.async_services_json(self.hass), 'services': api.async_services_json(hass),
'panels': PANELS, 'panels': hass.data[DATA_PANELS],
}) })
@ -186,13 +206,10 @@ class IndexView(HomeAssistantView):
requires_auth = False requires_auth = False
extra_urls = ['/states', '/states/{entity_id}'] extra_urls = ['/states', '/states/{entity_id}']
def __init__(self, hass, extra_urls): def __init__(self):
"""Initialize the frontend view.""" """Initialize the frontend view."""
super().__init__(hass)
from jinja2 import FileSystemLoader, Environment from jinja2 import FileSystemLoader, Environment
self.extra_urls = self.extra_urls + extra_urls
self.templates = Environment( self.templates = Environment(
loader=FileSystemLoader( loader=FileSystemLoader(
os.path.join(os.path.dirname(__file__), 'templates/') os.path.join(os.path.dirname(__file__), 'templates/')
@ -202,14 +219,16 @@ class IndexView(HomeAssistantView):
@asyncio.coroutine @asyncio.coroutine
def get(self, request, entity_id=None): def get(self, request, entity_id=None):
"""Serve the index view.""" """Serve the index view."""
hass = request.app['hass']
if entity_id is not None: if entity_id is not None:
state = self.hass.states.get(entity_id) state = hass.states.get(entity_id)
if (not state or state.domain != 'group' or if (not state or state.domain != 'group' or
not state.attributes.get(group.ATTR_VIEW)): not state.attributes.get(group.ATTR_VIEW)):
return self.json_message('Entity not found', HTTP_NOT_FOUND) return self.json_message('Entity not found', HTTP_NOT_FOUND)
if self.hass.http.development: if request.app[KEY_DEVELOPMENT]:
core_url = '/static/home-assistant-polymer/build/core.js' core_url = '/static/home-assistant-polymer/build/core.js'
ui_url = '/static/home-assistant-polymer/src/home-assistant.html' ui_url = '/static/home-assistant-polymer/src/home-assistant.html'
else: else:
@ -223,19 +242,21 @@ class IndexView(HomeAssistantView):
else: else:
panel = request.path.split('/')[1] panel = request.path.split('/')[1]
panel_url = PANELS[panel]['url'] if panel != 'states' else '' if panel == 'states':
panel_url = ''
else:
panel_url = hass.data[DATA_PANELS][panel]['url']
no_auth = 'true' no_auth = 'true'
if self.hass.config.api.api_password: if hass.config.api.api_password:
# require password if set # require password if set
no_auth = 'false' no_auth = 'false'
if self.hass.http.is_trusted_ip( if is_trusted_ip(request):
self.hass.http.get_real_ip(request)):
# bypass for trusted networks # bypass for trusted networks
no_auth = 'true' no_auth = 'true'
icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html']) icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html'])
template = yield from self.hass.loop.run_in_executor( template = yield from hass.loop.run_in_executor(
None, self.templates.get_template, 'index.html') None, self.templates.get_template, 'index.html')
# pylint is wrong # pylint is wrong
@ -244,7 +265,7 @@ class IndexView(HomeAssistantView):
resp = template.render( resp = template.render(
core_url=core_url, ui_url=ui_url, no_auth=no_auth, core_url=core_url, ui_url=ui_url, no_auth=no_auth,
icons_url=icons_url, icons=FINGERPRINTS['mdi.html'], icons_url=icons_url, icons=FINGERPRINTS['mdi.html'],
panel_url=panel_url, panels=PANELS) panel_url=panel_url, panels=hass.data[DATA_PANELS])
return web.Response(text=resp, content_type='text/html') return web.Response(text=resp, content_type='text/html')

View file

@ -1,17 +1,18 @@
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend.""" """DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
FINGERPRINTS = { FINGERPRINTS = {
"core.js": "5ed5e063d66eb252b5b288738c9c2d16", "core.js": "526d7d704ae478c30ae20c1426c2e4f4",
"frontend.html": "78be2dfedc4e95326cbcd9401fb17b4d", "frontend.html": "5baa4dc3b109ca80d4c282fb12c6c23a",
"mdi.html": "46a76f877ac9848899b8ed382427c16f", "mdi.html": "46a76f877ac9848899b8ed382427c16f",
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
"panels/ha-panel-dev-event.html": "550bf85345c454274a40d15b2795a002", "panels/ha-panel-dev-event.html": "c2d5ec676be98d4474d19f94d0262c1e",
"panels/ha-panel-dev-info.html": "ec613406ce7e20d93754233d55625c8a", "panels/ha-panel-dev-info.html": "ec613406ce7e20d93754233d55625c8a",
"panels/ha-panel-dev-service.html": "4a051878b92b002b8b018774ba207769", "panels/ha-panel-dev-service.html": "b3fe49532c5c03198fafb0c6ed58b76a",
"panels/ha-panel-dev-state.html": "65e5f791cc467561719bf591f1386054", "panels/ha-panel-dev-state.html": "65e5f791cc467561719bf591f1386054",
"panels/ha-panel-dev-template.html": "7d744ab7f7c08b6d6ad42069989de400", "panels/ha-panel-dev-template.html": "7d744ab7f7c08b6d6ad42069989de400",
"panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295", "panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295",
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab", "panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
"panels/ha-panel-logbook.html": "66108d82763359a218c9695f0553de40", "panels/ha-panel-logbook.html": "4bc5c8370a85a4215413fbae8f85addb",
"panels/ha-panel-map.html": "49ab2d6f180f8bdea7cffaa66b8a5d3e" "panels/ha-panel-map.html": "1bf6965b24d76db71a1871865cd4a3a2",
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
} }

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 6071315b1675dfef1090b4683c9639ef0f56cfc0 Subproject commit b76ad67d4abbc0cc492fc11842c9d163b4917ead

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,125 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket debug</title>
<style>
.controls {
display: flex;
flex-direction: row;
}
.controls textarea {
height: 160px;
min-width: 400px;
margin-right: 24px;
}
</style>
</head>
<body>
<div class='controls'>
<textarea id="messageinput">
{
"id": 1, "type": "subscribe_events", "event_type": "state_changed"
}
</textarea>
<pre>
Examples:
{
"id": 2, "type": "subscribe_events", "event_type": "state_changed"
}
{
"id": 3, "type": "call_service", "domain": "light", "service": "turn_off"
}
{
"id": 4, "type": "unsubscribe_events", "subscription": 2
}
{
"id": 5, "type": "get_states"
}
{
"id": 6, "type": "get_config"
}
{
"id": 7, "type": "get_services"
}
{
"id": 8, "type": "get_panels"
}
</pre>
</div>
<div>
<button type="button" onclick="openSocket();" >Open</button>
<button type="button" onclick="send();" >Send</button>
<button type="button" onclick="closeSocket();" >Close</button>
</div>
<!-- Server responses get written here -->
<pre id="messages"></pre>
<!-- Script to utilise the WebSocket -->
<script type="text/javascript">
var webSocket;
var messages = document.getElementById("messages");
function openSocket(){
var isOpen = false;
// Ensures only one connection is open at a time
if(webSocket !== undefined && webSocket.readyState !== WebSocket.CLOSED){
writeResponse("WebSocket is already opened.");
return;
}
// Create a new instance of the websocket
webSocket = new WebSocket("ws://localhost:8123/api/websocket");
/**
* Binds functions to the listeners for the websocket.
*/
webSocket.onopen = function(event){
if (!isOpen) {
isOpen = true;
writeResponse('Connection opened');
}
// For reasons I can't determine, onopen gets called twice
// and the first time event.data is undefined.
// Leave a comment if you know the answer.
if(event.data === undefined)
return;
writeResponse(event.data);
};
webSocket.onmessage = function(event){
writeResponse(event.data);
};
webSocket.onclose = function(event){
writeResponse("Connection closed");
};
}
/**
* Sends the value of the text input to the server
*/
function send(){
var text = document.getElementById("messageinput").value;
webSocket.send(text);
}
function closeSocket(){
webSocket.close();
}
function writeResponse(text){
messages.innerHTML += "\n" + text;
}
openSocket();
</script>
</body>
</html>

View file

@ -184,8 +184,8 @@ def setup(hass, config):
filters.included_entities = include[CONF_ENTITIES] filters.included_entities = include[CONF_ENTITIES]
filters.included_domains = include[CONF_DOMAINS] filters.included_domains = include[CONF_DOMAINS]
hass.http.register_view(Last5StatesView(hass)) hass.http.register_view(Last5StatesView)
hass.http.register_view(HistoryPeriodView(hass, filters)) hass.http.register_view(HistoryPeriodView(filters))
register_built_in_panel(hass, 'history', 'History', 'mdi:poll-box') register_built_in_panel(hass, 'history', 'History', 'mdi:poll-box')
return True return True
@ -197,14 +197,10 @@ class Last5StatesView(HomeAssistantView):
url = '/api/history/entity/{entity_id}/recent_states' url = '/api/history/entity/{entity_id}/recent_states'
name = 'api:history:entity-recent-states' name = 'api:history:entity-recent-states'
def __init__(self, hass):
"""Initilalize the history last 5 states view."""
super().__init__(hass)
@asyncio.coroutine @asyncio.coroutine
def get(self, request, entity_id): def get(self, request, entity_id):
"""Retrieve last 5 states of entity.""" """Retrieve last 5 states of entity."""
result = yield from self.hass.loop.run_in_executor( result = yield from request.app['hass'].loop.run_in_executor(
None, last_5_states, entity_id) None, last_5_states, entity_id)
return self.json(result) return self.json(result)
@ -216,9 +212,8 @@ class HistoryPeriodView(HomeAssistantView):
name = 'api:history:view-period' name = 'api:history:view-period'
extra_urls = ['/api/history/period/{datetime}'] extra_urls = ['/api/history/period/{datetime}']
def __init__(self, hass, filters): def __init__(self, filters):
"""Initilalize the history period view.""" """Initilalize the history period view."""
super().__init__(hass)
self.filters = filters self.filters = filters
@asyncio.coroutine @asyncio.coroutine
@ -240,7 +235,7 @@ class HistoryPeriodView(HomeAssistantView):
end_time = start_time + one_day end_time = start_time + one_day
entity_id = request.GET.get('filter_entity_id') entity_id = request.GET.get('filter_entity_id')
result = yield from self.hass.loop.run_in_executor( result = yield from request.app['hass'].loop.run_in_executor(
None, get_significant_states, start_time, end_time, entity_id, None, get_significant_states, start_time, end_time, entity_id,
self.filters) self.filters)

View file

@ -13,9 +13,9 @@ from functools import partial
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN, from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, CONF_PLATFORM, EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN, CONF_USERNAME, CONF_PASSWORD,
ATTR_ENTITY_ID) CONF_PLATFORM, CONF_HOSTS, CONF_NAME, ATTR_ENTITY_ID)
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.helpers import discovery from homeassistant.helpers import discovery
@ -23,13 +23,10 @@ from homeassistant.config import load_yaml_config_file
from homeassistant.util import Throttle from homeassistant.util import Throttle
DOMAIN = 'homematic' DOMAIN = 'homematic'
REQUIREMENTS = ["pyhomematic==0.1.16"] REQUIREMENTS = ["pyhomematic==0.1.18"]
HOMEMATIC = None
HOMEMATIC_LINK_DELAY = 0.5
MIN_TIME_BETWEEN_UPDATE_HUB = timedelta(seconds=300) MIN_TIME_BETWEEN_UPDATE_HUB = timedelta(seconds=300)
MIN_TIME_BETWEEN_UPDATE_VAR = timedelta(seconds=60) MIN_TIME_BETWEEN_UPDATE_VAR = timedelta(seconds=30)
DISCOVER_SWITCHES = 'homematic.switch' DISCOVER_SWITCHES = 'homematic.switch'
DISCOVER_LIGHTS = 'homematic.light' DISCOVER_LIGHTS = 'homematic.light'
@ -44,12 +41,15 @@ ATTR_CHANNEL = 'channel'
ATTR_NAME = 'name' ATTR_NAME = 'name'
ATTR_ADDRESS = 'address' ATTR_ADDRESS = 'address'
ATTR_VALUE = 'value' ATTR_VALUE = 'value'
ATTR_PROXY = 'proxy'
EVENT_KEYPRESS = 'homematic.keypress' EVENT_KEYPRESS = 'homematic.keypress'
EVENT_IMPULSE = 'homematic.impulse' EVENT_IMPULSE = 'homematic.impulse'
SERVICE_VIRTUALKEY = 'virtualkey' SERVICE_VIRTUALKEY = 'virtualkey'
SERVICE_SET_VALUE = 'set_value' SERVICE_RECONNECT = 'reconnect'
SERVICE_SET_VAR_VALUE = 'set_var_value'
SERVICE_SET_DEV_VALUE = 'set_dev_value'
HM_DEVICE_TYPES = { HM_DEVICE_TYPES = {
DISCOVER_SWITCHES: [ DISCOVER_SWITCHES: [
@ -109,44 +109,60 @@ CONF_RESOLVENAMES_OPTIONS = [
False False
] ]
DATA_HOMEMATIC = 'homematic'
DATA_DELAY = 'homematic_delay'
DATA_DEVINIT = 'homematic_devinit'
DATA_STORE = 'homematic_store'
CONF_LOCAL_IP = 'local_ip' CONF_LOCAL_IP = 'local_ip'
CONF_LOCAL_PORT = 'local_port' CONF_LOCAL_PORT = 'local_port'
CONF_REMOTE_IP = 'remote_ip' CONF_IP = 'ip'
CONF_REMOTE_PORT = 'remote_port' CONF_PORT = 'port'
CONF_RESOLVENAMES = 'resolvenames' CONF_RESOLVENAMES = 'resolvenames'
CONF_DELAY = 'delay'
CONF_VARIABLES = 'variables' CONF_VARIABLES = 'variables'
CONF_DEVICES = 'devices'
CONF_DELAY = 'delay'
CONF_PRIMARY = 'primary'
DEFAULT_LOCAL_IP = "0.0.0.0" DEFAULT_LOCAL_IP = "0.0.0.0"
DEFAULT_LOCAL_PORT = 0 DEFAULT_LOCAL_PORT = 0
DEFAULT_RESOLVENAMES = False DEFAULT_RESOLVENAMES = False
DEFAULT_REMOTE_PORT = 2001 DEFAULT_PORT = 2001
DEFAULT_USERNAME = "Admin" DEFAULT_USERNAME = "Admin"
DEFAULT_PASSWORD = "" DEFAULT_PASSWORD = ""
DEFAULT_VARIABLES = False DEFAULT_VARIABLES = False
DEFAULT_DEVICES = True
DEFAULT_DELAY = 0.5 DEFAULT_DELAY = 0.5
DEFAULT_PRIMARY = False
DEVICE_SCHEMA = vol.Schema({ DEVICE_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): "homematic", vol.Required(CONF_PLATFORM): "homematic",
vol.Required(ATTR_NAME): cv.string, vol.Required(ATTR_NAME): cv.string,
vol.Required(ATTR_ADDRESS): cv.string, vol.Required(ATTR_ADDRESS): cv.string,
vol.Required(ATTR_PROXY): cv.string,
vol.Optional(ATTR_CHANNEL, default=1): vol.Coerce(int), vol.Optional(ATTR_CHANNEL, default=1): vol.Coerce(int),
vol.Optional(ATTR_PARAM): cv.string, vol.Optional(ATTR_PARAM): cv.string,
}) })
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Required(CONF_REMOTE_IP): cv.string, vol.Required(CONF_HOSTS): {cv.match_all: {
vol.Optional(CONF_LOCAL_IP, default=DEFAULT_LOCAL_IP): cv.string, vol.Required(CONF_IP): cv.string,
vol.Optional(CONF_LOCAL_PORT, default=DEFAULT_LOCAL_PORT): cv.port, vol.Optional(CONF_PORT, default=DEFAULT_PORT):
vol.Optional(CONF_REMOTE_PORT, default=DEFAULT_REMOTE_PORT): cv.port, cv.port,
vol.Optional(CONF_RESOLVENAMES, default=DEFAULT_RESOLVENAMES):
vol.In(CONF_RESOLVENAMES_OPTIONS),
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
vol.Optional(CONF_VARIABLES, default=DEFAULT_VARIABLES):
cv.boolean,
vol.Optional(CONF_RESOLVENAMES, default=DEFAULT_RESOLVENAMES):
vol.In(CONF_RESOLVENAMES_OPTIONS),
vol.Optional(CONF_DEVICES, default=DEFAULT_DEVICES): cv.boolean,
vol.Optional(CONF_PRIMARY, default=DEFAULT_PRIMARY): cv.boolean,
}},
vol.Optional(CONF_LOCAL_IP, default=DEFAULT_LOCAL_IP): cv.string,
vol.Optional(CONF_LOCAL_PORT, default=DEFAULT_LOCAL_PORT): cv.port,
vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): vol.Coerce(float), vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): vol.Coerce(float),
vol.Optional(CONF_VARIABLES, default=DEFAULT_VARIABLES): cv.boolean,
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -154,106 +170,156 @@ SCHEMA_SERVICE_VIRTUALKEY = vol.Schema({
vol.Required(ATTR_ADDRESS): cv.string, vol.Required(ATTR_ADDRESS): cv.string,
vol.Required(ATTR_CHANNEL): vol.Coerce(int), vol.Required(ATTR_CHANNEL): vol.Coerce(int),
vol.Required(ATTR_PARAM): cv.string, vol.Required(ATTR_PARAM): cv.string,
vol.Optional(ATTR_PROXY): cv.string,
}) })
SCHEMA_SERVICE_SET_VALUE = vol.Schema({ SCHEMA_SERVICE_SET_VAR_VALUE = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_VALUE): cv.match_all, vol.Required(ATTR_VALUE): cv.match_all,
}) })
SCHEMA_SERVICE_SET_DEV_VALUE = vol.Schema({
vol.Required(ATTR_ADDRESS): cv.string,
vol.Required(ATTR_CHANNEL): vol.Coerce(int),
vol.Required(ATTR_PARAM): cv.string,
vol.Required(ATTR_VALUE): cv.match_all,
vol.Optional(ATTR_PROXY): cv.string,
})
def virtualkey(hass, address, channel, param): SCHEMA_SERVICE_RECONNECT = vol.Schema({})
def virtualkey(hass, address, channel, param, proxy=None):
"""Send virtual keypress to homematic controlller.""" """Send virtual keypress to homematic controlller."""
data = { data = {
ATTR_ADDRESS: address, ATTR_ADDRESS: address,
ATTR_CHANNEL: channel, ATTR_CHANNEL: channel,
ATTR_PARAM: param, ATTR_PARAM: param,
ATTR_PROXY: proxy,
} }
hass.services.call(DOMAIN, SERVICE_VIRTUALKEY, data) hass.services.call(DOMAIN, SERVICE_VIRTUALKEY, data)
def set_value(hass, entity_id, value): def set_var_value(hass, entity_id, value):
"""Change value of homematic system variable.""" """Change value of homematic system variable."""
data = { data = {
ATTR_ENTITY_ID: entity_id, ATTR_ENTITY_ID: entity_id,
ATTR_VALUE: value, ATTR_VALUE: value,
} }
hass.services.call(DOMAIN, SERVICE_SET_VALUE, data) hass.services.call(DOMAIN, SERVICE_SET_VAR_VALUE, data)
def set_dev_value(hass, address, channel, param, value, proxy=None):
"""Send virtual keypress to homematic controlller."""
data = {
ATTR_ADDRESS: address,
ATTR_CHANNEL: channel,
ATTR_PARAM: param,
ATTR_VALUE: value,
ATTR_PROXY: proxy,
}
hass.services.call(DOMAIN, SERVICE_SET_DEV_VALUE, data)
def reconnect(hass):
"""Reconnect to CCU/Homegear."""
hass.services.call(DOMAIN, SERVICE_RECONNECT, {})
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup(hass, config): def setup(hass, config):
"""Setup the Homematic component.""" """Setup the Homematic component."""
global HOMEMATIC, HOMEMATIC_LINK_DELAY
from pyhomematic import HMConnection from pyhomematic import HMConnection
component = EntityComponent(_LOGGER, DOMAIN, hass) component = EntityComponent(_LOGGER, DOMAIN, hass)
local_ip = config[DOMAIN].get(CONF_LOCAL_IP) hass.data[DATA_DELAY] = config[DOMAIN].get(CONF_DELAY)
local_port = config[DOMAIN].get(CONF_LOCAL_PORT) hass.data[DATA_DEVINIT] = {}
remote_ip = config[DOMAIN].get(CONF_REMOTE_IP) hass.data[DATA_STORE] = []
remote_port = config[DOMAIN].get(CONF_REMOTE_PORT)
resolvenames = config[DOMAIN].get(CONF_RESOLVENAMES)
username = config[DOMAIN].get(CONF_USERNAME)
password = config[DOMAIN].get(CONF_PASSWORD)
HOMEMATIC_LINK_DELAY = config[DOMAIN].get(CONF_DELAY)
use_variables = config[DOMAIN].get(CONF_VARIABLES)
if remote_ip is None or local_ip is None: # create hosts list for pyhomematic
_LOGGER.error("Missing remote CCU/Homegear or local address") remotes = {}
return False hosts = {}
for rname, rconfig in config[DOMAIN][CONF_HOSTS].items():
server = rconfig.get(CONF_IP)
remotes[rname] = {}
remotes[rname][CONF_IP] = server
remotes[rname][CONF_PORT] = rconfig.get(CONF_PORT)
remotes[rname][CONF_RESOLVENAMES] = rconfig.get(CONF_RESOLVENAMES)
remotes[rname][CONF_USERNAME] = rconfig.get(CONF_USERNAME)
remotes[rname][CONF_PASSWORD] = rconfig.get(CONF_PASSWORD)
if server not in hosts or rconfig.get(CONF_PRIMARY):
hosts[server] = {
CONF_VARIABLES: rconfig.get(CONF_VARIABLES),
CONF_NAME: rname,
}
hass.data[DATA_DEVINIT][rname] = rconfig.get(CONF_DEVICES)
# Create server thread # Create server thread
bound_system_callback = partial(_system_callback_handler, hass, config) bound_system_callback = partial(_system_callback_handler, hass, config)
HOMEMATIC = HMConnection(local=local_ip, hass.data[DATA_HOMEMATIC] = HMConnection(
localport=local_port, local=config[DOMAIN].get(CONF_LOCAL_IP),
remote=remote_ip, localport=config[DOMAIN].get(CONF_LOCAL_PORT),
remoteport=remote_port, remotes=remotes,
systemcallback=bound_system_callback, systemcallback=bound_system_callback,
resolvenames=resolvenames, interface_id="homeassistant"
rpcusername=username, )
rpcpassword=password,
interface_id="homeassistant")
# Start server thread, connect to peer, initialize to receive events # Start server thread, connect to peer, initialize to receive events
HOMEMATIC.start() hass.data[DATA_HOMEMATIC].start()
# Stops server when Homeassistant is shutting down # Stops server when Homeassistant is shutting down
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, HOMEMATIC.stop) hass.bus.listen_once(
EVENT_HOMEASSISTANT_STOP, hass.data[DATA_HOMEMATIC].stop)
hass.config.components.append(DOMAIN) hass.config.components.append(DOMAIN)
# init homematic hubs
hub_entities = []
for _, hub_data in hosts.items():
hub_entities.append(HMHub(hass, component, hub_data[CONF_NAME],
hub_data[CONF_VARIABLES]))
component.add_entities(hub_entities)
# regeister homematic services # regeister homematic services
descriptions = load_yaml_config_file( descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml')) os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_VIRTUALKEY, def _hm_service_virtualkey(service):
_hm_service_virtualkey, """Service handle virtualkey services."""
address = service.data.get(ATTR_ADDRESS)
channel = service.data.get(ATTR_CHANNEL)
param = service.data.get(ATTR_PARAM)
# device not found
hmdevice = _device_from_servicecall(hass, service)
if hmdevice is None:
_LOGGER.error("%s not found for service virtualkey!", address)
return
# if param exists for this device
if param not in hmdevice.ACTIONNODE:
_LOGGER.error("%s not datapoint in hm device %s", param, address)
return
# channel exists?
if channel not in hmdevice.ACTIONNODE[param]:
_LOGGER.error("%i is not a channel in hm device %s",
channel, address)
return
# call key
hmdevice.actionNodeData(param, True, channel)
hass.services.register(
DOMAIN, SERVICE_VIRTUALKEY, _hm_service_virtualkey,
descriptions[DOMAIN][SERVICE_VIRTUALKEY], descriptions[DOMAIN][SERVICE_VIRTUALKEY],
schema=SCHEMA_SERVICE_VIRTUALKEY) schema=SCHEMA_SERVICE_VIRTUALKEY)
entities = []
##
# init HM variable
variables = HOMEMATIC.getAllSystemVariables() if use_variables else {}
hm_var_store = {}
if variables is not None:
for key, value in variables.items():
varia = HMVariable(key, value)
hm_var_store.update({key: varia})
entities.append(varia)
# add homematic entites
entities.append(HMHub(hm_var_store, use_variables))
component.add_entities(entities)
##
# register set_value service if exists variables
if not variables:
return True
def _service_handle_value(service): def _service_handle_value(service):
"""Set value on homematic variable object.""" """Set value on homematic variable object."""
variable_list = component.extract_from_service(service) variable_list = component.extract_from_service(service)
@ -261,12 +327,43 @@ def setup(hass, config):
value = service.data[ATTR_VALUE] value = service.data[ATTR_VALUE]
for hm_variable in variable_list: for hm_variable in variable_list:
if isinstance(hm_variable, HMVariable):
hm_variable.hm_set(value) hm_variable.hm_set(value)
hass.services.register(DOMAIN, SERVICE_SET_VALUE, hass.services.register(
_service_handle_value, DOMAIN, SERVICE_SET_VAR_VALUE, _service_handle_value,
descriptions[DOMAIN][SERVICE_SET_VALUE], descriptions[DOMAIN][SERVICE_SET_VAR_VALUE],
schema=SCHEMA_SERVICE_SET_VALUE) schema=SCHEMA_SERVICE_SET_VAR_VALUE)
def _service_handle_reconnect(service):
"""Reconnect to all homematic hubs."""
hass.data[DATA_HOMEMATIC].reconnect()
hass.services.register(
DOMAIN, SERVICE_RECONNECT, _service_handle_reconnect,
descriptions[DOMAIN][SERVICE_RECONNECT],
schema=SCHEMA_SERVICE_RECONNECT)
def _service_handle_device(service):
"""Service handle set_dev_value services."""
address = service.data.get(ATTR_ADDRESS)
channel = service.data.get(ATTR_CHANNEL)
param = service.data.get(ATTR_PARAM)
value = service.data.get(ATTR_VALUE)
# device not found
hmdevice = _device_from_servicecall(hass, service)
if hmdevice is None:
_LOGGER.error("%s not found!", address)
return
# call key
hmdevice.setValue(param, value, channel)
hass.services.register(
DOMAIN, SERVICE_SET_DEV_VALUE, _service_handle_device,
descriptions[DOMAIN][SERVICE_SET_DEV_VALUE],
schema=SCHEMA_SERVICE_SET_DEV_VALUE)
return True return True
@ -274,22 +371,36 @@ def setup(hass, config):
def _system_callback_handler(hass, config, src, *args): def _system_callback_handler(hass, config, src, *args):
"""Callback handler.""" """Callback handler."""
if src == 'newDevices': if src == 'newDevices':
_LOGGER.debug("newDevices with: %s", str(args)) _LOGGER.debug("newDevices with: %s", args)
# pylint: disable=unused-variable # pylint: disable=unused-variable
(interface_id, dev_descriptions) = args (interface_id, dev_descriptions) = args
key_dict = {} proxy = interface_id.split('-')[-1]
# device support active?
if not hass.data[DATA_DEVINIT][proxy]:
return
##
# Get list of all keys of the devices (ignoring channels) # Get list of all keys of the devices (ignoring channels)
key_dict = {}
for dev in dev_descriptions: for dev in dev_descriptions:
key_dict[dev['ADDRESS'].split(':')[0]] = True key_dict[dev['ADDRESS'].split(':')[0]] = True
##
# remove device they allready init by HA
tmp_devs = key_dict.copy()
for dev in tmp_devs:
if dev in hass.data[DATA_STORE]:
del key_dict[dev]
else:
hass.data[DATA_STORE].append(dev)
# Register EVENTS # Register EVENTS
# Search all device with a EVENTNODE that include data # Search all device with a EVENTNODE that include data
bound_event_callback = partial(_hm_event_handler, hass) bound_event_callback = partial(_hm_event_handler, hass, proxy)
for dev in key_dict: for dev in key_dict:
if dev not in HOMEMATIC.devices: hmdevice = hass.data[DATA_HOMEMATIC].devices[proxy].get(dev)
continue
hmdevice = HOMEMATIC.devices.get(dev)
# have events? # have events?
if len(hmdevice.EVENTNODE) > 0: if len(hmdevice.EVENTNODE) > 0:
_LOGGER.debug("Register Events from %s", dev) _LOGGER.debug("Register Events from %s", dev)
@ -307,7 +418,8 @@ def _system_callback_handler(hass, config, src, *args):
('sensor', DISCOVER_SENSORS), ('sensor', DISCOVER_SENSORS),
('climate', DISCOVER_CLIMATE)): ('climate', DISCOVER_CLIMATE)):
# Get all devices of a specific type # Get all devices of a specific type
found_devices = _get_devices(discovery_type, key_dict) found_devices = _get_devices(
hass, discovery_type, key_dict, proxy)
# When devices of this type are found # When devices of this type are found
# they are setup in HA and an event is fired # they are setup in HA and an event is fired
@ -318,12 +430,12 @@ def _system_callback_handler(hass, config, src, *args):
}, config) }, config)
def _get_devices(device_type, keys): def _get_devices(hass, device_type, keys, proxy):
"""Get the Homematic devices.""" """Get the Homematic devices."""
device_arr = [] device_arr = []
for key in keys: for key in keys:
device = HOMEMATIC.devices[key] device = hass.data[DATA_HOMEMATIC].devices[proxy][key]
class_name = device.__class__.__name__ class_name = device.__class__.__name__
metadata = {} metadata = {}
@ -357,6 +469,7 @@ def _get_devices(device_type, keys):
device_dict = { device_dict = {
CONF_PLATFORM: "homematic", CONF_PLATFORM: "homematic",
ATTR_ADDRESS: key, ATTR_ADDRESS: key,
ATTR_PROXY: proxy,
ATTR_NAME: name, ATTR_NAME: name,
ATTR_CHANNEL: channel ATTR_CHANNEL: channel
} }
@ -395,28 +508,29 @@ def _create_ha_name(name, channel, param, count):
return "{} {} {}".format(name, channel, param) return "{} {} {}".format(name, channel, param)
def setup_hmdevice_discovery_helper(hmdevicetype, discovery_info, def setup_hmdevice_discovery_helper(hass, hmdevicetype, discovery_info,
add_callback_devices): add_callback_devices):
"""Helper to setup Homematic devices with discovery info.""" """Helper to setup Homematic devices with discovery info."""
devices = []
for config in discovery_info[ATTR_DISCOVER_DEVICES]: for config in discovery_info[ATTR_DISCOVER_DEVICES]:
_LOGGER.debug("Add device %s from config: %s", _LOGGER.debug("Add device %s from config: %s",
str(hmdevicetype), str(config)) str(hmdevicetype), str(config))
# create object and add to HA # create object and add to HA
new_device = hmdevicetype(config) new_device = hmdevicetype(hass, config)
new_device.link_homematic() new_device.link_homematic()
devices.append(new_device)
add_callback_devices([new_device]) add_callback_devices(devices)
return True return True
def _hm_event_handler(hass, device, caller, attribute, value): def _hm_event_handler(hass, proxy, device, caller, attribute, value):
"""Handle all pyhomematic device events.""" """Handle all pyhomematic device events."""
try: try:
channel = int(device.split(":")[1]) channel = int(device.split(":")[1])
address = device.split(":")[0] address = device.split(":")[0]
hmdevice = HOMEMATIC.devices.get(address) hmdevice = hass.data[DATA_HOMEMATIC].devices[proxy].get(address)
except (TypeError, ValueError): except (TypeError, ValueError):
_LOGGER.error("Event handling channel convert error!") _LOGGER.error("Event handling channel convert error!")
return return
@ -448,46 +562,40 @@ def _hm_event_handler(hass, device, caller, attribute, value):
_LOGGER.warning("Event is unknown and not forwarded to HA") _LOGGER.warning("Event is unknown and not forwarded to HA")
def _hm_service_virtualkey(call): def _device_from_servicecall(hass, service):
"""Callback for handle virtualkey services.""" """Extract homematic device from service call."""
address = call.data.get(ATTR_ADDRESS) address = service.data.get(ATTR_ADDRESS)
channel = call.data.get(ATTR_CHANNEL) proxy = service.data.get(ATTR_PROXY)
param = call.data.get(ATTR_PARAM)
if address not in HOMEMATIC.devices: if proxy:
_LOGGER.error("%s not found for service virtualkey!", address) return hass.data[DATA_HOMEMATIC].devices[proxy].get(address)
return
hmdevice = HOMEMATIC.devices.get(address)
# if param exists for this device for _, devices in hass.data[DATA_HOMEMATIC].devices.items():
if hmdevice is None or param not in hmdevice.ACTIONNODE: if address in devices:
_LOGGER.error("%s not datapoint in hm device %s", param, address) return devices[address]
return
# channel exists?
if channel in hmdevice.ACTIONNODE[param]:
_LOGGER.error("%i is not a channel in hm device %s", channel, address)
return
# call key
hmdevice.actionNodeData(param, 1, channel)
class HMHub(Entity): class HMHub(Entity):
"""The Homematic hub. I.e. CCU2/HomeGear.""" """The Homematic hub. I.e. CCU2/HomeGear."""
def __init__(self, variables_store, use_variables=False): def __init__(self, hass, component, name, use_variables):
"""Initialize Homematic hub.""" """Initialize Homematic hub."""
self.hass = hass
self._homematic = hass.data[DATA_HOMEMATIC]
self._component = component
self._name = name
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
self._store = variables_store self._store = {}
self._use_variables = use_variables self._use_variables = use_variables
self.update() # load data
self._update_hub_state()
self._init_variables()
@property @property
def name(self): def name(self):
"""Return the name of the device.""" """Return the name of the device."""
return 'Homematic' return self._name
@property @property
def state(self): def state(self):
@ -504,11 +612,6 @@ class HMHub(Entity):
"""Return the icon to use in the frontend, if any.""" """Return the icon to use in the frontend, if any."""
return "mdi:gradient" return "mdi:gradient"
@property
def available(self):
"""Return true if device is available."""
return True if HOMEMATIC is not None else False
def update(self): def update(self):
"""Update Hub data and all HM variables.""" """Update Hub data and all HM variables."""
self._update_hub_state() self._update_hub_state()
@ -517,30 +620,48 @@ class HMHub(Entity):
@Throttle(MIN_TIME_BETWEEN_UPDATE_HUB) @Throttle(MIN_TIME_BETWEEN_UPDATE_HUB)
def _update_hub_state(self): def _update_hub_state(self):
"""Retrieve latest state.""" """Retrieve latest state."""
if HOMEMATIC is None: state = self._homematic.getServiceMessages(self._name)
return
state = HOMEMATIC.getServiceMessages()
self._state = STATE_UNKNOWN if state is None else len(state) self._state = STATE_UNKNOWN if state is None else len(state)
@Throttle(MIN_TIME_BETWEEN_UPDATE_VAR) @Throttle(MIN_TIME_BETWEEN_UPDATE_VAR)
def _update_variables_state(self): def _update_variables_state(self):
"""Retrive all variable data and update hmvariable states.""" """Retrive all variable data and update hmvariable states."""
if HOMEMATIC is None or not self._use_variables: if not self._use_variables:
return return
variables = HOMEMATIC.getAllSystemVariables()
if variables is not None: variables = self._homematic.getAllSystemVariables(self._name)
if variables is None:
return
for key, value in variables.items(): for key, value in variables.items():
if key in self._store: if key in self._store:
self._store.get(key).hm_update(value) self._store.get(key).hm_update(value)
def _init_variables(self):
"""Load variables from hub."""
if not self._use_variables:
return
variables = self._homematic.getAllSystemVariables(self._name)
if variables is None:
return
entities = []
for key, value in variables.items():
entities.append(HMVariable(self.hass, self._name, key, value))
self._component.add_entities(entities)
class HMVariable(Entity): class HMVariable(Entity):
"""The Homematic system variable.""" """The Homematic system variable."""
def __init__(self, name, state): def __init__(self, hass, hub_name, name, state):
"""Initialize Homematic hub.""" """Initialize Homematic hub."""
self.hass = hass
self._homematic = hass.data[DATA_HOMEMATIC]
self._state = state self._state = state
self._name = name self._name = name
self._hub_name = hub_name
@property @property
def name(self): def name(self):
@ -562,31 +683,41 @@ class HMVariable(Entity):
"""Return false. Homematic Hub object update variable.""" """Return false. Homematic Hub object update variable."""
return False return False
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
attr = {
'hub': self._hub_name,
}
return attr
def hm_update(self, value): def hm_update(self, value):
"""Update variable over Hub object.""" """Update variable over Hub object."""
if value != self._state: if value != self._state:
self._state = value self._state = value
self.update_ha_state() self.schedule_update_ha_state()
def hm_set(self, value): def hm_set(self, value):
"""Set variable on homematic controller.""" """Set variable on homematic controller."""
if HOMEMATIC is not None:
if isinstance(self._state, bool): if isinstance(self._state, bool):
value = cv.boolean(value) value = cv.boolean(value)
else: else:
value = float(value) value = float(value)
HOMEMATIC.setSystemVariable(self._name, value) self._homematic.setSystemVariable(self._hub_name, self._name, value)
self._state = value self._state = value
self.update_ha_state() self.schedule_update_ha_state()
class HMDevice(Entity): class HMDevice(Entity):
"""The Homematic device base object.""" """The Homematic device base object."""
def __init__(self, config): def __init__(self, hass, config):
"""Initialize a generic Homematic device.""" """Initialize a generic Homematic device."""
self.hass = hass
self._homematic = hass.data[DATA_HOMEMATIC]
self._name = config.get(ATTR_NAME) self._name = config.get(ATTR_NAME)
self._address = config.get(ATTR_ADDRESS) self._address = config.get(ATTR_ADDRESS)
self._proxy = config.get(ATTR_PROXY)
self._channel = config.get(ATTR_CHANNEL) self._channel = config.get(ATTR_CHANNEL)
self._state = config.get(ATTR_PARAM) self._state = config.get(ATTR_PARAM)
self._data = {} self._data = {}
@ -636,6 +767,7 @@ class HMDevice(Entity):
# static attributes # static attributes
attr['ID'] = self._hmdevice.ADDRESS attr['ID'] = self._hmdevice.ADDRESS
attr['proxy'] = self._proxy
return attr return attr
@ -645,14 +777,8 @@ class HMDevice(Entity):
if self._connected: if self._connected:
return True return True
# pyhomematic is loaded
if HOMEMATIC is None:
return False
# Does a HMDevice from pyhomematic exist?
if self._address in HOMEMATIC.devices:
# Init # Init
self._hmdevice = HOMEMATIC.devices[self._address] self._hmdevice = self._homematic.devices[self._proxy][self._address]
self._connected = True self._connected = True
# Check if Homematic class is okay for HA class # Check if Homematic class is okay for HA class
@ -660,10 +786,10 @@ class HMDevice(Entity):
try: try:
# Init datapoints of this object # Init datapoints of this object
self._init_data() self._init_data()
if HOMEMATIC_LINK_DELAY: if self.hass.data[DATA_DELAY]:
# We delay / pause loading of data to avoid overloading # We delay / pause loading of data to avoid overloading
# of CCU / Homegear when doing auto detection # of CCU / Homegear when doing auto detection
time.sleep(HOMEMATIC_LINK_DELAY) time.sleep(self.hass.data[DATA_DELAY])
self._load_data_from_hm() self._load_data_from_hm()
_LOGGER.debug("%s datastruct: %s", self._name, str(self._data)) _LOGGER.debug("%s datastruct: %s", self._name, str(self._data))
@ -676,8 +802,6 @@ class HMDevice(Entity):
self._connected = False self._connected = False
_LOGGER.error("Exception while linking %s: %s", _LOGGER.error("Exception while linking %s: %s",
self._address, str(err)) self._address, str(err))
else:
_LOGGER.debug("%s not found in HOMEMATIC.devices", self._address)
def _hm_event_callback(self, device, caller, attribute, value): def _hm_event_callback(self, device, caller, attribute, value):
"""Handle all pyhomematic device events.""" """Handle all pyhomematic device events."""

View file

@ -5,35 +5,38 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/http/ https://home-assistant.io/components/http/
""" """
import asyncio import asyncio
import hmac
import json import json
import logging import logging
import mimetypes
import os
from pathlib import Path
import re
import ssl import ssl
from ipaddress import ip_address, ip_network from ipaddress import ip_network
from pathlib import Path
import os
import voluptuous as vol import voluptuous as vol
from aiohttp import web, hdrs from aiohttp import web
from aiohttp.file_sender import FileSender from aiohttp.web_exceptions import HTTPUnauthorized, HTTPMovedPermanently
from aiohttp.web_exceptions import (
HTTPUnauthorized, HTTPMovedPermanently, HTTPNotModified)
from aiohttp.web_urldispatcher import StaticRoute
from homeassistant.core import is_callback
import homeassistant.remote as rem
from homeassistant import util
from homeassistant.const import (
SERVER_PORT, HTTP_HEADER_HA_AUTH, # HTTP_HEADER_CACHE_CONTROL,
CONTENT_TYPE_JSON, ALLOWED_CORS_HEADERS, EVENT_HOMEASSISTANT_STOP,
EVENT_HOMEASSISTANT_START, HTTP_HEADER_X_FORWARDED_FOR)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.remote as rem
from homeassistant.util import get_local_ip
from homeassistant.components import persistent_notification from homeassistant.components import persistent_notification
from homeassistant.const import (
SERVER_PORT, CONTENT_TYPE_JSON, ALLOWED_CORS_HEADERS,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
from homeassistant.core import is_callback
from homeassistant.util.logging import HideSensitiveDataFilter
from .auth import auth_middleware
from .ban import ban_middleware, process_wrong_login
from .const import (
KEY_USE_X_FORWARDED_FOR, KEY_TRUSTED_NETWORKS,
KEY_BANS_ENABLED, KEY_LOGIN_THRESHOLD,
KEY_DEVELOPMENT, KEY_AUTHENTICATED)
from .static import GZIP_FILE_SENDER, staticresource_middleware
from .util import get_real_ip
DOMAIN = 'http' DOMAIN = 'http'
REQUIREMENTS = ('aiohttp_cors==0.4.0',) REQUIREMENTS = ('aiohttp_cors==0.5.0',)
CONF_API_PASSWORD = 'api_password' CONF_API_PASSWORD = 'api_password'
CONF_SERVER_HOST = 'server_host' CONF_SERVER_HOST = 'server_host'
@ -44,8 +47,9 @@ CONF_SSL_KEY = 'ssl_key'
CONF_CORS_ORIGINS = 'cors_allowed_origins' CONF_CORS_ORIGINS = 'cors_allowed_origins'
CONF_USE_X_FORWARDED_FOR = 'use_x_forwarded_for' CONF_USE_X_FORWARDED_FOR = 'use_x_forwarded_for'
CONF_TRUSTED_NETWORKS = 'trusted_networks' CONF_TRUSTED_NETWORKS = 'trusted_networks'
CONF_LOGIN_ATTEMPTS_THRESHOLD = 'login_attempts_threshold'
CONF_IP_BAN_ENABLED = 'ip_ban_enabled'
DATA_API_PASSWORD = 'api_password'
NOTIFICATION_ID_LOGIN = 'http-login' NOTIFICATION_ID_LOGIN = 'http-login'
# TLS configuation follows the best-practice guidelines specified here: # TLS configuation follows the best-practice guidelines specified here:
@ -69,68 +73,58 @@ CIPHERS = "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" \
"AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:" \ "AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:" \
"AES256-SHA:DES-CBC3-SHA:!DSS" "AES256-SHA:DES-CBC3-SHA:!DSS"
_FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema({ DEFAULT_SERVER_HOST = '0.0.0.0'
DOMAIN: vol.Schema({ DEFAULT_DEVELOPMENT = '0'
vol.Optional(CONF_API_PASSWORD): cv.string, DEFAULT_LOGIN_ATTEMPT_THRESHOLD = -1
vol.Optional(CONF_SERVER_HOST): cv.string,
HTTP_SCHEMA = vol.Schema({
vol.Optional(CONF_API_PASSWORD, default=None): cv.string,
vol.Optional(CONF_SERVER_HOST, default=DEFAULT_SERVER_HOST): cv.string,
vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT):
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)), vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
vol.Optional(CONF_DEVELOPMENT): cv.string, vol.Optional(CONF_DEVELOPMENT, default=DEFAULT_DEVELOPMENT): cv.string,
vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile, vol.Optional(CONF_SSL_CERTIFICATE, default=None): cv.isfile,
vol.Optional(CONF_SSL_KEY): cv.isfile, vol.Optional(CONF_SSL_KEY, default=None): cv.isfile,
vol.Optional(CONF_CORS_ORIGINS): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_CORS_ORIGINS, default=[]): vol.All(cv.ensure_list,
[cv.string]),
vol.Optional(CONF_USE_X_FORWARDED_FOR, default=False): cv.boolean, vol.Optional(CONF_USE_X_FORWARDED_FOR, default=False): cv.boolean,
vol.Optional(CONF_TRUSTED_NETWORKS): vol.Optional(CONF_TRUSTED_NETWORKS, default=[]):
vol.All(cv.ensure_list, [ip_network]) vol.All(cv.ensure_list, [ip_network]),
}), vol.Optional(CONF_LOGIN_ATTEMPTS_THRESHOLD,
default=DEFAULT_LOGIN_ATTEMPT_THRESHOLD): cv.positive_int,
vol.Optional(CONF_IP_BAN_ENABLED, default=True): cv.boolean
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: HTTP_SCHEMA,
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
# TEMP TO GET TESTS TO RUN @asyncio.coroutine
def request_class(): def async_setup(hass, config):
"""."""
raise Exception('not implemented')
class HideSensitiveFilter(logging.Filter):
"""Filter API password calls."""
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.http.api_password is None:
return True
record.msg = record.msg.replace(self.hass.http.api_password, '*******')
return True
def setup(hass, config):
"""Set up the HTTP API and debug interface.""" """Set up the HTTP API and debug interface."""
logging.getLogger('aiohttp.access').addFilter(HideSensitiveFilter(hass)) conf = config.get(DOMAIN)
conf = config.get(DOMAIN, {}) if conf is None:
conf = HTTP_SCHEMA({})
api_password = util.convert(conf.get(CONF_API_PASSWORD), str) api_password = conf[CONF_API_PASSWORD]
server_host = conf.get(CONF_SERVER_HOST, '0.0.0.0') server_host = conf[CONF_SERVER_HOST]
server_port = conf.get(CONF_SERVER_PORT, SERVER_PORT) server_port = conf[CONF_SERVER_PORT]
development = str(conf.get(CONF_DEVELOPMENT, '')) == '1' development = conf[CONF_DEVELOPMENT] == '1'
ssl_certificate = conf.get(CONF_SSL_CERTIFICATE) ssl_certificate = conf[CONF_SSL_CERTIFICATE]
ssl_key = conf.get(CONF_SSL_KEY) ssl_key = conf[CONF_SSL_KEY]
cors_origins = conf.get(CONF_CORS_ORIGINS, []) cors_origins = conf[CONF_CORS_ORIGINS]
use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False) use_x_forwarded_for = conf[CONF_USE_X_FORWARDED_FOR]
trusted_networks = [ trusted_networks = conf[CONF_TRUSTED_NETWORKS]
ip_network(trusted_network) is_ban_enabled = conf[CONF_IP_BAN_ENABLED]
for trusted_network in conf.get(CONF_TRUSTED_NETWORKS, [])] login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD]
if api_password is not None:
logging.getLogger('aiohttp.access').addFilter(
HideSensitiveDataFilter(api_password))
server = HomeAssistantWSGI( server = HomeAssistantWSGI(
hass, hass,
@ -142,7 +136,9 @@ def setup(hass, config):
ssl_key=ssl_key, ssl_key=ssl_key,
cors_origins=cors_origins, cors_origins=cors_origins,
use_x_forwarded_for=use_x_forwarded_for, use_x_forwarded_for=use_x_forwarded_for,
trusted_networks=trusted_networks trusted_networks=trusted_networks,
login_threshold=login_threshold,
is_ban_enabled=is_ban_enabled
) )
@asyncio.coroutine @asyncio.coroutine
@ -156,108 +152,40 @@ def setup(hass, config):
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_server) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_server)
yield from server.start() yield from server.start()
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_server) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_server)
hass.http = server hass.http = 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 get_local_ip(),
api_password, server_port, api_password, server_port,
ssl_certificate is not None) ssl_certificate is not None)
return True return True
class GzipFileSender(FileSender):
"""FileSender class capable of sending gzip version if available."""
# pylint: disable=invalid-name
development = False
@asyncio.coroutine
def send(self, request, filepath):
"""Send filepath to client using request."""
gzip = False
if 'gzip' in request.headers[hdrs.ACCEPT_ENCODING]:
gzip_path = filepath.with_name(filepath.name + '.gz')
if gzip_path.is_file():
filepath = gzip_path
gzip = True
st = filepath.stat()
modsince = request.if_modified_since
if modsince is not None and st.st_mtime <= modsince.timestamp():
raise HTTPNotModified()
ct, encoding = mimetypes.guess_type(str(filepath))
if not ct:
ct = 'application/octet-stream'
resp = self._response_factory()
resp.content_type = ct
if encoding:
resp.headers[hdrs.CONTENT_ENCODING] = encoding
if gzip:
resp.headers[hdrs.VARY] = hdrs.ACCEPT_ENCODING
resp.last_modified = st.st_mtime
# CACHE HACK
if not self.development:
cache_time = 31 * 86400 # = 1 month
resp.headers[hdrs.CACHE_CONTROL] = "public, max-age={}".format(
cache_time)
file_size = st.st_size
resp.content_length = file_size
resp.set_tcp_cork(True)
try:
with filepath.open('rb') as f:
yield from self._sendfile(request, resp, f, file_size)
finally:
resp.set_tcp_nodelay(True)
return resp
_GZIP_FILE_SENDER = GzipFileSender()
class HAStaticRoute(StaticRoute):
"""StaticRoute with support for fingerprinting."""
def __init__(self, prefix, path):
"""Initialize a static route with gzip and cache busting support."""
super().__init__(None, prefix, path)
self._file_sender = _GZIP_FILE_SENDER
def match(self, path):
"""Match path to filename."""
if not path.startswith(self._prefix):
return None
# Extra sauce to remove fingerprinted resource names
filename = path[self._prefix_len:]
fingerprinted = _FINGERPRINT.match(filename)
if fingerprinted:
filename = '{}.{}'.format(*fingerprinted.groups())
return {'filename': filename}
class HomeAssistantWSGI(object): class HomeAssistantWSGI(object):
"""WSGI server for Home Assistant.""" """WSGI server for Home Assistant."""
def __init__(self, hass, development, api_password, ssl_certificate, def __init__(self, hass, development, api_password, ssl_certificate,
ssl_key, server_host, server_port, cors_origins, ssl_key, server_host, server_port, cors_origins,
use_x_forwarded_for, trusted_networks): use_x_forwarded_for, trusted_networks,
login_threshold, is_ban_enabled):
"""Initialize the WSGI Home Assistant server.""" """Initialize the WSGI Home Assistant server."""
import aiohttp_cors import aiohttp_cors
self.app = web.Application(loop=hass.loop) middlewares = [auth_middleware, staticresource_middleware]
if is_ban_enabled:
middlewares.insert(0, ban_middleware)
self.app = web.Application(middlewares=middlewares, loop=hass.loop)
self.app['hass'] = hass
self.app[KEY_USE_X_FORWARDED_FOR] = use_x_forwarded_for
self.app[KEY_TRUSTED_NETWORKS] = trusted_networks
self.app[KEY_BANS_ENABLED] = is_ban_enabled
self.app[KEY_LOGIN_THRESHOLD] = login_threshold
self.app[KEY_DEVELOPMENT] = development
self.hass = hass self.hass = hass
self.development = development self.development = development
self.api_password = api_password self.api_password = api_password
@ -265,9 +193,6 @@ class HomeAssistantWSGI(object):
self.ssl_key = ssl_key self.ssl_key = ssl_key
self.server_host = server_host self.server_host = server_host
self.server_port = server_port self.server_port = server_port
self.use_x_forwarded_for = use_x_forwarded_for
self.trusted_networks = trusted_networks
self.event_forwarder = None
self._handler = None self._handler = None
self.server = None self.server = None
@ -281,9 +206,6 @@ class HomeAssistantWSGI(object):
else: else:
self.cors = None self.cors = None
# CACHE HACK
_GZIP_FILE_SENDER.development = development
def register_view(self, view): def register_view(self, view):
"""Register a view with the WSGI server. """Register a view with the WSGI server.
@ -293,7 +215,19 @@ class HomeAssistantWSGI(object):
""" """
if isinstance(view, type): if isinstance(view, type):
# Instantiate the view, if needed # Instantiate the view, if needed
view = view(self.hass) view = view()
if not hasattr(view, 'url'):
class_name = view.__class__.__name__
raise AttributeError(
'{0} missing required attribute "url"'.format(class_name)
)
if not hasattr(view, 'name'):
class_name = view.__class__.__name__
raise AttributeError(
'{0} missing required attribute "name"'.format(class_name)
)
view.register(self.app.router) view.register(self.app.router)
@ -318,19 +252,15 @@ class HomeAssistantWSGI(object):
Specify optional cache length of asset in days. Specify optional cache length of asset in days.
""" """
if os.path.isdir(path): if os.path.isdir(path):
assert url_root.startswith('/') self.app.router.add_static(url_root, path)
if not url_root.endswith('/'):
url_root += '/'
route = HAStaticRoute(url_root, path)
self.app.router.register_route(route)
return return
filepath = Path(path) filepath = Path(path)
@asyncio.coroutine @asyncio.coroutine
def serve_file(request): def serve_file(request):
"""Redirect to location.""" """Serve file from disk."""
res = yield from _GZIP_FILE_SENDER.send(request, filepath) res = yield from GZIP_FILE_SENDER.send(request, filepath)
return res return res
# aiohttp supports regex matching for variables. Using that as temp # aiohttp supports regex matching for variables. Using that as temp
@ -359,10 +289,21 @@ class HomeAssistantWSGI(object):
else: else:
context = None context = None
# Aiohttp freezes apps after start so that no changes can be made.
# However in Home Assistant components can be discovered after boot.
# This will now raise a RunTimeError.
# To work around this we now fake that we are frozen.
# A more appropriate fix would be to create a new app and
# re-register all redirects, views, static paths.
self.app._frozen = True # pylint: disable=protected-access
self._handler = self.app.make_handler() self._handler = self.app.make_handler()
self.server = yield from self.hass.loop.create_server( self.server = yield from self.hass.loop.create_server(
self._handler, self.server_host, self.server_port, ssl=context) self._handler, self.server_host, self.server_port, ssl=context)
self.app._frozen = False # pylint: disable=protected-access
@asyncio.coroutine @asyncio.coroutine
def stop(self): def stop(self):
"""Stop the wsgi server.""" """Stop the wsgi server."""
@ -372,21 +313,6 @@ class HomeAssistantWSGI(object):
yield from self._handler.finish_connections(60.0) yield from self._handler.finish_connections(60.0)
yield from self.app.cleanup() yield from self.app.cleanup()
def get_real_ip(self, request):
"""Return the clients correct ip address, even in proxied setups."""
if self.use_x_forwarded_for \
and HTTP_HEADER_X_FORWARDED_FOR in request.headers:
return request.headers.get(
HTTP_HEADER_X_FORWARDED_FOR).split(',')[0]
else:
peername = request.transport.get_extra_info('peername')
return peername[0] if peername is not None else None
def is_trusted_ip(self, remote_addr):
"""Match an ip address against trusted CIDR networks."""
return any(ip_address(remote_addr) in trusted_network
for trusted_network in self.hass.http.trusted_networks)
class HomeAssistantView(object): class HomeAssistantView(object):
"""Base view for all views.""" """Base view for all views."""
@ -395,22 +321,6 @@ class HomeAssistantView(object):
extra_urls = [] extra_urls = []
requires_auth = True # Views inheriting from this class can override this requires_auth = True # Views inheriting from this class can override this
def __init__(self, hass):
"""Initilalize the base view."""
if not hasattr(self, 'url'):
class_name = self.__class__.__name__
raise AttributeError(
'{0} missing required attribute "url"'.format(class_name)
)
if not hasattr(self, 'name'):
class_name = self.__class__.__name__
raise AttributeError(
'{0} missing required attribute "name"'.format(class_name)
)
self.hass = hass
# pylint: disable=no-self-use # pylint: disable=no-self-use
def json(self, result, status_code=200): def json(self, result, status_code=200):
"""Return a JSON response.""" """Return a JSON response."""
@ -428,7 +338,7 @@ class HomeAssistantView(object):
def file(self, request, fil): def file(self, request, fil):
"""Return a file.""" """Return a file."""
assert isinstance(fil, str), 'only string paths allowed' assert isinstance(fil, str), 'only string paths allowed'
response = yield from _GZIP_FILE_SENDER.send(request, Path(fil)) response = yield from GZIP_FILE_SENDER.send(request, Path(fil))
return response return response
def register(self, router): def register(self, router):
@ -455,53 +365,32 @@ class HomeAssistantView(object):
def request_handler_factory(view, handler): def request_handler_factory(view, handler):
"""Factory to wrap our handler classes. """Factory to wrap our handler classes."""
assert asyncio.iscoroutinefunction(handler) or is_callback(handler), \
"Handler should be a coroutine or a callback."
Eventually authentication should be managed by middleware.
"""
@asyncio.coroutine @asyncio.coroutine
def handle(request): def handle(request):
"""Handle incoming request.""" """Handle incoming request."""
if not view.hass.is_running: if not request.app['hass'].is_running:
return web.Response(status=503) return web.Response(status=503)
remote_addr = view.hass.http.get_real_ip(request) remote_addr = get_real_ip(request)
authenticated = request.get(KEY_AUTHENTICATED, False)
# Auth code verbose on purpose
authenticated = False
if view.hass.http.api_password is None:
authenticated = True
elif view.hass.http.is_trusted_ip(remote_addr):
authenticated = True
elif hmac.compare_digest(request.headers.get(HTTP_HEADER_HA_AUTH, ''),
view.hass.http.api_password):
# A valid auth header has been set
authenticated = True
elif hmac.compare_digest(request.GET.get(DATA_API_PASSWORD, ''),
view.hass.http.api_password):
authenticated = True
if view.requires_auth and not authenticated: if view.requires_auth and not authenticated:
yield from process_wrong_login(request)
_LOGGER.warning('Login attempt or request with an invalid ' _LOGGER.warning('Login attempt or request with an invalid '
'password from %s', remote_addr) 'password from %s', remote_addr)
persistent_notification.async_create( persistent_notification.async_create(
view.hass, request.app['hass'],
'Invalid password used from {}'.format(remote_addr), 'Invalid password used from {}'.format(remote_addr),
'Login attempt failed', NOTIFICATION_ID_LOGIN) 'Login attempt failed', NOTIFICATION_ID_LOGIN)
raise HTTPUnauthorized() raise HTTPUnauthorized()
request.authenticated = authenticated
_LOGGER.info('Serving %s to %s (auth: %s)', _LOGGER.info('Serving %s to %s (auth: %s)',
request.path, remote_addr, authenticated) request.path, remote_addr, authenticated)
assert asyncio.iscoroutinefunction(handler) or is_callback(handler), \
"Handler should be a coroutine or a callback."
result = handler(request, **request.match_info) result = handler(request, **request.match_info)
if asyncio.iscoroutine(result): if asyncio.iscoroutine(result):

View file

@ -0,0 +1,66 @@
"""Authentication for HTTP component."""
import asyncio
import hmac
import logging
from homeassistant.const import HTTP_HEADER_HA_AUTH
from .util import get_real_ip
from .const import KEY_TRUSTED_NETWORKS, KEY_AUTHENTICATED
DATA_API_PASSWORD = 'api_password'
_LOGGER = logging.getLogger(__name__)
@asyncio.coroutine
def auth_middleware(app, handler):
"""Authentication middleware."""
# If no password set, just always set authenticated=True
if app['hass'].http.api_password is None:
@asyncio.coroutine
def no_auth_middleware_handler(request):
"""Auth middleware to approve all requests."""
request[KEY_AUTHENTICATED] = True
return handler(request)
return no_auth_middleware_handler
@asyncio.coroutine
def auth_middleware_handler(request):
"""Auth middleware to check authentication."""
# Auth code verbose on purpose
authenticated = False
if (HTTP_HEADER_HA_AUTH in request.headers and
validate_password(request,
request.headers[HTTP_HEADER_HA_AUTH])):
# A valid auth header has been set
authenticated = True
elif (DATA_API_PASSWORD in request.GET and
validate_password(request, request.GET[DATA_API_PASSWORD])):
authenticated = True
elif is_trusted_ip(request):
authenticated = True
request[KEY_AUTHENTICATED] = authenticated
return handler(request)
return auth_middleware_handler
def is_trusted_ip(request):
"""Test if request is from a trusted ip."""
ip_addr = get_real_ip(request)
return ip_addr and any(
ip_addr in trusted_network for trusted_network
in request.app[KEY_TRUSTED_NETWORKS])
def validate_password(request, api_password):
"""Test if password is valid."""
return hmac.compare_digest(api_password,
request.app['hass'].http.api_password)

View file

@ -0,0 +1,132 @@
"""Ban logic for HTTP component."""
import asyncio
from collections import defaultdict
from datetime import datetime
from ipaddress import ip_address
import logging
from aiohttp.web_exceptions import HTTPForbidden
import voluptuous as vol
from homeassistant.components import persistent_notification
from homeassistant.config import load_yaml_config_file
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from homeassistant.util.yaml import dump
from .const import (
KEY_BANS_ENABLED, KEY_BANNED_IPS, KEY_LOGIN_THRESHOLD,
KEY_FAILED_LOGIN_ATTEMPTS)
from .util import get_real_ip
NOTIFICATION_ID_BAN = 'ip-ban'
IP_BANS_FILE = 'ip_bans.yaml'
ATTR_BANNED_AT = "banned_at"
SCHEMA_IP_BAN_ENTRY = vol.Schema({
vol.Optional('banned_at'): vol.Any(None, cv.datetime)
})
_LOGGER = logging.getLogger(__name__)
@asyncio.coroutine
def ban_middleware(app, handler):
"""IP Ban middleware."""
if not app[KEY_BANS_ENABLED]:
return handler
if KEY_BANNED_IPS not in app:
hass = app['hass']
app[KEY_BANNED_IPS] = yield from hass.loop.run_in_executor(
None, load_ip_bans_config, hass.config.path(IP_BANS_FILE))
@asyncio.coroutine
def ban_middleware_handler(request):
"""Verify if IP is not banned."""
ip_address_ = get_real_ip(request)
is_banned = any(ip_ban.ip_address == ip_address_
for ip_ban in request.app[KEY_BANNED_IPS])
if is_banned:
raise HTTPForbidden()
return handler(request)
return ban_middleware_handler
@asyncio.coroutine
def process_wrong_login(request):
"""Process a wrong login attempt."""
if (not request.app[KEY_BANS_ENABLED] or
request.app[KEY_LOGIN_THRESHOLD] < 1):
return
if KEY_FAILED_LOGIN_ATTEMPTS not in request.app:
request.app[KEY_FAILED_LOGIN_ATTEMPTS] = defaultdict(int)
remote_addr = get_real_ip(request)
request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] += 1
if (request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] >
request.app[KEY_LOGIN_THRESHOLD]):
new_ban = IpBan(remote_addr)
request.app[KEY_BANNED_IPS].append(new_ban)
hass = request.app['hass']
yield from hass.loop.run_in_executor(
None, update_ip_bans_config, hass.config.path(IP_BANS_FILE),
new_ban)
_LOGGER.warning('Banned IP %s for too many login attempts',
remote_addr)
persistent_notification.async_create(
hass,
'Too many login attempts from {}'.format(remote_addr),
'Banning IP address', NOTIFICATION_ID_BAN)
class IpBan(object):
"""Represents banned IP address."""
def __init__(self, ip_ban: str, banned_at: datetime=None) -> None:
"""Initializing Ip Ban object."""
self.ip_address = ip_address(ip_ban)
self.banned_at = banned_at or datetime.utcnow()
def load_ip_bans_config(path: str):
"""Loading list of banned IPs from config file."""
ip_list = []
try:
list_ = load_yaml_config_file(path)
except FileNotFoundError:
return []
except HomeAssistantError as err:
_LOGGER.error('Unable to load %s: %s', path, str(err))
return []
for ip_ban, ip_info in list_.items():
try:
ip_info = SCHEMA_IP_BAN_ENTRY(ip_info)
ip_list.append(IpBan(ip_ban, ip_info['banned_at']))
except vol.Invalid as err:
_LOGGER.error('Failed to load IP ban %s: %s', ip_info, err)
continue
return ip_list
def update_ip_bans_config(path: str, ip_ban: IpBan):
"""Update config file with new banned IP address."""
with open(path, 'a') as out:
ip_ = {str(ip_ban.ip_address): {
ATTR_BANNED_AT: ip_ban.banned_at.strftime("%Y-%m-%dT%H:%M:%S")
}}
out.write('\n')
out.write(dump(ip_))

View file

@ -0,0 +1,12 @@
"""HTTP specific constants."""
KEY_AUTHENTICATED = 'ha_authenticated'
KEY_USE_X_FORWARDED_FOR = 'ha_use_x_forwarded_for'
KEY_TRUSTED_NETWORKS = 'ha_trusted_networks'
KEY_REAL_IP = 'ha_real_ip'
KEY_BANS_ENABLED = 'ha_bans_enabled'
KEY_BANNED_IPS = 'ha_banned_ips'
KEY_FAILED_LOGIN_ATTEMPTS = 'ha_failed_login_attempts'
KEY_LOGIN_THRESHOLD = 'ha_login_treshold'
KEY_DEVELOPMENT = 'ha_development'
HTTP_HEADER_X_FORWARDED_FOR = 'X-Forwarded-For'

View file

@ -0,0 +1,93 @@
"""Static file handling for HTTP component."""
import asyncio
import mimetypes
import re
from aiohttp import hdrs
from aiohttp.file_sender import FileSender
from aiohttp.web_urldispatcher import StaticResource
from aiohttp.web_exceptions import HTTPNotModified
from .const import KEY_DEVELOPMENT
_FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
class GzipFileSender(FileSender):
"""FileSender class capable of sending gzip version if available."""
# pylint: disable=invalid-name
@asyncio.coroutine
def send(self, request, filepath):
"""Send filepath to client using request."""
gzip = False
if 'gzip' in request.headers[hdrs.ACCEPT_ENCODING]:
gzip_path = filepath.with_name(filepath.name + '.gz')
if gzip_path.is_file():
filepath = gzip_path
gzip = True
st = filepath.stat()
modsince = request.if_modified_since
if modsince is not None and st.st_mtime <= modsince.timestamp():
raise HTTPNotModified()
ct, encoding = mimetypes.guess_type(str(filepath))
if not ct:
ct = 'application/octet-stream'
resp = self._response_factory()
resp.content_type = ct
if encoding:
resp.headers[hdrs.CONTENT_ENCODING] = encoding
if gzip:
resp.headers[hdrs.VARY] = hdrs.ACCEPT_ENCODING
resp.last_modified = st.st_mtime
# CACHE HACK
if not request.app[KEY_DEVELOPMENT]:
cache_time = 31 * 86400 # = 1 month
resp.headers[hdrs.CACHE_CONTROL] = "public, max-age={}".format(
cache_time)
file_size = st.st_size
resp.content_length = file_size
with filepath.open('rb') as f:
yield from self._sendfile(request, resp, f, file_size)
return resp
GZIP_FILE_SENDER = GzipFileSender()
@asyncio.coroutine
def staticresource_middleware(app, handler):
"""Enhance StaticResourceHandler middleware.
Adds gzip encoding and fingerprinting matching.
"""
inst = getattr(handler, '__self__', None)
if not isinstance(inst, StaticResource):
return handler
# pylint: disable=protected-access
inst._file_sender = GZIP_FILE_SENDER
@asyncio.coroutine
def static_middleware_handler(request):
"""Strip out fingerprints from resource names."""
fingerprinted = _FINGERPRINT.match(request.match_info['filename'])
if fingerprinted:
request.match_info['filename'] = \
'{}.{}'.format(*fingerprinted.groups())
resp = yield from handler(request)
return resp
return static_middleware_handler

View file

@ -0,0 +1,25 @@
"""HTTP utilities."""
from ipaddress import ip_address
from .const import (
KEY_REAL_IP, KEY_USE_X_FORWARDED_FOR, HTTP_HEADER_X_FORWARDED_FOR)
def get_real_ip(request):
"""Get IP address of client."""
if KEY_REAL_IP in request:
return request[KEY_REAL_IP]
if (request.app[KEY_USE_X_FORWARDED_FOR] and
HTTP_HEADER_X_FORWARDED_FOR in request.headers):
request[KEY_REAL_IP] = ip_address(
request.headers.get(HTTP_HEADER_X_FORWARDED_FOR).split(',')[0])
else:
peername = request.transport.get_extra_info('peername')
if peername:
request[KEY_REAL_IP] = ip_address(peername[0])
else:
request[KEY_REAL_IP] = None
return request[KEY_REAL_IP]

View file

@ -21,6 +21,7 @@ _LOGGER = logging.getLogger(__name__)
CONF_DB_NAME = 'database' CONF_DB_NAME = 'database'
CONF_TAGS = 'tags' CONF_TAGS = 'tags'
CONF_DEFAULT_MEASUREMENT = 'default_measurement'
DEFAULT_DATABASE = 'home_assistant' DEFAULT_DATABASE = 'home_assistant'
DEFAULT_HOST = 'localhost' DEFAULT_HOST = 'localhost'
@ -40,6 +41,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_DB_NAME, default=DEFAULT_DATABASE): cv.string, vol.Optional(CONF_DB_NAME, default=DEFAULT_DATABASE): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
vol.Optional(CONF_DEFAULT_MEASUREMENT): cv.string,
vol.Optional(CONF_TAGS, default={}): vol.Optional(CONF_TAGS, default={}):
vol.Schema({cv.string: cv.string}), vol.Schema({cv.string: cv.string}),
vol.Optional(CONF_WHITELIST, default=[]): vol.Optional(CONF_WHITELIST, default=[]):
@ -65,6 +67,7 @@ def setup(hass, config):
blacklist = conf.get(CONF_BLACKLIST) blacklist = conf.get(CONF_BLACKLIST)
whitelist = conf.get(CONF_WHITELIST) whitelist = conf.get(CONF_WHITELIST)
tags = conf.get(CONF_TAGS) tags = conf.get(CONF_TAGS)
default_measurement = conf.get(CONF_DEFAULT_MEASUREMENT)
try: try:
influx = InfluxDBClient( influx = InfluxDBClient(
@ -96,6 +99,9 @@ def setup(hass, config):
measurement = state.attributes.get('unit_of_measurement') measurement = state.attributes.get('unit_of_measurement')
if measurement in (None, ''): if measurement in (None, ''):
if default_measurement:
measurement = default_measurement
else:
measurement = state.entity_id measurement = state.entity_id
json_body = [ json_body = [
@ -114,7 +120,11 @@ def setup(hass, config):
for key, value in state.attributes.items(): for key, value in state.attributes.items():
if key != 'unit_of_measurement': if key != 'unit_of_measurement':
if isinstance(value, (str, float, bool)):
json_body[0]['fields'][key] = value json_body[0]['fields'][key] = value
elif isinstance(value, int):
# Prevent column data errors in influxDB.
json_body[0]['fields'][key] = float(value)
json_body[0]['tags'].update(tags) json_body[0]['tags'].update(tags)

View file

@ -250,11 +250,10 @@ def setup(hass, config):
discovery.load_platform(hass, "sensor", DOMAIN, {}, config) discovery.load_platform(hass, "sensor", DOMAIN, {}, config)
hass.http.register_view(iOSIdentifyDeviceView(hass)) hass.http.register_view(iOSIdentifyDeviceView)
app_config = config.get(DOMAIN, {}) app_config = config.get(DOMAIN, {})
hass.http.register_view(iOSPushConfigView(hass, hass.http.register_view(iOSPushConfigView(app_config.get(CONF_PUSH, {})))
app_config.get(CONF_PUSH, {})))
return True return True
@ -266,9 +265,8 @@ class iOSPushConfigView(HomeAssistantView):
url = "/api/ios/push" url = "/api/ios/push"
name = "api:ios:push" name = "api:ios:push"
def __init__(self, hass, push_config): def __init__(self, push_config):
"""Init the view.""" """Init the view."""
super().__init__(hass)
self.push_config = push_config self.push_config = push_config
@callback @callback
@ -283,10 +281,6 @@ class iOSIdentifyDeviceView(HomeAssistantView):
url = "/api/ios/identify" url = "/api/ios/identify"
name = "api:ios:identify" name = "api:ios:identify"
def __init__(self, hass):
"""Init the view."""
super().__init__(hass)
@asyncio.coroutine @asyncio.coroutine
def post(self, request): def post(self, request):
"""Handle the POST request for device identification.""" """Handle the POST request for device identification."""

View file

@ -4,6 +4,7 @@ Provides functionality to interact with lights.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/light/ https://home-assistant.io/components/light/
""" """
import asyncio
import logging import logging
import os import os
import csv import csv
@ -64,6 +65,9 @@ ATTR_FLASH = "flash"
FLASH_SHORT = "short" FLASH_SHORT = "short"
FLASH_LONG = "long" FLASH_LONG = "long"
# List of possible effects
ATTR_EFFECT_LIST = "effect_list"
# Apply an effect to the light, can be EFFECT_COLORLOOP. # Apply an effect to the light, can be EFFECT_COLORLOOP.
ATTR_EFFECT = "effect" ATTR_EFFECT = "effect"
EFFECT_COLORLOOP = "colorloop" EFFECT_COLORLOOP = "colorloop"
@ -78,6 +82,8 @@ PROP_TO_ATTR = {
'rgb_color': ATTR_RGB_COLOR, 'rgb_color': ATTR_RGB_COLOR,
'xy_color': ATTR_XY_COLOR, 'xy_color': ATTR_XY_COLOR,
'white_value': ATTR_WHITE_VALUE, 'white_value': ATTR_WHITE_VALUE,
'effect_list': ATTR_EFFECT_LIST,
'effect': ATTR_EFFECT,
'supported_features': ATTR_SUPPORTED_FEATURES, 'supported_features': ATTR_SUPPORTED_FEATURES,
} }
@ -87,19 +93,20 @@ VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255))
LIGHT_TURN_ON_SCHEMA = vol.Schema({ LIGHT_TURN_ON_SCHEMA = vol.Schema({
ATTR_ENTITY_ID: cv.entity_ids, ATTR_ENTITY_ID: cv.entity_ids,
ATTR_PROFILE: str, ATTR_PROFILE: cv.string,
ATTR_TRANSITION: VALID_TRANSITION, ATTR_TRANSITION: VALID_TRANSITION,
ATTR_BRIGHTNESS: VALID_BRIGHTNESS, ATTR_BRIGHTNESS: VALID_BRIGHTNESS,
ATTR_COLOR_NAME: str, ATTR_COLOR_NAME: cv.string,
ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)), ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)),
vol.Coerce(tuple)), vol.Coerce(tuple)),
ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)), ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
vol.Coerce(tuple)), vol.Coerce(tuple)),
ATTR_COLOR_TEMP: vol.All(int, vol.Range(min=color_util.HASS_COLOR_MIN, ATTR_COLOR_TEMP: vol.All(vol.Coerce(int),
vol.Range(min=color_util.HASS_COLOR_MIN,
max=color_util.HASS_COLOR_MAX)), max=color_util.HASS_COLOR_MAX)),
ATTR_WHITE_VALUE: vol.All(int, vol.Range(min=0, max=255)), ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)),
ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]), ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]),
ATTR_EFFECT: vol.In([EFFECT_COLORLOOP, EFFECT_RANDOM, EFFECT_WHITE]), ATTR_EFFECT: cv.string,
}) })
LIGHT_TURN_OFF_SCHEMA = vol.Schema({ LIGHT_TURN_OFF_SCHEMA = vol.Schema({
@ -158,7 +165,7 @@ def async_turn_on(hass, entity_id=None, transition=None, brightness=None,
] if value is not None ] if value is not None
} }
hass.async_add_job(hass.services.async_call, DOMAIN, SERVICE_TURN_ON, data) hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data))
def turn_off(hass, entity_id=None, transition=None): def turn_off(hass, entity_id=None, transition=None):
@ -177,8 +184,8 @@ def async_turn_off(hass, entity_id=None, transition=None):
] if value is not None ] if value is not None
} }
hass.async_add_job(hass.services.async_call, DOMAIN, SERVICE_TURN_OFF, hass.async_add_job(hass.services.async_call(
data) DOMAIN, SERVICE_TURN_OFF, data))
def toggle(hass, entity_id=None, transition=None): def toggle(hass, entity_id=None, transition=None):
@ -193,13 +200,83 @@ def toggle(hass, entity_id=None, transition=None):
hass.services.call(DOMAIN, SERVICE_TOGGLE, data) hass.services.call(DOMAIN, SERVICE_TOGGLE, data)
def setup(hass, config): @asyncio.coroutine
def async_setup(hass, config):
"""Expose light control via statemachine and services.""" """Expose light control via statemachine and services."""
component = EntityComponent( component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LIGHTS) _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LIGHTS)
component.setup(config) yield from component.async_setup(config)
# Load built-in profiles and custom profiles # load profiles from files
profiles = yield from hass.loop.run_in_executor(
None, _load_profile_data, hass)
if profiles is None:
return False
@asyncio.coroutine
def async_handle_light_service(service):
"""Hande a turn light on or off service call."""
# Get the validated data
params = service.data.copy()
# Convert the entity ids to valid light ids
target_lights = component.async_extract_from_service(service)
params.pop(ATTR_ENTITY_ID, None)
# Processing extra data for turn light on request.
profile = profiles.get(params.pop(ATTR_PROFILE, None))
if profile:
params.setdefault(ATTR_XY_COLOR, profile[:2])
params.setdefault(ATTR_BRIGHTNESS, profile[2])
color_name = params.pop(ATTR_COLOR_NAME, None)
if color_name is not None:
params[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name)
update_tasks = []
for light in target_lights:
if service.service == SERVICE_TURN_ON:
yield from light.async_turn_on(**params)
elif service.service == SERVICE_TURN_OFF:
yield from light.async_turn_off(**params)
else:
yield from light.async_toggle(**params)
if light.should_poll:
update_coro = light.async_update_ha_state(True)
if hasattr(light, 'async_update'):
update_tasks.append(hass.loop.create_task(update_coro))
else:
yield from update_coro
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
# Listen for light on and light off service calls.
descriptions = yield from hass.loop.run_in_executor(
None, load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml'))
hass.services.async_register(
DOMAIN, SERVICE_TURN_ON, async_handle_light_service,
descriptions.get(SERVICE_TURN_ON), schema=LIGHT_TURN_ON_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_TURN_OFF, async_handle_light_service,
descriptions.get(SERVICE_TURN_OFF), schema=LIGHT_TURN_OFF_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_TOGGLE, async_handle_light_service,
descriptions.get(SERVICE_TOGGLE), schema=LIGHT_TOGGLE_SCHEMA)
return True
def _load_profile_data(hass):
"""Load built-in profiles and custom profiles."""
profile_paths = [os.path.join(os.path.dirname(__file__), profile_paths = [os.path.join(os.path.dirname(__file__),
LIGHT_PROFILES_FILE), LIGHT_PROFILES_FILE),
hass.config.path(LIGHT_PROFILES_FILE)] hass.config.path(LIGHT_PROFILES_FILE)]
@ -221,67 +298,8 @@ def setup(hass, config):
except vol.MultipleInvalid as ex: except vol.MultipleInvalid as ex:
_LOGGER.error("Error parsing light profile from %s: %s", _LOGGER.error("Error parsing light profile from %s: %s",
profile_path, ex) profile_path, ex)
return False return None
return profiles
def handle_light_service(service):
"""Hande a turn light on or off service call."""
# Get the validated data
params = service.data.copy()
# Convert the entity ids to valid light ids
target_lights = component.extract_from_service(service)
params.pop(ATTR_ENTITY_ID, None)
service_fun = None
if service.service == SERVICE_TURN_OFF:
service_fun = 'turn_off'
elif service.service == SERVICE_TOGGLE:
service_fun = 'toggle'
if service_fun:
for light in target_lights:
getattr(light, service_fun)(**params)
for light in target_lights:
if light.should_poll:
light.update_ha_state(True)
return
# Processing extra data for turn light on request.
profile = profiles.get(params.pop(ATTR_PROFILE, None))
if profile:
params.setdefault(ATTR_XY_COLOR, profile[:2])
params.setdefault(ATTR_BRIGHTNESS, profile[2])
color_name = params.pop(ATTR_COLOR_NAME, None)
if color_name is not None:
params[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name)
for light in target_lights:
light.turn_on(**params)
for light in target_lights:
if light.should_poll:
light.update_ha_state(True)
# Listen for light on and light off service calls.
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_light_service,
descriptions.get(SERVICE_TURN_ON),
schema=LIGHT_TURN_ON_SCHEMA)
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_light_service,
descriptions.get(SERVICE_TURN_OFF),
schema=LIGHT_TURN_OFF_SCHEMA)
hass.services.register(DOMAIN, SERVICE_TOGGLE, handle_light_service,
descriptions.get(SERVICE_TOGGLE),
schema=LIGHT_TOGGLE_SCHEMA)
return True
class Light(ToggleEntity): class Light(ToggleEntity):
@ -314,6 +332,16 @@ class Light(ToggleEntity):
"""Return the white value of this light between 0..255.""" """Return the white value of this light between 0..255."""
return None return None
@property
def effect_list(self):
"""Return the list of supported effects."""
return None
@property
def effect(self):
"""Return the current effect."""
return None
@property @property
def state_attributes(self): def state_attributes(self):
"""Return optional state attributes.""" """Return optional state attributes."""

View file

@ -7,25 +7,29 @@ https://home-assistant.io/components/demo/
import random import random
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_WHITE_VALUE, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT,
ATTR_XY_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, ATTR_RGB_COLOR, ATTR_WHITE_VALUE, ATTR_XY_COLOR, SUPPORT_BRIGHTNESS,
SUPPORT_WHITE_VALUE, Light) SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_RGB_COLOR, SUPPORT_WHITE_VALUE,
Light)
LIGHT_COLORS = [ LIGHT_COLORS = [
[237, 224, 33], [237, 224, 33],
[255, 63, 111], [255, 63, 111],
] ]
LIGHT_EFFECT_LIST = ['rainbow', 'none']
LIGHT_TEMPS = [240, 380] LIGHT_TEMPS = [240, 380]
SUPPORT_DEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR | SUPPORT_DEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT |
SUPPORT_WHITE_VALUE) SUPPORT_RGB_COLOR | SUPPORT_WHITE_VALUE)
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup the demo light platform.""" """Setup the demo light platform."""
add_devices_callback([ add_devices_callback([
DemoLight("Bed Light", False), DemoLight("Bed Light", False, effect_list=LIGHT_EFFECT_LIST,
effect=LIGHT_EFFECT_LIST[0]),
DemoLight("Ceiling Lights", True, LIGHT_COLORS[0], LIGHT_TEMPS[1]), DemoLight("Ceiling Lights", True, LIGHT_COLORS[0], LIGHT_TEMPS[1]),
DemoLight("Kitchen Lights", True, LIGHT_COLORS[1], LIGHT_TEMPS[0]) DemoLight("Kitchen Lights", True, LIGHT_COLORS[1], LIGHT_TEMPS[0])
]) ])
@ -36,7 +40,7 @@ class DemoLight(Light):
def __init__( def __init__(
self, name, state, rgb=None, ct=None, brightness=180, self, name, state, rgb=None, ct=None, brightness=180,
xy_color=(.5, .5), white=200): xy_color=(.5, .5), white=200, effect_list=None, effect=None):
"""Initialize the light.""" """Initialize the light."""
self._name = name self._name = name
self._state = state self._state = state
@ -45,6 +49,8 @@ class DemoLight(Light):
self._brightness = brightness self._brightness = brightness
self._xy_color = xy_color self._xy_color = xy_color
self._white = white self._white = white
self._effect_list = effect_list
self._effect = effect
@property @property
def should_poll(self): def should_poll(self):
@ -81,6 +87,16 @@ class DemoLight(Light):
"""Return the white value of this light between 0..255.""" """Return the white value of this light between 0..255."""
return self._white return self._white
@property
def effect_list(self):
"""Return the list of supported effects."""
return self._effect_list
@property
def effect(self):
"""Return the current effect."""
return self._effect
@property @property
def is_on(self): def is_on(self):
"""Return true if light is on.""" """Return true if light is on."""
@ -110,9 +126,12 @@ class DemoLight(Light):
if ATTR_WHITE_VALUE in kwargs: if ATTR_WHITE_VALUE in kwargs:
self._white = kwargs[ATTR_WHITE_VALUE] self._white = kwargs[ATTR_WHITE_VALUE]
self.update_ha_state() if ATTR_EFFECT in kwargs:
self._effect = kwargs[ATTR_EFFECT]
self.schedule_update_ha_state()
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Turn the light off.""" """Turn the light off."""
self._state = False self._state = False
self.update_ha_state() self.schedule_update_ha_state()

View file

@ -7,8 +7,9 @@ https://home-assistant.io/components/light.homematic/
import logging import logging
from homeassistant.components.light import (ATTR_BRIGHTNESS, from homeassistant.components.light import (ATTR_BRIGHTNESS,
SUPPORT_BRIGHTNESS, Light) SUPPORT_BRIGHTNESS, Light)
from homeassistant.components.homematic import HMDevice
from homeassistant.const import STATE_UNKNOWN from homeassistant.const import STATE_UNKNOWN
import homeassistant.components.homematic as homematic from homeassistant.loader import get_component
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -22,14 +23,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if discovery_info is None: if discovery_info is None:
return return
homematic = get_component("homematic")
return homematic.setup_hmdevice_discovery_helper( return homematic.setup_hmdevice_discovery_helper(
hass,
HMLight, HMLight,
discovery_info, discovery_info,
add_devices add_devices
) )
class HMLight(homematic.HMDevice, Light): class HMLight(HMDevice, Light):
"""Representation of a Homematic light.""" """Representation of a Homematic light."""
@property @property

View file

@ -61,10 +61,10 @@ class ISYLightDevice(isy.ISYDevice, Light):
def turn_off(self, **kwargs) -> None: def turn_off(self, **kwargs) -> None:
"""Send the turn off command to the ISY994 light device.""" """Send the turn off command to the ISY994 light device."""
if not self._node.fastoff(): if not self._node.off():
_LOGGER.debug('Unable to turn on light.') _LOGGER.debug('Unable to turn on light.')
def turn_on(self, brightness=100, **kwargs) -> None: def turn_on(self, brightness=None, **kwargs) -> None:
"""Send the turn on command to the ISY994 light device.""" """Send the turn on command to the ISY994 light device."""
if not self._node.on(val=brightness): if not self._node.on(val=brightness):
_LOGGER.debug('Unable to turn on light.') _LOGGER.debug('Unable to turn on light.')

View file

@ -140,7 +140,7 @@ def state(new_state):
# Update state. # Update state.
self._is_on = new_state self._is_on = new_state
self.group.enqueue(pipeline) self.group.enqueue(pipeline)
self.update_ha_state() self.schedule_update_ha_state()
return wrapper return wrapper
return decorator return decorator

View file

@ -283,7 +283,7 @@ class MqttLight(Light):
should_update = True should_update = True
if should_update: if should_update:
self.update_ha_state() self.schedule_update_ha_state()
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Turn the device off.""" """Turn the device off."""
@ -293,4 +293,4 @@ class MqttLight(Light):
if self._optimistic: if self._optimistic:
# Optimistically assume that switch has changed state. # Optimistically assume that switch has changed state.
self._state = False self._state = False
self.update_ha_state() self.schedule_update_ha_state()

View file

@ -216,7 +216,7 @@ class MqttJson(Light):
should_update = True should_update = True
if should_update: if should_update:
self.update_ha_state() self.schedule_update_ha_state()
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Turn the device off.""" """Turn the device off."""
@ -231,4 +231,4 @@ class MqttJson(Light):
if self._optimistic: if self._optimistic:
# Optimistically assume that the light has changed state. # Optimistically assume that the light has changed state.
self._state = False self._state = False
self.update_ha_state() self.schedule_update_ha_state()

View file

@ -10,8 +10,8 @@ import voluptuous as vol
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_TRANSITION, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, ATTR_TRANSITION,
ATTR_FLASH, SUPPORT_BRIGHTNESS, SUPPORT_FLASH, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, SUPPORT_FLASH,
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light) SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light)
from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, STATE_ON, STATE_OFF from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, STATE_ON, STATE_OFF
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
@ -27,6 +27,7 @@ DEPENDENCIES = ['mqtt']
DEFAULT_NAME = 'MQTT Template Light' DEFAULT_NAME = 'MQTT Template Light'
DEFAULT_OPTIMISTIC = False DEFAULT_OPTIMISTIC = False
CONF_EFFECT_LIST = "effect_list"
CONF_COMMAND_ON_TEMPLATE = 'command_on_template' CONF_COMMAND_ON_TEMPLATE = 'command_on_template'
CONF_COMMAND_OFF_TEMPLATE = 'command_off_template' CONF_COMMAND_OFF_TEMPLATE = 'command_off_template'
CONF_STATE_TEMPLATE = 'state_template' CONF_STATE_TEMPLATE = 'state_template'
@ -34,12 +35,14 @@ CONF_BRIGHTNESS_TEMPLATE = 'brightness_template'
CONF_RED_TEMPLATE = 'red_template' CONF_RED_TEMPLATE = 'red_template'
CONF_GREEN_TEMPLATE = 'green_template' CONF_GREEN_TEMPLATE = 'green_template'
CONF_BLUE_TEMPLATE = 'blue_template' CONF_BLUE_TEMPLATE = 'blue_template'
CONF_EFFECT_TEMPLATE = 'effect_template'
SUPPORT_MQTT_TEMPLATE = (SUPPORT_BRIGHTNESS | SUPPORT_FLASH | SUPPORT_MQTT_TEMPLATE = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | SUPPORT_FLASH |
SUPPORT_RGB_COLOR | SUPPORT_TRANSITION) SUPPORT_RGB_COLOR | SUPPORT_TRANSITION)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]),
vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
vol.Required(CONF_COMMAND_ON_TEMPLATE): cv.template, vol.Required(CONF_COMMAND_ON_TEMPLATE): cv.template,
@ -49,6 +52,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_RED_TEMPLATE): cv.template, vol.Optional(CONF_RED_TEMPLATE): cv.template,
vol.Optional(CONF_GREEN_TEMPLATE): cv.template, vol.Optional(CONF_GREEN_TEMPLATE): cv.template,
vol.Optional(CONF_BLUE_TEMPLATE): cv.template, vol.Optional(CONF_BLUE_TEMPLATE): cv.template,
vol.Optional(CONF_EFFECT_TEMPLATE): cv.template,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS): vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS):
vol.All(vol.Coerce(int), vol.In([0, 1, 2])), vol.All(vol.Coerce(int), vol.In([0, 1, 2])),
@ -61,6 +65,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices([MqttTemplate( add_devices([MqttTemplate(
hass, hass,
config.get(CONF_NAME), config.get(CONF_NAME),
config.get(CONF_EFFECT_LIST),
{ {
key: config.get(key) for key in ( key: config.get(key) for key in (
CONF_STATE_TOPIC, CONF_STATE_TOPIC,
@ -75,7 +80,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
CONF_BRIGHTNESS_TEMPLATE, CONF_BRIGHTNESS_TEMPLATE,
CONF_RED_TEMPLATE, CONF_RED_TEMPLATE,
CONF_GREEN_TEMPLATE, CONF_GREEN_TEMPLATE,
CONF_BLUE_TEMPLATE CONF_BLUE_TEMPLATE,
CONF_EFFECT_TEMPLATE
) )
}, },
config.get(CONF_OPTIMISTIC), config.get(CONF_OPTIMISTIC),
@ -87,10 +93,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class MqttTemplate(Light): class MqttTemplate(Light):
"""Representation of a MQTT Template light.""" """Representation of a MQTT Template light."""
def __init__(self, hass, name, topics, templates, optimistic, qos, retain): def __init__(self, hass, name, effect_list, topics, templates, optimistic,
qos, retain):
"""Initialize MQTT Template light.""" """Initialize MQTT Template light."""
self._hass = hass self._hass = hass
self._name = name self._name = name
self._effect_list = effect_list
self._topics = topics self._topics = topics
self._templates = templates self._templates = templates
for tpl in self._templates.values(): for tpl in self._templates.values():
@ -114,6 +122,7 @@ class MqttTemplate(Light):
self._rgb = [0, 0, 0] self._rgb = [0, 0, 0]
else: else:
self._rgb = None self._rgb = None
self._effect = None
def state_received(topic, payload, qos): def state_received(topic, payload, qos):
"""A new MQTT message has been received.""" """A new MQTT message has been received."""
@ -152,6 +161,17 @@ class MqttTemplate(Light):
except ValueError: except ValueError:
_LOGGER.warning('Invalid color value received') _LOGGER.warning('Invalid color value received')
# read effect
if self._templates[CONF_EFFECT_TEMPLATE] is not None:
effect = self._templates[CONF_EFFECT_TEMPLATE].\
render_with_possible_json_value(payload)
# validate effect value
if effect in self._effect_list:
self._effect = effect
else:
_LOGGER.warning('Unsupported effect value received')
self.update_ha_state() self.update_ha_state()
if self._topics[CONF_STATE_TOPIC] is not None: if self._topics[CONF_STATE_TOPIC] is not None:
@ -191,6 +211,16 @@ class MqttTemplate(Light):
"""Return True if unable to access real state of the entity.""" """Return True if unable to access real state of the entity."""
return self._optimistic return self._optimistic
@property
def effect_list(self):
"""Return the list of supported effects."""
return self._effect_list
@property
def effect(self):
"""Return the current effect."""
return self._effect
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
"""Turn the entity on.""" """Turn the entity on."""
# state # state
@ -214,6 +244,10 @@ class MqttTemplate(Light):
if self._optimistic: if self._optimistic:
self._rgb = kwargs[ATTR_RGB_COLOR] self._rgb = kwargs[ATTR_RGB_COLOR]
# effect
if ATTR_EFFECT in kwargs:
values['effect'] = kwargs.get(ATTR_EFFECT)
# flash # flash
if ATTR_FLASH in kwargs: if ATTR_FLASH in kwargs:
values['flash'] = kwargs.get(ATTR_FLASH) values['flash'] = kwargs.get(ATTR_FLASH)
@ -229,7 +263,7 @@ class MqttTemplate(Light):
) )
if self._optimistic: if self._optimistic:
self.update_ha_state() self.schedule_update_ha_state()
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Turn the entity off.""" """Turn the entity off."""
@ -249,4 +283,4 @@ class MqttTemplate(Light):
) )
if self._optimistic: if self._optimistic:
self.update_ha_state() self.schedule_update_ha_state()

View file

@ -115,7 +115,7 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light):
# optimistically assume that light has changed state # optimistically assume that light has changed state
self._state = True self._state = True
self._values[set_req.V_LIGHT] = STATE_ON self._values[set_req.V_LIGHT] = STATE_ON
self.update_ha_state() self.schedule_update_ha_state()
def _turn_on_dimmer(self, **kwargs): def _turn_on_dimmer(self, **kwargs):
"""Turn on dimmer child device.""" """Turn on dimmer child device."""
@ -135,7 +135,7 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light):
# optimistically assume that light has changed state # optimistically assume that light has changed state
self._brightness = brightness self._brightness = brightness
self._values[set_req.V_DIMMER] = percent self._values[set_req.V_DIMMER] = percent
self.update_ha_state() self.schedule_update_ha_state()
def _turn_on_rgb_and_w(self, hex_template, **kwargs): def _turn_on_rgb_and_w(self, hex_template, **kwargs):
"""Turn on RGB or RGBW child device.""" """Turn on RGB or RGBW child device."""
@ -165,7 +165,7 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light):
self._white = white self._white = white
if hex_color: if hex_color:
self._values[self.value_type] = hex_color self._values[self.value_type] = hex_color
self.update_ha_state() self.schedule_update_ha_state()
def _turn_off_light(self, value_type=None, value=None): def _turn_off_light(self, value_type=None, value=None):
"""Turn off light child device.""" """Turn off light child device."""
@ -211,7 +211,7 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light):
self._state = False self._state = False
self._values[value_type] = ( self._values[value_type] = (
STATE_OFF if set_req.V_LIGHT in self._values else value) STATE_OFF if set_req.V_LIGHT in self._values else value)
self.update_ha_state() self.schedule_update_ha_state()
def _update_light(self): def _update_light(self):
"""Update the controller with values from light child.""" """Update the controller with values from light child."""

View file

@ -19,7 +19,8 @@ from homeassistant.components.light import (
SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, PLATFORM_SCHEMA) SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['lightify==1.0.3'] REQUIREMENTS = ['https://github.com/tfriedel/python-lightify/archive/'
'd6eadcf311e6e21746182d1480e97b350dda2b3e.zip#lightify==1.0.4']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -92,37 +93,43 @@ class OsramLightifyLight(Light):
self._light = light self._light = light
self._light_id = light_id self._light_id = light_id
self.update_lights = update_lights self.update_lights = update_lights
self._brightness = 0
self._rgb = (0, 0, 0)
self._name = ""
self._temperature = TEMP_MIN
self._state = False
self.update()
@property @property
def name(self): def name(self):
"""Return the name of the device if any.""" """Return the name of the device if any."""
return self._light.name() return self._name
@property @property
def rgb_color(self): def rgb_color(self):
"""Last RGB color value set.""" """Last RGB color value set."""
return self._light.rgb() _LOGGER.debug("rgb_color light state for light: %s is: %s %s %s ",
self._name, self._rgb[0], self._rgb[1], self._rgb[2])
return self._rgb
@property @property
def color_temp(self): def color_temp(self):
"""Return the color temperature.""" """Return the color temperature."""
o_temp = self._light.temp() return self._temperature
temperature = int(TEMP_MIN_HASS + (TEMP_MAX_HASS - TEMP_MIN_HASS) *
(o_temp - TEMP_MIN) / (TEMP_MAX - TEMP_MIN))
return temperature
@property @property
def brightness(self): def brightness(self):
"""Brightness of this light between 0..255.""" """Brightness of this light between 0..255."""
return int(self._light.lum() * 2.55) _LOGGER.debug("brightness for light %s is: %s",
self._name, self._brightness)
return self._brightness
@property @property
def is_on(self): def is_on(self):
"""Update Status to True if device is on.""" """Update Status to True if device is on."""
self.update_lights()
_LOGGER.debug("is_on light state for light: %s is: %s", _LOGGER.debug("is_on light state for light: %s is: %s",
self._light.name(), self._light.on()) self._name, self._state)
return self._light.on() return self._state
@property @property
def supported_features(self): def supported_features(self):
@ -131,47 +138,86 @@ class OsramLightifyLight(Light):
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
"""Turn the device on.""" """Turn the device on."""
brightness = 100 _LOGGER.debug("turn_on Attempting to turn on light: %s ",
if self.brightness: self._name)
brightness = int(self.brightness / 2.55)
self._light.set_onoff(1)
self._state = self._light.on()
if ATTR_TRANSITION in kwargs: if ATTR_TRANSITION in kwargs:
fade = kwargs[ATTR_TRANSITION] * 10 transition = kwargs[ATTR_TRANSITION] * 10
_LOGGER.debug("turn_on requested transition time for light:"
" %s is: %s ",
self._name, transition)
else: else:
fade = 0 transition = 0
_LOGGER.debug("turn_on requested transition time for light:"
" %s is: %s ",
self._name, transition)
if ATTR_RGB_COLOR in kwargs: if ATTR_RGB_COLOR in kwargs:
red, green, blue = kwargs[ATTR_RGB_COLOR] red, green, blue = kwargs[ATTR_RGB_COLOR]
self._light.set_rgb(red, green, blue, fade) _LOGGER.debug("turn_on requested ATTR_RGB_COLOR for light:"
" %s is: %s %s %s ",
if ATTR_BRIGHTNESS in kwargs: self._name, red, green, blue)
brightness = int(kwargs[ATTR_BRIGHTNESS] / 2.55) self._light.set_rgb(red, green, blue, transition)
if ATTR_COLOR_TEMP in kwargs: if ATTR_COLOR_TEMP in kwargs:
color_t = kwargs[ATTR_COLOR_TEMP] color_t = kwargs[ATTR_COLOR_TEMP]
kelvin = int(((TEMP_MAX - TEMP_MIN) * (color_t - TEMP_MIN_HASS) / kelvin = int(((TEMP_MAX - TEMP_MIN) * (color_t - TEMP_MIN_HASS) /
(TEMP_MAX_HASS - TEMP_MIN_HASS)) + TEMP_MIN) (TEMP_MAX_HASS - TEMP_MIN_HASS)) + TEMP_MIN)
self._light.set_temperature(kelvin, fade) _LOGGER.debug("turn_on requested set_temperature for light:"
" %s: %s ", self._name, kelvin)
self._light.set_temperature(kelvin, transition)
if ATTR_BRIGHTNESS in kwargs:
self._brightness = kwargs[ATTR_BRIGHTNESS]
_LOGGER.debug("turn_on requested brightness for light: %s is: %s ",
self._name, self._brightness)
self._brightness = self._light.set_luminance(
int(self._brightness / 2.55),
transition)
if ATTR_EFFECT in kwargs:
effect = kwargs.get(ATTR_EFFECT) effect = kwargs.get(ATTR_EFFECT)
if effect == EFFECT_RANDOM: if effect == EFFECT_RANDOM:
self._light.set_rgb(random.randrange(0, 255), self._light.set_rgb(random.randrange(0, 255),
random.randrange(0, 255), random.randrange(0, 255),
random.randrange(0, 255), random.randrange(0, 255),
fade) transition)
_LOGGER.debug("turn_on requested random effect for light:"
" %s with transition %s ",
self._name, transition)
self._light.set_luminance(brightness, fade) self.schedule_update_ha_state()
self.update_ha_state()
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Turn the device off.""" """Turn the device off."""
_LOGGER.debug("turn_off Attempting to turn off light: %s ",
self._name)
if ATTR_TRANSITION in kwargs: if ATTR_TRANSITION in kwargs:
fade = kwargs[ATTR_TRANSITION] * 10 transition = kwargs[ATTR_TRANSITION] * 10
_LOGGER.debug("turn_off requested transition time for light:"
" %s is: %s ",
self._name, transition)
self._light.set_luminance(0, transition)
else: else:
fade = 0 transition = 0
self._light.set_luminance(0, fade) _LOGGER.debug("turn_off requested transition time for light:"
self.update_ha_state() " %s is: %s ",
self._name, transition)
self._light.set_onoff(0)
self._state = self._light.on()
self.schedule_update_ha_state()
def update(self): def update(self):
"""Synchronize state with bridge.""" """Synchronize state with bridge."""
self.update_lights(no_throttle=True) self.update_lights(no_throttle=True)
self._brightness = int(self._light.lum() * 2.55)
self._name = self._light.name()
self._rgb = self._light.rgb()
o_temp = self._light.temp()
self._temperature = int(TEMP_MIN_HASS + (TEMP_MAX_HASS - TEMP_MIN_HASS)
* (o_temp - TEMP_MIN) / (TEMP_MAX - TEMP_MIN))
self._state = self._light.on()

View file

@ -84,7 +84,7 @@ class SCSGateLight(Light):
ToggleStatusTask(target=self._scs_id, toggled=True)) ToggleStatusTask(target=self._scs_id, toggled=True))
self._toggled = True self._toggled = True
self.update_ha_state() self.schedule_update_ha_state()
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Turn the device off.""" """Turn the device off."""
@ -94,7 +94,7 @@ class SCSGateLight(Light):
ToggleStatusTask(target=self._scs_id, toggled=False)) ToggleStatusTask(target=self._scs_id, toggled=False))
self._toggled = False self._toggled = False
self.update_ha_state() self.schedule_update_ha_state()
def process_event(self, message): def process_event(self, message):
"""Handle a SCSGate message related with this light.""" """Handle a SCSGate message related with this light."""

View file

@ -6,14 +6,14 @@ https://home-assistant.io/components/light.tellstick/
""" """
import voluptuous as vol import voluptuous as vol
from homeassistant.components import tellstick
from homeassistant.components.light import (ATTR_BRIGHTNESS, from homeassistant.components.light import (ATTR_BRIGHTNESS,
SUPPORT_BRIGHTNESS, Light) SUPPORT_BRIGHTNESS, Light)
from homeassistant.components.tellstick import (DEFAULT_SIGNAL_REPETITIONS, from homeassistant.components.tellstick import (DEFAULT_SIGNAL_REPETITIONS,
ATTR_DISCOVER_DEVICES, ATTR_DISCOVER_DEVICES,
ATTR_DISCOVER_CONFIG) ATTR_DISCOVER_CONFIG,
DOMAIN, TellstickDevice)
PLATFORM_SCHEMA = vol.Schema({vol.Required("platform"): tellstick.DOMAIN}) PLATFORM_SCHEMA = vol.Schema({vol.Required("platform"): DOMAIN})
SUPPORT_TELLSTICK = SUPPORT_BRIGHTNESS SUPPORT_TELLSTICK = SUPPORT_BRIGHTNESS
@ -22,32 +22,25 @@ SUPPORT_TELLSTICK = SUPPORT_BRIGHTNESS
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Tellstick lights.""" """Setup Tellstick lights."""
if (discovery_info is None or if (discovery_info is None or
discovery_info[ATTR_DISCOVER_DEVICES] is None or discovery_info[ATTR_DISCOVER_DEVICES] is None):
tellstick.TELLCORE_REGISTRY is None):
return return
# Allow platform level override, fallback to module config
signal_repetitions = discovery_info.get(ATTR_DISCOVER_CONFIG, signal_repetitions = discovery_info.get(ATTR_DISCOVER_CONFIG,
DEFAULT_SIGNAL_REPETITIONS) DEFAULT_SIGNAL_REPETITIONS)
add_devices(TellstickLight( add_devices(TellstickLight(tellcore_id, signal_repetitions)
tellstick.TELLCORE_REGISTRY.get_device(switch_id), signal_repetitions) for tellcore_id in discovery_info[ATTR_DISCOVER_DEVICES])
for switch_id in discovery_info[ATTR_DISCOVER_DEVICES])
class TellstickLight(tellstick.TellstickDevice, Light): class TellstickLight(TellstickDevice, Light):
"""Representation of a Tellstick light.""" """Representation of a Tellstick light."""
def __init__(self, tellstick_device, signal_repetitions): def __init__(self, tellcore_id, signal_repetitions):
"""Initialize the light.""" """Initialize the light."""
self._brightness = 255 super().__init__(tellcore_id, signal_repetitions)
tellstick.TellstickDevice.__init__(self,
tellstick_device,
signal_repetitions)
@property self._brightness = 255
def is_on(self):
"""Return true if switch is on."""
return self._state
@property @property
def brightness(self): def brightness(self):
@ -59,37 +52,32 @@ class TellstickLight(tellstick.TellstickDevice, Light):
"""Flag supported features.""" """Flag supported features."""
return SUPPORT_TELLSTICK return SUPPORT_TELLSTICK
def set_tellstick_state(self, last_command_sent, last_data_sent): def _parse_ha_data(self, kwargs):
"""Update the internal representation of the switch.""" """Turn the value from HA into something useful."""
from tellcore.constants import TELLSTICK_TURNON, TELLSTICK_DIM return kwargs.get(ATTR_BRIGHTNESS)
if last_command_sent == TELLSTICK_DIM:
if last_data_sent is not None:
self._brightness = int(last_data_sent)
self._state = self._brightness > 0
else:
self._state = last_command_sent == TELLSTICK_TURNON
def _send_tellstick_command(self, command, data): def _parse_tellcore_data(self, tellcore_data):
"""Handle the turn_on / turn_off commands.""" """Turn the value recieved from tellcore into something useful."""
from tellcore.constants import (TELLSTICK_TURNOFF, TELLSTICK_DIM) if tellcore_data is not None:
if command == TELLSTICK_TURNOFF: brightness = int(tellcore_data)
self.tellstick_device.turn_off() return brightness
elif command == TELLSTICK_DIM:
self.tellstick_device.dim(self._brightness)
else: else:
raise NotImplementedError( return None
"Command not implemented: {}".format(command))
def turn_on(self, **kwargs): def _update_model(self, new_state, data):
"""Turn the switch on.""" """Update the device entity state to match the arguments."""
from tellcore.constants import TELLSTICK_DIM if new_state:
brightness = kwargs.get(ATTR_BRIGHTNESS) brightness = data
if brightness is not None: if brightness is not None:
self._brightness = brightness self._brightness = brightness
self.call_tellstick(TELLSTICK_DIM, self._brightness) self._state = (self._brightness > 0)
else:
self._state = False
def turn_off(self, **kwargs): def _send_tellstick_command(self):
"""Turn the switch off.""" """Let tellcore update the device to match the current state."""
from tellcore.constants import TELLSTICK_TURNOFF if self._state:
self.call_tellstick(TELLSTICK_TURNOFF) self._tellcore_device.dim(self._brightness)
else:
self._tellcore_device.turn_off()

View file

@ -53,13 +53,13 @@ class VeraLight(VeraDevice, Light):
self.vera_device.switch_on() self.vera_device.switch_on()
self._state = STATE_ON self._state = STATE_ON
self.update_ha_state(True) self.schedule_update_ha_state(True)
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
"""Turn the light off.""" """Turn the light off."""
self.vera_device.switch_off() self.vera_device.switch_off()
self._state = STATE_OFF self._state = STATE_OFF
self.update_ha_state() self.schedule_update_ha_state()
@property @property
def is_on(self): def is_on(self):

View file

@ -23,15 +23,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Wink lights.""" """Setup the Wink lights."""
import pywink import pywink
add_devices(WinkLight(light) for light in pywink.get_bulbs()) add_devices(WinkLight(light, hass) for light in pywink.get_bulbs())
class WinkLight(WinkDevice, Light): class WinkLight(WinkDevice, Light):
"""Representation of a Wink light.""" """Representation of a Wink light."""
def __init__(self, wink): def __init__(self, wink, hass):
"""Initialize the Wink device.""" """Initialize the Wink device."""
WinkDevice.__init__(self, wink) WinkDevice.__init__(self, wink, hass)
@property @property
def is_on(self): def is_on(self):

View file

@ -15,15 +15,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Wink platform.""" """Setup the Wink platform."""
import pywink import pywink
add_devices(WinkLockDevice(lock) for lock in pywink.get_locks()) add_devices(WinkLockDevice(lock, hass) for lock in pywink.get_locks())
class WinkLockDevice(WinkDevice, LockDevice): class WinkLockDevice(WinkDevice, LockDevice):
"""Representation of a Wink lock.""" """Representation of a Wink lock."""
def __init__(self, wink): def __init__(self, wink, hass):
"""Initialize the lock.""" """Initialize the lock."""
WinkDevice.__init__(self, wink) WinkDevice.__init__(self, wink, hass)
@property @property
def is_locked(self): def is_locked(self):

View file

@ -101,7 +101,7 @@ def setup(hass, config):
message = message.async_render() message = message.async_render()
async_log_entry(hass, name, message, domain, entity_id) async_log_entry(hass, name, message, domain, entity_id)
hass.http.register_view(LogbookView(hass, config)) hass.http.register_view(LogbookView(config))
register_built_in_panel(hass, 'logbook', 'Logbook', register_built_in_panel(hass, 'logbook', 'Logbook',
'mdi:format-list-bulleted-type') 'mdi:format-list-bulleted-type')
@ -118,9 +118,8 @@ class LogbookView(HomeAssistantView):
name = 'api:logbook' name = 'api:logbook'
extra_urls = ['/api/logbook/{datetime}'] extra_urls = ['/api/logbook/{datetime}']
def __init__(self, hass, config): def __init__(self, config):
"""Initilalize the logbook view.""" """Initilalize the logbook view."""
super().__init__(hass)
self.config = config self.config = config
@asyncio.coroutine @asyncio.coroutine
@ -140,13 +139,15 @@ class LogbookView(HomeAssistantView):
def get_results(): def get_results():
"""Query DB for results.""" """Query DB for results."""
events = recorder.get_model('Events') events = recorder.get_model('Events')
query = recorder.query('Events').filter( query = recorder.query('Events').order_by(
events.time_fired).filter(
(events.time_fired > start_day) & (events.time_fired > start_day) &
(events.time_fired < end_day)) (events.time_fired < end_day))
events = recorder.execute(query) events = recorder.execute(query)
return _exclude_events(events, self.config) return _exclude_events(events, self.config)
events = yield from self.hass.loop.run_in_executor(None, get_results) events = yield from request.app['hass'].loop.run_in_executor(
None, get_results)
return self.json(humanify(events)) return self.json(humanify(events))

View file

@ -17,7 +17,8 @@ from homeassistant.config import load_yaml_config_file
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.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe from homeassistant.util.async import run_coroutine_threadsafe
from homeassistant.const import ( from homeassistant.const import (
@ -58,6 +59,8 @@ ATTR_MEDIA_SEEK_POSITION = 'seek_position'
ATTR_MEDIA_CONTENT_ID = 'media_content_id' ATTR_MEDIA_CONTENT_ID = 'media_content_id'
ATTR_MEDIA_CONTENT_TYPE = 'media_content_type' ATTR_MEDIA_CONTENT_TYPE = 'media_content_type'
ATTR_MEDIA_DURATION = 'media_duration' ATTR_MEDIA_DURATION = 'media_duration'
ATTR_MEDIA_POSITION = 'media_position'
ATTR_MEDIA_POSITION_UPDATED_AT = 'media_position_updated_at'
ATTR_MEDIA_TITLE = 'media_title' ATTR_MEDIA_TITLE = 'media_title'
ATTR_MEDIA_ARTIST = 'media_artist' ATTR_MEDIA_ARTIST = 'media_artist'
ATTR_MEDIA_ALBUM_NAME = 'media_album_name' ATTR_MEDIA_ALBUM_NAME = 'media_album_name'
@ -119,6 +122,8 @@ ATTR_TO_PROPERTY = [
ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_ID,
ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_CONTENT_TYPE,
ATTR_MEDIA_DURATION, ATTR_MEDIA_DURATION,
ATTR_MEDIA_POSITION,
ATTR_MEDIA_POSITION_UPDATED_AT,
ATTR_MEDIA_TITLE, ATTR_MEDIA_TITLE,
ATTR_MEDIA_ARTIST, ATTR_MEDIA_ARTIST,
ATTR_MEDIA_ALBUM_NAME, ATTR_MEDIA_ALBUM_NAME,
@ -304,7 +309,7 @@ def setup(hass, config):
component = EntityComponent( component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
hass.http.register_view(MediaPlayerImageView(hass, component.entities)) hass.http.register_view(MediaPlayerImageView(component.entities))
component.setup(config) component.setup(config)
@ -446,6 +451,19 @@ class MediaPlayerDevice(Entity):
"""Duration of current playing media in seconds.""" """Duration of current playing media in seconds."""
return None return None
@property
def media_position(self):
"""Position of current playing media in seconds."""
return None
@property
def media_position_updated_at(self):
"""When was the position of the current playing media valid.
Returns value from homeassistant.util.dt.utcnow().
"""
return None
@property @property
def media_image_url(self): def media_image_url(self):
"""Image url of current playing media.""" """Image url of current playing media."""
@ -704,17 +722,25 @@ def _async_fetch_image(hass, url):
return cache_images[url] return cache_images[url]
content, content_type = (None, None) content, content_type = (None, None)
websession = async_get_clientsession(hass)
response = None
try: try:
with async_timeout.timeout(10, loop=hass.loop): with async_timeout.timeout(10, loop=hass.loop):
response = yield from hass.websession.get(url) response = yield from websession.get(url)
if response.status == 200: if response.status == 200:
content = yield from response.read() content = yield from response.read()
content_type = response.headers.get(CONTENT_TYPE_HEADER) content_type = response.headers.get(CONTENT_TYPE_HEADER)
yield from response.release()
except asyncio.TimeoutError: except asyncio.TimeoutError:
pass pass
if content: finally:
if response is not None:
yield from response.release()
if not content:
return (None, None)
cache_images[url] = (content, content_type) cache_images[url] = (content, content_type)
cache_urls.append(url) cache_urls.append(url)
@ -736,9 +762,8 @@ class MediaPlayerImageView(HomeAssistantView):
url = "/api/media_player_proxy/{entity_id}" url = "/api/media_player_proxy/{entity_id}"
name = "api:media_player:image" name = "api:media_player:image"
def __init__(self, hass, entities): def __init__(self, entities):
"""Initialize a media player view.""" """Initialize a media player view."""
super().__init__(hass)
self.entities = entities self.entities = entities
@asyncio.coroutine @asyncio.coroutine
@ -748,14 +773,14 @@ class MediaPlayerImageView(HomeAssistantView):
if player is None: if player is None:
return web.Response(status=404) return web.Response(status=404)
authenticated = (request.authenticated or authenticated = (request[KEY_AUTHENTICATED] or
request.GET.get('token') == player.access_token) request.GET.get('token') == player.access_token)
if not authenticated: if not authenticated:
return web.Response(status=401) return web.Response(status=401)
data, content_type = yield from _async_fetch_image( data, content_type = yield from _async_fetch_image(
self.hass, player.media_image_url) request.app['hass'], player.media_image_url)
if data is None: if data is None:
return web.Response(status=500) return web.Response(status=500)

View file

@ -81,6 +81,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
KNOWN_HOSTS.append(host) KNOWN_HOSTS.append(host)
except pychromecast.ChromecastConnectionError: except pychromecast.ChromecastConnectionError:
pass pass
else:
try:
# add the device anyway, get_chromecasts couldn't find it
casts.append(CastDevice(pychromecast.Chromecast(*host)))
KNOWN_HOSTS.append(host)
except pychromecast.ChromecastConnectionError:
pass
add_devices(casts) add_devices(casts)

33
homeassistant/components/media_player/denon.py Normal file → Executable file
View file

@ -10,8 +10,9 @@ import telnetlib
import voluptuous as vol import voluptuous as vol
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_SELECT_SOURCE,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF,
SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
MediaPlayerDevice) MediaPlayerDevice)
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN) CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN)
@ -21,8 +22,9 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Music station' DEFAULT_NAME = 'Music station'
SUPPORT_DENON = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_DENON = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_SELECT_SOURCE | SUPPORT_NEXT_TRACK | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF SUPPORT_TURN_ON | SUPPORT_TURN_OFF
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -51,6 +53,8 @@ class DenonDevice(MediaPlayerDevice):
self._host = host self._host = host
self._pwstate = 'PWSTANDBY' self._pwstate = 'PWSTANDBY'
self._volume = 0 self._volume = 0
self._source_list = {'TV': 'SITV', 'Tuner': 'SITUNER',
'Internet Radio': 'SIIRP', 'Favorites': 'SIFVP'}
self._muted = False self._muted = False
self._mediasource = '' self._mediasource = ''
@ -58,7 +62,14 @@ class DenonDevice(MediaPlayerDevice):
def telnet_request(cls, telnet, command): def telnet_request(cls, telnet, command):
"""Execute `command` and return the response.""" """Execute `command` and return the response."""
telnet.write(command.encode('ASCII') + b'\r') telnet.write(command.encode('ASCII') + b'\r')
return telnet.read_until(b'\r', timeout=0.2).decode('ASCII').strip() lines = []
while True:
line = telnet.read_until(b'\r', timeout=0.2)
if not line:
break
lines.append(line.decode('ASCII').strip())
return lines[0]
def telnet_command(self, command): def telnet_command(self, command):
"""Establish a telnet connection and sends `command`.""" """Establish a telnet connection and sends `command`."""
@ -75,9 +86,6 @@ class DenonDevice(MediaPlayerDevice):
return False return False
self._pwstate = self.telnet_request(telnet, 'PW?') self._pwstate = self.telnet_request(telnet, 'PW?')
# PW? sends also SISTATUS, which is not interesting
telnet.read_until(b"\r", timeout=0.2)
volume_str = self.telnet_request(telnet, 'MV?')[len('MV'):] volume_str = self.telnet_request(telnet, 'MV?')[len('MV'):]
self._volume = int(volume_str) / 60 self._volume = int(volume_str) / 60
self._muted = (self.telnet_request(telnet, 'MU?') == 'MUON') self._muted = (self.telnet_request(telnet, 'MU?') == 'MUON')
@ -111,6 +119,11 @@ class DenonDevice(MediaPlayerDevice):
"""Boolean if volume is currently muted.""" """Boolean if volume is currently muted."""
return self._muted return self._muted
@property
def source_list(self):
"""List of available input sources."""
return list(self._source_list.keys())
@property @property
def media_title(self): def media_title(self):
"""Current media source.""" """Current media source."""
@ -161,3 +174,7 @@ class DenonDevice(MediaPlayerDevice):
def turn_on(self): def turn_on(self):
"""Turn the media player on.""" """Turn the media player on."""
self.telnet_command('PWON') self.telnet_command('PWON')
def select_source(self, source):
"""Select input source."""
self.telnet_command(self._source_list.get(source))

View file

@ -0,0 +1,245 @@
"""
Support for Denon AVR receivers using their HTTP interface.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.denon/
"""
import logging
import voluptuous as vol
from homeassistant.components.media_player import (
SUPPORT_PAUSE, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, MEDIA_TYPE_CHANNEL,
MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_TURN_ON,
MEDIA_TYPE_MUSIC)
from homeassistant.const import (
CONF_HOST, STATE_OFF, STATE_PLAYING, STATE_PAUSED,
CONF_NAME, STATE_ON)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['denonavr==0.1.6']
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = None
SUPPORT_DENON = SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \
SUPPORT_SELECT_SOURCE | SUPPORT_PLAY_MEDIA | \
SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Denon platform."""
import denonavr
receiver = denonavr.DenonAVR(config.get(CONF_HOST), config.get(CONF_NAME))
add_devices([DenonDevice(receiver)])
_LOGGER.info("Denon receiver at host %s initialized",
config.get(CONF_HOST))
class DenonDevice(MediaPlayerDevice):
"""Representation of a Denon Media Player Device."""
def __init__(self, receiver):
"""Initialize the device."""
self._receiver = receiver
self._name = self._receiver.name
self._muted = self._receiver.muted
self._volume = self._receiver.volume
self._current_source = self._receiver.input_func
self._source_list = self._receiver.input_func_list
self._state = self._receiver.state
self._power = self._receiver.power
self._media_image_url = self._receiver.image_url
self._title = self._receiver.title
self._artist = self._receiver.artist
self._album = self._receiver.album
self._band = self._receiver.band
self._frequency = self._receiver.frequency
self._station = self._receiver.station
def update(self):
"""Get the latest status information from device."""
# Update denonavr
self._receiver.update()
# Refresh own data
self._name = self._receiver.name
self._muted = self._receiver.muted
self._volume = self._receiver.volume
self._current_source = self._receiver.input_func
self._source_list = self._receiver.input_func_list
self._state = self._receiver.state
self._power = self._receiver.power
self._media_image_url = self._receiver.image_url
self._title = self._receiver.title
self._artist = self._receiver.artist
self._album = self._receiver.album
self._band = self._receiver.band
self._frequency = self._receiver.frequency
self._station = self._receiver.station
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return self._muted
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
# Volume is send in a format like -50.0. Minimum is around -80.0
return (float(self._volume) + 80) / 100
@property
def source(self):
"""Return the current input source."""
return self._current_source
@property
def source_list(self):
"""List of available input sources."""
return self._source_list
@property
def supported_media_commands(self):
"""Flag of media commands that are supported."""
return SUPPORT_DENON
@property
def media_content_id(self):
"""Content ID of current playing media."""
return None
@property
def media_content_type(self):
"""Content type of current playing media."""
if self._state == STATE_PLAYING or self._state == STATE_PAUSED:
return MEDIA_TYPE_MUSIC
else:
return MEDIA_TYPE_CHANNEL
@property
def media_duration(self):
"""Duration of current playing media in seconds."""
return None
@property
def media_image_url(self):
"""Image url of current playing media."""
if self._power == "ON":
return self._media_image_url
else:
return None
@property
def media_title(self):
"""Title of current playing media."""
if self._title is not None:
return self._title
else:
return self._frequency
@property
def media_artist(self):
"""Artist of current playing media, music track only."""
if self._artist is not None:
return self._artist
else:
return self._band
@property
def media_album_name(self):
"""Album name of current playing media, music track only."""
if self._album is not None:
return self._album
else:
return self._station
@property
def media_album_artist(self):
"""Album artist of current playing media, music track only."""
return None
@property
def media_track(self):
"""Track number of current playing media, music track only."""
return None
@property
def media_series_title(self):
"""Title of series of current playing media, TV show only."""
return None
@property
def media_season(self):
"""Season of current playing media, TV show only."""
return None
@property
def media_episode(self):
"""Episode of current playing media, TV show only."""
return None
def media_play_pause(self):
"""Simulate play pause media player."""
return self._receiver.toggle_play_pause()
def media_previous_track(self):
"""Send previous track command."""
return self._receiver.previous_track()
def media_next_track(self):
"""Send next track command."""
return self._receiver.next_track()
def select_source(self, source):
"""Select input source."""
return self._receiver.set_input_func(source)
def turn_on(self):
"""Turn on media player."""
if self._receiver.power_on():
self._state = STATE_ON
return True
else:
return False
def turn_off(self):
"""Turn off media player."""
if self._receiver.power_off():
self._state = STATE_OFF
return True
else:
return False
def volume_up(self):
"""Volume up the media player."""
return self._receiver.volume_up()
def volume_down(self):
"""Volume down media player."""
return self._receiver.volume_down()
def mute_volume(self, mute):
"""Send mute command."""
return self._receiver.mute(mute)

View file

@ -0,0 +1,171 @@
"""
DuneHD implementation of the media player.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/media_player.dunehd/
"""
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.media_player import (
SUPPORT_PAUSE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_NEXT_TRACK,
SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, PLATFORM_SCHEMA,
MediaPlayerDevice)
from homeassistant.const import (
CONF_HOST, CONF_NAME, STATE_OFF, STATE_PAUSED, STATE_ON, STATE_PLAYING)
REQUIREMENTS = ['pdunehd==1.3']
DEFAULT_NAME = 'DuneHD'
CONF_SOURCES = 'sources'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_SOURCES): cv.ordered_dict(cv.string, cv.string),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
DUNEHD_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \
SUPPORT_SELECT_SOURCE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the media player demo platform."""
sources = config.get(CONF_SOURCES, {})
from pdunehd import DuneHDPlayer
add_devices([DuneHDPlayerEntity(
DuneHDPlayer(config[CONF_HOST]),
config[CONF_NAME],
sources)])
class DuneHDPlayerEntity(MediaPlayerDevice):
"""Implementation of the Dune HD player."""
def __init__(self, player, name, sources):
"""Setup entity to control Dune HD."""
self._player = player
self._name = name
self._sources = sources
self._media_title = None
self._state = None
self.update()
def update(self):
"""Update internal status of the entity."""
self._state = self._player.update_state()
self.__update_title()
return True
@property
def state(self):
"""Return player state."""
state = STATE_OFF
if 'playback_position' in self._state:
state = STATE_PLAYING
if self._state['player_state'] in ('playing', 'buffering'):
state = STATE_PLAYING
if int(self._state.get('playback_speed', 1234)) == 0:
state = STATE_PAUSED
if self._state['player_state'] == 'navigator':
state = STATE_ON
return state
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
return int(self._state.get('playback_volume', 0)) / 100
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return int(self._state.get('playback_mute', 0)) == 1
@property
def source_list(self):
"""List of available input sources."""
return list(self._sources.keys())
@property
def supported_media_commands(self):
"""Flag of media commands that are supported."""
return DUNEHD_PLAYER_SUPPORT
def volume_up(self):
"""Volume up media player."""
self._state = self._player.volume_up()
def volume_down(self):
"""Volume down media player."""
self._state = self._player.volume_down()
def mute_volume(self, mute):
"""Mute/unmute player volume."""
self._state = self._player.mute(mute)
def turn_off(self):
"""Turn off media player."""
self._media_title = None
self._state = self._player.turn_off()
self.schedule_update_ha_state()
def turn_on(self):
"""Turn off media player."""
self._state = self._player.turn_on()
self.schedule_update_ha_state()
def media_play(self):
"""Play media media player."""
self._state = self._player.play()
self.schedule_update_ha_state()
def media_pause(self):
"""Pause media player."""
self._state = self._player.pause()
self.schedule_update_ha_state()
@property
def media_title(self):
"""Current media source."""
self.__update_title()
if self._media_title:
return self._media_title
return self._state.get('playback_url', 'Not playing')
def __update_title(self):
if self._state['player_state'] == 'bluray_playback':
self._media_title = 'Blu-Ray'
elif 'playback_url' in self._state:
sources = self._sources
sval = sources.values()
skey = sources.keys()
pburl = self._state['playback_url']
if pburl in sval:
self._media_title = list(skey)[list(sval).index(pburl)]
else:
self._media_title = pburl
def select_source(self, source):
"""Select input source."""
self._media_title = source
self._state = self._player.launch_media_url(self._sources.get(source))
self.schedule_update_ha_state()
def media_previous_track(self):
"""Send previous track command."""
self._state = self._player.previous_track()
self.schedule_update_ha_state()
def media_next_track(self):
"""Send next track command."""
self._state = self._player.next_track()
self.schedule_update_ha_state()

View file

@ -100,7 +100,7 @@ class MpdDevice(MediaPlayerDevice):
try: try:
self.status = self.client.status() self.status = self.client.status()
self.currentsong = self.client.currentsong() self.currentsong = self.client.currentsong()
except (mpd.ConnectionError, BrokenPipeError, ValueError): except (mpd.ConnectionError, OSError, BrokenPipeError, ValueError):
# Cleanly disconnect in case connection is not in valid state # Cleanly disconnect in case connection is not in valid state
try: try:
self.client.disconnect() self.client.disconnect()
@ -133,7 +133,7 @@ class MpdDevice(MediaPlayerDevice):
@property @property
def media_content_id(self): def media_content_id(self):
"""Content ID of current playing media.""" """Content ID of current playing media."""
return self.currentsong['id'] return self.currentsong.get('file')
@property @property
def media_content_type(self): def media_content_type(self):

View file

@ -120,7 +120,7 @@ class PandoraMediaPlayer(MediaPlayerDevice):
self.update_playing_status() self.update_playing_status()
self._player_state = STATE_IDLE self._player_state = STATE_IDLE
self.update_ha_state() self.schedule_update_ha_state()
def turn_off(self): def turn_off(self):
"""Turn the media player off.""" """Turn the media player off."""
@ -138,24 +138,24 @@ class PandoraMediaPlayer(MediaPlayerDevice):
_LOGGER.info('Killed Pianobar subprocess') _LOGGER.info('Killed Pianobar subprocess')
self._pianobar = None self._pianobar = None
self._player_state = STATE_OFF self._player_state = STATE_OFF
self.update_ha_state() self.schedule_update_ha_state()
def media_play(self): def media_play(self):
"""Send play command.""" """Send play command."""
self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE) self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE)
self._player_state = STATE_PLAYING self._player_state = STATE_PLAYING
self.update_ha_state() self.schedule_update_ha_state()
def media_pause(self): def media_pause(self):
"""Send pause command.""" """Send pause command."""
self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE) self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE)
self._player_state = STATE_PAUSED self._player_state = STATE_PAUSED
self.update_ha_state() self.schedule_update_ha_state()
def media_next_track(self): def media_next_track(self):
"""Go to next track.""" """Go to next track."""
self._send_pianobar_command(SERVICE_MEDIA_NEXT_TRACK) self._send_pianobar_command(SERVICE_MEDIA_NEXT_TRACK)
self.update_ha_state() self.schedule_update_ha_state()
@property @property
def supported_media_commands(self): def supported_media_commands(self):
@ -350,6 +350,8 @@ class PandoraMediaPlayer(MediaPlayerDevice):
pass pass
except pexpect.exceptions.TIMEOUT: except pexpect.exceptions.TIMEOUT:
pass pass
except pexpect.exceptions.EOF:
pass
def _pianobar_exists(): def _pianobar_exists():

View file

@ -9,13 +9,14 @@ from datetime import timedelta
import voluptuous as vol import voluptuous as vol
from homeassistant.components.media_player import (
PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF,
SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_MUTE, MediaPlayerDevice)
from homeassistant.const import (
STATE_ON, STATE_OFF, STATE_UNKNOWN, CONF_HOST, CONF_NAME)
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.media_player import (
PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK,
SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_STEP, MediaPlayerDevice)
from homeassistant.const import (
CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN)
from homeassistant.util import Throttle
REQUIREMENTS = ['ha-philipsjs==0.0.1'] REQUIREMENTS = ['ha-philipsjs==0.0.1']
@ -26,6 +27,9 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
SUPPORT_PHILIPS_JS = SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | \ SUPPORT_PHILIPS_JS = SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | \
SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE
SUPPORT_PHILIPS_JS_TV = SUPPORT_PHILIPS_JS | SUPPORT_NEXT_TRACK | \
SUPPORT_PREVIOUS_TRACK
DEFAULT_DEVICE = 'default' DEFAULT_DEVICE = 'default'
DEFAULT_HOST = '127.0.0.1' DEFAULT_HOST = '127.0.0.1'
DEFAULT_NAME = 'Philips TV' DEFAULT_NAME = 'Philips TV'
@ -68,6 +72,8 @@ class PhilipsTV(MediaPlayerDevice):
self._source_list = [] self._source_list = []
self._connfail = 0 self._connfail = 0
self._source_mapping = {} self._source_mapping = {}
self._watching_tv = None
self._channel_name = None
@property @property
def name(self): def name(self):
@ -82,6 +88,9 @@ class PhilipsTV(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."""
if self._watching_tv:
return SUPPORT_PHILIPS_JS_TV
else:
return SUPPORT_PHILIPS_JS return SUPPORT_PHILIPS_JS
@property @property
@ -106,6 +115,7 @@ class PhilipsTV(MediaPlayerDevice):
self._source = source self._source = source
if not self._tv.on: if not self._tv.on:
self._state = STATE_OFF self._state = STATE_OFF
self._watching_tv = bool(self._tv.source_id == 'tv')
@property @property
def volume_level(self): def volume_level(self):
@ -141,9 +151,23 @@ class PhilipsTV(MediaPlayerDevice):
if not self._tv.on: if not self._tv.on:
self._state = STATE_OFF self._state = STATE_OFF
def media_previous_track(self):
"""Send rewind commmand."""
self._tv.sendKey('Previous')
def media_next_track(self):
"""Send fast forward commmand."""
self._tv.sendKey('Next')
@property @property
def media_title(self): def media_title(self):
"""Title of current playing media.""" """Title of current playing media."""
if self._watching_tv:
if self._channel_name:
return '{} - {}'.format(self._source, self._channel_name)
else:
return self._source
else:
return self._source return self._source
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
@ -167,3 +191,12 @@ class PhilipsTV(MediaPlayerDevice):
self._state = STATE_ON self._state = STATE_ON
else: else:
self._state = STATE_OFF self._state = STATE_OFF
self._watching_tv = bool(self._tv.source_id == 'tv')
self._tv.getChannelId()
self._tv.getChannels()
if self._tv.channels and self._tv.channel_id in self._tv.channels:
self._channel_name = self._tv.channels[self._tv.channel_id]['name']
else:
self._channel_name = None

View file

@ -204,3 +204,36 @@ sonos_clear_sleep_timer:
entity_id: entity_id:
description: Name(s) of entites that will have the timer cleared. description: Name(s) of entites that will have the timer cleared.
example: 'media_player.living_room_sonos' example: 'media_player.living_room_sonos'
soundtouch_play_everywhere:
description: Play on all Bose Soundtouch devices
fields:
entity_id:
description: Name of entites that will coordinate the grouping. Platform dependent. It is a shortcut for creating a multi-room zone with all devices
example: 'media_player.soundtouch_home'
soundtouch_create_zone:
description: Create a multi-room zone
fields:
entity_id:
description: Name of entites that will coordinate the multi-room zone. Platform dependent.
example: 'media_player.soundtouch_home'
soundtouch_add_zone_slave:
description: Add a slave to a multi-room zone
fields:
entity_id:
description: Name of entites that will be added to the multi-room zone. Platform dependent.
example: 'media_player.soundtouch_home'
soundtouch_remove_zone_slave:
description: Remove a slave from the multi-room zone
fields:
entity_id:
description: Name of entites that will be remove from the multi-room zone. Platform dependent.
example: 'media_player.soundtouch_home'

View file

@ -15,11 +15,13 @@ from homeassistant.components.media_player import (
ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice) SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA)
from homeassistant.const import ( from homeassistant.const import (
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF, ATTR_ENTITY_ID) STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF, ATTR_ENTITY_ID,
CONF_HOSTS)
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.dt import utcnow
REQUIREMENTS = ['SoCo==0.12'] REQUIREMENTS = ['SoCo==0.12']
@ -48,9 +50,18 @@ SERVICE_CLEAR_TIMER = 'sonos_clear_sleep_timer'
SUPPORT_SOURCE_LINEIN = 'Line-in' SUPPORT_SOURCE_LINEIN = 'Line-in'
SUPPORT_SOURCE_TV = 'TV' SUPPORT_SOURCE_TV = 'TV'
CONF_ADVERTISE_ADDR = 'advertise_addr'
CONF_INTERFACE_ADDR = 'interface_addr'
# Service call validation schemas # Service call validation schemas
ATTR_SLEEP_TIME = 'sleep_time' ATTR_SLEEP_TIME = 'sleep_time'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_ADVERTISE_ADDR): cv.string,
vol.Optional(CONF_INTERFACE_ADDR): cv.string,
vol.Optional(CONF_HOSTS): cv.ensure_list(cv.string),
})
SONOS_SCHEMA = vol.Schema({ SONOS_SCHEMA = vol.Schema({
ATTR_ENTITY_ID: cv.entity_ids, ATTR_ENTITY_ID: cv.entity_ids,
}) })
@ -69,6 +80,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
import soco import soco
global DEVICES global DEVICES
advertise_addr = config.get(CONF_ADVERTISE_ADDR, None)
if advertise_addr:
soco.config.EVENT_ADVERTISE_IP = advertise_addr
if discovery_info: if discovery_info:
player = soco.SoCo(discovery_info) player = soco.SoCo(discovery_info)
@ -86,18 +101,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return False return False
players = None players = None
hosts = config.get('hosts', None) hosts = config.get(CONF_HOSTS, None)
if hosts: if hosts:
# Support retro compatibility with comma separated list of hosts # Support retro compatibility with comma separated list of hosts
# from config # from config
hosts = hosts[0] if len(hosts) == 1 else hosts
hosts = hosts.split(',') if isinstance(hosts, str) else hosts hosts = hosts.split(',') if isinstance(hosts, str) else hosts
players = [] players = []
for host in hosts: for host in hosts:
players.append(soco.SoCo(socket.gethostbyname(host))) players.append(soco.SoCo(socket.gethostbyname(host)))
if not players: if not players:
players = soco.discover(interface_addr=config.get('interface_addr', players = soco.discover(interface_addr=config.get(CONF_INTERFACE_ADDR))
None))
if not players: if not players:
_LOGGER.warning('No Sonos speakers found.') _LOGGER.warning('No Sonos speakers found.')
@ -264,6 +279,8 @@ class SonosDevice(MediaPlayerDevice):
self._coordinator = None self._coordinator = None
self._media_content_id = None self._media_content_id = None
self._media_duration = None self._media_duration = None
self._media_position = None
self._media_position_updated_at = None
self._media_image_url = None self._media_image_url = None
self._media_artist = None self._media_artist = None
self._media_album_name = None self._media_album_name = None
@ -404,6 +421,9 @@ class SonosDevice(MediaPlayerDevice):
media_album_name = track_info.get('album') media_album_name = track_info.get('album')
media_title = track_info.get('title') media_title = track_info.get('title')
media_position = None
media_position_updated_at = None
is_radio_stream = \ is_radio_stream = \
current_media_uri.startswith('x-sonosapi-stream:') or \ current_media_uri.startswith('x-sonosapi-stream:') or \
current_media_uri.startswith('x-rincon-mp3radio:') current_media_uri.startswith('x-rincon-mp3radio:')
@ -425,7 +445,6 @@ class SonosDevice(MediaPlayerDevice):
media_image_url = None media_image_url = None
elif is_radio_stream: elif is_radio_stream:
is_radio_stream = True
media_image_url = self._format_media_image_url( media_image_url = self._format_media_image_url(
current_media_uri current_media_uri
) )
@ -489,6 +508,46 @@ class SonosDevice(MediaPlayerDevice):
support_next_track = True support_next_track = True
support_pause = True support_pause = True
position_info = self._player.avTransport.GetPositionInfo(
[('InstanceID', 0),
('Channel', 'Master')]
)
rel_time = _parse_timespan(
position_info.get("RelTime")
)
# player no longer reports position?
update_media_position = rel_time is None and \
self._media_position is not None
# player started reporting position?
update_media_position |= rel_time is not None and \
self._media_position is None
# position changed?
if rel_time is not None and \
self._media_position is not None:
time_diff = utcnow() - self._media_position_updated_at
time_diff = time_diff.total_seconds()
calculated_position = \
self._media_position + \
time_diff
update_media_position = \
abs(calculated_position - rel_time) > 1.5
if update_media_position:
media_position = rel_time
media_position_updated_at = utcnow()
else:
# don't update media_position (don't want unneeded
# state transitions)
media_position = self._media_position
media_position_updated_at = \
self._media_position_updated_at
playlist_position = track_info.get('playlist_position') playlist_position = track_info.get('playlist_position')
if playlist_position in ('', 'NOT_IMPLEMENTED', None): if playlist_position in ('', 'NOT_IMPLEMENTED', None):
playlist_position = None playlist_position = None
@ -514,6 +573,8 @@ class SonosDevice(MediaPlayerDevice):
self._media_duration = _parse_timespan( self._media_duration = _parse_timespan(
track_info.get('duration') track_info.get('duration')
) )
self._media_position = media_position
self._media_position_updated_at = media_position_updated_at
self._media_image_url = media_image_url self._media_image_url = media_image_url
self._media_artist = media_artist self._media_artist = media_artist
self._media_album_name = media_album_name self._media_album_name = media_album_name
@ -541,6 +602,8 @@ class SonosDevice(MediaPlayerDevice):
self._coordinator = None self._coordinator = None
self._media_content_id = None self._media_content_id = None
self._media_duration = None self._media_duration = None
self._media_position = None
self._media_position_updated_at = None
self._media_image_url = None self._media_image_url = None
self._media_artist = None self._media_artist = None
self._media_album_name = None self._media_album_name = None
@ -642,6 +705,25 @@ class SonosDevice(MediaPlayerDevice):
else: else:
return self._media_duration return self._media_duration
@property
def media_position(self):
"""Position of current playing media in seconds."""
if self._coordinator:
return self._coordinator.media_position
else:
return self._media_position
@property
def media_position_updated_at(self):
"""When was the position of the current playing media valid.
Returns value from homeassistant.util.dt.utcnow().
"""
if self._coordinator:
return self._coordinator.media_position_updated_at
else:
return self._media_position_updated_at
@property @property
def media_image_url(self): def media_image_url(self):
"""Image url of current playing media.""" """Image url of current playing media."""

View file

@ -0,0 +1,393 @@
"""Support for interface with a Bose Soundtouch."""
import logging
from os import path
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.media_player import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
SUPPORT_VOLUME_SET, SUPPORT_TURN_ON, MediaPlayerDevice, PLATFORM_SCHEMA)
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, CONF_PORT,
STATE_PAUSED, STATE_PLAYING,
STATE_UNAVAILABLE)
REQUIREMENTS = ['libsoundtouch==0.1.0']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'media_player'
SERVICE_PLAY_EVERYWHERE = 'soundtouch_play_everywhere'
SERVICE_CREATE_ZONE = 'soundtouch_create_zone'
SERVICE_ADD_ZONE_SLAVE = 'soundtouch_add_zone_slave'
SERVICE_REMOVE_ZONE_SLAVE = 'soundtouch_remove_zone_slave'
MAP_STATUS = {
"PLAY_STATE": STATE_PLAYING,
"BUFFERING_STATE": STATE_PLAYING,
"PAUSE_STATE": STATE_PAUSED,
"STOp_STATE": STATE_OFF
}
SOUNDTOUCH_PLAY_EVERYWHERE = vol.Schema({
'master': cv.entity_id,
})
SOUNDTOUCH_CREATE_ZONE_SCHEMA = vol.Schema({
'master': cv.entity_id,
'slaves': cv.entity_ids
})
SOUNDTOUCH_ADD_ZONE_SCHEMA = vol.Schema({
'master': cv.entity_id,
'slaves': cv.entity_ids
})
SOUNDTOUCH_REMOVE_ZONE_SCHEMA = vol.Schema({
'master': cv.entity_id,
'slaves': cv.entity_ids
})
DEFAULT_NAME = 'Bose Soundtouch'
DEFAULT_PORT = 8090
DEVICES = []
SUPPORT_SOUNDTOUCH = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | \
SUPPORT_VOLUME_SET | SUPPORT_TURN_ON
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Bose Soundtouch platform."""
name = config.get(CONF_NAME)
remote_config = {
'name': 'HomeAssistant',
'description': config.get(CONF_NAME),
'id': 'ha.component.soundtouch',
'port': config.get(CONF_PORT),
'host': config.get(CONF_HOST)
}
soundtouch_device = SoundTouchDevice(name, remote_config)
DEVICES.append(soundtouch_device)
add_devices([soundtouch_device])
descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_PLAY_EVERYWHERE,
play_everywhere_service,
descriptions.get(SERVICE_PLAY_EVERYWHERE),
schema=SOUNDTOUCH_PLAY_EVERYWHERE)
hass.services.register(DOMAIN, SERVICE_CREATE_ZONE,
create_zone_service,
descriptions.get(SERVICE_CREATE_ZONE),
schema=SOUNDTOUCH_CREATE_ZONE_SCHEMA)
hass.services.register(DOMAIN, SERVICE_REMOVE_ZONE_SLAVE,
remove_zone_slave,
descriptions.get(SERVICE_REMOVE_ZONE_SLAVE),
schema=SOUNDTOUCH_REMOVE_ZONE_SCHEMA)
hass.services.register(DOMAIN, SERVICE_ADD_ZONE_SLAVE,
add_zone_slave,
descriptions.get(SERVICE_ADD_ZONE_SLAVE),
schema=SOUNDTOUCH_ADD_ZONE_SCHEMA)
def play_everywhere_service(service):
"""
Create a zone (multi-room) and play on all devices.
:param service: Home Assistant service with 'master' data set
:Example:
- service: media_player.soundtouch_play_everywhere
data:
master: media_player.soundtouch_living_room
"""
master_device_id = service.data.get('master')
slaves = [d for d in DEVICES if d.entity_id != master_device_id]
master = next([device for device in DEVICES if
device.entity_id == master_device_id].__iter__(), None)
if master is None:
_LOGGER.warning(
"Unable to find master with entity_id:" + str(master_device_id))
elif not slaves:
_LOGGER.warning("Unable to create zone without slaves")
else:
_LOGGER.info(
"Creating zone with master " + str(master.device.config.name))
master.device.create_zone([slave.device for slave in slaves])
def create_zone_service(service):
"""
Create a zone (multi-room) on a master and play on specified slaves.
At least one master and one slave must be specified
:param service: Home Assistant service with 'master' and 'slaves' data set
:Example:
- service: media_player.soundtouch_create_zone
data:
master: media_player.soundtouch_living_room
slaves:
- media_player.soundtouch_room
- media_player.soundtouch_kitchen
"""
master_device_id = service.data.get('master')
slaves_ids = service.data.get('slaves')
slaves = [device for device in DEVICES if device.entity_id in slaves_ids]
master = next([device for device in DEVICES if
device.entity_id == master_device_id].__iter__(), None)
if master is None:
_LOGGER.warning(
"Unable to find master with entity_id:" + master_device_id)
elif not slaves:
_LOGGER.warning("Unable to create zone without slaves")
else:
_LOGGER.info(
"Creating zone with master " + str(master.device.config.name))
master.device.create_zone([slave.device for slave in slaves])
def add_zone_slave(service):
"""
Add slave(s) to and existing zone (multi-room).
Zone must already exist and slaves array can not be empty.
:param service: Home Assistant service with 'master' and 'slaves' data set
:Example:
- service: media_player.soundtouch_add_zone_slave
data:
master: media_player.soundtouch_living_room
slaves:
- media_player.soundtouch_room
"""
master_device_id = service.data.get('master')
slaves_ids = service.data.get('slaves')
slaves = [device for device in DEVICES if device.entity_id in slaves_ids]
master = next([device for device in DEVICES if
device.entity_id == master_device_id].__iter__(), None)
if master is None:
_LOGGER.warning(
"Unable to find master with entity_id:" + str(master_device_id))
elif not slaves:
_LOGGER.warning("Unable to find slaves to add")
else:
_LOGGER.info(
"Adding slaves to zone with master " + str(
master.device.config.name))
master.device.add_zone_slave([slave.device for slave in slaves])
def remove_zone_slave(service):
"""
Remove slave(s) from and existing zone (multi-room).
Zone must already exist and slaves array can not be empty.
Note: If removing last slave, the zone will be deleted and you'll have to
create a new one. You will not be able to add a new slave anymore
:param service: Home Assistant service with 'master' and 'slaves' data set
:Example:
- service: media_player.soundtouch_remove_zone_slave
data:
master: media_player.soundtouch_living_room
slaves:
- media_player.soundtouch_room
"""
master_device_id = service.data.get('master')
slaves_ids = service.data.get('slaves')
slaves = [device for device in DEVICES if device.entity_id in slaves_ids]
master = next([device for device in DEVICES if
device.entity_id == master_device_id].__iter__(), None)
if master is None:
_LOGGER.warning(
"Unable to find master with entity_id:" + master_device_id)
elif not slaves:
_LOGGER.warning("Unable to find slaves to remove")
else:
_LOGGER.info("Removing slaves from zone with master " +
str(master.device.config.name))
master.device.remove_zone_slave([slave.device for slave in slaves])
class SoundTouchDevice(MediaPlayerDevice):
"""Representation of a SoundTouch Bose device."""
def __init__(self, name, config):
"""Create Soundtouch Entity."""
from libsoundtouch import soundtouch_device
self._name = name
self._device = soundtouch_device(config['host'], config['port'])
self._status = self._device.status()
self._volume = self._device.volume()
self._config = config
@property
def config(self):
"""Return specific soundtouch configuration."""
return self._config
@property
def device(self):
"""Return Soundtouch device."""
return self._device
def update(self):
"""Retrieve the latest data."""
self._status = self._device.status()
self._volume = self._device.volume()
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
return self._volume.actual / 100
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
if self._status.source == 'STANDBY':
return STATE_OFF
else:
return MAP_STATUS.get(self._status.play_status, STATE_UNAVAILABLE)
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return self._volume.muted
@property
def supported_media_commands(self):
"""Flag of media commands that are supported."""
return SUPPORT_SOUNDTOUCH
def turn_off(self):
"""Turn off media player."""
self._device.power_off()
self._status = self._device.status()
def turn_on(self):
"""Turn the media player on."""
self._device.power_on()
self._status = self._device.status()
def volume_up(self):
"""Volume up the media player."""
self._device.volume_up()
self._volume = self._device.volume()
def volume_down(self):
"""Volume down media player."""
self._device.volume_down()
self._volume = self._device.volume()
def set_volume_level(self, volume):
"""Set volume level, range 0..1."""
self._device.set_volume(int(volume * 100))
self._volume = self._device.volume()
def mute_volume(self, mute):
"""Send mute command."""
self._device.mute()
self._volume = self._device.volume()
def media_play_pause(self):
"""Simulate play pause media player."""
self._device.play_pause()
self._status = self._device.status()
def media_play(self):
"""Send play command."""
self._device.play()
self._status = self._device.status()
def media_pause(self):
"""Send media pause command to media player."""
self._device.pause()
self._status = self._device.status()
def media_next_track(self):
"""Send next track command."""
self._device.next_track()
self._status = self._device.status()
def media_previous_track(self):
"""Send the previous track command."""
self._device.previous_track()
self._status = self._device.status()
@property
def media_image_url(self):
"""Image url of current playing media."""
return self._status.image
@property
def media_title(self):
"""Title of current playing media."""
if self._status.station_name is not None:
return self._status.station_name
elif self._status.artist is not None:
return self._status.artist + " - " + self._status.track
else:
return None
@property
def media_duration(self):
"""Duration of current playing media in seconds."""
return self._status.duration
@property
def media_artist(self):
"""Artist of current playing media."""
return self._status.artist
@property
def media_track(self):
"""Artist of current playing media."""
return self._status.track
@property
def media_album_name(self):
"""Album name of current playing media."""
return self._status.album
def play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media."""
_LOGGER.info("Starting media with media_id:" + str(media_id))
presets = self._device.presets()
preset = next([preset for preset in presets if
preset.preset_id == str(media_id)].__iter__(), None)
if preset is not None:
_LOGGER.info("Playing preset: " + preset.name)
self._device.select_preset(preset)
else:
_LOGGER.warning("Unable to find preset with id " + str(media_id))

View file

@ -12,7 +12,7 @@ from homeassistant.components.mqtt import PROTOCOL_311
from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.util.async import run_coroutine_threadsafe from homeassistant.util.async import run_coroutine_threadsafe
REQUIREMENTS = ['hbmqtt==0.7.1'] REQUIREMENTS = ['hbmqtt==0.8']
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']

View file

@ -10,36 +10,110 @@ import socket
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME, CONF_STRUCTURE) from homeassistant.helpers import discovery
from homeassistant.const import (CONF_STRUCTURE, CONF_FILENAME)
from homeassistant.loader import get_component
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['python-nest==2.11.0'] REQUIREMENTS = [
'http://github.com/technicalpickles/python-nest'
'/archive/0be5c8a6307ee81540f21aac4fcd22cc5d98c988.zip' # nest-cam branch
'#python-nest==3.0.0']
DOMAIN = 'nest' DOMAIN = 'nest'
DATA_NEST = 'nest' DATA_NEST = 'nest'
NEST_CONFIG_FILE = 'nest.conf'
CONF_CLIENT_ID = 'client_id'
CONF_CLIENT_SECRET = 'client_secret'
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_CLIENT_SECRET): cv.string,
vol.Optional(CONF_STRUCTURE): vol.All(cv.ensure_list, cv.string) vol.Optional(CONF_STRUCTURE): vol.All(cv.ensure_list, cv.string)
}) })
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
def request_configuration(nest, hass, config):
"""Request configuration steps from the user."""
configurator = get_component('configurator')
if 'nest' in _CONFIGURING:
_LOGGER.debug("configurator failed")
configurator.notify_errors(
_CONFIGURING['nest'], "Failed to configure, please try again.")
return
def nest_configuration_callback(data):
"""The actions to do when our configuration callback is called."""
_LOGGER.debug("configurator callback")
pin = data.get('pin')
setup_nest(hass, nest, config, pin=pin)
_CONFIGURING['nest'] = configurator.request_config(
hass, "Nest", nest_configuration_callback,
description=('To configure Nest, click Request Authorization below, '
'log into your Nest account, '
'and then enter the resulting PIN'),
link_name='Request Authorization',
link_url=nest.authorize_url,
submit_caption="Confirm",
fields=[{'id': 'pin', 'name': 'Enter the PIN', 'type': ''}]
)
def setup_nest(hass, nest, config, pin=None):
"""Setup Nest Devices."""
if pin is not None:
_LOGGER.debug("pin acquired, requesting access token")
nest.request_token(pin)
if nest.access_token is None:
_LOGGER.debug("no access_token, requesting configuration")
request_configuration(nest, hass, config)
return
if 'nest' in _CONFIGURING:
_LOGGER.debug("configuration done")
configurator = get_component('configurator')
configurator.request_done(_CONFIGURING.pop('nest'))
_LOGGER.debug("proceeding with setup")
conf = config[DOMAIN]
hass.data[DATA_NEST] = NestDevice(hass, conf, nest)
_LOGGER.debug("proceeding with discovery")
discovery.load_platform(hass, 'climate', DOMAIN, {}, config)
discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config)
discovery.load_platform(hass, 'camera', DOMAIN, {}, config)
_LOGGER.debug("setup done")
return True
def setup(hass, config): def setup(hass, config):
"""Setup the Nest thermostat component.""" """Setup the Nest thermostat component."""
import nest import nest
conf = config[DOMAIN] if 'nest' in _CONFIGURING:
username = conf[CONF_USERNAME] return
password = conf[CONF_PASSWORD]
nest = nest.Nest(username, password) conf = config[DOMAIN]
hass.data[DATA_NEST] = NestDevice(hass, conf, nest) client_id = conf[CONF_CLIENT_ID]
client_secret = conf[CONF_CLIENT_SECRET]
filename = config.get(CONF_FILENAME, NEST_CONFIG_FILE)
access_token_cache_file = hass.config.path(filename)
nest = nest.Nest(
access_token_cache_file=access_token_cache_file,
client_id=client_id, client_secret=client_secret)
setup_nest(hass, nest, config)
return True return True
@ -85,3 +159,32 @@ class NestDevice(object):
except socket.error: except socket.error:
_LOGGER.error( _LOGGER.error(
"Connection error logging into the nest web service.") "Connection error logging into the nest web service.")
def camera_devices(self):
"""Generator returning list of camera devices."""
try:
for structure in self.nest.structures:
if structure.name in self._structure:
for device in structure.cameradevices:
yield(structure, device)
else:
_LOGGER.info("Ignoring structure %s, not in %s",
structure.name, self._structure)
except socket.error:
_LOGGER.error(
"Connection error logging into the nest web service.")
def is_thermostat(device):
"""Target devices that are Nest Thermostats."""
return bool(device.__class__.__name__ == 'Device')
def is_protect(device):
"""Target devices that are Nest Protect Smoke Alarms."""
return bool(device.__class__.__name__ == 'ProtectDevice')
def is_camera(device):
"""Target devices that are Nest Protect Smoke Alarms."""
return bool(device.__class__.__name__ == 'CameraDevice')

View file

@ -4,35 +4,37 @@ Provides functionality to notify people.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/notify/ https://home-assistant.io/components/notify/
""" """
from functools import partial
import logging import logging
import os import os
from functools import partial
import voluptuous as vol import voluptuous as vol
import homeassistant.bootstrap as bootstrap import homeassistant.bootstrap as bootstrap
from homeassistant.config import load_yaml_config_file
from homeassistant.helpers import config_per_platform
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.config import load_yaml_config_file
from homeassistant.const import CONF_NAME, CONF_PLATFORM from homeassistant.const import CONF_NAME, CONF_PLATFORM
from homeassistant.helpers import config_per_platform
from homeassistant.util import slugify from homeassistant.util import slugify
DOMAIN = "notify" _LOGGER = logging.getLogger(__name__)
# Title of notification
ATTR_TITLE = "title"
ATTR_TITLE_DEFAULT = "Home Assistant"
# Target of the notification (user, device, etc)
ATTR_TARGET = 'target'
# Text to notify user of
ATTR_MESSAGE = "message"
# Platform specific data # Platform specific data
ATTR_DATA = 'data' ATTR_DATA = 'data'
SERVICE_NOTIFY = "notify" # Text to notify user of
ATTR_MESSAGE = 'message'
# Target of the notification (user, device, etc)
ATTR_TARGET = 'target'
# Title of notification
ATTR_TITLE = 'title'
ATTR_TITLE_DEFAULT = "Home Assistant"
DOMAIN = 'notify'
SERVICE_NOTIFY = 'notify'
PLATFORM_SCHEMA = vol.Schema({ PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): cv.string, vol.Required(CONF_PLATFORM): cv.string,
@ -46,8 +48,6 @@ NOTIFY_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_DATA): dict, vol.Optional(ATTR_DATA): dict,
}) })
_LOGGER = logging.getLogger(__name__)
def send_message(hass, message, title=None, data=None): def send_message(hass, message, title=None, data=None):
"""Send a notification message.""" """Send a notification message."""
@ -78,7 +78,7 @@ def setup(hass, config):
hass, config, DOMAIN, platform) hass, config, DOMAIN, platform)
if notify_implementation is None: if notify_implementation is None:
_LOGGER.error("Unknown notification service specified.") _LOGGER.error("Unknown notification service specified")
continue continue
notify_service = notify_implementation.get_service(hass, p_config) notify_service = notify_implementation.get_service(hass, p_config)
@ -114,7 +114,7 @@ def setup(hass, config):
if hasattr(notify_service, 'targets'): if hasattr(notify_service, 'targets'):
platform_name = (p_config.get(CONF_NAME) or platform) platform_name = (p_config.get(CONF_NAME) or platform)
for name, target in notify_service.targets.items(): for name, target in notify_service.targets.items():
target_name = slugify("{}_{}".format(platform_name, name)) target_name = slugify('{}_{}'.format(platform_name, name))
targets[target_name] = target targets[target_name] = target
hass.services.register(DOMAIN, target_name, hass.services.register(DOMAIN, target_name,
service_call_handler, service_call_handler,
@ -124,10 +124,9 @@ def setup(hass, config):
platform_name = (p_config.get(CONF_NAME) or SERVICE_NOTIFY) platform_name = (p_config.get(CONF_NAME) or SERVICE_NOTIFY)
platform_name_slug = slugify(platform_name) platform_name_slug = slugify(platform_name)
hass.services.register(DOMAIN, platform_name_slug, hass.services.register(
service_call_handler, DOMAIN, platform_name_slug, service_call_handler,
descriptions.get(SERVICE_NOTIFY), descriptions.get(SERVICE_NOTIFY), schema=NOTIFY_SERVICE_SCHEMA)
schema=NOTIFY_SERVICE_SCHEMA)
success = True success = True
return success return success

View file

@ -14,7 +14,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['freesms==0.1.0'] REQUIREMENTS = ['freesms==0.1.1']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({

View file

@ -107,8 +107,8 @@ def get_service(hass, config):
return None return None
hass.http.register_view( hass.http.register_view(
HTML5PushRegistrationView(hass, registrations, json_path)) HTML5PushRegistrationView(registrations, json_path))
hass.http.register_view(HTML5PushCallbackView(hass, registrations)) hass.http.register_view(HTML5PushCallbackView(registrations))
gcm_api_key = config.get(ATTR_GCM_API_KEY) gcm_api_key = config.get(ATTR_GCM_API_KEY)
gcm_sender_id = config.get(ATTR_GCM_SENDER_ID) gcm_sender_id = config.get(ATTR_GCM_SENDER_ID)
@ -168,9 +168,8 @@ class HTML5PushRegistrationView(HomeAssistantView):
url = '/api/notify.html5' url = '/api/notify.html5'
name = 'api:notify.html5' name = 'api:notify.html5'
def __init__(self, hass, registrations, json_path): def __init__(self, registrations, json_path):
"""Init HTML5PushRegistrationView.""" """Init HTML5PushRegistrationView."""
super().__init__(hass)
self.registrations = registrations self.registrations = registrations
self.json_path = json_path self.json_path = json_path
@ -237,9 +236,8 @@ class HTML5PushCallbackView(HomeAssistantView):
url = '/api/notify.html5/callback' url = '/api/notify.html5/callback'
name = 'api:notify.html5/callback' name = 'api:notify.html5/callback'
def __init__(self, hass, registrations): def __init__(self, registrations):
"""Init HTML5PushCallbackView.""" """Init HTML5PushCallbackView."""
super().__init__(hass)
self.registrations = registrations self.registrations = registrations
def decode_jwt(self, token): def decode_jwt(self, token):
@ -324,7 +322,7 @@ class HTML5PushCallbackView(HomeAssistantView):
event_name = '{}.{}'.format(NOTIFY_CALLBACK_EVENT, event_name = '{}.{}'.format(NOTIFY_CALLBACK_EVENT,
event_payload[ATTR_TYPE]) event_payload[ATTR_TYPE])
self.hass.bus.fire(event_name, event_payload) request.app['hass'].bus.fire(event_name, event_payload)
return self.json({'status': 'ok', return self.json({'status': 'ok',
'event': event_payload[ATTR_TYPE]}) 'event': event_payload[ATTR_TYPE]})

View file

@ -13,7 +13,7 @@ from homeassistant.components.notify import (
from homeassistant.const import (CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT) from homeassistant.const import (CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['sendgrid==3.6.2'] REQUIREMENTS = ['sendgrid==3.6.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View file

@ -14,7 +14,7 @@ from homeassistant.const import (
CONF_API_KEY, CONF_USERNAME, CONF_ICON) CONF_API_KEY, CONF_USERNAME, CONF_ICON)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['slacker==0.9.29'] REQUIREMENTS = ['slacker==0.9.30']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View file

@ -13,7 +13,8 @@ from homeassistant.const import (CONF_MAC, CONF_NAME, EVENT_HOMEASSISTANT_STOP)
REQUIREMENTS = [ REQUIREMENTS = [
'--only-binary=all ' # avoid compilation of gattlib '--only-binary=all ' # avoid compilation of gattlib
'git+https://github.com/getSenic/nuimo-linux-python' 'http://github.com/getSenic/nuimo-linux-python'
'/archive/29fc42987f74d8090d0e2382e8f248ff5990b8c9.zip'
'#nuimo==1.0.0'] '#nuimo==1.0.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View file

@ -27,7 +27,7 @@ import homeassistant.util.dt as dt_util
DOMAIN = 'recorder' DOMAIN = 'recorder'
REQUIREMENTS = ['sqlalchemy==1.1.3'] REQUIREMENTS = ['sqlalchemy==1.1.4']
DEFAULT_URL = 'sqlite:///{hass_config_path}' DEFAULT_URL = 'sqlite:///{hass_config_path}'
DEFAULT_DB_FILE = 'home-assistant_v2.db' DEFAULT_DB_FILE = 'home-assistant_v2.db'

View file

@ -0,0 +1,144 @@
"""
Component to interface with universal remote control devices.
For more details about this component, please refer to the documentation
at https://home-assistant.io/components/remote/
"""
from datetime import timedelta
import logging
import os
import voluptuous as vol
from homeassistant.config import load_yaml_config_file
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity import ToggleEntity
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
from homeassistant.components import group
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
_LOGGER = logging.getLogger(__name__)
ATTR_ACTIVITY = 'activity'
ATTR_COMMAND = 'command'
ATTR_DEVICE = 'device'
DOMAIN = 'remote'
ENTITY_ID_ALL_REMOTES = group.ENTITY_ID_FORMAT.format('all_remotes')
ENTITY_ID_FORMAT = DOMAIN + '.{}'
GROUP_NAME_ALL_REMOTES = 'all remotes'
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
SCAN_INTERVAL = 30
SERVICE_SEND_COMMAND = 'send_command'
SERVICE_SYNC = 'sync'
REMOTE_SERVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
})
REMOTE_SERVICE_TURN_ON_SCHEMA = REMOTE_SERVICE_SCHEMA.extend({
vol.Optional(ATTR_ACTIVITY): cv.string
})
REMOTE_SERVICE_SEND_COMMAND_SCHEMA = REMOTE_SERVICE_SCHEMA.extend({
vol.Required(ATTR_DEVICE): cv.string,
vol.Required(ATTR_COMMAND): cv.string,
})
def is_on(hass, entity_id=None):
"""Return if the remote is on based on the statemachine."""
entity_id = entity_id or ENTITY_ID_ALL_REMOTES
return hass.states.is_state(entity_id, STATE_ON)
def turn_on(hass, activity=None, entity_id=None):
"""Turn all or specified remote on."""
data = {ATTR_ACTIVITY: activity}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_TURN_ON, data)
def turn_off(hass, entity_id=None):
"""Turn all or specified remote off."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.services.call(DOMAIN, SERVICE_TURN_OFF, data)
def send_command(hass, device, command, entity_id=None):
"""Send a command to a device."""
data = {ATTR_DEVICE: str(device), ATTR_COMMAND: command}
if entity_id:
data[ATTR_ENTITY_ID] = entity_id
hass.services.call(DOMAIN, SERVICE_SEND_COMMAND, data)
def sync(hass, entity_id=None):
"""Sync remote device."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.services.call(DOMAIN, SERVICE_SYNC, data)
def setup(hass, config):
"""Track states and offer events for remotes."""
component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_REMOTES)
component.setup(config)
def handle_remote_service(service):
"""Handle calls to the remote services."""
target_remotes = component.extract_from_service(service)
activity_id = service.data.get(ATTR_ACTIVITY)
device = service.data.get(ATTR_DEVICE)
command = service.data.get(ATTR_COMMAND)
for remote in target_remotes:
if service.service == SERVICE_TURN_ON:
remote.turn_on(activity=activity_id)
elif service.service == SERVICE_SEND_COMMAND:
remote.send_command(device=device, command=command)
elif service.service == SERVICE_SYNC:
remote.sync()
else:
remote.turn_off()
if remote.should_poll:
remote.update_ha_state(True)
descriptions = load_yaml_config_file(
os.path.join(os.path.dirname(__file__), 'services.yaml'))
hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_remote_service,
descriptions.get(SERVICE_TURN_OFF),
schema=REMOTE_SERVICE_SCHEMA)
hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_remote_service,
descriptions.get(SERVICE_TURN_ON),
schema=REMOTE_SERVICE_TURN_ON_SCHEMA)
hass.services.register(DOMAIN, SERVICE_SEND_COMMAND, handle_remote_service,
descriptions.get(SERVICE_SEND_COMMAND),
schema=REMOTE_SERVICE_SEND_COMMAND_SCHEMA)
return True
class RemoteDevice(ToggleEntity):
"""Representation of a remote."""
def turn_on(self, **kwargs):
"""Turn a device on with the remote."""
raise NotImplementedError()
def turn_off(self, **kwargs):
"""Turn a device off with the remote."""
raise NotImplementedError()
def send_command(self, **kwargs):
"""Send a command to a device."""
raise NotImplementedError()

View file

@ -0,0 +1,57 @@
"""
Demo platform that has two fake remotes.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
from homeassistant.components.remote import RemoteDevice
from homeassistant.const import DEVICE_DEFAULT_NAME
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Setup the demo remotes."""
add_devices_callback([
DemoRemote('Remote One', False, None),
DemoRemote('Remote Two', True, 'mdi:remote'),
])
class DemoRemote(RemoteDevice):
"""Representation of a demo remote."""
def __init__(self, name, state, icon):
"""Initialize the Demo Remote."""
self._name = name or DEVICE_DEFAULT_NAME
self._state = state
self._icon = icon
@property
def should_poll(self):
"""No polling needed for a demo remote."""
return False
@property
def name(self):
"""Return the name of the device if any."""
return self._name
@property
def icon(self):
"""Return the icon to use for device if any."""
return self._icon
@property
def is_on(self):
"""Return true if remote is on."""
return self._state
def turn_on(self, **kwargs):
"""Turn the remote on."""
self._state = True
self.schedule_update_ha_state()
def turn_off(self, **kwargs):
"""Turn the remote off."""
self._state = False
self.schedule_update_ha_state()

Some files were not shown because too many files have changed in this diff Show more