diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 3c8e9f5d21c..58888b19af7 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -245,7 +245,7 @@ def async_api_turn_on(hass, config, request, entity): yield from hass.services.async_call(domain, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity.entity_id - }, blocking=True) + }, blocking=False) return api_message(request) @@ -261,7 +261,7 @@ def async_api_turn_off(hass, config, request, entity): yield from hass.services.async_call(domain, SERVICE_TURN_OFF, { ATTR_ENTITY_ID: entity.entity_id - }, blocking=True) + }, blocking=False) return api_message(request) @@ -276,7 +276,7 @@ def async_api_set_brightness(hass, config, request, entity): yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity.entity_id, light.ATTR_BRIGHTNESS_PCT: brightness, - }, blocking=True) + }, blocking=False) return api_message(request) @@ -300,7 +300,7 @@ def async_api_adjust_brightness(hass, config, request, entity): yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity.entity_id, light.ATTR_BRIGHTNESS_PCT: brightness, - }, blocking=True) + }, blocking=False) return api_message(request) @@ -321,14 +321,14 @@ def async_api_set_color(hass, config, request, entity): yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity.entity_id, light.ATTR_RGB_COLOR: rgb, - }, blocking=True) + }, blocking=False) else: xyz = color_util.color_RGB_to_xy(*rgb) yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity.entity_id, light.ATTR_XY_COLOR: (xyz[0], xyz[1]), light.ATTR_BRIGHTNESS: xyz[2], - }, blocking=True) + }, blocking=False) return api_message(request) @@ -343,7 +343,7 @@ def async_api_set_color_temperature(hass, config, request, entity): yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity.entity_id, light.ATTR_KELVIN: kelvin, - }, blocking=True) + }, blocking=False) return api_message(request) @@ -361,7 +361,7 @@ def async_api_decrease_color_temp(hass, config, request, entity): yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity.entity_id, light.ATTR_COLOR_TEMP: value, - }, blocking=True) + }, blocking=False) return api_message(request) @@ -379,7 +379,7 @@ def async_api_increase_color_temp(hass, config, request, entity): yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity.entity_id, light.ATTR_COLOR_TEMP: value, - }, blocking=True) + }, blocking=False) return api_message(request) @@ -391,7 +391,7 @@ def async_api_activate(hass, config, request, entity): """Process a activate request.""" yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity.entity_id - }, blocking=True) + }, blocking=False) return api_message(request) @@ -421,8 +421,8 @@ def async_api_set_percentage(hass, config, request, entity): service = SERVICE_SET_COVER_POSITION data[cover.ATTR_POSITION] = percentage - yield from hass.services.async_call(entity.domain, service, - data, blocking=True) + yield from hass.services.async_call( + entity.domain, service, data, blocking=False) return api_message(request) @@ -469,8 +469,8 @@ def async_api_adjust_percentage(hass, config, request, entity): data[cover.ATTR_POSITION] = max(0, percentage_delta + current) - yield from hass.services.async_call(entity.domain, service, - data, blocking=True) + yield from hass.services.async_call( + entity.domain, service, data, blocking=False) return api_message(request) @@ -482,7 +482,7 @@ def async_api_lock(hass, config, request, entity): """Process a lock request.""" yield from hass.services.async_call(entity.domain, SERVICE_LOCK, { ATTR_ENTITY_ID: entity.entity_id - }, blocking=True) + }, blocking=False) return api_message(request) @@ -495,7 +495,7 @@ def async_api_unlock(hass, config, request, entity): """Process a unlock request.""" yield from hass.services.async_call(entity.domain, SERVICE_UNLOCK, { ATTR_ENTITY_ID: entity.entity_id - }, blocking=True) + }, blocking=False) return api_message(request) @@ -512,8 +512,9 @@ def async_api_set_volume(hass, config, request, entity): media_player.ATTR_MEDIA_VOLUME_LEVEL: volume, } - yield from hass.services.async_call(entity.domain, SERVICE_VOLUME_SET, - data, blocking=True) + yield from hass.services.async_call( + entity.domain, SERVICE_VOLUME_SET, + data, blocking=False) return api_message(request) @@ -540,9 +541,9 @@ def async_api_adjust_volume(hass, config, request, entity): media_player.ATTR_MEDIA_VOLUME_LEVEL: volume, } - yield from hass.services.async_call(entity.domain, - media_player.SERVICE_VOLUME_SET, - data, blocking=True) + yield from hass.services.async_call( + entity.domain, media_player.SERVICE_VOLUME_SET, + data, blocking=False) return api_message(request) @@ -559,9 +560,9 @@ def async_api_set_mute(hass, config, request, entity): media_player.ATTR_MEDIA_VOLUME_MUTED: mute, } - yield from hass.services.async_call(entity.domain, - media_player.SERVICE_VOLUME_MUTE, - data, blocking=True) + yield from hass.services.async_call( + entity.domain, media_player.SERVICE_VOLUME_MUTE, + data, blocking=False) return api_message(request) @@ -575,8 +576,9 @@ def async_api_play(hass, config, request, entity): ATTR_ENTITY_ID: entity.entity_id } - yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_PLAY, - data, blocking=True) + yield from hass.services.async_call( + entity.domain, SERVICE_MEDIA_PLAY, + data, blocking=False) return api_message(request) @@ -590,8 +592,9 @@ def async_api_pause(hass, config, request, entity): ATTR_ENTITY_ID: entity.entity_id } - yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_PAUSE, - data, blocking=True) + yield from hass.services.async_call( + entity.domain, SERVICE_MEDIA_PAUSE, + data, blocking=False) return api_message(request) @@ -605,8 +608,9 @@ def async_api_stop(hass, config, request, entity): ATTR_ENTITY_ID: entity.entity_id } - yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_STOP, - data, blocking=True) + yield from hass.services.async_call( + entity.domain, SERVICE_MEDIA_STOP, + data, blocking=False) return api_message(request) @@ -620,9 +624,9 @@ def async_api_next(hass, config, request, entity): ATTR_ENTITY_ID: entity.entity_id } - yield from hass.services.async_call(entity.domain, - SERVICE_MEDIA_NEXT_TRACK, - data, blocking=True) + yield from hass.services.async_call( + entity.domain, SERVICE_MEDIA_NEXT_TRACK, + data, blocking=False) return api_message(request) @@ -636,8 +640,8 @@ def async_api_previous(hass, config, request, entity): ATTR_ENTITY_ID: entity.entity_id } - yield from hass.services.async_call(entity.domain, - SERVICE_MEDIA_PREVIOUS_TRACK, - data, blocking=True) + yield from hass.services.async_call( + entity.domain, SERVICE_MEDIA_PREVIOUS_TRACK, + data, blocking=False) return api_message(request) diff --git a/homeassistant/components/binary_sensor/concord232.py b/homeassistant/components/binary_sensor/concord232.py index 7ba88f76611..73cf77f2b93 100755 --- a/homeassistant/components/binary_sensor/concord232.py +++ b/homeassistant/components/binary_sensor/concord232.py @@ -62,6 +62,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error("Unable to connect to Concord232: %s", str(ex)) return False + # The order of zones returned by client.list_zones() can vary. + # When the zones are not named, this can result in the same entity + # name mapping to different sensors in an unpredictable way. Sort + # the zones by zone number to prevent this. + + client.zones.sort(key=lambda zone: zone['number']) + for zone in client.zones: _LOGGER.info("Loading Zone found: %s", zone['name']) if zone['number'] not in exclude: @@ -118,7 +125,7 @@ class Concord232ZoneSensor(BinarySensorDevice): def is_on(self): """Return true if the binary sensor is on.""" # True means "faulted" or "open" or "abnormal state" - return bool(self._zone['state'] == 'Normal') + return bool(self._zone['state'] != 'Normal') def update(self): """Get updated stats from API.""" diff --git a/homeassistant/components/binary_sensor/isy994.py b/homeassistant/components/binary_sensor/isy994.py index a5b61c9ffed..e3b5d7b4369 100644 --- a/homeassistant/components/binary_sensor/isy994.py +++ b/homeassistant/components/binary_sensor/isy994.py @@ -62,10 +62,9 @@ def setup_platform(hass, config: ConfigType, node.nid, node.parent_nid) else: device_type = _detect_device_type(node) - if device_type in ['moisture', 'opening']: - subnode_id = int(node.nid[-1]) - # Leak and door/window sensors work the same way with negative - # nodes and heartbeat nodes + subnode_id = int(node.nid[-1]) + if device_type == 'opening': + # Door/window sensors use an optional "negative" node if subnode_id == 4: # Subnode 4 is the heartbeat node, which we will represent # as a separate binary_sensor @@ -74,6 +73,14 @@ def setup_platform(hass, config: ConfigType, devices.append(device) elif subnode_id == 2: parent_device.add_negative_node(node) + elif device_type == 'moisture': + # Moisure nodes have a subnode 2, but we ignore it because it's + # just the inverse of the primary node. + if subnode_id == 4: + # Heartbeat node + device = ISYBinarySensorHeartbeat(node, parent_device) + parent_device.add_heartbeat_device(device) + devices.append(device) else: # We don't yet have any special logic for other sensor types, # so add the nodes as individual devices @@ -165,7 +172,8 @@ class ISYBinarySensorDevice(isy.ISYDevice, BinarySensorDevice): """ self._negative_node = child - if not _is_val_unknown(self._negative_node): + # pylint: disable=protected-access + if not _is_val_unknown(self._negative_node.status._val): # If the negative node has a value, it means the negative node is # in use for this device. Therefore, we cannot determine the state # of the sensor until we receive our first ON event. diff --git a/homeassistant/components/calendar/caldav.py b/homeassistant/components/calendar/caldav.py index 1647b9522b8..36894dcab61 100644 --- a/homeassistant/components/calendar/caldav.py +++ b/homeassistant/components/calendar/caldav.py @@ -25,11 +25,11 @@ CONF_DEVICE_ID = 'device_id' CONF_CALENDARS = 'calendars' CONF_CUSTOM_CALENDARS = 'custom_calendars' CONF_CALENDAR = 'calendar' -CONF_ALL_DAY = 'all_day' CONF_SEARCH = 'search' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_URL): vol.Url, + # pylint: disable=no-value-for-parameter + vol.Required(CONF_URL): vol.Url(), vol.Optional(CONF_CALENDARS, default=[]): vol.All(cv.ensure_list, vol.Schema([ cv.string @@ -88,7 +88,7 @@ def setup_platform(hass, config, add_devices, disc_info=None): WebDavCalendarEventDevice(hass, device_data, calendar, - cust_calendar.get(CONF_ALL_DAY), + True, cust_calendar.get(CONF_SEARCH)) ) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index cd206135dde..2bbb4dbe405 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -582,9 +582,10 @@ def _is_latest(js_option, request): from user_agents import parse useragent = parse(request.headers.get('User-Agent')) - # on iOS every browser is a Safari which we support from version 10. + # on iOS every browser is a Safari which we support from version 11. if useragent.os.family == 'iOS': - return useragent.os.version[0] >= 10 + # Was >= 10, temp setting it to 12 to work around issue #11387 + return useragent.os.version[0] >= 12 family_min_version = { 'Chrome': 50, # Probably can reduce this diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index a11c8c0f22c..46f25e4e05f 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -169,6 +169,8 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, vol.Optional(CONF_RESOLVENAMES, default=DEFAULT_RESOLVENAMES): vol.In(CONF_RESOLVENAMES_OPTIONS), + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, vol.Optional(CONF_CALLBACK_IP): cv.string, vol.Optional(CONF_CALLBACK_PORT): cv.port, }}, @@ -747,10 +749,6 @@ class HMDevice(Entity): """Return device specific state attributes.""" attr = {} - # No data available - if not self.available: - return attr - # Generate a dictionary with attributes for node, data in HM_ATTRIBUTE_SUPPORT.items(): # Is an attribute and exists for this object @@ -806,6 +804,9 @@ class HMDevice(Entity): if attribute == 'UNREACH': self._available = bool(value) has_changed = True + elif not self.available: + self._available = False + has_changed = True # If it has changed data point, update HASS if has_changed: diff --git a/homeassistant/components/hue.py b/homeassistant/components/hue.py index 3dad4429b53..6147f706658 100644 --- a/homeassistant/components/hue.py +++ b/homeassistant/components/hue.py @@ -160,6 +160,8 @@ class HueBridge(object): self.allow_hue_groups = allow_hue_groups self.bridge = None + self.lights = {} + self.lightgroups = {} self.configured = False self.config_request_id = None diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index a454143bcd2..f5c910ea116 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -31,10 +31,6 @@ DEPENDENCIES = ['hue'] _LOGGER = logging.getLogger(__name__) -DATA_KEY = 'hue_lights' -DATA_LIGHTS = 'lights' -DATA_LIGHTGROUPS = 'lightgroups' - MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) @@ -93,8 +89,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info is None or 'bridge_id' not in discovery_info: return - setup_data(hass) - if config is not None and len(config) > 0: # Legacy configuration, will be removed in 0.60 config_str = yaml.dump([config]) @@ -110,12 +104,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): unthrottled_update_lights(hass, bridge, add_devices) -def setup_data(hass): - """Initialize internal data. Useful from tests.""" - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = {DATA_LIGHTS: {}, DATA_LIGHTGROUPS: {}} - - @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) def update_lights(hass, bridge, add_devices): """Update the Hue light objects with latest info from the bridge.""" @@ -176,18 +164,17 @@ def process_lights(hass, api, bridge, bridge_type, update_lights_cb): new_lights = [] - lights = hass.data[DATA_KEY][DATA_LIGHTS] for light_id, info in api_lights.items(): - if light_id not in lights: - lights[light_id] = HueLight( + if light_id not in bridge.lights: + bridge.lights[light_id] = HueLight( int(light_id), info, bridge, update_lights_cb, bridge_type, bridge.allow_unreachable, bridge.allow_in_emulated_hue) - new_lights.append(lights[light_id]) + new_lights.append(bridge.lights[light_id]) else: - lights[light_id].info = info - lights[light_id].schedule_update_ha_state() + bridge.lights[light_id].info = info + bridge.lights[light_id].schedule_update_ha_state() return new_lights @@ -202,23 +189,22 @@ def process_groups(hass, api, bridge, bridge_type, update_lights_cb): new_lights = [] - groups = hass.data[DATA_KEY][DATA_LIGHTGROUPS] for lightgroup_id, info in api_groups.items(): if 'state' not in info: _LOGGER.warning('Group info does not contain state. ' 'Please update your hub.') return [] - if lightgroup_id not in groups: - groups[lightgroup_id] = HueLight( + if lightgroup_id not in bridge.lightgroups: + bridge.lightgroups[lightgroup_id] = HueLight( int(lightgroup_id), info, bridge, update_lights_cb, bridge_type, bridge.allow_unreachable, bridge.allow_in_emulated_hue, True) - new_lights.append(groups[lightgroup_id]) + new_lights.append(bridge.lightgroups[lightgroup_id]) else: - groups[lightgroup_id].info = info - groups[lightgroup_id].schedule_update_ha_state() + bridge.lightgroups[lightgroup_id].info = info + bridge.lightgroups[lightgroup_id].schedule_update_ha_state() return new_lights diff --git a/homeassistant/components/lock/sesame.py b/homeassistant/components/lock/sesame.py index 02b049618d2..5bc40435486 100644 --- a/homeassistant/components/lock/sesame.py +++ b/homeassistant/components/lock/sesame.py @@ -25,46 +25,53 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument -def setup_platform(hass, config: ConfigType, - add_devices: Callable[[list], None], discovery_info=None): +def setup_platform( + hass, config: ConfigType, + add_devices: Callable[[list], None], discovery_info=None): """Set up the Sesame platform.""" import pysesame email = config.get(CONF_EMAIL) password = config.get(CONF_PASSWORD) - add_devices([SesameDevice(sesame) for - sesame in pysesame.get_sesames(email, password)]) + add_devices([SesameDevice(sesame) for sesame in + pysesame.get_sesames(email, password)], + update_before_add=True) class SesameDevice(LockDevice): """Representation of a Sesame device.""" - _sesame = None - def __init__(self, sesame: object) -> None: """Initialize the Sesame device.""" self._sesame = sesame + # Cached properties from pysesame object. + self._device_id = None + self._nickname = None + self._is_unlocked = False + self._api_enabled = False + self._battery = -1 + @property def name(self) -> str: """Return the name of the device.""" - return self._sesame.nickname + return self._nickname @property def available(self) -> bool: """Return True if entity is available.""" - return self._sesame.api_enabled + return self._api_enabled @property def is_locked(self) -> bool: """Return True if the device is currently locked, else False.""" - return not self._sesame.is_unlocked + return not self._is_unlocked @property def state(self) -> str: """Get the state of the device.""" - if self._sesame.is_unlocked: + if self._is_unlocked: return STATE_UNLOCKED return STATE_LOCKED @@ -79,11 +86,16 @@ class SesameDevice(LockDevice): def update(self) -> None: """Update the internal state of the device.""" self._sesame.update_state() + self._nickname = self._sesame.nickname + self._api_enabled = self._sesame.api_enabled + self._is_unlocked = self._sesame.is_unlocked + self._device_id = self._sesame.device_id + self._battery = self._sesame.battery @property def device_state_attributes(self) -> dict: """Return the state attributes.""" attributes = {} - attributes[ATTR_DEVICE_ID] = self._sesame.device_id - attributes[ATTR_BATTERY_LEVEL] = self._sesame.battery + attributes[ATTR_DEVICE_ID] = self._device_id + attributes[ATTR_BATTERY_LEVEL] = self._battery return attributes diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index 0abdb90e67a..9d3e0b90fa4 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -322,17 +322,17 @@ class LgWebOSDevice(MediaPlayerDevice): def select_source(self, source): """Select input source.""" - source = self._source_list.get(source) - if source is None: + source_dict = self._source_list.get(source) + if source_dict is None: _LOGGER.warning("Source %s not found for %s", source, self.name) return - self._current_source_id = self._source_list[source]['id'] - if source.get('title'): - self._current_source = self._source_list[source]['title'] - self._client.launch_app(self._source_list[source]['id']) - elif source.get('label'): - self._current_source = self._source_list[source]['label'] - self._client.set_input(self._source_list[source]['id']) + self._current_source_id = source_dict['id'] + if source_dict.get('title'): + self._current_source = source_dict['title'] + self._client.launch_app(source_dict['id']) + elif source_dict.get('label'): + self._current_source = source_dict['label'] + self._client.set_input(source_dict['id']) def media_play(self): """Send play command.""" diff --git a/homeassistant/components/switch/modbus.py b/homeassistant/components/switch/modbus.py index c731b336dfb..211ff54d5a4 100644 --- a/homeassistant/components/switch/modbus.py +++ b/homeassistant/components/switch/modbus.py @@ -141,10 +141,17 @@ class ModbusRegisterSwitch(ModbusCoilSwitch): self._verify_register = ( verify_register if verify_register else self._register) self._register_type = register_type - self._state_on = ( - state_on if state_on else self._command_on) - self._state_off = ( - state_off if state_off else self._command_off) + + if state_on is not None: + self._state_on = state_on + else: + self._state_on = self._command_on + + if state_off is not None: + self._state_off = state_off + else: + self._state_off = self._command_off + self._is_on = None def turn_on(self, **kwargs): diff --git a/homeassistant/const.py b/homeassistant/const.py index dd15e1fb75d..b4fc8061d87 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 60 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 55a412af1fd..a0876dea5df 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -346,6 +346,7 @@ def test_exclude_filters(hass): )) msg = yield from smart_home.async_handle_message(hass, config, request) + yield from hass.async_block_till_done() msg = msg['event'] @@ -378,6 +379,7 @@ def test_include_filters(hass): )) msg = yield from smart_home.async_handle_message(hass, config, request) + yield from hass.async_block_till_done() msg = msg['event'] @@ -393,6 +395,7 @@ def test_api_entity_not_exists(hass): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -442,6 +445,7 @@ def test_api_turn_on(hass, domain): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -475,6 +479,7 @@ def test_api_turn_off(hass, domain): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -501,6 +506,7 @@ def test_api_set_brightness(hass): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -532,6 +538,7 @@ def test_api_adjust_brightness(hass, result, adjust): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -566,6 +573,7 @@ def test_api_set_color_rgb(hass): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -600,6 +608,7 @@ def test_api_set_color_xy(hass): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -629,6 +638,7 @@ def test_api_set_color_temperature(hass): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -658,6 +668,7 @@ def test_api_decrease_color_temp(hass, result, initial): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -687,6 +698,7 @@ def test_api_increase_color_temp(hass, result, initial): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -714,6 +726,7 @@ def test_api_activate(hass, domain): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -740,6 +753,7 @@ def test_api_set_percentage_fan(hass): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -769,6 +783,7 @@ def test_api_set_percentage_cover(hass): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -800,6 +815,7 @@ def test_api_adjust_percentage_fan(hass, result, adjust): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -832,6 +848,7 @@ def test_api_adjust_percentage_cover(hass, result, adjust): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -859,6 +876,7 @@ def test_api_lock(hass, domain): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -885,6 +903,7 @@ def test_api_play(hass, domain): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -911,6 +930,7 @@ def test_api_pause(hass, domain): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -937,6 +957,7 @@ def test_api_stop(hass, domain): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -963,6 +984,7 @@ def test_api_next(hass, domain): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -989,6 +1011,7 @@ def test_api_previous(hass, domain): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -1017,6 +1040,7 @@ def test_api_set_volume(hass): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -1048,6 +1072,7 @@ def test_api_adjust_volume(hass, result, adjust): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] @@ -1077,6 +1102,7 @@ def test_api_mute(hass, domain): msg = yield from smart_home.async_handle_message( hass, DEFAULT_CONFIG, request) + yield from hass.async_block_till_done() assert 'event' in msg msg = msg['event'] diff --git a/tests/components/calendar/test_caldav.py b/tests/components/calendar/test_caldav.py index 8a44f96fe87..7234d40c410 100644 --- a/tests/components/calendar/test_caldav.py +++ b/tests/components/calendar/test_caldav.py @@ -121,8 +121,10 @@ class TestComponentsWebDavCalendar(unittest.TestCase): assert len(devices) == 2 assert devices[0].name == "First" assert devices[0].dev_id == "First" + self.assertFalse(devices[0].data.include_all_day) assert devices[1].name == "Second" assert devices[1].dev_id == "Second" + self.assertFalse(devices[1].data.include_all_day) caldav.setup_platform(self.hass, { @@ -167,6 +169,7 @@ class TestComponentsWebDavCalendar(unittest.TestCase): assert len(devices) == 1 assert devices[0].name == "HomeOffice" assert devices[0].dev_id == "Second HomeOffice" + self.assertTrue(devices[0].data.include_all_day) caldav.setup_platform(self.hass, { diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 5e5bd4f6c7f..7955cecba04 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -36,27 +36,45 @@ class TestSetup(unittest.TestCase): self.mock_lights = [] self.mock_groups = [] self.mock_add_devices = MagicMock() - hue_light.setup_data(self.hass) def setup_mocks_for_process_lights(self): """Set up all mocks for process_lights tests.""" - self.mock_bridge = MagicMock() + self.mock_bridge = self.create_mock_bridge('host') self.mock_api = MagicMock() self.mock_api.get.return_value = {} self.mock_bridge.get_api.return_value = self.mock_api self.mock_bridge_type = MagicMock() - hue_light.setup_data(self.hass) def setup_mocks_for_process_groups(self): """Set up all mocks for process_groups tests.""" - self.mock_bridge = MagicMock() + self.mock_bridge = self.create_mock_bridge('host') self.mock_bridge.get_group.return_value = { 'name': 'Group 0', 'state': {'any_on': True}} + self.mock_api = MagicMock() self.mock_api.get.return_value = {} self.mock_bridge.get_api.return_value = self.mock_api + self.mock_bridge_type = MagicMock() - hue_light.setup_data(self.hass) + + def create_mock_bridge(self, host, allow_hue_groups=True): + """Return a mock HueBridge with reasonable defaults.""" + mock_bridge = MagicMock() + mock_bridge.host = host + mock_bridge.allow_hue_groups = allow_hue_groups + mock_bridge.lights = {} + mock_bridge.lightgroups = {} + return mock_bridge + + def create_mock_lights(self, lights): + """Return a dict suitable for mocking api.get('lights').""" + mock_bridge_lights = lights + + for light_id, info in mock_bridge_lights.items(): + if 'state' not in info: + info['state'] = {'on': False} + + return mock_bridge_lights def test_setup_platform_no_discovery_info(self): """Test setup_platform without discovery info.""" @@ -211,6 +229,70 @@ class TestSetup(unittest.TestCase): self.mock_add_devices.assert_called_once_with( self.mock_lights) + @MockDependency('phue') + def test_update_lights_with_two_bridges(self, mock_phue): + """Test the update_lights function with two bridges.""" + self.setup_mocks_for_update_lights() + + mock_bridge_one = self.create_mock_bridge('one', False) + mock_bridge_one_lights = self.create_mock_lights( + {1: {'name': 'b1l1'}, 2: {'name': 'b1l2'}}) + + mock_bridge_two = self.create_mock_bridge('two', False) + mock_bridge_two_lights = self.create_mock_lights( + {1: {'name': 'b2l1'}, 3: {'name': 'b2l3'}}) + + with patch('homeassistant.components.light.hue.get_bridge_type', + return_value=self.mock_bridge_type): + with patch('homeassistant.components.light.hue.HueLight.' + 'schedule_update_ha_state'): + mock_api = MagicMock() + mock_api.get.return_value = mock_bridge_one_lights + with patch.object(mock_bridge_one, 'get_api', + return_value=mock_api): + hue_light.unthrottled_update_lights( + self.hass, mock_bridge_one, self.mock_add_devices) + + mock_api = MagicMock() + mock_api.get.return_value = mock_bridge_two_lights + with patch.object(mock_bridge_two, 'get_api', + return_value=mock_api): + hue_light.unthrottled_update_lights( + self.hass, mock_bridge_two, self.mock_add_devices) + + self.assertEquals(sorted(mock_bridge_one.lights.keys()), [1, 2]) + self.assertEquals(sorted(mock_bridge_two.lights.keys()), [1, 3]) + + self.assertEquals(len(self.mock_add_devices.mock_calls), 2) + + # first call + name, args, kwargs = self.mock_add_devices.mock_calls[0] + self.assertEquals(len(args), 1) + self.assertEquals(len(kwargs), 0) + + # one argument, a list of lights in bridge one; each of them is an + # object of type HueLight so we can't straight up compare them + lights = args[0] + self.assertEquals( + lights[0].unique_id, + '{}.b1l1.Light.1'.format(hue_light.HueLight)) + self.assertEquals( + lights[1].unique_id, + '{}.b1l2.Light.2'.format(hue_light.HueLight)) + + # second call works the same + name, args, kwargs = self.mock_add_devices.mock_calls[1] + self.assertEquals(len(args), 1) + self.assertEquals(len(kwargs), 0) + + lights = args[0] + self.assertEquals( + lights[0].unique_id, + '{}.b2l1.Light.1'.format(hue_light.HueLight)) + self.assertEquals( + lights[1].unique_id, + '{}.b2l3.Light.3'.format(hue_light.HueLight)) + def test_process_lights_api_error(self): """Test the process_lights function when the bridge errors out.""" self.setup_mocks_for_process_lights() @@ -221,9 +303,7 @@ class TestSetup(unittest.TestCase): None) self.assertEquals([], ret) - self.assertEquals( - {}, - self.hass.data[hue_light.DATA_KEY][hue_light.DATA_LIGHTS]) + self.assertEquals(self.mock_bridge.lights, {}) def test_process_lights_no_lights(self): """Test the process_lights function when bridge returns no lights.""" @@ -234,9 +314,7 @@ class TestSetup(unittest.TestCase): None) self.assertEquals([], ret) - self.assertEquals( - {}, - self.hass.data[hue_light.DATA_KEY][hue_light.DATA_LIGHTS]) + self.assertEquals(self.mock_bridge.lights, {}) @patch('homeassistant.components.light.hue.HueLight') def test_process_lights_some_lights(self, mock_hue_light): @@ -260,9 +338,7 @@ class TestSetup(unittest.TestCase): self.mock_bridge_type, self.mock_bridge.allow_unreachable, self.mock_bridge.allow_in_emulated_hue), ]) - self.assertEquals( - len(self.hass.data[hue_light.DATA_KEY][hue_light.DATA_LIGHTS]), - 2) + self.assertEquals(len(self.mock_bridge.lights), 2) @patch('homeassistant.components.light.hue.HueLight') def test_process_lights_new_light(self, mock_hue_light): @@ -274,8 +350,7 @@ class TestSetup(unittest.TestCase): self.setup_mocks_for_process_lights() self.mock_api.get.return_value = { 1: {'state': 'on'}, 2: {'state': 'off'}} - self.hass.data[ - hue_light.DATA_KEY][hue_light.DATA_LIGHTS][1] = MagicMock() + self.mock_bridge.lights = {1: MagicMock()} ret = hue_light.process_lights( self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type, @@ -288,11 +363,9 @@ class TestSetup(unittest.TestCase): self.mock_bridge_type, self.mock_bridge.allow_unreachable, self.mock_bridge.allow_in_emulated_hue), ]) - self.hass.data[hue_light.DATA_KEY][hue_light.DATA_LIGHTS][ - 1].schedule_update_ha_state.assert_called_once_with() - self.assertEquals( - len(self.hass.data[hue_light.DATA_KEY][hue_light.DATA_LIGHTS]), - 2) + self.assertEquals(len(self.mock_bridge.lights), 2) + self.mock_bridge.lights[1]\ + .schedule_update_ha_state.assert_called_once_with() def test_process_groups_api_error(self): """Test the process_groups function when the bridge errors out.""" @@ -304,9 +377,7 @@ class TestSetup(unittest.TestCase): None) self.assertEquals([], ret) - self.assertEquals( - {}, - self.hass.data[hue_light.DATA_KEY][hue_light.DATA_LIGHTGROUPS]) + self.assertEquals(self.mock_bridge.lightgroups, {}) def test_process_groups_no_state(self): """Test the process_groups function when bridge returns no status.""" @@ -318,9 +389,7 @@ class TestSetup(unittest.TestCase): None) self.assertEquals([], ret) - self.assertEquals( - {}, - self.hass.data[hue_light.DATA_KEY][hue_light.DATA_LIGHTGROUPS]) + self.assertEquals(self.mock_bridge.lightgroups, {}) @patch('homeassistant.components.light.hue.HueLight') def test_process_groups_some_groups(self, mock_hue_light): @@ -344,10 +413,7 @@ class TestSetup(unittest.TestCase): self.mock_bridge_type, self.mock_bridge.allow_unreachable, self.mock_bridge.allow_in_emulated_hue, True), ]) - self.assertEquals( - len(self.hass.data[ - hue_light.DATA_KEY][hue_light.DATA_LIGHTGROUPS]), - 2) + self.assertEquals(len(self.mock_bridge.lightgroups), 2) @patch('homeassistant.components.light.hue.HueLight') def test_process_groups_new_group(self, mock_hue_light): @@ -359,8 +425,7 @@ class TestSetup(unittest.TestCase): self.setup_mocks_for_process_groups() self.mock_api.get.return_value = { 1: {'state': 'on'}, 2: {'state': 'off'}} - self.hass.data[ - hue_light.DATA_KEY][hue_light.DATA_LIGHTGROUPS][1] = MagicMock() + self.mock_bridge.lightgroups = {1: MagicMock()} ret = hue_light.process_groups( self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type, @@ -373,12 +438,9 @@ class TestSetup(unittest.TestCase): self.mock_bridge_type, self.mock_bridge.allow_unreachable, self.mock_bridge.allow_in_emulated_hue, True), ]) - self.hass.data[hue_light.DATA_KEY][hue_light.DATA_LIGHTGROUPS][ - 1].schedule_update_ha_state.assert_called_once_with() - self.assertEquals( - len(self.hass.data[ - hue_light.DATA_KEY][hue_light.DATA_LIGHTGROUPS]), - 2) + self.assertEquals(len(self.mock_bridge.lightgroups), 2) + self.mock_bridge.lightgroups[1]\ + .schedule_update_ha_state.assert_called_once_with() class TestHueLight(unittest.TestCase): diff --git a/tests/components/media_player/test_webostv.py b/tests/components/media_player/test_webostv.py new file mode 100644 index 00000000000..8017ad6cd54 --- /dev/null +++ b/tests/components/media_player/test_webostv.py @@ -0,0 +1,60 @@ +"""The tests for the LG webOS media player platform.""" +import unittest +from unittest import mock + +from homeassistant.components.media_player import webostv + + +class FakeLgWebOSDevice(webostv.LgWebOSDevice): + """A fake device without the client setup required for the real one.""" + + def __init__(self, *args, **kwargs): + """Initialise parameters needed for tests with fake values.""" + self._source_list = {} + self._client = mock.MagicMock() + self._name = 'fake_device' + self._current_source = None + + +class TestLgWebOSDevice(unittest.TestCase): + """Test the LgWebOSDevice class.""" + + def setUp(self): + """Configure a fake device for each test.""" + self.device = FakeLgWebOSDevice() + + def test_select_source_with_empty_source_list(self): + """Ensure we don't call client methods when we don't have sources.""" + self.device.select_source('nonexistent') + assert 0 == self.device._client.launch_app.call_count + assert 0 == self.device._client.set_input.call_count + + def test_select_source_with_titled_entry(self): + """Test that a titled source is treated as an app.""" + self.device._source_list = { + 'existent': { + 'id': 'existent_id', + 'title': 'existent_title', + }, + } + + self.device.select_source('existent') + + assert 'existent_title' == self.device._current_source + assert [mock.call('existent_id')] == ( + self.device._client.launch_app.call_args_list) + + def test_select_source_with_labelled_entry(self): + """Test that a labelled source is treated as an input source.""" + self.device._source_list = { + 'existent': { + 'id': 'existent_id', + 'label': 'existent_label', + }, + } + + self.device.select_source('existent') + + assert 'existent_label' == self.device._current_source + assert [mock.call('existent_id')] == ( + self.device._client.set_input.call_args_list)