Merge pull request #14199 from home-assistant/rc

0.68.1
pull/14238/merge 0.68.1
Paulus Schoutsen 2018-04-30 14:04:29 -04:00 committed by GitHub
commit d1460de89b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 178 additions and 95 deletions

View File

@ -1,6 +1,7 @@
"""Provide configuration end points for Automations."""
import asyncio
from collections import OrderedDict
import uuid
from homeassistant.const import CONF_ID
from homeassistant.components.config import EditIdBasedConfigView
@ -29,7 +30,12 @@ class EditAutomationConfigView(EditIdBasedConfigView):
"""Set value."""
index = None
for index, cur_value in enumerate(data):
if cur_value[CONF_ID] == config_key:
# When people copy paste their automations to the config file,
# they sometimes forget to add IDs. Fix it here.
if CONF_ID not in cur_value:
cur_value[CONF_ID] = uuid.uuid4().hex
elif cur_value[CONF_ID] == config_key:
break
else:
cur_value = OrderedDict()

View File

@ -102,18 +102,23 @@ class _GoogleEntity:
if state.state == STATE_UNAVAILABLE:
return None
entity_config = self.config.entity_config.get(state.entity_id, {})
name = (entity_config.get(CONF_NAME) or state.name).strip()
# If an empty string
if not name:
return None
traits = self.traits()
# Found no supported traits for this entity
if not traits:
return None
entity_config = self.config.entity_config.get(state.entity_id, {})
device = {
'id': state.entity_id,
'name': {
'name': entity_config.get(CONF_NAME) or state.name
'name': name
},
'attributes': {},
'traits': [trait.name for trait in traits],

View File

@ -3,6 +3,7 @@
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/homekit/
"""
import ipaddress
import logging
from zlib import adler32
@ -12,8 +13,8 @@ from homeassistant.components.cover import (
SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION)
from homeassistant.const import (
ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
ATTR_DEVICE_CLASS, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
ATTR_DEVICE_CLASS, CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS,
TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import FILTER_SCHEMA
from homeassistant.util import get_local_ip
@ -35,6 +36,8 @@ REQUIREMENTS = ['HAP-python==1.1.9']
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All({
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_IP_ADDRESS):
vol.All(ipaddress.ip_address, cv.string),
vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean,
vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA,
vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config,
@ -48,11 +51,12 @@ async def async_setup(hass, config):
conf = config[DOMAIN]
port = conf[CONF_PORT]
ip_address = conf.get(CONF_IP_ADDRESS)
auto_start = conf[CONF_AUTO_START]
entity_filter = conf[CONF_FILTER]
entity_config = conf[CONF_ENTITY_CONFIG]
homekit = HomeKit(hass, port, entity_filter, entity_config)
homekit = HomeKit(hass, port, ip_address, entity_filter, entity_config)
homekit.setup()
if auto_start:
@ -151,10 +155,11 @@ def generate_aid(entity_id):
class HomeKit():
"""Class to handle all actions between HomeKit and Home Assistant."""
def __init__(self, hass, port, entity_filter, entity_config):
def __init__(self, hass, port, ip_address, entity_filter, entity_config):
"""Initialize a HomeKit object."""
self.hass = hass
self._port = port
self._ip_address = ip_address
self._filter = entity_filter
self._config = entity_config
self.started = False
@ -169,9 +174,10 @@ class HomeKit():
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, self.stop)
ip_addr = self._ip_address or get_local_ip()
path = self.hass.config.path(HOMEKIT_FILE)
self.bridge = HomeBridge(self.hass)
self.driver = HomeDriver(self.bridge, self._port, get_local_ip(), path)
self.driver = HomeDriver(self.bridge, self._port, ip_addr, path)
def add_bridge_accessory(self, state):
"""Try adding accessory to bridge if configured beforehand."""

View File

@ -31,7 +31,7 @@ KNOWN_DEVICES = "{}-devices".format(DOMAIN)
_LOGGER = logging.getLogger(__name__)
def homekit_http_send(self, message_body=None):
def homekit_http_send(self, message_body=None, encode_chunked=False):
r"""Send the currently buffered request and clear the buffer.
Appends an extra \r\n to the buffer.

View File

@ -242,26 +242,13 @@ class HueLight(Light):
@property
def hs_color(self):
"""Return the hs color value."""
# pylint: disable=redefined-outer-name
mode = self._color_mode
if mode not in ('hs', 'xy'):
return
source = self.light.action if self.is_group else self.light.state
hue = source.get('hue')
sat = source.get('sat')
if mode in ('xy', 'hs'):
return color.color_xy_to_hs(*source['xy'])
# Sometimes the state will not include valid hue/sat values.
# Reported as issue 13434
if hue is not None and sat is not None:
return hue / 65535 * 360, sat / 255 * 100
if 'xy' not in source:
return None
return color.color_xy_to_hs(*source['xy'])
return None
@property
def color_temp(self):

View File

@ -11,8 +11,8 @@ import voluptuous as vol
from homeassistant.const import (CONF_HOST, CONF_NAME)
from homeassistant.components.light import (
Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_HS_COLOR,
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, PLATFORM_SCHEMA)
Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS,
SUPPORT_COLOR_TEMP, SUPPORT_COLOR, PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv
from homeassistant.util.color import \
color_temperature_mired_to_kelvin as mired_to_kelvin
@ -90,15 +90,15 @@ class TPLinkSmartBulb(Light):
if ATTR_COLOR_TEMP in kwargs:
self.smartbulb.color_temp = \
mired_to_kelvin(kwargs[ATTR_COLOR_TEMP])
if ATTR_KELVIN in kwargs:
self.smartbulb.color_temp = kwargs[ATTR_KELVIN]
if ATTR_BRIGHTNESS in kwargs:
brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255)
self.smartbulb.brightness = brightness_to_percentage(brightness)
brightness = brightness_to_percentage(
kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255))
if ATTR_HS_COLOR in kwargs:
hue, sat = kwargs.get(ATTR_HS_COLOR)
hsv = (hue, sat, 100)
hsv = (int(hue), int(sat), brightness)
self.smartbulb.hsv = hsv
elif ATTR_BRIGHTNESS in kwargs:
self.smartbulb.brightness = brightness
def turn_off(self, **kwargs):
"""Turn the light off."""

View File

@ -306,13 +306,18 @@ class CastDevice(MediaPlayerDevice):
_LOGGER.debug("Discovered chromecast with same UUID: %s", discover)
self.hass.async_add_job(self.async_set_cast_info(discover))
async def async_stop(event):
"""Disconnect socket on Home Assistant stop."""
await self._async_disconnect()
async_dispatcher_connect(self.hass, SIGNAL_CAST_DISCOVERED,
async_cast_discovered)
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop)
self.hass.async_add_job(self.async_set_cast_info(self._cast_info))
async def async_will_remove_from_hass(self) -> None:
"""Disconnect Chromecast object when removed."""
self._async_disconnect()
await self._async_disconnect()
if self._cast_info.uuid is not None:
# Remove the entity from the added casts so that it can dynamically
# be re-added again.
@ -328,7 +333,7 @@ class CastDevice(MediaPlayerDevice):
if old_cast_info.host_port == cast_info.host_port:
# Nothing connection-related updated
return
self._async_disconnect()
await self._async_disconnect()
# Failed connection will unfortunately never raise an exception, it
# will instead just try connecting indefinitely.
@ -348,22 +353,27 @@ class CastDevice(MediaPlayerDevice):
_LOGGER.debug("Connection successful!")
self.async_schedule_update_ha_state()
@callback
def _async_disconnect(self):
async def _async_disconnect(self):
"""Disconnect Chromecast object if it is set."""
if self._chromecast is None:
# Can't disconnect if not connected.
return
_LOGGER.debug("Disconnecting from previous chromecast socket.")
_LOGGER.debug("Disconnecting from chromecast socket.")
self._available = False
self._chromecast.disconnect(blocking=False)
self.async_schedule_update_ha_state()
await self.hass.async_add_job(self._chromecast.disconnect)
# Invalidate some attributes
self._chromecast = None
self.cast_status = None
self.media_status = None
self.media_status_received = None
self._status_listener.invalidate()
self._status_listener = None
if self._status_listener is not None:
self._status_listener.invalidate()
self._status_listener = None
self.async_schedule_update_ha_state()
# ========== Callbacks ==========
def new_cast_status(self, cast_status):

View File

@ -14,7 +14,8 @@ from homeassistant.const import (CONF_ACCESS_TOKEN, CONF_NAME, STATE_UNKNOWN)
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['eliqonline==1.0.13']
# pylint: disable=import-error, no-member
REQUIREMENTS = [] # ['eliqonline==1.0.13'] - package disappeared
_LOGGER = logging.getLogger(__name__)

View File

@ -2,7 +2,7 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
MINOR_VERSION = 68
PATCH_VERSION = '0'
PATCH_VERSION = '1'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 5, 3)

View File

@ -276,9 +276,6 @@ dsmr_parser==0.11
# homeassistant.components.sensor.dweet
dweepy==0.3.0
# homeassistant.components.sensor.eliqonline
eliqonline==1.0.13
# homeassistant.components.enocean
enocean==0.40

View File

@ -42,13 +42,13 @@ async def test_update_device_config(hass, aiohttp_client):
client = await aiohttp_client(hass.http.app)
orig_data = [
{
'id': 'sun',
},
{
'id': 'moon',
}
]
{
'id': 'sun',
},
{
'id': 'moon',
}
]
def mock_read(path):
"""Mock reading data."""
@ -81,3 +81,56 @@ async def test_update_device_config(hass, aiohttp_client):
'action': [],
}
assert written[0] == orig_data
async def test_bad_formatted_automations(hass, aiohttp_client):
"""Test that we handle automations without ID."""
with patch.object(config, 'SECTIONS', ['automation']):
await async_setup_component(hass, 'config', {})
client = await aiohttp_client(hass.http.app)
orig_data = [
{
# No ID
'action': {
'event': 'hello'
}
},
{
'id': 'moon',
}
]
def mock_read(path):
"""Mock reading data."""
return orig_data
written = []
def mock_write(path, data):
"""Mock writing data."""
written.append(data)
with patch('homeassistant.components.config._read', mock_read), \
patch('homeassistant.components.config._write', mock_write):
resp = await client.post(
'/api/config/automation/config/moon', data=json.dumps({
'trigger': [],
'action': [],
'condition': [],
}))
assert resp.status == 200
result = await resp.json()
assert result == {'result': 'ok'}
# Verify ID added to orig_data
assert 'id' in orig_data[0]
assert orig_data[1] == {
'id': 'moon',
'trigger': [],
'condition': [],
'action': [],
}

View File

@ -286,3 +286,29 @@ async def test_unavailable_state_doesnt_sync(hass):
'devices': []
}
}
async def test_empty_name_doesnt_sync(hass):
"""Test that an entity with empty name does not sync over."""
light = DemoLight(
None, ' ',
state=False,
)
light.hass = hass
light.entity_id = 'light.demo_light'
await light.async_update_ha_state()
result = await sh.async_handle_message(hass, BASIC_CONFIG, {
"requestId": REQ_ID,
"inputs": [{
"intent": "action.devices.SYNC"
}]
})
assert result == {
'requestId': REQ_ID,
'payload': {
'agentUserId': 'test-agent',
'devices': []
}
}

View File

@ -11,7 +11,8 @@ from homeassistant.components.homekit.const import (
DEFAULT_PORT, SERVICE_HOMEKIT_START)
from homeassistant.helpers.entityfilter import generate_filter
from homeassistant.const import (
CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
CONF_IP_ADDRESS, CONF_PORT,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
from tests.common import get_test_home_assistant
from tests.components.homekit.test_accessories import patch_debounce
@ -59,7 +60,7 @@ class TestHomeKit(unittest.TestCase):
self.hass, DOMAIN, {DOMAIN: {}}))
self.assertEqual(mock_homekit.mock_calls, [
call(self.hass, DEFAULT_PORT, ANY, {}),
call(self.hass, DEFAULT_PORT, None, ANY, {}),
call().setup()])
# Test auto start enabled
@ -74,7 +75,8 @@ class TestHomeKit(unittest.TestCase):
"""Test async_setup with auto start disabled and test service calls."""
mock_homekit.return_value = homekit = Mock()
config = {DOMAIN: {CONF_AUTO_START: False, CONF_PORT: 11111}}
config = {DOMAIN: {CONF_AUTO_START: False, CONF_PORT: 11111,
CONF_IP_ADDRESS: '172.0.0.0'}}
self.assertTrue(setup.setup_component(
self.hass, DOMAIN, config))
@ -82,7 +84,7 @@ class TestHomeKit(unittest.TestCase):
self.hass.block_till_done()
self.assertEqual(mock_homekit.mock_calls, [
call(self.hass, 11111, ANY, {}),
call(self.hass, 11111, '172.0.0.0', ANY, {}),
call().setup()])
# Test start call with driver stopped.
@ -101,7 +103,7 @@ class TestHomeKit(unittest.TestCase):
def test_homekit_setup(self):
"""Test setup of bridge and driver."""
homekit = HomeKit(self.hass, DEFAULT_PORT, {}, {})
homekit = HomeKit(self.hass, DEFAULT_PORT, None, {}, {})
with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver, \
patch('homeassistant.util.get_local_ip') as mock_ip:
@ -117,9 +119,17 @@ class TestHomeKit(unittest.TestCase):
self.assertEqual(
self.hass.bus.listeners.get(EVENT_HOMEASSISTANT_STOP), 1)
def test_homekit_setup_ip_address(self):
"""Test setup with given IP address."""
homekit = HomeKit(self.hass, DEFAULT_PORT, '172.0.0.0', {}, {})
with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver:
homekit.setup()
mock_driver.assert_called_with(ANY, DEFAULT_PORT, '172.0.0.0', ANY)
def test_homekit_add_accessory(self):
"""Add accessory if config exists and get_acc returns an accessory."""
homekit = HomeKit(self.hass, None, lambda entity_id: True, {})
homekit = HomeKit(self.hass, None, None, lambda entity_id: True, {})
homekit.bridge = HomeBridge(self.hass)
with patch(PATH_HOMEKIT + '.accessories.HomeBridge.add_accessory') \
@ -142,7 +152,7 @@ class TestHomeKit(unittest.TestCase):
def test_homekit_entity_filter(self):
"""Test the entity filter."""
entity_filter = generate_filter(['cover'], ['demo.test'], [], [])
homekit = HomeKit(self.hass, None, entity_filter, {})
homekit = HomeKit(self.hass, None, None, entity_filter, {})
with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc:
mock_get_acc.return_value = None
@ -162,7 +172,7 @@ class TestHomeKit(unittest.TestCase):
@patch(PATH_HOMEKIT + '.HomeKit.add_bridge_accessory')
def test_homekit_start(self, mock_add_bridge_acc, mock_show_setup_msg):
"""Test HomeKit start method."""
homekit = HomeKit(self.hass, None, {}, {'cover.demo': {}})
homekit = HomeKit(self.hass, None, None, {}, {'cover.demo': {}})
homekit.bridge = HomeBridge(self.hass)
homekit.driver = Mock()
@ -184,7 +194,7 @@ class TestHomeKit(unittest.TestCase):
def test_homekit_stop(self):
"""Test HomeKit stop method."""
homekit = HomeKit(None, None, None, None)
homekit = HomeKit(None, None, None, None, None)
homekit.driver = Mock()
# Test if started = False

View File

@ -237,7 +237,7 @@ async def test_lights(hass, mock_bridge):
assert lamp_1 is not None
assert lamp_1.state == 'on'
assert lamp_1.attributes['brightness'] == 144
assert lamp_1.attributes['hs_color'] == (71.896, 83.137)
assert lamp_1.attributes['hs_color'] == (36.067, 69.804)
lamp_2 = hass.states.get('light.hue_lamp_2')
assert lamp_2 is not None
@ -253,7 +253,7 @@ async def test_lights_color_mode(hass, mock_bridge):
assert lamp_1 is not None
assert lamp_1.state == 'on'
assert lamp_1.attributes['brightness'] == 144
assert lamp_1.attributes['hs_color'] == (71.896, 83.137)
assert lamp_1.attributes['hs_color'] == (36.067, 69.804)
assert 'color_temp' not in lamp_1.attributes
new_light1_on = LIGHT_1_ON.copy()
@ -650,37 +650,11 @@ def test_hs_color():
assert light.hs_color is None
light = hue_light.HueLight(
light=Mock(state={
'colormode': 'hs',
'hue': 1234,
'sat': 123,
}),
request_bridge_update=None,
bridge=Mock(),
is_group=False,
)
assert light.hs_color == (1234 / 65535 * 360, 123 / 255 * 100)
light = hue_light.HueLight(
light=Mock(state={
'colormode': 'xy',
'hue': 1234,
'sat': 123,
}),
request_bridge_update=None,
bridge=Mock(),
is_group=False,
)
assert light.hs_color == (1234 / 65535 * 360, 123 / 255 * 100)
light = hue_light.HueLight(
light=Mock(state={
'colormode': 'xy',
'hue': None,
'sat': 123,
'xy': [0.4, 0.5]
}),
request_bridge_update=None,

View File

@ -346,8 +346,16 @@ async def test_switched_host(hass: HomeAssistantType):
async_dispatcher_send(hass, cast.SIGNAL_CAST_DISCOVERED, changed)
await hass.async_block_till_done()
assert get_chromecast.call_count == 1
chromecast.disconnect.assert_called_once_with(blocking=False)
assert chromecast.disconnect.call_count == 1
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
chromecast.disconnect.assert_called_once_with(blocking=False)
async def test_disconnect_on_stop(hass: HomeAssistantType):
"""Test cast device disconnects socket on stop."""
info = get_fake_chromecast_info()
with patch('pychromecast.dial.get_device_status', return_value=info):
chromecast, _ = await async_setup_media_player_cast(hass, info)
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
assert chromecast.disconnect.call_count == 1