Compare commits

..

8 Commits

Author SHA1 Message Date
Paulus Schoutsen
c49751542f Version bump to 0.68.0b1 2018-04-24 23:19:33 -04:00
Mark Coombes
2e3a27e418 Update device classes for contact sensor HomeKit (#14051) 2018-04-24 23:19:16 -04:00
Matthew Garrett
7566bb5aed Handle HomeKit configuration failure more cleanly (#14041)
* Handle HomeKit configuration failure more cleanly

Add support for handling cases where HomeKit configuration fails, and give
the user more information about what to do.

* Don't consume the exception for a homekit.UnknownError

If we get an UnknownError then we should alert the user but also still
generate the backtrace so there's actually something for them to file in
a bug report.
2018-04-24 23:19:16 -04:00
Otto Winter
fc1f6ee0f0 Revert cast platform polling mode (#14027) 2018-04-24 23:19:16 -04:00
Matt Schmitt
cb839eff0f HomeKit Alarm Control Panel Code Exception Fix (#14025)
* Catch exception for KeyError
* Use get and added test
2018-04-24 23:19:15 -04:00
Paulus Schoutsen
2bc87bfcf0 Order the output of the automation editor (#14019)
* Order the output of the automation editor

* Lint
2018-04-24 23:19:15 -04:00
Johann Kellerman
44be80145b Qwikswitch binary sensors (#14008) 2018-04-24 23:19:15 -04:00
Paulus Schoutsen
8cb1e17ad8 Bump frontend to 20180425.0 2018-04-24 23:18:46 -04:00
17 changed files with 337 additions and 189 deletions

View File

@@ -0,0 +1,70 @@
"""
Support for Qwikswitch Binary Sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.qwikswitch/
"""
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.qwikswitch import QSEntity, DOMAIN as QWIKSWITCH
from homeassistant.core import callback
DEPENDENCIES = [QWIKSWITCH]
_LOGGER = logging.getLogger(__name__)
async def async_setup_platform(hass, _, add_devices, discovery_info=None):
"""Add binary sensor from the main Qwikswitch component."""
if discovery_info is None:
return
qsusb = hass.data[QWIKSWITCH]
_LOGGER.debug("Setup qwikswitch.binary_sensor %s, %s",
qsusb, discovery_info)
devs = [QSBinarySensor(sensor) for sensor in discovery_info[QWIKSWITCH]]
add_devices(devs)
class QSBinarySensor(QSEntity, BinarySensorDevice):
"""Sensor based on a Qwikswitch relay/dimmer module."""
_val = False
def __init__(self, sensor):
"""Initialize the sensor."""
from pyqwikswitch import SENSORS
super().__init__(sensor['id'], sensor['name'])
self.channel = sensor['channel']
sensor_type = sensor['type']
self._decode, _ = SENSORS[sensor_type]
self._invert = not sensor.get('invert', False)
self._class = sensor.get('class', 'door')
@callback
def update_packet(self, packet):
"""Receive update packet from QSUSB."""
val = self._decode(packet, channel=self.channel)
_LOGGER.debug("Update %s (%s:%s) decoded as %s: %s",
self.entity_id, self.qsid, self.channel, val, packet)
if val is not None:
self._val = bool(val)
self.async_schedule_update_ha_state()
@property
def is_on(self):
"""Check if device is on (non-zero)."""
return self._val == self._invert
@property
def unique_id(self):
"""Return a unique identifier for this sensor."""
return "qs{}:{}".format(self.qsid, self.channel)
@property
def device_class(self):
"""Return the class of this sensor."""
return self._class

View File

@@ -1,6 +1,8 @@
"""Provide configuration end points for Automations."""
import asyncio
from collections import OrderedDict
from homeassistant.const import CONF_ID
from homeassistant.components.config import EditIdBasedConfigView
from homeassistant.components.automation import (
PLATFORM_SCHEMA, DOMAIN, async_reload)
@@ -13,8 +15,38 @@ CONFIG_PATH = 'automations.yaml'
@asyncio.coroutine
def async_setup(hass):
"""Set up the Automation config API."""
hass.http.register_view(EditIdBasedConfigView(
hass.http.register_view(EditAutomationConfigView(
DOMAIN, 'config', CONFIG_PATH, cv.string,
PLATFORM_SCHEMA, post_write_hook=async_reload
))
return True
class EditAutomationConfigView(EditIdBasedConfigView):
"""Edit automation config."""
def _write_value(self, hass, data, config_key, new_value):
"""Set value."""
index = None
for index, cur_value in enumerate(data):
if cur_value[CONF_ID] == config_key:
break
else:
cur_value = OrderedDict()
cur_value[CONF_ID] = config_key
index = len(data)
data.append(cur_value)
# Iterate through some keys that we want to have ordered in the output
updated_value = OrderedDict()
for key in ('id', 'alias', 'trigger', 'condition', 'action'):
if key in cur_value:
updated_value[key] = cur_value[key]
if key in new_value:
updated_value[key] = new_value[key]
# We cover all current fields above, but just in case we start
# supporting more fields in the future.
updated_value.update(cur_value)
updated_value.update(new_value)
data[index] = updated_value

View File

@@ -24,7 +24,7 @@ from homeassistant.core import callback
from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass
REQUIREMENTS = ['home-assistant-frontend==20180420.0']
REQUIREMENTS = ['home-assistant-frontend==20180425.0']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']

View File

@@ -102,6 +102,8 @@ PROP_CELSIUS = {'minValue': -273, 'maxValue': 999}
# #### Device Class ####
DEVICE_CLASS_CO2 = 'co2'
DEVICE_CLASS_DOOR = 'door'
DEVICE_CLASS_GARAGE_DOOR = 'garage_door'
DEVICE_CLASS_GAS = 'gas'
DEVICE_CLASS_HUMIDITY = 'humidity'
DEVICE_CLASS_LIGHT = 'light'
@@ -112,3 +114,4 @@ DEVICE_CLASS_OPENING = 'opening'
DEVICE_CLASS_PM25 = 'pm25'
DEVICE_CLASS_SMOKE = 'smoke'
DEVICE_CLASS_TEMPERATURE = 'temperature'
DEVICE_CLASS_WINDOW = 'window'

View File

@@ -30,7 +30,7 @@ class SecuritySystem(HomeAccessory):
def __init__(self, *args, config):
"""Initialize a SecuritySystem accessory object."""
super().__init__(*args, category=CATEGORY_ALARM_SYSTEM)
self._alarm_code = config[ATTR_CODE]
self._alarm_code = config.get(ATTR_CODE)
self.flag_target_state = False
serv_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM)

View File

@@ -20,6 +20,7 @@ from .const import (
DEVICE_CLASS_MOTION, SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED,
DEVICE_CLASS_OCCUPANCY, SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED,
DEVICE_CLASS_OPENING, SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE,
DEVICE_CLASS_DOOR, DEVICE_CLASS_GARAGE_DOOR, DEVICE_CLASS_WINDOW,
DEVICE_CLASS_SMOKE, SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED)
from .util import (
convert_to_float, temperature_to_homekit, density_to_air_quality)
@@ -29,13 +30,16 @@ _LOGGER = logging.getLogger(__name__)
BINARY_SENSOR_SERVICE_MAP = {
DEVICE_CLASS_CO2: (SERV_CARBON_DIOXIDE_SENSOR,
CHAR_CARBON_DIOXIDE_DETECTED),
DEVICE_CLASS_DOOR: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE),
DEVICE_CLASS_GARAGE_DOOR: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE),
DEVICE_CLASS_GAS: (SERV_CARBON_MONOXIDE_SENSOR,
CHAR_CARBON_MONOXIDE_DETECTED),
DEVICE_CLASS_MOISTURE: (SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED),
DEVICE_CLASS_MOTION: (SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED),
DEVICE_CLASS_OCCUPANCY: (SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED),
DEVICE_CLASS_OPENING: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE),
DEVICE_CLASS_SMOKE: (SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED)}
DEVICE_CLASS_SMOKE: (SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED),
DEVICE_CLASS_WINDOW: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE)}
@TYPES.register('TemperatureSensor')

View File

@@ -14,7 +14,7 @@ from homeassistant.components.discovery import SERVICE_HOMEKIT
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['homekit==0.5']
REQUIREMENTS = ['homekit==0.6']
DOMAIN = 'homekit_controller'
HOMEKIT_DIR = '.homekit'
@@ -133,10 +133,31 @@ class HKDevice():
import homekit
pairing_id = str(uuid.uuid4())
code = callback_data.get('code').strip()
self.pairing_data = homekit.perform_pair_setup(
self.conn, code, pairing_id)
try:
self.pairing_data = homekit.perform_pair_setup(self.conn, code,
pairing_id)
except homekit.exception.UnavailableError:
error_msg = "This accessory is already paired to another device. \
Please reset the accessory and try again."
_configurator = self.hass.data[DOMAIN+self.hkid]
self.configurator.notify_errors(_configurator, error_msg)
return
except homekit.exception.AuthenticationError:
error_msg = "Incorrect HomeKit code for {}. Please check it and \
try again.".format(self.model)
_configurator = self.hass.data[DOMAIN+self.hkid]
self.configurator.notify_errors(_configurator, error_msg)
return
except homekit.exception.UnknownError:
error_msg = "Received an unknown error. Please file a bug."
_configurator = self.hass.data[DOMAIN+self.hkid]
self.configurator.notify_errors(_configurator, error_msg)
raise
if self.pairing_data is not None:
homekit.save_pairing(self.pairing_file, self.pairing_data)
_configurator = self.hass.data[DOMAIN+self.hkid]
self.configurator.request_done(_configurator)
self.accessory_setup()
else:
error_msg = "Unable to pair, please try again"

View File

@@ -288,8 +288,7 @@ class CastDevice(MediaPlayerDevice):
self._chromecast = None # type: Optional[pychromecast.Chromecast]
self.cast_status = None
self.media_status = None
self.media_status_position = None
self.media_status_position_received = None
self.media_status_received = None
self._available = False # type: bool
self._status_listener = None # type: Optional[CastStatusListener]
@@ -362,26 +361,10 @@ class CastDevice(MediaPlayerDevice):
self._chromecast = None
self.cast_status = None
self.media_status = None
self.media_status_position = None
self.media_status_position_received = None
self.media_status_received = None
self._status_listener.invalidate()
self._status_listener = None
def update(self):
"""Periodically update the properties.
Even though we receive callbacks for most state changes, some 3rd party
apps don't always send them. Better poll every now and then if the
chromecast is active (i.e. an app is running).
"""
if not self._available:
# Not connected or not available.
return
if self._chromecast.media_controller.is_active:
# We can only update status if the media namespace is active
self._chromecast.media_controller.update_status()
# ========== Callbacks ==========
def new_cast_status(self, cast_status):
"""Handle updates of the cast status."""
@@ -390,36 +373,8 @@ class CastDevice(MediaPlayerDevice):
def new_media_status(self, media_status):
"""Handle updates of the media status."""
# Only use media position for playing/paused,
# and for normal playback rate
if (media_status is None or
abs(media_status.playback_rate - 1) > 0.01 or
not (media_status.player_is_playing or
media_status.player_is_paused)):
self.media_status_position = None
self.media_status_position_received = None
else:
# Avoid unnecessary state attribute updates if player_state and
# calculated position stay the same
now = dt_util.utcnow()
do_update = \
(self.media_status is None or
self.media_status_position is None or
self.media_status.player_state != media_status.player_state)
if not do_update:
if media_status.player_is_playing:
elapsed = now - self.media_status_position_received
do_update = abs(media_status.current_time -
(self.media_status_position +
elapsed.total_seconds())) > 1
else:
do_update = \
self.media_status_position != media_status.current_time
if do_update:
self.media_status_position = media_status.current_time
self.media_status_position_received = now
self.media_status = media_status
self.media_status_received = dt_util.utcnow()
self.schedule_update_ha_state()
def new_connection_status(self, connection_status):
@@ -496,8 +451,8 @@ class CastDevice(MediaPlayerDevice):
# ========== Properties ==========
@property
def should_poll(self):
"""Polling needed for cast integration, see async_update."""
return True
"""No polling needed."""
return False
@property
def name(self):
@@ -625,7 +580,12 @@ class CastDevice(MediaPlayerDevice):
@property
def media_position(self):
"""Position of current playing media in seconds."""
return self.media_status_position
if self.media_status is None or \
not (self.media_status.player_is_playing or
self.media_status.player_is_paused or
self.media_status.player_is_idle):
return None
return self.media_status.current_time
@property
def media_position_updated_at(self):
@@ -633,7 +593,7 @@ class CastDevice(MediaPlayerDevice):
Returns value from homeassistant.util.dt.utcnow().
"""
return self.media_status_position_received
return self.media_status_received
@property
def unique_id(self) -> Optional[str]:

View File

@@ -8,17 +8,18 @@ import logging
import voluptuous as vol
from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA
from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_URL,
CONF_SENSORS, CONF_SWITCHES)
CONF_SENSORS, CONF_SWITCHES, CONF_URL, EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP)
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform
from homeassistant.helpers.entity import Entity
from homeassistant.components.light import ATTR_BRIGHTNESS
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyqwikswitch==0.71']
REQUIREMENTS = ['pyqwikswitch==0.8']
_LOGGER = logging.getLogger(__name__)
@@ -28,6 +29,7 @@ CONF_DIMMER_ADJUST = 'dimmer_adjust'
CONF_BUTTON_EVENTS = 'button_events'
CV_DIM_VALUE = vol.All(vol.Coerce(float), vol.Range(min=1, max=3))
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_URL, default='http://127.0.0.1:2020'):
@@ -40,6 +42,8 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional('channel', default=1): int,
vol.Required('name'): str,
vol.Required('type'): str,
vol.Optional('class'): DEVICE_CLASSES_SCHEMA,
vol.Optional('invert'): bool
})]),
vol.Optional(CONF_SWITCHES, default=[]): vol.All(
cv.ensure_list, [str])
@@ -115,7 +119,7 @@ class QSToggleEntity(QSEntity):
async def async_setup(hass, config):
"""Qwiskswitch component setup."""
from pyqwikswitch.async_ import QSUsb
from pyqwikswitch import CMD_BUTTONS, QS_CMD, QS_ID, QSType
from pyqwikswitch import CMD_BUTTONS, QS_CMD, QS_ID, QSType, SENSORS
# Add cmd's to in /&listen packets will fire events
# By default only buttons of type [TOGGLE,SCENE EXE,LEVEL]
@@ -143,22 +147,39 @@ async def async_setup(hass, config):
hass.data[DOMAIN] = qsusb
_new = {'switch': [], 'light': [], 'sensor': sensors}
comps = {'switch': [], 'light': [], 'sensor': [], 'binary_sensor': []}
try:
for sens in sensors:
_, _type = SENSORS[sens['type']]
if _type is bool:
comps['binary_sensor'].append(sens)
continue
comps['sensor'].append(sens)
for _key in ('invert', 'class'):
if _key in sens:
_LOGGER.warning(
"%s should only be used for binary_sensors: %s",
_key, sens)
except KeyError:
_LOGGER.warning("Sensor validation failed")
for qsid, dev in qsusb.devices.items():
if qsid in switches:
if dev.qstype != QSType.relay:
_LOGGER.warning(
"You specified a switch that is not a relay %s", qsid)
continue
_new['switch'].append(qsid)
comps['switch'].append(qsid)
elif dev.qstype in (QSType.relay, QSType.dimmer):
_new['light'].append(qsid)
comps['light'].append(qsid)
else:
_LOGGER.warning("Ignored unknown QSUSB device: %s", dev)
continue
# Load platforms
for comp_name, comp_conf in _new.items():
for comp_name, comp_conf in comps.items():
if comp_conf:
load_platform(hass, comp_name, DOMAIN, {DOMAIN: comp_conf}, config)
@@ -190,9 +211,8 @@ async def async_setup(hass, config):
@callback
def async_stop(_):
"""Stop the listener queue and clean up."""
"""Stop the listener."""
hass.data[DOMAIN].stop()
_LOGGER.info("Waiting for long poll to QSUSB to time out (max 30sec)")
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, async_stop)

View File

@@ -36,18 +36,18 @@ class QSSensor(QSEntity):
super().__init__(sensor['id'], sensor['name'])
self.channel = sensor['channel']
self.sensor_type = sensor['type']
sensor_type = sensor['type']
self._decode, self.unit = SENSORS[self.sensor_type]
self._decode, self.unit = SENSORS[sensor_type]
if isinstance(self.unit, type):
self.unit = "{}:{}".format(self.sensor_type, self.channel)
self.unit = "{}:{}".format(sensor_type, self.channel)
@callback
def update_packet(self, packet):
"""Receive update packet from QSUSB."""
val = self._decode(packet.get('data'), channel=self.channel)
_LOGGER.debug("Update %s (%s) decoded as %s: %s: %s",
self.entity_id, self.qsid, val, self.channel, packet)
val = self._decode(packet, channel=self.channel)
_LOGGER.debug("Update %s (%s:%s) decoded as %s: %s",
self.entity_id, self.qsid, self.channel, val, packet)
if val is not None:
self._val = val
self.async_schedule_update_ha_state()

View File

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

View File

@@ -386,10 +386,10 @@ hipnotify==1.0.8
holidays==0.9.4
# homeassistant.components.frontend
home-assistant-frontend==20180420.0
home-assistant-frontend==20180425.0
# homeassistant.components.homekit_controller
# homekit==0.5
# homekit==0.6
# homeassistant.components.homematicip_cloud
homematicip==0.8
@@ -898,7 +898,7 @@ pyowm==2.8.0
pypollencom==1.1.2
# homeassistant.components.qwikswitch
pyqwikswitch==0.71
pyqwikswitch==0.8
# homeassistant.components.rainbird
pyrainbird==0.1.3

View File

@@ -81,7 +81,7 @@ hbmqtt==0.9.1
holidays==0.9.4
# homeassistant.components.frontend
home-assistant-frontend==20180420.0
home-assistant-frontend==20180425.0
# homeassistant.components.influxdb
# homeassistant.components.sensor.influxdb
@@ -149,7 +149,7 @@ pymonoprice==0.3
pynx584==0.4
# homeassistant.components.qwikswitch
pyqwikswitch==0.71
pyqwikswitch==0.8
# homeassistant.components.sensor.darksky
# homeassistant.components.weather.darksky

View File

@@ -0,0 +1,83 @@
"""Test Automation config panel."""
import json
from unittest.mock import patch
from homeassistant.bootstrap import async_setup_component
from homeassistant.components import config
async def test_get_device_config(hass, aiohttp_client):
"""Test getting device config."""
with patch.object(config, 'SECTIONS', ['automation']):
await async_setup_component(hass, 'config', {})
client = await aiohttp_client(hass.http.app)
def mock_read(path):
"""Mock reading data."""
return [
{
'id': 'sun',
},
{
'id': 'moon',
}
]
with patch('homeassistant.components.config._read', mock_read):
resp = await client.get(
'/api/config/automation/config/moon')
assert resp.status == 200
result = await resp.json()
assert result == {'id': 'moon'}
async def test_update_device_config(hass, aiohttp_client):
"""Test updating device config."""
with patch.object(config, 'SECTIONS', ['automation']):
await async_setup_component(hass, 'config', {})
client = await aiohttp_client(hass.http.app)
orig_data = [
{
'id': 'sun',
},
{
'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'}
assert list(orig_data[1]) == ['id', 'trigger', 'condition', 'action']
assert orig_data[1] == {
'id': 'moon',
'trigger': [],
'condition': [],
'action': [],
}
assert written[0] == orig_data

View File

@@ -109,8 +109,16 @@ class TestHomekitSecuritySystems(unittest.TestCase):
acc = SecuritySystem(self.hass, 'SecuritySystem', acp,
2, config={ATTR_CODE: None})
acc.run()
# Set from HomeKit
acc.char_target_state.client_update_value(0)
self.hass.block_till_done()
self.assertEqual(
self.events[0].data[ATTR_SERVICE], 'alarm_arm_home')
self.assertNotIn(ATTR_CODE, self.events[0].data[ATTR_SERVICE_DATA])
self.assertEqual(acc.char_target_state.value, 0)
acc = SecuritySystem(self.hass, 'SecuritySystem', acp,
2, config={})
# Set from HomeKit
acc.char_target_state.client_update_value(0)
self.hass.block_till_done()

View File

@@ -1,7 +1,6 @@
"""The tests for the Cast Media player platform."""
# pylint: disable=protected-access
import asyncio
import datetime as dt
from typing import Optional
from unittest.mock import patch, MagicMock, Mock
from uuid import UUID
@@ -15,8 +14,7 @@ from homeassistant.components.media_player.cast import ChromecastInfo
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.dispatcher import async_dispatcher_connect, \
async_dispatcher_send
from homeassistant.components.media_player import cast, \
ATTR_MEDIA_POSITION, ATTR_MEDIA_POSITION_UPDATED_AT
from homeassistant.components.media_player import cast
from homeassistant.setup import async_setup_component
@@ -288,8 +286,6 @@ async def test_entity_media_states(hass: HomeAssistantType):
assert entity.unique_id == full_info.uuid
media_status = MagicMock(images=None)
media_status.current_time = 0
media_status.playback_rate = 1
media_status.player_is_playing = True
entity.new_media_status(media_status)
await hass.async_block_till_done()
@@ -324,85 +320,6 @@ async def test_entity_media_states(hass: HomeAssistantType):
assert state.state == 'unknown'
async def test_entity_media_position(hass: HomeAssistantType):
"""Test various entity media states."""
info = get_fake_chromecast_info()
full_info = attr.evolve(info, model_name='google home',
friendly_name='Speaker', uuid=FakeUUID)
with patch('pychromecast.dial.get_device_status',
return_value=full_info):
chromecast, entity = await async_setup_media_player_cast(hass, info)
media_status = MagicMock(images=None)
media_status.current_time = 10
media_status.playback_rate = 1
media_status.player_is_playing = True
media_status.player_is_paused = False
media_status.player_is_idle = False
now = dt.datetime.now(dt.timezone.utc)
with patch('homeassistant.util.dt.utcnow', return_value=now):
entity.new_media_status(media_status)
await hass.async_block_till_done()
state = hass.states.get('media_player.speaker')
assert state.attributes[ATTR_MEDIA_POSITION] == 10
assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now
media_status.current_time = 15
now_plus_5 = now + dt.timedelta(seconds=5)
with patch('homeassistant.util.dt.utcnow', return_value=now_plus_5):
entity.new_media_status(media_status)
await hass.async_block_till_done()
state = hass.states.get('media_player.speaker')
assert state.attributes[ATTR_MEDIA_POSITION] == 10
assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now
media_status.current_time = 20
with patch('homeassistant.util.dt.utcnow', return_value=now_plus_5):
entity.new_media_status(media_status)
await hass.async_block_till_done()
state = hass.states.get('media_player.speaker')
assert state.attributes[ATTR_MEDIA_POSITION] == 20
assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_5
media_status.current_time = 25
now_plus_10 = now + dt.timedelta(seconds=10)
media_status.player_is_playing = False
media_status.player_is_paused = True
with patch('homeassistant.util.dt.utcnow', return_value=now_plus_10):
entity.new_media_status(media_status)
await hass.async_block_till_done()
state = hass.states.get('media_player.speaker')
assert state.attributes[ATTR_MEDIA_POSITION] == 25
assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_10
now_plus_15 = now + dt.timedelta(seconds=15)
with patch('homeassistant.util.dt.utcnow', return_value=now_plus_15):
entity.new_media_status(media_status)
await hass.async_block_till_done()
state = hass.states.get('media_player.speaker')
assert state.attributes[ATTR_MEDIA_POSITION] == 25
assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_10
media_status.current_time = 30
now_plus_20 = now + dt.timedelta(seconds=20)
with patch('homeassistant.util.dt.utcnow', return_value=now_plus_20):
entity.new_media_status(media_status)
await hass.async_block_till_done()
state = hass.states.get('media_player.speaker')
assert state.attributes[ATTR_MEDIA_POSITION] == 30
assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_20
media_status.player_is_paused = False
media_status.player_is_idle = True
with patch('homeassistant.util.dt.utcnow', return_value=now_plus_20):
entity.new_media_status(media_status)
await hass.async_block_till_done()
state = hass.states.get('media_player.speaker')
assert ATTR_MEDIA_POSITION not in state.attributes
assert ATTR_MEDIA_POSITION_UPDATED_AT not in state.attributes
async def test_switched_host(hass: HomeAssistantType):
"""Test cast device listens for changed hosts and disconnects old cast."""
info = get_fake_chromecast_info()

View File

@@ -13,17 +13,19 @@ _LOGGER = logging.getLogger(__name__)
class AiohttpClientMockResponseList(list):
"""List that fires an event on empty pop, for aiohttp Mocker."""
"""Return multiple values for aiohttp Mocker.
aoihttp mocker uses decode to fetch the next value.
"""
def decode(self, _):
"""Return next item from list."""
try:
res = list.pop(self)
res = list.pop(self, 0)
_LOGGER.debug("MockResponseList popped %s: %s", res, self)
return res
except IndexError:
_LOGGER.debug("MockResponseList empty")
return ""
raise AssertionError("MockResponseList empty")
async def wait_till_empty(self, hass):
"""Wait until empty."""
@@ -52,8 +54,8 @@ def aioclient_mock():
yield mock_session
async def test_sensor_device(hass, aioclient_mock):
"""Test a sensor device."""
async def test_binary_sensor_device(hass, aioclient_mock):
"""Test a binary sensor device."""
config = {
'qwikswitch': {
'sensors': {
@@ -67,21 +69,49 @@ async def test_sensor_device(hass, aioclient_mock):
await async_setup_component(hass, QWIKSWITCH, config)
await hass.async_block_till_done()
state_obj = hass.states.get('sensor.s1')
assert state_obj
assert state_obj.state == 'None'
state_obj = hass.states.get('binary_sensor.s1')
assert state_obj.state == 'off'
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
LISTEN.append( # Close
"""{"id":"@a00001","cmd":"","data":"4e0e1601","rssi":"61%"}""")
LISTEN.append('{"id":"@a00001","cmd":"","data":"4e0e1601","rssi":"61%"}')
LISTEN.append('') # Will cause a sleep
await hass.async_block_till_done()
state_obj = hass.states.get('sensor.s1')
assert state_obj.state == 'True'
state_obj = hass.states.get('binary_sensor.s1')
assert state_obj.state == 'on'
# Causes a 30second delay: can be uncommented when upstream library
# allows cancellation of asyncio.sleep(30) on failed packet ("")
# LISTEN.append( # Open
# """{"id":"@a00001","cmd":"","data":"4e0e1701","rssi":"61%"}""")
# await LISTEN.wait_till_empty(hass)
# state_obj = hass.states.get('sensor.s1')
# assert state_obj.state == 'False'
LISTEN.append('{"id":"@a00001","cmd":"","data":"4e0e1701","rssi":"61%"}')
hass.data[QWIKSWITCH]._sleep_task.cancel()
await LISTEN.wait_till_empty(hass)
state_obj = hass.states.get('binary_sensor.s1')
assert state_obj.state == 'off'
async def test_sensor_device(hass, aioclient_mock):
"""Test a sensor device."""
config = {
'qwikswitch': {
'sensors': {
'name': 'ss1',
'id': '@a00001',
'channel': 1,
'type': 'qwikcord',
}
}
}
await async_setup_component(hass, QWIKSWITCH, config)
await hass.async_block_till_done()
state_obj = hass.states.get('sensor.ss1')
assert state_obj.state == 'None'
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
LISTEN.append(
'{"id":"@a00001","name":"ss1","type":"rel",'
'"val":"4733800001a00000"}')
LISTEN.append('') # Will cause a sleep
await LISTEN.wait_till_empty(hass) # await hass.async_block_till_done()
state_obj = hass.states.get('sensor.ss1')
assert state_obj.state == 'None'