forked from home-assistant/core
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfa36f3546 | ||
|
|
96d8fbe513 | ||
|
|
1e9d91be0e | ||
|
|
2402897f47 | ||
|
|
b857d5dad0 | ||
|
|
d17753009a | ||
|
|
91a9da8f0c | ||
|
|
e3415c4e22 | ||
|
|
9bca3f3103 | ||
|
|
7c3ae884df | ||
|
|
8a4aace789 | ||
|
|
0e74cd833d | ||
|
|
5e2911f071 | ||
|
|
7dacc4a7bb | ||
|
|
0cc9555d14 | ||
|
|
d712a3dc38 | ||
|
|
84446bed14 | ||
|
|
e92b15f966 | ||
|
|
a458ce8069 | ||
|
|
5e492db9a3 | ||
|
|
6d56519297 | ||
|
|
60bcb12a48 | ||
|
|
58509f8bba | ||
|
|
db6a6fa4cb | ||
|
|
d89bfcdaa5 | ||
|
|
840e27adec | ||
|
|
31a8537ab5 | ||
|
|
3a2cdd3de0 | ||
|
|
2009e98497 | ||
|
|
b354a18bf3 | ||
|
|
1cd3cd8d77 | ||
|
|
d9556392bc | ||
|
|
695fb412cd | ||
|
|
93322b0251 | ||
|
|
9b9b625ac4 | ||
|
|
0ae6585a90 |
@@ -59,8 +59,12 @@ def setup(hass, config):
|
||||
|
||||
for alarm in target_alarms:
|
||||
getattr(alarm, method)(code)
|
||||
if alarm.should_poll:
|
||||
alarm.update_ha_state(True)
|
||||
|
||||
for alarm in target_alarms:
|
||||
if not alarm.should_poll:
|
||||
continue
|
||||
|
||||
alarm.update_ha_state(True)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
@@ -20,7 +20,7 @@ DEPENDENCIES = ['enocean']
|
||||
DEFAULT_NAME = 'EnOcean binary sensor'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ID): cv.string,
|
||||
vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_SENSOR_CLASS, default=None): SENSOR_CLASSES_SCHEMA,
|
||||
})
|
||||
|
||||
@@ -60,6 +60,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup Nest binary sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
nest = hass.data[DATA_NEST]
|
||||
conf = config.get(CONF_MONITORED_CONDITIONS, _VALID_BINARY_SENSOR_TYPES)
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ 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]
|
||||
|
||||
@@ -31,10 +31,11 @@ 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
|
||||
|
||||
_LOGGER.debug("Setting up nest thermostat")
|
||||
|
||||
temp_unit = hass.config.units.temperature_unit
|
||||
|
||||
add_devices(
|
||||
@@ -85,6 +86,8 @@ class NestThermostat(ClimateDevice):
|
||||
self._eco_temperature = None
|
||||
self._is_locked = None
|
||||
self._locked_temperature = None
|
||||
self._min_temperature = None
|
||||
self._max_temperature = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -203,18 +206,12 @@ class NestThermostat(ClimateDevice):
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Identify min_temp in Nest API or defaults if not available."""
|
||||
if self._is_locked:
|
||||
return self._locked_temperature[0]
|
||||
else:
|
||||
return None
|
||||
return self._min_temperature
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Identify max_temp in Nest API or defaults if not available."""
|
||||
if self._is_locked:
|
||||
return self._locked_temperature[1]
|
||||
else:
|
||||
return None
|
||||
return self._max_temperature
|
||||
|
||||
def update(self):
|
||||
"""Cache value from Python-nest."""
|
||||
@@ -228,8 +225,10 @@ class NestThermostat(ClimateDevice):
|
||||
self._away = self.structure.away == 'away'
|
||||
self._eco_temperature = self.device.eco_temperature
|
||||
self._locked_temperature = self.device.locked_temperature
|
||||
self._min_temperature = self.device.min_temperature
|
||||
self._max_temperature = self.device.max_temperature
|
||||
self._is_locked = self.device.is_locked
|
||||
if self.device.temperature == 'C':
|
||||
if self.device.temperature_scale == 'C':
|
||||
self._temperature_scale = TEMP_CELSIUS
|
||||
else:
|
||||
self._temperature_scale = TEMP_FAHRENHEIT
|
||||
|
||||
@@ -135,12 +135,19 @@ def setup(hass, config):
|
||||
params = service.data.copy()
|
||||
params.pop(ATTR_ENTITY_ID, None)
|
||||
|
||||
if method:
|
||||
for cover in component.extract_from_service(service):
|
||||
getattr(cover, method['method'])(**params)
|
||||
if not method:
|
||||
return
|
||||
|
||||
if cover.should_poll:
|
||||
cover.update_ha_state(True)
|
||||
covers = component.extract_from_service(service)
|
||||
|
||||
for cover in covers:
|
||||
getattr(cover, method['method'])(**params)
|
||||
|
||||
for cover in covers:
|
||||
if not cover.should_poll:
|
||||
continue
|
||||
|
||||
cover.update_ha_state(True)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
@@ -1,566 +0,0 @@
|
||||
"""
|
||||
Support for local control of entities by emulating the Phillips Hue bridge.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/emulated_hue/
|
||||
"""
|
||||
import asyncio
|
||||
import threading
|
||||
import socket
|
||||
import logging
|
||||
import os
|
||||
import select
|
||||
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import util, core
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
||||
STATE_ON, STATE_OFF, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
|
||||
)
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS
|
||||
)
|
||||
from homeassistant.components.http import (
|
||||
HomeAssistantView, HomeAssistantWSGI
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DOMAIN = 'emulated_hue'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_HOST_IP = 'host_ip'
|
||||
CONF_LISTEN_PORT = 'listen_port'
|
||||
CONF_OFF_MAPS_TO_ON_DOMAINS = 'off_maps_to_on_domains'
|
||||
CONF_EXPOSE_BY_DEFAULT = 'expose_by_default'
|
||||
CONF_EXPOSED_DOMAINS = 'exposed_domains'
|
||||
|
||||
ATTR_EMULATED_HUE = 'emulated_hue'
|
||||
ATTR_EMULATED_HUE_NAME = 'emulated_hue_name'
|
||||
|
||||
DEFAULT_LISTEN_PORT = 8300
|
||||
DEFAULT_OFF_MAPS_TO_ON_DOMAINS = ['script', 'scene']
|
||||
DEFAULT_EXPOSE_BY_DEFAULT = True
|
||||
DEFAULT_EXPOSED_DOMAINS = [
|
||||
'switch', 'light', 'group', 'input_boolean', 'media_player', 'fan'
|
||||
]
|
||||
|
||||
HUE_API_STATE_ON = 'on'
|
||||
HUE_API_STATE_BRI = 'bri'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_HOST_IP): cv.string,
|
||||
vol.Optional(CONF_LISTEN_PORT, default=DEFAULT_LISTEN_PORT):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
|
||||
vol.Optional(CONF_OFF_MAPS_TO_ON_DOMAINS): cv.ensure_list,
|
||||
vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean,
|
||||
vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, yaml_config):
|
||||
"""Activate the emulated_hue component."""
|
||||
config = Config(yaml_config)
|
||||
|
||||
server = HomeAssistantWSGI(
|
||||
hass,
|
||||
development=False,
|
||||
server_host=config.host_ip_addr,
|
||||
server_port=config.listen_port,
|
||||
api_password=None,
|
||||
ssl_certificate=None,
|
||||
ssl_key=None,
|
||||
cors_origins=None,
|
||||
use_x_forwarded_for=False,
|
||||
trusted_networks=[],
|
||||
login_threshold=0,
|
||||
is_ban_enabled=False
|
||||
)
|
||||
|
||||
server.register_view(DescriptionXmlView(config))
|
||||
server.register_view(HueUsernameView)
|
||||
server.register_view(HueLightsView(config))
|
||||
|
||||
upnp_listener = UPNPResponderThread(
|
||||
config.host_ip_addr, config.listen_port)
|
||||
|
||||
@asyncio.coroutine
|
||||
def stop_emulated_hue_bridge(event):
|
||||
"""Stop the emulated hue bridge."""
|
||||
upnp_listener.stop()
|
||||
yield from server.stop()
|
||||
|
||||
@asyncio.coroutine
|
||||
def start_emulated_hue_bridge(event):
|
||||
"""Start the emulated hue bridge."""
|
||||
upnp_listener.start()
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||
stop_emulated_hue_bridge)
|
||||
yield from server.start()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class Config(object):
|
||||
"""Holds configuration variables for the emulated hue bridge."""
|
||||
|
||||
def __init__(self, yaml_config):
|
||||
"""Initialize the instance."""
|
||||
conf = yaml_config.get(DOMAIN, {})
|
||||
|
||||
# Get the IP address that will be passed to the Echo during discovery
|
||||
self.host_ip_addr = conf.get(CONF_HOST_IP)
|
||||
if self.host_ip_addr is None:
|
||||
self.host_ip_addr = util.get_local_ip()
|
||||
_LOGGER.warning(
|
||||
"Listen IP address not specified, auto-detected address is %s",
|
||||
self.host_ip_addr)
|
||||
|
||||
# Get the port that the Hue bridge will listen on
|
||||
self.listen_port = conf.get(CONF_LISTEN_PORT)
|
||||
if not isinstance(self.listen_port, int):
|
||||
self.listen_port = DEFAULT_LISTEN_PORT
|
||||
_LOGGER.warning(
|
||||
"Listen port not specified, defaulting to %s",
|
||||
self.listen_port)
|
||||
|
||||
# Get domains that cause both "on" and "off" commands to map to "on"
|
||||
# This is primarily useful for things like scenes or scripts, which
|
||||
# don't really have a concept of being off
|
||||
self.off_maps_to_on_domains = conf.get(CONF_OFF_MAPS_TO_ON_DOMAINS)
|
||||
if not isinstance(self.off_maps_to_on_domains, list):
|
||||
self.off_maps_to_on_domains = DEFAULT_OFF_MAPS_TO_ON_DOMAINS
|
||||
|
||||
# Get whether or not entities should be exposed by default, or if only
|
||||
# explicitly marked ones will be exposed
|
||||
self.expose_by_default = conf.get(
|
||||
CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT)
|
||||
|
||||
# Get domains that are exposed by default when expose_by_default is
|
||||
# True
|
||||
self.exposed_domains = conf.get(
|
||||
CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS)
|
||||
|
||||
|
||||
class DescriptionXmlView(HomeAssistantView):
|
||||
"""Handles requests for the description.xml file."""
|
||||
|
||||
url = '/description.xml'
|
||||
name = 'description:xml'
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the instance of the view."""
|
||||
self.config = config
|
||||
|
||||
@core.callback
|
||||
def get(self, request):
|
||||
"""Handle a GET request."""
|
||||
xml_template = """<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<root xmlns="urn:schemas-upnp-org:device-1-0">
|
||||
<specVersion>
|
||||
<major>1</major>
|
||||
<minor>0</minor>
|
||||
</specVersion>
|
||||
<URLBase>http://{0}:{1}/</URLBase>
|
||||
<device>
|
||||
<deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType>
|
||||
<friendlyName>HASS Bridge ({0})</friendlyName>
|
||||
<manufacturer>Royal Philips Electronics</manufacturer>
|
||||
<manufacturerURL>http://www.philips.com</manufacturerURL>
|
||||
<modelDescription>Philips hue Personal Wireless Lighting</modelDescription>
|
||||
<modelName>Philips hue bridge 2015</modelName>
|
||||
<modelNumber>BSB002</modelNumber>
|
||||
<modelURL>http://www.meethue.com</modelURL>
|
||||
<serialNumber>1234</serialNumber>
|
||||
<UDN>uuid:2f402f80-da50-11e1-9b23-001788255acc</UDN>
|
||||
</device>
|
||||
</root>
|
||||
"""
|
||||
|
||||
resp_text = xml_template.format(
|
||||
self.config.host_ip_addr, self.config.listen_port)
|
||||
|
||||
return web.Response(text=resp_text, content_type='text/xml')
|
||||
|
||||
|
||||
class HueUsernameView(HomeAssistantView):
|
||||
"""Handle requests to create a username for the emulated hue bridge."""
|
||||
|
||||
url = '/api'
|
||||
name = 'hue:api'
|
||||
extra_urls = ['/api/']
|
||||
requires_auth = False
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Handle a POST request."""
|
||||
try:
|
||||
data = yield from request.json()
|
||||
except ValueError:
|
||||
return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
|
||||
|
||||
if 'devicetype' not in data:
|
||||
return self.json_message('devicetype not specified',
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
return self.json([{'success': {'username': '12345678901234567890'}}])
|
||||
|
||||
|
||||
class HueLightsView(HomeAssistantView):
|
||||
"""Handle requests for getting and setting info about entities."""
|
||||
|
||||
url = '/api/{username}/lights'
|
||||
name = 'api:username:lights'
|
||||
extra_urls = ['/api/{username}/lights/{entity_id}',
|
||||
'/api/{username}/lights/{entity_id}/state']
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the instance of the view."""
|
||||
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(hass)
|
||||
|
||||
if not request.path.endswith('state'):
|
||||
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 hass.states.get(entity_id) is None:
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
|
||||
try:
|
||||
json_data = yield from request.json()
|
||||
except ValueError:
|
||||
return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
|
||||
|
||||
result = yield from self.async_put_light_state(hass, json_data,
|
||||
entity_id)
|
||||
return result
|
||||
|
||||
@core.callback
|
||||
def async_get_lights_list(self, hass):
|
||||
"""Process a request to get the list of available lights."""
|
||||
json_response = {}
|
||||
|
||||
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, hass, entity_id):
|
||||
"""Process a request to get the state of an individual light."""
|
||||
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)
|
||||
|
||||
cached_state = self.cached_states.get(entity_id, None)
|
||||
|
||||
if cached_state is None:
|
||||
final_state = entity.state == STATE_ON
|
||||
final_brightness = entity.attributes.get(
|
||||
ATTR_BRIGHTNESS, 255 if final_state else 0)
|
||||
else:
|
||||
final_state, final_brightness = cached_state
|
||||
|
||||
json_response = entity_to_json(entity, final_state, final_brightness)
|
||||
|
||||
return self.json(json_response)
|
||||
|
||||
@asyncio.coroutine
|
||||
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 = hass.states.get(entity_id)
|
||||
if entity is None:
|
||||
return web.Response(text="Entity not found", status=404)
|
||||
|
||||
if not self.is_entity_exposed(entity):
|
||||
return web.Response(text="Entity not found", status=404)
|
||||
|
||||
# Parse the request into requested "on" status and brightness
|
||||
parsed = parse_hue_api_put_light_body(request_json, entity)
|
||||
|
||||
if parsed is None:
|
||||
return web.Response(text="Bad request", status=400)
|
||||
|
||||
result, brightness = parsed
|
||||
|
||||
# Convert the resulting "on" status into the service we need to call
|
||||
service = SERVICE_TURN_ON if result else SERVICE_TURN_OFF
|
||||
|
||||
# Construct what we need to send to the service
|
||||
data = {ATTR_ENTITY_ID: entity_id}
|
||||
|
||||
# If the requested entity is a script add some variables
|
||||
if entity.domain.lower() == "script":
|
||||
data['variables'] = {
|
||||
'requested_state': STATE_ON if result else STATE_OFF
|
||||
}
|
||||
|
||||
if brightness is not None:
|
||||
data['variables']['requested_level'] = brightness
|
||||
|
||||
elif brightness is not None:
|
||||
data[ATTR_BRIGHTNESS] = brightness
|
||||
|
||||
if entity.domain.lower() in config.off_maps_to_on_domains:
|
||||
# Map the off command to on
|
||||
service = SERVICE_TURN_ON
|
||||
|
||||
# Caching is required because things like scripts and scenes won't
|
||||
# report as "off" to Alexa if an "off" command is received, because
|
||||
# they'll map to "on". Thus, instead of reporting its actual
|
||||
# status, we report what Alexa will want to see, which is the same
|
||||
# as the actual requested command.
|
||||
self.cached_states[entity_id] = (result, brightness)
|
||||
|
||||
# Perform the requested action
|
||||
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)]
|
||||
|
||||
if brightness is not None:
|
||||
json_response.append(create_hue_success_response(
|
||||
entity_id, HUE_API_STATE_BRI, brightness))
|
||||
|
||||
return self.json(json_response)
|
||||
|
||||
def is_entity_exposed(self, entity):
|
||||
"""Determine if an entity should be exposed on the emulated bridge.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
config = self.config
|
||||
|
||||
if entity.attributes.get('view') is not None:
|
||||
# Ignore entities that are views
|
||||
return False
|
||||
|
||||
domain = entity.domain.lower()
|
||||
explicit_expose = entity.attributes.get(ATTR_EMULATED_HUE, None)
|
||||
|
||||
domain_exposed_by_default = \
|
||||
config.expose_by_default and domain in config.exposed_domains
|
||||
|
||||
# Expose an entity if the entity's domain is exposed by default and
|
||||
# the configuration doesn't explicitly exclude it from being
|
||||
# exposed, or if the entity is explicitly exposed
|
||||
is_default_exposed = \
|
||||
domain_exposed_by_default and explicit_expose is not False
|
||||
|
||||
return is_default_exposed or explicit_expose
|
||||
|
||||
|
||||
def parse_hue_api_put_light_body(request_json, entity):
|
||||
"""Parse the body of a request to change the state of a light."""
|
||||
if HUE_API_STATE_ON in request_json:
|
||||
if not isinstance(request_json[HUE_API_STATE_ON], bool):
|
||||
return None
|
||||
|
||||
if request_json['on']:
|
||||
# Echo requested device be turned on
|
||||
brightness = None
|
||||
report_brightness = False
|
||||
result = True
|
||||
else:
|
||||
# Echo requested device be turned off
|
||||
brightness = None
|
||||
report_brightness = False
|
||||
result = False
|
||||
|
||||
if HUE_API_STATE_BRI in request_json:
|
||||
# Make sure the entity actually supports brightness
|
||||
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS:
|
||||
try:
|
||||
# Clamp brightness from 0 to 255
|
||||
brightness = \
|
||||
max(0, min(int(request_json[HUE_API_STATE_BRI]), 255))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
report_brightness = True
|
||||
result = (brightness > 0)
|
||||
elif entity.domain.lower() == "script":
|
||||
# Convert 0-255 to 0-100
|
||||
level = int(request_json[HUE_API_STATE_BRI]) / 255 * 100
|
||||
|
||||
brightness = round(level)
|
||||
report_brightness = True
|
||||
result = True
|
||||
|
||||
return (result, brightness) if report_brightness else (result, None)
|
||||
|
||||
|
||||
def entity_to_json(entity, is_on=None, brightness=None):
|
||||
"""Convert an entity to its Hue bridge JSON representation."""
|
||||
if is_on is None:
|
||||
is_on = entity.state == STATE_ON
|
||||
|
||||
if brightness is None:
|
||||
brightness = 255 if is_on else 0
|
||||
|
||||
name = entity.attributes.get(
|
||||
ATTR_EMULATED_HUE_NAME, entity.attributes[ATTR_FRIENDLY_NAME])
|
||||
|
||||
return {
|
||||
'state':
|
||||
{
|
||||
HUE_API_STATE_ON: is_on,
|
||||
HUE_API_STATE_BRI: brightness,
|
||||
'reachable': True
|
||||
},
|
||||
'type': 'Dimmable light',
|
||||
'name': name,
|
||||
'modelid': 'HASS123',
|
||||
'uniqueid': entity.entity_id,
|
||||
'swversion': '123'
|
||||
}
|
||||
|
||||
|
||||
def create_hue_success_response(entity_id, attr, value):
|
||||
"""Create a success response for an attribute set on a light."""
|
||||
success_key = '/lights/{}/state/{}'.format(entity_id, attr)
|
||||
return {'success': {success_key: value}}
|
||||
|
||||
|
||||
class UPNPResponderThread(threading.Thread):
|
||||
"""Handle responding to UPNP/SSDP discovery requests."""
|
||||
|
||||
_interrupted = False
|
||||
|
||||
def __init__(self, host_ip_addr, listen_port):
|
||||
"""Initialize the class."""
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
self.host_ip_addr = host_ip_addr
|
||||
self.listen_port = listen_port
|
||||
|
||||
# Note that the double newline at the end of
|
||||
# this string is required per the SSDP spec
|
||||
resp_template = """HTTP/1.1 200 OK
|
||||
CACHE-CONTROL: max-age=60
|
||||
EXT:
|
||||
LOCATION: http://{0}:{1}/description.xml
|
||||
SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/0.1
|
||||
ST: urn:schemas-upnp-org:device:basic:1
|
||||
USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1
|
||||
|
||||
"""
|
||||
|
||||
self.upnp_response = resp_template.format(host_ip_addr, listen_port) \
|
||||
.replace("\n", "\r\n") \
|
||||
.encode('utf-8')
|
||||
|
||||
# Set up a pipe for signaling to the receiver that it's time to
|
||||
# shutdown. Essentially, we place the SSDP socket into nonblocking
|
||||
# mode and use select() to wait for data to arrive on either the SSDP
|
||||
# socket or the pipe. If data arrives on either one, select() returns
|
||||
# and tells us which filenos have data ready to read.
|
||||
#
|
||||
# When we want to stop the responder, we write data to the pipe, which
|
||||
# causes the select() to return and indicate that said pipe has data
|
||||
# ready to be read, which indicates to us that the responder needs to
|
||||
# be shutdown.
|
||||
self._interrupted_read_pipe, self._interrupted_write_pipe = os.pipe()
|
||||
|
||||
def run(self):
|
||||
"""Run the server."""
|
||||
# Listen for UDP port 1900 packets sent to SSDP multicast address
|
||||
ssdp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
ssdp_socket.setblocking(False)
|
||||
|
||||
# Required for receiving multicast
|
||||
ssdp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
|
||||
ssdp_socket.setsockopt(
|
||||
socket.SOL_IP,
|
||||
socket.IP_MULTICAST_IF,
|
||||
socket.inet_aton(self.host_ip_addr))
|
||||
|
||||
ssdp_socket.setsockopt(
|
||||
socket.SOL_IP,
|
||||
socket.IP_ADD_MEMBERSHIP,
|
||||
socket.inet_aton("239.255.255.250") +
|
||||
socket.inet_aton(self.host_ip_addr))
|
||||
|
||||
ssdp_socket.bind(("239.255.255.250", 1900))
|
||||
|
||||
while True:
|
||||
if self._interrupted:
|
||||
clean_socket_close(ssdp_socket)
|
||||
return
|
||||
|
||||
try:
|
||||
read, _, _ = select.select(
|
||||
[self._interrupted_read_pipe, ssdp_socket], [],
|
||||
[ssdp_socket])
|
||||
|
||||
if self._interrupted_read_pipe in read:
|
||||
# Implies self._interrupted is True
|
||||
clean_socket_close(ssdp_socket)
|
||||
return
|
||||
elif ssdp_socket in read:
|
||||
data, addr = ssdp_socket.recvfrom(1024)
|
||||
else:
|
||||
continue
|
||||
except socket.error as ex:
|
||||
if self._interrupted:
|
||||
clean_socket_close(ssdp_socket)
|
||||
return
|
||||
|
||||
_LOGGER.error("UPNP Responder socket exception occured: %s",
|
||||
ex.__str__)
|
||||
|
||||
if "M-SEARCH" in data.decode('utf-8'):
|
||||
# SSDP M-SEARCH method received, respond to it with our info
|
||||
resp_socket = socket.socket(
|
||||
socket.AF_INET, socket.SOCK_DGRAM)
|
||||
|
||||
resp_socket.sendto(self.upnp_response, addr)
|
||||
resp_socket.close()
|
||||
|
||||
def stop(self):
|
||||
"""Stop the server."""
|
||||
# Request for server
|
||||
self._interrupted = True
|
||||
os.write(self._interrupted_write_pipe, bytes([0]))
|
||||
self.join()
|
||||
|
||||
|
||||
def clean_socket_close(sock):
|
||||
"""Close a socket connection and logs its closure."""
|
||||
_LOGGER.info("UPNP responder shutting down.")
|
||||
|
||||
sock.close()
|
||||
198
homeassistant/components/emulated_hue/__init__.py
Normal file
198
homeassistant/components/emulated_hue/__init__.py
Normal file
@@ -0,0 +1,198 @@
|
||||
"""
|
||||
Support for local control of entities by emulating the Phillips Hue bridge.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/emulated_hue/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import util
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.components.http import HomeAssistantWSGI
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from .hue_api import (
|
||||
HueUsernameView, HueAllLightsStateView, HueOneLightStateView,
|
||||
HueOneLightChangeView)
|
||||
from .upnp import DescriptionXmlView, UPNPResponderThread
|
||||
|
||||
DOMAIN = 'emulated_hue'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_HOST_IP = 'host_ip'
|
||||
CONF_LISTEN_PORT = 'listen_port'
|
||||
CONF_OFF_MAPS_TO_ON_DOMAINS = 'off_maps_to_on_domains'
|
||||
CONF_EXPOSE_BY_DEFAULT = 'expose_by_default'
|
||||
CONF_EXPOSED_DOMAINS = 'exposed_domains'
|
||||
CONF_TYPE = 'type'
|
||||
|
||||
TYPE_ALEXA = 'alexa'
|
||||
TYPE_GOOGLE = 'google_home'
|
||||
|
||||
DEFAULT_LISTEN_PORT = 8300
|
||||
DEFAULT_OFF_MAPS_TO_ON_DOMAINS = ['script', 'scene']
|
||||
DEFAULT_EXPOSE_BY_DEFAULT = True
|
||||
DEFAULT_EXPOSED_DOMAINS = [
|
||||
'switch', 'light', 'group', 'input_boolean', 'media_player', 'fan'
|
||||
]
|
||||
DEFAULT_TYPE = TYPE_ALEXA
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_HOST_IP): cv.string,
|
||||
vol.Optional(CONF_LISTEN_PORT, default=DEFAULT_LISTEN_PORT):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)),
|
||||
vol.Optional(CONF_OFF_MAPS_TO_ON_DOMAINS): cv.ensure_list,
|
||||
vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean,
|
||||
vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list,
|
||||
vol.Optional(CONF_TYPE, default=DEFAULT_TYPE):
|
||||
vol.Any(TYPE_ALEXA, TYPE_GOOGLE)
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
ATTR_EMULATED_HUE = 'emulated_hue'
|
||||
|
||||
|
||||
def setup(hass, yaml_config):
|
||||
"""Activate the emulated_hue component."""
|
||||
config = Config(yaml_config.get(DOMAIN, {}))
|
||||
|
||||
server = HomeAssistantWSGI(
|
||||
hass,
|
||||
development=False,
|
||||
server_host=config.host_ip_addr,
|
||||
server_port=config.listen_port,
|
||||
api_password=None,
|
||||
ssl_certificate=None,
|
||||
ssl_key=None,
|
||||
cors_origins=None,
|
||||
use_x_forwarded_for=False,
|
||||
trusted_networks=[],
|
||||
login_threshold=0,
|
||||
is_ban_enabled=False
|
||||
)
|
||||
|
||||
server.register_view(DescriptionXmlView(config))
|
||||
server.register_view(HueUsernameView)
|
||||
server.register_view(HueAllLightsStateView(config))
|
||||
server.register_view(HueOneLightStateView(config))
|
||||
server.register_view(HueOneLightChangeView(config))
|
||||
|
||||
upnp_listener = UPNPResponderThread(
|
||||
config.host_ip_addr, config.listen_port)
|
||||
|
||||
@asyncio.coroutine
|
||||
def stop_emulated_hue_bridge(event):
|
||||
"""Stop the emulated hue bridge."""
|
||||
upnp_listener.stop()
|
||||
yield from server.stop()
|
||||
|
||||
@asyncio.coroutine
|
||||
def start_emulated_hue_bridge(event):
|
||||
"""Start the emulated hue bridge."""
|
||||
upnp_listener.start()
|
||||
yield from server.start()
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||
stop_emulated_hue_bridge)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class Config(object):
|
||||
"""Holds configuration variables for the emulated hue bridge."""
|
||||
|
||||
def __init__(self, conf):
|
||||
"""Initialize the instance."""
|
||||
self.type = conf.get(CONF_TYPE)
|
||||
self.numbers = {}
|
||||
self.cached_states = {}
|
||||
|
||||
# Get the IP address that will be passed to the Echo during discovery
|
||||
self.host_ip_addr = conf.get(CONF_HOST_IP)
|
||||
if self.host_ip_addr is None:
|
||||
self.host_ip_addr = util.get_local_ip()
|
||||
_LOGGER.warning(
|
||||
"Listen IP address not specified, auto-detected address is %s",
|
||||
self.host_ip_addr)
|
||||
|
||||
# Get the port that the Hue bridge will listen on
|
||||
self.listen_port = conf.get(CONF_LISTEN_PORT)
|
||||
if not isinstance(self.listen_port, int):
|
||||
self.listen_port = DEFAULT_LISTEN_PORT
|
||||
_LOGGER.warning(
|
||||
"Listen port not specified, defaulting to %s",
|
||||
self.listen_port)
|
||||
|
||||
if self.type == TYPE_GOOGLE and self.listen_port != 80:
|
||||
_LOGGER.warning('When targetting Google Home, listening port has '
|
||||
'to be port 80')
|
||||
|
||||
# Get domains that cause both "on" and "off" commands to map to "on"
|
||||
# This is primarily useful for things like scenes or scripts, which
|
||||
# don't really have a concept of being off
|
||||
self.off_maps_to_on_domains = conf.get(CONF_OFF_MAPS_TO_ON_DOMAINS)
|
||||
if not isinstance(self.off_maps_to_on_domains, list):
|
||||
self.off_maps_to_on_domains = DEFAULT_OFF_MAPS_TO_ON_DOMAINS
|
||||
|
||||
# Get whether or not entities should be exposed by default, or if only
|
||||
# explicitly marked ones will be exposed
|
||||
self.expose_by_default = conf.get(
|
||||
CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT)
|
||||
|
||||
# Get domains that are exposed by default when expose_by_default is
|
||||
# True
|
||||
self.exposed_domains = conf.get(
|
||||
CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS)
|
||||
|
||||
def entity_id_to_number(self, entity_id):
|
||||
"""Get a unique number for the entity id."""
|
||||
if self.type == TYPE_ALEXA:
|
||||
return entity_id
|
||||
|
||||
# Google Home
|
||||
for number, ent_id in self.numbers.items():
|
||||
if entity_id == ent_id:
|
||||
return number
|
||||
|
||||
number = str(len(self.numbers) + 1)
|
||||
self.numbers[number] = entity_id
|
||||
return number
|
||||
|
||||
def number_to_entity_id(self, number):
|
||||
"""Convert unique number to entity id."""
|
||||
if self.type == TYPE_ALEXA:
|
||||
return number
|
||||
|
||||
# Google Home
|
||||
assert isinstance(number, str)
|
||||
return self.numbers.get(number)
|
||||
|
||||
def is_entity_exposed(self, entity):
|
||||
"""Determine if an entity should be exposed on the emulated bridge.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
if entity.attributes.get('view') is not None:
|
||||
# Ignore entities that are views
|
||||
return False
|
||||
|
||||
domain = entity.domain.lower()
|
||||
explicit_expose = entity.attributes.get(ATTR_EMULATED_HUE, None)
|
||||
|
||||
domain_exposed_by_default = \
|
||||
self.expose_by_default and domain in self.exposed_domains
|
||||
|
||||
# Expose an entity if the entity's domain is exposed by default and
|
||||
# the configuration doesn't explicitly exclude it from being
|
||||
# exposed, or if the entity is explicitly exposed
|
||||
is_default_exposed = \
|
||||
domain_exposed_by_default and explicit_expose is not False
|
||||
|
||||
return is_default_exposed or explicit_expose
|
||||
275
homeassistant/components/emulated_hue/hue_api.py
Normal file
275
homeassistant/components/emulated_hue/hue_api.py
Normal file
@@ -0,0 +1,275 @@
|
||||
"""Provides a Hue API to control Home Assistant."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from homeassistant import core
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||
STATE_ON, STATE_OFF, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
|
||||
)
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS
|
||||
)
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_EMULATED_HUE = 'emulated_hue'
|
||||
ATTR_EMULATED_HUE_NAME = 'emulated_hue_name'
|
||||
|
||||
HUE_API_STATE_ON = 'on'
|
||||
HUE_API_STATE_BRI = 'bri'
|
||||
|
||||
|
||||
class HueUsernameView(HomeAssistantView):
|
||||
"""Handle requests to create a username for the emulated hue bridge."""
|
||||
|
||||
url = '/api'
|
||||
name = 'emulated_hue:api:create_username'
|
||||
extra_urls = ['/api/']
|
||||
requires_auth = False
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Handle a POST request."""
|
||||
try:
|
||||
data = yield from request.json()
|
||||
except ValueError:
|
||||
return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
|
||||
|
||||
if 'devicetype' not in data:
|
||||
return self.json_message('devicetype not specified',
|
||||
HTTP_BAD_REQUEST)
|
||||
|
||||
return self.json([{'success': {'username': '12345678901234567890'}}])
|
||||
|
||||
|
||||
class HueAllLightsStateView(HomeAssistantView):
|
||||
"""Handle requests for getting and setting info about entities."""
|
||||
|
||||
url = '/api/{username}/lights'
|
||||
name = 'emulated_hue:lights:state'
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the instance of the view."""
|
||||
self.config = config
|
||||
|
||||
@core.callback
|
||||
def get(self, request, username):
|
||||
"""Process a request to get the list of available lights."""
|
||||
hass = request.app['hass']
|
||||
json_response = {}
|
||||
|
||||
for entity in hass.states.async_all():
|
||||
if self.config.is_entity_exposed(entity):
|
||||
number = self.config.entity_id_to_number(entity.entity_id)
|
||||
json_response[number] = entity_to_json(entity)
|
||||
|
||||
return self.json(json_response)
|
||||
|
||||
|
||||
class HueOneLightStateView(HomeAssistantView):
|
||||
"""Handle requests for getting and setting info about entities."""
|
||||
|
||||
url = '/api/{username}/lights/{entity_id}'
|
||||
name = 'emulated_hue:light:state'
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the instance of the view."""
|
||||
self.config = config
|
||||
|
||||
@core.callback
|
||||
def get(self, request, username, entity_id=None):
|
||||
"""Process a request to get the state of an individual light."""
|
||||
hass = request.app['hass']
|
||||
entity_id = self.config.number_to_entity_id(entity_id)
|
||||
entity = hass.states.get(entity_id)
|
||||
|
||||
if entity is None:
|
||||
_LOGGER.error('Entity not found: %s', entity_id)
|
||||
return web.Response(text="Entity not found", status=404)
|
||||
|
||||
if not self.config.is_entity_exposed(entity):
|
||||
_LOGGER.error('Entity not exposed: %s', entity_id)
|
||||
return web.Response(text="Entity not exposed", status=404)
|
||||
|
||||
cached_state = self.config.cached_states.get(entity_id, None)
|
||||
|
||||
if cached_state is None:
|
||||
final_state = entity.state == STATE_ON
|
||||
final_brightness = entity.attributes.get(
|
||||
ATTR_BRIGHTNESS, 255 if final_state else 0)
|
||||
else:
|
||||
final_state, final_brightness = cached_state
|
||||
|
||||
json_response = entity_to_json(entity, final_state, final_brightness)
|
||||
|
||||
return self.json(json_response)
|
||||
|
||||
|
||||
class HueOneLightChangeView(HomeAssistantView):
|
||||
"""Handle requests for getting and setting info about entities."""
|
||||
|
||||
url = '/api/{username}/lights/{entity_number}/state'
|
||||
name = 'emulated_hue:light:state'
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the instance of the view."""
|
||||
self.config = config
|
||||
|
||||
@asyncio.coroutine
|
||||
def put(self, request, username, entity_number):
|
||||
"""Process a request to set the state of an individual light."""
|
||||
config = self.config
|
||||
hass = request.app['hass']
|
||||
entity_id = config.number_to_entity_id(entity_number)
|
||||
|
||||
if entity_id is None:
|
||||
_LOGGER.error('Unknown entity number: %s', entity_number)
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
|
||||
entity = hass.states.get(entity_id)
|
||||
|
||||
if entity is None:
|
||||
_LOGGER.error('Entity not found: %s', entity_id)
|
||||
return self.json_message('Entity not found', HTTP_NOT_FOUND)
|
||||
|
||||
if not config.is_entity_exposed(entity):
|
||||
_LOGGER.error('Entity not exposed: %s', entity_id)
|
||||
return web.Response(text="Entity not exposed", status=404)
|
||||
|
||||
try:
|
||||
request_json = yield from request.json()
|
||||
except ValueError:
|
||||
_LOGGER.error('Received invalid json')
|
||||
return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
|
||||
|
||||
# Parse the request into requested "on" status and brightness
|
||||
parsed = parse_hue_api_put_light_body(request_json, entity)
|
||||
|
||||
if parsed is None:
|
||||
_LOGGER.error('Unable to parse data: %s', request_json)
|
||||
return web.Response(text="Bad request", status=400)
|
||||
|
||||
result, brightness = parsed
|
||||
|
||||
# Convert the resulting "on" status into the service we need to call
|
||||
service = SERVICE_TURN_ON if result else SERVICE_TURN_OFF
|
||||
|
||||
# Construct what we need to send to the service
|
||||
data = {ATTR_ENTITY_ID: entity_id}
|
||||
|
||||
# If the requested entity is a script add some variables
|
||||
if entity.domain == "script":
|
||||
data['variables'] = {
|
||||
'requested_state': STATE_ON if result else STATE_OFF
|
||||
}
|
||||
|
||||
if brightness is not None:
|
||||
data['variables']['requested_level'] = brightness
|
||||
|
||||
elif brightness is not None:
|
||||
data[ATTR_BRIGHTNESS] = brightness
|
||||
|
||||
if entity.domain in config.off_maps_to_on_domains:
|
||||
# Map the off command to on
|
||||
service = SERVICE_TURN_ON
|
||||
|
||||
# Caching is required because things like scripts and scenes won't
|
||||
# report as "off" to Alexa if an "off" command is received, because
|
||||
# they'll map to "on". Thus, instead of reporting its actual
|
||||
# status, we report what Alexa will want to see, which is the same
|
||||
# as the actual requested command.
|
||||
config.cached_states[entity_id] = (result, brightness)
|
||||
|
||||
# Perform the requested action
|
||||
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)]
|
||||
|
||||
if brightness is not None:
|
||||
json_response.append(create_hue_success_response(
|
||||
entity_id, HUE_API_STATE_BRI, brightness))
|
||||
|
||||
return self.json(json_response)
|
||||
|
||||
|
||||
def parse_hue_api_put_light_body(request_json, entity):
|
||||
"""Parse the body of a request to change the state of a light."""
|
||||
if HUE_API_STATE_ON in request_json:
|
||||
if not isinstance(request_json[HUE_API_STATE_ON], bool):
|
||||
return None
|
||||
|
||||
if request_json['on']:
|
||||
# Echo requested device be turned on
|
||||
brightness = None
|
||||
report_brightness = False
|
||||
result = True
|
||||
else:
|
||||
# Echo requested device be turned off
|
||||
brightness = None
|
||||
report_brightness = False
|
||||
result = False
|
||||
|
||||
if HUE_API_STATE_BRI in request_json:
|
||||
# Make sure the entity actually supports brightness
|
||||
entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
if (entity_features & SUPPORT_BRIGHTNESS) == SUPPORT_BRIGHTNESS:
|
||||
try:
|
||||
# Clamp brightness from 0 to 255
|
||||
brightness = \
|
||||
max(0, min(int(request_json[HUE_API_STATE_BRI]), 255))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
report_brightness = True
|
||||
result = (brightness > 0)
|
||||
elif entity.domain.lower() == "script":
|
||||
# Convert 0-255 to 0-100
|
||||
level = int(request_json[HUE_API_STATE_BRI]) / 255 * 100
|
||||
|
||||
brightness = round(level)
|
||||
report_brightness = True
|
||||
result = True
|
||||
|
||||
return (result, brightness) if report_brightness else (result, None)
|
||||
|
||||
|
||||
def entity_to_json(entity, is_on=None, brightness=None):
|
||||
"""Convert an entity to its Hue bridge JSON representation."""
|
||||
if is_on is None:
|
||||
is_on = entity.state == STATE_ON
|
||||
|
||||
if brightness is None:
|
||||
brightness = 255 if is_on else 0
|
||||
|
||||
name = entity.attributes.get(
|
||||
ATTR_EMULATED_HUE_NAME, entity.attributes[ATTR_FRIENDLY_NAME])
|
||||
|
||||
return {
|
||||
'state':
|
||||
{
|
||||
HUE_API_STATE_ON: is_on,
|
||||
HUE_API_STATE_BRI: brightness,
|
||||
'reachable': True
|
||||
},
|
||||
'type': 'Dimmable light',
|
||||
'name': name,
|
||||
'modelid': 'HASS123',
|
||||
'uniqueid': entity.entity_id,
|
||||
'swversion': '123'
|
||||
}
|
||||
|
||||
|
||||
def create_hue_success_response(entity_id, attr, value):
|
||||
"""Create a success response for an attribute set on a light."""
|
||||
success_key = '/lights/{}/state/{}'.format(entity_id, attr)
|
||||
return {'success': {success_key: value}}
|
||||
167
homeassistant/components/emulated_hue/upnp.py
Normal file
167
homeassistant/components/emulated_hue/upnp.py
Normal file
@@ -0,0 +1,167 @@
|
||||
"""Provides a UPNP discovery method that mimicks Hue hubs."""
|
||||
import threading
|
||||
import socket
|
||||
import logging
|
||||
import os
|
||||
import select
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from homeassistant import core
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DescriptionXmlView(HomeAssistantView):
|
||||
"""Handles requests for the description.xml file."""
|
||||
|
||||
url = '/description.xml'
|
||||
name = 'description:xml'
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the instance of the view."""
|
||||
self.config = config
|
||||
|
||||
@core.callback
|
||||
def get(self, request):
|
||||
"""Handle a GET request."""
|
||||
xml_template = """<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<root xmlns="urn:schemas-upnp-org:device-1-0">
|
||||
<specVersion>
|
||||
<major>1</major>
|
||||
<minor>0</minor>
|
||||
</specVersion>
|
||||
<URLBase>http://{0}:{1}/</URLBase>
|
||||
<device>
|
||||
<deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType>
|
||||
<friendlyName>HASS Bridge ({0})</friendlyName>
|
||||
<manufacturer>Royal Philips Electronics</manufacturer>
|
||||
<manufacturerURL>http://www.philips.com</manufacturerURL>
|
||||
<modelDescription>Philips hue Personal Wireless Lighting</modelDescription>
|
||||
<modelName>Philips hue bridge 2015</modelName>
|
||||
<modelNumber>BSB002</modelNumber>
|
||||
<modelURL>http://www.meethue.com</modelURL>
|
||||
<serialNumber>1234</serialNumber>
|
||||
<UDN>uuid:2f402f80-da50-11e1-9b23-001788255acc</UDN>
|
||||
</device>
|
||||
</root>
|
||||
"""
|
||||
|
||||
resp_text = xml_template.format(
|
||||
self.config.host_ip_addr, self.config.listen_port)
|
||||
|
||||
return web.Response(text=resp_text, content_type='text/xml')
|
||||
|
||||
|
||||
class UPNPResponderThread(threading.Thread):
|
||||
"""Handle responding to UPNP/SSDP discovery requests."""
|
||||
|
||||
_interrupted = False
|
||||
|
||||
def __init__(self, host_ip_addr, listen_port):
|
||||
"""Initialize the class."""
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
self.host_ip_addr = host_ip_addr
|
||||
self.listen_port = listen_port
|
||||
|
||||
# Note that the double newline at the end of
|
||||
# this string is required per the SSDP spec
|
||||
resp_template = """HTTP/1.1 200 OK
|
||||
CACHE-CONTROL: max-age=60
|
||||
EXT:
|
||||
LOCATION: http://{0}:{1}/description.xml
|
||||
SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/0.1
|
||||
hue-bridgeid: 1234
|
||||
ST: urn:schemas-upnp-org:device:basic:1
|
||||
USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1
|
||||
|
||||
"""
|
||||
|
||||
self.upnp_response = resp_template.format(host_ip_addr, listen_port) \
|
||||
.replace("\n", "\r\n") \
|
||||
.encode('utf-8')
|
||||
|
||||
# Set up a pipe for signaling to the receiver that it's time to
|
||||
# shutdown. Essentially, we place the SSDP socket into nonblocking
|
||||
# mode and use select() to wait for data to arrive on either the SSDP
|
||||
# socket or the pipe. If data arrives on either one, select() returns
|
||||
# and tells us which filenos have data ready to read.
|
||||
#
|
||||
# When we want to stop the responder, we write data to the pipe, which
|
||||
# causes the select() to return and indicate that said pipe has data
|
||||
# ready to be read, which indicates to us that the responder needs to
|
||||
# be shutdown.
|
||||
self._interrupted_read_pipe, self._interrupted_write_pipe = os.pipe()
|
||||
|
||||
def run(self):
|
||||
"""Run the server."""
|
||||
# Listen for UDP port 1900 packets sent to SSDP multicast address
|
||||
ssdp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
ssdp_socket.setblocking(False)
|
||||
|
||||
# Required for receiving multicast
|
||||
ssdp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
|
||||
ssdp_socket.setsockopt(
|
||||
socket.SOL_IP,
|
||||
socket.IP_MULTICAST_IF,
|
||||
socket.inet_aton(self.host_ip_addr))
|
||||
|
||||
ssdp_socket.setsockopt(
|
||||
socket.SOL_IP,
|
||||
socket.IP_ADD_MEMBERSHIP,
|
||||
socket.inet_aton("239.255.255.250") +
|
||||
socket.inet_aton(self.host_ip_addr))
|
||||
|
||||
ssdp_socket.bind(("239.255.255.250", 1900))
|
||||
|
||||
while True:
|
||||
if self._interrupted:
|
||||
clean_socket_close(ssdp_socket)
|
||||
return
|
||||
|
||||
try:
|
||||
read, _, _ = select.select(
|
||||
[self._interrupted_read_pipe, ssdp_socket], [],
|
||||
[ssdp_socket])
|
||||
|
||||
if self._interrupted_read_pipe in read:
|
||||
# Implies self._interrupted is True
|
||||
clean_socket_close(ssdp_socket)
|
||||
return
|
||||
elif ssdp_socket in read:
|
||||
data, addr = ssdp_socket.recvfrom(1024)
|
||||
else:
|
||||
continue
|
||||
except socket.error as ex:
|
||||
if self._interrupted:
|
||||
clean_socket_close(ssdp_socket)
|
||||
return
|
||||
|
||||
_LOGGER.error("UPNP Responder socket exception occured: %s",
|
||||
ex.__str__)
|
||||
|
||||
if "M-SEARCH" in data.decode('utf-8'):
|
||||
# SSDP M-SEARCH method received, respond to it with our info
|
||||
resp_socket = socket.socket(
|
||||
socket.AF_INET, socket.SOCK_DGRAM)
|
||||
|
||||
resp_socket.sendto(self.upnp_response, addr)
|
||||
resp_socket.close()
|
||||
|
||||
def stop(self):
|
||||
"""Stop the server."""
|
||||
# Request for server
|
||||
self._interrupted = True
|
||||
os.write(self._interrupted_write_pipe, bytes([0]))
|
||||
self.join()
|
||||
|
||||
|
||||
def clean_socket_close(sock):
|
||||
"""Close a socket connection and logs its closure."""
|
||||
_LOGGER.info("UPNP responder shutting down.")
|
||||
|
||||
sock.close()
|
||||
@@ -1,12 +1,12 @@
|
||||
"""DO NOT MODIFY. Auto-generated by script/fingerprint_frontend."""
|
||||
|
||||
FINGERPRINTS = {
|
||||
"core.js": "526d7d704ae478c30ae20c1426c2e4f4",
|
||||
"frontend.html": "5baa4dc3b109ca80d4c282fb12c6c23a",
|
||||
"core.js": "5dfb2d3e567fad37af0321d4b29265ed",
|
||||
"frontend.html": "6a89b74ab2b76c7d28fad2aea9444ec2",
|
||||
"mdi.html": "46a76f877ac9848899b8ed382427c16f",
|
||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||
"panels/ha-panel-dev-event.html": "c2d5ec676be98d4474d19f94d0262c1e",
|
||||
"panels/ha-panel-dev-info.html": "ec613406ce7e20d93754233d55625c8a",
|
||||
"panels/ha-panel-dev-info.html": "a9c07bf281fe9791fb15827ec1286825",
|
||||
"panels/ha-panel-dev-service.html": "b3fe49532c5c03198fafb0c6ed58b76a",
|
||||
"panels/ha-panel-dev-state.html": "65e5f791cc467561719bf591f1386054",
|
||||
"panels/ha-panel-dev-template.html": "7d744ab7f7c08b6d6ad42069989de400",
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1,2 +1,2 @@
|
||||
<html><head><meta charset="UTF-8"></head><body><dom-module id="ha-panel-dev-info"><template><style include="iron-positioning ha-style">:host{background-color:#fff;-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial}.content{padding:24px}.about{text-align:center;line-height:2em}.version{@apply(--paper-font-headline)}.develop{@apply(--paper-font-subhead)}.about a{color:var(--dark-primary-color)}.error-log-intro{margin-top:16px;border-top:1px solid var(--light-primary-color);padding-top:16px}paper-icon-button{float:right}.error-log{@apply(--paper-font-code1)
|
||||
clear: both;white-space:pre-wrap}</style><app-header-layout has-scrolling-region=""><app-header fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">About</div></app-toolbar></app-header><div class="content fit"><div class="about"><p class="version"><a href="https://home-assistant.io"><img src="/static/icons/favicon-192x192.png" height="192"></a><br>Home Assistant<br>[[hassVersion]]</p><p>Path to configuration.yaml: [[hassConfigDir]]</p><p class="develop"><a href="https://home-assistant.io/developers/credits/" target="_blank">Developed by a bunch of awesome people.</a></p><p>Published under the MIT license<br>Source: <a href="https://github.com/home-assistant/home-assistant" target="_blank">server</a> — <a href="https://github.com/home-assistant/home-assistant-polymer" target="_blank">frontend-ui</a> — <a href="https://github.com/home-assistant/home-assistant-js" target="_blank">frontend-core</a></p><p>Built using <a href="https://www.python.org">Python 3</a>, <a href="https://www.polymer-project.org" target="_blank">Polymer [[polymerVersion]]</a>, <a href="https://optimizely.github.io/nuclear-js/" target="_blank">NuclearJS [[nuclearVersion]]</a><br>Icons by <a href="https://www.google.com/design/icons/" target="_blank">Google</a> and <a href="https://MaterialDesignIcons.com" target="_blank">MaterialDesignIcons.com</a>.</p></div><p class="error-log-intro">The following errors have been logged this session:<paper-icon-button icon="mdi:refresh" on-tap="refreshErrorLog"></paper-icon-button></p><div class="error-log">[[errorLog]]</div></div></app-header-layout></template></dom-module><script>Polymer({is:"ha-panel-dev-info",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},hassVersion:{type:String,bindNuclear:function(r){return r.configGetters.serverVersion}},hassConfigDir:{type:String,bindNuclear:function(r){return r.configGetters.configDir}},polymerVersion:{type:String,value:Polymer.version},nuclearVersion:{type:String,value:"1.3.0"},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(r){r&&r.preventDefault(),this.errorLog="Loading error log…",this.hass.errorLogActions.fetchErrorLog().then(function(r){this.errorLog=r||"No errors have been reported."}.bind(this))}})</script></body></html>
|
||||
clear: both;white-space:pre-wrap}</style><app-header-layout has-scrolling-region=""><app-header fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">About</div></app-toolbar></app-header><div class="content fit"><div class="about"><p class="version"><a href="https://home-assistant.io"><img src="/static/icons/favicon-192x192.png" height="192"></a><br>Home Assistant<br>[[hassVersion]]</p><p>Path to configuration.yaml: [[hassConfigDir]]</p><p class="develop"><a href="https://home-assistant.io/developers/credits/" target="_blank">Developed by a bunch of awesome people.</a></p><p>Published under the MIT license<br>Source: <a href="https://github.com/home-assistant/home-assistant" target="_blank">server</a> — <a href="https://github.com/home-assistant/home-assistant-polymer" target="_blank">frontend-ui</a> — <a href="https://github.com/home-assistant/home-assistant-js" target="_blank">frontend-core</a></p><p>Built using <a href="https://www.python.org">Python 3</a>, <a href="https://www.polymer-project.org" target="_blank">Polymer [[polymerVersion]]</a>, <a href="https://optimizely.github.io/nuclear-js/" target="_blank">NuclearJS [[nuclearVersion]]</a><br>Icons by <a href="https://www.google.com/design/icons/" target="_blank">Google</a> and <a href="https://MaterialDesignIcons.com" target="_blank">MaterialDesignIcons.com</a>.</p></div><p class="error-log-intro">The following errors have been logged this session:<paper-icon-button icon="mdi:refresh" on-tap="refreshErrorLog"></paper-icon-button></p><div class="error-log">[[errorLog]]</div></div></app-header-layout></template></dom-module><script>Polymer({is:"ha-panel-dev-info",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},hassVersion:{type:String,bindNuclear:function(r){return r.configGetters.serverVersion}},hassConfigDir:{type:String,bindNuclear:function(r){return r.configGetters.configDir}},polymerVersion:{type:String,value:Polymer.version},nuclearVersion:{type:String,value:"1.4.0"},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(r){r&&r.preventDefault(),this.errorLog="Loading error log…",this.hass.errorLogActions.fetchErrorLog().then(function(r){this.errorLog=r||"No errors have been reported."}.bind(this))}})</script></body></html>
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -544,19 +544,19 @@ def _hm_event_handler(hass, proxy, device, caller, attribute, value):
|
||||
|
||||
# keypress event
|
||||
if attribute in HM_PRESS_EVENTS:
|
||||
hass.bus.fire(EVENT_KEYPRESS, {
|
||||
hass.add_job(hass.bus.async_fire(EVENT_KEYPRESS, {
|
||||
ATTR_NAME: hmdevice.NAME,
|
||||
ATTR_PARAM: attribute,
|
||||
ATTR_CHANNEL: channel
|
||||
})
|
||||
}))
|
||||
return
|
||||
|
||||
# impulse event
|
||||
if attribute in HM_IMPULSE_EVENTS:
|
||||
hass.bus.fire(EVENT_KEYPRESS, {
|
||||
hass.add_job(hass.bus.async_fire(EVENT_KEYPRESS, {
|
||||
ATTR_NAME: hmdevice.NAME,
|
||||
ATTR_CHANNEL: channel
|
||||
})
|
||||
}))
|
||||
return
|
||||
|
||||
_LOGGER.warning("Event is unknown and not forwarded to HA")
|
||||
|
||||
@@ -32,7 +32,7 @@ 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 .static import FILE_SENDER, GZIP_FILE_SENDER, staticresource_middleware
|
||||
from .util import get_real_ip
|
||||
|
||||
DOMAIN = 'http'
|
||||
@@ -277,9 +277,15 @@ class HomeAssistantWSGI(object):
|
||||
@asyncio.coroutine
|
||||
def start(self):
|
||||
"""Start the wsgi server."""
|
||||
cors_added = set()
|
||||
if self.cors is not None:
|
||||
for route in list(self.app.router.routes()):
|
||||
if hasattr(route, 'resource'):
|
||||
route = route.resource
|
||||
if route in cors_added:
|
||||
continue
|
||||
self.cors.add(route)
|
||||
cors_added.add(route)
|
||||
|
||||
if self.ssl_certificate:
|
||||
context = ssl.SSLContext(SSL_VERSION)
|
||||
@@ -338,7 +344,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 FILE_SENDER.send(request, Path(fil))
|
||||
return response
|
||||
|
||||
def register(self, router):
|
||||
|
||||
@@ -63,6 +63,7 @@ class GzipFileSender(FileSender):
|
||||
|
||||
|
||||
GZIP_FILE_SENDER = GzipFileSender()
|
||||
FILE_SENDER = FileSender()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
|
||||
@@ -24,23 +24,20 @@ CONF_TAGS = 'tags'
|
||||
CONF_DEFAULT_MEASUREMENT = 'default_measurement'
|
||||
|
||||
DEFAULT_DATABASE = 'home_assistant'
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_PORT = 8086
|
||||
DEFAULT_SSL = False
|
||||
DEFAULT_VERIFY_SSL = False
|
||||
DEFAULT_VERIFY_SSL = True
|
||||
DOMAIN = 'influxdb'
|
||||
TIMEOUT = 5
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_HOST): cv.string,
|
||||
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
|
||||
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
|
||||
vol.Optional(CONF_BLACKLIST, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.entity_id]),
|
||||
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_PORT): cv.port,
|
||||
vol.Optional(CONF_SSL): cv.boolean,
|
||||
vol.Optional(CONF_DEFAULT_MEASUREMENT): cv.string,
|
||||
vol.Optional(CONF_TAGS, default={}):
|
||||
vol.Schema({cv.string: cv.string}),
|
||||
@@ -57,23 +54,34 @@ def setup(hass, config):
|
||||
|
||||
conf = config[DOMAIN]
|
||||
|
||||
host = conf.get(CONF_HOST)
|
||||
port = conf.get(CONF_PORT)
|
||||
database = conf.get(CONF_DB_NAME)
|
||||
username = conf.get(CONF_USERNAME)
|
||||
password = conf.get(CONF_PASSWORD)
|
||||
ssl = conf.get(CONF_SSL)
|
||||
verify_ssl = conf.get(CONF_VERIFY_SSL)
|
||||
kwargs = {
|
||||
'database': conf[CONF_DB_NAME],
|
||||
'verify_ssl': conf[CONF_VERIFY_SSL],
|
||||
'timeout': TIMEOUT
|
||||
}
|
||||
|
||||
if CONF_HOST in conf:
|
||||
kwargs['host'] = conf[CONF_HOST]
|
||||
|
||||
if CONF_PORT in conf:
|
||||
kwargs['port'] = conf[CONF_PORT]
|
||||
|
||||
if CONF_USERNAME in conf:
|
||||
kwargs['username'] = conf[CONF_USERNAME]
|
||||
|
||||
if CONF_PASSWORD in conf:
|
||||
kwargs['password'] = conf[CONF_PASSWORD]
|
||||
|
||||
if CONF_SSL in conf:
|
||||
kwargs['ssl'] = conf[CONF_SSL]
|
||||
|
||||
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(
|
||||
host=host, port=port, username=username, password=password,
|
||||
database=database, ssl=ssl, verify_ssl=verify_ssl,
|
||||
timeout=TIMEOUT)
|
||||
influx = InfluxDBClient(**kwargs)
|
||||
influx.query("select * from /.*/ LIMIT 1;")
|
||||
except exceptions.InfluxDBClientError as exc:
|
||||
_LOGGER.error("Database host is not accessible due to '%s', please "
|
||||
@@ -120,7 +128,8 @@ def setup(hass, config):
|
||||
|
||||
for key, value in state.attributes.items():
|
||||
if key != 'unit_of_measurement':
|
||||
if isinstance(value, (str, float, bool)):
|
||||
if isinstance(value, (str, float, bool)) or \
|
||||
key.endswith('_id'):
|
||||
json_body[0]['fields'][key] = value
|
||||
elif isinstance(value, int):
|
||||
# Prevent column data errors in influxDB.
|
||||
|
||||
@@ -236,7 +236,6 @@ def async_setup(hass, config):
|
||||
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)
|
||||
@@ -245,12 +244,18 @@ def async_setup(hass, config):
|
||||
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
|
||||
update_tasks = []
|
||||
|
||||
for light in target_lights:
|
||||
if not light.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.loop.create_task(
|
||||
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)
|
||||
|
||||
@@ -26,8 +26,9 @@ DEPENDENCIES = ['enocean']
|
||||
SUPPORT_ENOCEAN = SUPPORT_BRIGHTNESS
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ID): cv.string,
|
||||
vol.Required(CONF_SENDER_ID): cv.string,
|
||||
vol.Optional(CONF_ID, default=[]): vol.All(cv.ensure_list,
|
||||
[vol.Coerce(int)]),
|
||||
vol.Required(CONF_SENDER_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
@@ -85,8 +85,11 @@ def setup(hass, config):
|
||||
else:
|
||||
item.unlock(code=code)
|
||||
|
||||
if item.should_poll:
|
||||
item.update_ha_state(True)
|
||||
for item in target_locks:
|
||||
if not item.should_poll:
|
||||
continue
|
||||
|
||||
item.update_ha_state(True)
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
@@ -36,10 +36,10 @@ SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_TURN_OFF_ACTION, default=None): vol.In(TURN_OFF_ACTION),
|
||||
vol.Optional(CONF_USERNAME): cv.string,
|
||||
vol.Inclusive(CONF_USERNAME, 'auth'): cv.string,
|
||||
vol.Inclusive(CONF_PASSWORD, 'auth'): cv.string,
|
||||
})
|
||||
|
||||
|
||||
@@ -51,11 +51,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
if jsonrpc_url:
|
||||
url = jsonrpc_url.rstrip('/jsonrpc')
|
||||
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
if username is not None:
|
||||
auth = (username, password)
|
||||
else:
|
||||
auth = None
|
||||
|
||||
add_devices([
|
||||
KodiDevice(
|
||||
config.get(CONF_NAME),
|
||||
url,
|
||||
auth=(config.get(CONF_USERNAME), config.get(CONF_PASSWORD)),
|
||||
auth=auth,
|
||||
turn_off_action=config.get(CONF_TURN_OFF_ACTION)),
|
||||
])
|
||||
|
||||
@@ -68,10 +76,15 @@ class KodiDevice(MediaPlayerDevice):
|
||||
import jsonrpc_requests
|
||||
self._name = name
|
||||
self._url = url
|
||||
|
||||
kwargs = {'timeout': 5}
|
||||
|
||||
if auth is not None:
|
||||
kwargs['auth'] = auth
|
||||
|
||||
self._server = jsonrpc_requests.Server(
|
||||
'{}/jsonrpc'.format(self._url),
|
||||
auth=auth,
|
||||
timeout=5)
|
||||
'{}/jsonrpc'.format(self._url), **kwargs)
|
||||
|
||||
self._turn_off_action = turn_off_action
|
||||
self._players = list()
|
||||
self._properties = None
|
||||
|
||||
@@ -200,6 +200,7 @@ class PlexClient(MediaPlayerDevice):
|
||||
self.update_devices = update_devices
|
||||
self.update_sessions = update_sessions
|
||||
self.set_device(device)
|
||||
self._season = None
|
||||
|
||||
def set_device(self, device):
|
||||
"""Set the device property."""
|
||||
@@ -240,9 +241,15 @@ class PlexClient(MediaPlayerDevice):
|
||||
|
||||
def update(self):
|
||||
"""Get the latest details."""
|
||||
from plexapi.video import Show
|
||||
|
||||
self.update_devices(no_throttle=True)
|
||||
self.update_sessions(no_throttle=True)
|
||||
|
||||
if isinstance(self.session, Show):
|
||||
self._season = self._convert_na_to_none(
|
||||
self.session.seasons()[0].index)
|
||||
|
||||
# pylint: disable=no-self-use, singleton-comparison
|
||||
def _convert_na_to_none(self, value):
|
||||
"""Convert PlexAPI _NA() instances to None."""
|
||||
@@ -310,9 +317,7 @@ class PlexClient(MediaPlayerDevice):
|
||||
@property
|
||||
def media_season(self):
|
||||
"""Season of curent playing media (TV Show only)."""
|
||||
from plexapi.video import Show
|
||||
if isinstance(self.session, Show):
|
||||
return self._convert_na_to_none(self.session.seasons()[0].index)
|
||||
return self._season
|
||||
|
||||
@property
|
||||
def media_series_title(self):
|
||||
|
||||
@@ -59,7 +59,7 @@ 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),
|
||||
vol.Optional(CONF_HOSTS): vol.All(cv.ensure_list, [cv.string]),
|
||||
})
|
||||
|
||||
SONOS_SCHEMA = vol.Schema({
|
||||
|
||||
@@ -19,8 +19,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = [
|
||||
'http://github.com/technicalpickles/python-nest'
|
||||
'/archive/0be5c8a6307ee81540f21aac4fcd22cc5d98c988.zip' # nest-cam branch
|
||||
'#python-nest==3.0.0']
|
||||
'/archive/b8391d2b3cb8682f8b0c2bdff477179983609f39.zip' # nest-cam branch
|
||||
'#python-nest==3.0.2']
|
||||
|
||||
DOMAIN = 'nest'
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ DEFAULT_PORT = 8080
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD): cv.string,
|
||||
vol.Inclusive(CONF_USERNAME, 'auth'): cv.string,
|
||||
vol.Inclusive(CONF_PASSWORD, 'auth'): cv.string,
|
||||
})
|
||||
|
||||
ATTR_DISPLAYTIME = 'displaytime'
|
||||
@@ -33,7 +33,13 @@ def get_service(hass, config):
|
||||
"""Return the notify service."""
|
||||
url = '{}:{}'.format(config.get(CONF_HOST), config.get(CONF_PORT))
|
||||
|
||||
auth = (config.get(CONF_USERNAME), config.get(CONF_PASSWORD))
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
if username is not None:
|
||||
auth = (username, password)
|
||||
else:
|
||||
auth = None
|
||||
|
||||
return KODINotificationService(
|
||||
url,
|
||||
@@ -48,10 +54,14 @@ class KODINotificationService(BaseNotificationService):
|
||||
"""Initialize the service."""
|
||||
import jsonrpc_requests
|
||||
self._url = url
|
||||
|
||||
kwargs = {'timeout': 5}
|
||||
|
||||
if auth is not None:
|
||||
kwargs['auth'] = auth
|
||||
|
||||
self._server = jsonrpc_requests.Server(
|
||||
'{}/jsonrpc'.format(self._url),
|
||||
auth=auth,
|
||||
timeout=5)
|
||||
'{}/jsonrpc'.format(self._url), **kwargs)
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
"""Send a message to Kodi."""
|
||||
|
||||
@@ -4,7 +4,9 @@ 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/
|
||||
"""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import functools as ft
|
||||
import logging
|
||||
import os
|
||||
|
||||
@@ -80,21 +82,17 @@ def send_command(hass, device, command, entity_id=None):
|
||||
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):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Track states and offer events for remotes."""
|
||||
component = EntityComponent(
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_REMOTES)
|
||||
component.setup(config)
|
||||
yield from component.async_setup(config)
|
||||
|
||||
def handle_remote_service(service):
|
||||
@asyncio.coroutine
|
||||
def async_handle_remote_service(service):
|
||||
"""Handle calls to the remote services."""
|
||||
target_remotes = component.extract_from_service(service)
|
||||
target_remotes = component.async_extract_from_service(service)
|
||||
|
||||
activity_id = service.data.get(ATTR_ACTIVITY)
|
||||
device = service.data.get(ATTR_DEVICE)
|
||||
@@ -102,28 +100,43 @@ def setup(hass, config):
|
||||
|
||||
for remote in target_remotes:
|
||||
if service.service == SERVICE_TURN_ON:
|
||||
remote.turn_on(activity=activity_id)
|
||||
yield from remote.async_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()
|
||||
yield from remote.async_send_command(
|
||||
device=device, command=command)
|
||||
else:
|
||||
remote.turn_off()
|
||||
yield from remote.async_turn_off()
|
||||
|
||||
if remote.should_poll:
|
||||
remote.update_ha_state(True)
|
||||
update_tasks = []
|
||||
for remote in target_remotes:
|
||||
if not remote.should_poll:
|
||||
continue
|
||||
|
||||
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)
|
||||
update_coro = hass.loop.create_task(
|
||||
remote.async_update_ha_state(True))
|
||||
if hasattr(remote, '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)
|
||||
|
||||
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_OFF, async_handle_remote_service,
|
||||
descriptions.get(SERVICE_TURN_OFF),
|
||||
schema=REMOTE_SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TURN_ON, async_handle_remote_service,
|
||||
descriptions.get(SERVICE_TURN_ON),
|
||||
schema=REMOTE_SERVICE_TURN_ON_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SEND_COMMAND, async_handle_remote_service,
|
||||
descriptions.get(SERVICE_SEND_COMMAND),
|
||||
schema=REMOTE_SERVICE_SEND_COMMAND_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
@@ -131,14 +144,11 @@ def setup(hass, config):
|
||||
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()
|
||||
|
||||
def async_send_command(self, **kwargs):
|
||||
"""Send a command to a device."""
|
||||
yield from self.hass.loop.run_in_executor(
|
||||
None, ft.partial(self.send_command, **kwargs))
|
||||
|
||||
@@ -14,7 +14,8 @@ import homeassistant.components.remote as remote
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_HOST, CONF_PORT, ATTR_ENTITY_ID)
|
||||
from homeassistant.components.remote import PLATFORM_SCHEMA, DOMAIN
|
||||
from homeassistant.components.remote import (
|
||||
PLATFORM_SCHEMA, DOMAIN, ATTR_DEVICE, ATTR_COMMAND, ATTR_ACTIVITY)
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
|
||||
@@ -22,10 +23,6 @@ REQUIREMENTS = ['pyharmony==1.0.12']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_DEVICE = 'device'
|
||||
ATTR_COMMAND = 'command'
|
||||
ATTR_ACTIVITY = 'activity'
|
||||
|
||||
DEFAULT_PORT = 5222
|
||||
DEVICES = []
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ DEFAULT_NAME = 'EnOcean sensor'
|
||||
DEPENDENCIES = ['enocean']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ID): cv.string,
|
||||
vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
@@ -68,6 +68,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Setup the Nest Sensor."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
nest = hass.data[DATA_NEST]
|
||||
conf = config.get(CONF_MONITORED_CONDITIONS, _VALID_SENSOR_TYPES)
|
||||
|
||||
@@ -125,13 +128,20 @@ class NestSensor(Entity):
|
||||
|
||||
# device specific
|
||||
self._location = self.device.where
|
||||
self._name = self.device.name_long
|
||||
self._name = "{} {}".format(self.device.name_long,
|
||||
self.variable.replace("_", " "))
|
||||
self._state = None
|
||||
self._unit = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the nest, if any."""
|
||||
return "{} {}".format(self._name, self.variable.replace("_", " "))
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit the value is expressed in."""
|
||||
return self._unit
|
||||
|
||||
|
||||
class NestBasicSensor(NestSensor):
|
||||
@@ -142,13 +152,10 @@ class NestBasicSensor(NestSensor):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit the value is expressed in."""
|
||||
return SENSOR_UNITS.get(self.variable, None)
|
||||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
self._unit = SENSOR_UNITS.get(self.variable, None)
|
||||
|
||||
if self.variable == 'operation_mode':
|
||||
self._state = getattr(self.device, "mode")
|
||||
else:
|
||||
@@ -158,14 +165,6 @@ class NestBasicSensor(NestSensor):
|
||||
class NestTempSensor(NestSensor):
|
||||
"""Representation of a Nest Temperature sensor."""
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit the value is expressed in."""
|
||||
if self.device.temperature_scale == 'C':
|
||||
return TEMP_CELSIUS
|
||||
else:
|
||||
return TEMP_FAHRENHEIT
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
@@ -173,6 +172,11 @@ class NestTempSensor(NestSensor):
|
||||
|
||||
def update(self):
|
||||
"""Retrieve latest state."""
|
||||
if self.device.temperature_scale == 'C':
|
||||
self._unit = TEMP_CELSIUS
|
||||
else:
|
||||
self._unit = TEMP_FAHRENHEIT
|
||||
|
||||
temp = getattr(self.device, self.variable)
|
||||
if temp is None:
|
||||
self._state = None
|
||||
|
||||
@@ -151,15 +151,9 @@ class SynoApi():
|
||||
except:
|
||||
_LOGGER.error("Error setting up Synology DSM")
|
||||
|
||||
def utilisation(self):
|
||||
"""Return utilisation information from API."""
|
||||
if self._api is not None:
|
||||
return self._api.utilisation
|
||||
|
||||
def storage(self):
|
||||
"""Return storage information from API."""
|
||||
if self._api is not None:
|
||||
return self._api.storage
|
||||
# Will be updated when `update` gets called.
|
||||
self.utilisation = self._api.utilisation
|
||||
self.storage = self._api.storage
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
@@ -219,14 +213,14 @@ class SynoNasUtilSensor(SynoNasSensor):
|
||||
'memory_total_swap', 'memory_total_real']
|
||||
|
||||
if self.var_id in network_sensors or self.var_id in memory_sensors:
|
||||
attr = getattr(self._api.utilisation(), self.var_id)(False)
|
||||
attr = getattr(self._api.utilisation, self.var_id)(False)
|
||||
|
||||
if self.var_id in network_sensors:
|
||||
return round(attr / 1024.0, 1)
|
||||
elif self.var_id in memory_sensors:
|
||||
return round(attr / 1024.0 / 1024.0, 1)
|
||||
else:
|
||||
return getattr(self._api.utilisation(), self.var_id)
|
||||
return getattr(self._api.utilisation, self.var_id)
|
||||
|
||||
|
||||
class SynoNasStorageSensor(SynoNasSensor):
|
||||
@@ -240,7 +234,7 @@ class SynoNasStorageSensor(SynoNasSensor):
|
||||
|
||||
if self.monitor_device is not None:
|
||||
if self.var_id in temp_sensors:
|
||||
attr = getattr(self._api.storage(),
|
||||
attr = getattr(self._api.storage,
|
||||
self.var_id)(self.monitor_device)
|
||||
|
||||
if self._api.temp_unit == TEMP_CELSIUS:
|
||||
@@ -248,5 +242,5 @@ class SynoNasStorageSensor(SynoNasSensor):
|
||||
else:
|
||||
return round(attr * 1.8 + 32.0, 1)
|
||||
else:
|
||||
return getattr(self._api.storage(),
|
||||
return getattr(self._api.storage,
|
||||
self.var_id)(self.monitor_device)
|
||||
|
||||
@@ -82,7 +82,6 @@ def async_setup(hass, config):
|
||||
"""Handle calls to the switch services."""
|
||||
target_switches = component.async_extract_from_service(service)
|
||||
|
||||
update_tasks = []
|
||||
for switch in target_switches:
|
||||
if service.service == SERVICE_TURN_ON:
|
||||
yield from switch.async_turn_on()
|
||||
@@ -91,12 +90,17 @@ def async_setup(hass, config):
|
||||
else:
|
||||
yield from switch.async_turn_off()
|
||||
|
||||
if switch.should_poll:
|
||||
update_coro = switch.async_update_ha_state(True)
|
||||
if hasattr(switch, 'async_update'):
|
||||
update_tasks.append(hass.loop.create_task(update_coro))
|
||||
else:
|
||||
yield from update_coro
|
||||
update_tasks = []
|
||||
for switch in target_switches:
|
||||
if not switch.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = hass.loop.create_task(
|
||||
switch.async_update_ha_state(True))
|
||||
if hasattr(switch, '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)
|
||||
|
||||
@@ -20,7 +20,7 @@ DEFAULT_NAME = 'EnOcean Switch'
|
||||
DEPENDENCIES = ['enocean']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ID): cv.string,
|
||||
vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
return False
|
||||
finally:
|
||||
if response is not None:
|
||||
yield from response.close()
|
||||
yield from response.release()
|
||||
|
||||
try:
|
||||
token = data['data']['token']
|
||||
@@ -72,7 +72,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
return False
|
||||
finally:
|
||||
if response is not None:
|
||||
yield from response.close()
|
||||
yield from response.release()
|
||||
|
||||
yield from async_add_devices(
|
||||
HookSmartHome(
|
||||
@@ -127,7 +127,7 @@ class HookSmartHome(SwitchDevice):
|
||||
|
||||
finally:
|
||||
if response is not None:
|
||||
yield from response.close()
|
||||
yield from response.release()
|
||||
|
||||
_LOGGER.debug("Got: %s", data)
|
||||
return data['return_value'] == '1'
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.const import (CONF_HOST, CONF_NAME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['https://github.com/GadgetReactor/pyHS100/archive/'
|
||||
'fadb76c5a0e04f4995f16055845ffedc6d658316.zip#pyHS100==0.2.1']
|
||||
'1f771b7d8090a91c6a58931532e42730b021cbde.zip#pyHS100==0.2.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -204,7 +204,6 @@ class ActiveConnection:
|
||||
self.hass = hass
|
||||
self.request = request
|
||||
self.wsock = None
|
||||
self.socket_task = None
|
||||
self.event_listeners = {}
|
||||
|
||||
def debug(self, message1, message2=''):
|
||||
@@ -220,34 +219,6 @@ class ActiveConnection:
|
||||
self.debug('Sending', message)
|
||||
self.wsock.send_json(message, dumps=JSON_DUMP)
|
||||
|
||||
@callback
|
||||
def _cancel_connection(self, event):
|
||||
"""Cancel this connection."""
|
||||
self.socket_task.cancel()
|
||||
|
||||
@asyncio.coroutine
|
||||
def _call_service_helper(self, msg):
|
||||
"""Helper to call a service and fire complete message."""
|
||||
yield from self.hass.services.async_call(msg['domain'], msg['service'],
|
||||
msg['service_data'], True)
|
||||
try:
|
||||
self.send_message(result_message(msg['id']))
|
||||
except RuntimeError:
|
||||
# Socket has been closed.
|
||||
pass
|
||||
|
||||
@callback
|
||||
def _forward_event(self, iden, event):
|
||||
"""Helper to forward events to websocket."""
|
||||
if event.event_type == EVENT_TIME_CHANGED:
|
||||
return
|
||||
|
||||
try:
|
||||
self.send_message(event_message(iden, event))
|
||||
except RuntimeError:
|
||||
# Socket has been closed.
|
||||
pass
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle(self):
|
||||
"""Handle the websocket connection."""
|
||||
@@ -255,9 +226,15 @@ class ActiveConnection:
|
||||
yield from wsock.prepare(self.request)
|
||||
|
||||
# Set up to cancel this connection when Home Assistant shuts down
|
||||
self.socket_task = asyncio.Task.current_task(loop=self.hass.loop)
|
||||
self.hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP,
|
||||
self._cancel_connection)
|
||||
socket_task = asyncio.Task.current_task(loop=self.hass.loop)
|
||||
|
||||
@callback
|
||||
def cancel_connection(event):
|
||||
"""Cancel this connection."""
|
||||
socket_task.cancel()
|
||||
|
||||
unsub_stop = self.hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP,
|
||||
cancel_connection)
|
||||
|
||||
self.debug('Connected')
|
||||
|
||||
@@ -351,6 +328,8 @@ class ActiveConnection:
|
||||
_LOGGER.exception(error)
|
||||
|
||||
finally:
|
||||
unsub_stop()
|
||||
|
||||
for unsub in self.event_listeners.values():
|
||||
unsub()
|
||||
|
||||
@@ -363,8 +342,20 @@ class ActiveConnection:
|
||||
"""Handle subscribe events command."""
|
||||
msg = SUBSCRIBE_EVENTS_MESSAGE_SCHEMA(msg)
|
||||
|
||||
@callback
|
||||
def forward_events(event):
|
||||
"""Helper to forward events to websocket."""
|
||||
if event.event_type == EVENT_TIME_CHANGED:
|
||||
return
|
||||
|
||||
try:
|
||||
self.send_message(event_message(msg['id'], event))
|
||||
except RuntimeError:
|
||||
# Socket has been closed.
|
||||
pass
|
||||
|
||||
self.event_listeners[msg['id']] = self.hass.bus.async_listen(
|
||||
msg['event_type'], partial(self._forward_event, msg['id']))
|
||||
msg['event_type'], forward_events)
|
||||
|
||||
self.send_message(result_message(msg['id']))
|
||||
|
||||
@@ -386,7 +377,18 @@ class ActiveConnection:
|
||||
"""Handle call service command."""
|
||||
msg = CALL_SERVICE_MESSAGE_SCHEMA(msg)
|
||||
|
||||
self.hass.async_add_job(self._call_service_helper(msg))
|
||||
@asyncio.coroutine
|
||||
def call_service_helper(msg):
|
||||
"""Helper to call a service and fire complete message."""
|
||||
yield from self.hass.services.async_call(
|
||||
msg['domain'], msg['service'], msg['service_data'], True)
|
||||
try:
|
||||
self.send_message(result_message(msg['id']))
|
||||
except RuntimeError:
|
||||
# Socket has been closed.
|
||||
pass
|
||||
|
||||
self.hass.async_add_job(call_service_helper(msg))
|
||||
|
||||
def handle_get_states(self, msg):
|
||||
"""Handle get states command."""
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 34
|
||||
PATCH_VERSION = '0'
|
||||
PATCH_VERSION = '5'
|
||||
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
|
||||
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
|
||||
REQUIRED_PYTHON_VER = (3, 4, 2)
|
||||
|
||||
@@ -164,13 +164,13 @@ hikvision==0.4
|
||||
# http://github.com/adafruit/Adafruit_Python_DHT/archive/310c59b0293354d07d94375f1365f7b9b9110c7d.zip#Adafruit_DHT==1.3.0
|
||||
|
||||
# homeassistant.components.nest
|
||||
http://github.com/technicalpickles/python-nest/archive/0be5c8a6307ee81540f21aac4fcd22cc5d98c988.zip#python-nest==3.0.0
|
||||
http://github.com/technicalpickles/python-nest/archive/b8391d2b3cb8682f8b0c2bdff477179983609f39.zip#python-nest==3.0.2
|
||||
|
||||
# homeassistant.components.light.flux_led
|
||||
https://github.com/Danielhiversen/flux_led/archive/0.9.zip#flux_led==0.9
|
||||
|
||||
# homeassistant.components.switch.tplink
|
||||
https://github.com/GadgetReactor/pyHS100/archive/fadb76c5a0e04f4995f16055845ffedc6d658316.zip#pyHS100==0.2.1
|
||||
https://github.com/GadgetReactor/pyHS100/archive/1f771b7d8090a91c6a58931532e42730b021cbde.zip#pyHS100==0.2.0
|
||||
|
||||
# homeassistant.components.switch.dlink
|
||||
https://github.com/LinuxChristian/pyW215/archive/v0.3.7.zip#pyW215==0.3.7
|
||||
|
||||
1
tests/components/emulated_hue/__init__.py
Normal file
1
tests/components/emulated_hue/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Tests for emulated_hue."""
|
||||
784
tests/components/test_emulated_hue.py → tests/components/emulated_hue/test_hue_api.py
Executable file → Normal file
784
tests/components/test_emulated_hue.py → tests/components/emulated_hue/test_hue_api.py
Executable file → Normal file
@@ -1,429 +1,355 @@
|
||||
"""The tests for the emulated Hue component."""
|
||||
import json
|
||||
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from homeassistant import bootstrap, const, core
|
||||
import homeassistant.components as core_components
|
||||
from homeassistant.components import emulated_hue, http, light, script
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
from homeassistant.components.emulated_hue import (
|
||||
HUE_API_STATE_ON, HUE_API_STATE_BRI)
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
from tests.common import get_test_instance_port, get_test_home_assistant
|
||||
|
||||
HTTP_SERVER_PORT = get_test_instance_port()
|
||||
BRIDGE_SERVER_PORT = get_test_instance_port()
|
||||
|
||||
BRIDGE_URL_BASE = 'http://127.0.0.1:{}'.format(BRIDGE_SERVER_PORT) + '{}'
|
||||
JSON_HEADERS = {const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON}
|
||||
|
||||
|
||||
def setup_hass_instance(emulated_hue_config):
|
||||
"""Set up the Home Assistant instance to test."""
|
||||
hass = get_test_home_assistant()
|
||||
|
||||
# We need to do this to get access to homeassistant/turn_(on,off)
|
||||
run_coroutine_threadsafe(
|
||||
core_components.async_setup(hass, {core.DOMAIN: {}}), hass.loop
|
||||
).result()
|
||||
|
||||
bootstrap.setup_component(
|
||||
hass, http.DOMAIN,
|
||||
{http.DOMAIN: {http.CONF_SERVER_PORT: HTTP_SERVER_PORT}})
|
||||
|
||||
bootstrap.setup_component(hass, emulated_hue.DOMAIN, emulated_hue_config)
|
||||
|
||||
return hass
|
||||
|
||||
|
||||
def start_hass_instance(hass):
|
||||
"""Start the Home Assistant instance to test."""
|
||||
hass.start()
|
||||
|
||||
|
||||
class TestEmulatedHue(unittest.TestCase):
|
||||
"""Test the emulated Hue component."""
|
||||
|
||||
hass = None
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Setup the class."""
|
||||
cls.hass = setup_hass_instance({
|
||||
emulated_hue.DOMAIN: {
|
||||
emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT
|
||||
}})
|
||||
|
||||
start_hass_instance(cls.hass)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""Stop the class."""
|
||||
cls.hass.stop()
|
||||
|
||||
def test_description_xml(self):
|
||||
"""Test the description."""
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
result = requests.get(
|
||||
BRIDGE_URL_BASE.format('/description.xml'), timeout=5)
|
||||
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertTrue('text/xml' in result.headers['content-type'])
|
||||
|
||||
# Make sure the XML is parsable
|
||||
# pylint: disable=bare-except
|
||||
try:
|
||||
ET.fromstring(result.text)
|
||||
except:
|
||||
self.fail('description.xml is not valid XML!')
|
||||
|
||||
def test_create_username(self):
|
||||
"""Test the creation of an username."""
|
||||
request_json = {'devicetype': 'my_device'}
|
||||
|
||||
result = requests.post(
|
||||
BRIDGE_URL_BASE.format('/api'), data=json.dumps(request_json),
|
||||
timeout=5)
|
||||
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertTrue('application/json' in result.headers['content-type'])
|
||||
|
||||
resp_json = result.json()
|
||||
success_json = resp_json[0]
|
||||
|
||||
self.assertTrue('success' in success_json)
|
||||
self.assertTrue('username' in success_json['success'])
|
||||
|
||||
def test_valid_username_request(self):
|
||||
"""Test request with a valid username."""
|
||||
request_json = {'invalid_key': 'my_device'}
|
||||
|
||||
result = requests.post(
|
||||
BRIDGE_URL_BASE.format('/api'), data=json.dumps(request_json),
|
||||
timeout=5)
|
||||
|
||||
self.assertEqual(result.status_code, 400)
|
||||
|
||||
|
||||
class TestEmulatedHueExposedByDefault(unittest.TestCase):
|
||||
"""Test class for emulated hue component."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Setup the class."""
|
||||
cls.hass = setup_hass_instance({
|
||||
emulated_hue.DOMAIN: {
|
||||
emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT,
|
||||
emulated_hue.CONF_EXPOSE_BY_DEFAULT: True
|
||||
}
|
||||
})
|
||||
|
||||
bootstrap.setup_component(cls.hass, light.DOMAIN, {
|
||||
'light': [
|
||||
{
|
||||
'platform': 'demo',
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
bootstrap.setup_component(cls.hass, script.DOMAIN, {
|
||||
'script': {
|
||||
'set_kitchen_light': {
|
||||
'sequence': [
|
||||
{
|
||||
'service_template':
|
||||
"light.turn_{{ requested_state }}",
|
||||
'data_template': {
|
||||
'entity_id': 'light.kitchen_lights',
|
||||
'brightness': "{{ requested_level }}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
start_hass_instance(cls.hass)
|
||||
|
||||
# Kitchen light is explicitly excluded from being exposed
|
||||
kitchen_light_entity = cls.hass.states.get('light.kitchen_lights')
|
||||
attrs = dict(kitchen_light_entity.attributes)
|
||||
attrs[emulated_hue.ATTR_EMULATED_HUE] = False
|
||||
cls.hass.states.set(
|
||||
kitchen_light_entity.entity_id, kitchen_light_entity.state,
|
||||
attributes=attrs)
|
||||
|
||||
# Expose the script
|
||||
script_entity = cls.hass.states.get('script.set_kitchen_light')
|
||||
attrs = dict(script_entity.attributes)
|
||||
attrs[emulated_hue.ATTR_EMULATED_HUE] = True
|
||||
cls.hass.states.set(
|
||||
script_entity.entity_id, script_entity.state, attributes=attrs
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""Stop the class."""
|
||||
cls.hass.stop()
|
||||
|
||||
def test_discover_lights(self):
|
||||
"""Test the discovery of lights."""
|
||||
result = requests.get(
|
||||
BRIDGE_URL_BASE.format('/api/username/lights'), timeout=5)
|
||||
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertTrue('application/json' in result.headers['content-type'])
|
||||
|
||||
result_json = result.json()
|
||||
|
||||
# Make sure the lights we added to the config are there
|
||||
self.assertTrue('light.ceiling_lights' in result_json)
|
||||
self.assertTrue('light.bed_light' in result_json)
|
||||
self.assertTrue('script.set_kitchen_light' in result_json)
|
||||
self.assertTrue('light.kitchen_lights' not in result_json)
|
||||
|
||||
def test_get_light_state(self):
|
||||
"""Test the getting of light state."""
|
||||
# Turn office light on and set to 127 brightness
|
||||
self.hass.services.call(
|
||||
light.DOMAIN, const.SERVICE_TURN_ON,
|
||||
{
|
||||
const.ATTR_ENTITY_ID: 'light.ceiling_lights',
|
||||
light.ATTR_BRIGHTNESS: 127
|
||||
},
|
||||
blocking=True)
|
||||
|
||||
office_json = self.perform_get_light_state('light.ceiling_lights', 200)
|
||||
|
||||
self.assertEqual(office_json['state'][HUE_API_STATE_ON], True)
|
||||
self.assertEqual(office_json['state'][HUE_API_STATE_BRI], 127)
|
||||
|
||||
# Turn bedroom light off
|
||||
self.hass.services.call(
|
||||
light.DOMAIN, const.SERVICE_TURN_OFF,
|
||||
{
|
||||
const.ATTR_ENTITY_ID: 'light.bed_light'
|
||||
},
|
||||
blocking=True)
|
||||
|
||||
bedroom_json = self.perform_get_light_state('light.bed_light', 200)
|
||||
|
||||
self.assertEqual(bedroom_json['state'][HUE_API_STATE_ON], False)
|
||||
self.assertEqual(bedroom_json['state'][HUE_API_STATE_BRI], 0)
|
||||
|
||||
# Make sure kitchen light isn't accessible
|
||||
kitchen_url = '/api/username/lights/{}'.format('light.kitchen_lights')
|
||||
kitchen_result = requests.get(
|
||||
BRIDGE_URL_BASE.format(kitchen_url), timeout=5)
|
||||
|
||||
self.assertEqual(kitchen_result.status_code, 404)
|
||||
|
||||
def test_put_light_state(self):
|
||||
"""Test the seeting of light states."""
|
||||
self.perform_put_test_on_ceiling_lights()
|
||||
|
||||
# Turn the bedroom light on first
|
||||
self.hass.services.call(
|
||||
light.DOMAIN, const.SERVICE_TURN_ON,
|
||||
{const.ATTR_ENTITY_ID: 'light.bed_light',
|
||||
light.ATTR_BRIGHTNESS: 153},
|
||||
blocking=True)
|
||||
|
||||
bed_light = self.hass.states.get('light.bed_light')
|
||||
self.assertEqual(bed_light.state, STATE_ON)
|
||||
self.assertEqual(bed_light.attributes[light.ATTR_BRIGHTNESS], 153)
|
||||
|
||||
# Go through the API to turn it off
|
||||
bedroom_result = self.perform_put_light_state(
|
||||
'light.bed_light', False)
|
||||
|
||||
bedroom_result_json = bedroom_result.json()
|
||||
|
||||
self.assertEqual(bedroom_result.status_code, 200)
|
||||
self.assertTrue(
|
||||
'application/json' in bedroom_result.headers['content-type'])
|
||||
|
||||
self.assertEqual(len(bedroom_result_json), 1)
|
||||
|
||||
# Check to make sure the state changed
|
||||
bed_light = self.hass.states.get('light.bed_light')
|
||||
self.assertEqual(bed_light.state, STATE_OFF)
|
||||
|
||||
# Make sure we can't change the kitchen light state
|
||||
kitchen_result = self.perform_put_light_state(
|
||||
'light.kitchen_light', True)
|
||||
self.assertEqual(kitchen_result.status_code, 404)
|
||||
|
||||
def test_put_light_state_script(self):
|
||||
"""Test the setting of script variables."""
|
||||
# Turn the kitchen light off first
|
||||
self.hass.services.call(
|
||||
light.DOMAIN, const.SERVICE_TURN_OFF,
|
||||
{const.ATTR_ENTITY_ID: 'light.kitchen_lights'},
|
||||
blocking=True)
|
||||
|
||||
# Emulated hue converts 0-100% to 0-255.
|
||||
level = 23
|
||||
brightness = round(level * 255 / 100)
|
||||
|
||||
script_result = self.perform_put_light_state(
|
||||
'script.set_kitchen_light', True, brightness)
|
||||
|
||||
script_result_json = script_result.json()
|
||||
|
||||
self.assertEqual(script_result.status_code, 200)
|
||||
self.assertEqual(len(script_result_json), 2)
|
||||
|
||||
# Wait until script is complete before continuing
|
||||
self.hass.block_till_done()
|
||||
|
||||
kitchen_light = self.hass.states.get('light.kitchen_lights')
|
||||
self.assertEqual(kitchen_light.state, 'on')
|
||||
self.assertEqual(
|
||||
kitchen_light.attributes[light.ATTR_BRIGHTNESS],
|
||||
level)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def test_put_with_form_urlencoded_content_type(self):
|
||||
"""Test the form with urlencoded content."""
|
||||
# Needed for Alexa
|
||||
self.perform_put_test_on_ceiling_lights(
|
||||
'application/x-www-form-urlencoded')
|
||||
|
||||
# Make sure we fail gracefully when we can't parse the data
|
||||
data = {'key1': 'value1', 'key2': 'value2'}
|
||||
result = requests.put(
|
||||
BRIDGE_URL_BASE.format(
|
||||
'/api/username/lights/{}/state'.format(
|
||||
'light.ceiling_lights')), data=data)
|
||||
|
||||
self.assertEqual(result.status_code, 400)
|
||||
|
||||
def test_entity_not_found(self):
|
||||
"""Test for entity which are not found."""
|
||||
result = requests.get(
|
||||
BRIDGE_URL_BASE.format(
|
||||
'/api/username/lights/{}'.format("not.existant_entity")),
|
||||
timeout=5)
|
||||
|
||||
self.assertEqual(result.status_code, 404)
|
||||
|
||||
result = requests.put(
|
||||
BRIDGE_URL_BASE.format(
|
||||
'/api/username/lights/{}/state'.format("non.existant_entity")),
|
||||
timeout=5)
|
||||
|
||||
self.assertEqual(result.status_code, 404)
|
||||
|
||||
def test_allowed_methods(self):
|
||||
"""Test the allowed methods."""
|
||||
result = requests.get(
|
||||
BRIDGE_URL_BASE.format(
|
||||
'/api/username/lights/{}/state'.format(
|
||||
"light.ceiling_lights")))
|
||||
|
||||
self.assertEqual(result.status_code, 405)
|
||||
|
||||
result = requests.put(
|
||||
BRIDGE_URL_BASE.format(
|
||||
'/api/username/lights/{}'.format("light.ceiling_lights")),
|
||||
data={'key1': 'value1'})
|
||||
|
||||
self.assertEqual(result.status_code, 405)
|
||||
|
||||
result = requests.put(
|
||||
BRIDGE_URL_BASE.format('/api/username/lights'),
|
||||
data={'key1': 'value1'})
|
||||
|
||||
self.assertEqual(result.status_code, 405)
|
||||
|
||||
def test_proper_put_state_request(self):
|
||||
"""Test the request to set the state."""
|
||||
# Test proper on value parsing
|
||||
result = requests.put(
|
||||
BRIDGE_URL_BASE.format(
|
||||
'/api/username/lights/{}/state'.format(
|
||||
'light.ceiling_lights')),
|
||||
data=json.dumps({HUE_API_STATE_ON: 1234}))
|
||||
|
||||
self.assertEqual(result.status_code, 400)
|
||||
|
||||
# Test proper brightness value parsing
|
||||
result = requests.put(
|
||||
BRIDGE_URL_BASE.format(
|
||||
'/api/username/lights/{}/state'.format(
|
||||
'light.ceiling_lights')), data=json.dumps({
|
||||
HUE_API_STATE_ON: True,
|
||||
HUE_API_STATE_BRI: 'Hello world!'
|
||||
}))
|
||||
|
||||
self.assertEqual(result.status_code, 400)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def perform_put_test_on_ceiling_lights(self,
|
||||
content_type='application/json'):
|
||||
"""Test the setting of a light."""
|
||||
# Turn the office light off first
|
||||
self.hass.services.call(
|
||||
light.DOMAIN, const.SERVICE_TURN_OFF,
|
||||
{const.ATTR_ENTITY_ID: 'light.ceiling_lights'},
|
||||
blocking=True)
|
||||
|
||||
ceiling_lights = self.hass.states.get('light.ceiling_lights')
|
||||
self.assertEqual(ceiling_lights.state, STATE_OFF)
|
||||
|
||||
# Go through the API to turn it on
|
||||
office_result = self.perform_put_light_state(
|
||||
'light.ceiling_lights', True, 56, content_type)
|
||||
|
||||
office_result_json = office_result.json()
|
||||
|
||||
self.assertEqual(office_result.status_code, 200)
|
||||
self.assertTrue(
|
||||
'application/json' in office_result.headers['content-type'])
|
||||
|
||||
self.assertEqual(len(office_result_json), 2)
|
||||
|
||||
# Check to make sure the state changed
|
||||
ceiling_lights = self.hass.states.get('light.ceiling_lights')
|
||||
self.assertEqual(ceiling_lights.state, STATE_ON)
|
||||
self.assertEqual(ceiling_lights.attributes[light.ATTR_BRIGHTNESS], 56)
|
||||
|
||||
def perform_get_light_state(self, entity_id, expected_status):
|
||||
"""Test the gettting of a light state."""
|
||||
result = requests.get(
|
||||
BRIDGE_URL_BASE.format(
|
||||
'/api/username/lights/{}'.format(entity_id)), timeout=5)
|
||||
|
||||
self.assertEqual(result.status_code, expected_status)
|
||||
|
||||
if expected_status == 200:
|
||||
self.assertTrue(
|
||||
'application/json' in result.headers['content-type'])
|
||||
|
||||
return result.json()
|
||||
|
||||
return None
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def perform_put_light_state(self, entity_id, is_on, brightness=None,
|
||||
content_type='application/json'):
|
||||
"""Test the setting of a light state."""
|
||||
url = BRIDGE_URL_BASE.format(
|
||||
'/api/username/lights/{}/state'.format(entity_id))
|
||||
|
||||
req_headers = {'Content-Type': content_type}
|
||||
|
||||
data = {HUE_API_STATE_ON: is_on}
|
||||
|
||||
if brightness is not None:
|
||||
data[HUE_API_STATE_BRI] = brightness
|
||||
|
||||
result = requests.put(
|
||||
url, data=json.dumps(data), timeout=5, headers=req_headers)
|
||||
|
||||
return result
|
||||
"""The tests for the emulated Hue component."""
|
||||
import json
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
import requests
|
||||
|
||||
from homeassistant import bootstrap, const, core
|
||||
import homeassistant.components as core_components
|
||||
from homeassistant.components import emulated_hue, http, light, script
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
from homeassistant.components.emulated_hue.hue_api import (
|
||||
HUE_API_STATE_ON, HUE_API_STATE_BRI)
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
from tests.common import get_test_instance_port, get_test_home_assistant
|
||||
|
||||
HTTP_SERVER_PORT = get_test_instance_port()
|
||||
BRIDGE_SERVER_PORT = get_test_instance_port()
|
||||
|
||||
BRIDGE_URL_BASE = 'http://127.0.0.1:{}'.format(BRIDGE_SERVER_PORT) + '{}'
|
||||
JSON_HEADERS = {const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON}
|
||||
|
||||
|
||||
class TestEmulatedHueExposedByDefault(unittest.TestCase):
|
||||
"""Test class for emulated hue component."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Setup the class."""
|
||||
cls.hass = hass = get_test_home_assistant()
|
||||
|
||||
# We need to do this to get access to homeassistant/turn_(on,off)
|
||||
run_coroutine_threadsafe(
|
||||
core_components.async_setup(hass, {core.DOMAIN: {}}), hass.loop
|
||||
).result()
|
||||
|
||||
bootstrap.setup_component(
|
||||
hass, http.DOMAIN,
|
||||
{http.DOMAIN: {http.CONF_SERVER_PORT: HTTP_SERVER_PORT}})
|
||||
|
||||
with patch('homeassistant.components'
|
||||
'.emulated_hue.UPNPResponderThread'):
|
||||
bootstrap.setup_component(hass, emulated_hue.DOMAIN, {
|
||||
emulated_hue.DOMAIN: {
|
||||
emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT,
|
||||
emulated_hue.CONF_EXPOSE_BY_DEFAULT: True
|
||||
}
|
||||
})
|
||||
|
||||
bootstrap.setup_component(cls.hass, light.DOMAIN, {
|
||||
'light': [
|
||||
{
|
||||
'platform': 'demo',
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
bootstrap.setup_component(cls.hass, script.DOMAIN, {
|
||||
'script': {
|
||||
'set_kitchen_light': {
|
||||
'sequence': [
|
||||
{
|
||||
'service_template':
|
||||
"light.turn_{{ requested_state }}",
|
||||
'data_template': {
|
||||
'entity_id': 'light.kitchen_lights',
|
||||
'brightness': "{{ requested_level }}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
cls.hass.start()
|
||||
|
||||
# Kitchen light is explicitly excluded from being exposed
|
||||
kitchen_light_entity = cls.hass.states.get('light.kitchen_lights')
|
||||
attrs = dict(kitchen_light_entity.attributes)
|
||||
attrs[emulated_hue.ATTR_EMULATED_HUE] = False
|
||||
cls.hass.states.set(
|
||||
kitchen_light_entity.entity_id, kitchen_light_entity.state,
|
||||
attributes=attrs)
|
||||
|
||||
# Expose the script
|
||||
script_entity = cls.hass.states.get('script.set_kitchen_light')
|
||||
attrs = dict(script_entity.attributes)
|
||||
attrs[emulated_hue.ATTR_EMULATED_HUE] = True
|
||||
cls.hass.states.set(
|
||||
script_entity.entity_id, script_entity.state, attributes=attrs
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""Stop the class."""
|
||||
cls.hass.stop()
|
||||
|
||||
def test_discover_lights(self):
|
||||
"""Test the discovery of lights."""
|
||||
result = requests.get(
|
||||
BRIDGE_URL_BASE.format('/api/username/lights'), timeout=5)
|
||||
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertTrue('application/json' in result.headers['content-type'])
|
||||
|
||||
result_json = result.json()
|
||||
|
||||
# Make sure the lights we added to the config are there
|
||||
self.assertTrue('light.ceiling_lights' in result_json)
|
||||
self.assertTrue('light.bed_light' in result_json)
|
||||
self.assertTrue('script.set_kitchen_light' in result_json)
|
||||
self.assertTrue('light.kitchen_lights' not in result_json)
|
||||
|
||||
def test_get_light_state(self):
|
||||
"""Test the getting of light state."""
|
||||
# Turn office light on and set to 127 brightness
|
||||
self.hass.services.call(
|
||||
light.DOMAIN, const.SERVICE_TURN_ON,
|
||||
{
|
||||
const.ATTR_ENTITY_ID: 'light.ceiling_lights',
|
||||
light.ATTR_BRIGHTNESS: 127
|
||||
},
|
||||
blocking=True)
|
||||
|
||||
office_json = self.perform_get_light_state('light.ceiling_lights', 200)
|
||||
|
||||
self.assertEqual(office_json['state'][HUE_API_STATE_ON], True)
|
||||
self.assertEqual(office_json['state'][HUE_API_STATE_BRI], 127)
|
||||
|
||||
# Turn bedroom light off
|
||||
self.hass.services.call(
|
||||
light.DOMAIN, const.SERVICE_TURN_OFF,
|
||||
{
|
||||
const.ATTR_ENTITY_ID: 'light.bed_light'
|
||||
},
|
||||
blocking=True)
|
||||
|
||||
bedroom_json = self.perform_get_light_state('light.bed_light', 200)
|
||||
|
||||
self.assertEqual(bedroom_json['state'][HUE_API_STATE_ON], False)
|
||||
self.assertEqual(bedroom_json['state'][HUE_API_STATE_BRI], 0)
|
||||
|
||||
# Make sure kitchen light isn't accessible
|
||||
kitchen_url = '/api/username/lights/{}'.format('light.kitchen_lights')
|
||||
kitchen_result = requests.get(
|
||||
BRIDGE_URL_BASE.format(kitchen_url), timeout=5)
|
||||
|
||||
self.assertEqual(kitchen_result.status_code, 404)
|
||||
|
||||
def test_put_light_state(self):
|
||||
"""Test the seeting of light states."""
|
||||
self.perform_put_test_on_ceiling_lights()
|
||||
|
||||
# Turn the bedroom light on first
|
||||
self.hass.services.call(
|
||||
light.DOMAIN, const.SERVICE_TURN_ON,
|
||||
{const.ATTR_ENTITY_ID: 'light.bed_light',
|
||||
light.ATTR_BRIGHTNESS: 153},
|
||||
blocking=True)
|
||||
|
||||
bed_light = self.hass.states.get('light.bed_light')
|
||||
self.assertEqual(bed_light.state, STATE_ON)
|
||||
self.assertEqual(bed_light.attributes[light.ATTR_BRIGHTNESS], 153)
|
||||
|
||||
# Go through the API to turn it off
|
||||
bedroom_result = self.perform_put_light_state(
|
||||
'light.bed_light', False)
|
||||
|
||||
bedroom_result_json = bedroom_result.json()
|
||||
|
||||
self.assertEqual(bedroom_result.status_code, 200)
|
||||
self.assertTrue(
|
||||
'application/json' in bedroom_result.headers['content-type'])
|
||||
|
||||
self.assertEqual(len(bedroom_result_json), 1)
|
||||
|
||||
# Check to make sure the state changed
|
||||
bed_light = self.hass.states.get('light.bed_light')
|
||||
self.assertEqual(bed_light.state, STATE_OFF)
|
||||
|
||||
# Make sure we can't change the kitchen light state
|
||||
kitchen_result = self.perform_put_light_state(
|
||||
'light.kitchen_light', True)
|
||||
self.assertEqual(kitchen_result.status_code, 404)
|
||||
|
||||
def test_put_light_state_script(self):
|
||||
"""Test the setting of script variables."""
|
||||
# Turn the kitchen light off first
|
||||
self.hass.services.call(
|
||||
light.DOMAIN, const.SERVICE_TURN_OFF,
|
||||
{const.ATTR_ENTITY_ID: 'light.kitchen_lights'},
|
||||
blocking=True)
|
||||
|
||||
# Emulated hue converts 0-100% to 0-255.
|
||||
level = 23
|
||||
brightness = round(level * 255 / 100)
|
||||
|
||||
script_result = self.perform_put_light_state(
|
||||
'script.set_kitchen_light', True, brightness)
|
||||
|
||||
script_result_json = script_result.json()
|
||||
|
||||
self.assertEqual(script_result.status_code, 200)
|
||||
self.assertEqual(len(script_result_json), 2)
|
||||
|
||||
# Wait until script is complete before continuing
|
||||
self.hass.block_till_done()
|
||||
|
||||
kitchen_light = self.hass.states.get('light.kitchen_lights')
|
||||
self.assertEqual(kitchen_light.state, 'on')
|
||||
self.assertEqual(
|
||||
kitchen_light.attributes[light.ATTR_BRIGHTNESS],
|
||||
level)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def test_put_with_form_urlencoded_content_type(self):
|
||||
"""Test the form with urlencoded content."""
|
||||
# Needed for Alexa
|
||||
self.perform_put_test_on_ceiling_lights(
|
||||
'application/x-www-form-urlencoded')
|
||||
|
||||
# Make sure we fail gracefully when we can't parse the data
|
||||
data = {'key1': 'value1', 'key2': 'value2'}
|
||||
result = requests.put(
|
||||
BRIDGE_URL_BASE.format(
|
||||
'/api/username/lights/{}/state'.format(
|
||||
'light.ceiling_lights')), data=data)
|
||||
|
||||
self.assertEqual(result.status_code, 400)
|
||||
|
||||
def test_entity_not_found(self):
|
||||
"""Test for entity which are not found."""
|
||||
result = requests.get(
|
||||
BRIDGE_URL_BASE.format(
|
||||
'/api/username/lights/{}'.format("not.existant_entity")),
|
||||
timeout=5)
|
||||
|
||||
self.assertEqual(result.status_code, 404)
|
||||
|
||||
result = requests.put(
|
||||
BRIDGE_URL_BASE.format(
|
||||
'/api/username/lights/{}/state'.format("non.existant_entity")),
|
||||
timeout=5)
|
||||
|
||||
self.assertEqual(result.status_code, 404)
|
||||
|
||||
def test_allowed_methods(self):
|
||||
"""Test the allowed methods."""
|
||||
result = requests.get(
|
||||
BRIDGE_URL_BASE.format(
|
||||
'/api/username/lights/{}/state'.format(
|
||||
"light.ceiling_lights")))
|
||||
|
||||
self.assertEqual(result.status_code, 405)
|
||||
|
||||
result = requests.put(
|
||||
BRIDGE_URL_BASE.format(
|
||||
'/api/username/lights/{}'.format("light.ceiling_lights")),
|
||||
data={'key1': 'value1'})
|
||||
|
||||
self.assertEqual(result.status_code, 405)
|
||||
|
||||
result = requests.put(
|
||||
BRIDGE_URL_BASE.format('/api/username/lights'),
|
||||
data={'key1': 'value1'})
|
||||
|
||||
self.assertEqual(result.status_code, 405)
|
||||
|
||||
def test_proper_put_state_request(self):
|
||||
"""Test the request to set the state."""
|
||||
# Test proper on value parsing
|
||||
result = requests.put(
|
||||
BRIDGE_URL_BASE.format(
|
||||
'/api/username/lights/{}/state'.format(
|
||||
'light.ceiling_lights')),
|
||||
data=json.dumps({HUE_API_STATE_ON: 1234}))
|
||||
|
||||
self.assertEqual(result.status_code, 400)
|
||||
|
||||
# Test proper brightness value parsing
|
||||
result = requests.put(
|
||||
BRIDGE_URL_BASE.format(
|
||||
'/api/username/lights/{}/state'.format(
|
||||
'light.ceiling_lights')), data=json.dumps({
|
||||
HUE_API_STATE_ON: True,
|
||||
HUE_API_STATE_BRI: 'Hello world!'
|
||||
}))
|
||||
|
||||
self.assertEqual(result.status_code, 400)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def perform_put_test_on_ceiling_lights(self,
|
||||
content_type='application/json'):
|
||||
"""Test the setting of a light."""
|
||||
# Turn the office light off first
|
||||
self.hass.services.call(
|
||||
light.DOMAIN, const.SERVICE_TURN_OFF,
|
||||
{const.ATTR_ENTITY_ID: 'light.ceiling_lights'},
|
||||
blocking=True)
|
||||
|
||||
ceiling_lights = self.hass.states.get('light.ceiling_lights')
|
||||
self.assertEqual(ceiling_lights.state, STATE_OFF)
|
||||
|
||||
# Go through the API to turn it on
|
||||
office_result = self.perform_put_light_state(
|
||||
'light.ceiling_lights', True, 56, content_type)
|
||||
|
||||
office_result_json = office_result.json()
|
||||
|
||||
self.assertEqual(office_result.status_code, 200)
|
||||
self.assertTrue(
|
||||
'application/json' in office_result.headers['content-type'])
|
||||
|
||||
self.assertEqual(len(office_result_json), 2)
|
||||
|
||||
# Check to make sure the state changed
|
||||
ceiling_lights = self.hass.states.get('light.ceiling_lights')
|
||||
self.assertEqual(ceiling_lights.state, STATE_ON)
|
||||
self.assertEqual(ceiling_lights.attributes[light.ATTR_BRIGHTNESS], 56)
|
||||
|
||||
def perform_get_light_state(self, entity_id, expected_status):
|
||||
"""Test the gettting of a light state."""
|
||||
result = requests.get(
|
||||
BRIDGE_URL_BASE.format(
|
||||
'/api/username/lights/{}'.format(entity_id)), timeout=5)
|
||||
|
||||
self.assertEqual(result.status_code, expected_status)
|
||||
|
||||
if expected_status == 200:
|
||||
self.assertTrue(
|
||||
'application/json' in result.headers['content-type'])
|
||||
|
||||
return result.json()
|
||||
|
||||
return None
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def perform_put_light_state(self, entity_id, is_on, brightness=None,
|
||||
content_type='application/json'):
|
||||
"""Test the setting of a light state."""
|
||||
url = BRIDGE_URL_BASE.format(
|
||||
'/api/username/lights/{}/state'.format(entity_id))
|
||||
|
||||
req_headers = {'Content-Type': content_type}
|
||||
|
||||
data = {HUE_API_STATE_ON: is_on}
|
||||
|
||||
if brightness is not None:
|
||||
data[HUE_API_STATE_BRI] = brightness
|
||||
|
||||
result = requests.put(
|
||||
url, data=json.dumps(data), timeout=5, headers=req_headers)
|
||||
|
||||
return result
|
||||
55
tests/components/emulated_hue/test_init.py
Executable file
55
tests/components/emulated_hue/test_init.py
Executable file
@@ -0,0 +1,55 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.emulated_hue import Config, _LOGGER
|
||||
|
||||
|
||||
def test_config_google_home_entity_id_to_number():
|
||||
"""Test config adheres to the type."""
|
||||
conf = Config({
|
||||
'type': 'google_home'
|
||||
})
|
||||
|
||||
number = conf.entity_id_to_number('light.test')
|
||||
assert number == '1'
|
||||
|
||||
number = conf.entity_id_to_number('light.test')
|
||||
assert number == '1'
|
||||
|
||||
number = conf.entity_id_to_number('light.test2')
|
||||
assert number == '2'
|
||||
|
||||
entity_id = conf.number_to_entity_id('1')
|
||||
assert entity_id == 'light.test'
|
||||
|
||||
|
||||
def test_config_alexa_entity_id_to_number():
|
||||
"""Test config adheres to the type."""
|
||||
conf = Config({
|
||||
'type': 'alexa'
|
||||
})
|
||||
|
||||
number = conf.entity_id_to_number('light.test')
|
||||
assert number == 'light.test'
|
||||
|
||||
number = conf.entity_id_to_number('light.test')
|
||||
assert number == 'light.test'
|
||||
|
||||
number = conf.entity_id_to_number('light.test2')
|
||||
assert number == 'light.test2'
|
||||
|
||||
entity_id = conf.number_to_entity_id('light.test')
|
||||
assert entity_id == 'light.test'
|
||||
|
||||
|
||||
def test_warning_config_google_home_listen_port():
|
||||
"""Test we warn when non-default port is used for Google Home."""
|
||||
with patch.object(_LOGGER, 'warning') as mock_warn:
|
||||
Config({
|
||||
'type': 'google_home',
|
||||
'host_ip': '123.123.123.123',
|
||||
'listen_port': 8300
|
||||
})
|
||||
|
||||
assert mock_warn.called
|
||||
assert mock_warn.mock_calls[0][1][0] == \
|
||||
"When targetting Google Home, listening port has to be port 80"
|
||||
120
tests/components/emulated_hue/test_upnp.py
Normal file
120
tests/components/emulated_hue/test_upnp.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""The tests for the emulated Hue component."""
|
||||
import json
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
import requests
|
||||
|
||||
from homeassistant import bootstrap, const, core
|
||||
import homeassistant.components as core_components
|
||||
from homeassistant.components import emulated_hue, http
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
|
||||
from tests.common import get_test_instance_port, get_test_home_assistant
|
||||
|
||||
HTTP_SERVER_PORT = get_test_instance_port()
|
||||
BRIDGE_SERVER_PORT = get_test_instance_port()
|
||||
|
||||
BRIDGE_URL_BASE = 'http://127.0.0.1:{}'.format(BRIDGE_SERVER_PORT) + '{}'
|
||||
JSON_HEADERS = {const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON}
|
||||
|
||||
|
||||
def setup_hass_instance(emulated_hue_config):
|
||||
"""Set up the Home Assistant instance to test."""
|
||||
hass = get_test_home_assistant()
|
||||
|
||||
# We need to do this to get access to homeassistant/turn_(on,off)
|
||||
run_coroutine_threadsafe(
|
||||
core_components.async_setup(hass, {core.DOMAIN: {}}), hass.loop
|
||||
).result()
|
||||
|
||||
bootstrap.setup_component(
|
||||
hass, http.DOMAIN,
|
||||
{http.DOMAIN: {http.CONF_SERVER_PORT: HTTP_SERVER_PORT}})
|
||||
|
||||
bootstrap.setup_component(hass, emulated_hue.DOMAIN, emulated_hue_config)
|
||||
|
||||
return hass
|
||||
|
||||
|
||||
def start_hass_instance(hass):
|
||||
"""Start the Home Assistant instance to test."""
|
||||
hass.start()
|
||||
|
||||
|
||||
class TestEmulatedHue(unittest.TestCase):
|
||||
"""Test the emulated Hue component."""
|
||||
|
||||
hass = None
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Setup the class."""
|
||||
cls.hass = hass = get_test_home_assistant()
|
||||
|
||||
# We need to do this to get access to homeassistant/turn_(on,off)
|
||||
run_coroutine_threadsafe(
|
||||
core_components.async_setup(hass, {core.DOMAIN: {}}), hass.loop
|
||||
).result()
|
||||
|
||||
bootstrap.setup_component(
|
||||
hass, http.DOMAIN,
|
||||
{http.DOMAIN: {http.CONF_SERVER_PORT: HTTP_SERVER_PORT}})
|
||||
|
||||
with patch('homeassistant.components'
|
||||
'.emulated_hue.UPNPResponderThread'):
|
||||
bootstrap.setup_component(hass, emulated_hue.DOMAIN, {
|
||||
emulated_hue.DOMAIN: {
|
||||
emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT
|
||||
}})
|
||||
|
||||
cls.hass.start()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""Stop the class."""
|
||||
cls.hass.stop()
|
||||
|
||||
def test_description_xml(self):
|
||||
"""Test the description."""
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
result = requests.get(
|
||||
BRIDGE_URL_BASE.format('/description.xml'), timeout=5)
|
||||
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertTrue('text/xml' in result.headers['content-type'])
|
||||
|
||||
# Make sure the XML is parsable
|
||||
# pylint: disable=bare-except
|
||||
try:
|
||||
ET.fromstring(result.text)
|
||||
except:
|
||||
self.fail('description.xml is not valid XML!')
|
||||
|
||||
def test_create_username(self):
|
||||
"""Test the creation of an username."""
|
||||
request_json = {'devicetype': 'my_device'}
|
||||
|
||||
result = requests.post(
|
||||
BRIDGE_URL_BASE.format('/api'), data=json.dumps(request_json),
|
||||
timeout=5)
|
||||
|
||||
self.assertEqual(result.status_code, 200)
|
||||
self.assertTrue('application/json' in result.headers['content-type'])
|
||||
|
||||
resp_json = result.json()
|
||||
success_json = resp_json[0]
|
||||
|
||||
self.assertTrue('success' in success_json)
|
||||
self.assertTrue('username' in success_json['success'])
|
||||
|
||||
def test_valid_username_request(self):
|
||||
"""Test request with a valid username."""
|
||||
request_json = {'invalid_key': 'my_device'}
|
||||
|
||||
result = requests.post(
|
||||
BRIDGE_URL_BASE.format('/api'), data=json.dumps(request_json),
|
||||
timeout=5)
|
||||
|
||||
self.assertEqual(result.status_code, 400)
|
||||
@@ -44,6 +44,10 @@ def setUpModule():
|
||||
|
||||
bootstrap.setup_component(hass, 'api')
|
||||
|
||||
# Registering static path as it caused CORS to blow up
|
||||
hass.http.register_static_path(
|
||||
'/custom_components', hass.config.path('custom_components'))
|
||||
|
||||
hass.start()
|
||||
|
||||
|
||||
@@ -53,11 +57,12 @@ def tearDownModule():
|
||||
hass.stop()
|
||||
|
||||
|
||||
class TestHttp:
|
||||
class TestCors:
|
||||
"""Test HTTP component."""
|
||||
|
||||
def test_cors_allowed_with_password_in_url(self):
|
||||
"""Test cross origin resource sharing with password in url."""
|
||||
|
||||
req = requests.get(_url(const.URL_API),
|
||||
params={'api_password': API_PASSWORD},
|
||||
headers={const.HTTP_HEADER_ORIGIN: HTTP_BASE_URL})
|
||||
|
||||
@@ -9,7 +9,6 @@ from homeassistant.const import (
|
||||
SERVICE_TURN_ON, SERVICE_TURN_OFF)
|
||||
from tests.common import get_test_home_assistant, mock_service
|
||||
|
||||
SERVICE_SYNC = 'sync'
|
||||
SERVICE_SEND_COMMAND = 'send_command'
|
||||
|
||||
|
||||
@@ -83,22 +82,6 @@ class TestDemoRemote(unittest.TestCase):
|
||||
self.assertEqual(SERVICE_TURN_OFF, call.service)
|
||||
self.assertEqual('entity_id_val', call.data[ATTR_ENTITY_ID])
|
||||
|
||||
# Test sync
|
||||
sync_calls = mock_service(
|
||||
self.hass, remote.DOMAIN, SERVICE_SYNC)
|
||||
|
||||
remote.sync(
|
||||
self.hass, entity_id='entity_id_val')
|
||||
|
||||
self.hass.block_till_done()
|
||||
|
||||
self.assertEqual(1, len(sync_calls))
|
||||
call = sync_calls[-1]
|
||||
|
||||
self.assertEqual(remote.DOMAIN, call.domain)
|
||||
self.assertEqual(SERVICE_SYNC, call.service)
|
||||
self.assertEqual('entity_id_val', call.data[ATTR_ENTITY_ID])
|
||||
|
||||
# Test send_command
|
||||
send_command_calls = mock_service(
|
||||
self.hass, remote.DOMAIN, SERVICE_SEND_COMMAND)
|
||||
|
||||
@@ -11,7 +11,6 @@ import homeassistant.components.remote as remote
|
||||
|
||||
from tests.common import mock_service, get_test_home_assistant
|
||||
TEST_PLATFORM = {remote.DOMAIN: {CONF_PLATFORM: 'test'}}
|
||||
SERVICE_SYNC = 'sync'
|
||||
SERVICE_SEND_COMMAND = 'send_command'
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user