commit
94b719e150
239 changed files with 10393 additions and 2204 deletions
13
.coveragerc
13
.coveragerc
|
@ -40,9 +40,6 @@ omit =
|
|||
homeassistant/components/isy994.py
|
||||
homeassistant/components/*/isy994.py
|
||||
|
||||
homeassistant/components/litejet.py
|
||||
homeassistant/components/*/litejet.py
|
||||
|
||||
homeassistant/components/modbus.py
|
||||
homeassistant/components/*/modbus.py
|
||||
|
||||
|
@ -127,6 +124,7 @@ omit =
|
|||
homeassistant/components/binary_sensor/concord232.py
|
||||
homeassistant/components/binary_sensor/rest.py
|
||||
homeassistant/components/browser.py
|
||||
homeassistant/components/camera/amcrest.py
|
||||
homeassistant/components/camera/bloomsky.py
|
||||
homeassistant/components/camera/foscam.py
|
||||
homeassistant/components/camera/mjpeg.py
|
||||
|
@ -152,6 +150,7 @@ omit =
|
|||
homeassistant/components/device_tracker/bt_home_hub_5.py
|
||||
homeassistant/components/device_tracker/cisco_ios.py
|
||||
homeassistant/components/device_tracker/fritz.py
|
||||
homeassistant/components/device_tracker/gpslogger.py
|
||||
homeassistant/components/device_tracker/icloud.py
|
||||
homeassistant/components/device_tracker/luci.py
|
||||
homeassistant/components/device_tracker/netgear.py
|
||||
|
@ -188,7 +187,9 @@ omit =
|
|||
homeassistant/components/media_player/cast.py
|
||||
homeassistant/components/media_player/cmus.py
|
||||
homeassistant/components/media_player/denon.py
|
||||
homeassistant/components/media_player/denonavr.py
|
||||
homeassistant/components/media_player/directv.py
|
||||
homeassistant/components/media_player/dunehd.py
|
||||
homeassistant/components/media_player/emby.py
|
||||
homeassistant/components/media_player/firetv.py
|
||||
homeassistant/components/media_player/gpmdp.py
|
||||
|
@ -240,6 +241,7 @@ omit =
|
|||
homeassistant/components/notify/xmpp.py
|
||||
homeassistant/components/nuimo_controller.py
|
||||
homeassistant/components/openalpr.py
|
||||
homeassistant/components/remote/harmony.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/arwn.py
|
||||
|
@ -279,6 +281,7 @@ omit =
|
|||
homeassistant/components/sensor/miflora.py
|
||||
homeassistant/components/sensor/mqtt_room.py
|
||||
homeassistant/components/sensor/neurio_energy.py
|
||||
homeassistant/components/sensor/nut.py
|
||||
homeassistant/components/sensor/nzbget.py
|
||||
homeassistant/components/sensor/ohmconnect.py
|
||||
homeassistant/components/sensor/onewire.py
|
||||
|
@ -291,6 +294,7 @@ omit =
|
|||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/serial_pm.py
|
||||
homeassistant/components/sensor/snmp.py
|
||||
homeassistant/components/sensor/sonarr.py
|
||||
homeassistant/components/sensor/speedtest.py
|
||||
homeassistant/components/sensor/steam_online.py
|
||||
homeassistant/components/sensor/supervisord.py
|
||||
|
@ -306,14 +310,17 @@ omit =
|
|||
homeassistant/components/sensor/twitch.py
|
||||
homeassistant/components/sensor/uber.py
|
||||
homeassistant/components/sensor/vasttrafik.py
|
||||
homeassistant/components/sensor/waqi.py
|
||||
homeassistant/components/sensor/xbox_live.py
|
||||
homeassistant/components/sensor/yweather.py
|
||||
homeassistant/components/sensor/waqi.py
|
||||
homeassistant/components/switch/acer_projector.py
|
||||
homeassistant/components/switch/anel_pwrctrl.py
|
||||
homeassistant/components/switch/arest.py
|
||||
homeassistant/components/switch/dlink.py
|
||||
homeassistant/components/switch/edimax.py
|
||||
homeassistant/components/switch/hikvisioncam.py
|
||||
homeassistant/components/switch/hook.py
|
||||
homeassistant/components/switch/mystrom.py
|
||||
homeassistant/components/switch/netio.py
|
||||
homeassistant/components/switch/orvibo.py
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -62,6 +62,7 @@ pip-log.txt
|
|||
.coverage
|
||||
.tox
|
||||
nosetests.xml
|
||||
htmlcov/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
|
|
@ -365,6 +365,7 @@ def async_from_config_dict(config: Dict[str, Any],
|
|||
Dynamically loads required components and its dependencies.
|
||||
This method is a coroutine.
|
||||
"""
|
||||
hass.async_track_tasks()
|
||||
setup_lock = hass.data.get('setup_lock')
|
||||
if setup_lock is None:
|
||||
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()
|
||||
|
||||
yield from hass.async_stop_track_tasks()
|
||||
|
||||
return hass
|
||||
|
||||
|
||||
|
|
|
@ -117,11 +117,11 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
|||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
self._alarm.arm('home')
|
||||
self._alarm.arm('stay')
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._alarm.arm('auto')
|
||||
self._alarm.arm('exit')
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Alarm trigger command."""
|
||||
|
|
|
@ -118,7 +118,7 @@ class AlexaIntentsView(HomeAssistantView):
|
|||
|
||||
def __init__(self, hass, intents):
|
||||
"""Initialize Alexa view."""
|
||||
super().__init__(hass)
|
||||
super().__init__()
|
||||
|
||||
intents = copy.deepcopy(intents)
|
||||
template.attach(hass, intents)
|
||||
|
@ -150,7 +150,7 @@ class AlexaIntentsView(HomeAssistantView):
|
|||
return None
|
||||
|
||||
intent = req.get('intent')
|
||||
response = AlexaResponse(self.hass, intent)
|
||||
response = AlexaResponse(request.app['hass'], intent)
|
||||
|
||||
if req_type == 'LaunchRequest':
|
||||
response.add_speech(
|
||||
|
@ -282,7 +282,7 @@ class AlexaFlashBriefingView(HomeAssistantView):
|
|||
|
||||
def __init__(self, hass, flash_briefings):
|
||||
"""Initialize Alexa view."""
|
||||
super().__init__(hass)
|
||||
super().__init__()
|
||||
self.flash_briefings = copy.deepcopy(flash_briefings)
|
||||
template.attach(hass, self.flash_briefings)
|
||||
|
||||
|
|
|
@ -77,8 +77,10 @@ class APIEventStream(HomeAssistantView):
|
|||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""Provide a streaming interface for the event bus."""
|
||||
# pylint: disable=no-self-use
|
||||
hass = request.app['hass']
|
||||
stop_obj = object()
|
||||
to_write = asyncio.Queue(loop=self.hass.loop)
|
||||
to_write = asyncio.Queue(loop=hass.loop)
|
||||
|
||||
restrict = request.GET.get('restrict')
|
||||
if restrict:
|
||||
|
@ -106,7 +108,7 @@ class APIEventStream(HomeAssistantView):
|
|||
response.content_type = 'text/event-stream'
|
||||
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:
|
||||
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
|
||||
|
@ -117,7 +119,7 @@ class APIEventStream(HomeAssistantView):
|
|||
while True:
|
||||
try:
|
||||
with async_timeout.timeout(STREAM_PING_INTERVAL,
|
||||
loop=self.hass.loop):
|
||||
loop=hass.loop):
|
||||
payload = yield from to_write.get()
|
||||
|
||||
if payload is stop_obj:
|
||||
|
@ -145,7 +147,7 @@ class APIConfigView(HomeAssistantView):
|
|||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get current configuration."""
|
||||
return self.json(self.hass.config.as_dict())
|
||||
return self.json(request.app['hass'].config.as_dict())
|
||||
|
||||
|
||||
class APIDiscoveryView(HomeAssistantView):
|
||||
|
@ -158,10 +160,11 @@ class APIDiscoveryView(HomeAssistantView):
|
|||
@ha.callback
|
||||
def get(self, request):
|
||||
"""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({
|
||||
'base_url': self.hass.config.api.base_url,
|
||||
'location_name': self.hass.config.location_name,
|
||||
'base_url': hass.config.api.base_url,
|
||||
'location_name': hass.config.location_name,
|
||||
'requires_api_password': needs_auth,
|
||||
'version': __version__
|
||||
})
|
||||
|
@ -176,7 +179,7 @@ class APIStatesView(HomeAssistantView):
|
|||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get current states."""
|
||||
return self.json(self.hass.states.async_all())
|
||||
return self.json(request.app['hass'].states.async_all())
|
||||
|
||||
|
||||
class APIEntityStateView(HomeAssistantView):
|
||||
|
@ -188,7 +191,7 @@ class APIEntityStateView(HomeAssistantView):
|
|||
@ha.callback
|
||||
def get(self, request, entity_id):
|
||||
"""Retrieve state of entity."""
|
||||
state = self.hass.states.get(entity_id)
|
||||
state = request.app['hass'].states.get(entity_id)
|
||||
if state:
|
||||
return self.json(state)
|
||||
else:
|
||||
|
@ -197,6 +200,7 @@ class APIEntityStateView(HomeAssistantView):
|
|||
@asyncio.coroutine
|
||||
def post(self, request, entity_id):
|
||||
"""Update state of entity."""
|
||||
hass = request.app['hass']
|
||||
try:
|
||||
data = yield from request.json()
|
||||
except ValueError:
|
||||
|
@ -211,15 +215,14 @@ class APIEntityStateView(HomeAssistantView):
|
|||
attributes = data.get('attributes')
|
||||
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
|
||||
self.hass.states.async_set(entity_id, new_state, attributes,
|
||||
force_update)
|
||||
hass.states.async_set(entity_id, new_state, attributes, force_update)
|
||||
|
||||
# Read the state back for our response
|
||||
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))
|
||||
|
||||
|
@ -228,7 +231,7 @@ class APIEntityStateView(HomeAssistantView):
|
|||
@ha.callback
|
||||
def delete(self, request, entity_id):
|
||||
"""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')
|
||||
else:
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
|
@ -243,7 +246,7 @@ class APIEventListenersView(HomeAssistantView):
|
|||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get event listeners."""
|
||||
return self.json(async_events_json(self.hass))
|
||||
return self.json(async_events_json(request.app['hass']))
|
||||
|
||||
|
||||
class APIEventView(HomeAssistantView):
|
||||
|
@ -271,7 +274,8 @@ class APIEventView(HomeAssistantView):
|
|||
if 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))
|
||||
|
||||
|
@ -285,7 +289,7 @@ class APIServicesView(HomeAssistantView):
|
|||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get registered services."""
|
||||
return self.json(async_services_json(self.hass))
|
||||
return self.json(async_services_json(request.app['hass']))
|
||||
|
||||
|
||||
class APIDomainServicesView(HomeAssistantView):
|
||||
|
@ -300,12 +304,12 @@ class APIDomainServicesView(HomeAssistantView):
|
|||
|
||||
Returns a list of changed states.
|
||||
"""
|
||||
hass = request.app['hass']
|
||||
body = yield from request.text()
|
||||
data = json.loads(body) if body else None
|
||||
|
||||
with AsyncTrackStates(self.hass) as changed_states:
|
||||
yield from self.hass.services.async_call(domain, service, data,
|
||||
True)
|
||||
with AsyncTrackStates(hass) as changed_states:
|
||||
yield from hass.services.async_call(domain, service, data, True)
|
||||
|
||||
return self.json(changed_states)
|
||||
|
||||
|
@ -320,6 +324,7 @@ class APIEventForwardingView(HomeAssistantView):
|
|||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Setup an event forwarder."""
|
||||
hass = request.app['hass']
|
||||
try:
|
||||
data = yield from request.json()
|
||||
except ValueError:
|
||||
|
@ -340,14 +345,14 @@ class APIEventForwardingView(HomeAssistantView):
|
|||
|
||||
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)
|
||||
if not valid:
|
||||
return self.json_message("Unable to validate API.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
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)
|
||||
|
||||
|
@ -389,7 +394,7 @@ class APIComponentsView(HomeAssistantView):
|
|||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get current loaded components."""
|
||||
return self.json(self.hass.config.components)
|
||||
return self.json(request.app['hass'].config.components)
|
||||
|
||||
|
||||
class APIErrorLogView(HomeAssistantView):
|
||||
|
@ -402,7 +407,7 @@ class APIErrorLogView(HomeAssistantView):
|
|||
def get(self, request):
|
||||
"""Serve error log."""
|
||||
resp = yield from self.file(
|
||||
request, self.hass.config.path(ERROR_LOG_FILENAME))
|
||||
request, request.app['hass'].config.path(ERROR_LOG_FILENAME))
|
||||
return resp
|
||||
|
||||
|
||||
|
@ -417,7 +422,7 @@ class APITemplateView(HomeAssistantView):
|
|||
"""Render a template."""
|
||||
try:
|
||||
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'))
|
||||
except (ValueError, TemplateError) as ex:
|
||||
return self.json_message('Error rendering template: {}'.format(ex),
|
||||
|
|
|
@ -11,22 +11,34 @@ import voluptuous as vol
|
|||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
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']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_NUMBER = 'number'
|
||||
CONF_HELD_MORE_THAN = 'held_more_than'
|
||||
CONF_HELD_LESS_THAN = 'held_less_than'
|
||||
|
||||
TRIGGER_SCHEMA = vol.Schema({
|
||||
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):
|
||||
"""Listen for events based on configuration."""
|
||||
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
|
||||
def call_action():
|
||||
|
@ -34,8 +46,53 @@ def async_trigger(hass, config, action):
|
|||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
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
|
||||
|
|
|
@ -4,6 +4,7 @@ Component to interface with binary sensors.
|
|||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
@ -39,13 +40,13 @@ 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."""
|
||||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
component.setup(config)
|
||||
|
||||
yield from component.async_setup(config)
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
@ -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/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
|
@ -23,7 +22,7 @@ DEFAULT_NAME = 'Binary Command Sensor'
|
|||
DEFAULT_PAYLOAD_ON = 'ON'
|
||||
DEFAULT_PAYLOAD_OFF = 'OFF'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
SCAN_INTERVAL = 60
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COMMAND): cv.string,
|
||||
|
|
|
@ -7,7 +7,8 @@ https://home-assistant.io/components/binary_sensor.homematic/
|
|||
import logging
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
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__)
|
||||
|
||||
|
@ -32,14 +33,16 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
|||
if discovery_info is None:
|
||||
return
|
||||
|
||||
homematic = get_component("homematic")
|
||||
return homematic.setup_hmdevice_discovery_helper(
|
||||
hass,
|
||||
HMBinarySensor,
|
||||
discovery_info,
|
||||
add_callback_devices
|
||||
)
|
||||
|
||||
|
||||
class HMBinarySensor(homematic.HMDevice, BinarySensorDevice):
|
||||
class HMBinarySensor(HMDevice, BinarySensorDevice):
|
||||
"""Representation of a binary Homematic device."""
|
||||
|
||||
@property
|
||||
|
|
|
@ -4,46 +4,97 @@ Support for Nest Thermostat Binary Sensors.
|
|||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.nest/
|
||||
"""
|
||||
from itertools import chain
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.sensor.nest import NestSensor
|
||||
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
|
||||
|
||||
DEPENDENCIES = ['nest']
|
||||
BINARY_TYPES = ['fan',
|
||||
'hvac_ac_state',
|
||||
'hvac_aux_heater_state',
|
||||
'hvac_heater_state',
|
||||
'hvac_heat_x2_state',
|
||||
'hvac_heat_x3_state',
|
||||
'hvac_alt_heat_state',
|
||||
'hvac_alt_heat_x2_state',
|
||||
'hvac_emer_heat_state',
|
||||
'online']
|
||||
|
||||
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_aux_heater_state',
|
||||
'hvac_heater_state',
|
||||
'hvac_heat_x2_state',
|
||||
'hvac_heat_x3_state',
|
||||
'hvac_alt_heat_state',
|
||||
'hvac_alt_heat_x2_state',
|
||||
'hvac_emer_heat_state']
|
||||
|
||||
_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({
|
||||
vol.Optional(CONF_SCAN_INTERVAL):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
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):
|
||||
"""Setup Nest binary sensors."""
|
||||
nest = hass.data[DATA_NEST]
|
||||
conf = config.get(CONF_MONITORED_CONDITIONS, _VALID_BINARY_SENSOR_TYPES)
|
||||
|
||||
all_sensors = []
|
||||
for structure, device in nest.devices():
|
||||
all_sensors.extend(
|
||||
[NestBinarySensor(structure, device, variable)
|
||||
for variable in config[CONF_MONITORED_CONDITIONS]])
|
||||
for variable in conf:
|
||||
if variable in _BINARY_TYPES_DEPRECATED:
|
||||
wstr = (variable + " is no a longer supported "
|
||||
"monitored_conditions. See "
|
||||
"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):
|
||||
|
@ -57,3 +108,21 @@ class NestBinarySensor(NestSensor, BinarySensorDevice):
|
|||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
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)
|
||||
|
|
128
homeassistant/components/binary_sensor/threshold.py
Normal file
128
homeassistant/components/binary_sensor/threshold.py
Normal 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)
|
|
@ -4,8 +4,6 @@ Support for Wink binary sensors.
|
|||
For more details about this platform, please refer to the documentation at
|
||||
at https://home-assistant.io/components/binary_sensor.wink/
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
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():
|
||||
if sensor.capability() in SENSOR_TYPES:
|
||||
add_devices([WinkBinarySensorDevice(sensor)])
|
||||
add_devices([WinkBinarySensorDevice(sensor, hass)])
|
||||
|
||||
for key in pywink.get_keys():
|
||||
add_devices([WinkBinarySensorDevice(key)])
|
||||
add_devices([WinkBinarySensorDevice(key, hass)])
|
||||
|
||||
for sensor in pywink.get_smoke_and_co_detectors():
|
||||
add_devices([WinkBinarySensorDevice(sensor)])
|
||||
add_devices([WinkBinarySensorDevice(sensor, hass)])
|
||||
|
||||
|
||||
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
"""Representation of a Wink binary sensor."""
|
||||
|
||||
def __init__(self, wink):
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink binary sensor."""
|
||||
super().__init__(wink)
|
||||
super().__init__(wink, hass)
|
||||
wink = get_component('wink')
|
||||
self._unit_of_measurement = self.wink.UNIT
|
||||
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
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
|
|
|
@ -13,7 +13,7 @@ from aiohttp import web
|
|||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
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'
|
||||
DEPENDENCIES = ['http']
|
||||
|
@ -33,8 +33,8 @@ def async_setup(hass, config):
|
|||
component = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
|
||||
hass.http.register_view(CameraImageView(hass, component.entities))
|
||||
hass.http.register_view(CameraMjpegStream(hass, component.entities))
|
||||
hass.http.register_view(CameraImageView(component.entities))
|
||||
hass.http.register_view(CameraMjpegStream(component.entities))
|
||||
|
||||
yield from component.async_setup(config)
|
||||
return True
|
||||
|
@ -165,9 +165,8 @@ class CameraView(HomeAssistantView):
|
|||
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, hass, entities):
|
||||
def __init__(self, entities):
|
||||
"""Initialize a basic camera view."""
|
||||
super().__init__(hass)
|
||||
self.entities = entities
|
||||
|
||||
@asyncio.coroutine
|
||||
|
@ -178,7 +177,7 @@ class CameraView(HomeAssistantView):
|
|||
if camera is None:
|
||||
return web.Response(status=404)
|
||||
|
||||
authenticated = (request.authenticated or
|
||||
authenticated = (request[KEY_AUTHENTICATED] or
|
||||
request.GET.get('token') == camera.access_token)
|
||||
|
||||
if not authenticated:
|
||||
|
|
79
homeassistant/components/camera/amcrest.py
Normal file
79
homeassistant/components/camera/amcrest.py
Normal 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
|
|
@ -18,6 +18,7 @@ from homeassistant.const import (
|
|||
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
||||
from homeassistant.exceptions import TemplateError
|
||||
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.util.async import run_coroutine_threadsafe
|
||||
|
||||
|
@ -96,8 +97,7 @@ class GenericCamera(Camera):
|
|||
def fetch():
|
||||
"""Read image from a URL."""
|
||||
try:
|
||||
kwargs = {'timeout': 10, 'auth': self._auth}
|
||||
response = requests.get(url, **kwargs)
|
||||
response = requests.get(url, timeout=10, auth=self._auth)
|
||||
return response.content
|
||||
except requests.exceptions.RequestException as error:
|
||||
_LOGGER.error('Error getting camera image: %s', error)
|
||||
|
@ -107,12 +107,13 @@ class GenericCamera(Camera):
|
|||
None, fetch)
|
||||
# async
|
||||
else:
|
||||
response = None
|
||||
try:
|
||||
websession = async_get_clientsession(self.hass)
|
||||
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)
|
||||
self._last_image = yield from response.read()
|
||||
yield from response.release()
|
||||
self._last_image = yield from response.read()
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.error('Timeout getting camera image')
|
||||
return self._last_image
|
||||
|
@ -120,6 +121,9 @@ class GenericCamera(Camera):
|
|||
aiohttp.errors.ClientDisconnectedError) as err:
|
||||
_LOGGER.error('Error getting new camera image: %s', err)
|
||||
return self._last_image
|
||||
finally:
|
||||
if response is not None:
|
||||
self.hass.async_add_job(response.release())
|
||||
|
||||
self._last_url = url
|
||||
return self._last_image
|
||||
|
|
|
@ -20,6 +20,7 @@ from homeassistant.const import (
|
|||
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_AUTHENTICATION,
|
||||
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
|
||||
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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -101,29 +102,33 @@ class MjpegCamera(Camera):
|
|||
return
|
||||
|
||||
# connect to stream
|
||||
websession = async_get_clientsession(self.hass)
|
||||
stream = None
|
||||
response = None
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
stream = yield from self.hass.websession.get(
|
||||
self._mjpeg_url,
|
||||
auth=self._auth
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
raise HTTPGatewayTimeout()
|
||||
stream = yield from websession.get(self._mjpeg_url,
|
||||
auth=self._auth)
|
||||
|
||||
response = web.StreamResponse()
|
||||
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
|
||||
response = web.StreamResponse()
|
||||
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
|
||||
|
||||
yield from response.prepare(request)
|
||||
yield from response.prepare(request)
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = yield from stream.content.read(102400)
|
||||
if not data:
|
||||
break
|
||||
response.write(data)
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
raise HTTPGatewayTimeout()
|
||||
|
||||
finally:
|
||||
self.hass.async_add_job(stream.release())
|
||||
yield from response.write_eof()
|
||||
if stream is not None:
|
||||
self.hass.async_add_job(stream.release())
|
||||
if response is not None:
|
||||
yield from response.write_eof()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
|
108
homeassistant/components/camera/nest.py
Normal file
108
homeassistant/components/camera/nest.py
Normal 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
|
|
@ -14,12 +14,13 @@ from aiohttp import web
|
|||
from aiohttp.web_exceptions import HTTPGatewayTimeout
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
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 (
|
||||
Camera, PLATFORM_SCHEMA)
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_get_clientsession, async_create_clientsession)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
|
@ -59,23 +60,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Setup a Synology IP Camera."""
|
||||
if not config.get(CONF_VERIFY_SSL):
|
||||
connector = aiohttp.TCPConnector(verify_ssl=False)
|
||||
|
||||
@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
|
||||
)
|
||||
verify_ssl = config.get(CONF_VERIFY_SSL)
|
||||
websession_init = async_get_clientsession(hass, verify_ssl)
|
||||
|
||||
# Determine API to use for authentication
|
||||
syno_api_url = SYNO_API_URL.format(
|
||||
|
@ -87,24 +73,27 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
'version': '1',
|
||||
'query': 'SYNO.'
|
||||
}
|
||||
query_req = None
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||
query_req = yield from websession_init.get(
|
||||
syno_api_url,
|
||||
params=query_payload
|
||||
)
|
||||
|
||||
query_resp = yield from query_req.json()
|
||||
auth_path = query_resp['data'][AUTH_API]['path']
|
||||
camera_api = query_resp['data'][CAMERA_API]['path']
|
||||
camera_path = query_resp['data'][CAMERA_API]['path']
|
||||
streaming_path = query_resp['data'][STREAMING_API]['path']
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
_LOGGER.exception("Error on %s", syno_api_url)
|
||||
return False
|
||||
|
||||
query_resp = yield from query_req.json()
|
||||
auth_path = query_resp['data'][AUTH_API]['path']
|
||||
camera_api = query_resp['data'][CAMERA_API]['path']
|
||||
camera_path = query_resp['data'][CAMERA_API]['path']
|
||||
streaming_path = query_resp['data'][STREAMING_API]['path']
|
||||
|
||||
# cleanup
|
||||
yield from query_req.release()
|
||||
finally:
|
||||
if query_req is not None:
|
||||
yield from query_req.release()
|
||||
|
||||
# Authticate to NAS to get a session id
|
||||
syno_auth_url = SYNO_API_URL.format(
|
||||
|
@ -118,19 +107,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
syno_auth_url
|
||||
)
|
||||
|
||||
websession_init.detach()
|
||||
|
||||
# init websession
|
||||
websession = aiohttp.ClientSession(
|
||||
loop=hass.loop, connector=connector, 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)
|
||||
websession = async_create_clientsession(
|
||||
hass, verify_ssl, cookies={'id': session_id})
|
||||
|
||||
# Use SessionID to get cameras in system
|
||||
syno_camera_url = SYNO_API_URL.format(
|
||||
|
@ -190,20 +169,23 @@ def get_session_id(hass, websession, username, password, login_url):
|
|||
'session': 'SurveillanceStation',
|
||||
'format': 'sid'
|
||||
}
|
||||
auth_req = None
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
|
||||
auth_req = yield from websession.get(
|
||||
login_url,
|
||||
params=auth_payload
|
||||
)
|
||||
auth_resp = yield from auth_req.json()
|
||||
return auth_resp['data']['sid']
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
_LOGGER.exception("Error on %s", login_url)
|
||||
return False
|
||||
|
||||
auth_resp = yield from auth_req.json()
|
||||
yield from auth_req.release()
|
||||
|
||||
return auth_resp['data']['sid']
|
||||
finally:
|
||||
if auth_req is not None:
|
||||
yield from auth_req.release()
|
||||
|
||||
|
||||
class SynologyCamera(Camera):
|
||||
|
@ -271,30 +253,34 @@ class SynologyCamera(Camera):
|
|||
'cameraId': self._camera_id,
|
||||
'format': 'mjpeg'
|
||||
}
|
||||
stream = None
|
||||
response = None
|
||||
try:
|
||||
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
|
||||
stream = yield from self._websession.get(
|
||||
streaming_url,
|
||||
params=streaming_payload
|
||||
)
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
_LOGGER.exception("Error on %s", streaming_url)
|
||||
raise HTTPGatewayTimeout()
|
||||
response = web.StreamResponse()
|
||||
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
|
||||
|
||||
response = web.StreamResponse()
|
||||
response.content_type = stream.headers.get(CONTENT_TYPE_HEADER)
|
||||
yield from response.prepare(request)
|
||||
|
||||
yield from response.prepare(request)
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = yield from stream.content.read(102400)
|
||||
if not data:
|
||||
break
|
||||
response.write(data)
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.errors.ClientError):
|
||||
_LOGGER.exception("Error on %s", streaming_url)
|
||||
raise HTTPGatewayTimeout()
|
||||
|
||||
finally:
|
||||
self.hass.async_add_job(stream.release())
|
||||
yield from response.write_eof()
|
||||
if stream is not None:
|
||||
self.hass.async_add_job(stream.release())
|
||||
if response is not None:
|
||||
yield from response.write_eof()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
|
|
@ -58,6 +58,11 @@ ATTR_OPERATION_LIST = "operation_list"
|
|||
ATTR_SWING_MODE = "swing_mode"
|
||||
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 = [
|
||||
ATTR_TEMPERATURE,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
|
@ -371,6 +376,14 @@ class ClimateDevice(Entity):
|
|||
else:
|
||||
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
|
||||
def state_attributes(self):
|
||||
"""Return the optional state attributes."""
|
||||
|
@ -562,16 +575,18 @@ class ClimateDevice(Entity):
|
|||
|
||||
def _convert_for_display(self, temp):
|
||||
"""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
|
||||
|
||||
value = convert_temperature(temp, self.temperature_unit,
|
||||
self.unit_of_measurement)
|
||||
|
||||
if self.unit_of_measurement is TEMP_CELSIUS:
|
||||
decimal_count = 1
|
||||
# Round in the units appropriate
|
||||
if self.precision == PRECISION_HALVES:
|
||||
return round(value * 2) / 2.0
|
||||
elif self.precision == PRECISION_TENTHS:
|
||||
return round(value, 1)
|
||||
else:
|
||||
# Users of fahrenheit generally expect integer units.
|
||||
decimal_count = 0
|
||||
|
||||
return round(value, decimal_count)
|
||||
# PRECISION_WHOLE as a fall back
|
||||
return round(value)
|
||||
|
|
|
@ -21,10 +21,10 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
DEPENDENCIES = ['switch', 'sensor']
|
||||
|
||||
TOL_TEMP = 0.3
|
||||
DEFAULT_TOLERANCE = 0.3
|
||||
DEFAULT_NAME = 'Generic Thermostat'
|
||||
|
||||
CONF_NAME = 'name'
|
||||
DEFAULT_NAME = 'Generic Thermostat'
|
||||
CONF_HEATER = 'heater'
|
||||
CONF_SENSOR = 'target_sensor'
|
||||
CONF_MIN_TEMP = 'min_temp'
|
||||
|
@ -32,6 +32,7 @@ CONF_MAX_TEMP = 'max_temp'
|
|||
CONF_TARGET_TEMP = 'target_temp'
|
||||
CONF_AC_MODE = 'ac_mode'
|
||||
CONF_MIN_DUR = 'min_cycle_duration'
|
||||
CONF_TOLERANCE = 'tolerance'
|
||||
|
||||
|
||||
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_TEMP): vol.Coerce(float),
|
||||
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),
|
||||
})
|
||||
|
||||
|
@ -56,23 +58,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
target_temp = config.get(CONF_TARGET_TEMP)
|
||||
ac_mode = config.get(CONF_AC_MODE)
|
||||
min_cycle_duration = config.get(CONF_MIN_DUR)
|
||||
tolerance = config.get(CONF_TOLERANCE)
|
||||
|
||||
add_devices([GenericThermostat(
|
||||
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):
|
||||
"""Representation of a GenericThermostat device."""
|
||||
|
||||
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."""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self.heater_entity_id = heater_entity_id
|
||||
self.ac_mode = ac_mode
|
||||
self.min_cycle_duration = min_cycle_duration
|
||||
self._tolerance = tolerance
|
||||
|
||||
self._active = False
|
||||
self._cur_temp = None
|
||||
|
@ -193,7 +198,7 @@ class GenericThermostat(ClimateDevice):
|
|||
return
|
||||
|
||||
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
|
||||
if too_hot and not is_cooling:
|
||||
_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)
|
||||
switch.turn_off(self.hass, self.heater_entity_id)
|
||||
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
|
||||
|
||||
if too_cold and not is_heating:
|
||||
|
|
|
@ -5,10 +5,11 @@ For more details about this platform, please refer to the documentation at
|
|||
https://home-assistant.io/components/climate.homematic/
|
||||
"""
|
||||
import logging
|
||||
import homeassistant.components.homematic as homematic
|
||||
from homeassistant.components.climate import ClimateDevice, STATE_AUTO
|
||||
from homeassistant.components.homematic import HMDevice
|
||||
from homeassistant.util.temperature import convert
|
||||
from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
DEPENDENCIES = ['homematic']
|
||||
|
||||
|
@ -29,14 +30,16 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
|||
if discovery_info is None:
|
||||
return
|
||||
|
||||
homematic = get_component("homematic")
|
||||
return homematic.setup_hmdevice_discovery_helper(
|
||||
hass,
|
||||
HMThermostat,
|
||||
discovery_info,
|
||||
add_callback_devices
|
||||
)
|
||||
|
||||
|
||||
class HMThermostat(homematic.HMDevice, ClimateDevice):
|
||||
class HMThermostat(HMDevice, ClimateDevice):
|
||||
"""Representation of a Homematic thermostat."""
|
||||
|
||||
@property
|
||||
|
@ -94,13 +97,9 @@ class HMThermostat(homematic.HMDevice, ClimateDevice):
|
|||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if not self.available:
|
||||
if not self.available or temperature is 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)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
|
|
|
@ -14,7 +14,8 @@ from homeassistant.components.climate import (
|
|||
PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||
ATTR_TEMPERATURE)
|
||||
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']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -24,10 +25,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
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):
|
||||
"""Setup the Nest thermostat."""
|
||||
_LOGGER.debug("Setting up nest thermostat")
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
temp_unit = hass.config.units.temperature_unit
|
||||
|
||||
add_devices(
|
||||
[NestThermostat(structure, device, temp_unit)
|
||||
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:
|
||||
self._operation_list.append(STATE_AUTO)
|
||||
|
||||
self._operation_list.append(STATE_ECO)
|
||||
|
||||
# feature of device
|
||||
self._has_humidifier = self.device.has_humidifier
|
||||
self._has_dehumidifier = self.device.has_dehumidifier
|
||||
self._has_fan = self.device.has_fan
|
||||
|
||||
# data attributes
|
||||
|
@ -68,41 +77,24 @@ class NestThermostat(ClimateDevice):
|
|||
self._location = None
|
||||
self._name = None
|
||||
self._humidity = None
|
||||
self._target_humidity = None
|
||||
self._target_temperature = None
|
||||
self._temperature = None
|
||||
self._temperature_scale = None
|
||||
self._mode = None
|
||||
self._fan = None
|
||||
self._away_temperature = None
|
||||
self._eco_temperature = None
|
||||
self._is_locked = None
|
||||
self._locked_temperature = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the nest, if any."""
|
||||
if self._location is None:
|
||||
return self._name
|
||||
else:
|
||||
if self._name == '':
|
||||
return self._location.capitalize()
|
||||
else:
|
||||
return self._location.capitalize() + '(' + self._name + ')'
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@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 {}
|
||||
return self._temperature_scale
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
|
@ -112,21 +104,17 @@ class NestThermostat(ClimateDevice):
|
|||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if self._mode == 'cool':
|
||||
return STATE_COOL
|
||||
elif self._mode == 'heat':
|
||||
return STATE_HEAT
|
||||
elif self._mode == 'range':
|
||||
if self._mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]:
|
||||
return self._mode
|
||||
elif self._mode == STATE_HEAT_COOL:
|
||||
return STATE_AUTO
|
||||
elif self._mode == 'off':
|
||||
return STATE_OFF
|
||||
else:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""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
|
||||
else:
|
||||
return None
|
||||
|
@ -134,10 +122,11 @@ class NestThermostat(ClimateDevice):
|
|||
@property
|
||||
def target_temperature_low(self):
|
||||
"""Return the lower bound temperature we try to reach."""
|
||||
if self.is_away_mode_on and self._away_temperature[0]:
|
||||
# away_temperature is always a low, high tuple
|
||||
return self._away_temperature[0]
|
||||
if self._mode == 'range':
|
||||
if (self.is_away_mode_on or self._mode == STATE_ECO) and \
|
||||
self._eco_temperature[0]:
|
||||
# eco_temperature is always a low, high tuple
|
||||
return self._eco_temperature[0]
|
||||
if self._mode == STATE_HEAT_COOL:
|
||||
return self._target_temperature[0]
|
||||
else:
|
||||
return None
|
||||
|
@ -145,10 +134,11 @@ class NestThermostat(ClimateDevice):
|
|||
@property
|
||||
def target_temperature_high(self):
|
||||
"""Return the upper bound temperature we try to reach."""
|
||||
if self.is_away_mode_on and self._away_temperature[1]:
|
||||
# away_temperature is always a low, high tuple
|
||||
return self._away_temperature[1]
|
||||
if self._mode == 'range':
|
||||
if (self.is_away_mode_on or self._mode == STATE_ECO) and \
|
||||
self._eco_temperature[1]:
|
||||
# eco_temperature is always a low, high tuple
|
||||
return self._eco_temperature[1]
|
||||
if self._mode == STATE_HEAT_COOL:
|
||||
return self._target_temperature[1]
|
||||
else:
|
||||
return None
|
||||
|
@ -163,8 +153,7 @@ class NestThermostat(ClimateDevice):
|
|||
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
if target_temp_low is not None and target_temp_high is not None:
|
||||
|
||||
if self._mode == 'range':
|
||||
if self._mode == STATE_HEAT_COOL:
|
||||
temp = (target_temp_low, target_temp_high)
|
||||
else:
|
||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
||||
|
@ -173,14 +162,11 @@ class NestThermostat(ClimateDevice):
|
|||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
if operation_mode == STATE_HEAT:
|
||||
self.device.mode = 'heat'
|
||||
elif operation_mode == STATE_COOL:
|
||||
self.device.mode = 'cool'
|
||||
if operation_mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]:
|
||||
device_mode = operation_mode
|
||||
elif operation_mode == STATE_AUTO:
|
||||
self.device.mode = 'range'
|
||||
elif operation_mode == STATE_OFF:
|
||||
self.device.mode = 'off'
|
||||
device_mode = STATE_HEAT_COOL
|
||||
self.device.mode = device_mode
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
|
@ -217,30 +203,33 @@ class NestThermostat(ClimateDevice):
|
|||
@property
|
||||
def min_temp(self):
|
||||
"""Identify min_temp in Nest API or defaults if not available."""
|
||||
temp = self._away_temperature[0]
|
||||
if temp is None:
|
||||
return super().min_temp
|
||||
if self._is_locked:
|
||||
return self._locked_temperature[0]
|
||||
else:
|
||||
return temp
|
||||
return None
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Identify max_temp in Nest API or defaults if not available."""
|
||||
temp = self._away_temperature[1]
|
||||
if temp is None:
|
||||
return super().max_temp
|
||||
if self._is_locked:
|
||||
return self._locked_temperature[1]
|
||||
else:
|
||||
return temp
|
||||
return None
|
||||
|
||||
def update(self):
|
||||
"""Cache value from Python-nest."""
|
||||
self._location = self.device.where
|
||||
self._name = self.device.name
|
||||
self._humidity = self.device.humidity,
|
||||
self._target_humidity = self.device.target_humidity,
|
||||
self._temperature = self.device.temperature
|
||||
self._mode = self.device.mode
|
||||
self._target_temperature = self.device.target
|
||||
self._fan = self.device.fan
|
||||
self._away = self.structure.away
|
||||
self._away_temperature = self.device.away_temperature
|
||||
self._away = self.structure.away == 'away'
|
||||
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
|
||||
|
|
|
@ -7,7 +7,8 @@ https://home-assistant.io/components/climate.proliphix/
|
|||
import voluptuous as vol
|
||||
|
||||
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 (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
@ -60,6 +61,15 @@ class ProliphixThermostat(ClimateDevice):
|
|||
"""Return the name of the thermostat."""
|
||||
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
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
|
|
|
@ -30,7 +30,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
"""Setup the Wink thermostat."""
|
||||
import pywink
|
||||
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())
|
||||
|
||||
|
||||
|
@ -38,9 +38,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
"""Representation of a Wink thermostat."""
|
||||
|
||||
def __init__(self, wink, temp_unit):
|
||||
def __init__(self, wink, hass, temp_unit):
|
||||
"""Initialize the Wink device."""
|
||||
super().__init__(wink)
|
||||
super().__init__(wink, hass)
|
||||
wink = get_component('wink')
|
||||
self._config_temp_unit = temp_unit
|
||||
|
||||
|
|
|
@ -12,7 +12,8 @@ import logging
|
|||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.components.cover import CoverDevice,\
|
||||
ATTR_POSITION
|
||||
import homeassistant.components.homematic as homematic
|
||||
from homeassistant.components.homematic import HMDevice
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -24,14 +25,16 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None):
|
|||
if discovery_info is None:
|
||||
return
|
||||
|
||||
homematic = get_component("homematic")
|
||||
return homematic.setup_hmdevice_discovery_helper(
|
||||
hass,
|
||||
HMCover,
|
||||
discovery_info,
|
||||
add_callback_devices
|
||||
)
|
||||
|
||||
|
||||
class HMCover(homematic.HMDevice, CoverDevice):
|
||||
class HMCover(HMDevice, CoverDevice):
|
||||
"""Represents a Homematic Cover in Home Assistant."""
|
||||
|
||||
@property
|
||||
|
|
|
@ -15,18 +15,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
"""Setup the Wink cover platform."""
|
||||
import pywink
|
||||
|
||||
add_devices(WinkCoverDevice(shade) for shade in
|
||||
add_devices(WinkCoverDevice(shade, hass) for shade in
|
||||
pywink.get_shades())
|
||||
add_devices(WinkCoverDevice(door) for door in
|
||||
add_devices(WinkCoverDevice(door, hass) for door in
|
||||
pywink.get_garage_doors())
|
||||
|
||||
|
||||
class WinkCoverDevice(WinkDevice, CoverDevice):
|
||||
"""Representation of a Wink cover device."""
|
||||
|
||||
def __init__(self, wink):
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the cover."""
|
||||
WinkDevice.__init__(self, wink)
|
||||
WinkDevice.__init__(self, wink, hass)
|
||||
|
||||
def close_cover(self):
|
||||
"""Close the shade."""
|
||||
|
|
|
@ -86,16 +86,11 @@ def setup(hass, config):
|
|||
group.Group.create_group(hass, 'people', [
|
||||
'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy',
|
||||
'device_tracker.demo_paulus'])
|
||||
group.Group.create_group(hass, 'thermostats', [
|
||||
'thermostat.nest', 'thermostat.thermostat'])
|
||||
group.Group.create_group(hass, 'downstairs', [
|
||||
'group.living_room', 'group.kitchen',
|
||||
'scene.romantic_lights', 'rollershutter.kitchen_window',
|
||||
'rollershutter.living_room_window', 'group.doors',
|
||||
'thermostat.nest',
|
||||
], view=True)
|
||||
group.Group.create_group(hass, 'Upstairs', [
|
||||
'thermostat.thermostat', 'group.bedroom',
|
||||
'thermostat.ecobee',
|
||||
], view=True)
|
||||
|
||||
# Setup scripts
|
||||
|
|
|
@ -10,6 +10,8 @@ import logging
|
|||
import os
|
||||
from typing import Any, Sequence, Callable
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.bootstrap import (
|
||||
|
@ -19,6 +21,7 @@ from homeassistant.components import group, zone
|
|||
from homeassistant.components.discovery import SERVICE_NETGEAR
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
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.entity import Entity
|
||||
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
|
||||
|
@ -278,6 +281,9 @@ class DeviceTracker(object):
|
|||
yield from self.group.async_update_tracked_entity_ids(
|
||||
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
|
||||
self.hass.async_add_job(
|
||||
self.async_update_config(self.hass.config.path(YAML_DEVICES),
|
||||
|
@ -291,7 +297,7 @@ class DeviceTracker(object):
|
|||
This method is a coroutine.
|
||||
"""
|
||||
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),
|
||||
dev_id, device)
|
||||
|
||||
|
@ -328,6 +334,7 @@ class Device(Entity):
|
|||
last_seen = None # type: dt_util.dt.datetime
|
||||
battery = None # type: str
|
||||
attributes = None # type: dict
|
||||
vendor = None # type: str
|
||||
|
||||
# Track if the last update of this device was HOME.
|
||||
last_update_home = False
|
||||
|
@ -336,7 +343,7 @@ class Device(Entity):
|
|||
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
|
||||
track: bool, dev_id: str, mac: str, name: 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."""
|
||||
self.hass = hass
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
|
||||
|
@ -362,6 +369,7 @@ class Device(Entity):
|
|||
self.config_picture = picture
|
||||
|
||||
self.away_hide = hide_if_away
|
||||
self.vendor = vendor
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -460,6 +468,53 @@ class Device(Entity):
|
|||
self._state = STATE_HOME
|
||||
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):
|
||||
"""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('picture', default=None): vol.Any(None, cv.string),
|
||||
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:
|
||||
result = []
|
||||
|
@ -546,7 +602,8 @@ def update_config(path: str, dev_id: str, device: Device):
|
|||
'mac': device.mac,
|
||||
'picture': device.config_picture,
|
||||
'track': device.track,
|
||||
CONF_AWAY_HIDE: device.away_hide
|
||||
CONF_AWAY_HIDE: device.away_hide,
|
||||
'vendor': device.vendor,
|
||||
}}
|
||||
out.write('\n')
|
||||
out.write(dump(device))
|
||||
|
|
72
homeassistant/components/device_tracker/gpslogger.py
Normal file
72
homeassistant/components/device_tracker/gpslogger.py
Normal 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)
|
|
@ -23,7 +23,7 @@ DEPENDENCIES = ['http']
|
|||
|
||||
def setup_scanner(hass, config, see):
|
||||
"""Setup an endpoint for the Locative application."""
|
||||
hass.http.register_view(LocativeView(hass, see))
|
||||
hass.http.register_view(LocativeView(see))
|
||||
|
||||
return True
|
||||
|
||||
|
@ -34,27 +34,26 @@ class LocativeView(HomeAssistantView):
|
|||
url = '/api/locative'
|
||||
name = 'api:locative'
|
||||
|
||||
def __init__(self, hass, see):
|
||||
def __init__(self, see):
|
||||
"""Initialize Locative url endpoints."""
|
||||
super().__init__(hass)
|
||||
self.see = see
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""Locative message received as GET."""
|
||||
res = yield from self._handle(request.GET)
|
||||
res = yield from self._handle(request.app['hass'], request.GET)
|
||||
return res
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Locative message received."""
|
||||
data = yield from request.post()
|
||||
res = yield from self._handle(data)
|
||||
res = yield from self._handle(request.app['hass'], data)
|
||||
return res
|
||||
|
||||
@asyncio.coroutine
|
||||
# pylint: disable=too-many-return-statements
|
||||
def _handle(self, data):
|
||||
def _handle(self, hass, data):
|
||||
"""Handle locative request."""
|
||||
if 'latitude' not in data or 'longitude' not in data:
|
||||
return ('Latitude and longitude not specified.',
|
||||
|
@ -81,19 +80,19 @@ class LocativeView(HomeAssistantView):
|
|||
gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE])
|
||||
|
||||
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,
|
||||
location_name=location_name,
|
||||
gps=gps_location))
|
||||
return 'Setting location to {}'.format(location_name)
|
||||
|
||||
elif direction == 'exit':
|
||||
current_state = self.hass.states.get(
|
||||
current_state = hass.states.get(
|
||||
'{}.{}'.format(DOMAIN, device))
|
||||
|
||||
if current_state is None or current_state.state == location_name:
|
||||
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,
|
||||
location_name=location_name,
|
||||
gps=gps_location))
|
||||
|
|
|
@ -147,7 +147,7 @@ def setup_scanner(hass, config, see):
|
|||
data_type, max_gps_accuracy, payload)
|
||||
return None
|
||||
if convert(data.get('acc'), float, 1.0) == 0.0:
|
||||
_LOGGER.warning('Ignoring %s update because GPS accuracy'
|
||||
_LOGGER.warning('Ignoring %s update because GPS accuracy '
|
||||
'is zero: %s',
|
||||
data_type, payload)
|
||||
return None
|
||||
|
|
|
@ -75,14 +75,16 @@ def setup(hass, yaml_config):
|
|||
api_password=None,
|
||||
ssl_certificate=None,
|
||||
ssl_key=None,
|
||||
cors_origins=[],
|
||||
cors_origins=None,
|
||||
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(HueUsernameView(hass))
|
||||
server.register_view(HueLightsView(hass, config))
|
||||
server.register_view(DescriptionXmlView(config))
|
||||
server.register_view(HueUsernameView)
|
||||
server.register_view(HueLightsView(config))
|
||||
|
||||
upnp_listener = UPNPResponderThread(
|
||||
config.host_ip_addr, config.listen_port)
|
||||
|
@ -154,9 +156,8 @@ class DescriptionXmlView(HomeAssistantView):
|
|||
name = 'description:xml'
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, hass, config):
|
||||
def __init__(self, config):
|
||||
"""Initialize the instance of the view."""
|
||||
super().__init__(hass)
|
||||
self.config = config
|
||||
|
||||
@core.callback
|
||||
|
@ -198,10 +199,6 @@ class HueUsernameView(HomeAssistantView):
|
|||
extra_urls = ['/api/']
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initialize the instance of the view."""
|
||||
super().__init__(hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Handle a POST request."""
|
||||
|
@ -226,30 +223,33 @@ class HueLightsView(HomeAssistantView):
|
|||
'/api/{username}/lights/{entity_id}/state']
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, hass, config):
|
||||
def __init__(self, config):
|
||||
"""Initialize the instance of the view."""
|
||||
super().__init__(hass)
|
||||
self.config = config
|
||||
self.cached_states = {}
|
||||
|
||||
@core.callback
|
||||
def get(self, request, username, entity_id=None):
|
||||
"""Handle a GET request."""
|
||||
hass = request.app['hass']
|
||||
|
||||
if entity_id is None:
|
||||
return self.async_get_lights_list()
|
||||
return self.async_get_lights_list(hass)
|
||||
|
||||
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)
|
||||
|
||||
@asyncio.coroutine
|
||||
def put(self, request, username, entity_id=None):
|
||||
"""Handle a PUT request."""
|
||||
hass = request.app['hass']
|
||||
|
||||
if not request.path.endswith('state'):
|
||||
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)
|
||||
|
||||
try:
|
||||
|
@ -257,24 +257,25 @@ class HueLightsView(HomeAssistantView):
|
|||
except ValueError:
|
||||
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
|
||||
|
||||
@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."""
|
||||
json_response = {}
|
||||
|
||||
for entity in self.hass.states.async_all():
|
||||
for entity in hass.states.async_all():
|
||||
if self.is_entity_exposed(entity):
|
||||
json_response[entity.entity_id] = entity_to_json(entity)
|
||||
|
||||
return self.json(json_response)
|
||||
|
||||
@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."""
|
||||
entity = self.hass.states.get(entity_id)
|
||||
entity = hass.states.get(entity_id)
|
||||
if entity is None or not self.is_entity_exposed(entity):
|
||||
return web.Response(text="Entity not found", status=404)
|
||||
|
||||
|
@ -292,12 +293,12 @@ class HueLightsView(HomeAssistantView):
|
|||
return self.json(json_response)
|
||||
|
||||
@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."""
|
||||
config = self.config
|
||||
|
||||
# Retrieve the entity from the state machine
|
||||
entity = self.hass.states.get(entity_id)
|
||||
entity = hass.states.get(entity_id)
|
||||
if entity is None:
|
||||
return web.Response(text="Entity not found", status=404)
|
||||
|
||||
|
@ -342,8 +343,8 @@ class HueLightsView(HomeAssistantView):
|
|||
self.cached_states[entity_id] = (result, brightness)
|
||||
|
||||
# Perform the requested action
|
||||
yield from self.hass.services.async_call(core.DOMAIN, service, data,
|
||||
blocking=True)
|
||||
yield from hass.services.async_call(core.DOMAIN, service, data,
|
||||
blocking=True)
|
||||
|
||||
json_response = \
|
||||
[create_hue_success_response(entity_id, HUE_API_STATE_ON, result)]
|
||||
|
|
|
@ -75,8 +75,7 @@ def setup(hass, config):
|
|||
descriptions[DOMAIN][SERVICE_CHECKIN],
|
||||
schema=CHECKIN_SERVICE_SCHEMA)
|
||||
|
||||
hass.http.register_view(FoursquarePushReceiver(
|
||||
hass, config[CONF_PUSH_SECRET]))
|
||||
hass.http.register_view(FoursquarePushReceiver(config[CONF_PUSH_SECRET]))
|
||||
|
||||
return True
|
||||
|
||||
|
@ -88,9 +87,8 @@ class FoursquarePushReceiver(HomeAssistantView):
|
|||
url = "/api/foursquare"
|
||||
name = "foursquare"
|
||||
|
||||
def __init__(self, hass, push_secret):
|
||||
def __init__(self, push_secret):
|
||||
"""Initialize the OAuth callback view."""
|
||||
super().__init__(hass)
|
||||
self.push_secret = push_secret
|
||||
|
||||
@asyncio.coroutine
|
||||
|
@ -110,4 +108,4 @@ class FoursquarePushReceiver(HomeAssistantView):
|
|||
"push secret: %s", secret)
|
||||
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)
|
||||
|
|
|
@ -8,17 +8,18 @@ import os
|
|||
from aiohttp import web
|
||||
|
||||
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.http import HomeAssistantView
|
||||
from homeassistant.components.http.auth import is_trusted_ip
|
||||
from homeassistant.components.http.const import KEY_DEVELOPMENT
|
||||
from .version import FINGERPRINTS
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api']
|
||||
DEPENDENCIES = ['api', 'websocket_api']
|
||||
URL_PANEL_COMPONENT = '/frontend/panels/{}.html'
|
||||
URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
|
||||
STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static')
|
||||
PANELS = {}
|
||||
MANIFEST_JSON = {
|
||||
"background_color": "#FFFFFF",
|
||||
"description": "Open-source home automation platform running on Python 3.",
|
||||
|
@ -32,6 +33,16 @@ MANIFEST_JSON = {
|
|||
"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)
|
||||
_REGISTERED_COMPONENTS = set()
|
||||
_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.
|
||||
"""
|
||||
panels = hass.data.get(DATA_PANELS)
|
||||
if panels is None:
|
||||
panels = hass.data[DATA_PANELS] = {}
|
||||
|
||||
if url_path is None:
|
||||
url_path = component_name
|
||||
|
||||
if url_path in PANELS:
|
||||
if url_path in panels:
|
||||
_LOGGER.warning('Overwriting component %s', url_path)
|
||||
if not os.path.isfile(path):
|
||||
_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)
|
||||
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):
|
||||
|
@ -134,29 +157,24 @@ def setup(hass, config):
|
|||
if os.path.isdir(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')
|
||||
|
||||
for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state',
|
||||
'dev-template'):
|
||||
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
|
||||
|
||||
|
||||
|
@ -169,12 +187,14 @@ class BootstrapView(HomeAssistantView):
|
|||
@callback
|
||||
def get(self, request):
|
||||
"""Return all data needed to bootstrap Home Assistant."""
|
||||
hass = request.app['hass']
|
||||
|
||||
return self.json({
|
||||
'config': self.hass.config.as_dict(),
|
||||
'states': self.hass.states.async_all(),
|
||||
'events': api.async_events_json(self.hass),
|
||||
'services': api.async_services_json(self.hass),
|
||||
'panels': PANELS,
|
||||
'config': hass.config.as_dict(),
|
||||
'states': hass.states.async_all(),
|
||||
'events': api.async_events_json(hass),
|
||||
'services': api.async_services_json(hass),
|
||||
'panels': hass.data[DATA_PANELS],
|
||||
})
|
||||
|
||||
|
||||
|
@ -186,13 +206,10 @@ class IndexView(HomeAssistantView):
|
|||
requires_auth = False
|
||||
extra_urls = ['/states', '/states/{entity_id}']
|
||||
|
||||
def __init__(self, hass, extra_urls):
|
||||
def __init__(self):
|
||||
"""Initialize the frontend view."""
|
||||
super().__init__(hass)
|
||||
|
||||
from jinja2 import FileSystemLoader, Environment
|
||||
|
||||
self.extra_urls = self.extra_urls + extra_urls
|
||||
self.templates = Environment(
|
||||
loader=FileSystemLoader(
|
||||
os.path.join(os.path.dirname(__file__), 'templates/')
|
||||
|
@ -202,14 +219,16 @@ class IndexView(HomeAssistantView):
|
|||
@asyncio.coroutine
|
||||
def get(self, request, entity_id=None):
|
||||
"""Serve the index view."""
|
||||
hass = request.app['hass']
|
||||
|
||||
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
|
||||
not state.attributes.get(group.ATTR_VIEW)):
|
||||
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'
|
||||
ui_url = '/static/home-assistant-polymer/src/home-assistant.html'
|
||||
else:
|
||||
|
@ -223,19 +242,21 @@ class IndexView(HomeAssistantView):
|
|||
else:
|
||||
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'
|
||||
if self.hass.config.api.api_password:
|
||||
if hass.config.api.api_password:
|
||||
# require password if set
|
||||
no_auth = 'false'
|
||||
if self.hass.http.is_trusted_ip(
|
||||
self.hass.http.get_real_ip(request)):
|
||||
if is_trusted_ip(request):
|
||||
# bypass for trusted networks
|
||||
no_auth = 'true'
|
||||
|
||||
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')
|
||||
|
||||
# pylint is wrong
|
||||
|
@ -244,7 +265,7 @@ class IndexView(HomeAssistantView):
|
|||
resp = template.render(
|
||||
core_url=core_url, ui_url=ui_url, no_auth=no_auth,
|
||||
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')
|
||||
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
|
||||
|
||||
FINGERPRINTS = {
|
||||
"core.js": "5ed5e063d66eb252b5b288738c9c2d16",
|
||||
"frontend.html": "78be2dfedc4e95326cbcd9401fb17b4d",
|
||||
"core.js": "526d7d704ae478c30ae20c1426c2e4f4",
|
||||
"frontend.html": "5baa4dc3b109ca80d4c282fb12c6c23a",
|
||||
"mdi.html": "46a76f877ac9848899b8ed382427c16f",
|
||||
"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-service.html": "4a051878b92b002b8b018774ba207769",
|
||||
"panels/ha-panel-dev-service.html": "b3fe49532c5c03198fafb0c6ed58b76a",
|
||||
"panels/ha-panel-dev-state.html": "65e5f791cc467561719bf591f1386054",
|
||||
"panels/ha-panel-dev-template.html": "7d744ab7f7c08b6d6ad42069989de400",
|
||||
"panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295",
|
||||
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
|
||||
"panels/ha-panel-logbook.html": "66108d82763359a218c9695f0553de40",
|
||||
"panels/ha-panel-map.html": "49ab2d6f180f8bdea7cffaa66b8a5d3e"
|
||||
"panels/ha-panel-logbook.html": "4bc5c8370a85a4215413fbae8f85addb",
|
||||
"panels/ha-panel-map.html": "1bf6965b24d76db71a1871865cd4a3a2",
|
||||
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -1 +1 @@
|
|||
Subproject commit 6071315b1675dfef1090b4683c9639ef0f56cfc0
|
||||
Subproject commit b76ad67d4abbc0cc492fc11842c9d163b4917ead
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
125
homeassistant/components/frontend/www_static/websocket_test.html
Normal file
125
homeassistant/components/frontend/www_static/websocket_test.html
Normal 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>
|
Binary file not shown.
|
@ -184,8 +184,8 @@ def setup(hass, config):
|
|||
filters.included_entities = include[CONF_ENTITIES]
|
||||
filters.included_domains = include[CONF_DOMAINS]
|
||||
|
||||
hass.http.register_view(Last5StatesView(hass))
|
||||
hass.http.register_view(HistoryPeriodView(hass, filters))
|
||||
hass.http.register_view(Last5StatesView)
|
||||
hass.http.register_view(HistoryPeriodView(filters))
|
||||
register_built_in_panel(hass, 'history', 'History', 'mdi:poll-box')
|
||||
|
||||
return True
|
||||
|
@ -197,14 +197,10 @@ class Last5StatesView(HomeAssistantView):
|
|||
url = '/api/history/entity/{entity_id}/recent_states'
|
||||
name = 'api:history:entity-recent-states'
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initilalize the history last 5 states view."""
|
||||
super().__init__(hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request, entity_id):
|
||||
"""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)
|
||||
return self.json(result)
|
||||
|
||||
|
@ -216,9 +212,8 @@ class HistoryPeriodView(HomeAssistantView):
|
|||
name = 'api:history:view-period'
|
||||
extra_urls = ['/api/history/period/{datetime}']
|
||||
|
||||
def __init__(self, hass, filters):
|
||||
def __init__(self, filters):
|
||||
"""Initilalize the history period view."""
|
||||
super().__init__(hass)
|
||||
self.filters = filters
|
||||
|
||||
@asyncio.coroutine
|
||||
|
@ -240,7 +235,7 @@ class HistoryPeriodView(HomeAssistantView):
|
|||
end_time = start_time + one_day
|
||||
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,
|
||||
self.filters)
|
||||
|
||||
|
|
|
@ -13,9 +13,9 @@ from functools import partial
|
|||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN,
|
||||
CONF_USERNAME, CONF_PASSWORD, CONF_PLATFORM,
|
||||
ATTR_ENTITY_ID)
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_PLATFORM, CONF_HOSTS, CONF_NAME, ATTR_ENTITY_ID)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers import discovery
|
||||
|
@ -23,13 +23,10 @@ from homeassistant.config import load_yaml_config_file
|
|||
from homeassistant.util import Throttle
|
||||
|
||||
DOMAIN = 'homematic'
|
||||
REQUIREMENTS = ["pyhomematic==0.1.16"]
|
||||
|
||||
HOMEMATIC = None
|
||||
HOMEMATIC_LINK_DELAY = 0.5
|
||||
REQUIREMENTS = ["pyhomematic==0.1.18"]
|
||||
|
||||
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_LIGHTS = 'homematic.light'
|
||||
|
@ -44,12 +41,15 @@ ATTR_CHANNEL = 'channel'
|
|||
ATTR_NAME = 'name'
|
||||
ATTR_ADDRESS = 'address'
|
||||
ATTR_VALUE = 'value'
|
||||
ATTR_PROXY = 'proxy'
|
||||
|
||||
EVENT_KEYPRESS = 'homematic.keypress'
|
||||
EVENT_IMPULSE = 'homematic.impulse'
|
||||
|
||||
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 = {
|
||||
DISCOVER_SWITCHES: [
|
||||
|
@ -109,44 +109,60 @@ CONF_RESOLVENAMES_OPTIONS = [
|
|||
False
|
||||
]
|
||||
|
||||
DATA_HOMEMATIC = 'homematic'
|
||||
DATA_DELAY = 'homematic_delay'
|
||||
DATA_DEVINIT = 'homematic_devinit'
|
||||
DATA_STORE = 'homematic_store'
|
||||
|
||||
CONF_LOCAL_IP = 'local_ip'
|
||||
CONF_LOCAL_PORT = 'local_port'
|
||||
CONF_REMOTE_IP = 'remote_ip'
|
||||
CONF_REMOTE_PORT = 'remote_port'
|
||||
CONF_IP = 'ip'
|
||||
CONF_PORT = 'port'
|
||||
CONF_RESOLVENAMES = 'resolvenames'
|
||||
CONF_DELAY = 'delay'
|
||||
CONF_VARIABLES = 'variables'
|
||||
CONF_DEVICES = 'devices'
|
||||
CONF_DELAY = 'delay'
|
||||
CONF_PRIMARY = 'primary'
|
||||
|
||||
DEFAULT_LOCAL_IP = "0.0.0.0"
|
||||
DEFAULT_LOCAL_PORT = 0
|
||||
DEFAULT_RESOLVENAMES = False
|
||||
DEFAULT_REMOTE_PORT = 2001
|
||||
DEFAULT_PORT = 2001
|
||||
DEFAULT_USERNAME = "Admin"
|
||||
DEFAULT_PASSWORD = ""
|
||||
DEFAULT_VARIABLES = False
|
||||
DEFAULT_DEVICES = True
|
||||
DEFAULT_DELAY = 0.5
|
||||
DEFAULT_PRIMARY = False
|
||||
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): "homematic",
|
||||
vol.Required(ATTR_NAME): 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_PARAM): cv.string,
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_REMOTE_IP): cv.string,
|
||||
vol.Required(CONF_HOSTS): {cv.match_all: {
|
||||
vol.Required(CONF_IP): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT):
|
||||
cv.port,
|
||||
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): 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_REMOTE_PORT, default=DEFAULT_REMOTE_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_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): vol.Coerce(float),
|
||||
vol.Optional(CONF_VARIABLES, default=DEFAULT_VARIABLES): cv.boolean,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
@ -154,105 +170,155 @@ SCHEMA_SERVICE_VIRTUALKEY = vol.Schema({
|
|||
vol.Required(ATTR_ADDRESS): cv.string,
|
||||
vol.Required(ATTR_CHANNEL): vol.Coerce(int),
|
||||
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_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."""
|
||||
data = {
|
||||
ATTR_ADDRESS: address,
|
||||
ATTR_CHANNEL: channel,
|
||||
ATTR_PARAM: param,
|
||||
ATTR_PROXY: proxy,
|
||||
}
|
||||
|
||||
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."""
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
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
|
||||
def setup(hass, config):
|
||||
"""Setup the Homematic component."""
|
||||
global HOMEMATIC, HOMEMATIC_LINK_DELAY
|
||||
from pyhomematic import HMConnection
|
||||
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
|
||||
local_ip = config[DOMAIN].get(CONF_LOCAL_IP)
|
||||
local_port = config[DOMAIN].get(CONF_LOCAL_PORT)
|
||||
remote_ip = config[DOMAIN].get(CONF_REMOTE_IP)
|
||||
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)
|
||||
hass.data[DATA_DELAY] = config[DOMAIN].get(CONF_DELAY)
|
||||
hass.data[DATA_DEVINIT] = {}
|
||||
hass.data[DATA_STORE] = []
|
||||
|
||||
if remote_ip is None or local_ip is None:
|
||||
_LOGGER.error("Missing remote CCU/Homegear or local address")
|
||||
return False
|
||||
# create hosts list for pyhomematic
|
||||
remotes = {}
|
||||
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
|
||||
bound_system_callback = partial(_system_callback_handler, hass, config)
|
||||
HOMEMATIC = HMConnection(local=local_ip,
|
||||
localport=local_port,
|
||||
remote=remote_ip,
|
||||
remoteport=remote_port,
|
||||
systemcallback=bound_system_callback,
|
||||
resolvenames=resolvenames,
|
||||
rpcusername=username,
|
||||
rpcpassword=password,
|
||||
interface_id="homeassistant")
|
||||
hass.data[DATA_HOMEMATIC] = HMConnection(
|
||||
local=config[DOMAIN].get(CONF_LOCAL_IP),
|
||||
localport=config[DOMAIN].get(CONF_LOCAL_PORT),
|
||||
remotes=remotes,
|
||||
systemcallback=bound_system_callback,
|
||||
interface_id="homeassistant"
|
||||
)
|
||||
|
||||
# Start server thread, connect to peer, initialize to receive events
|
||||
HOMEMATIC.start()
|
||||
hass.data[DATA_HOMEMATIC].start()
|
||||
|
||||
# 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)
|
||||
|
||||
# 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
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_VIRTUALKEY,
|
||||
_hm_service_virtualkey,
|
||||
descriptions[DOMAIN][SERVICE_VIRTUALKEY],
|
||||
schema=SCHEMA_SERVICE_VIRTUALKEY)
|
||||
def _hm_service_virtualkey(service):
|
||||
"""Service handle virtualkey services."""
|
||||
address = service.data.get(ATTR_ADDRESS)
|
||||
channel = service.data.get(ATTR_CHANNEL)
|
||||
param = service.data.get(ATTR_PARAM)
|
||||
|
||||
entities = []
|
||||
# device not found
|
||||
hmdevice = _device_from_servicecall(hass, service)
|
||||
if hmdevice is None:
|
||||
_LOGGER.error("%s not found for service virtualkey!", address)
|
||||
return
|
||||
|
||||
##
|
||||
# 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)
|
||||
# if param exists for this device
|
||||
if param not in hmdevice.ACTIONNODE:
|
||||
_LOGGER.error("%s not datapoint in hm device %s", param, address)
|
||||
return
|
||||
|
||||
# add homematic entites
|
||||
entities.append(HMHub(hm_var_store, use_variables))
|
||||
component.add_entities(entities)
|
||||
# channel exists?
|
||||
if channel not in hmdevice.ACTIONNODE[param]:
|
||||
_LOGGER.error("%i is not a channel in hm device %s",
|
||||
channel, address)
|
||||
return
|
||||
|
||||
##
|
||||
# register set_value service if exists variables
|
||||
if not variables:
|
||||
return True
|
||||
# call key
|
||||
hmdevice.actionNodeData(param, True, channel)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_VIRTUALKEY, _hm_service_virtualkey,
|
||||
descriptions[DOMAIN][SERVICE_VIRTUALKEY],
|
||||
schema=SCHEMA_SERVICE_VIRTUALKEY)
|
||||
|
||||
def _service_handle_value(service):
|
||||
"""Set value on homematic variable object."""
|
||||
|
@ -261,12 +327,43 @@ def setup(hass, config):
|
|||
value = service.data[ATTR_VALUE]
|
||||
|
||||
for hm_variable in variable_list:
|
||||
hm_variable.hm_set(value)
|
||||
if isinstance(hm_variable, HMVariable):
|
||||
hm_variable.hm_set(value)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_SET_VALUE,
|
||||
_service_handle_value,
|
||||
descriptions[DOMAIN][SERVICE_SET_VALUE],
|
||||
schema=SCHEMA_SERVICE_SET_VALUE)
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SET_VAR_VALUE, _service_handle_value,
|
||||
descriptions[DOMAIN][SERVICE_SET_VAR_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
|
||||
|
||||
|
@ -274,22 +371,36 @@ def setup(hass, config):
|
|||
def _system_callback_handler(hass, config, src, *args):
|
||||
"""Callback handler."""
|
||||
if src == 'newDevices':
|
||||
_LOGGER.debug("newDevices with: %s", str(args))
|
||||
_LOGGER.debug("newDevices with: %s", args)
|
||||
# pylint: disable=unused-variable
|
||||
(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)
|
||||
key_dict = {}
|
||||
for dev in dev_descriptions:
|
||||
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
|
||||
# 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:
|
||||
if dev not in HOMEMATIC.devices:
|
||||
continue
|
||||
hmdevice = hass.data[DATA_HOMEMATIC].devices[proxy].get(dev)
|
||||
|
||||
hmdevice = HOMEMATIC.devices.get(dev)
|
||||
# have events?
|
||||
if len(hmdevice.EVENTNODE) > 0:
|
||||
_LOGGER.debug("Register Events from %s", dev)
|
||||
|
@ -307,7 +418,8 @@ def _system_callback_handler(hass, config, src, *args):
|
|||
('sensor', DISCOVER_SENSORS),
|
||||
('climate', DISCOVER_CLIMATE)):
|
||||
# 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
|
||||
# they are setup in HA and an event is fired
|
||||
|
@ -318,12 +430,12 @@ def _system_callback_handler(hass, config, src, *args):
|
|||
}, config)
|
||||
|
||||
|
||||
def _get_devices(device_type, keys):
|
||||
def _get_devices(hass, device_type, keys, proxy):
|
||||
"""Get the Homematic devices."""
|
||||
device_arr = []
|
||||
|
||||
for key in keys:
|
||||
device = HOMEMATIC.devices[key]
|
||||
device = hass.data[DATA_HOMEMATIC].devices[proxy][key]
|
||||
class_name = device.__class__.__name__
|
||||
metadata = {}
|
||||
|
||||
|
@ -357,6 +469,7 @@ def _get_devices(device_type, keys):
|
|||
device_dict = {
|
||||
CONF_PLATFORM: "homematic",
|
||||
ATTR_ADDRESS: key,
|
||||
ATTR_PROXY: proxy,
|
||||
ATTR_NAME: name,
|
||||
ATTR_CHANNEL: channel
|
||||
}
|
||||
|
@ -395,28 +508,29 @@ def _create_ha_name(name, channel, param, count):
|
|||
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):
|
||||
"""Helper to setup Homematic devices with discovery info."""
|
||||
devices = []
|
||||
for config in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
_LOGGER.debug("Add device %s from config: %s",
|
||||
str(hmdevicetype), str(config))
|
||||
|
||||
# create object and add to HA
|
||||
new_device = hmdevicetype(config)
|
||||
new_device = hmdevicetype(hass, config)
|
||||
new_device.link_homematic()
|
||||
devices.append(new_device)
|
||||
|
||||
add_callback_devices([new_device])
|
||||
|
||||
add_callback_devices(devices)
|
||||
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."""
|
||||
try:
|
||||
channel = int(device.split(":")[1])
|
||||
address = device.split(":")[0]
|
||||
hmdevice = HOMEMATIC.devices.get(address)
|
||||
hmdevice = hass.data[DATA_HOMEMATIC].devices[proxy].get(address)
|
||||
except (TypeError, ValueError):
|
||||
_LOGGER.error("Event handling channel convert error!")
|
||||
return
|
||||
|
@ -448,46 +562,40 @@ def _hm_event_handler(hass, device, caller, attribute, value):
|
|||
_LOGGER.warning("Event is unknown and not forwarded to HA")
|
||||
|
||||
|
||||
def _hm_service_virtualkey(call):
|
||||
"""Callback for handle virtualkey services."""
|
||||
address = call.data.get(ATTR_ADDRESS)
|
||||
channel = call.data.get(ATTR_CHANNEL)
|
||||
param = call.data.get(ATTR_PARAM)
|
||||
def _device_from_servicecall(hass, service):
|
||||
"""Extract homematic device from service call."""
|
||||
address = service.data.get(ATTR_ADDRESS)
|
||||
proxy = service.data.get(ATTR_PROXY)
|
||||
|
||||
if address not in HOMEMATIC.devices:
|
||||
_LOGGER.error("%s not found for service virtualkey!", address)
|
||||
return
|
||||
hmdevice = HOMEMATIC.devices.get(address)
|
||||
if proxy:
|
||||
return hass.data[DATA_HOMEMATIC].devices[proxy].get(address)
|
||||
|
||||
# if param exists for this device
|
||||
if hmdevice is None or param not in hmdevice.ACTIONNODE:
|
||||
_LOGGER.error("%s not datapoint in hm device %s", param, 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)
|
||||
for _, devices in hass.data[DATA_HOMEMATIC].devices.items():
|
||||
if address in devices:
|
||||
return devices[address]
|
||||
|
||||
|
||||
class HMHub(Entity):
|
||||
"""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."""
|
||||
self.hass = hass
|
||||
self._homematic = hass.data[DATA_HOMEMATIC]
|
||||
self._component = component
|
||||
self._name = name
|
||||
self._state = STATE_UNKNOWN
|
||||
self._store = variables_store
|
||||
self._store = {}
|
||||
self._use_variables = use_variables
|
||||
|
||||
self.update()
|
||||
# load data
|
||||
self._update_hub_state()
|
||||
self._init_variables()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return 'Homematic'
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
@ -504,11 +612,6 @@ class HMHub(Entity):
|
|||
"""Return the icon to use in the frontend, if any."""
|
||||
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):
|
||||
"""Update Hub data and all HM variables."""
|
||||
self._update_hub_state()
|
||||
|
@ -517,30 +620,48 @@ class HMHub(Entity):
|
|||
@Throttle(MIN_TIME_BETWEEN_UPDATE_HUB)
|
||||
def _update_hub_state(self):
|
||||
"""Retrieve latest state."""
|
||||
if HOMEMATIC is None:
|
||||
return
|
||||
state = HOMEMATIC.getServiceMessages()
|
||||
state = self._homematic.getServiceMessages(self._name)
|
||||
self._state = STATE_UNKNOWN if state is None else len(state)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATE_VAR)
|
||||
def _update_variables_state(self):
|
||||
"""Retrive all variable data and update hmvariable states."""
|
||||
if HOMEMATIC is None or not self._use_variables:
|
||||
if not self._use_variables:
|
||||
return
|
||||
variables = HOMEMATIC.getAllSystemVariables()
|
||||
if variables is not None:
|
||||
for key, value in variables.items():
|
||||
if key in self._store:
|
||||
self._store.get(key).hm_update(value)
|
||||
|
||||
variables = self._homematic.getAllSystemVariables(self._name)
|
||||
if variables is None:
|
||||
return
|
||||
|
||||
for key, value in variables.items():
|
||||
if key in self._store:
|
||||
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):
|
||||
"""The Homematic system variable."""
|
||||
|
||||
def __init__(self, name, state):
|
||||
def __init__(self, hass, hub_name, name, state):
|
||||
"""Initialize Homematic hub."""
|
||||
self.hass = hass
|
||||
self._homematic = hass.data[DATA_HOMEMATIC]
|
||||
self._state = state
|
||||
self._name = name
|
||||
self._hub_name = hub_name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -562,31 +683,41 @@ class HMVariable(Entity):
|
|||
"""Return false. Homematic Hub object update variable."""
|
||||
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):
|
||||
"""Update variable over Hub object."""
|
||||
if value != self._state:
|
||||
self._state = value
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def hm_set(self, value):
|
||||
"""Set variable on homematic controller."""
|
||||
if HOMEMATIC is not None:
|
||||
if isinstance(self._state, bool):
|
||||
value = cv.boolean(value)
|
||||
else:
|
||||
value = float(value)
|
||||
HOMEMATIC.setSystemVariable(self._name, value)
|
||||
self._state = value
|
||||
self.update_ha_state()
|
||||
if isinstance(self._state, bool):
|
||||
value = cv.boolean(value)
|
||||
else:
|
||||
value = float(value)
|
||||
self._homematic.setSystemVariable(self._hub_name, self._name, value)
|
||||
self._state = value
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
|
||||
class HMDevice(Entity):
|
||||
"""The Homematic device base object."""
|
||||
|
||||
def __init__(self, config):
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize a generic Homematic device."""
|
||||
self.hass = hass
|
||||
self._homematic = hass.data[DATA_HOMEMATIC]
|
||||
self._name = config.get(ATTR_NAME)
|
||||
self._address = config.get(ATTR_ADDRESS)
|
||||
self._proxy = config.get(ATTR_PROXY)
|
||||
self._channel = config.get(ATTR_CHANNEL)
|
||||
self._state = config.get(ATTR_PARAM)
|
||||
self._data = {}
|
||||
|
@ -636,6 +767,7 @@ class HMDevice(Entity):
|
|||
|
||||
# static attributes
|
||||
attr['ID'] = self._hmdevice.ADDRESS
|
||||
attr['proxy'] = self._proxy
|
||||
|
||||
return attr
|
||||
|
||||
|
@ -645,39 +777,31 @@ class HMDevice(Entity):
|
|||
if self._connected:
|
||||
return True
|
||||
|
||||
# pyhomematic is loaded
|
||||
if HOMEMATIC is None:
|
||||
return False
|
||||
# Init
|
||||
self._hmdevice = self._homematic.devices[self._proxy][self._address]
|
||||
self._connected = True
|
||||
|
||||
# Does a HMDevice from pyhomematic exist?
|
||||
if self._address in HOMEMATIC.devices:
|
||||
# Init
|
||||
self._hmdevice = HOMEMATIC.devices[self._address]
|
||||
self._connected = True
|
||||
# Check if Homematic class is okay for HA class
|
||||
_LOGGER.info("Start linking %s to %s", self._address, self._name)
|
||||
try:
|
||||
# Init datapoints of this object
|
||||
self._init_data()
|
||||
if self.hass.data[DATA_DELAY]:
|
||||
# We delay / pause loading of data to avoid overloading
|
||||
# of CCU / Homegear when doing auto detection
|
||||
time.sleep(self.hass.data[DATA_DELAY])
|
||||
self._load_data_from_hm()
|
||||
_LOGGER.debug("%s datastruct: %s", self._name, str(self._data))
|
||||
|
||||
# Check if Homematic class is okay for HA class
|
||||
_LOGGER.info("Start linking %s to %s", self._address, self._name)
|
||||
try:
|
||||
# Init datapoints of this object
|
||||
self._init_data()
|
||||
if HOMEMATIC_LINK_DELAY:
|
||||
# We delay / pause loading of data to avoid overloading
|
||||
# of CCU / Homegear when doing auto detection
|
||||
time.sleep(HOMEMATIC_LINK_DELAY)
|
||||
self._load_data_from_hm()
|
||||
_LOGGER.debug("%s datastruct: %s", self._name, str(self._data))
|
||||
|
||||
# Link events from pyhomatic
|
||||
self._subscribe_homematic_events()
|
||||
self._available = not self._hmdevice.UNREACH
|
||||
_LOGGER.debug("%s linking done", self._name)
|
||||
# pylint: disable=broad-except
|
||||
except Exception as err:
|
||||
self._connected = False
|
||||
_LOGGER.error("Exception while linking %s: %s",
|
||||
self._address, str(err))
|
||||
else:
|
||||
_LOGGER.debug("%s not found in HOMEMATIC.devices", self._address)
|
||||
# Link events from pyhomatic
|
||||
self._subscribe_homematic_events()
|
||||
self._available = not self._hmdevice.UNREACH
|
||||
_LOGGER.debug("%s linking done", self._name)
|
||||
# pylint: disable=broad-except
|
||||
except Exception as err:
|
||||
self._connected = False
|
||||
_LOGGER.error("Exception while linking %s: %s",
|
||||
self._address, str(err))
|
||||
|
||||
def _hm_event_callback(self, device, caller, attribute, value):
|
||||
"""Handle all pyhomematic device events."""
|
||||
|
|
|
@ -5,35 +5,38 @@ For more details about this component, please refer to the documentation at
|
|||
https://home-assistant.io/components/http/
|
||||
"""
|
||||
import asyncio
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import ssl
|
||||
from ipaddress import ip_address, ip_network
|
||||
from ipaddress import ip_network
|
||||
from pathlib import Path
|
||||
|
||||
import os
|
||||
import voluptuous as vol
|
||||
from aiohttp import web, hdrs
|
||||
from aiohttp.file_sender import FileSender
|
||||
from aiohttp.web_exceptions import (
|
||||
HTTPUnauthorized, HTTPMovedPermanently, HTTPNotModified)
|
||||
from aiohttp.web_urldispatcher import StaticRoute
|
||||
from aiohttp import web
|
||||
from aiohttp.web_exceptions import HTTPUnauthorized, HTTPMovedPermanently
|
||||
|
||||
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.remote as rem
|
||||
from homeassistant.util import get_local_ip
|
||||
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'
|
||||
REQUIREMENTS = ('aiohttp_cors==0.4.0',)
|
||||
REQUIREMENTS = ('aiohttp_cors==0.5.0',)
|
||||
|
||||
CONF_API_PASSWORD = 'api_password'
|
||||
CONF_SERVER_HOST = 'server_host'
|
||||
|
@ -44,8 +47,9 @@ CONF_SSL_KEY = 'ssl_key'
|
|||
CONF_CORS_ORIGINS = 'cors_allowed_origins'
|
||||
CONF_USE_X_FORWARDED_FOR = 'use_x_forwarded_for'
|
||||
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'
|
||||
|
||||
# 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-SHA:DES-CBC3-SHA:!DSS"
|
||||
|
||||
_FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_SERVER_HOST = '0.0.0.0'
|
||||
DEFAULT_DEVELOPMENT = '0'
|
||||
DEFAULT_LOGIN_ATTEMPT_THRESHOLD = -1
|
||||
|
||||
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.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
|
||||
vol.Optional(CONF_DEVELOPMENT, default=DEFAULT_DEVELOPMENT): cv.string,
|
||||
vol.Optional(CONF_SSL_CERTIFICATE, default=None): cv.isfile,
|
||||
vol.Optional(CONF_SSL_KEY, default=None): cv.isfile,
|
||||
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_TRUSTED_NETWORKS, default=[]):
|
||||
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: vol.Schema({
|
||||
vol.Optional(CONF_API_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_SERVER_HOST): cv.string,
|
||||
vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
|
||||
vol.Optional(CONF_DEVELOPMENT): cv.string,
|
||||
vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile,
|
||||
vol.Optional(CONF_SSL_KEY): cv.isfile,
|
||||
vol.Optional(CONF_CORS_ORIGINS): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_USE_X_FORWARDED_FOR, default=False): cv.boolean,
|
||||
vol.Optional(CONF_TRUSTED_NETWORKS):
|
||||
vol.All(cv.ensure_list, [ip_network])
|
||||
}),
|
||||
DOMAIN: HTTP_SCHEMA,
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
# TEMP TO GET TESTS TO RUN
|
||||
def request_class():
|
||||
"""."""
|
||||
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):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""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)
|
||||
server_host = conf.get(CONF_SERVER_HOST, '0.0.0.0')
|
||||
server_port = conf.get(CONF_SERVER_PORT, SERVER_PORT)
|
||||
development = str(conf.get(CONF_DEVELOPMENT, '')) == '1'
|
||||
ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
|
||||
ssl_key = conf.get(CONF_SSL_KEY)
|
||||
cors_origins = conf.get(CONF_CORS_ORIGINS, [])
|
||||
use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False)
|
||||
trusted_networks = [
|
||||
ip_network(trusted_network)
|
||||
for trusted_network in conf.get(CONF_TRUSTED_NETWORKS, [])]
|
||||
api_password = conf[CONF_API_PASSWORD]
|
||||
server_host = conf[CONF_SERVER_HOST]
|
||||
server_port = conf[CONF_SERVER_PORT]
|
||||
development = conf[CONF_DEVELOPMENT] == '1'
|
||||
ssl_certificate = conf[CONF_SSL_CERTIFICATE]
|
||||
ssl_key = conf[CONF_SSL_KEY]
|
||||
cors_origins = conf[CONF_CORS_ORIGINS]
|
||||
use_x_forwarded_for = conf[CONF_USE_X_FORWARDED_FOR]
|
||||
trusted_networks = conf[CONF_TRUSTED_NETWORKS]
|
||||
is_ban_enabled = conf[CONF_IP_BAN_ENABLED]
|
||||
login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD]
|
||||
|
||||
if api_password is not None:
|
||||
logging.getLogger('aiohttp.access').addFilter(
|
||||
HideSensitiveDataFilter(api_password))
|
||||
|
||||
server = HomeAssistantWSGI(
|
||||
hass,
|
||||
|
@ -142,7 +136,9 @@ def setup(hass, config):
|
|||
ssl_key=ssl_key,
|
||||
cors_origins=cors_origins,
|
||||
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
|
||||
|
@ -156,108 +152,40 @@ def setup(hass, config):
|
|||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_server)
|
||||
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.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,
|
||||
ssl_certificate is not None)
|
||||
|
||||
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):
|
||||
"""WSGI server for Home Assistant."""
|
||||
|
||||
def __init__(self, hass, development, api_password, ssl_certificate,
|
||||
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."""
|
||||
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.development = development
|
||||
self.api_password = api_password
|
||||
|
@ -265,9 +193,6 @@ class HomeAssistantWSGI(object):
|
|||
self.ssl_key = ssl_key
|
||||
self.server_host = server_host
|
||||
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.server = None
|
||||
|
||||
|
@ -281,9 +206,6 @@ class HomeAssistantWSGI(object):
|
|||
else:
|
||||
self.cors = None
|
||||
|
||||
# CACHE HACK
|
||||
_GZIP_FILE_SENDER.development = development
|
||||
|
||||
def register_view(self, view):
|
||||
"""Register a view with the WSGI server.
|
||||
|
||||
|
@ -293,7 +215,19 @@ class HomeAssistantWSGI(object):
|
|||
"""
|
||||
if isinstance(view, type):
|
||||
# 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)
|
||||
|
||||
|
@ -318,19 +252,15 @@ class HomeAssistantWSGI(object):
|
|||
Specify optional cache length of asset in days.
|
||||
"""
|
||||
if os.path.isdir(path):
|
||||
assert url_root.startswith('/')
|
||||
if not url_root.endswith('/'):
|
||||
url_root += '/'
|
||||
route = HAStaticRoute(url_root, path)
|
||||
self.app.router.register_route(route)
|
||||
self.app.router.add_static(url_root, path)
|
||||
return
|
||||
|
||||
filepath = Path(path)
|
||||
|
||||
@asyncio.coroutine
|
||||
def serve_file(request):
|
||||
"""Redirect to location."""
|
||||
res = yield from _GZIP_FILE_SENDER.send(request, filepath)
|
||||
"""Serve file from disk."""
|
||||
res = yield from GZIP_FILE_SENDER.send(request, filepath)
|
||||
return res
|
||||
|
||||
# aiohttp supports regex matching for variables. Using that as temp
|
||||
|
@ -359,10 +289,21 @@ class HomeAssistantWSGI(object):
|
|||
else:
|
||||
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.server = yield from self.hass.loop.create_server(
|
||||
self._handler, self.server_host, self.server_port, ssl=context)
|
||||
|
||||
self.app._frozen = False # pylint: disable=protected-access
|
||||
|
||||
@asyncio.coroutine
|
||||
def stop(self):
|
||||
"""Stop the wsgi server."""
|
||||
|
@ -372,21 +313,6 @@ class HomeAssistantWSGI(object):
|
|||
yield from self._handler.finish_connections(60.0)
|
||||
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):
|
||||
"""Base view for all views."""
|
||||
|
@ -395,22 +321,6 @@ class HomeAssistantView(object):
|
|||
extra_urls = []
|
||||
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
|
||||
def json(self, result, status_code=200):
|
||||
"""Return a JSON response."""
|
||||
|
@ -428,7 +338,7 @@ class HomeAssistantView(object):
|
|||
def file(self, request, fil):
|
||||
"""Return a file."""
|
||||
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
|
||||
|
||||
def register(self, router):
|
||||
|
@ -455,53 +365,32 @@ class HomeAssistantView(object):
|
|||
|
||||
|
||||
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
|
||||
def handle(request):
|
||||
"""Handle incoming request."""
|
||||
if not view.hass.is_running:
|
||||
if not request.app['hass'].is_running:
|
||||
return web.Response(status=503)
|
||||
|
||||
remote_addr = view.hass.http.get_real_ip(request)
|
||||
|
||||
# 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
|
||||
remote_addr = get_real_ip(request)
|
||||
authenticated = request.get(KEY_AUTHENTICATED, False)
|
||||
|
||||
if view.requires_auth and not authenticated:
|
||||
yield from process_wrong_login(request)
|
||||
_LOGGER.warning('Login attempt or request with an invalid '
|
||||
'password from %s', remote_addr)
|
||||
persistent_notification.async_create(
|
||||
view.hass,
|
||||
request.app['hass'],
|
||||
'Invalid password used from {}'.format(remote_addr),
|
||||
'Login attempt failed', NOTIFICATION_ID_LOGIN)
|
||||
raise HTTPUnauthorized()
|
||||
|
||||
request.authenticated = authenticated
|
||||
|
||||
_LOGGER.info('Serving %s to %s (auth: %s)',
|
||||
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)
|
||||
|
||||
if asyncio.iscoroutine(result):
|
66
homeassistant/components/http/auth.py
Normal file
66
homeassistant/components/http/auth.py
Normal 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)
|
132
homeassistant/components/http/ban.py
Normal file
132
homeassistant/components/http/ban.py
Normal 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_))
|
12
homeassistant/components/http/const.py
Normal file
12
homeassistant/components/http/const.py
Normal 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'
|
93
homeassistant/components/http/static.py
Normal file
93
homeassistant/components/http/static.py
Normal 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
|
25
homeassistant/components/http/util.py
Normal file
25
homeassistant/components/http/util.py
Normal 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]
|
|
@ -21,6 +21,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
CONF_DB_NAME = 'database'
|
||||
CONF_TAGS = 'tags'
|
||||
CONF_DEFAULT_MEASUREMENT = 'default_measurement'
|
||||
|
||||
DEFAULT_DATABASE = 'home_assistant'
|
||||
DEFAULT_HOST = 'localhost'
|
||||
|
@ -40,6 +41,7 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
vol.Optional(CONF_DB_NAME, default=DEFAULT_DATABASE): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
|
||||
vol.Optional(CONF_DEFAULT_MEASUREMENT): cv.string,
|
||||
vol.Optional(CONF_TAGS, default={}):
|
||||
vol.Schema({cv.string: cv.string}),
|
||||
vol.Optional(CONF_WHITELIST, default=[]):
|
||||
|
@ -65,6 +67,7 @@ def setup(hass, config):
|
|||
blacklist = conf.get(CONF_BLACKLIST)
|
||||
whitelist = conf.get(CONF_WHITELIST)
|
||||
tags = conf.get(CONF_TAGS)
|
||||
default_measurement = conf.get(CONF_DEFAULT_MEASUREMENT)
|
||||
|
||||
try:
|
||||
influx = InfluxDBClient(
|
||||
|
@ -96,7 +99,10 @@ def setup(hass, config):
|
|||
|
||||
measurement = state.attributes.get('unit_of_measurement')
|
||||
if measurement in (None, ''):
|
||||
measurement = state.entity_id
|
||||
if default_measurement:
|
||||
measurement = default_measurement
|
||||
else:
|
||||
measurement = state.entity_id
|
||||
|
||||
json_body = [
|
||||
{
|
||||
|
@ -114,7 +120,11 @@ def setup(hass, config):
|
|||
|
||||
for key, value in state.attributes.items():
|
||||
if key != 'unit_of_measurement':
|
||||
json_body[0]['fields'][key] = value
|
||||
if isinstance(value, (str, float, bool)):
|
||||
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)
|
||||
|
||||
|
|
|
@ -250,11 +250,10 @@ def setup(hass, config):
|
|||
|
||||
discovery.load_platform(hass, "sensor", DOMAIN, {}, config)
|
||||
|
||||
hass.http.register_view(iOSIdentifyDeviceView(hass))
|
||||
hass.http.register_view(iOSIdentifyDeviceView)
|
||||
|
||||
app_config = config.get(DOMAIN, {})
|
||||
hass.http.register_view(iOSPushConfigView(hass,
|
||||
app_config.get(CONF_PUSH, {})))
|
||||
hass.http.register_view(iOSPushConfigView(app_config.get(CONF_PUSH, {})))
|
||||
|
||||
return True
|
||||
|
||||
|
@ -266,9 +265,8 @@ class iOSPushConfigView(HomeAssistantView):
|
|||
url = "/api/ios/push"
|
||||
name = "api:ios:push"
|
||||
|
||||
def __init__(self, hass, push_config):
|
||||
def __init__(self, push_config):
|
||||
"""Init the view."""
|
||||
super().__init__(hass)
|
||||
self.push_config = push_config
|
||||
|
||||
@callback
|
||||
|
@ -283,10 +281,6 @@ class iOSIdentifyDeviceView(HomeAssistantView):
|
|||
url = "/api/ios/identify"
|
||||
name = "api:ios:identify"
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Init the view."""
|
||||
super().__init__(hass)
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Handle the POST request for device identification."""
|
||||
|
|
|
@ -4,6 +4,7 @@ Provides functionality to interact with lights.
|
|||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/light/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import csv
|
||||
|
@ -64,6 +65,9 @@ ATTR_FLASH = "flash"
|
|||
FLASH_SHORT = "short"
|
||||
FLASH_LONG = "long"
|
||||
|
||||
# List of possible effects
|
||||
ATTR_EFFECT_LIST = "effect_list"
|
||||
|
||||
# Apply an effect to the light, can be EFFECT_COLORLOOP.
|
||||
ATTR_EFFECT = "effect"
|
||||
EFFECT_COLORLOOP = "colorloop"
|
||||
|
@ -78,6 +82,8 @@ PROP_TO_ATTR = {
|
|||
'rgb_color': ATTR_RGB_COLOR,
|
||||
'xy_color': ATTR_XY_COLOR,
|
||||
'white_value': ATTR_WHITE_VALUE,
|
||||
'effect_list': ATTR_EFFECT_LIST,
|
||||
'effect': ATTR_EFFECT,
|
||||
'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({
|
||||
ATTR_ENTITY_ID: cv.entity_ids,
|
||||
ATTR_PROFILE: str,
|
||||
ATTR_PROFILE: cv.string,
|
||||
ATTR_TRANSITION: VALID_TRANSITION,
|
||||
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)),
|
||||
vol.Coerce(tuple)),
|
||||
ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)),
|
||||
vol.Coerce(tuple)),
|
||||
ATTR_COLOR_TEMP: vol.All(int, vol.Range(min=color_util.HASS_COLOR_MIN,
|
||||
max=color_util.HASS_COLOR_MAX)),
|
||||
ATTR_WHITE_VALUE: vol.All(int, vol.Range(min=0, max=255)),
|
||||
ATTR_COLOR_TEMP: vol.All(vol.Coerce(int),
|
||||
vol.Range(min=color_util.HASS_COLOR_MIN,
|
||||
max=color_util.HASS_COLOR_MAX)),
|
||||
ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)),
|
||||
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({
|
||||
|
@ -158,7 +165,7 @@ def async_turn_on(hass, entity_id=None, transition=None, brightness=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):
|
||||
|
@ -177,8 +184,8 @@ def async_turn_off(hass, entity_id=None, transition=None):
|
|||
] if value is not None
|
||||
}
|
||||
|
||||
hass.async_add_job(hass.services.async_call, DOMAIN, SERVICE_TURN_OFF,
|
||||
data)
|
||||
hass.async_add_job(hass.services.async_call(
|
||||
DOMAIN, SERVICE_TURN_OFF, data))
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Expose light control via statemachine and services."""
|
||||
component = EntityComponent(
|
||||
_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__),
|
||||
LIGHT_PROFILES_FILE),
|
||||
hass.config.path(LIGHT_PROFILES_FILE)]
|
||||
|
@ -221,67 +298,8 @@ def setup(hass, config):
|
|||
except vol.MultipleInvalid as ex:
|
||||
_LOGGER.error("Error parsing light profile from %s: %s",
|
||||
profile_path, ex)
|
||||
return False
|
||||
|
||||
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
|
||||
return None
|
||||
return profiles
|
||||
|
||||
|
||||
class Light(ToggleEntity):
|
||||
|
@ -314,6 +332,16 @@ class Light(ToggleEntity):
|
|||
"""Return the white value of this light between 0..255."""
|
||||
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
|
||||
def state_attributes(self):
|
||||
"""Return optional state attributes."""
|
||||
|
|
|
@ -7,25 +7,29 @@ https://home-assistant.io/components/demo/
|
|||
import random
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_WHITE_VALUE,
|
||||
ATTR_XY_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR,
|
||||
SUPPORT_WHITE_VALUE, Light)
|
||||
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT,
|
||||
ATTR_RGB_COLOR, ATTR_WHITE_VALUE, ATTR_XY_COLOR, SUPPORT_BRIGHTNESS,
|
||||
SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_RGB_COLOR, SUPPORT_WHITE_VALUE,
|
||||
Light)
|
||||
|
||||
LIGHT_COLORS = [
|
||||
[237, 224, 33],
|
||||
[255, 63, 111],
|
||||
]
|
||||
|
||||
LIGHT_EFFECT_LIST = ['rainbow', 'none']
|
||||
|
||||
LIGHT_TEMPS = [240, 380]
|
||||
|
||||
SUPPORT_DEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR |
|
||||
SUPPORT_WHITE_VALUE)
|
||||
SUPPORT_DEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT |
|
||||
SUPPORT_RGB_COLOR | SUPPORT_WHITE_VALUE)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Setup the demo light platform."""
|
||||
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("Kitchen Lights", True, LIGHT_COLORS[1], LIGHT_TEMPS[0])
|
||||
])
|
||||
|
@ -36,7 +40,7 @@ class DemoLight(Light):
|
|||
|
||||
def __init__(
|
||||
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."""
|
||||
self._name = name
|
||||
self._state = state
|
||||
|
@ -45,6 +49,8 @@ class DemoLight(Light):
|
|||
self._brightness = brightness
|
||||
self._xy_color = xy_color
|
||||
self._white = white
|
||||
self._effect_list = effect_list
|
||||
self._effect = effect
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
@ -81,6 +87,16 @@ class DemoLight(Light):
|
|||
"""Return the white value of this light between 0..255."""
|
||||
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
|
||||
def is_on(self):
|
||||
"""Return true if light is on."""
|
||||
|
@ -110,9 +126,12 @@ class DemoLight(Light):
|
|||
if ATTR_WHITE_VALUE in kwargs:
|
||||
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):
|
||||
"""Turn the light off."""
|
||||
self._state = False
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
|
|
@ -7,8 +7,9 @@ https://home-assistant.io/components/light.homematic/
|
|||
import logging
|
||||
from homeassistant.components.light import (ATTR_BRIGHTNESS,
|
||||
SUPPORT_BRIGHTNESS, Light)
|
||||
from homeassistant.components.homematic import HMDevice
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
import homeassistant.components.homematic as homematic
|
||||
from homeassistant.loader import get_component
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -22,14 +23,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
if discovery_info is None:
|
||||
return
|
||||
|
||||
homematic = get_component("homematic")
|
||||
return homematic.setup_hmdevice_discovery_helper(
|
||||
hass,
|
||||
HMLight,
|
||||
discovery_info,
|
||||
add_devices
|
||||
)
|
||||
|
||||
|
||||
class HMLight(homematic.HMDevice, Light):
|
||||
class HMLight(HMDevice, Light):
|
||||
"""Representation of a Homematic light."""
|
||||
|
||||
@property
|
||||
|
|
|
@ -61,10 +61,10 @@ class ISYLightDevice(isy.ISYDevice, Light):
|
|||
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
"""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.')
|
||||
|
||||
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."""
|
||||
if not self._node.on(val=brightness):
|
||||
_LOGGER.debug('Unable to turn on light.')
|
||||
|
|
|
@ -140,7 +140,7 @@ def state(new_state):
|
|||
# Update state.
|
||||
self._is_on = new_state
|
||||
self.group.enqueue(pipeline)
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
|
|
@ -283,7 +283,7 @@ class MqttLight(Light):
|
|||
should_update = True
|
||||
|
||||
if should_update:
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the device off."""
|
||||
|
@ -293,4 +293,4 @@ class MqttLight(Light):
|
|||
if self._optimistic:
|
||||
# Optimistically assume that switch has changed state.
|
||||
self._state = False
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
|
|
@ -216,7 +216,7 @@ class MqttJson(Light):
|
|||
should_update = True
|
||||
|
||||
if should_update:
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the device off."""
|
||||
|
@ -231,4 +231,4 @@ class MqttJson(Light):
|
|||
if self._optimistic:
|
||||
# Optimistically assume that the light has changed state.
|
||||
self._state = False
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
|
|
@ -10,8 +10,8 @@ import voluptuous as vol
|
|||
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_TRANSITION, PLATFORM_SCHEMA,
|
||||
ATTR_FLASH, SUPPORT_BRIGHTNESS, SUPPORT_FLASH,
|
||||
ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, ATTR_TRANSITION,
|
||||
PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, SUPPORT_FLASH,
|
||||
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light)
|
||||
from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, STATE_ON, STATE_OFF
|
||||
from homeassistant.components.mqtt import (
|
||||
|
@ -27,6 +27,7 @@ DEPENDENCIES = ['mqtt']
|
|||
DEFAULT_NAME = 'MQTT Template Light'
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
|
||||
CONF_EFFECT_LIST = "effect_list"
|
||||
CONF_COMMAND_ON_TEMPLATE = 'command_on_template'
|
||||
CONF_COMMAND_OFF_TEMPLATE = 'command_off_template'
|
||||
CONF_STATE_TEMPLATE = 'state_template'
|
||||
|
@ -34,12 +35,14 @@ CONF_BRIGHTNESS_TEMPLATE = 'brightness_template'
|
|||
CONF_RED_TEMPLATE = 'red_template'
|
||||
CONF_GREEN_TEMPLATE = 'green_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)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
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.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
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_GREEN_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_QOS, default=mqtt.DEFAULT_QOS):
|
||||
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(
|
||||
hass,
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_EFFECT_LIST),
|
||||
{
|
||||
key: config.get(key) for key in (
|
||||
CONF_STATE_TOPIC,
|
||||
|
@ -75,7 +80,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
CONF_BRIGHTNESS_TEMPLATE,
|
||||
CONF_RED_TEMPLATE,
|
||||
CONF_GREEN_TEMPLATE,
|
||||
CONF_BLUE_TEMPLATE
|
||||
CONF_BLUE_TEMPLATE,
|
||||
CONF_EFFECT_TEMPLATE
|
||||
)
|
||||
},
|
||||
config.get(CONF_OPTIMISTIC),
|
||||
|
@ -87,10 +93,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
class MqttTemplate(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."""
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._effect_list = effect_list
|
||||
self._topics = topics
|
||||
self._templates = templates
|
||||
for tpl in self._templates.values():
|
||||
|
@ -114,6 +122,7 @@ class MqttTemplate(Light):
|
|||
self._rgb = [0, 0, 0]
|
||||
else:
|
||||
self._rgb = None
|
||||
self._effect = None
|
||||
|
||||
def state_received(topic, payload, qos):
|
||||
"""A new MQTT message has been received."""
|
||||
|
@ -152,6 +161,17 @@ class MqttTemplate(Light):
|
|||
except ValueError:
|
||||
_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()
|
||||
|
||||
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 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):
|
||||
"""Turn the entity on."""
|
||||
# state
|
||||
|
@ -214,6 +244,10 @@ class MqttTemplate(Light):
|
|||
if self._optimistic:
|
||||
self._rgb = kwargs[ATTR_RGB_COLOR]
|
||||
|
||||
# effect
|
||||
if ATTR_EFFECT in kwargs:
|
||||
values['effect'] = kwargs.get(ATTR_EFFECT)
|
||||
|
||||
# flash
|
||||
if ATTR_FLASH in kwargs:
|
||||
values['flash'] = kwargs.get(ATTR_FLASH)
|
||||
|
@ -229,7 +263,7 @@ class MqttTemplate(Light):
|
|||
)
|
||||
|
||||
if self._optimistic:
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the entity off."""
|
||||
|
@ -249,4 +283,4 @@ class MqttTemplate(Light):
|
|||
)
|
||||
|
||||
if self._optimistic:
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
|
|
@ -115,7 +115,7 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light):
|
|||
# optimistically assume that light has changed state
|
||||
self._state = True
|
||||
self._values[set_req.V_LIGHT] = STATE_ON
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _turn_on_dimmer(self, **kwargs):
|
||||
"""Turn on dimmer child device."""
|
||||
|
@ -135,7 +135,7 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light):
|
|||
# optimistically assume that light has changed state
|
||||
self._brightness = brightness
|
||||
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):
|
||||
"""Turn on RGB or RGBW child device."""
|
||||
|
@ -165,7 +165,7 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light):
|
|||
self._white = white
|
||||
if 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):
|
||||
"""Turn off light child device."""
|
||||
|
@ -211,7 +211,7 @@ class MySensorsLight(mysensors.MySensorsDeviceEntity, Light):
|
|||
self._state = False
|
||||
self._values[value_type] = (
|
||||
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):
|
||||
"""Update the controller with values from light child."""
|
||||
|
|
|
@ -19,7 +19,8 @@ from homeassistant.components.light import (
|
|||
SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, PLATFORM_SCHEMA)
|
||||
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__)
|
||||
|
||||
|
@ -92,37 +93,43 @@ class OsramLightifyLight(Light):
|
|||
self._light = light
|
||||
self._light_id = light_id
|
||||
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
|
||||
def name(self):
|
||||
"""Return the name of the device if any."""
|
||||
return self._light.name()
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
"""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
|
||||
def color_temp(self):
|
||||
"""Return the color temperature."""
|
||||
o_temp = self._light.temp()
|
||||
temperature = int(TEMP_MIN_HASS + (TEMP_MAX_HASS - TEMP_MIN_HASS) *
|
||||
(o_temp - TEMP_MIN) / (TEMP_MAX - TEMP_MIN))
|
||||
return temperature
|
||||
return self._temperature
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""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
|
||||
def is_on(self):
|
||||
"""Update Status to True if device is on."""
|
||||
self.update_lights()
|
||||
_LOGGER.debug("is_on light state for light: %s is: %s",
|
||||
self._light.name(), self._light.on())
|
||||
return self._light.on()
|
||||
self._name, self._state)
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
|
@ -131,47 +138,86 @@ class OsramLightifyLight(Light):
|
|||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the device on."""
|
||||
brightness = 100
|
||||
if self.brightness:
|
||||
brightness = int(self.brightness / 2.55)
|
||||
_LOGGER.debug("turn_on Attempting to turn on light: %s ",
|
||||
self._name)
|
||||
|
||||
self._light.set_onoff(1)
|
||||
self._state = self._light.on()
|
||||
|
||||
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:
|
||||
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:
|
||||
red, green, blue = kwargs[ATTR_RGB_COLOR]
|
||||
self._light.set_rgb(red, green, blue, fade)
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
brightness = int(kwargs[ATTR_BRIGHTNESS] / 2.55)
|
||||
_LOGGER.debug("turn_on requested ATTR_RGB_COLOR for light:"
|
||||
" %s is: %s %s %s ",
|
||||
self._name, red, green, blue)
|
||||
self._light.set_rgb(red, green, blue, transition)
|
||||
|
||||
if ATTR_COLOR_TEMP in kwargs:
|
||||
color_t = kwargs[ATTR_COLOR_TEMP]
|
||||
kelvin = int(((TEMP_MAX - TEMP_MIN) * (color_t - TEMP_MIN_HASS) /
|
||||
(TEMP_MAX_HASS - TEMP_MIN_HASS)) + TEMP_MIN)
|
||||
self._light.set_temperature(kelvin, fade)
|
||||
_LOGGER.debug("turn_on requested set_temperature for light:"
|
||||
" %s: %s ", self._name, kelvin)
|
||||
self._light.set_temperature(kelvin, transition)
|
||||
|
||||
effect = kwargs.get(ATTR_EFFECT)
|
||||
if effect == EFFECT_RANDOM:
|
||||
self._light.set_rgb(random.randrange(0, 255),
|
||||
random.randrange(0, 255),
|
||||
random.randrange(0, 255),
|
||||
fade)
|
||||
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)
|
||||
|
||||
self._light.set_luminance(brightness, fade)
|
||||
self.update_ha_state()
|
||||
if ATTR_EFFECT in kwargs:
|
||||
effect = kwargs.get(ATTR_EFFECT)
|
||||
if effect == EFFECT_RANDOM:
|
||||
self._light.set_rgb(random.randrange(0, 255),
|
||||
random.randrange(0, 255),
|
||||
random.randrange(0, 255),
|
||||
transition)
|
||||
_LOGGER.debug("turn_on requested random effect for light:"
|
||||
" %s with transition %s ",
|
||||
self._name, transition)
|
||||
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the device off."""
|
||||
_LOGGER.debug("turn_off Attempting to turn off light: %s ",
|
||||
self._name)
|
||||
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:
|
||||
fade = 0
|
||||
self._light.set_luminance(0, fade)
|
||||
self.update_ha_state()
|
||||
transition = 0
|
||||
_LOGGER.debug("turn_off requested transition time for light:"
|
||||
" %s is: %s ",
|
||||
self._name, transition)
|
||||
self._light.set_onoff(0)
|
||||
self._state = self._light.on()
|
||||
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def update(self):
|
||||
"""Synchronize state with bridge."""
|
||||
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()
|
||||
|
|
|
@ -84,7 +84,7 @@ class SCSGateLight(Light):
|
|||
ToggleStatusTask(target=self._scs_id, toggled=True))
|
||||
|
||||
self._toggled = True
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the device off."""
|
||||
|
@ -94,7 +94,7 @@ class SCSGateLight(Light):
|
|||
ToggleStatusTask(target=self._scs_id, toggled=False))
|
||||
|
||||
self._toggled = False
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def process_event(self, message):
|
||||
"""Handle a SCSGate message related with this light."""
|
||||
|
|
|
@ -6,14 +6,14 @@ https://home-assistant.io/components/light.tellstick/
|
|||
"""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import tellstick
|
||||
from homeassistant.components.light import (ATTR_BRIGHTNESS,
|
||||
SUPPORT_BRIGHTNESS, Light)
|
||||
from homeassistant.components.tellstick import (DEFAULT_SIGNAL_REPETITIONS,
|
||||
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
|
||||
|
||||
|
@ -22,32 +22,25 @@ SUPPORT_TELLSTICK = SUPPORT_BRIGHTNESS
|
|||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup Tellstick lights."""
|
||||
if (discovery_info is None or
|
||||
discovery_info[ATTR_DISCOVER_DEVICES] is None or
|
||||
tellstick.TELLCORE_REGISTRY is None):
|
||||
discovery_info[ATTR_DISCOVER_DEVICES] is None):
|
||||
return
|
||||
|
||||
# Allow platform level override, fallback to module config
|
||||
signal_repetitions = discovery_info.get(ATTR_DISCOVER_CONFIG,
|
||||
DEFAULT_SIGNAL_REPETITIONS)
|
||||
|
||||
add_devices(TellstickLight(
|
||||
tellstick.TELLCORE_REGISTRY.get_device(switch_id), signal_repetitions)
|
||||
for switch_id in discovery_info[ATTR_DISCOVER_DEVICES])
|
||||
add_devices(TellstickLight(tellcore_id, signal_repetitions)
|
||||
for tellcore_id in discovery_info[ATTR_DISCOVER_DEVICES])
|
||||
|
||||
|
||||
class TellstickLight(tellstick.TellstickDevice, Light):
|
||||
class TellstickLight(TellstickDevice, Light):
|
||||
"""Representation of a Tellstick light."""
|
||||
|
||||
def __init__(self, tellstick_device, signal_repetitions):
|
||||
def __init__(self, tellcore_id, signal_repetitions):
|
||||
"""Initialize the light."""
|
||||
self._brightness = 255
|
||||
tellstick.TellstickDevice.__init__(self,
|
||||
tellstick_device,
|
||||
signal_repetitions)
|
||||
super().__init__(tellcore_id, signal_repetitions)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if switch is on."""
|
||||
return self._state
|
||||
self._brightness = 255
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
|
@ -59,37 +52,32 @@ class TellstickLight(tellstick.TellstickDevice, Light):
|
|||
"""Flag supported features."""
|
||||
return SUPPORT_TELLSTICK
|
||||
|
||||
def set_tellstick_state(self, last_command_sent, last_data_sent):
|
||||
"""Update the internal representation of the switch."""
|
||||
from tellcore.constants import TELLSTICK_TURNON, TELLSTICK_DIM
|
||||
if last_command_sent == TELLSTICK_DIM:
|
||||
if last_data_sent is not None:
|
||||
self._brightness = int(last_data_sent)
|
||||
self._state = self._brightness > 0
|
||||
def _parse_ha_data(self, kwargs):
|
||||
"""Turn the value from HA into something useful."""
|
||||
return kwargs.get(ATTR_BRIGHTNESS)
|
||||
|
||||
def _parse_tellcore_data(self, tellcore_data):
|
||||
"""Turn the value recieved from tellcore into something useful."""
|
||||
if tellcore_data is not None:
|
||||
brightness = int(tellcore_data)
|
||||
return brightness
|
||||
else:
|
||||
self._state = last_command_sent == TELLSTICK_TURNON
|
||||
return None
|
||||
|
||||
def _send_tellstick_command(self, command, data):
|
||||
"""Handle the turn_on / turn_off commands."""
|
||||
from tellcore.constants import (TELLSTICK_TURNOFF, TELLSTICK_DIM)
|
||||
if command == TELLSTICK_TURNOFF:
|
||||
self.tellstick_device.turn_off()
|
||||
elif command == TELLSTICK_DIM:
|
||||
self.tellstick_device.dim(self._brightness)
|
||||
def _update_model(self, new_state, data):
|
||||
"""Update the device entity state to match the arguments."""
|
||||
if new_state:
|
||||
brightness = data
|
||||
if brightness is not None:
|
||||
self._brightness = brightness
|
||||
|
||||
self._state = (self._brightness > 0)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Command not implemented: {}".format(command))
|
||||
self._state = False
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the switch on."""
|
||||
from tellcore.constants import TELLSTICK_DIM
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||
if brightness is not None:
|
||||
self._brightness = brightness
|
||||
|
||||
self.call_tellstick(TELLSTICK_DIM, self._brightness)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the switch off."""
|
||||
from tellcore.constants import TELLSTICK_TURNOFF
|
||||
self.call_tellstick(TELLSTICK_TURNOFF)
|
||||
def _send_tellstick_command(self):
|
||||
"""Let tellcore update the device to match the current state."""
|
||||
if self._state:
|
||||
self._tellcore_device.dim(self._brightness)
|
||||
else:
|
||||
self._tellcore_device.turn_off()
|
||||
|
|
|
@ -53,13 +53,13 @@ class VeraLight(VeraDevice, Light):
|
|||
self.vera_device.switch_on()
|
||||
|
||||
self._state = STATE_ON
|
||||
self.update_ha_state(True)
|
||||
self.schedule_update_ha_state(True)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the light off."""
|
||||
self.vera_device.switch_off()
|
||||
self._state = STATE_OFF
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
|
|
@ -23,15 +23,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
"""Setup the Wink lights."""
|
||||
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):
|
||||
"""Representation of a Wink light."""
|
||||
|
||||
def __init__(self, wink):
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the Wink device."""
|
||||
WinkDevice.__init__(self, wink)
|
||||
WinkDevice.__init__(self, wink, hass)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
|
|
@ -15,15 +15,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
"""Setup the Wink platform."""
|
||||
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):
|
||||
"""Representation of a Wink lock."""
|
||||
|
||||
def __init__(self, wink):
|
||||
def __init__(self, wink, hass):
|
||||
"""Initialize the lock."""
|
||||
WinkDevice.__init__(self, wink)
|
||||
WinkDevice.__init__(self, wink, hass)
|
||||
|
||||
@property
|
||||
def is_locked(self):
|
||||
|
|
|
@ -101,7 +101,7 @@ def setup(hass, config):
|
|||
message = message.async_render()
|
||||
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',
|
||||
'mdi:format-list-bulleted-type')
|
||||
|
@ -118,9 +118,8 @@ class LogbookView(HomeAssistantView):
|
|||
name = 'api:logbook'
|
||||
extra_urls = ['/api/logbook/{datetime}']
|
||||
|
||||
def __init__(self, hass, config):
|
||||
def __init__(self, config):
|
||||
"""Initilalize the logbook view."""
|
||||
super().__init__(hass)
|
||||
self.config = config
|
||||
|
||||
@asyncio.coroutine
|
||||
|
@ -140,13 +139,15 @@ class LogbookView(HomeAssistantView):
|
|||
def get_results():
|
||||
"""Query DB for results."""
|
||||
events = recorder.get_model('Events')
|
||||
query = recorder.query('Events').filter(
|
||||
(events.time_fired > start_day) &
|
||||
(events.time_fired < end_day))
|
||||
query = recorder.query('Events').order_by(
|
||||
events.time_fired).filter(
|
||||
(events.time_fired > start_day) &
|
||||
(events.time_fired < end_day))
|
||||
events = recorder.execute(query)
|
||||
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))
|
||||
|
||||
|
|
|
@ -17,7 +17,8 @@ from homeassistant.config import load_yaml_config_file
|
|||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
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
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
from homeassistant.const import (
|
||||
|
@ -58,6 +59,8 @@ ATTR_MEDIA_SEEK_POSITION = 'seek_position'
|
|||
ATTR_MEDIA_CONTENT_ID = 'media_content_id'
|
||||
ATTR_MEDIA_CONTENT_TYPE = 'media_content_type'
|
||||
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_ARTIST = 'media_artist'
|
||||
ATTR_MEDIA_ALBUM_NAME = 'media_album_name'
|
||||
|
@ -119,6 +122,8 @@ ATTR_TO_PROPERTY = [
|
|||
ATTR_MEDIA_CONTENT_ID,
|
||||
ATTR_MEDIA_CONTENT_TYPE,
|
||||
ATTR_MEDIA_DURATION,
|
||||
ATTR_MEDIA_POSITION,
|
||||
ATTR_MEDIA_POSITION_UPDATED_AT,
|
||||
ATTR_MEDIA_TITLE,
|
||||
ATTR_MEDIA_ARTIST,
|
||||
ATTR_MEDIA_ALBUM_NAME,
|
||||
|
@ -304,7 +309,7 @@ def setup(hass, config):
|
|||
component = EntityComponent(
|
||||
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)
|
||||
|
||||
|
@ -446,6 +451,19 @@ class MediaPlayerDevice(Entity):
|
|||
"""Duration of current playing media in seconds."""
|
||||
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
|
||||
def media_image_url(self):
|
||||
"""Image url of current playing media."""
|
||||
|
@ -704,27 +722,35 @@ def _async_fetch_image(hass, url):
|
|||
return cache_images[url]
|
||||
|
||||
content, content_type = (None, None)
|
||||
websession = async_get_clientsession(hass)
|
||||
response = None
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=hass.loop):
|
||||
response = yield from hass.websession.get(url)
|
||||
if response.status == 200:
|
||||
content = yield from response.read()
|
||||
content_type = response.headers.get(CONTENT_TYPE_HEADER)
|
||||
yield from response.release()
|
||||
response = yield from websession.get(url)
|
||||
if response.status == 200:
|
||||
content = yield from response.read()
|
||||
content_type = response.headers.get(CONTENT_TYPE_HEADER)
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
|
||||
if content:
|
||||
cache_images[url] = (content, content_type)
|
||||
cache_urls.append(url)
|
||||
finally:
|
||||
if response is not None:
|
||||
yield from response.release()
|
||||
|
||||
while len(cache_urls) > cache_maxsize:
|
||||
# remove oldest item from cache
|
||||
oldest_url = cache_urls[0]
|
||||
if oldest_url in cache_images:
|
||||
del cache_images[oldest_url]
|
||||
if not content:
|
||||
return (None, None)
|
||||
|
||||
cache_urls = cache_urls[1:]
|
||||
cache_images[url] = (content, content_type)
|
||||
cache_urls.append(url)
|
||||
|
||||
while len(cache_urls) > cache_maxsize:
|
||||
# remove oldest item from cache
|
||||
oldest_url = cache_urls[0]
|
||||
if oldest_url in cache_images:
|
||||
del cache_images[oldest_url]
|
||||
|
||||
cache_urls = cache_urls[1:]
|
||||
|
||||
return content, content_type
|
||||
|
||||
|
@ -736,9 +762,8 @@ class MediaPlayerImageView(HomeAssistantView):
|
|||
url = "/api/media_player_proxy/{entity_id}"
|
||||
name = "api:media_player:image"
|
||||
|
||||
def __init__(self, hass, entities):
|
||||
def __init__(self, entities):
|
||||
"""Initialize a media player view."""
|
||||
super().__init__(hass)
|
||||
self.entities = entities
|
||||
|
||||
@asyncio.coroutine
|
||||
|
@ -748,14 +773,14 @@ class MediaPlayerImageView(HomeAssistantView):
|
|||
if player is None:
|
||||
return web.Response(status=404)
|
||||
|
||||
authenticated = (request.authenticated or
|
||||
authenticated = (request[KEY_AUTHENTICATED] or
|
||||
request.GET.get('token') == player.access_token)
|
||||
|
||||
if not authenticated:
|
||||
return web.Response(status=401)
|
||||
|
||||
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:
|
||||
return web.Response(status=500)
|
||||
|
|
|
@ -81,6 +81,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
KNOWN_HOSTS.append(host)
|
||||
except pychromecast.ChromecastConnectionError:
|
||||
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)
|
||||
|
||||
|
|
33
homeassistant/components/media_player/denon.py
Normal file → Executable file
33
homeassistant/components/media_player/denon.py
Normal file → Executable file
|
@ -10,8 +10,9 @@ import telnetlib
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
|
||||
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||
PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_SELECT_SOURCE,
|
||||
SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF,
|
||||
SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||
MediaPlayerDevice)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN)
|
||||
|
@ -21,8 +22,9 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
DEFAULT_NAME = 'Music station'
|
||||
|
||||
SUPPORT_DENON = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
|
||||
SUPPORT_DENON = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | \
|
||||
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \
|
||||
SUPPORT_SELECT_SOURCE | SUPPORT_NEXT_TRACK | \
|
||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
@ -51,6 +53,8 @@ class DenonDevice(MediaPlayerDevice):
|
|||
self._host = host
|
||||
self._pwstate = 'PWSTANDBY'
|
||||
self._volume = 0
|
||||
self._source_list = {'TV': 'SITV', 'Tuner': 'SITUNER',
|
||||
'Internet Radio': 'SIIRP', 'Favorites': 'SIFVP'}
|
||||
self._muted = False
|
||||
self._mediasource = ''
|
||||
|
||||
|
@ -58,7 +62,14 @@ class DenonDevice(MediaPlayerDevice):
|
|||
def telnet_request(cls, telnet, command):
|
||||
"""Execute `command` and return the response."""
|
||||
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):
|
||||
"""Establish a telnet connection and sends `command`."""
|
||||
|
@ -75,9 +86,6 @@ class DenonDevice(MediaPlayerDevice):
|
|||
return False
|
||||
|
||||
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'):]
|
||||
self._volume = int(volume_str) / 60
|
||||
self._muted = (self.telnet_request(telnet, 'MU?') == 'MUON')
|
||||
|
@ -111,6 +119,11 @@ class DenonDevice(MediaPlayerDevice):
|
|||
"""Boolean if volume is currently muted."""
|
||||
return self._muted
|
||||
|
||||
@property
|
||||
def source_list(self):
|
||||
"""List of available input sources."""
|
||||
return list(self._source_list.keys())
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Current media source."""
|
||||
|
@ -161,3 +174,7 @@ class DenonDevice(MediaPlayerDevice):
|
|||
def turn_on(self):
|
||||
"""Turn the media player on."""
|
||||
self.telnet_command('PWON')
|
||||
|
||||
def select_source(self, source):
|
||||
"""Select input source."""
|
||||
self.telnet_command(self._source_list.get(source))
|
||||
|
|
245
homeassistant/components/media_player/denonavr.py
Normal file
245
homeassistant/components/media_player/denonavr.py
Normal 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)
|
171
homeassistant/components/media_player/dunehd.py
Normal file
171
homeassistant/components/media_player/dunehd.py
Normal 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()
|
|
@ -100,7 +100,7 @@ class MpdDevice(MediaPlayerDevice):
|
|||
try:
|
||||
self.status = self.client.status()
|
||||
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
|
||||
try:
|
||||
self.client.disconnect()
|
||||
|
@ -133,7 +133,7 @@ class MpdDevice(MediaPlayerDevice):
|
|||
@property
|
||||
def media_content_id(self):
|
||||
"""Content ID of current playing media."""
|
||||
return self.currentsong['id']
|
||||
return self.currentsong.get('file')
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
|
|
|
@ -120,7 +120,7 @@ class PandoraMediaPlayer(MediaPlayerDevice):
|
|||
self.update_playing_status()
|
||||
|
||||
self._player_state = STATE_IDLE
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn the media player off."""
|
||||
|
@ -138,24 +138,24 @@ class PandoraMediaPlayer(MediaPlayerDevice):
|
|||
_LOGGER.info('Killed Pianobar subprocess')
|
||||
self._pianobar = None
|
||||
self._player_state = STATE_OFF
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def media_play(self):
|
||||
"""Send play command."""
|
||||
self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE)
|
||||
self._player_state = STATE_PLAYING
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def media_pause(self):
|
||||
"""Send pause command."""
|
||||
self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE)
|
||||
self._player_state = STATE_PAUSED
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def media_next_track(self):
|
||||
"""Go to next track."""
|
||||
self._send_pianobar_command(SERVICE_MEDIA_NEXT_TRACK)
|
||||
self.update_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def supported_media_commands(self):
|
||||
|
@ -350,6 +350,8 @@ class PandoraMediaPlayer(MediaPlayerDevice):
|
|||
pass
|
||||
except pexpect.exceptions.TIMEOUT:
|
||||
pass
|
||||
except pexpect.exceptions.EOF:
|
||||
pass
|
||||
|
||||
|
||||
def _pianobar_exists():
|
||||
|
|
|
@ -9,13 +9,14 @@ from datetime import timedelta
|
|||
|
||||
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
|
||||
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']
|
||||
|
||||
|
@ -26,6 +27,9 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
|||
SUPPORT_PHILIPS_JS = SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | \
|
||||
SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE
|
||||
|
||||
SUPPORT_PHILIPS_JS_TV = SUPPORT_PHILIPS_JS | SUPPORT_NEXT_TRACK | \
|
||||
SUPPORT_PREVIOUS_TRACK
|
||||
|
||||
DEFAULT_DEVICE = 'default'
|
||||
DEFAULT_HOST = '127.0.0.1'
|
||||
DEFAULT_NAME = 'Philips TV'
|
||||
|
@ -68,6 +72,8 @@ class PhilipsTV(MediaPlayerDevice):
|
|||
self._source_list = []
|
||||
self._connfail = 0
|
||||
self._source_mapping = {}
|
||||
self._watching_tv = None
|
||||
self._channel_name = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -82,7 +88,10 @@ class PhilipsTV(MediaPlayerDevice):
|
|||
@property
|
||||
def supported_media_commands(self):
|
||||
"""Flag of media commands that are supported."""
|
||||
return SUPPORT_PHILIPS_JS
|
||||
if self._watching_tv:
|
||||
return SUPPORT_PHILIPS_JS_TV
|
||||
else:
|
||||
return SUPPORT_PHILIPS_JS
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
@ -106,6 +115,7 @@ class PhilipsTV(MediaPlayerDevice):
|
|||
self._source = source
|
||||
if not self._tv.on:
|
||||
self._state = STATE_OFF
|
||||
self._watching_tv = bool(self._tv.source_id == 'tv')
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
|
@ -141,10 +151,24 @@ class PhilipsTV(MediaPlayerDevice):
|
|||
if not self._tv.on:
|
||||
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
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
return self._source
|
||||
if self._watching_tv:
|
||||
if self._channel_name:
|
||||
return '{} - {}'.format(self._source, self._channel_name)
|
||||
else:
|
||||
return self._source
|
||||
else:
|
||||
return self._source
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
|
@ -167,3 +191,12 @@ class PhilipsTV(MediaPlayerDevice):
|
|||
self._state = STATE_ON
|
||||
else:
|
||||
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
|
||||
|
|
|
@ -204,3 +204,36 @@ sonos_clear_sleep_timer:
|
|||
entity_id:
|
||||
description: Name(s) of entites that will have the timer cleared.
|
||||
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'
|
|
@ -15,11 +15,13 @@ from homeassistant.components.media_player import (
|
|||
ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
|
||||
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST,
|
||||
SUPPORT_SELECT_SOURCE, MediaPlayerDevice)
|
||||
SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA)
|
||||
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
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
REQUIREMENTS = ['SoCo==0.12']
|
||||
|
||||
|
@ -48,9 +50,18 @@ SERVICE_CLEAR_TIMER = 'sonos_clear_sleep_timer'
|
|||
SUPPORT_SOURCE_LINEIN = 'Line-in'
|
||||
SUPPORT_SOURCE_TV = 'TV'
|
||||
|
||||
CONF_ADVERTISE_ADDR = 'advertise_addr'
|
||||
CONF_INTERFACE_ADDR = 'interface_addr'
|
||||
|
||||
# Service call validation schemas
|
||||
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({
|
||||
ATTR_ENTITY_ID: cv.entity_ids,
|
||||
})
|
||||
|
@ -69,6 +80,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
import soco
|
||||
global DEVICES
|
||||
|
||||
advertise_addr = config.get(CONF_ADVERTISE_ADDR, None)
|
||||
if advertise_addr:
|
||||
soco.config.EVENT_ADVERTISE_IP = advertise_addr
|
||||
|
||||
if discovery_info:
|
||||
player = soco.SoCo(discovery_info)
|
||||
|
||||
|
@ -86,18 +101,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
return False
|
||||
|
||||
players = None
|
||||
hosts = config.get('hosts', None)
|
||||
hosts = config.get(CONF_HOSTS, None)
|
||||
if hosts:
|
||||
# Support retro compatibility with comma separated list of hosts
|
||||
# from config
|
||||
hosts = hosts[0] if len(hosts) == 1 else hosts
|
||||
hosts = hosts.split(',') if isinstance(hosts, str) else hosts
|
||||
players = []
|
||||
for host in hosts:
|
||||
players.append(soco.SoCo(socket.gethostbyname(host)))
|
||||
|
||||
if not players:
|
||||
players = soco.discover(interface_addr=config.get('interface_addr',
|
||||
None))
|
||||
players = soco.discover(interface_addr=config.get(CONF_INTERFACE_ADDR))
|
||||
|
||||
if not players:
|
||||
_LOGGER.warning('No Sonos speakers found.')
|
||||
|
@ -264,6 +279,8 @@ class SonosDevice(MediaPlayerDevice):
|
|||
self._coordinator = None
|
||||
self._media_content_id = None
|
||||
self._media_duration = None
|
||||
self._media_position = None
|
||||
self._media_position_updated_at = None
|
||||
self._media_image_url = None
|
||||
self._media_artist = None
|
||||
self._media_album_name = None
|
||||
|
@ -404,6 +421,9 @@ class SonosDevice(MediaPlayerDevice):
|
|||
media_album_name = track_info.get('album')
|
||||
media_title = track_info.get('title')
|
||||
|
||||
media_position = None
|
||||
media_position_updated_at = None
|
||||
|
||||
is_radio_stream = \
|
||||
current_media_uri.startswith('x-sonosapi-stream:') or \
|
||||
current_media_uri.startswith('x-rincon-mp3radio:')
|
||||
|
@ -425,7 +445,6 @@ class SonosDevice(MediaPlayerDevice):
|
|||
media_image_url = None
|
||||
|
||||
elif is_radio_stream:
|
||||
is_radio_stream = True
|
||||
media_image_url = self._format_media_image_url(
|
||||
current_media_uri
|
||||
)
|
||||
|
@ -489,6 +508,46 @@ class SonosDevice(MediaPlayerDevice):
|
|||
support_next_track = 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')
|
||||
if playlist_position in ('', 'NOT_IMPLEMENTED', None):
|
||||
playlist_position = None
|
||||
|
@ -514,6 +573,8 @@ class SonosDevice(MediaPlayerDevice):
|
|||
self._media_duration = _parse_timespan(
|
||||
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_artist = media_artist
|
||||
self._media_album_name = media_album_name
|
||||
|
@ -541,6 +602,8 @@ class SonosDevice(MediaPlayerDevice):
|
|||
self._coordinator = None
|
||||
self._media_content_id = None
|
||||
self._media_duration = None
|
||||
self._media_position = None
|
||||
self._media_position_updated_at = None
|
||||
self._media_image_url = None
|
||||
self._media_artist = None
|
||||
self._media_album_name = None
|
||||
|
@ -642,6 +705,25 @@ class SonosDevice(MediaPlayerDevice):
|
|||
else:
|
||||
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
|
||||
def media_image_url(self):
|
||||
"""Image url of current playing media."""
|
||||
|
|
393
homeassistant/components/media_player/soundtouch.py
Normal file
393
homeassistant/components/media_player/soundtouch.py
Normal 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))
|
|
@ -12,7 +12,7 @@ from homeassistant.components.mqtt import PROTOCOL_311
|
|||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
REQUIREMENTS = ['hbmqtt==0.7.1']
|
||||
REQUIREMENTS = ['hbmqtt==0.8']
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
|
||||
|
|
|
@ -10,36 +10,110 @@ import socket
|
|||
import voluptuous as vol
|
||||
|
||||
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__)
|
||||
|
||||
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'
|
||||
|
||||
DATA_NEST = 'nest'
|
||||
|
||||
NEST_CONFIG_FILE = 'nest.conf'
|
||||
CONF_CLIENT_ID = 'client_id'
|
||||
CONF_CLIENT_SECRET = 'client_secret'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_CLIENT_ID): cv.string,
|
||||
vol.Required(CONF_CLIENT_SECRET): cv.string,
|
||||
vol.Optional(CONF_STRUCTURE): vol.All(cv.ensure_list, cv.string)
|
||||
})
|
||||
}, 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):
|
||||
"""Setup the Nest thermostat component."""
|
||||
import nest
|
||||
|
||||
conf = config[DOMAIN]
|
||||
username = conf[CONF_USERNAME]
|
||||
password = conf[CONF_PASSWORD]
|
||||
if 'nest' in _CONFIGURING:
|
||||
return
|
||||
|
||||
nest = nest.Nest(username, password)
|
||||
hass.data[DATA_NEST] = NestDevice(hass, conf, nest)
|
||||
conf = config[DOMAIN]
|
||||
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
|
||||
|
||||
|
@ -85,3 +159,32 @@ class NestDevice(object):
|
|||
except socket.error:
|
||||
_LOGGER.error(
|
||||
"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')
|
||||
|
|
|
@ -4,35 +4,37 @@ Provides functionality to notify people.
|
|||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/notify/
|
||||
"""
|
||||
from functools import partial
|
||||
import logging
|
||||
import os
|
||||
from functools import partial
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
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
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import CONF_NAME, CONF_PLATFORM
|
||||
from homeassistant.helpers import config_per_platform
|
||||
from homeassistant.util import slugify
|
||||
|
||||
DOMAIN = "notify"
|
||||
|
||||
# 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"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Platform specific 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({
|
||||
vol.Required(CONF_PLATFORM): cv.string,
|
||||
|
@ -46,8 +48,6 @@ NOTIFY_SERVICE_SCHEMA = vol.Schema({
|
|||
vol.Optional(ATTR_DATA): dict,
|
||||
})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def send_message(hass, message, title=None, data=None):
|
||||
"""Send a notification message."""
|
||||
|
@ -78,7 +78,7 @@ def setup(hass, config):
|
|||
hass, config, DOMAIN, platform)
|
||||
|
||||
if notify_implementation is None:
|
||||
_LOGGER.error("Unknown notification service specified.")
|
||||
_LOGGER.error("Unknown notification service specified")
|
||||
continue
|
||||
|
||||
notify_service = notify_implementation.get_service(hass, p_config)
|
||||
|
@ -114,7 +114,7 @@ def setup(hass, config):
|
|||
if hasattr(notify_service, 'targets'):
|
||||
platform_name = (p_config.get(CONF_NAME) or platform)
|
||||
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
|
||||
hass.services.register(DOMAIN, target_name,
|
||||
service_call_handler,
|
||||
|
@ -124,10 +124,9 @@ def setup(hass, config):
|
|||
platform_name = (p_config.get(CONF_NAME) or SERVICE_NOTIFY)
|
||||
platform_name_slug = slugify(platform_name)
|
||||
|
||||
hass.services.register(DOMAIN, platform_name_slug,
|
||||
service_call_handler,
|
||||
descriptions.get(SERVICE_NOTIFY),
|
||||
schema=NOTIFY_SERVICE_SCHEMA)
|
||||
hass.services.register(
|
||||
DOMAIN, platform_name_slug, service_call_handler,
|
||||
descriptions.get(SERVICE_NOTIFY), schema=NOTIFY_SERVICE_SCHEMA)
|
||||
success = True
|
||||
|
||||
return success
|
||||
|
|
|
@ -14,7 +14,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME
|
|||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['freesms==0.1.0']
|
||||
REQUIREMENTS = ['freesms==0.1.1']
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
|
|
@ -107,8 +107,8 @@ def get_service(hass, config):
|
|||
return None
|
||||
|
||||
hass.http.register_view(
|
||||
HTML5PushRegistrationView(hass, registrations, json_path))
|
||||
hass.http.register_view(HTML5PushCallbackView(hass, registrations))
|
||||
HTML5PushRegistrationView(registrations, json_path))
|
||||
hass.http.register_view(HTML5PushCallbackView(registrations))
|
||||
|
||||
gcm_api_key = config.get(ATTR_GCM_API_KEY)
|
||||
gcm_sender_id = config.get(ATTR_GCM_SENDER_ID)
|
||||
|
@ -168,9 +168,8 @@ class HTML5PushRegistrationView(HomeAssistantView):
|
|||
url = '/api/notify.html5'
|
||||
name = 'api:notify.html5'
|
||||
|
||||
def __init__(self, hass, registrations, json_path):
|
||||
def __init__(self, registrations, json_path):
|
||||
"""Init HTML5PushRegistrationView."""
|
||||
super().__init__(hass)
|
||||
self.registrations = registrations
|
||||
self.json_path = json_path
|
||||
|
||||
|
@ -237,9 +236,8 @@ class HTML5PushCallbackView(HomeAssistantView):
|
|||
url = '/api/notify.html5/callback'
|
||||
name = 'api:notify.html5/callback'
|
||||
|
||||
def __init__(self, hass, registrations):
|
||||
def __init__(self, registrations):
|
||||
"""Init HTML5PushCallbackView."""
|
||||
super().__init__(hass)
|
||||
self.registrations = registrations
|
||||
|
||||
def decode_jwt(self, token):
|
||||
|
@ -324,7 +322,7 @@ class HTML5PushCallbackView(HomeAssistantView):
|
|||
|
||||
event_name = '{}.{}'.format(NOTIFY_CALLBACK_EVENT,
|
||||
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',
|
||||
'event': event_payload[ATTR_TYPE]})
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ from homeassistant.components.notify import (
|
|||
from homeassistant.const import (CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['sendgrid==3.6.2']
|
||||
REQUIREMENTS = ['sendgrid==3.6.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ from homeassistant.const import (
|
|||
CONF_API_KEY, CONF_USERNAME, CONF_ICON)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['slacker==0.9.29']
|
||||
REQUIREMENTS = ['slacker==0.9.30']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -13,7 +13,8 @@ from homeassistant.const import (CONF_MAC, CONF_NAME, EVENT_HOMEASSISTANT_STOP)
|
|||
|
||||
REQUIREMENTS = [
|
||||
'--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']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
|
|
@ -27,7 +27,7 @@ import homeassistant.util.dt as dt_util
|
|||
|
||||
DOMAIN = 'recorder'
|
||||
|
||||
REQUIREMENTS = ['sqlalchemy==1.1.3']
|
||||
REQUIREMENTS = ['sqlalchemy==1.1.4']
|
||||
|
||||
DEFAULT_URL = 'sqlite:///{hass_config_path}'
|
||||
DEFAULT_DB_FILE = 'home-assistant_v2.db'
|
||||
|
|
144
homeassistant/components/remote/__init__.py
Executable file
144
homeassistant/components/remote/__init__.py
Executable 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()
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue