mirror of
https://github.com/home-assistant/core.git
synced 2025-11-02 15:39:25 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c49751542f | ||
|
|
2e3a27e418 | ||
|
|
7566bb5aed | ||
|
|
fc1f6ee0f0 | ||
|
|
cb839eff0f | ||
|
|
2bc87bfcf0 | ||
|
|
44be80145b | ||
|
|
8cb1e17ad8 |
70
homeassistant/components/binary_sensor/qwikswitch.py
Normal file
70
homeassistant/components/binary_sensor/qwikswitch.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
83
tests/components/config/test_automation.py
Normal file
83
tests/components/config/test_automation.py
Normal 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
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
Reference in New Issue
Block a user