Merge pull request #4626 from home-assistant/dev

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

View file

@ -40,9 +40,6 @@ omit =
homeassistant/components/isy994.py
homeassistant/components/*/isy994.py
homeassistant/components/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
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.command_line/
"""
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,

View file

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

View file

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

View file

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

View file

@ -4,8 +4,6 @@ Support for Wink binary sensors.
For more details about this platform, please refer to the documentation at
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."""

View file

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

View file

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

View file

@ -18,6 +18,7 @@ from homeassistant.const import (
HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
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

View file

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

View file

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

View file

@ -14,12 +14,13 @@ from aiohttp import web
from aiohttp.web_exceptions import HTTPGatewayTimeout
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):

View file

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

View file

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

View file

@ -5,10 +5,11 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.homematic/
"""
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):

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -23,7 +23,7 @@ DEPENDENCIES = ['http']
def setup_scanner(hass, config, see):
"""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))

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit 6071315b1675dfef1090b4683c9639ef0f56cfc0
Subproject commit b76ad67d4abbc0cc492fc11842c9d163b4917ead

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

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

View file

@ -184,8 +184,8 @@ def setup(hass, config):
filters.included_entities = include[CONF_ENTITIES]
filters.included_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)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -21,6 +21,7 @@ _LOGGER = logging.getLogger(__name__)
CONF_DB_NAME = 'database'
CONF_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)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -100,7 +100,7 @@ class MpdDevice(MediaPlayerDevice):
try:
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):

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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