diff --git a/.coveragerc b/.coveragerc
index 48b45db347b..eae6498cd0a 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -94,6 +94,12 @@ omit =
homeassistant/components/envisalink.py
homeassistant/components/*/envisalink.py
+ homeassistant/components/fritzbox.py
+ homeassistant/components/*/fritzbox.py
+
+ homeassistant/components/eufy.py
+ homeassistant/components/*/eufy.py
+
homeassistant/components/gc100.py
homeassistant/components/*/gc100.py
@@ -106,6 +112,9 @@ omit =
homeassistant/components/hive.py
homeassistant/components/*/hive.py
+ homeassistant/components/homekit_controller/__init__.py
+ homeassistant/components/*/homekit_controller.py
+
homeassistant/components/homematic/__init__.py
homeassistant/components/*/homematic.py
@@ -190,8 +199,8 @@ omit =
homeassistant/components/pilight.py
homeassistant/components/*/pilight.py
- homeassistant/components/qwikswitch.py
- homeassistant/components/*/qwikswitch.py
+ homeassistant/components/switch/qwikswitch.py
+ homeassistant/components/light/qwikswitch.py
homeassistant/components/rachio.py
homeassistant/components/*/rachio.py
@@ -639,7 +648,9 @@ omit =
homeassistant/components/sensor/sensehat.py
homeassistant/components/sensor/serial_pm.py
homeassistant/components/sensor/serial.py
+ homeassistant/components/sensor/sht31.py
homeassistant/components/sensor/shodan.py
+ homeassistant/components/sensor/sigfox.py
homeassistant/components/sensor/simulated.py
homeassistant/components/sensor/skybeacon.py
homeassistant/components/sensor/sma.py
@@ -669,6 +680,7 @@ omit =
homeassistant/components/sensor/uber.py
homeassistant/components/sensor/upnp.py
homeassistant/components/sensor/ups.py
+ homeassistant/components/sensor/uscis.py
homeassistant/components/sensor/vasttrafik.py
homeassistant/components/sensor/viaggiatreno.py
homeassistant/components/sensor/waqi.py
diff --git a/.travis.yml b/.travis.yml
index fce86348817..bf2d05bb185 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -31,7 +31,7 @@ script: travis_wait 30 tox --develop
services:
- docker
before_deploy:
- - docker pull lokalise/lokalise-cli@sha256:79b3108211ed1fcc9f7b09a011bfc53c240fc2f3b7fa7f0c8390f593271b4cd7
+ - docker pull lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21
deploy:
skip_cleanup: true
provider: script
diff --git a/CODEOWNERS b/CODEOWNERS
index 67aef6a248f..528716e174d 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -63,6 +63,7 @@ homeassistant/components/media_player/xiaomi_tv.py @fattdev
homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth
homeassistant/components/plant.py @ChristianKuehnel
homeassistant/components/sensor/airvisual.py @bachya
+homeassistant/components/sensor/filter.py @dgomes
homeassistant/components/sensor/gearbest.py @HerrHofrat
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
@@ -72,6 +73,7 @@ homeassistant/components/sensor/sma.py @kellerza
homeassistant/components/sensor/sql.py @dgomes
homeassistant/components/sensor/sytadin.py @gautric
homeassistant/components/sensor/tibber.py @danielhiversen
+homeassistant/components/sensor/upnp.py @dgomes
homeassistant/components/sensor/waqi.py @andrey-git
homeassistant/components/switch/rainmachine.py @bachya
homeassistant/components/switch/tplink.py @rytilahti
diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py
index aa966027922..deb1746c167 100644
--- a/homeassistant/__main__.py
+++ b/homeassistant/__main__.py
@@ -126,6 +126,10 @@ def get_arguments() -> argparse.Namespace:
default=None,
help='Log file to write to. If not set, CONFIG/home-assistant.log '
'is used')
+ parser.add_argument(
+ '--log-no-color',
+ action='store_true',
+ help="Disable color logs")
parser.add_argument(
'--runner',
action='store_true',
@@ -259,13 +263,14 @@ def setup_and_run_hass(config_dir: str,
hass = bootstrap.from_config_dict(
config, config_dir=config_dir, verbose=args.verbose,
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days,
- log_file=args.log_file)
+ log_file=args.log_file, log_no_color=args.log_no_color)
else:
config_file = ensure_config_file(config_dir)
print('Config directory:', config_dir)
hass = bootstrap.from_config_file(
config_file, verbose=args.verbose, skip_pip=args.skip_pip,
- log_rotate_days=args.log_rotate_days, log_file=args.log_file)
+ log_rotate_days=args.log_rotate_days, log_file=args.log_file,
+ log_no_color=args.log_no_color)
if hass is None:
return None
diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py
index 00822d93299..e0962568a66 100644
--- a/homeassistant/bootstrap.py
+++ b/homeassistant/bootstrap.py
@@ -42,7 +42,8 @@ def from_config_dict(config: Dict[str, Any],
verbose: bool = False,
skip_pip: bool = False,
log_rotate_days: Any = None,
- log_file: Any = None) \
+ log_file: Any = None,
+ log_no_color: bool = False) \
-> Optional[core.HomeAssistant]:
"""Try to configure Home Assistant from a configuration dictionary.
@@ -60,7 +61,7 @@ def from_config_dict(config: Dict[str, Any],
hass = hass.loop.run_until_complete(
async_from_config_dict(
config, hass, config_dir, enable_log, verbose, skip_pip,
- log_rotate_days, log_file)
+ log_rotate_days, log_file, log_no_color)
)
return hass
@@ -74,7 +75,8 @@ def async_from_config_dict(config: Dict[str, Any],
verbose: bool = False,
skip_pip: bool = False,
log_rotate_days: Any = None,
- log_file: Any = None) \
+ log_file: Any = None,
+ log_no_color: bool = False) \
-> Optional[core.HomeAssistant]:
"""Try to configure Home Assistant from a configuration dictionary.
@@ -84,7 +86,8 @@ def async_from_config_dict(config: Dict[str, Any],
start = time()
if enable_log:
- async_enable_logging(hass, verbose, log_rotate_days, log_file)
+ async_enable_logging(hass, verbose, log_rotate_days, log_file,
+ log_no_color)
core_config = config.get(core.DOMAIN, {})
@@ -164,7 +167,8 @@ def from_config_file(config_path: str,
verbose: bool = False,
skip_pip: bool = True,
log_rotate_days: Any = None,
- log_file: Any = None):
+ log_file: Any = None,
+ log_no_color: bool = False):
"""Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter if given,
@@ -176,7 +180,8 @@ def from_config_file(config_path: str,
# run task
hass = hass.loop.run_until_complete(
async_from_config_file(
- config_path, hass, verbose, skip_pip, log_rotate_days, log_file)
+ config_path, hass, verbose, skip_pip,
+ log_rotate_days, log_file, log_no_color)
)
return hass
@@ -188,7 +193,8 @@ def async_from_config_file(config_path: str,
verbose: bool = False,
skip_pip: bool = True,
log_rotate_days: Any = None,
- log_file: Any = None):
+ log_file: Any = None,
+ log_no_color: bool = False):
"""Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter.
@@ -199,7 +205,8 @@ def async_from_config_file(config_path: str,
hass.config.config_dir = config_dir
yield from async_mount_local_lib_path(config_dir, hass.loop)
- async_enable_logging(hass, verbose, log_rotate_days, log_file)
+ async_enable_logging(hass, verbose, log_rotate_days, log_file,
+ log_no_color)
try:
config_dict = yield from hass.async_add_job(
@@ -216,40 +223,51 @@ def async_from_config_file(config_path: str,
@core.callback
-def async_enable_logging(hass: core.HomeAssistant, verbose: bool = False,
- log_rotate_days=None, log_file=None) -> None:
+def async_enable_logging(hass: core.HomeAssistant,
+ verbose: bool = False,
+ log_rotate_days=None,
+ log_file=None,
+ log_no_color: bool = False) -> None:
"""Set up the logging.
This method must be run in the event loop.
"""
- logging.basicConfig(level=logging.INFO)
fmt = ("%(asctime)s %(levelname)s (%(threadName)s) "
"[%(name)s] %(message)s")
- colorfmt = "%(log_color)s{}%(reset)s".format(fmt)
datefmt = '%Y-%m-%d %H:%M:%S'
+ if not log_no_color:
+ try:
+ from colorlog import ColoredFormatter
+ # basicConfig must be called after importing colorlog in order to
+ # ensure that the handlers it sets up wraps the correct streams.
+ logging.basicConfig(level=logging.INFO)
+
+ colorfmt = "%(log_color)s{}%(reset)s".format(fmt)
+ logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
+ colorfmt,
+ datefmt=datefmt,
+ reset=True,
+ log_colors={
+ 'DEBUG': 'cyan',
+ 'INFO': 'green',
+ 'WARNING': 'yellow',
+ 'ERROR': 'red',
+ 'CRITICAL': 'red',
+ }
+ ))
+ except ImportError:
+ pass
+
+ # If the above initialization failed for any reason, setup the default
+ # formatting. If the above succeeds, this wil result in a no-op.
+ logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO)
+
# Suppress overly verbose logs from libraries that aren't helpful
logging.getLogger('requests').setLevel(logging.WARNING)
logging.getLogger('urllib3').setLevel(logging.WARNING)
logging.getLogger('aiohttp.access').setLevel(logging.WARNING)
- try:
- from colorlog import ColoredFormatter
- logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
- colorfmt,
- datefmt=datefmt,
- reset=True,
- log_colors={
- 'DEBUG': 'cyan',
- 'INFO': 'green',
- 'WARNING': 'yellow',
- 'ERROR': 'red',
- 'CRITICAL': 'red',
- }
- ))
- except ImportError:
- pass
-
# Log errors to a file if we have write access to file or config dir
if log_file is None:
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
diff --git a/homeassistant/components/abode.py b/homeassistant/components/abode.py
index 08918c77f01..2f56bb7c2b5 100644
--- a/homeassistant/components/abode.py
+++ b/homeassistant/components/abode.py
@@ -19,7 +19,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['abodepy==0.12.3']
+REQUIREMENTS = ['abodepy==0.13.1']
_LOGGER = logging.getLogger(__name__)
@@ -27,6 +27,7 @@ CONF_ATTRIBUTION = "Data provided by goabode.com"
CONF_POLLING = 'polling'
DOMAIN = 'abode'
+DEFAULT_CACHEDB = './abodepy_cache.pickle'
NOTIFICATION_ID = 'abode_notification'
NOTIFICATION_TITLE = 'Abode Security Setup'
@@ -87,12 +88,13 @@ ABODE_PLATFORMS = [
class AbodeSystem(object):
"""Abode System class."""
- def __init__(self, username, password, name, polling, exclude, lights):
+ def __init__(self, username, password, cache,
+ name, polling, exclude, lights):
"""Initialize the system."""
import abodepy
self.abode = abodepy.Abode(
username, password, auto_login=True, get_devices=True,
- get_automations=True)
+ get_automations=True, cache_path=cache)
self.name = name
self.polling = polling
self.exclude = exclude
@@ -129,8 +131,9 @@ def setup(hass, config):
lights = conf.get(CONF_LIGHTS)
try:
+ cache = hass.config.path(DEFAULT_CACHEDB)
hass.data[DOMAIN] = AbodeSystem(
- username, password, name, polling, exclude, lights)
+ username, password, cache, name, polling, exclude, lights)
except (AbodeException, ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Abode: %s", str(ex))
diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py
index 707f8d02958..c5c68f1af40 100644
--- a/homeassistant/components/alexa/smart_home.py
+++ b/homeassistant/components/alexa/smart_home.py
@@ -1471,6 +1471,7 @@ async def async_api_adjust_target_temp(hass, config, request, entity):
async def async_api_set_thermostat_mode(hass, config, request, entity):
"""Process a set thermostat mode request."""
mode = request[API_PAYLOAD]['thermostatMode']
+ mode = mode if isinstance(mode, str) else mode['value']
operation_list = entity.attributes.get(climate.ATTR_OPERATION_LIST)
# Work around a pylint false positive due to
diff --git a/homeassistant/components/binary_sensor/bmw_connected_drive.py b/homeassistant/components/binary_sensor/bmw_connected_drive.py
index e7af5af988b..0abf6eb1064 100644
--- a/homeassistant/components/binary_sensor/bmw_connected_drive.py
+++ b/homeassistant/components/binary_sensor/bmw_connected_drive.py
@@ -46,6 +46,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
self._vehicle = vehicle
self._attribute = attribute
self._name = '{} {}'.format(self._vehicle.name, self._attribute)
+ self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute)
self._sensor_name = sensor_name
self._device_class = device_class
self._state = None
@@ -55,6 +56,11 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
"""Data update is triggered from BMWConnectedDriveEntity."""
return False
+ @property
+ def unique_id(self):
+ """Return the unique ID of the binary sensor."""
+ return self._unique_id
+
@property
def name(self):
"""Return the name of the binary sensor."""
diff --git a/homeassistant/components/binary_sensor/hive.py b/homeassistant/components/binary_sensor/hive.py
index 2d4cbd8d070..46dd1b193e8 100644
--- a/homeassistant/components/binary_sensor/hive.py
+++ b/homeassistant/components/binary_sensor/hive.py
@@ -32,6 +32,7 @@ class HiveBinarySensorEntity(BinarySensorDevice):
self.device_type = hivedevice["HA_DeviceType"]
self.node_device_type = hivedevice["Hive_DeviceType"]
self.session = hivesession
+ self.attributes = {}
self.data_updatesource = '{}.{}'.format(self.device_type,
self.node_id)
@@ -52,6 +53,11 @@ class HiveBinarySensorEntity(BinarySensorDevice):
"""Return the name of the binary sensor."""
return self.node_name
+ @property
+ def device_state_attributes(self):
+ """Show Device Attributes."""
+ return self.attributes
+
@property
def is_on(self):
"""Return true if the binary sensor is on."""
@@ -61,3 +67,5 @@ class HiveBinarySensorEntity(BinarySensorDevice):
def update(self):
"""Update all Node data from Hive."""
self.session.core.update_data(self.node_id)
+ self.attributes = self.session.attributes.state_attributes(
+ self.node_id)
diff --git a/homeassistant/components/binary_sensor/maxcube.py b/homeassistant/components/binary_sensor/maxcube.py
index 1043004243a..c131de5420a 100644
--- a/homeassistant/components/binary_sensor/maxcube.py
+++ b/homeassistant/components/binary_sensor/maxcube.py
@@ -7,7 +7,7 @@ https://home-assistant.io/components/maxcube/
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
-from homeassistant.components.maxcube import MAXCUBE_HANDLE
+from homeassistant.components.maxcube import DATA_KEY
from homeassistant.const import STATE_UNKNOWN
_LOGGER = logging.getLogger(__name__)
@@ -15,16 +15,17 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Iterate through all MAX! Devices and add window shutters."""
- cube = hass.data[MAXCUBE_HANDLE].cube
devices = []
+ for handler in hass.data[DATA_KEY].values():
+ cube = handler.cube
+ for device in cube.devices:
+ name = "{} {}".format(
+ cube.room_by_id(device.room_id).name, device.name)
- for device in cube.devices:
- name = "{} {}".format(
- cube.room_by_id(device.room_id).name, device.name)
-
- # Only add Window Shutters
- if cube.is_windowshutter(device):
- devices.append(MaxCubeShutter(hass, name, device.rf_address))
+ # Only add Window Shutters
+ if cube.is_windowshutter(device):
+ devices.append(
+ MaxCubeShutter(handler, name, device.rf_address))
if devices:
add_devices(devices)
@@ -33,12 +34,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class MaxCubeShutter(BinarySensorDevice):
"""Representation of a MAX! Cube Binary Sensor device."""
- def __init__(self, hass, name, rf_address):
+ def __init__(self, handler, name, rf_address):
"""Initialize MAX! Cube BinarySensorDevice."""
self._name = name
self._sensor_type = 'window'
self._rf_address = rf_address
- self._cubehandle = hass.data[MAXCUBE_HANDLE]
+ self._cubehandle = handler
self._state = STATE_UNKNOWN
@property
diff --git a/homeassistant/components/bmw_connected_drive.py b/homeassistant/components/bmw_connected_drive/__init__.py
similarity index 55%
rename from homeassistant/components/bmw_connected_drive.py
rename to homeassistant/components/bmw_connected_drive/__init__.py
index 48452b6d79b..347bab6f529 100644
--- a/homeassistant/components/bmw_connected_drive.py
+++ b/homeassistant/components/bmw_connected_drive/__init__.py
@@ -20,7 +20,7 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = 'bmw_connected_drive'
CONF_REGION = 'region'
-
+ATTR_VIN = 'vin'
ACCOUNT_SCHEMA = vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
@@ -35,35 +35,40 @@ CONFIG_SCHEMA = vol.Schema({
},
}, extra=vol.ALLOW_EXTRA)
+SERVICE_SCHEMA = vol.Schema({
+ vol.Required(ATTR_VIN): cv.string,
+})
+
BMW_COMPONENTS = ['binary_sensor', 'device_tracker', 'lock', 'sensor']
UPDATE_INTERVAL = 5 # in minutes
+SERVICE_UPDATE_STATE = 'update_state'
-def setup(hass, config):
+_SERVICE_MAP = {
+ 'light_flash': 'trigger_remote_light_flash',
+ 'sound_horn': 'trigger_remote_horn',
+ 'activate_air_conditioning': 'trigger_remote_air_conditioning',
+}
+
+
+def setup(hass, config: dict):
"""Set up the BMW connected drive components."""
accounts = []
for name, account_config in config[DOMAIN].items():
- username = account_config[CONF_USERNAME]
- password = account_config[CONF_PASSWORD]
- region = account_config[CONF_REGION]
- _LOGGER.debug('Adding new account %s', name)
- bimmer = BMWConnectedDriveAccount(username, password, region, name)
- accounts.append(bimmer)
-
- # update every UPDATE_INTERVAL minutes, starting now
- # this should even out the load on the servers
-
- now = datetime.datetime.now()
- track_utc_time_change(
- hass, bimmer.update,
- minute=range(now.minute % UPDATE_INTERVAL, 60, UPDATE_INTERVAL),
- second=now.second)
+ accounts.append(setup_account(account_config, hass, name))
hass.data[DOMAIN] = accounts
- for account in accounts:
- account.update()
+ def _update_all(call) -> None:
+ """Update all BMW accounts."""
+ for cd_account in hass.data[DOMAIN]:
+ cd_account.update()
+
+ # Service to manually trigger updates for all accounts.
+ hass.services.register(DOMAIN, SERVICE_UPDATE_STATE, _update_all)
+
+ _update_all(None)
for component in BMW_COMPONENTS:
discovery.load_platform(hass, component, DOMAIN, {}, config)
@@ -71,6 +76,48 @@ def setup(hass, config):
return True
+def setup_account(account_config: dict, hass, name: str) \
+ -> 'BMWConnectedDriveAccount':
+ """Set up a new BMWConnectedDriveAccount based on the config."""
+ username = account_config[CONF_USERNAME]
+ password = account_config[CONF_PASSWORD]
+ region = account_config[CONF_REGION]
+ _LOGGER.debug('Adding new account %s', name)
+ cd_account = BMWConnectedDriveAccount(username, password, region, name)
+
+ def execute_service(call):
+ """Execute a service for a vehicle.
+
+ This must be a member function as we need access to the cd_account
+ object here.
+ """
+ vin = call.data[ATTR_VIN]
+ vehicle = cd_account.account.get_vehicle(vin)
+ if not vehicle:
+ _LOGGER.error('Could not find a vehicle for VIN "%s"!', vin)
+ return
+ function_name = _SERVICE_MAP[call.service]
+ function_call = getattr(vehicle.remote_services, function_name)
+ function_call()
+
+ # register the remote services
+ for service in _SERVICE_MAP:
+ hass.services.register(
+ DOMAIN, service,
+ execute_service,
+ schema=SERVICE_SCHEMA)
+
+ # update every UPDATE_INTERVAL minutes, starting now
+ # this should even out the load on the servers
+ now = datetime.datetime.now()
+ track_utc_time_change(
+ hass, cd_account.update,
+ minute=range(now.minute % UPDATE_INTERVAL, 60, UPDATE_INTERVAL),
+ second=now.second)
+
+ return cd_account
+
+
class BMWConnectedDriveAccount(object):
"""Representation of a BMW vehicle."""
diff --git a/homeassistant/components/bmw_connected_drive/services.yaml b/homeassistant/components/bmw_connected_drive/services.yaml
new file mode 100644
index 00000000000..3c180271919
--- /dev/null
+++ b/homeassistant/components/bmw_connected_drive/services.yaml
@@ -0,0 +1,42 @@
+# Describes the format for available services for bmw_connected_drive
+#
+# The services related to locking/unlocking are implemented in the lock
+# component to avoid redundancy.
+
+light_flash:
+ description: >
+ Flash the lights of the vehicle. The vehicle is identified via the vin
+ (see below).
+ fields:
+ vin:
+ description: >
+ The vehicle identification number (VIN) of the vehicle, 17 characters
+ example: WBANXXXXXX1234567
+
+sound_horn:
+ description: >
+ Sound the horn of the vehicle. The vehicle is identified via the vin
+ (see below).
+ fields:
+ vin:
+ description: >
+ The vehicle identification number (VIN) of the vehicle, 17 characters
+ example: WBANXXXXXX1234567
+
+activate_air_conditioning:
+ description: >
+ Start the air conditioning of the vehicle. What exactly is started here
+ depends on the type of vehicle. It might range from just ventilation over
+ auxilary heating to real air conditioning. The vehicle is identified via
+ the vin (see below).
+ fields:
+ vin:
+ description: >
+ The vehicle identification number (VIN) of the vehicle, 17 characters
+ example: WBANXXXXXX1234567
+
+update_state:
+ description: >
+ Fetch the last state of the vehicles of all your accounts from the BMW
+ server. This does *not* trigger an update from the vehicle, it just gets
+ the data from the BMW servers. This service does not require any attributes.
\ No newline at end of file
diff --git a/homeassistant/components/calendar/google.py b/homeassistant/components/calendar/google.py
index a8763e8ca9e..6c26c65ebe7 100644
--- a/homeassistant/components/calendar/google.py
+++ b/homeassistant/components/calendar/google.py
@@ -11,6 +11,7 @@ from datetime import timedelta
from homeassistant.components.calendar import CalendarEventDevice
from homeassistant.components.google import (
CONF_CAL_ID, CONF_ENTITIES, CONF_TRACK, TOKEN_FILE,
+ CONF_IGNORE_AVAILABILITY, CONF_SEARCH,
GoogleCalendarService)
from homeassistant.util import Throttle, dt
@@ -18,7 +19,7 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_GOOGLE_SEARCH_PARAMS = {
'orderBy': 'startTime',
- 'maxResults': 1,
+ 'maxResults': 5,
'singleEvents': True,
}
@@ -45,18 +46,22 @@ class GoogleCalendarEventDevice(CalendarEventDevice):
def __init__(self, hass, calendar_service, calendar, data):
"""Create the Calendar event device."""
self.data = GoogleCalendarData(calendar_service, calendar,
- data.get('search', None))
+ data.get(CONF_SEARCH),
+ data.get(CONF_IGNORE_AVAILABILITY))
+
super().__init__(hass, data)
class GoogleCalendarData(object):
"""Class to utilize calendar service object to get next event."""
- def __init__(self, calendar_service, calendar_id, search=None):
+ def __init__(self, calendar_service, calendar_id, search,
+ ignore_availability):
"""Set up how we are going to search the google calendar."""
self.calendar_service = calendar_service
self.calendar_id = calendar_id
self.search = search
+ self.ignore_availability = ignore_availability
self.event = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
@@ -80,5 +85,17 @@ class GoogleCalendarData(object):
result = events.list(**params).execute()
items = result.get('items', [])
- self.event = items[0] if len(items) == 1 else None
+
+ new_event = None
+ for item in items:
+ if (not self.ignore_availability
+ and 'transparency' in item.keys()):
+ if item['transparency'] == 'opaque':
+ new_event = item
+ break
+ else:
+ new_event = item
+ break
+
+ self.event = new_event
return True
diff --git a/homeassistant/components/calendar/services.yaml b/homeassistant/components/calendar/services.yaml
index 61ff4345fbe..ebf0c7b1591 100644
--- a/homeassistant/components/calendar/services.yaml
+++ b/homeassistant/components/calendar/services.yaml
@@ -1,21 +1,26 @@
# Describes the format for available calendar services
-todoist:
- new_task:
- description: Create a new task and add it to a project.
- fields:
- content:
- description: The name of the task (Required).
- example: Pick up the mail
- project:
- description: The name of the project this task should belong to. Defaults to Inbox (Optional).
- example: Errands
- labels:
- description: Any labels that you want to apply to this task, separated by a comma (Optional).
- example: Chores,Deliveries
- priority:
- description: The priority of this task, from 1 (normal) to 4 (urgent) (Optional).
- example: 2
- due_date:
- description: The day this task is due, in format YYYY-MM-DD (Optional).
- example: "2018-04-01"
+todoist_new_task:
+ description: Create a new task and add it to a project.
+ fields:
+ content:
+ description: The name of the task.
+ example: Pick up the mail
+ project:
+ description: The name of the project this task should belong to. Defaults to Inbox.
+ example: Errands
+ labels:
+ description: Any labels that you want to apply to this task, separated by a comma.
+ example: Chores,Deliveries
+ priority:
+ description: The priority of this task, from 1 (normal) to 4 (urgent).
+ example: 2
+ due_date_string:
+ description: The day this task is due, in natural language.
+ example: "tomorrow"
+ due_date_lang:
+ description: The language of due_date_string.
+ example: "en"
+ due_date:
+ description: The day this task is due, in format YYYY-MM-DD.
+ example: "2018-04-01"
diff --git a/homeassistant/components/calendar/todoist.py b/homeassistant/components/calendar/todoist.py
index 02840c7d0ee..b70e44456db 100644
--- a/homeassistant/components/calendar/todoist.py
+++ b/homeassistant/components/calendar/todoist.py
@@ -41,6 +41,14 @@ CONTENT = 'content'
DESCRIPTION = 'description'
# Calendar Platform: Used in the '_get_date()' method
DATETIME = 'dateTime'
+# Service Call: When is this task due (in natural language)?
+DUE_DATE_STRING = 'due_date_string'
+# Service Call: The language of DUE_DATE_STRING
+DUE_DATE_LANG = 'due_date_lang'
+# Service Call: The available options of DUE_DATE_LANG
+DUE_DATE_VALID_LANGS = ['en', 'da', 'pl', 'zh', 'ko', 'de',
+ 'pt', 'ja', 'it', 'fr', 'sv', 'ru',
+ 'es', 'nl']
# Attribute: When is this task due?
# Service Call: When is this task due?
DUE_DATE = 'due_date'
@@ -83,7 +91,11 @@ NEW_TASK_SERVICE_SCHEMA = vol.Schema({
vol.Optional(PROJECT_NAME, default='inbox'): vol.All(cv.string, vol.Lower),
vol.Optional(LABELS): cv.ensure_list_csv,
vol.Optional(PRIORITY): vol.All(vol.Coerce(int), vol.Range(min=1, max=4)),
- vol.Optional(DUE_DATE): cv.string,
+
+ vol.Exclusive(DUE_DATE_STRING, 'due_date'): cv.string,
+ vol.Optional(DUE_DATE_LANG):
+ vol.All(cv.string, vol.In(DUE_DATE_VALID_LANGS)),
+ vol.Exclusive(DUE_DATE, 'due_date'): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@@ -186,6 +198,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if PRIORITY in call.data:
item.update(priority=call.data[PRIORITY])
+ if DUE_DATE_STRING in call.data:
+ item.update(date_string=call.data[DUE_DATE_STRING])
+
+ if DUE_DATE_LANG in call.data:
+ item.update(date_lang=call.data[DUE_DATE_LANG])
+
if DUE_DATE in call.data:
due_date = dt.parse_datetime(call.data[DUE_DATE])
if due_date is None:
diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py
index 7ea23f4fd65..550d4035ddd 100644
--- a/homeassistant/components/climate/__init__.py
+++ b/homeassistant/components/climate/__init__.py
@@ -40,6 +40,7 @@ STATE_HEAT = 'heat'
STATE_COOL = 'cool'
STATE_IDLE = 'idle'
STATE_AUTO = 'auto'
+STATE_MANUAL = 'manual'
STATE_DRY = 'dry'
STATE_FAN_ONLY = 'fan_only'
STATE_ECO = 'eco'
diff --git a/homeassistant/components/climate/fritzbox.py b/homeassistant/components/climate/fritzbox.py
new file mode 100755
index 00000000000..839da8c9d53
--- /dev/null
+++ b/homeassistant/components/climate/fritzbox.py
@@ -0,0 +1,153 @@
+"""
+Support for AVM Fritz!Box smarthome thermostate devices.
+
+For more details about this component, please refer to the documentation at
+http://home-assistant.io/components/climate.fritzbox/
+"""
+import logging
+
+import requests
+
+from homeassistant.components.fritzbox import DOMAIN as FRITZBOX_DOMAIN
+from homeassistant.components.fritzbox import (
+ ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_BATTERY_LOW, ATTR_STATE_LOCKED)
+from homeassistant.components.climate import (
+ ATTR_OPERATION_MODE, ClimateDevice, STATE_ECO, STATE_HEAT, STATE_MANUAL,
+ SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE)
+from homeassistant.const import (
+ ATTR_TEMPERATURE, PRECISION_HALVES, TEMP_CELSIUS)
+
+DEPENDENCIES = ['fritzbox']
+
+_LOGGER = logging.getLogger(__name__)
+
+SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE)
+
+OPERATION_LIST = [STATE_HEAT, STATE_ECO]
+
+MIN_TEMPERATURE = 8
+MAX_TEMPERATURE = 28
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the Fritzbox smarthome thermostat platform."""
+ devices = []
+ fritz_list = hass.data[FRITZBOX_DOMAIN]
+
+ for fritz in fritz_list:
+ device_list = fritz.get_devices()
+ for device in device_list:
+ if device.has_thermostat:
+ devices.append(FritzboxThermostat(device, fritz))
+
+ add_devices(devices)
+
+
+class FritzboxThermostat(ClimateDevice):
+ """The thermostat class for Fritzbox smarthome thermostates."""
+
+ def __init__(self, device, fritz):
+ """Initialize the thermostat."""
+ self._device = device
+ self._fritz = fritz
+ self._current_temperature = self._device.actual_temperature
+ self._target_temperature = self._device.target_temperature
+ self._comfort_temperature = self._device.comfort_temperature
+ self._eco_temperature = self._device.eco_temperature
+
+ @property
+ def supported_features(self):
+ """Return the list of supported features."""
+ return SUPPORT_FLAGS
+
+ @property
+ def available(self):
+ """Return if thermostat is available."""
+ return self._device.present
+
+ @property
+ def name(self):
+ """Return the name of the device."""
+ return self._device.name
+
+ @property
+ def temperature_unit(self):
+ """Return the unit of measurement that is used."""
+ return TEMP_CELSIUS
+
+ @property
+ def precision(self):
+ """Return precision 0.5."""
+ return PRECISION_HALVES
+
+ @property
+ def current_temperature(self):
+ """Return the current temperature."""
+ return self._current_temperature
+
+ @property
+ def target_temperature(self):
+ """Return the temperature we try to reach."""
+ return self._target_temperature
+
+ def set_temperature(self, **kwargs):
+ """Set new target temperature."""
+ if ATTR_OPERATION_MODE in kwargs:
+ operation_mode = kwargs.get(ATTR_OPERATION_MODE)
+ self.set_operation_mode(operation_mode)
+ elif ATTR_TEMPERATURE in kwargs:
+ temperature = kwargs.get(ATTR_TEMPERATURE)
+ self._device.set_target_temperature(temperature)
+
+ @property
+ def current_operation(self):
+ """Return the current operation mode."""
+ if self._target_temperature == self._comfort_temperature:
+ return STATE_HEAT
+ elif self._target_temperature == self._eco_temperature:
+ return STATE_ECO
+ return STATE_MANUAL
+
+ @property
+ def operation_list(self):
+ """Return the list of available operation modes."""
+ return OPERATION_LIST
+
+ def set_operation_mode(self, operation_mode):
+ """Set new operation mode."""
+ if operation_mode == STATE_HEAT:
+ self.set_temperature(temperature=self._comfort_temperature)
+ elif operation_mode == STATE_ECO:
+ self.set_temperature(temperature=self._eco_temperature)
+
+ @property
+ def min_temp(self):
+ """Return the minimum temperature."""
+ return MIN_TEMPERATURE
+
+ @property
+ def max_temp(self):
+ """Return the maximum temperature."""
+ return MAX_TEMPERATURE
+
+ @property
+ def device_state_attributes(self):
+ """Return the device specific state attributes."""
+ attrs = {
+ ATTR_STATE_DEVICE_LOCKED: self._device.device_lock,
+ ATTR_STATE_LOCKED: self._device.lock,
+ ATTR_STATE_BATTERY_LOW: self._device.battery_low,
+ }
+ return attrs
+
+ def update(self):
+ """Update the data from the thermostat."""
+ try:
+ self._device.update()
+ self._current_temperature = self._device.actual_temperature
+ self._target_temperature = self._device.target_temperature
+ self._comfort_temperature = self._device.comfort_temperature
+ self._eco_temperature = self._device.eco_temperature
+ except requests.exceptions.HTTPError as ex:
+ _LOGGER.warning("Fritzbox connection error: %s", ex)
+ self._fritz.login()
diff --git a/homeassistant/components/climate/hive.py b/homeassistant/components/climate/hive.py
index 760ef131049..eb3aecae3a1 100644
--- a/homeassistant/components/climate/hive.py
+++ b/homeassistant/components/climate/hive.py
@@ -38,7 +38,10 @@ class HiveClimateEntity(ClimateDevice):
self.node_id = hivedevice["Hive_NodeID"]
self.node_name = hivedevice["Hive_NodeName"]
self.device_type = hivedevice["HA_DeviceType"]
+ if self.device_type == "Heating":
+ self.thermostat_node_id = hivedevice["Thermostat_NodeID"]
self.session = hivesession
+ self.attributes = {}
self.data_updatesource = '{}.{}'.format(self.device_type,
self.node_id)
@@ -71,6 +74,11 @@ class HiveClimateEntity(ClimateDevice):
friendly_name = "Hot Water"
return friendly_name
+ @property
+ def device_state_attributes(self):
+ """Show Device Attributes."""
+ return self.attributes
+
@property
def temperature_unit(self):
"""Return the unit of measurement."""
@@ -175,4 +183,9 @@ class HiveClimateEntity(ClimateDevice):
def update(self):
"""Update all Node data from Hive."""
+ node = self.node_id
+ if self.device_type == "Heating":
+ node = self.thermostat_node_id
+
self.session.core.update_data(self.node_id)
+ self.attributes = self.session.attributes.state_attributes(node)
diff --git a/homeassistant/components/climate/maxcube.py b/homeassistant/components/climate/maxcube.py
index 067d11437b2..712ebb4f4ce 100644
--- a/homeassistant/components/climate/maxcube.py
+++ b/homeassistant/components/climate/maxcube.py
@@ -10,7 +10,7 @@ import logging
from homeassistant.components.climate import (
ClimateDevice, STATE_AUTO, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_OPERATION_MODE)
-from homeassistant.components.maxcube import MAXCUBE_HANDLE
+from homeassistant.components.maxcube import DATA_KEY
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
_LOGGER = logging.getLogger(__name__)
@@ -24,16 +24,16 @@ SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Iterate through all MAX! Devices and add thermostats."""
- cube = hass.data[MAXCUBE_HANDLE].cube
-
devices = []
+ for handler in hass.data[DATA_KEY].values():
+ cube = handler.cube
+ for device in cube.devices:
+ name = '{} {}'.format(
+ cube.room_by_id(device.room_id).name, device.name)
- for device in cube.devices:
- name = '{} {}'.format(
- cube.room_by_id(device.room_id).name, device.name)
-
- if cube.is_thermostat(device) or cube.is_wallthermostat(device):
- devices.append(MaxCubeClimate(hass, name, device.rf_address))
+ if cube.is_thermostat(device) or cube.is_wallthermostat(device):
+ devices.append(
+ MaxCubeClimate(handler, name, device.rf_address))
if devices:
add_devices(devices)
@@ -42,14 +42,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class MaxCubeClimate(ClimateDevice):
"""MAX! Cube ClimateDevice."""
- def __init__(self, hass, name, rf_address):
+ def __init__(self, handler, name, rf_address):
"""Initialize MAX! Cube ClimateDevice."""
self._name = name
self._unit_of_measurement = TEMP_CELSIUS
self._operation_list = [STATE_AUTO, STATE_MANUAL, STATE_BOOST,
STATE_VACATION]
self._rf_address = rf_address
- self._cubehandle = hass.data[MAXCUBE_HANDLE]
+ self._cubehandle = handler
@property
def supported_features(self):
diff --git a/homeassistant/components/climate/modbus.py b/homeassistant/components/climate/modbus.py
new file mode 100644
index 00000000000..7d392e5a40f
--- /dev/null
+++ b/homeassistant/components/climate/modbus.py
@@ -0,0 +1,148 @@
+"""
+Platform for a Generic Modbus Thermostat.
+
+This uses a setpoint and process
+value within the controller, so both the current temperature register and the
+target temperature register need to be configured.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/climate.modbus/
+"""
+import logging
+import struct
+
+import voluptuous as vol
+
+from homeassistant.const import (
+ CONF_NAME, CONF_SLAVE, ATTR_TEMPERATURE)
+from homeassistant.components.climate import (
+ ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE)
+
+import homeassistant.components.modbus as modbus
+import homeassistant.helpers.config_validation as cv
+
+DEPENDENCIES = ['modbus']
+
+# Parameters not defined by homeassistant.const
+CONF_TARGET_TEMP = 'target_temp_register'
+CONF_CURRENT_TEMP = 'current_temp_register'
+CONF_DATA_TYPE = 'data_type'
+CONF_COUNT = 'data_count'
+CONF_PRECISION = 'precision'
+
+DATA_TYPE_INT = 'int'
+DATA_TYPE_UINT = 'uint'
+DATA_TYPE_FLOAT = 'float'
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_NAME): cv.string,
+ vol.Required(CONF_SLAVE): cv.positive_int,
+ vol.Required(CONF_TARGET_TEMP): cv.positive_int,
+ vol.Required(CONF_CURRENT_TEMP): cv.positive_int,
+ vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_FLOAT):
+ vol.In([DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT]),
+ vol.Optional(CONF_COUNT, default=2): cv.positive_int,
+ vol.Optional(CONF_PRECISION, default=1): cv.positive_int
+})
+
+_LOGGER = logging.getLogger(__name__)
+
+SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the Modbus Thermostat Platform."""
+ name = config.get(CONF_NAME)
+ modbus_slave = config.get(CONF_SLAVE)
+ target_temp_register = config.get(CONF_TARGET_TEMP)
+ current_temp_register = config.get(CONF_CURRENT_TEMP)
+ data_type = config.get(CONF_DATA_TYPE)
+ count = config.get(CONF_COUNT)
+ precision = config.get(CONF_PRECISION)
+
+ add_devices([ModbusThermostat(name, modbus_slave,
+ target_temp_register, current_temp_register,
+ data_type, count, precision)], True)
+
+
+class ModbusThermostat(ClimateDevice):
+ """Representation of a Modbus Thermostat."""
+
+ def __init__(self, name, modbus_slave, target_temp_register,
+ current_temp_register, data_type, count, precision):
+ """Initialize the unit."""
+ self._name = name
+ self._slave = modbus_slave
+ self._target_temperature_register = target_temp_register
+ self._current_temperature_register = current_temp_register
+ self._target_temperature = None
+ self._current_temperature = None
+ self._data_type = data_type
+ self._count = int(count)
+ self._precision = precision
+ self._structure = '>f'
+
+ data_types = {DATA_TYPE_INT: {1: 'h', 2: 'i', 4: 'q'},
+ DATA_TYPE_UINT: {1: 'H', 2: 'I', 4: 'Q'},
+ DATA_TYPE_FLOAT: {1: 'e', 2: 'f', 4: 'd'}}
+
+ self._structure = '>{}'.format(data_types[self._data_type]
+ [self._count])
+
+ @property
+ def supported_features(self):
+ """Return the list of supported features."""
+ return SUPPORT_FLAGS
+
+ def update(self):
+ """Update Target & Current Temperature."""
+ self._target_temperature = self.read_register(
+ self._target_temperature_register)
+ self._current_temperature = self.read_register(
+ self._current_temperature_register)
+
+ @property
+ def name(self):
+ """Return the name of the climate device."""
+ return self._name
+
+ @property
+ def current_temperature(self):
+ """Return the current temperature."""
+ return self._current_temperature
+
+ @property
+ def target_temperature(self):
+ """Return the temperature we try to reach."""
+ return self._target_temperature
+
+ def set_temperature(self, **kwargs):
+ """Set new target temperature."""
+ target_temperature = kwargs.get(ATTR_TEMPERATURE)
+ if target_temperature is None:
+ return
+ byte_string = struct.pack(self._structure, target_temperature)
+ register_value = struct.unpack('>h', byte_string[0:2])[0]
+
+ try:
+ self.write_register(self._target_temperature_register,
+ register_value)
+ except AttributeError as ex:
+ _LOGGER.error(ex)
+
+ def read_register(self, register):
+ """Read holding register using the modbus hub slave."""
+ try:
+ result = modbus.HUB.read_holding_registers(self._slave, register,
+ self._count)
+ except AttributeError as ex:
+ _LOGGER.error(ex)
+ byte_string = b''.join(
+ [x.to_bytes(2, byteorder='big') for x in result.registers])
+ val = struct.unpack(self._structure, byte_string)[0]
+ register_value = format(val, '.{}f'.format(self._precision))
+ return register_value
+
+ def write_register(self, register, value):
+ """Write register using the modbus hub slave."""
+ modbus.HUB.write_registers(self._slave, register, [value, 0])
diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py
index d11f6890a7b..0a5344fdf98 100644
--- a/homeassistant/components/climate/nest.py
+++ b/homeassistant/components/climate/nest.py
@@ -187,6 +187,11 @@ class NestThermostat(ClimateDevice):
device_mode = operation_mode
elif operation_mode == STATE_AUTO:
device_mode = NEST_MODE_HEAT_COOL
+ else:
+ device_mode = STATE_OFF
+ _LOGGER.error(
+ "An error occurred while setting device mode. "
+ "Invalid operation mode: %s", operation_mode)
self.device.mode = device_mode
@property
diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py
index aa42325b75b..d2aa918eda2 100644
--- a/homeassistant/components/config/config_entries.py
+++ b/homeassistant/components/config/config_entries.py
@@ -1,11 +1,10 @@
"""Http views to control the config manager."""
import asyncio
-import voluptuous as vol
-
-from homeassistant import config_entries
+from homeassistant import config_entries, data_entry_flow
from homeassistant.components.http import HomeAssistantView
-from homeassistant.components.http.data_validator import RequestDataValidator
+from homeassistant.helpers.data_entry_flow import (
+ FlowManagerIndexView, FlowManagerResourceView)
REQUIREMENTS = ['voluptuous-serialize==1']
@@ -16,15 +15,17 @@ def async_setup(hass):
"""Enable the Home Assistant views."""
hass.http.register_view(ConfigManagerEntryIndexView)
hass.http.register_view(ConfigManagerEntryResourceView)
- hass.http.register_view(ConfigManagerFlowIndexView)
- hass.http.register_view(ConfigManagerFlowResourceView)
+ hass.http.register_view(
+ ConfigManagerFlowIndexView(hass.config_entries.flow))
+ hass.http.register_view(
+ ConfigManagerFlowResourceView(hass.config_entries.flow))
hass.http.register_view(ConfigManagerAvailableFlowView)
return True
def _prepare_json(result):
"""Convert result for JSON."""
- if result['type'] != config_entries.RESULT_TYPE_FORM:
+ if result['type'] != data_entry_flow.RESULT_TYPE_FORM:
return result
import voluptuous_serialize
@@ -78,7 +79,7 @@ class ConfigManagerEntryResourceView(HomeAssistantView):
return self.json(result)
-class ConfigManagerFlowIndexView(HomeAssistantView):
+class ConfigManagerFlowIndexView(FlowManagerIndexView):
"""View to create config flows."""
url = '/api/config/config_entries/flow'
@@ -94,81 +95,16 @@ class ConfigManagerFlowIndexView(HomeAssistantView):
hass = request.app['hass']
return self.json([
- flow for flow in hass.config_entries.flow.async_progress()
- if flow['source'] != config_entries.SOURCE_USER])
-
- @RequestDataValidator(vol.Schema({
- vol.Required('domain'): str,
- }))
- @asyncio.coroutine
- def post(self, request, data):
- """Handle a POST request."""
- hass = request.app['hass']
-
- try:
- result = yield from hass.config_entries.flow.async_init(
- data['domain'])
- except config_entries.UnknownHandler:
- return self.json_message('Invalid handler specified', 404)
- except config_entries.UnknownStep:
- return self.json_message('Handler does not support init', 400)
-
- result = _prepare_json(result)
-
- return self.json(result)
+ flw for flw in hass.config_entries.flow.async_progress()
+ if flw['source'] != data_entry_flow.SOURCE_USER])
-class ConfigManagerFlowResourceView(HomeAssistantView):
+class ConfigManagerFlowResourceView(FlowManagerResourceView):
"""View to interact with the flow manager."""
url = '/api/config/config_entries/flow/{flow_id}'
name = 'api:config:config_entries:flow:resource'
- @asyncio.coroutine
- def get(self, request, flow_id):
- """Get the current state of a flow."""
- hass = request.app['hass']
-
- try:
- result = yield from hass.config_entries.flow.async_configure(
- flow_id)
- except config_entries.UnknownFlow:
- return self.json_message('Invalid flow specified', 404)
-
- result = _prepare_json(result)
-
- return self.json(result)
-
- @RequestDataValidator(vol.Schema(dict), allow_empty=True)
- @asyncio.coroutine
- def post(self, request, flow_id, data):
- """Handle a POST request."""
- hass = request.app['hass']
-
- try:
- result = yield from hass.config_entries.flow.async_configure(
- flow_id, data)
- except config_entries.UnknownFlow:
- return self.json_message('Invalid flow specified', 404)
- except vol.Invalid:
- return self.json_message('User input malformed', 400)
-
- result = _prepare_json(result)
-
- return self.json(result)
-
- @asyncio.coroutine
- def delete(self, request, flow_id):
- """Cancel a flow in progress."""
- hass = request.app['hass']
-
- try:
- hass.config_entries.flow.async_abort(flow_id)
- except config_entries.UnknownFlow:
- return self.json_message('Invalid flow specified', 404)
-
- return self.json_message('Flow aborted')
-
class ConfigManagerAvailableFlowView(HomeAssistantView):
"""View to query available flows."""
diff --git a/homeassistant/components/cover/opengarage.py b/homeassistant/components/cover/opengarage.py
index d68021d7db3..028a7a0c9fc 100644
--- a/homeassistant/components/cover/opengarage.py
+++ b/homeassistant/components/cover/opengarage.py
@@ -18,30 +18,31 @@ import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
-ATTR_DISTANCE_SENSOR = "distance_sensor"
-ATTR_DOOR_STATE = "door_state"
-ATTR_SIGNAL_STRENGTH = "wifi_signal"
+ATTR_DISTANCE_SENSOR = 'distance_sensor'
+ATTR_DOOR_STATE = 'door_state'
+ATTR_SIGNAL_STRENGTH = 'wifi_signal'
-CONF_DEVICEKEY = "device_key"
+CONF_DEVICE_ID = 'device_id'
+CONF_DEVICE_KEY = 'device_key'
DEFAULT_NAME = 'OpenGarage'
DEFAULT_PORT = 80
-STATE_CLOSING = "closing"
-STATE_OFFLINE = "offline"
-STATE_OPENING = "opening"
-STATE_STOPPED = "stopped"
+STATE_CLOSING = 'closing'
+STATE_OFFLINE = 'offline'
+STATE_OPENING = 'opening'
+STATE_STOPPED = 'stopped'
STATES_MAP = {
0: STATE_CLOSED,
- 1: STATE_OPEN
+ 1: STATE_OPEN,
}
COVER_SCHEMA = vol.Schema({
- vol.Required(CONF_DEVICEKEY): cv.string,
+ vol.Required(CONF_DEVICE_KEY): cv.string,
vol.Required(CONF_HOST): cv.string,
+ vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
- vol.Optional(CONF_NAME): cv.string
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@@ -50,7 +51,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
- """Set up OpenGarage covers."""
+ """Set up the OpenGarage covers."""
covers = []
devices = config.get(CONF_COVERS)
@@ -59,8 +60,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
CONF_NAME: device_config.get(CONF_NAME),
CONF_HOST: device_config.get(CONF_HOST),
CONF_PORT: device_config.get(CONF_PORT),
- "device_id": device_config.get(CONF_DEVICE, device_id),
- CONF_DEVICEKEY: device_config.get(CONF_DEVICEKEY)
+ CONF_DEVICE_ID: device_config.get(CONF_DEVICE, device_id),
+ CONF_DEVICE_KEY: device_config.get(CONF_DEVICE_KEY)
}
covers.append(OpenGarageCover(hass, args))
@@ -79,8 +80,8 @@ class OpenGarageCover(CoverDevice):
self.hass = hass
self._name = args[CONF_NAME]
self.device_id = args['device_id']
- self._devicekey = args[CONF_DEVICEKEY]
- self._state = STATE_UNKNOWN
+ self._device_key = args[CONF_DEVICE_KEY]
+ self._state = None
self._state_before_move = None
self.dist = None
self.signal = None
@@ -138,8 +139,8 @@ class OpenGarageCover(CoverDevice):
try:
status = self._get_status()
if self._name is None:
- if status["name"] is not None:
- self._name = status["name"]
+ if status['name'] is not None:
+ self._name = status['name']
state = STATES_MAP.get(status.get('door'), STATE_UNKNOWN)
if self._state_before_move is not None:
if self._state_before_move != state:
@@ -152,7 +153,7 @@ class OpenGarageCover(CoverDevice):
self.signal = status.get('rssi')
self.dist = status.get('dist')
self._available = True
- except (requests.exceptions.RequestException) as ex:
+ except requests.exceptions.RequestException as ex:
_LOGGER.error("Unable to connect to OpenGarage device: %(reason)s",
dict(reason=ex))
self._state = STATE_OFFLINE
@@ -166,15 +167,15 @@ class OpenGarageCover(CoverDevice):
def _push_button(self):
"""Send commands to API."""
url = '{}/cc?dkey={}&click=1'.format(
- self.opengarage_url, self._devicekey)
+ self.opengarage_url, self._device_key)
try:
response = requests.get(url, timeout=10).json()
- if response["result"] == 2:
- _LOGGER.error("Unable to control %s: device_key is incorrect.",
+ if response['result'] == 2:
+ _LOGGER.error("Unable to control %s: Device key is incorrect",
self._name)
self._state = self._state_before_move
self._state_before_move = None
- except (requests.exceptions.RequestException) as ex:
+ except requests.exceptions.RequestException as ex:
_LOGGER.error("Unable to connect to OpenGarage device: %(reason)s",
dict(reason=ex))
self._state = self._state_before_move
diff --git a/homeassistant/components/cover/tahoma.py b/homeassistant/components/cover/tahoma.py
index 6fb8e92e051..c99076de851 100644
--- a/homeassistant/components/cover/tahoma.py
+++ b/homeassistant/components/cover/tahoma.py
@@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
- """Set up Tahoma covers."""
+ """Set up the Tahoma covers."""
controller = hass.data[TAHOMA_DOMAIN]['controller']
devices = []
for device in hass.data[TAHOMA_DOMAIN]['devices']['cover']:
diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json
index 69165dbbbaf..7ea68af01c1 100644
--- a/homeassistant/components/deconz/.translations/en.json
+++ b/homeassistant/components/deconz/.translations/en.json
@@ -18,6 +18,7 @@
"no_key": "Couldn't get an API key"
},
"abort": {
+ "already_configured": "Bridge is already configured",
"no_bridges": "No deCONZ bridges discovered",
"one_instance_only": "Component only supports one deCONZ instance"
}
diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py
index 85ba271ec3a..064725eda95 100644
--- a/homeassistant/components/deconz/__init__.py
+++ b/homeassistant/components/deconz/__init__.py
@@ -4,28 +4,20 @@ Support for deCONZ devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/deconz/
"""
-import logging
-
import voluptuous as vol
-from homeassistant import config_entries
-from homeassistant.components.discovery import SERVICE_DECONZ
from homeassistant.const import (
CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
from homeassistant.core import callback
-from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers import discovery, aiohttp_client
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
-from homeassistant.util.json import load_json, save_json
+from homeassistant.helpers import (
+ aiohttp_client, discovery, config_validation as cv)
+from homeassistant.util.json import load_json
-REQUIREMENTS = ['pydeconz==35']
+# Loading the config flow file will register the flow
+from .config_flow import configured_hosts
+from .const import CONFIG_FILE, DATA_DECONZ_ID, DOMAIN, _LOGGER
-_LOGGER = logging.getLogger(__name__)
-
-DOMAIN = 'deconz'
-DATA_DECONZ_ID = 'deconz_entities'
-
-CONFIG_FILE = 'deconz.conf'
+REQUIREMENTS = ['pydeconz==36']
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
@@ -46,46 +38,38 @@ SERVICE_SCHEMA = vol.Schema({
})
-CONFIG_INSTRUCTIONS = """
-Unlock your deCONZ gateway to register with Home Assistant.
-
-1. [Go to deCONZ system settings](http://{}:{}/edit_system.html)
-2. Press "Unlock Gateway" button
-
-[deCONZ platform documentation](https://home-assistant.io/components/deconz/)
-"""
-
-
async def async_setup(hass, config):
- """Set up services and configuration for deCONZ component."""
- result = False
- config_file = await hass.async_add_job(
- load_json, hass.config.path(CONFIG_FILE))
-
- async def async_deconz_discovered(service, discovery_info):
- """Call when deCONZ gateway has been found."""
- deconz_config = {}
- deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST)
- deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT)
- await async_request_configuration(hass, config, deconz_config)
-
- if config_file:
- result = await async_setup_deconz(hass, config, config_file)
-
- if not result and DOMAIN in config and CONF_HOST in config[DOMAIN]:
- deconz_config = config[DOMAIN]
- if CONF_API_KEY in deconz_config:
- result = await async_setup_deconz(hass, config, deconz_config)
- else:
- await async_request_configuration(hass, config, deconz_config)
- return True
-
- if not result:
- discovery.async_listen(hass, SERVICE_DECONZ, async_deconz_discovered)
+ """Load configuration for deCONZ component.
+ Discovery has loaded the component if DOMAIN is not present in config.
+ """
+ if DOMAIN in config:
+ deconz_config = None
+ config_file = await hass.async_add_job(
+ load_json, hass.config.path(CONFIG_FILE))
+ if config_file:
+ deconz_config = config_file
+ elif CONF_HOST in config[DOMAIN]:
+ deconz_config = config[DOMAIN]
+ if deconz_config and not configured_hosts(hass):
+ hass.async_add_job(hass.config_entries.flow.async_init(
+ DOMAIN, source='import', data=deconz_config
+ ))
return True
+async def async_setup_entry(hass, entry):
+ """Set up a deCONZ bridge for a config entry."""
+ if DOMAIN in hass.data:
+ _LOGGER.error(
+ "Config entry failed since one deCONZ instance already exists")
+ return False
+ result = await async_setup_deconz(hass, None, entry.data)
+ if result:
+ return True
+ return False
+
+
async def async_setup_deconz(hass, config, deconz_config):
"""Set up a deCONZ session.
@@ -94,8 +78,8 @@ async def async_setup_deconz(hass, config, deconz_config):
"""
_LOGGER.debug("deCONZ config %s", deconz_config)
from pydeconz import DeconzSession
- websession = async_get_clientsession(hass)
- deconz = DeconzSession(hass.loop, websession, **deconz_config)
+ session = aiohttp_client.async_get_clientsession(hass)
+ deconz = DeconzSession(hass.loop, session, **deconz_config)
result = await deconz.async_load_parameters()
if result is False:
_LOGGER.error("Failed to communicate with deCONZ")
@@ -152,121 +136,3 @@ async def async_setup_deconz(hass, config, deconz_config):
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deconz_shutdown)
return True
-
-
-async def async_request_configuration(hass, config, deconz_config):
- """Request configuration steps from the user."""
- configurator = hass.components.configurator
-
- async def async_configuration_callback(data):
- """Set up actions to do when our configuration callback is called."""
- from pydeconz.utils import async_get_api_key
- websession = async_get_clientsession(hass)
- api_key = await async_get_api_key(websession, **deconz_config)
- if api_key:
- deconz_config[CONF_API_KEY] = api_key
- result = await async_setup_deconz(hass, config, deconz_config)
- if result:
- await hass.async_add_job(
- save_json, hass.config.path(CONFIG_FILE), deconz_config)
- configurator.async_request_done(request_id)
- return
- else:
- configurator.async_notify_errors(
- request_id, "Couldn't load configuration.")
- else:
- configurator.async_notify_errors(
- request_id, "Couldn't get an API key.")
- return
-
- instructions = CONFIG_INSTRUCTIONS.format(
- deconz_config[CONF_HOST], deconz_config[CONF_PORT])
-
- request_id = configurator.async_request_config(
- "deCONZ", async_configuration_callback,
- description=instructions,
- entity_picture="/static/images/logo_deconz.jpeg",
- submit_caption="I have unlocked the gateway",
- )
-
-
-@config_entries.HANDLERS.register(DOMAIN)
-class DeconzFlowHandler(config_entries.ConfigFlowHandler):
- """Handle a deCONZ config flow."""
-
- VERSION = 1
-
- def __init__(self):
- """Initialize the deCONZ flow."""
- self.bridges = []
- self.deconz_config = {}
-
- async def async_step_init(self, user_input=None):
- """Handle a flow start."""
- from pydeconz.utils import async_discovery
-
- if DOMAIN in self.hass.data:
- return self.async_abort(
- reason='one_instance_only'
- )
-
- if user_input is not None:
- for bridge in self.bridges:
- if bridge[CONF_HOST] == user_input[CONF_HOST]:
- self.deconz_config = bridge
- return await self.async_step_link()
-
- session = aiohttp_client.async_get_clientsession(self.hass)
- self.bridges = await async_discovery(session)
-
- if len(self.bridges) == 1:
- self.deconz_config = self.bridges[0]
- return await self.async_step_link()
- elif len(self.bridges) > 1:
- hosts = []
- for bridge in self.bridges:
- hosts.append(bridge[CONF_HOST])
- return self.async_show_form(
- step_id='init',
- data_schema=vol.Schema({
- vol.Required(CONF_HOST): vol.In(hosts)
- })
- )
-
- return self.async_abort(
- reason='no_bridges'
- )
-
- async def async_step_link(self, user_input=None):
- """Attempt to link with the deCONZ bridge."""
- from pydeconz.utils import async_get_api_key
- errors = {}
-
- if user_input is not None:
- session = aiohttp_client.async_get_clientsession(self.hass)
- api_key = await async_get_api_key(session, **self.deconz_config)
- if api_key:
- self.deconz_config[CONF_API_KEY] = api_key
- return self.async_create_entry(
- title='deCONZ',
- data=self.deconz_config
- )
- else:
- errors['base'] = 'no_key'
-
- return self.async_show_form(
- step_id='link',
- errors=errors,
- )
-
-
-async def async_setup_entry(hass, entry):
- """Set up a bridge for a config entry."""
- if DOMAIN in hass.data:
- _LOGGER.error(
- "Config entry failed since one deCONZ instance already exists")
- return False
- result = await async_setup_deconz(hass, None, entry.data)
- if result:
- return True
- return False
diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py
new file mode 100644
index 00000000000..e900782ea65
--- /dev/null
+++ b/homeassistant/components/deconz/config_flow.py
@@ -0,0 +1,139 @@
+"""Config flow to configure deCONZ component."""
+
+import voluptuous as vol
+
+from homeassistant import config_entries, data_entry_flow
+from homeassistant.core import callback
+from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
+from homeassistant.helpers import aiohttp_client
+from homeassistant.util.json import load_json
+
+from .const import CONFIG_FILE, DOMAIN
+
+
+@callback
+def configured_hosts(hass):
+ """Return a set of the configured hosts."""
+ return set(entry.data['host'] for entry
+ in hass.config_entries.async_entries(DOMAIN))
+
+
+@config_entries.HANDLERS.register(DOMAIN)
+class DeconzFlowHandler(data_entry_flow.FlowHandler):
+ """Handle a deCONZ config flow."""
+
+ VERSION = 1
+
+ def __init__(self):
+ """Initialize the deCONZ config flow."""
+ self.bridges = []
+ self.deconz_config = {}
+
+ async def async_step_init(self, user_input=None):
+ """Handle a deCONZ config flow start."""
+ from pydeconz.utils import async_discovery
+
+ if configured_hosts(self.hass):
+ return self.async_abort(reason='one_instance_only')
+
+ if user_input is not None:
+ for bridge in self.bridges:
+ if bridge[CONF_HOST] == user_input[CONF_HOST]:
+ self.deconz_config = bridge
+ return await self.async_step_link()
+
+ session = aiohttp_client.async_get_clientsession(self.hass)
+ self.bridges = await async_discovery(session)
+
+ if len(self.bridges) == 1:
+ self.deconz_config = self.bridges[0]
+ return await self.async_step_link()
+ elif len(self.bridges) > 1:
+ hosts = []
+ for bridge in self.bridges:
+ hosts.append(bridge[CONF_HOST])
+ return self.async_show_form(
+ step_id='init',
+ data_schema=vol.Schema({
+ vol.Required(CONF_HOST): vol.In(hosts)
+ })
+ )
+
+ return self.async_abort(
+ reason='no_bridges'
+ )
+
+ async def async_step_link(self, user_input=None):
+ """Attempt to link with the deCONZ bridge."""
+ from pydeconz.utils import async_get_api_key, async_get_bridgeid
+ errors = {}
+
+ if user_input is not None:
+ if configured_hosts(self.hass):
+ return self.async_abort(reason='one_instance_only')
+ session = aiohttp_client.async_get_clientsession(self.hass)
+ api_key = await async_get_api_key(session, **self.deconz_config)
+ if api_key:
+ self.deconz_config[CONF_API_KEY] = api_key
+ if 'bridgeid' not in self.deconz_config:
+ self.deconz_config['bridgeid'] = await async_get_bridgeid(
+ session, **self.deconz_config)
+ return self.async_create_entry(
+ title='deCONZ-' + self.deconz_config['bridgeid'],
+ data=self.deconz_config
+ )
+ errors['base'] = 'no_key'
+
+ return self.async_show_form(
+ step_id='link',
+ errors=errors,
+ )
+
+ async def async_step_discovery(self, discovery_info):
+ """Prepare configuration for a discovered deCONZ bridge.
+
+ This flow is triggered by the discovery component.
+ """
+ deconz_config = {}
+ deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST)
+ deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT)
+ deconz_config['bridgeid'] = discovery_info.get('serial')
+
+ config_file = await self.hass.async_add_job(
+ load_json, self.hass.config.path(CONFIG_FILE))
+ if config_file and \
+ config_file[CONF_HOST] == deconz_config[CONF_HOST] and \
+ CONF_API_KEY in config_file:
+ deconz_config[CONF_API_KEY] = config_file[CONF_API_KEY]
+
+ return await self.async_step_import(deconz_config)
+
+ async def async_step_import(self, import_config):
+ """Import a deCONZ bridge as a config entry.
+
+ This flow is triggered by `async_setup` for configured bridges.
+ This flow is also triggered by `async_step_discovery`.
+
+ This will execute for any bridge that does not have a
+ config entry yet (based on host).
+
+ If an API key is provided, we will create an entry.
+ Otherwise we will delegate to `link` step which
+ will ask user to link the bridge.
+ """
+ from pydeconz.utils import async_get_bridgeid
+
+ if configured_hosts(self.hass):
+ return self.async_abort(reason='one_instance_only')
+ elif CONF_API_KEY not in import_config:
+ self.deconz_config = import_config
+ return await self.async_step_link()
+
+ if 'bridgeid' not in import_config:
+ session = aiohttp_client.async_get_clientsession(self.hass)
+ import_config['bridgeid'] = await async_get_bridgeid(
+ session, **import_config)
+ return self.async_create_entry(
+ title='deCONZ-' + import_config['bridgeid'],
+ data=import_config
+ )
diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py
new file mode 100644
index 00000000000..c5820c971f6
--- /dev/null
+++ b/homeassistant/components/deconz/const.py
@@ -0,0 +1,8 @@
+"""Constants for the deCONZ component."""
+import logging
+
+_LOGGER = logging.getLogger('homeassistant.components.deconz')
+
+DOMAIN = 'deconz'
+CONFIG_FILE = 'deconz.conf'
+DATA_DECONZ_ID = 'deconz_entities'
diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json
index 69165dbbbaf..7ea68af01c1 100644
--- a/homeassistant/components/deconz/strings.json
+++ b/homeassistant/components/deconz/strings.json
@@ -18,6 +18,7 @@
"no_key": "Couldn't get an API key"
},
"abort": {
+ "already_configured": "Bridge is already configured",
"no_bridges": "No deCONZ bridges discovered",
"one_instance_only": "Component only supports one deCONZ instance"
}
diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py
index 45f0e51a214..b24f7784faf 100644
--- a/homeassistant/components/device_tracker/__init__.py
+++ b/homeassistant/components/device_tracker/__init__.py
@@ -605,6 +605,17 @@ class DeviceScanner(object):
"""
return self.hass.async_add_job(self.get_device_name, device)
+ def get_extra_attributes(self, device: str) -> dict:
+ """Get the extra attributes of a device."""
+ raise NotImplementedError()
+
+ def async_get_extra_attributes(self, device: str) -> Any:
+ """Get the extra attributes of a device.
+
+ This method must be run in the event loop and returns a coroutine.
+ """
+ return self.hass.async_add_job(self.get_extra_attributes, device)
+
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
"""Load devices from YAML configuration file."""
@@ -690,10 +701,20 @@ def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
host_name = yield from scanner.async_get_device_name(mac)
seen.add(mac)
+ try:
+ extra_attributes = (yield from
+ scanner.async_get_extra_attributes(mac))
+ except NotImplementedError:
+ extra_attributes = dict()
+
kwargs = {
'mac': mac,
'host_name': host_name,
- 'source_type': SOURCE_TYPE_ROUTER
+ 'source_type': SOURCE_TYPE_ROUTER,
+ 'attributes': {
+ 'scanner': scanner.__class__.__name__,
+ **extra_attributes
+ }
}
zone_home = hass.states.get(zone.ENTITY_ID_HOME)
diff --git a/homeassistant/components/device_tracker/bmw_connected_drive.py b/homeassistant/components/device_tracker/bmw_connected_drive.py
index 2267bb51944..f36afc622ee 100644
--- a/homeassistant/components/device_tracker/bmw_connected_drive.py
+++ b/homeassistant/components/device_tracker/bmw_connected_drive.py
@@ -48,8 +48,11 @@ class BMWDeviceTracker(object):
return
_LOGGER.debug('Updating %s', dev_id)
-
+ attrs = {
+ 'vin': self.vehicle.vin,
+ }
self._see(
dev_id=dev_id, host_name=self.vehicle.name,
- gps=self.vehicle.state.gps_position, icon='mdi:car'
+ gps=self.vehicle.state.gps_position, attributes=attrs,
+ icon='mdi:car'
)
diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py
index 9e257616361..d1e59293365 100644
--- a/homeassistant/components/device_tracker/google_maps.py
+++ b/homeassistant/components/device_tracker/google_maps.py
@@ -19,7 +19,7 @@ from homeassistant.util import slugify
_LOGGER = logging.getLogger(__name__)
-REQUIREMENTS = ['locationsharinglib==0.4.0']
+REQUIREMENTS = ['locationsharinglib==1.2.1']
CREDENTIALS_FILE = '.google_maps_location_sharing.cookies'
diff --git a/homeassistant/components/device_tracker/mikrotik.py b/homeassistant/components/device_tracker/mikrotik.py
index 154fc3d2a63..a6a67749f76 100644
--- a/homeassistant/components/device_tracker/mikrotik.py
+++ b/homeassistant/components/device_tracker/mikrotik.py
@@ -176,7 +176,7 @@ class MikrotikScanner(DeviceScanner):
for device in device_names
if device.get('mac-address')}
- if self.wireless_exist:
+ if self.wireless_exist or self.capsman_exist:
self.last_results = {
device.get('mac-address'):
mac_names.get(device.get('mac-address'))
diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py
index 23cb7ea8f9d..3c090e8cd3b 100644
--- a/homeassistant/components/device_tracker/nmap_tracker.py
+++ b/homeassistant/components/device_tracker/nmap_tracker.py
@@ -80,6 +80,8 @@ class NmapDeviceScanner(DeviceScanner):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
+ _LOGGER.debug("Nmap last results %s", self.last_results)
+
return [device.mac for device in self.last_results]
def get_device_name(self, device):
@@ -91,6 +93,13 @@ class NmapDeviceScanner(DeviceScanner):
return filter_named[0]
return None
+ def get_extra_attributes(self, device):
+ """Return the IP of the given device."""
+ filter_ip = next((
+ result.ip for result in self.last_results
+ if result.mac == device), None)
+ return {'ip': filter_ip}
+
def _update_info(self):
"""Scan the network for devices.
diff --git a/homeassistant/components/device_tracker/ubus.py b/homeassistant/components/device_tracker/ubus.py
index dd12df7b070..3d7ef5cef6e 100644
--- a/homeassistant/components/device_tracker/ubus.py
+++ b/homeassistant/components/device_tracker/ubus.py
@@ -103,6 +103,9 @@ class UbusDeviceScanner(DeviceScanner):
"""Return the name of the given device or None if we don't know."""
if self.mac2name is None:
self._generate_mac2name()
+ if self.mac2name is None:
+ # Generation of mac2name dictionary failed
+ return None
name = self.mac2name.get(device.upper(), None)
return name
diff --git a/homeassistant/components/device_tracker/unifi.py b/homeassistant/components/device_tracker/unifi.py
index d8a52aaaeb4..b7efe65dd01 100644
--- a/homeassistant/components/device_tracker/unifi.py
+++ b/homeassistant/components/device_tracker/unifi.py
@@ -122,3 +122,9 @@ class UnifiScanner(DeviceScanner):
name = client.get('name') or client.get('hostname')
_LOGGER.debug("Device mac %s name %s", device, name)
return name
+
+ def get_extra_attributes(self, device):
+ """Return the extra attributes of the device."""
+ client = self._clients.get(device, {})
+ _LOGGER.debug("Device mac %s attributes %s", device, client)
+ return client
diff --git a/homeassistant/components/device_tracker/xiaomi_miio.py b/homeassistant/components/device_tracker/xiaomi_miio.py
index 61568892388..c5769253657 100644
--- a/homeassistant/components/device_tracker/xiaomi_miio.py
+++ b/homeassistant/components/device_tracker/xiaomi_miio.py
@@ -20,7 +20,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
})
-REQUIREMENTS = ['python-miio==0.3.9']
+REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
def get_scanner(hass, config):
@@ -41,7 +41,7 @@ def get_scanner(hass, config):
device_info.model,
device_info.firmware_version,
device_info.hardware_version)
- scanner = XiaomiMiioDeviceScanner(hass, device)
+ scanner = XiaomiMiioDeviceScanner(device)
except DeviceException as ex:
_LOGGER.error("Device unavailable or token incorrect: %s", ex)
@@ -51,7 +51,7 @@ def get_scanner(hass, config):
class XiaomiMiioDeviceScanner(DeviceScanner):
"""This class queries a Xiaomi Mi WiFi Repeater."""
- def __init__(self, hass, device):
+ def __init__(self, device):
"""Initialize the scanner."""
self.device = device
diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py
index 01ef36b778b..f0ebcba8366 100644
--- a/homeassistant/components/discovery.py
+++ b/homeassistant/components/discovery.py
@@ -13,7 +13,7 @@ import os
import voluptuous as vol
-from homeassistant import config_entries
+from homeassistant import data_entry_flow
from homeassistant.core import callback
from homeassistant.const import EVENT_HOMEASSISTANT_START
import homeassistant.helpers.config_validation as cv
@@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.discovery import async_load_platform, async_discover
import homeassistant.util.dt as dt_util
-REQUIREMENTS = ['netdisco==1.3.0']
+REQUIREMENTS = ['netdisco==1.3.1']
DOMAIN = 'discovery'
@@ -40,8 +40,10 @@ SERVICE_HUE = 'philips_hue'
SERVICE_DECONZ = 'deconz'
SERVICE_DAIKIN = 'daikin'
SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
+SERVICE_HOMEKIT = 'homekit'
CONFIG_ENTRY_HANDLERS = {
+ SERVICE_DECONZ: 'deconz',
SERVICE_HUE: 'hue',
}
@@ -56,7 +58,6 @@ SERVICE_HANDLERS = {
SERVICE_WINK: ('wink', None),
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
SERVICE_TELLDUSLIVE: ('tellduslive', None),
- SERVICE_DECONZ: ('deconz', None),
SERVICE_DAIKIN: ('daikin', None),
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
'google_cast': ('media_player', 'cast'),
@@ -77,15 +78,23 @@ SERVICE_HANDLERS = {
'bose_soundtouch': ('media_player', 'soundtouch'),
'bluesound': ('media_player', 'bluesound'),
'songpal': ('media_player', 'songpal'),
+ 'kodi': ('media_player', 'kodi'),
+}
+
+OPTIONAL_SERVICE_HANDLERS = {
+ SERVICE_HOMEKIT: ('homekit_controller', None),
}
CONF_IGNORE = 'ignore'
+CONF_ENABLE = 'enable'
CONFIG_SCHEMA = vol.Schema({
vol.Required(DOMAIN): vol.Schema({
vol.Optional(CONF_IGNORE, default=[]):
vol.All(cv.ensure_list, [
- vol.In(list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS))])
+ vol.In(list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS))]),
+ vol.Optional(CONF_ENABLE, default=[]):
+ vol.All(cv.ensure_list, [vol.In(OPTIONAL_SERVICE_HANDLERS)])
}),
}, extra=vol.ALLOW_EXTRA)
@@ -104,6 +113,9 @@ async def async_setup(hass, config):
# Platforms ignore by config
ignored_platforms = config[DOMAIN][CONF_IGNORE]
+ # Optional platforms enabled by config
+ enabled_platforms = config[DOMAIN][CONF_ENABLE]
+
async def new_service_found(service, info):
"""Handle a new service if one is found."""
if service in ignored_platforms:
@@ -119,13 +131,16 @@ async def async_setup(hass, config):
if service in CONFIG_ENTRY_HANDLERS:
await hass.config_entries.flow.async_init(
CONFIG_ENTRY_HANDLERS[service],
- source=config_entries.SOURCE_DISCOVERY,
+ source=data_entry_flow.SOURCE_DISCOVERY,
data=info
)
return
comp_plat = SERVICE_HANDLERS.get(service)
+ if not comp_plat and service in enabled_platforms:
+ comp_plat = OPTIONAL_SERVICE_HANDLERS[service]
+
# We do not know how to handle this service.
if not comp_plat:
logger.info("Unknown service discovered: %s %s", service, info)
diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py
index d1503dc74dc..9c29cea704c 100644
--- a/homeassistant/components/ecobee.py
+++ b/homeassistant/components/ecobee.py
@@ -16,7 +16,7 @@ from homeassistant.const import CONF_API_KEY
from homeassistant.util import Throttle
from homeassistant.util.json import save_json
-REQUIREMENTS = ['python-ecobee-api==0.0.17']
+REQUIREMENTS = ['python-ecobee-api==0.0.18']
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/eufy.py b/homeassistant/components/eufy.py
new file mode 100644
index 00000000000..733aa0adbfe
--- /dev/null
+++ b/homeassistant/components/eufy.py
@@ -0,0 +1,77 @@
+"""
+Support for Eufy devices.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/eufy/
+"""
+import logging
+
+import voluptuous as vol
+
+from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ADDRESS, \
+ CONF_DEVICES, CONF_USERNAME, CONF_PASSWORD, CONF_TYPE, CONF_NAME
+from homeassistant.helpers import discovery
+
+import homeassistant.helpers.config_validation as cv
+
+
+REQUIREMENTS = ['lakeside==0.5']
+
+_LOGGER = logging.getLogger(__name__)
+
+DOMAIN = 'eufy'
+
+DEVICE_SCHEMA = vol.Schema({
+ vol.Required(CONF_ADDRESS): cv.string,
+ vol.Required(CONF_ACCESS_TOKEN): cv.string,
+ vol.Required(CONF_TYPE): cv.string,
+ vol.Optional(CONF_NAME): cv.string
+})
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.Schema({
+ vol.Optional(CONF_DEVICES, default=[]): vol.All(cv.ensure_list,
+ [DEVICE_SCHEMA]),
+ vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
+ vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
+ }),
+}, extra=vol.ALLOW_EXTRA)
+
+EUFY_DISPATCH = {
+ 'T1011': 'light',
+ 'T1012': 'light',
+ 'T1013': 'light',
+ 'T1201': 'switch',
+ 'T1202': 'switch',
+ 'T1211': 'switch'
+}
+
+
+def setup(hass, config):
+ """Set up Eufy devices."""
+ # pylint: disable=import-error
+ import lakeside
+
+ if CONF_USERNAME in config[DOMAIN] and CONF_PASSWORD in config[DOMAIN]:
+ data = lakeside.get_devices(config[DOMAIN][CONF_USERNAME],
+ config[DOMAIN][CONF_PASSWORD])
+ for device in data:
+ kind = device['type']
+ if kind not in EUFY_DISPATCH:
+ continue
+ discovery.load_platform(hass, EUFY_DISPATCH[kind], DOMAIN, device,
+ config)
+
+ for device_info in config[DOMAIN][CONF_DEVICES]:
+ kind = device_info['type']
+ if kind not in EUFY_DISPATCH:
+ continue
+ device = {}
+ device['address'] = device_info['address']
+ device['code'] = device_info['access_token']
+ device['type'] = device_info['type']
+ device['name'] = device_info['name']
+ discovery.load_platform(hass, EUFY_DISPATCH[kind], DOMAIN, device,
+ config)
+
+ return True
diff --git a/homeassistant/components/fritzbox.py b/homeassistant/components/fritzbox.py
new file mode 100755
index 00000000000..a3c35aaa597
--- /dev/null
+++ b/homeassistant/components/fritzbox.py
@@ -0,0 +1,83 @@
+"""
+Support for AVM Fritz!Box smarthome devices.
+
+For more details about this component, please refer to the documentation at
+http://home-assistant.io/components/fritzbox/
+"""
+import logging
+
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.const import (
+ CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME,
+ EVENT_HOMEASSISTANT_STOP)
+from homeassistant.helpers import discovery
+
+_LOGGER = logging.getLogger(__name__)
+
+REQUIREMENTS = ['pyfritzhome==0.3.7']
+
+SUPPORTED_DOMAINS = ['climate', 'switch']
+
+DOMAIN = 'fritzbox'
+
+ATTR_STATE_DEVICE_LOCKED = 'device_locked'
+ATTR_STATE_LOCKED = 'locked'
+ATTR_STATE_BATTERY_LOW = 'battery_low'
+
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.Schema({
+ vol.Required(CONF_DEVICES):
+ vol.All(cv.ensure_list, [
+ vol.Schema({
+ vol.Required(CONF_HOST): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ vol.Required(CONF_USERNAME): cv.string,
+ }),
+ ]),
+ })
+}, extra=vol.ALLOW_EXTRA)
+
+
+def setup(hass, config):
+ """Set up the fritzbox component."""
+ from pyfritzhome import Fritzhome, LoginError
+
+ fritz_list = []
+
+ configured_devices = config[DOMAIN].get(CONF_DEVICES)
+ for device in configured_devices:
+ host = device.get(CONF_HOST)
+ username = device.get(CONF_USERNAME)
+ password = device.get(CONF_PASSWORD)
+ fritzbox = Fritzhome(host=host, user=username,
+ password=password)
+ try:
+ fritzbox.login()
+ _LOGGER.info("Connected to device %s", device)
+ except LoginError:
+ _LOGGER.warning("Login to Fritz!Box %s as %s failed",
+ host, username)
+ continue
+
+ fritz_list.append(fritzbox)
+
+ if not fritz_list:
+ _LOGGER.info("No fritzboxes configured")
+ return False
+
+ hass.data[DOMAIN] = fritz_list
+
+ def logout_fritzboxes(event):
+ """Close all connections to the fritzboxes."""
+ for fritz in fritz_list:
+ fritz.logout()
+
+ hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, logout_fritzboxes)
+
+ for domain in SUPPORTED_DOMAINS:
+ discovery.load_platform(hass, domain, DOMAIN, {}, config)
+
+ return True
diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py
index 3fc3eff0a14..87ca8bd2a28 100644
--- a/homeassistant/components/frontend/__init__.py
+++ b/homeassistant/components/frontend/__init__.py
@@ -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==20180404.0']
+REQUIREMENTS = ['home-assistant-frontend==20180420.0']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
diff --git a/homeassistant/components/google.py b/homeassistant/components/google.py
index 30151ee1a56..b41d4ea33a2 100644
--- a/homeassistant/components/google.py
+++ b/homeassistant/components/google.py
@@ -44,6 +44,7 @@ CONF_ENTITIES = 'entities'
CONF_TRACK = 'track'
CONF_SEARCH = 'search'
CONF_OFFSET = 'offset'
+CONF_IGNORE_AVAILABILITY = 'ignore_availability'
DEFAULT_CONF_TRACK_NEW = True
DEFAULT_CONF_OFFSET = '!!'
@@ -74,8 +75,9 @@ _SINGLE_CALSEARCH_CONFIG = vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_DEVICE_ID): cv.string,
vol.Optional(CONF_TRACK): cv.boolean,
- vol.Optional(CONF_SEARCH): vol.Any(cv.string, None),
+ vol.Optional(CONF_SEARCH): cv.string,
vol.Optional(CONF_OFFSET): cv.string,
+ vol.Optional(CONF_IGNORE_AVAILABILITY, default=True): cv.boolean,
})
DEVICE_SCHEMA = vol.Schema({
diff --git a/homeassistant/components/hdmi_cec.py b/homeassistant/components/hdmi_cec.py
index 8e2464d0922..b5d64f48dc7 100644
--- a/homeassistant/components/hdmi_cec.py
+++ b/homeassistant/components/hdmi_cec.py
@@ -35,7 +35,7 @@ CONF_TYPES = 'types'
ICON_UNKNOWN = 'mdi:help'
ICON_AUDIO = 'mdi:speaker'
ICON_PLAYER = 'mdi:play'
-ICON_TUNER = 'mdi:nest-thermostat'
+ICON_TUNER = 'mdi:radio'
ICON_RECORDER = 'mdi:microphone'
ICON_TV = 'mdi:television'
ICONS_BY_TYPE = {
diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py
index 8ab91b08a3d..b5ac37b1451 100644
--- a/homeassistant/components/history.py
+++ b/homeassistant/components/history.py
@@ -118,6 +118,30 @@ def state_changes_during_period(hass, start_time, end_time=None,
return states_to_json(hass, states, start_time, entity_ids)
+def get_last_state_changes(hass, number_of_states, entity_id):
+ """Return the last number_of_states."""
+ from homeassistant.components.recorder.models import States
+
+ start_time = dt_util.utcnow()
+
+ with session_scope(hass=hass) as session:
+ query = session.query(States).filter(
+ (States.last_changed == States.last_updated))
+
+ if entity_id is not None:
+ query = query.filter_by(entity_id=entity_id.lower())
+
+ entity_ids = [entity_id] if entity_id is not None else None
+
+ states = execute(
+ query.order_by(States.last_updated.desc()).limit(number_of_states))
+
+ return states_to_json(hass, reversed(states),
+ start_time,
+ entity_ids,
+ include_start_time_state=False)
+
+
def get_states(hass, utc_point_in_time, entity_ids=None, run=None,
filters=None):
"""Return the states at a specific point in time."""
diff --git a/homeassistant/components/hive.py b/homeassistant/components/hive.py
index abe52ebe98a..aa662fc2fb6 100644
--- a/homeassistant/components/hive.py
+++ b/homeassistant/components/hive.py
@@ -12,7 +12,7 @@ from homeassistant.const import (CONF_PASSWORD, CONF_SCAN_INTERVAL,
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform
-REQUIREMENTS = ['pyhiveapi==0.2.11']
+REQUIREMENTS = ['pyhiveapi==0.2.14']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'hive'
@@ -44,6 +44,8 @@ class HiveSession:
light = None
sensor = None
switch = None
+ weather = None
+ attributes = None
def setup(hass, config):
@@ -70,6 +72,8 @@ def setup(hass, config):
session.hotwater = Pyhiveapi.Hotwater()
session.light = Pyhiveapi.Light()
session.switch = Pyhiveapi.Switch()
+ session.weather = Pyhiveapi.Weather()
+ session.attributes = Pyhiveapi.Attributes()
hass.data[DATA_HIVE] = session
for ha_type, hive_type in DEVICETYPES.items():
diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py
index 948e26be291..24c6dfa8a76 100644
--- a/homeassistant/components/homekit/__init__.py
+++ b/homeassistant/components/homekit/__init__.py
@@ -8,12 +8,11 @@ from zlib import adler32
import voluptuous as vol
-from homeassistant.components.climate import (
- SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
-from homeassistant.components.cover import SUPPORT_SET_POSITION
+from homeassistant.components.cover import (
+ SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION)
from homeassistant.const import (
- ATTR_CODE, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
- CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT,
+ ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
+ ATTR_DEVICE_CLASS, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import FILTER_SCHEMA
@@ -21,14 +20,16 @@ from homeassistant.util import get_local_ip
from homeassistant.util.decorator import Registry
from .const import (
DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER,
- DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START)
+ DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START,
+ DEVICE_CLASS_CO2, DEVICE_CLASS_LIGHT, DEVICE_CLASS_HUMIDITY,
+ DEVICE_CLASS_PM25, DEVICE_CLASS_TEMPERATURE)
from .util import (
validate_entity_config, show_setup_message)
TYPES = Registry()
_LOGGER = logging.getLogger(__name__)
-REQUIREMENTS = ['HAP-python==1.1.8']
+REQUIREMENTS = ['HAP-python==1.1.9']
CONFIG_SCHEMA = vol.Schema({
@@ -79,55 +80,64 @@ def get_accessory(hass, state, aid, config):
state.entity_id)
return None
- if state.domain == 'sensor':
- unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
- if unit == TEMP_CELSIUS or unit == TEMP_FAHRENHEIT:
- _LOGGER.debug('Add "%s" as "%s"',
- state.entity_id, 'TemperatureSensor')
- return TYPES['TemperatureSensor'](hass, state.entity_id,
- state.name, aid=aid)
- elif unit == '%':
- _LOGGER.debug('Add "%s" as %s"',
- state.entity_id, 'HumiditySensor')
- return TYPES['HumiditySensor'](hass, state.entity_id, state.name,
- aid=aid)
+ a_type = None
+ config = config or {}
- elif state.domain == 'cover':
- # Only add covers that support set_cover_position
- features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
- if features & SUPPORT_SET_POSITION:
- _LOGGER.debug('Add "%s" as "%s"',
- state.entity_id, 'WindowCovering')
- return TYPES['WindowCovering'](hass, state.entity_id, state.name,
- aid=aid)
+ if state.domain == 'alarm_control_panel':
+ a_type = 'SecuritySystem'
- elif state.domain == 'alarm_control_panel':
- _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'SecuritySystem')
- return TYPES['SecuritySystem'](hass, state.entity_id, state.name,
- alarm_code=config.get(ATTR_CODE),
- aid=aid)
+ elif state.domain == 'binary_sensor' or state.domain == 'device_tracker':
+ a_type = 'BinarySensor'
elif state.domain == 'climate':
- features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
- support_temp_range = SUPPORT_TARGET_TEMPERATURE_LOW | \
- SUPPORT_TARGET_TEMPERATURE_HIGH
- # Check if climate device supports auto mode
- support_auto = bool(features & support_temp_range)
+ a_type = 'Thermostat'
- _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Thermostat')
- return TYPES['Thermostat'](hass, state.entity_id,
- state.name, support_auto, aid=aid)
+ elif state.domain == 'cover':
+ features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
+ device_class = state.attributes.get(ATTR_DEVICE_CLASS)
+
+ if device_class == 'garage' and \
+ features & (SUPPORT_OPEN | SUPPORT_CLOSE):
+ a_type = 'GarageDoorOpener'
+ elif features & SUPPORT_SET_POSITION:
+ a_type = 'WindowCovering'
+ elif features & (SUPPORT_OPEN | SUPPORT_CLOSE):
+ a_type = 'WindowCoveringBasic'
elif state.domain == 'light':
- _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Light')
- return TYPES['Light'](hass, state.entity_id, state.name, aid=aid)
+ a_type = 'Light'
+
+ elif state.domain == 'lock':
+ a_type = 'Lock'
+
+ elif state.domain == 'sensor':
+ unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
+ device_class = state.attributes.get(ATTR_DEVICE_CLASS)
+
+ if device_class == DEVICE_CLASS_TEMPERATURE or unit == TEMP_CELSIUS \
+ or unit == TEMP_FAHRENHEIT:
+ a_type = 'TemperatureSensor'
+ elif device_class == DEVICE_CLASS_HUMIDITY or unit == '%':
+ a_type = 'HumiditySensor'
+ elif device_class == DEVICE_CLASS_PM25 \
+ or DEVICE_CLASS_PM25 in state.entity_id:
+ a_type = 'AirQualitySensor'
+ elif device_class == DEVICE_CLASS_CO2 \
+ or DEVICE_CLASS_CO2 in state.entity_id:
+ a_type = 'CarbonDioxideSensor'
+ elif device_class == DEVICE_CLASS_LIGHT or unit == 'lm' or \
+ unit == 'lux':
+ a_type = 'LightSensor'
elif state.domain == 'switch' or state.domain == 'remote' \
or state.domain == 'input_boolean' or state.domain == 'script':
- _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Switch')
- return TYPES['Switch'](hass, state.entity_id, state.name, aid=aid)
+ a_type = 'Switch'
- return None
+ if a_type is None:
+ return None
+
+ _LOGGER.debug('Add "%s" as "%s"', state.entity_id, a_type)
+ return TYPES[a_type](hass, state.name, state.entity_id, aid, config=config)
def generate_aid(entity_id):
@@ -143,7 +153,7 @@ class HomeKit():
def __init__(self, hass, port, entity_filter, entity_config):
"""Initialize a HomeKit object."""
- self._hass = hass
+ self.hass = hass
self._port = port
self._filter = entity_filter
self._config = entity_config
@@ -156,11 +166,11 @@ class HomeKit():
"""Setup bridge and accessory driver."""
from .accessories import HomeBridge, HomeDriver
- self._hass.bus.async_listen_once(
+ self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, self.stop)
- path = self._hass.config.path(HOMEKIT_FILE)
- self.bridge = HomeBridge(self._hass)
+ path = self.hass.config.path(HOMEKIT_FILE)
+ self.bridge = HomeBridge(self.hass)
self.driver = HomeDriver(self.bridge, self._port, get_local_ip(), path)
def add_bridge_accessory(self, state):
@@ -169,7 +179,7 @@ class HomeKit():
return
aid = generate_aid(state.entity_id)
conf = self._config.pop(state.entity_id, {})
- acc = get_accessory(self._hass, state, aid, conf)
+ acc = get_accessory(self.hass, state, aid, conf)
if acc is not None:
self.bridge.add_accessory(acc)
@@ -181,15 +191,15 @@ class HomeKit():
# pylint: disable=unused-variable
from . import ( # noqa F401
- type_covers, type_lights, type_security_systems, type_sensors,
- type_switches, type_thermostats)
+ type_covers, type_lights, type_locks, type_security_systems,
+ type_sensors, type_switches, type_thermostats)
- for state in self._hass.states.all():
+ for state in self.hass.states.all():
self.add_bridge_accessory(state)
self.bridge.set_broker(self.driver)
if not self.bridge.paired:
- show_setup_message(self.bridge, self._hass)
+ show_setup_message(self.hass, self.bridge)
_LOGGER.debug('Driver start')
self.driver.start()
diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py
index da45bee9e90..d9b90a77d68 100644
--- a/homeassistant/components/homekit/accessories.py
+++ b/homeassistant/components/homekit/accessories.py
@@ -1,21 +1,64 @@
"""Extend the basic Accessory and Bridge functions."""
+from datetime import timedelta
+from functools import wraps
+from inspect import getmodule
import logging
from pyhap.accessory import Accessory, Bridge, Category
from pyhap.accessory_driver import AccessoryDriver
-from homeassistant.helpers.event import async_track_state_change
+from homeassistant.core import callback as ha_callback
+from homeassistant.helpers.event import (
+ async_track_state_change, track_point_in_utc_time)
+from homeassistant.util import dt as dt_util
from .const import (
- ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME,
- MANUFACTURER, SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, CHAR_MODEL,
- CHAR_NAME, CHAR_SERIAL_NUMBER)
+ DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME, MANUFACTURER,
+ SERV_ACCESSORY_INFO, CHAR_MANUFACTURER,
+ CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER)
from .util import (
show_setup_message, dismiss_setup_message)
_LOGGER = logging.getLogger(__name__)
+def debounce(func):
+ """Decorator function. Debounce callbacks form HomeKit."""
+ @ha_callback
+ def call_later_listener(*args):
+ """Callback listener called from call_later."""
+ # pylint: disable=unsubscriptable-object
+ nonlocal lastargs, remove_listener
+ hass = lastargs['hass']
+ hass.async_add_job(func, *lastargs['args'])
+ lastargs = remove_listener = None
+
+ @wraps(func)
+ def wrapper(*args):
+ """Wrapper starts async timer.
+
+ The accessory must have 'self.hass' and 'self.entity_id' as attributes.
+ """
+ # pylint: disable=not-callable
+ hass = args[0].hass
+ nonlocal lastargs, remove_listener
+ if remove_listener:
+ remove_listener()
+ lastargs = remove_listener = None
+ lastargs = {'hass': hass, 'args': [*args]}
+ remove_listener = track_point_in_utc_time(
+ hass, call_later_listener,
+ dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT))
+ logger.debug('%s: Start %s timeout', args[0].entity_id,
+ func.__name__.replace('set_', ''))
+
+ remove_listener = None
+ lastargs = None
+ name = getmodule(func).__name__
+ logger = logging.getLogger(name)
+ return wrapper
+
+
def add_preload_service(acc, service, chars=None):
"""Define and return a service to be available for the accessory."""
from pyhap.loader import get_serv_loader, get_char_loader
@@ -29,6 +72,18 @@ def add_preload_service(acc, service, chars=None):
return service
+def setup_char(char_name, service, value=None, properties=None, callback=None):
+ """Helper function to return fully configured characteristic."""
+ char = service.get_characteristic(char_name)
+ if value:
+ char.value = value
+ if properties:
+ char.override_properties(properties)
+ if callback:
+ char.setter_callback = callback
+ return char
+
+
def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER,
serial_number='0000'):
"""Set the default accessory information."""
@@ -42,14 +97,13 @@ def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER,
class HomeAccessory(Accessory):
"""Adapter class for Accessory."""
- # pylint: disable=no-member
-
- def __init__(self, name=ACCESSORY_NAME, model=ACCESSORY_MODEL,
- category='OTHER', **kwargs):
+ def __init__(self, hass, name, entity_id, aid, category):
"""Initialize a Accessory object."""
- super().__init__(name, **kwargs)
- set_accessory_info(self, name, model)
+ super().__init__(name, aid=aid)
+ set_accessory_info(self, name, model=entity_id)
self.category = getattr(Category, category, Category.OTHER)
+ self.entity_id = entity_id
+ self.hass = hass
def _set_services(self):
add_preload_service(self, SERV_ACCESSORY_INFO)
@@ -57,19 +111,33 @@ class HomeAccessory(Accessory):
def run(self):
"""Method called by accessory after driver is started."""
state = self.hass.states.get(self.entity_id)
- self.update_state(new_state=state)
+ self.update_state_callback(new_state=state)
async_track_state_change(
- self.hass, self.entity_id, self.update_state)
+ self.hass, self.entity_id, self.update_state_callback)
+
+ def update_state_callback(self, entity_id=None, old_state=None,
+ new_state=None):
+ """Callback from state change listener."""
+ _LOGGER.debug('New_state: %s', new_state)
+ if new_state is None:
+ return
+ self.update_state(new_state)
+
+ def update_state(self, new_state):
+ """Method called on state change to update HomeKit value.
+
+ Overridden by accessory types.
+ """
+ pass
class HomeBridge(Bridge):
"""Adapter class for Bridge."""
- def __init__(self, hass, name=BRIDGE_NAME,
- model=BRIDGE_MODEL, **kwargs):
+ def __init__(self, hass, name=BRIDGE_NAME):
"""Initialize a Bridge object."""
- super().__init__(name, **kwargs)
- set_accessory_info(self, name, model)
+ super().__init__(name)
+ set_accessory_info(self, name, model=BRIDGE_MODEL)
self.hass = hass
def _set_services(self):
@@ -87,7 +155,7 @@ class HomeBridge(Bridge):
def remove_paired_client(self, client_uuid):
"""Override super function to show setup message if unpaired."""
super().remove_paired_client(client_uuid)
- show_setup_message(self, self.hass)
+ show_setup_message(self.hass, self)
class HomeDriver(AccessoryDriver):
diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py
index d1c3d84b517..1c498b4b3b9 100644
--- a/homeassistant/components/homekit/const.py
+++ b/homeassistant/components/homekit/const.py
@@ -1,5 +1,6 @@
"""Constants used be the HomeKit component."""
# #### MISC ####
+DEBOUNCE_TIMEOUT = 0.5
DOMAIN = 'homekit'
HOMEKIT_FILE = '.homekit.state'
HOMEKIT_NOTIFY_ID = 4663548
@@ -17,15 +18,15 @@ DEFAULT_PORT = 51827
SERVICE_HOMEKIT_START = 'start'
# #### STRING CONSTANTS ####
-ACCESSORY_MODEL = 'homekit.accessory'
-ACCESSORY_NAME = 'Home Accessory'
BRIDGE_MODEL = 'homekit.bridge'
BRIDGE_NAME = 'Home Assistant'
MANUFACTURER = 'HomeAssistant'
# #### Categories ####
CATEGORY_ALARM_SYSTEM = 'ALARM_SYSTEM'
+CATEGORY_GARAGE_DOOR_OPENER = 'GARAGE_DOOR_OPENER'
CATEGORY_LIGHT = 'LIGHTBULB'
+CATEGORY_LOCK = 'DOOR_LOCK'
CATEGORY_SENSOR = 'SENSOR'
CATEGORY_SWITCH = 'SWITCH'
CATEGORY_THERMOSTAT = 'THERMOSTAT'
@@ -34,40 +35,80 @@ CATEGORY_WINDOW_COVERING = 'WINDOW_COVERING'
# #### Services ####
SERV_ACCESSORY_INFO = 'AccessoryInformation'
-SERV_HUMIDITY_SENSOR = 'HumiditySensor'
-# CurrentRelativeHumidity | StatusActive, StatusFault, StatusTampered,
-# StatusLowBattery, Name
+SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor'
+SERV_CARBON_DIOXIDE_SENSOR = 'CarbonDioxideSensor'
+SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor'
+SERV_CONTACT_SENSOR = 'ContactSensor'
+SERV_GARAGE_DOOR_OPENER = 'GarageDoorOpener'
+SERV_HUMIDITY_SENSOR = 'HumiditySensor' # CurrentRelativeHumidity
+SERV_LEAK_SENSOR = 'LeakSensor'
+SERV_LIGHT_SENSOR = 'LightSensor'
SERV_LIGHTBULB = 'Lightbulb' # On | Brightness, Hue, Saturation, Name
+SERV_LOCK = 'LockMechanism'
+SERV_MOTION_SENSOR = 'MotionSensor'
+SERV_OCCUPANCY_SENSOR = 'OccupancySensor'
SERV_SECURITY_SYSTEM = 'SecuritySystem'
+SERV_SMOKE_SENSOR = 'SmokeSensor'
SERV_SWITCH = 'Switch'
SERV_TEMPERATURE_SENSOR = 'TemperatureSensor'
SERV_THERMOSTAT = 'Thermostat'
SERV_WINDOW_COVERING = 'WindowCovering'
+# CurrentPosition, TargetPosition, PositionState
# #### Characteristics ####
+CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity'
+CHAR_AIR_QUALITY = 'AirQuality'
CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100]
+CHAR_CARBON_DIOXIDE_DETECTED = 'CarbonDioxideDetected'
+CHAR_CARBON_DIOXIDE_LEVEL = 'CarbonDioxideLevel'
+CHAR_CARBON_DIOXIDE_PEAK_LEVEL = 'CarbonDioxidePeakLevel'
+CHAR_CARBON_MONOXIDE_DETECTED = 'CarbonMonoxideDetected'
CHAR_COLOR_TEMPERATURE = 'ColorTemperature'
+CHAR_CONTACT_SENSOR_STATE = 'ContactSensorState'
CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature'
+CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = 'CurrentAmbientLightLevel'
+CHAR_CURRENT_DOOR_STATE = 'CurrentDoorState'
CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState'
-CHAR_CURRENT_POSITION = 'CurrentPosition'
+CHAR_CURRENT_POSITION = 'CurrentPosition' # Int | [0, 100]
CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent
CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState'
CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature'
CHAR_HUE = 'Hue' # arcdegress | [0, 360]
+CHAR_LEAK_DETECTED = 'LeakDetected'
+CHAR_LOCK_CURRENT_STATE = 'LockCurrentState'
+CHAR_LOCK_TARGET_STATE = 'LockTargetState'
+CHAR_LINK_QUALITY = 'LinkQuality'
CHAR_MANUFACTURER = 'Manufacturer'
CHAR_MODEL = 'Model'
+CHAR_MOTION_DETECTED = 'MotionDetected'
CHAR_NAME = 'Name'
+CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected'
CHAR_ON = 'On' # boolean
CHAR_POSITION_STATE = 'PositionState'
CHAR_SATURATION = 'Saturation' # percent
CHAR_SERIAL_NUMBER = 'SerialNumber'
+CHAR_SMOKE_DETECTED = 'SmokeDetected'
+CHAR_TARGET_DOOR_STATE = 'TargetDoorState'
CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState'
-CHAR_TARGET_POSITION = 'TargetPosition'
+CHAR_TARGET_POSITION = 'TargetPosition' # Int | [0, 100]
CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState'
CHAR_TARGET_TEMPERATURE = 'TargetTemperature'
CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits'
# #### Properties ####
PROP_CELSIUS = {'minValue': -273, 'maxValue': 999}
+
+# #### Device Class ####
+DEVICE_CLASS_CO2 = 'co2'
+DEVICE_CLASS_GAS = 'gas'
+DEVICE_CLASS_HUMIDITY = 'humidity'
+DEVICE_CLASS_LIGHT = 'light'
+DEVICE_CLASS_MOISTURE = 'moisture'
+DEVICE_CLASS_MOTION = 'motion'
+DEVICE_CLASS_OCCUPANCY = 'occupancy'
+DEVICE_CLASS_OPENING = 'opening'
+DEVICE_CLASS_PM25 = 'pm25'
+DEVICE_CLASS_SMOKE = 'smoke'
+DEVICE_CLASS_TEMPERATURE = 'temperature'
diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py
index 3650a948f5d..8ec715e0e01 100644
--- a/homeassistant/components/homekit/type_covers.py
+++ b/homeassistant/components/homekit/type_covers.py
@@ -1,18 +1,67 @@
"""Class to hold all cover accessories."""
import logging
-from homeassistant.components.cover import ATTR_CURRENT_POSITION
+from homeassistant.components.cover import (
+ ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP)
+from homeassistant.const import (
+ ATTR_ENTITY_ID, SERVICE_SET_COVER_POSITION, STATE_OPEN, STATE_CLOSED,
+ SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_STOP_COVER,
+ ATTR_SUPPORTED_FEATURES)
from . import TYPES
-from .accessories import HomeAccessory, add_preload_service
+from .accessories import HomeAccessory, add_preload_service, setup_char
from .const import (
CATEGORY_WINDOW_COVERING, SERV_WINDOW_COVERING,
- CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE)
-
+ CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE,
+ CATEGORY_GARAGE_DOOR_OPENER, SERV_GARAGE_DOOR_OPENER,
+ CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE)
_LOGGER = logging.getLogger(__name__)
+@TYPES.register('GarageDoorOpener')
+class GarageDoorOpener(HomeAccessory):
+ """Generate a Garage Door Opener accessory for a cover entity.
+
+ The cover entity must be in the 'garage' device class
+ and support no more than open, close, and stop.
+ """
+
+ def __init__(self, *args, config):
+ """Initialize a GarageDoorOpener accessory object."""
+ super().__init__(*args, category=CATEGORY_GARAGE_DOOR_OPENER)
+ self.flag_target_state = False
+
+ serv_garage_door = add_preload_service(self, SERV_GARAGE_DOOR_OPENER)
+ self.char_current_state = setup_char(
+ CHAR_CURRENT_DOOR_STATE, serv_garage_door, value=0)
+ self.char_target_state = setup_char(
+ CHAR_TARGET_DOOR_STATE, serv_garage_door, value=0,
+ callback=self.set_state)
+
+ def set_state(self, value):
+ """Change garage state if call came from HomeKit."""
+ _LOGGER.debug('%s: Set state to %d', self.entity_id, value)
+ self.flag_target_state = True
+
+ if value == 0:
+ self.char_current_state.set_value(3)
+ self.hass.components.cover.open_cover(self.entity_id)
+ elif value == 1:
+ self.char_current_state.set_value(2)
+ self.hass.components.cover.close_cover(self.entity_id)
+
+ def update_state(self, new_state):
+ """Update cover state after state changed."""
+ hass_state = new_state.state
+ if hass_state in (STATE_OPEN, STATE_CLOSED):
+ current_state = 0 if hass_state == STATE_OPEN else 1
+ self.char_current_state.set_value(current_state)
+ if not self.flag_target_state:
+ self.char_target_state.set_value(current_state)
+ self.flag_target_state = False
+
+
@TYPES.register('WindowCovering')
class WindowCovering(HomeAccessory):
"""Generate a Window accessory for a cover entity.
@@ -20,54 +69,91 @@ class WindowCovering(HomeAccessory):
The cover entity must support: set_cover_position.
"""
- def __init__(self, hass, entity_id, display_name, **kwargs):
+ def __init__(self, *args, config):
"""Initialize a WindowCovering accessory object."""
- super().__init__(display_name, entity_id,
- CATEGORY_WINDOW_COVERING, **kwargs)
-
- self.hass = hass
- self.entity_id = entity_id
-
- self.current_position = None
+ super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
self.homekit_target = None
serv_cover = add_preload_service(self, SERV_WINDOW_COVERING)
- self.char_current_position = serv_cover. \
- get_characteristic(CHAR_CURRENT_POSITION)
- self.char_target_position = serv_cover. \
- get_characteristic(CHAR_TARGET_POSITION)
- self.char_position_state = serv_cover. \
- get_characteristic(CHAR_POSITION_STATE)
- self.char_current_position.value = 0
- self.char_target_position.value = 0
- self.char_position_state.value = 0
-
- self.char_target_position.setter_callback = self.move_cover
+ self.char_current_position = setup_char(
+ CHAR_CURRENT_POSITION, serv_cover, value=0)
+ self.char_target_position = setup_char(
+ CHAR_TARGET_POSITION, serv_cover, value=0,
+ callback=self.move_cover)
def move_cover(self, value):
"""Move cover to value if call came from HomeKit."""
- self.char_target_position.set_value(value, should_callback=False)
- if value != self.current_position:
- _LOGGER.debug('%s: Set position to %d', self.entity_id, value)
- self.homekit_target = value
- if value > self.current_position:
- self.char_position_state.set_value(1)
- elif value < self.current_position:
- self.char_position_state.set_value(0)
- self.hass.components.cover.set_cover_position(
- value, self.entity_id)
+ _LOGGER.debug('%s: Set position to %d', self.entity_id, value)
+ self.homekit_target = value
- def update_state(self, entity_id=None, old_state=None, new_state=None):
+ params = {ATTR_ENTITY_ID: self.entity_id, ATTR_POSITION: value}
+ self.hass.services.call(DOMAIN, SERVICE_SET_COVER_POSITION, params)
+
+ def update_state(self, new_state):
"""Update cover position after state changed."""
- if new_state is None:
- return
-
current_position = new_state.attributes.get(ATTR_CURRENT_POSITION)
if isinstance(current_position, int):
- self.current_position = current_position
- self.char_current_position.set_value(self.current_position)
+ self.char_current_position.set_value(current_position)
if self.homekit_target is None or \
- abs(self.current_position - self.homekit_target) < 6:
- self.char_target_position.set_value(self.current_position)
- self.char_position_state.set_value(2)
+ abs(current_position - self.homekit_target) < 6:
+ self.char_target_position.set_value(current_position)
self.homekit_target = None
+
+
+@TYPES.register('WindowCoveringBasic')
+class WindowCoveringBasic(HomeAccessory):
+ """Generate a Window accessory for a cover entity.
+
+ The cover entity must support: open_cover, close_cover,
+ stop_cover (optional).
+ """
+
+ def __init__(self, *args, config):
+ """Initialize a WindowCovering accessory object."""
+ super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
+ features = self.hass.states.get(self.entity_id) \
+ .attributes.get(ATTR_SUPPORTED_FEATURES)
+ self.supports_stop = features & SUPPORT_STOP
+
+ serv_cover = add_preload_service(self, SERV_WINDOW_COVERING)
+ self.char_current_position = setup_char(
+ CHAR_CURRENT_POSITION, serv_cover, value=0)
+ self.char_target_position = setup_char(
+ CHAR_TARGET_POSITION, serv_cover, value=0,
+ callback=self.move_cover)
+ self.char_position_state = setup_char(
+ CHAR_POSITION_STATE, serv_cover, value=2)
+
+ def move_cover(self, value):
+ """Move cover to value if call came from HomeKit."""
+ _LOGGER.debug('%s: Set position to %d', self.entity_id, value)
+
+ if self.supports_stop:
+ if value > 70:
+ service, position = (SERVICE_OPEN_COVER, 100)
+ elif value < 30:
+ service, position = (SERVICE_CLOSE_COVER, 0)
+ else:
+ service, position = (SERVICE_STOP_COVER, 50)
+ else:
+ if value >= 50:
+ service, position = (SERVICE_OPEN_COVER, 100)
+ else:
+ service, position = (SERVICE_CLOSE_COVER, 0)
+
+ self.hass.services.call(DOMAIN, service,
+ {ATTR_ENTITY_ID: self.entity_id})
+
+ # Snap the current/target position to the expected final position.
+ self.char_current_position.set_value(position)
+ self.char_target_position.set_value(position)
+ self.char_position_state.set_value(2)
+
+ def update_state(self, new_state):
+ """Update cover position after state changed."""
+ position_mapping = {STATE_OPEN: 100, STATE_CLOSED: 0}
+ hk_position = position_mapping.get(new_state.state)
+ if hk_position is not None:
+ self.char_current_position.set_value(hk_position)
+ self.char_target_position.set_value(hk_position)
+ self.char_position_state.set_value(2)
diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py
index 018d3cd2e74..9a7bce76fba 100644
--- a/homeassistant/components/homekit/type_lights.py
+++ b/homeassistant/components/homekit/type_lights.py
@@ -7,7 +7,8 @@ from homeassistant.components.light import (
from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF
from . import TYPES
-from .accessories import HomeAccessory, add_preload_service
+from .accessories import (
+ HomeAccessory, add_preload_service, debounce, setup_char)
from .const import (
CATEGORY_LIGHT, SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE,
CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION)
@@ -24,12 +25,9 @@ class Light(HomeAccessory):
Currently supports: state, brightness, color temperature, rgb_color.
"""
- def __init__(self, hass, entity_id, name, **kwargs):
+ def __init__(self, *args, config):
"""Initialize a new Light accessory object."""
- super().__init__(name, entity_id, CATEGORY_LIGHT, **kwargs)
-
- self.hass = hass
- self.entity_id = entity_id
+ super().__init__(*args, category=CATEGORY_LIGHT)
self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False,
CHAR_HUE: False, CHAR_SATURATION: False,
CHAR_COLOR_TEMPERATURE: False, RGB_COLOR: False}
@@ -49,36 +47,29 @@ class Light(HomeAccessory):
self._saturation = None
serv_light = add_preload_service(self, SERV_LIGHTBULB, self.chars)
- self.char_on = serv_light.get_characteristic(CHAR_ON)
- self.char_on.setter_callback = self.set_state
- self.char_on.value = self._state
+ self.char_on = setup_char(
+ CHAR_ON, serv_light, value=self._state, callback=self.set_state)
if CHAR_BRIGHTNESS in self.chars:
- self.char_brightness = serv_light \
- .get_characteristic(CHAR_BRIGHTNESS)
- self.char_brightness.setter_callback = self.set_brightness
- self.char_brightness.value = 0
+ self.char_brightness = setup_char(
+ CHAR_BRIGHTNESS, serv_light, value=0,
+ callback=self.set_brightness)
if CHAR_COLOR_TEMPERATURE in self.chars:
- self.char_color_temperature = serv_light \
- .get_characteristic(CHAR_COLOR_TEMPERATURE)
- self.char_color_temperature.setter_callback = \
- self.set_color_temperature
min_mireds = self.hass.states.get(self.entity_id) \
.attributes.get(ATTR_MIN_MIREDS, 153)
max_mireds = self.hass.states.get(self.entity_id) \
.attributes.get(ATTR_MAX_MIREDS, 500)
- self.char_color_temperature.override_properties({
- 'minValue': min_mireds, 'maxValue': max_mireds})
- self.char_color_temperature.value = min_mireds
+ self.char_color_temperature = setup_char(
+ CHAR_COLOR_TEMPERATURE, serv_light, value=min_mireds,
+ properties={'minValue': min_mireds, 'maxValue': max_mireds},
+ callback=self.set_color_temperature)
if CHAR_HUE in self.chars:
- self.char_hue = serv_light.get_characteristic(CHAR_HUE)
- self.char_hue.setter_callback = self.set_hue
- self.char_hue.value = 0
+ self.char_hue = setup_char(
+ CHAR_HUE, serv_light, value=0, callback=self.set_hue)
if CHAR_SATURATION in self.chars:
- self.char_saturation = serv_light \
- .get_characteristic(CHAR_SATURATION)
- self.char_saturation.setter_callback = self.set_saturation
- self.char_saturation.value = 75
+ self.char_saturation = setup_char(
+ CHAR_SATURATION, serv_light, value=75,
+ callback=self.set_saturation)
def set_state(self, value):
"""Set state if call came from HomeKit."""
@@ -87,18 +78,17 @@ class Light(HomeAccessory):
_LOGGER.debug('%s: Set state to %d', self.entity_id, value)
self._flag[CHAR_ON] = True
- self.char_on.set_value(value, should_callback=False)
if value == 1:
self.hass.components.light.turn_on(self.entity_id)
elif value == 0:
self.hass.components.light.turn_off(self.entity_id)
+ @debounce
def set_brightness(self, value):
"""Set brightness if call came from HomeKit."""
_LOGGER.debug('%s: Set brightness to %d', self.entity_id, value)
self._flag[CHAR_BRIGHTNESS] = True
- self.char_brightness.set_value(value, should_callback=False)
if value != 0:
self.hass.components.light.turn_on(
self.entity_id, brightness_pct=value)
@@ -109,14 +99,12 @@ class Light(HomeAccessory):
"""Set color temperature if call came from HomeKit."""
_LOGGER.debug('%s: Set color temp to %s', self.entity_id, value)
self._flag[CHAR_COLOR_TEMPERATURE] = True
- self.char_color_temperature.set_value(value, should_callback=False)
self.hass.components.light.turn_on(self.entity_id, color_temp=value)
def set_saturation(self, value):
"""Set saturation if call came from HomeKit."""
_LOGGER.debug('%s: Set saturation to %d', self.entity_id, value)
self._flag[CHAR_SATURATION] = True
- self.char_saturation.set_value(value, should_callback=False)
self._saturation = value
self.set_color()
@@ -124,7 +112,6 @@ class Light(HomeAccessory):
"""Set hue if call came from HomeKit."""
_LOGGER.debug('%s: Set hue to %d', self.entity_id, value)
self._flag[CHAR_HUE] = True
- self.char_hue.set_value(value, should_callback=False)
self._hue = value
self.set_color()
@@ -140,17 +127,14 @@ class Light(HomeAccessory):
self.hass.components.light.turn_on(
self.entity_id, hs_color=color)
- def update_state(self, entity_id=None, old_state=None, new_state=None):
+ def update_state(self, new_state):
"""Update light after state change."""
- if not new_state:
- return
-
# Handle State
state = new_state.state
if state in (STATE_ON, STATE_OFF):
self._state = 1 if state == STATE_ON else 0
if not self._flag[CHAR_ON] and self.char_on.value != self._state:
- self.char_on.set_value(self._state, should_callback=False)
+ self.char_on.set_value(self._state)
self._flag[CHAR_ON] = False
# Handle Brightness
@@ -159,17 +143,16 @@ class Light(HomeAccessory):
if not self._flag[CHAR_BRIGHTNESS] and isinstance(brightness, int):
brightness = round(brightness / 255 * 100, 0)
if self.char_brightness.value != brightness:
- self.char_brightness.set_value(brightness,
- should_callback=False)
+ self.char_brightness.set_value(brightness)
self._flag[CHAR_BRIGHTNESS] = False
# Handle color temperature
if CHAR_COLOR_TEMPERATURE in self.chars:
color_temperature = new_state.attributes.get(ATTR_COLOR_TEMP)
if not self._flag[CHAR_COLOR_TEMPERATURE] \
- and isinstance(color_temperature, int):
- self.char_color_temperature.set_value(color_temperature,
- should_callback=False)
+ and isinstance(color_temperature, int) and \
+ self.char_color_temperature.value != color_temperature:
+ self.char_color_temperature.set_value(color_temperature)
self._flag[CHAR_COLOR_TEMPERATURE] = False
# Handle Color
@@ -180,8 +163,7 @@ class Light(HomeAccessory):
hue != self._hue or saturation != self._saturation) and \
isinstance(hue, (int, float)) and \
isinstance(saturation, (int, float)):
- self.char_hue.set_value(hue, should_callback=False)
- self.char_saturation.set_value(saturation,
- should_callback=False)
+ self.char_hue.set_value(hue)
+ self.char_saturation.set_value(saturation)
self._hue, self._saturation = (hue, saturation)
self._flag[RGB_COLOR] = False
diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py
new file mode 100644
index 00000000000..f34fc6c6a7f
--- /dev/null
+++ b/homeassistant/components/homekit/type_locks.py
@@ -0,0 +1,67 @@
+"""Class to hold all lock accessories."""
+import logging
+
+from homeassistant.components.lock import (
+ ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN)
+
+from . import TYPES
+from .accessories import HomeAccessory, add_preload_service, setup_char
+from .const import (
+ CATEGORY_LOCK, SERV_LOCK, CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE)
+
+_LOGGER = logging.getLogger(__name__)
+
+HASS_TO_HOMEKIT = {STATE_UNLOCKED: 0,
+ STATE_LOCKED: 1,
+ # value 2 is Jammed which hass doesn't have a state for
+ STATE_UNKNOWN: 3}
+HOMEKIT_TO_HASS = {c: s for s, c in HASS_TO_HOMEKIT.items()}
+STATE_TO_SERVICE = {STATE_LOCKED: 'lock',
+ STATE_UNLOCKED: 'unlock'}
+
+
+@TYPES.register('Lock')
+class Lock(HomeAccessory):
+ """Generate a Lock accessory for a lock entity.
+
+ The lock entity must support: unlock and lock.
+ """
+
+ def __init__(self, *args, config):
+ """Initialize a Lock accessory object."""
+ super().__init__(*args, category=CATEGORY_LOCK)
+ self.flag_target_state = False
+
+ serv_lock_mechanism = add_preload_service(self, SERV_LOCK)
+ self.char_current_state = setup_char(
+ CHAR_LOCK_CURRENT_STATE, serv_lock_mechanism,
+ value=HASS_TO_HOMEKIT[STATE_UNKNOWN])
+ self.char_target_state = setup_char(
+ CHAR_LOCK_TARGET_STATE, serv_lock_mechanism,
+ value=HASS_TO_HOMEKIT[STATE_LOCKED], callback=self.set_state)
+
+ def set_state(self, value):
+ """Set lock state to value if call came from HomeKit."""
+ _LOGGER.debug("%s: Set state to %d", self.entity_id, value)
+ self.flag_target_state = True
+
+ hass_value = HOMEKIT_TO_HASS.get(value)
+ service = STATE_TO_SERVICE[hass_value]
+
+ params = {ATTR_ENTITY_ID: self.entity_id}
+ self.hass.services.call('lock', service, params)
+
+ def update_state(self, new_state):
+ """Update lock after state changed."""
+ hass_state = new_state.state
+ if hass_state in HASS_TO_HOMEKIT:
+ current_lock_state = HASS_TO_HOMEKIT[hass_state]
+ self.char_current_state.set_value(current_lock_state)
+ _LOGGER.debug('%s: Updated current state to %s (%d)',
+ self.entity_id, hass_state, current_lock_state)
+
+ # LockTargetState only supports locked and unlocked
+ if hass_state in (STATE_LOCKED, STATE_UNLOCKED):
+ if not self.flag_target_state:
+ self.char_target_state.set_value(current_lock_state)
+ self.flag_target_state = False
diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py
index 2cce6653db3..6b8457a3aa5 100644
--- a/homeassistant/components/homekit/type_security_systems.py
+++ b/homeassistant/components/homekit/type_security_systems.py
@@ -7,7 +7,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_CODE)
from . import TYPES
-from .accessories import HomeAccessory, add_preload_service
+from .accessories import HomeAccessory, add_preload_service, setup_char
from .const import (
CATEGORY_ALARM_SYSTEM, SERV_SECURITY_SYSTEM,
CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE)
@@ -27,33 +27,24 @@ STATE_TO_SERVICE = {STATE_ALARM_DISARMED: 'alarm_disarm',
class SecuritySystem(HomeAccessory):
"""Generate an SecuritySystem accessory for an alarm control panel."""
- def __init__(self, hass, entity_id, display_name, alarm_code, **kwargs):
+ def __init__(self, *args, config):
"""Initialize a SecuritySystem accessory object."""
- super().__init__(display_name, entity_id,
- CATEGORY_ALARM_SYSTEM, **kwargs)
-
- self.hass = hass
- self.entity_id = entity_id
- self._alarm_code = alarm_code
-
+ super().__init__(*args, category=CATEGORY_ALARM_SYSTEM)
+ self._alarm_code = config[ATTR_CODE]
self.flag_target_state = False
serv_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM)
- self.char_current_state = serv_alarm. \
- get_characteristic(CHAR_CURRENT_SECURITY_STATE)
- self.char_current_state.value = 3
- self.char_target_state = serv_alarm. \
- get_characteristic(CHAR_TARGET_SECURITY_STATE)
- self.char_target_state.value = 3
-
- self.char_target_state.setter_callback = self.set_security_state
+ self.char_current_state = setup_char(
+ CHAR_CURRENT_SECURITY_STATE, serv_alarm, value=3)
+ self.char_target_state = setup_char(
+ CHAR_TARGET_SECURITY_STATE, serv_alarm, value=3,
+ callback=self.set_security_state)
def set_security_state(self, value):
"""Move security state to value if call came from HomeKit."""
_LOGGER.debug('%s: Set security state to %d',
self.entity_id, value)
self.flag_target_state = True
- self.char_target_state.set_value(value, should_callback=False)
hass_value = HOMEKIT_TO_HASS[value]
service = STATE_TO_SERVICE[hass_value]
@@ -62,23 +53,16 @@ class SecuritySystem(HomeAccessory):
params[ATTR_CODE] = self._alarm_code
self.hass.services.call('alarm_control_panel', service, params)
- def update_state(self, entity_id=None, old_state=None, new_state=None):
+ def update_state(self, new_state):
"""Update security state after state changed."""
- if new_state is None:
- return
-
hass_state = new_state.state
- if hass_state not in HASS_TO_HOMEKIT:
- return
+ if hass_state in HASS_TO_HOMEKIT:
+ current_security_state = HASS_TO_HOMEKIT[hass_state]
+ self.char_current_state.set_value(current_security_state)
+ _LOGGER.debug('%s: Updated current state to %s (%d)',
+ self.entity_id, hass_state, current_security_state)
- current_security_state = HASS_TO_HOMEKIT[hass_state]
- self.char_current_state.set_value(current_security_state,
- should_callback=False)
- _LOGGER.debug('%s: Updated current state to %s (%d)',
- self.entity_id, hass_state, current_security_state)
-
- if not self.flag_target_state:
- self.char_target_state.set_value(current_security_state,
- should_callback=False)
- if self.char_target_state.value == self.char_current_state.value:
- self.flag_target_state = False
+ if not self.flag_target_state:
+ self.char_target_state.set_value(current_security_state)
+ if self.char_target_state.value == self.char_current_state.value:
+ self.flag_target_state = False
diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py
index 80521df5991..6aa8d92c0af 100644
--- a/homeassistant/components/homekit/type_sensors.py
+++ b/homeassistant/components/homekit/type_sensors.py
@@ -2,18 +2,41 @@
import logging
from homeassistant.const import (
- ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
+ ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS,
+ ATTR_DEVICE_CLASS, STATE_ON, STATE_HOME)
from . import TYPES
-from .accessories import HomeAccessory, add_preload_service
+from .accessories import HomeAccessory, add_preload_service, setup_char
from .const import (
CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR,
- CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS)
-from .util import convert_to_float, temperature_to_homekit
-
+ CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS,
+ SERV_AIR_QUALITY_SENSOR, CHAR_AIR_QUALITY, CHAR_AIR_PARTICULATE_DENSITY,
+ CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL,
+ SERV_LIGHT_SENSOR, CHAR_CURRENT_AMBIENT_LIGHT_LEVEL,
+ DEVICE_CLASS_CO2, SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED,
+ 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)
+from .util import (
+ convert_to_float, temperature_to_homekit, density_to_air_quality)
_LOGGER = logging.getLogger(__name__)
+BINARY_SENSOR_SERVICE_MAP = {
+ DEVICE_CLASS_CO2: (SERV_CARBON_DIOXIDE_SENSOR,
+ CHAR_CARBON_DIOXIDE_DETECTED),
+ 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)}
+
@TYPES.register('TemperatureSensor')
class TemperatureSensor(HomeAccessory):
@@ -22,29 +45,22 @@ class TemperatureSensor(HomeAccessory):
Sensor entity must return temperature in °C, °F.
"""
- def __init__(self, hass, entity_id, name, **kwargs):
+ def __init__(self, *args, config):
"""Initialize a TemperatureSensor accessory object."""
- super().__init__(name, entity_id, CATEGORY_SENSOR, **kwargs)
-
- self.hass = hass
- self.entity_id = entity_id
-
+ super().__init__(*args, category=CATEGORY_SENSOR)
serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR)
- self.char_temp = serv_temp.get_characteristic(CHAR_CURRENT_TEMPERATURE)
- self.char_temp.override_properties(properties=PROP_CELSIUS)
- self.char_temp.value = 0
+ self.char_temp = setup_char(
+ CHAR_CURRENT_TEMPERATURE, serv_temp, value=0,
+ properties=PROP_CELSIUS)
self.unit = None
- def update_state(self, entity_id=None, old_state=None, new_state=None):
+ def update_state(self, new_state):
"""Update temperature after state changed."""
- if new_state is None:
- return
-
unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
temperature = convert_to_float(new_state.state)
if temperature:
temperature = temperature_to_homekit(temperature, unit)
- self.char_temp.set_value(temperature, should_callback=False)
+ self.char_temp.set_value(temperature)
_LOGGER.debug('%s: Current temperature set to %d°C',
self.entity_id, temperature)
@@ -53,25 +69,113 @@ class TemperatureSensor(HomeAccessory):
class HumiditySensor(HomeAccessory):
"""Generate a HumiditySensor accessory as humidity sensor."""
- def __init__(self, hass, entity_id, name, *args, **kwargs):
+ def __init__(self, *args, config):
"""Initialize a HumiditySensor accessory object."""
- super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs)
-
- self.hass = hass
- self.entity_id = entity_id
-
+ super().__init__(*args, category=CATEGORY_SENSOR)
serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR)
- self.char_humidity = serv_humidity \
- .get_characteristic(CHAR_CURRENT_HUMIDITY)
- self.char_humidity.value = 0
+ self.char_humidity = setup_char(
+ CHAR_CURRENT_HUMIDITY, serv_humidity, value=0)
- def update_state(self, entity_id=None, old_state=None, new_state=None):
+ def update_state(self, new_state):
"""Update accessory after state change."""
- if new_state is None:
- return
-
humidity = convert_to_float(new_state.state)
if humidity:
- self.char_humidity.set_value(humidity, should_callback=False)
+ self.char_humidity.set_value(humidity)
_LOGGER.debug('%s: Percent set to %d%%',
self.entity_id, humidity)
+
+
+@TYPES.register('AirQualitySensor')
+class AirQualitySensor(HomeAccessory):
+ """Generate a AirQualitySensor accessory as air quality sensor."""
+
+ def __init__(self, *args, config):
+ """Initialize a AirQualitySensor accessory object."""
+ super().__init__(*args, category=CATEGORY_SENSOR)
+
+ serv_air_quality = add_preload_service(self, SERV_AIR_QUALITY_SENSOR,
+ [CHAR_AIR_PARTICULATE_DENSITY])
+ self.char_quality = setup_char(
+ CHAR_AIR_QUALITY, serv_air_quality, value=0)
+ self.char_density = setup_char(
+ CHAR_AIR_PARTICULATE_DENSITY, serv_air_quality, value=0)
+
+ def update_state(self, new_state):
+ """Update accessory after state change."""
+ density = convert_to_float(new_state.state)
+ if density is not None:
+ self.char_density.set_value(density)
+ self.char_quality.set_value(density_to_air_quality(density))
+ _LOGGER.debug('%s: Set to %d', self.entity_id, density)
+
+
+@TYPES.register('CarbonDioxideSensor')
+class CarbonDioxideSensor(HomeAccessory):
+ """Generate a CarbonDioxideSensor accessory as CO2 sensor."""
+
+ def __init__(self, *args, config):
+ """Initialize a CarbonDioxideSensor accessory object."""
+ super().__init__(*args, category=CATEGORY_SENSOR)
+
+ serv_co2 = add_preload_service(self, SERV_CARBON_DIOXIDE_SENSOR, [
+ CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL])
+ self.char_co2 = setup_char(
+ CHAR_CARBON_DIOXIDE_LEVEL, serv_co2, value=0)
+ self.char_peak = setup_char(
+ CHAR_CARBON_DIOXIDE_PEAK_LEVEL, serv_co2, value=0)
+ self.char_detected = setup_char(
+ CHAR_CARBON_DIOXIDE_DETECTED, serv_co2, value=0)
+
+ def update_state(self, new_state):
+ """Update accessory after state change."""
+ co2 = convert_to_float(new_state.state)
+ if co2 is not None:
+ self.char_co2.set_value(co2)
+ if co2 > self.char_peak.value:
+ self.char_peak.set_value(co2)
+ self.char_detected.set_value(co2 > 1000)
+ _LOGGER.debug('%s: Set to %d', self.entity_id, co2)
+
+
+@TYPES.register('LightSensor')
+class LightSensor(HomeAccessory):
+ """Generate a LightSensor accessory as light sensor."""
+
+ def __init__(self, *args, config):
+ """Initialize a LightSensor accessory object."""
+ super().__init__(*args, category=CATEGORY_SENSOR)
+
+ serv_light = add_preload_service(self, SERV_LIGHT_SENSOR)
+ self.char_light = setup_char(
+ CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, serv_light, value=0)
+
+ def update_state(self, new_state):
+ """Update accessory after state change."""
+ luminance = convert_to_float(new_state.state)
+ if luminance is not None:
+ self.char_light.set_value(luminance)
+ _LOGGER.debug('%s: Set to %d', self.entity_id, luminance)
+
+
+@TYPES.register('BinarySensor')
+class BinarySensor(HomeAccessory):
+ """Generate a BinarySensor accessory as binary sensor."""
+
+ def __init__(self, *args, config):
+ """Initialize a BinarySensor accessory object."""
+ super().__init__(*args, category=CATEGORY_SENSOR)
+ device_class = self.hass.states.get(self.entity_id).attributes \
+ .get(ATTR_DEVICE_CLASS)
+ service_char = BINARY_SENSOR_SERVICE_MAP[device_class] \
+ if device_class in BINARY_SENSOR_SERVICE_MAP \
+ else BINARY_SENSOR_SERVICE_MAP[DEVICE_CLASS_OCCUPANCY]
+
+ service = add_preload_service(self, service_char[0])
+ self.char_detected = setup_char(service_char[1], service, value=0)
+
+ def update_state(self, new_state):
+ """Update accessory after state change."""
+ state = new_state.state
+ detected = (state == STATE_ON) or (state == STATE_HOME)
+ self.char_detected.set_value(detected)
+ _LOGGER.debug('%s: Set to %d', self.entity_id, detected)
diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py
index 689edde6f37..aaf13e4ea7e 100644
--- a/homeassistant/components/homekit/type_switches.py
+++ b/homeassistant/components/homekit/type_switches.py
@@ -6,7 +6,7 @@ from homeassistant.const import (
from homeassistant.core import split_entity_id
from . import TYPES
-from .accessories import HomeAccessory, add_preload_service
+from .accessories import HomeAccessory, add_preload_service, setup_char
from .const import CATEGORY_SWITCH, SERV_SWITCH, CHAR_ON
_LOGGER = logging.getLogger(__name__)
@@ -16,40 +16,30 @@ _LOGGER = logging.getLogger(__name__)
class Switch(HomeAccessory):
"""Generate a Switch accessory."""
- def __init__(self, hass, entity_id, display_name, **kwargs):
+ def __init__(self, *args, config):
"""Initialize a Switch accessory object to represent a remote."""
- super().__init__(display_name, entity_id, CATEGORY_SWITCH, **kwargs)
-
- self.hass = hass
- self.entity_id = entity_id
- self._domain = split_entity_id(entity_id)[0]
-
+ super().__init__(*args, category=CATEGORY_SWITCH)
+ self._domain = split_entity_id(self.entity_id)[0]
self.flag_target_state = False
serv_switch = add_preload_service(self, SERV_SWITCH)
- self.char_on = serv_switch.get_characteristic(CHAR_ON)
- self.char_on.value = False
- self.char_on.setter_callback = self.set_state
+ self.char_on = setup_char(
+ CHAR_ON, serv_switch, value=False, callback=self.set_state)
def set_state(self, value):
"""Move switch state to value if call came from HomeKit."""
_LOGGER.debug('%s: Set switch state to %s',
self.entity_id, value)
self.flag_target_state = True
- self.char_on.set_value(value, should_callback=False)
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
self.hass.services.call(self._domain, service,
{ATTR_ENTITY_ID: self.entity_id})
- def update_state(self, entity_id=None, old_state=None, new_state=None):
+ def update_state(self, new_state):
"""Update switch state after state changed."""
- if new_state is None:
- return
-
current_state = (new_state.state == STATE_ON)
if not self.flag_target_state:
_LOGGER.debug('%s: Set current state to %s',
self.entity_id, current_state)
- self.char_on.set_value(current_state, should_callback=False)
-
+ self.char_on.set_value(current_state)
self.flag_target_state = False
diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py
index 69b61062791..ce10b96c51c 100644
--- a/homeassistant/components/homekit/type_thermostats.py
+++ b/homeassistant/components/homekit/type_thermostats.py
@@ -5,12 +5,15 @@ from homeassistant.components.climate import (
ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE,
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
ATTR_OPERATION_MODE, ATTR_OPERATION_LIST,
- STATE_HEAT, STATE_COOL, STATE_AUTO)
+ STATE_HEAT, STATE_COOL, STATE_AUTO,
+ SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
from homeassistant.const import (
- ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
+ ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT,
+ STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
from . import TYPES
-from .accessories import HomeAccessory, add_preload_service
+from .accessories import (
+ HomeAccessory, add_preload_service, debounce, setup_char)
from .const import (
CATEGORY_THERMOSTAT, SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING,
CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE,
@@ -26,78 +29,66 @@ HC_HASS_TO_HOMEKIT = {STATE_OFF: 0, STATE_HEAT: 1,
STATE_COOL: 2, STATE_AUTO: 3}
HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()}
+SUPPORT_TEMP_RANGE = SUPPORT_TARGET_TEMPERATURE_LOW | \
+ SUPPORT_TARGET_TEMPERATURE_HIGH
+
@TYPES.register('Thermostat')
class Thermostat(HomeAccessory):
"""Generate a Thermostat accessory for a climate."""
- def __init__(self, hass, entity_id, display_name, support_auto, **kwargs):
+ def __init__(self, *args, config):
"""Initialize a Thermostat accessory object."""
- super().__init__(display_name, entity_id,
- CATEGORY_THERMOSTAT, **kwargs)
-
- self.hass = hass
- self.entity_id = entity_id
- self._call_timer = None
+ super().__init__(*args, category=CATEGORY_THERMOSTAT)
self._unit = TEMP_CELSIUS
-
self.heat_cool_flag_target_state = False
self.temperature_flag_target_state = False
self.coolingthresh_flag_target_state = False
self.heatingthresh_flag_target_state = False
# Add additional characteristics if auto mode is supported
- extra_chars = [
- CHAR_COOLING_THRESHOLD_TEMPERATURE,
- CHAR_HEATING_THRESHOLD_TEMPERATURE] if support_auto else None
+ self.chars = []
+ features = self.hass.states.get(self.entity_id) \
+ .attributes.get(ATTR_SUPPORTED_FEATURES)
+ if features & SUPPORT_TEMP_RANGE:
+ self.chars.extend((CHAR_COOLING_THRESHOLD_TEMPERATURE,
+ CHAR_HEATING_THRESHOLD_TEMPERATURE))
- # Preload the thermostat service
- serv_thermostat = add_preload_service(self, SERV_THERMOSTAT,
- extra_chars)
+ serv_thermostat = add_preload_service(
+ self, SERV_THERMOSTAT, self.chars)
# Current and target mode characteristics
- self.char_current_heat_cool = serv_thermostat. \
- get_characteristic(CHAR_CURRENT_HEATING_COOLING)
- self.char_current_heat_cool.value = 0
- self.char_target_heat_cool = serv_thermostat. \
- get_characteristic(CHAR_TARGET_HEATING_COOLING)
- self.char_target_heat_cool.value = 0
- self.char_target_heat_cool.setter_callback = self.set_heat_cool
+ self.char_current_heat_cool = setup_char(
+ CHAR_CURRENT_HEATING_COOLING, serv_thermostat, value=0)
+ self.char_target_heat_cool = setup_char(
+ CHAR_TARGET_HEATING_COOLING, serv_thermostat, value=0,
+ callback=self.set_heat_cool)
# Current and target temperature characteristics
- self.char_current_temp = serv_thermostat. \
- get_characteristic(CHAR_CURRENT_TEMPERATURE)
- self.char_current_temp.value = 21.0
- self.char_target_temp = serv_thermostat. \
- get_characteristic(CHAR_TARGET_TEMPERATURE)
- self.char_target_temp.value = 21.0
- self.char_target_temp.setter_callback = self.set_target_temperature
+ self.char_current_temp = setup_char(
+ CHAR_CURRENT_TEMPERATURE, serv_thermostat, value=21.0)
+ self.char_target_temp = setup_char(
+ CHAR_TARGET_TEMPERATURE, serv_thermostat, value=21.0,
+ callback=self.set_target_temperature)
# Display units characteristic
- self.char_display_units = serv_thermostat. \
- get_characteristic(CHAR_TEMP_DISPLAY_UNITS)
- self.char_display_units.value = 0
+ self.char_display_units = setup_char(
+ CHAR_TEMP_DISPLAY_UNITS, serv_thermostat, value=0)
# If the device supports it: high and low temperature characteristics
- if support_auto:
- self.char_cooling_thresh_temp = serv_thermostat. \
- get_characteristic(CHAR_COOLING_THRESHOLD_TEMPERATURE)
- self.char_cooling_thresh_temp.value = 23.0
- self.char_cooling_thresh_temp.setter_callback = \
- self.set_cooling_threshold
-
- self.char_heating_thresh_temp = serv_thermostat. \
- get_characteristic(CHAR_HEATING_THRESHOLD_TEMPERATURE)
- self.char_heating_thresh_temp.value = 19.0
- self.char_heating_thresh_temp.setter_callback = \
- self.set_heating_threshold
- else:
- self.char_cooling_thresh_temp = None
- self.char_heating_thresh_temp = None
+ self.char_cooling_thresh_temp = None
+ self.char_heating_thresh_temp = None
+ if CHAR_COOLING_THRESHOLD_TEMPERATURE in self.chars:
+ self.char_cooling_thresh_temp = setup_char(
+ CHAR_COOLING_THRESHOLD_TEMPERATURE, serv_thermostat,
+ value=23.0, callback=self.set_cooling_threshold)
+ if CHAR_HEATING_THRESHOLD_TEMPERATURE in self.chars:
+ self.char_heating_thresh_temp = setup_char(
+ CHAR_HEATING_THRESHOLD_TEMPERATURE, serv_thermostat,
+ value=19.0, callback=self.set_heating_threshold)
def set_heat_cool(self, value):
"""Move operation mode to value if call came from HomeKit."""
- self.char_target_heat_cool.set_value(value, should_callback=False)
if value in HC_HOMEKIT_TO_HASS:
_LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value)
self.heat_cool_flag_target_state = True
@@ -105,12 +96,12 @@ class Thermostat(HomeAccessory):
self.hass.components.climate.set_operation_mode(
operation_mode=hass_value, entity_id=self.entity_id)
+ @debounce
def set_cooling_threshold(self, value):
"""Set cooling threshold temp to value if call came from HomeKit."""
_LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C',
self.entity_id, value)
self.coolingthresh_flag_target_state = True
- self.char_cooling_thresh_temp.set_value(value, should_callback=False)
low = self.char_heating_thresh_temp.value
low = temperature_to_states(low, self._unit)
value = temperature_to_states(value, self._unit)
@@ -118,12 +109,12 @@ class Thermostat(HomeAccessory):
entity_id=self.entity_id, target_temp_high=value,
target_temp_low=low)
+ @debounce
def set_heating_threshold(self, value):
"""Set heating threshold temp to value if call came from HomeKit."""
_LOGGER.debug('%s: Set heating threshold temperature to %.2f°C',
self.entity_id, value)
self.heatingthresh_flag_target_state = True
- self.char_heating_thresh_temp.set_value(value, should_callback=False)
# Home assistant always wants to set low and high at the same time
high = self.char_cooling_thresh_temp.value
high = temperature_to_states(high, self._unit)
@@ -132,21 +123,18 @@ class Thermostat(HomeAccessory):
entity_id=self.entity_id, target_temp_high=high,
target_temp_low=value)
+ @debounce
def set_target_temperature(self, value):
"""Set target temperature to value if call came from HomeKit."""
_LOGGER.debug('%s: Set target temperature to %.2f°C',
self.entity_id, value)
self.temperature_flag_target_state = True
- self.char_target_temp.set_value(value, should_callback=False)
value = temperature_to_states(value, self._unit)
self.hass.components.climate.set_temperature(
temperature=value, entity_id=self.entity_id)
- def update_state(self, entity_id=None, old_state=None, new_state=None):
+ def update_state(self, new_state):
"""Update security state after state changed."""
- if new_state is None:
- return
-
self._unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT,
TEMP_CELSIUS)
@@ -161,8 +149,7 @@ class Thermostat(HomeAccessory):
if isinstance(target_temp, (int, float)):
target_temp = temperature_to_homekit(target_temp, self._unit)
if not self.temperature_flag_target_state:
- self.char_target_temp.set_value(target_temp,
- should_callback=False)
+ self.char_target_temp.set_value(target_temp)
self.temperature_flag_target_state = False
# Update cooling threshold temperature if characteristic exists
@@ -172,8 +159,7 @@ class Thermostat(HomeAccessory):
cooling_thresh = temperature_to_homekit(cooling_thresh,
self._unit)
if not self.coolingthresh_flag_target_state:
- self.char_cooling_thresh_temp.set_value(
- cooling_thresh, should_callback=False)
+ self.char_cooling_thresh_temp.set_value(cooling_thresh)
self.coolingthresh_flag_target_state = False
# Update heating threshold temperature if characteristic exists
@@ -183,8 +169,7 @@ class Thermostat(HomeAccessory):
heating_thresh = temperature_to_homekit(heating_thresh,
self._unit)
if not self.heatingthresh_flag_target_state:
- self.char_heating_thresh_temp.set_value(
- heating_thresh, should_callback=False)
+ self.char_heating_thresh_temp.set_value(heating_thresh)
self.heatingthresh_flag_target_state = False
# Update display units
@@ -197,7 +182,7 @@ class Thermostat(HomeAccessory):
and operation_mode in HC_HASS_TO_HOMEKIT:
if not self.heat_cool_flag_target_state:
self.char_target_heat_cool.set_value(
- HC_HASS_TO_HOMEKIT[operation_mode], should_callback=False)
+ HC_HASS_TO_HOMEKIT[operation_mode])
self.heat_cool_flag_target_state = False
# Set current operation mode based on temperatures and target mode
diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py
index af2c74d9c3c..29fe3c8f265 100644
--- a/homeassistant/components/homekit/util.py
+++ b/homeassistant/components/homekit/util.py
@@ -33,7 +33,7 @@ def validate_entity_config(values):
return entities
-def show_setup_message(bridge, hass):
+def show_setup_message(hass, bridge):
"""Display persistent notification with setup information."""
pin = bridge.pincode.decode()
_LOGGER.info('Pincode: %s', pin)
@@ -64,3 +64,16 @@ def temperature_to_homekit(temperature, unit):
def temperature_to_states(temperature, unit):
"""Convert temperature back from Celsius to Home Assistant unit."""
return round(temp_util.convert(temperature, TEMP_CELSIUS, unit), 1)
+
+
+def density_to_air_quality(density):
+ """Map PM2.5 density to HomeKit AirQuality level."""
+ if density <= 35:
+ return 1
+ elif density <= 75:
+ return 2
+ elif density <= 115:
+ return 3
+ elif density <= 150:
+ return 4
+ return 5
diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py
new file mode 100644
index 00000000000..c33edd07918
--- /dev/null
+++ b/homeassistant/components/homekit_controller/__init__.py
@@ -0,0 +1,228 @@
+"""
+Support for Homekit device discovery.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/homekit_controller/
+"""
+import http
+import json
+import logging
+import os
+import uuid
+
+from homeassistant.components.discovery import SERVICE_HOMEKIT
+from homeassistant.helpers import discovery
+from homeassistant.helpers.entity import Entity
+
+REQUIREMENTS = ['homekit==0.5']
+
+DOMAIN = 'homekit_controller'
+HOMEKIT_DIR = '.homekit'
+
+# Mapping from Homekit type to component.
+HOMEKIT_ACCESSORY_DISPATCH = {
+ 'lightbulb': 'light',
+ 'outlet': 'switch',
+}
+
+KNOWN_ACCESSORIES = "{}-accessories".format(DOMAIN)
+KNOWN_DEVICES = "{}-devices".format(DOMAIN)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def homekit_http_send(self, message_body=None):
+ r"""Send the currently buffered request and clear the buffer.
+
+ Appends an extra \r\n to the buffer.
+ A message_body may be specified, to be appended to the request.
+ """
+ self._buffer.extend((b"", b""))
+ msg = b"\r\n".join(self._buffer)
+ del self._buffer[:]
+
+ if message_body is not None:
+ msg = msg + message_body
+
+ self.send(msg)
+
+
+def get_serial(accessory):
+ """Obtain the serial number of a HomeKit device."""
+ # pylint: disable=import-error
+ import homekit
+ for service in accessory['services']:
+ if homekit.ServicesTypes.get_short(service['type']) != \
+ 'accessory-information':
+ continue
+ for characteristic in service['characteristics']:
+ ctype = homekit.CharacteristicsTypes.get_short(
+ characteristic['type'])
+ if ctype != 'serial-number':
+ continue
+ return characteristic['value']
+ return None
+
+
+class HKDevice():
+ """HomeKit device."""
+
+ def __init__(self, hass, host, port, model, hkid, config_num, config):
+ """Initialise a generic HomeKit device."""
+ # pylint: disable=import-error
+ import homekit
+
+ _LOGGER.info("Setting up Homekit device %s", model)
+ self.hass = hass
+ self.host = host
+ self.port = port
+ self.model = model
+ self.hkid = hkid
+ self.config_num = config_num
+ self.config = config
+ self.configurator = hass.components.configurator
+
+ data_dir = os.path.join(hass.config.path(), HOMEKIT_DIR)
+ if not os.path.isdir(data_dir):
+ os.mkdir(data_dir)
+
+ self.pairing_file = os.path.join(data_dir, 'hk-{}'.format(hkid))
+ self.pairing_data = homekit.load_pairing(self.pairing_file)
+
+ # Monkey patch httpclient for increased compatibility
+ # pylint: disable=protected-access
+ http.client.HTTPConnection._send_output = homekit_http_send
+
+ self.conn = http.client.HTTPConnection(self.host, port=self.port)
+ if self.pairing_data is not None:
+ self.accessory_setup()
+ else:
+ self.configure()
+
+ def accessory_setup(self):
+ """Handle setup of a HomeKit accessory."""
+ # pylint: disable=import-error
+ import homekit
+ self.controllerkey, self.accessorykey = \
+ homekit.get_session_keys(self.conn, self.pairing_data)
+ self.securecon = homekit.SecureHttp(self.conn.sock,
+ self.accessorykey,
+ self.controllerkey)
+ response = self.securecon.get('/accessories')
+ data = json.loads(response.read().decode())
+ for accessory in data['accessories']:
+ serial = get_serial(accessory)
+ if serial in self.hass.data[KNOWN_ACCESSORIES]:
+ continue
+ self.hass.data[KNOWN_ACCESSORIES][serial] = self
+ aid = accessory['aid']
+ for service in accessory['services']:
+ service_info = {'serial': serial,
+ 'aid': aid,
+ 'iid': service['iid']}
+ devtype = homekit.ServicesTypes.get_short(service['type'])
+ _LOGGER.debug("Found %s", devtype)
+ component = HOMEKIT_ACCESSORY_DISPATCH.get(devtype, None)
+ if component is not None:
+ discovery.load_platform(self.hass, component, DOMAIN,
+ service_info, self.config)
+
+ def device_config_callback(self, callback_data):
+ """Handle initial pairing."""
+ # pylint: disable=import-error
+ 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)
+ if self.pairing_data is not None:
+ homekit.save_pairing(self.pairing_file, self.pairing_data)
+ self.accessory_setup()
+ else:
+ error_msg = "Unable to pair, please try again"
+ _configurator = self.hass.data[DOMAIN+self.hkid]
+ self.configurator.notify_errors(_configurator, error_msg)
+
+ def configure(self):
+ """Obtain the pairing code for a HomeKit device."""
+ description = "Please enter the HomeKit code for your {}".format(
+ self.model)
+ self.hass.data[DOMAIN+self.hkid] = \
+ self.configurator.request_config(self.model,
+ self.device_config_callback,
+ description=description,
+ submit_caption="submit",
+ fields=[{'id': 'code',
+ 'name': 'HomeKit code',
+ 'type': 'string'}])
+
+
+class HomeKitEntity(Entity):
+ """Representation of a Home Assistant HomeKit device."""
+
+ def __init__(self, accessory, devinfo):
+ """Initialise a generic HomeKit device."""
+ self._name = accessory.model
+ self._securecon = accessory.securecon
+ self._aid = devinfo['aid']
+ self._iid = devinfo['iid']
+ self._address = "homekit-{}-{}".format(devinfo['serial'], self._iid)
+ self._features = 0
+ self._chars = {}
+
+ def update(self):
+ """Obtain a HomeKit device's state."""
+ response = self._securecon.get('/accessories')
+ data = json.loads(response.read().decode())
+ for accessory in data['accessories']:
+ if accessory['aid'] != self._aid:
+ continue
+ for service in accessory['services']:
+ if service['iid'] != self._iid:
+ continue
+ self.update_characteristics(service['characteristics'])
+ break
+
+ @property
+ def unique_id(self):
+ """Return the ID of this device."""
+ return self._address
+
+ @property
+ def name(self):
+ """Return the name of the device if any."""
+ return self._name
+
+ def update_characteristics(self, characteristics):
+ """Synchronise a HomeKit device state with Home Assistant."""
+ raise NotImplementedError
+
+
+# pylint: too-many-function-args
+def setup(hass, config):
+ """Set up for Homekit devices."""
+ def discovery_dispatch(service, discovery_info):
+ """Dispatcher for Homekit discovery events."""
+ # model, id
+ host = discovery_info['host']
+ port = discovery_info['port']
+ model = discovery_info['properties']['md']
+ hkid = discovery_info['properties']['id']
+ config_num = int(discovery_info['properties']['c#'])
+
+ # Only register a device once, but rescan if the config has changed
+ if hkid in hass.data[KNOWN_DEVICES]:
+ device = hass.data[KNOWN_DEVICES][hkid]
+ if config_num > device.config_num and \
+ device.pairing_info is not None:
+ device.accessory_setup()
+ return
+
+ _LOGGER.debug('Discovered unique device %s', hkid)
+ device = HKDevice(hass, host, port, model, hkid, config_num, config)
+ hass.data[KNOWN_DEVICES][hkid] = device
+
+ hass.data[KNOWN_ACCESSORIES] = {}
+ hass.data[KNOWN_DEVICES] = {}
+ discovery.listen(hass, SERVICE_HOMEKIT, discovery_dispatch)
+ return True
diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py
index c542cd9e88e..1528943a7f9 100644
--- a/homeassistant/components/homematic/__init__.py
+++ b/homeassistant/components/homematic/__init__.py
@@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
from homeassistant.loader import bind_hass
-REQUIREMENTS = ['pyhomematic==0.1.40']
+REQUIREMENTS = ['pyhomematic==0.1.41']
DOMAIN = 'homematic'
_LOGGER = logging.getLogger(__name__)
@@ -69,7 +69,8 @@ HM_DEVICE_TYPES = {
'WeatherStation', 'ThermostatWall2', 'TemperatureDiffSensor',
'TemperatureSensor', 'CO2Sensor', 'IPSwitchPowermeter', 'HMWIOSwitch',
'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall',
- 'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat'],
+ 'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat',
+ 'IPWeatherSensor'],
DISCOVER_CLIMATE: [
'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2',
'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall',
@@ -78,7 +79,7 @@ HM_DEVICE_TYPES = {
'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2',
'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor',
'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain',
- 'WiredSensor', 'PresenceIP'],
+ 'WiredSensor', 'PresenceIP', 'IPWeatherSensor'],
DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'],
DISCOVER_LOCKS: ['KeyMatic']
}
@@ -89,7 +90,7 @@ HM_IGNORE_DISCOVERY_NODE = [
]
HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = {
- 'ACTUAL_TEMPERATURE': ['IPAreaThermostat'],
+ 'ACTUAL_TEMPERATURE': ['IPAreaThermostat', 'IPWeatherSensor'],
}
HM_ATTRIBUTE_SUPPORT = {
diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py
index 557a47f3e05..0aed854d4e4 100644
--- a/homeassistant/components/hue/__init__.py
+++ b/homeassistant/components/hue/__init__.py
@@ -131,3 +131,9 @@ async def async_setup_entry(hass, entry):
bridge = HueBridge(hass, entry, allow_unreachable, allow_groups)
hass.data[DOMAIN][host] = bridge
return await bridge.async_setup()
+
+
+async def async_unload_entry(hass, entry):
+ """Unload a config entry."""
+ bridge = hass.data[DOMAIN].pop(entry.data['host'])
+ return await bridge.async_reset()
diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py
index 8093c84971e..5ff5e2dbf6f 100644
--- a/homeassistant/components/hue/bridge.py
+++ b/homeassistant/components/hue/bridge.py
@@ -30,6 +30,7 @@ class HueBridge(object):
self.allow_groups = allow_groups
self.available = True
self.api = None
+ self._cancel_retry_setup = None
@property
def host(self):
@@ -39,18 +40,17 @@ class HueBridge(object):
async def async_setup(self, tries=0):
"""Set up a phue bridge based on host parameter."""
host = self.host
+ hass = self.hass
try:
self.api = await get_bridge(
- self.hass, host,
- self.config_entry.data['username']
- )
+ hass, host, self.config_entry.data['username'])
except AuthenticationRequired:
# usernames can become invalid if hub is reset or user removed.
# We are going to fail the config entry setup and initiate a new
# linking procedure. When linking succeeds, it will remove the
# old config entry.
- self.hass.async_add_job(self.hass.config_entries.flow.async_init(
+ hass.async_add_job(hass.config_entries.flow.async_init(
DOMAIN, source='import', data={
'host': host,
}
@@ -68,8 +68,8 @@ class HueBridge(object):
# This feels hacky, we should find a better way to do this
self.config_entry.state = config_entries.ENTRY_STATE_LOADED
- # Unhandled edge case: cancel this if we discover bridge on new IP
- self.hass.helpers.event.async_call_later(retry_delay, retry_setup)
+ self._cancel_retry_setup = hass.helpers.event.async_call_later(
+ retry_delay, retry_setup)
return False
@@ -78,16 +78,43 @@ class HueBridge(object):
host)
return False
- self.hass.async_add_job(
- self.hass.helpers.discovery.async_load_platform(
- 'light', DOMAIN, {'host': host}))
+ hass.async_add_job(hass.config_entries.async_forward_entry_setup(
+ self.config_entry, 'light'))
- self.hass.services.async_register(
+ hass.services.async_register(
DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene,
schema=SCENE_SCHEMA)
return True
+ async def async_reset(self):
+ """Reset this bridge to default state.
+
+ Will cancel any scheduled setup retry and will unload
+ the config entry.
+ """
+ # The bridge can be in 3 states:
+ # - Setup was successful, self.api is not None
+ # - Authentication was wrong, self.api is None, not retrying setup.
+ # - Host was down. self.api is None, we're retrying setup
+
+ # If we have a retry scheduled, we were never setup.
+ if self._cancel_retry_setup is not None:
+ self._cancel_retry_setup()
+ self._cancel_retry_setup = None
+ return True
+
+ # If the authentication was wrong.
+ if self.api is None:
+ return True
+
+ self.hass.services.async_remove(DOMAIN, SERVICE_HUE_SCENE)
+
+ # If setup was successful, we set api variable, forwarded entry and
+ # register service
+ return await self.hass.config_entries.async_forward_entry_unload(
+ self.config_entry, 'light')
+
async def hue_activate_scene(self, call, updated=False):
"""Service to call directly into bridge to set scenes."""
group_name = call.data[ATTR_GROUP_NAME]
diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py
index 11e399c984d..af67a594495 100644
--- a/homeassistant/components/hue/config_flow.py
+++ b/homeassistant/components/hue/config_flow.py
@@ -6,7 +6,7 @@ import os
import async_timeout
import voluptuous as vol
-from homeassistant import config_entries
+from homeassistant import config_entries, data_entry_flow
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
@@ -41,7 +41,7 @@ def _find_username_from_config(hass, filename):
@config_entries.HANDLERS.register(DOMAIN)
-class HueFlowHandler(config_entries.ConfigFlowHandler):
+class HueFlowHandler(data_entry_flow.FlowHandler):
"""Handle a Hue config flow."""
VERSION = 1
diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py
index 031fa263e5a..0c0100bc9f5 100644
--- a/homeassistant/components/ihc/__init__.py
+++ b/homeassistant/components/ihc/__init__.py
@@ -1,4 +1,5 @@
-"""IHC component.
+"""
+Support for IHC devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/ihc/
@@ -6,18 +7,18 @@ https://home-assistant.io/components/ihc/
import logging
import os.path
import xml.etree.ElementTree
+
import voluptuous as vol
from homeassistant.components.ihc.const import (
- ATTR_IHC_ID, ATTR_VALUE, CONF_INFO, CONF_AUTOSETUP,
- CONF_BINARY_SENSOR, CONF_LIGHT, CONF_SENSOR, CONF_SWITCH,
- CONF_XPATH, CONF_NODE, CONF_DIMMABLE, CONF_INVERTING,
- SERVICE_SET_RUNTIME_VALUE_BOOL, SERVICE_SET_RUNTIME_VALUE_INT,
- SERVICE_SET_RUNTIME_VALUE_FLOAT)
+ ATTR_IHC_ID, ATTR_VALUE, CONF_AUTOSETUP, CONF_BINARY_SENSOR, CONF_DIMMABLE,
+ CONF_INFO, CONF_INVERTING, CONF_LIGHT, CONF_NODE, CONF_SENSOR, CONF_SWITCH,
+ CONF_XPATH, SERVICE_SET_RUNTIME_VALUE_BOOL,
+ SERVICE_SET_RUNTIME_VALUE_FLOAT, SERVICE_SET_RUNTIME_VALUE_INT)
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (
- CONF_URL, CONF_USERNAME, CONF_PASSWORD, CONF_ID, CONF_NAME,
- CONF_UNIT_OF_MEASUREMENT, CONF_TYPE, TEMP_CELSIUS)
+ CONF_ID, CONF_NAME, CONF_PASSWORD, CONF_TYPE, CONF_UNIT_OF_MEASUREMENT,
+ CONF_URL, CONF_USERNAME, TEMP_CELSIUS)
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import HomeAssistantType
@@ -36,7 +37,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_AUTOSETUP, default=True): cv.boolean,
- vol.Optional(CONF_INFO, default=True): cv.boolean
+ vol.Optional(CONF_INFO, default=True): cv.boolean,
}),
}, extra=vol.ALLOW_EXTRA)
@@ -97,7 +98,7 @@ IHC_PLATFORMS = ('binary_sensor', 'light', 'sensor', 'switch')
def setup(hass, config):
- """Setup the IHC component."""
+ """Set up the IHC component."""
from ihcsdk.ihccontroller import IHCController
conf = config[DOMAIN]
url = conf[CONF_URL]
@@ -106,7 +107,7 @@ def setup(hass, config):
ihc_controller = IHCController(url, username, password)
if not ihc_controller.authenticate():
- _LOGGER.error("Unable to authenticate on ihc controller.")
+ _LOGGER.error("Unable to authenticate on IHC controller")
return False
if (conf[CONF_AUTOSETUP] and
@@ -125,7 +126,7 @@ def autosetup_ihc_products(hass: HomeAssistantType, config, ihc_controller):
"""Auto setup of IHC products from the ihc project file."""
project_xml = ihc_controller.get_project()
if not project_xml:
- _LOGGER.error("Unable to read project from ihc controller.")
+ _LOGGER.error("Unable to read project from ICH controller")
return False
project = xml.etree.ElementTree.fromstring(project_xml)
@@ -150,7 +151,7 @@ def autosetup_ihc_products(hass: HomeAssistantType, config, ihc_controller):
def get_discovery_info(component_setup, groups):
- """Get discovery info for specified component."""
+ """Get discovery info for specified IHC component."""
discovery_data = {}
for group in groups:
groupname = group.attrib['name']
@@ -173,7 +174,7 @@ def get_discovery_info(component_setup, groups):
def setup_service_functions(hass: HomeAssistantType, ihc_controller):
- """Setup the ihc service functions."""
+ """Setup the IHC service functions."""
def set_runtime_value_bool(call):
"""Set a IHC runtime bool value service function."""
ihc_id = call.data[ATTR_IHC_ID]
diff --git a/homeassistant/components/ihc/ihcdevice.py b/homeassistant/components/ihc/ihcdevice.py
index 59f4d95f0a1..de6db875def 100644
--- a/homeassistant/components/ihc/ihcdevice.py
+++ b/homeassistant/components/ihc/ihcdevice.py
@@ -1,4 +1,4 @@
-"""Implements a base class for all IHC devices."""
+"""Implementation of a base class for all IHC devices."""
import asyncio
from xml.etree.ElementTree import Element
@@ -6,7 +6,7 @@ from homeassistant.helpers.entity import Entity
class IHCDevice(Entity):
- """Base class for all ihc devices.
+ """Base class for all IHC devices.
All IHC devices have an associated IHC resource. IHCDevice handled the
registration of the IHC controller callback when the IHC resource changes.
@@ -31,13 +31,13 @@ class IHCDevice(Entity):
@asyncio.coroutine
def async_added_to_hass(self):
- """Add callback for ihc changes."""
+ """Add callback for IHC changes."""
self.ihc_controller.add_notify_event(
self.ihc_id, self.on_ihc_change, True)
@property
def should_poll(self) -> bool:
- """No polling needed for ihc devices."""
+ """No polling needed for IHC devices."""
return False
@property
@@ -58,7 +58,7 @@ class IHCDevice(Entity):
}
def on_ihc_change(self, ihc_id, value):
- """Callback when ihc resource changes.
+ """Callback when IHC resource changes.
Derived classes must overwrite this to do device specific stuff.
"""
diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py
index 39d3203795e..30a1a800a44 100644
--- a/homeassistant/components/light/__init__.py
+++ b/homeassistant/components/light/__init__.py
@@ -334,7 +334,7 @@ class SetIntentHandler(intent.IntentHandler):
async def async_setup(hass, config):
"""Expose light control via state machine and services."""
- component = EntityComponent(
+ component = hass.data[DOMAIN] = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LIGHTS)
await component.async_setup(config)
@@ -388,6 +388,16 @@ async def async_setup(hass, config):
return True
+async def async_setup_entry(hass, entry):
+ """Setup a config entry."""
+ return await hass.data[DOMAIN].async_setup_entry(entry)
+
+
+async def async_unload_entry(hass, entry):
+ """Unload a config entry."""
+ return await hass.data[DOMAIN].async_unload_entry(entry)
+
+
class Profiles:
"""Representation of available color profiles."""
diff --git a/homeassistant/components/light/abode.py b/homeassistant/components/light/abode.py
index bfea19fc3fa..8b7e09d86bc 100644
--- a/homeassistant/components/light/abode.py
+++ b/homeassistant/components/light/abode.py
@@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.abode/
"""
import logging
-
+from math import ceil
from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_HS_COLOR,
@@ -51,7 +51,9 @@ class AbodeLight(AbodeDevice, Light):
*kwargs[ATTR_HS_COLOR]))
if ATTR_BRIGHTNESS in kwargs and self._device.is_dimmable:
- self._device.set_level(kwargs[ATTR_BRIGHTNESS])
+ # Convert HASS brightness (0-255) to Abode brightness (0-99)
+ # If 100 is sent to Abode, response is 99 causing an error
+ self._device.set_level(ceil(kwargs[ATTR_BRIGHTNESS] * 99 / 255.0))
else:
self._device.switch_on()
@@ -68,7 +70,12 @@ class AbodeLight(AbodeDevice, Light):
def brightness(self):
"""Return the brightness of the light."""
if self._device.is_dimmable and self._device.has_brightness:
- return self._device.brightness
+ brightness = int(self._device.brightness)
+ # Abode returns 100 during device initialization and device refresh
+ if brightness == 100:
+ return 255
+ # Convert Abode brightness (0-99) to HASS brightness (0-255)
+ return ceil(brightness * 255 / 99.0)
@property
def hs_color(self):
diff --git a/homeassistant/components/light/eufy.py b/homeassistant/components/light/eufy.py
new file mode 100644
index 00000000000..a66e219c1a8
--- /dev/null
+++ b/homeassistant/components/light/eufy.py
@@ -0,0 +1,168 @@
+"""
+Support for Eufy lights.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/light.eufy/
+"""
+import logging
+
+from homeassistant.components.light import (
+ ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS,
+ SUPPORT_COLOR_TEMP, SUPPORT_COLOR, Light)
+
+import homeassistant.util.color as color_util
+
+from homeassistant.util.color import (
+ color_temperature_mired_to_kelvin as mired_to_kelvin,
+ color_temperature_kelvin_to_mired as kelvin_to_mired)
+
+DEPENDENCIES = ['eufy']
+
+_LOGGER = logging.getLogger(__name__)
+
+EUFY_MAX_KELVIN = 6500
+EUFY_MIN_KELVIN = 2700
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up Eufy bulbs."""
+ if discovery_info is None:
+ return
+ add_devices([EufyLight(discovery_info)], True)
+
+
+class EufyLight(Light):
+ """Representation of a Eufy light."""
+
+ def __init__(self, device):
+ """Initialize the light."""
+ # pylint: disable=import-error
+ import lakeside
+
+ self._temp = None
+ self._brightness = None
+ self._hs = None
+ self._state = None
+ self._name = device['name']
+ self._address = device['address']
+ self._code = device['code']
+ self._type = device['type']
+ self._bulb = lakeside.bulb(self._address, self._code, self._type)
+ self._colormode = False
+ if self._type == "T1011":
+ self._features = SUPPORT_BRIGHTNESS
+ elif self._type == "T1012":
+ self._features = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP
+ elif self._type == "T1013":
+ self._features = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | \
+ SUPPORT_COLOR
+ self._bulb.connect()
+
+ def update(self):
+ """Synchronise state from the bulb."""
+ self._bulb.update()
+ self._brightness = self._bulb.brightness
+ self._temp = self._bulb.temperature
+ if self._bulb.colors:
+ self._colormode = True
+ self._hs = color_util.color_RGB_to_hs(*self._bulb.colors)
+ else:
+ self._colormode = False
+ self._state = self._bulb.power
+
+ @property
+ def unique_id(self):
+ """Return the ID of this light."""
+ return self._address
+
+ @property
+ def name(self):
+ """Return the name of the device if any."""
+ return self._name
+
+ @property
+ def is_on(self):
+ """Return true if device is on."""
+ return self._state
+
+ @property
+ def brightness(self):
+ """Return the brightness of this light between 0..255."""
+ return int(self._brightness * 255 / 100)
+
+ @property
+ def min_mireds(self):
+ """Return minimum supported color temperature."""
+ return kelvin_to_mired(EUFY_MAX_KELVIN)
+
+ @property
+ def max_mireds(self):
+ """Return maximu supported color temperature."""
+ return kelvin_to_mired(EUFY_MIN_KELVIN)
+
+ @property
+ def color_temp(self):
+ """Return the color temperature of this light."""
+ temp_in_k = int(EUFY_MIN_KELVIN + (self._temp *
+ (EUFY_MAX_KELVIN - EUFY_MIN_KELVIN)
+ / 100))
+ return kelvin_to_mired(temp_in_k)
+
+ @property
+ def hs_color(self):
+ """Return the color of this light."""
+ if not self._colormode:
+ return None
+ return self._hs
+
+ @property
+ def supported_features(self):
+ """Flag supported features."""
+ return self._features
+
+ def turn_on(self, **kwargs):
+ """Turn the specified light on."""
+ brightness = kwargs.get(ATTR_BRIGHTNESS)
+ colortemp = kwargs.get(ATTR_COLOR_TEMP)
+ # pylint: disable=invalid-name
+ hs = kwargs.get(ATTR_HS_COLOR)
+
+ if brightness is not None:
+ brightness = int(brightness * 100 / 255)
+ else:
+ brightness = max(1, self._brightness)
+
+ if colortemp is not None:
+ self._colormode = False
+ temp_in_k = mired_to_kelvin(colortemp)
+ relative_temp = temp_in_k - EUFY_MIN_KELVIN
+ temp = int(relative_temp * 100 /
+ (EUFY_MAX_KELVIN - EUFY_MIN_KELVIN))
+ else:
+ temp = None
+
+ if hs is not None:
+ rgb = color_util.color_hsv_to_RGB(
+ hs[0], hs[1], brightness / 255 * 100)
+ self._colormode = True
+ elif self._colormode:
+ rgb = color_util.color_hsv_to_RGB(
+ self._hs[0], self._hs[1], brightness / 255 * 100)
+ else:
+ rgb = None
+
+ try:
+ self._bulb.set_state(power=True, brightness=brightness,
+ temperature=temp, colors=rgb)
+ except BrokenPipeError:
+ self._bulb.connect()
+ self._bulb.set_state(power=True, brightness=brightness,
+ temperature=temp, colors=rgb)
+
+ def turn_off(self, **kwargs):
+ """Turn the specified light off."""
+ try:
+ self._bulb.set_state(power=False)
+ except BrokenPipeError:
+ self._bulb.connect()
+ self._bulb.set_state(power=False)
diff --git a/homeassistant/components/light/hive.py b/homeassistant/components/light/hive.py
index c4ecc5a9d2c..1fd9e8aaaca 100644
--- a/homeassistant/components/light/hive.py
+++ b/homeassistant/components/light/hive.py
@@ -34,6 +34,7 @@ class HiveDeviceLight(Light):
self.device_type = hivedevice["HA_DeviceType"]
self.light_device_type = hivedevice["Hive_Light_DeviceType"]
self.session = hivesession
+ self.attributes = {}
self.data_updatesource = '{}.{}'.format(self.device_type,
self.node_id)
self.session.entities.append(self)
@@ -48,6 +49,11 @@ class HiveDeviceLight(Light):
"""Return the display name of this light."""
return self.node_name
+ @property
+ def device_state_attributes(self):
+ """Show Device Attributes."""
+ return self.attributes
+
@property
def brightness(self):
"""Brightness of the light (an integer in the range 1-255)."""
@@ -136,3 +142,5 @@ class HiveDeviceLight(Light):
def update(self):
"""Update all Node data from Hive."""
self.session.core.update_data(self.node_id)
+ self.attributes = self.session.attributes.state_attributes(
+ self.node_id)
diff --git a/homeassistant/components/light/homekit_controller.py b/homeassistant/components/light/homekit_controller.py
new file mode 100644
index 00000000000..e6dc09e455c
--- /dev/null
+++ b/homeassistant/components/light/homekit_controller.py
@@ -0,0 +1,134 @@
+"""
+Support for Homekit lights.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/light.homekit_controller/
+"""
+import json
+import logging
+
+from homeassistant.components.homekit_controller import (
+ HomeKitEntity, KNOWN_ACCESSORIES)
+from homeassistant.components.light import (
+ ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_COLOR_TEMP, SUPPORT_BRIGHTNESS,
+ SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light)
+
+DEPENDENCIES = ['homekit_controller']
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up Homekit lighting."""
+ if discovery_info is not None:
+ accessory = hass.data[KNOWN_ACCESSORIES][discovery_info['serial']]
+ add_devices([HomeKitLight(accessory, discovery_info)], True)
+
+
+class HomeKitLight(HomeKitEntity, Light):
+ """Representation of a Homekit light."""
+
+ def __init__(self, *args):
+ """Initialise the light."""
+ super().__init__(*args)
+ self._on = None
+ self._brightness = None
+ self._color_temperature = None
+ self._hue = None
+ self._saturation = None
+
+ def update_characteristics(self, characteristics):
+ """Synchronise light state with Home Assistant."""
+ # pylint: disable=import-error
+ import homekit
+
+ for characteristic in characteristics:
+ ctype = characteristic['type']
+ ctype = homekit.CharacteristicsTypes.get_short(ctype)
+ if ctype == "on":
+ self._chars['on'] = characteristic['iid']
+ self._on = characteristic['value']
+ elif ctype == 'brightness':
+ self._chars['brightness'] = characteristic['iid']
+ self._features |= SUPPORT_BRIGHTNESS
+ self._brightness = characteristic['value']
+ elif ctype == 'color-temperature':
+ self._chars['color_temperature'] = characteristic['iid']
+ self._features |= SUPPORT_COLOR_TEMP
+ self._color_temperature = characteristic['value']
+ elif ctype == "hue":
+ self._chars['hue'] = characteristic['iid']
+ self._features |= SUPPORT_COLOR
+ self._hue = characteristic['value']
+ elif ctype == "saturation":
+ self._chars['saturation'] = characteristic['iid']
+ self._features |= SUPPORT_COLOR
+ self._saturation = characteristic['value']
+
+ @property
+ def is_on(self):
+ """Return true if device is on."""
+ return self._on
+
+ @property
+ def brightness(self):
+ """Return the brightness of this light between 0..255."""
+ if self._features & SUPPORT_BRIGHTNESS:
+ return self._brightness * 255 / 100
+ return None
+
+ @property
+ def hs_color(self):
+ """Return the color property."""
+ if self._features & SUPPORT_COLOR:
+ return (self._hue, self._saturation)
+ return None
+
+ @property
+ def color_temp(self):
+ """Return the color temperature."""
+ if self._features & SUPPORT_COLOR_TEMP:
+ return self._color_temperature
+ return None
+
+ @property
+ def supported_features(self):
+ """Flag supported features."""
+ return self._features
+
+ def turn_on(self, **kwargs):
+ """Turn the specified light on."""
+ hs_color = kwargs.get(ATTR_HS_COLOR)
+ temperature = kwargs.get(ATTR_COLOR_TEMP)
+ brightness = kwargs.get(ATTR_BRIGHTNESS)
+
+ characteristics = []
+ if hs_color is not None:
+ characteristics.append({'aid': self._aid,
+ 'iid': self._chars['hue'],
+ 'value': hs_color[0]})
+ characteristics.append({'aid': self._aid,
+ 'iid': self._chars['saturation'],
+ 'value': hs_color[1]})
+ if brightness is not None:
+ characteristics.append({'aid': self._aid,
+ 'iid': self._chars['brightness'],
+ 'value': int(brightness * 100 / 255)})
+
+ if temperature is not None:
+ characteristics.append({'aid': self._aid,
+ 'iid': self._chars['color-temperature'],
+ 'value': int(temperature)})
+ characteristics.append({'aid': self._aid,
+ 'iid': self._chars['on'],
+ 'value': True})
+ body = json.dumps({'characteristics': characteristics})
+ self._securecon.put('/characteristics', body)
+
+ def turn_off(self, **kwargs):
+ """Turn the specified light off."""
+ characteristics = [{'aid': self._aid,
+ 'iid': self._chars['on'],
+ 'value': False}]
+ body = json.dumps({'characteristics': characteristics})
+ self._securecon.put('/characteristics', body)
diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py
index 1701b886b68..6eb8de99c99 100644
--- a/homeassistant/components/light/hue.py
+++ b/homeassistant/components/light/hue.py
@@ -49,11 +49,17 @@ GROUP_MIN_API_VERSION = (1, 13, 0)
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
- """Set up the Hue lights."""
- if discovery_info is None:
- return
+ """Old way of setting up Hue lights.
- bridge = hass.data[hue.DOMAIN][discovery_info['host']]
+ Can only be called when a user accidentally mentions hue platform in their
+ config. But even in that case it would have been ignored.
+ """
+ pass
+
+
+async def async_setup_entry(hass, config_entry, async_add_devices):
+ """Set up the Hue lights from a config entry."""
+ bridge = hass.data[hue.DOMAIN][config_entry.data['host']]
cur_lights = {}
cur_groups = {}
diff --git a/homeassistant/components/light/nanoleaf_aurora.py b/homeassistant/components/light/nanoleaf_aurora.py
index 2a9066bd55f..99c07166037 100644
--- a/homeassistant/components/light/nanoleaf_aurora.py
+++ b/homeassistant/components/light/nanoleaf_aurora.py
@@ -1,11 +1,6 @@
"""
Support for Nanoleaf Aurora platform.
-Based in large parts upon Software-2's ha-aurora and fully
-reliant on Software-2's nanoleaf-aurora Python Library, see
-https://github.com/software-2/ha-aurora as well as
-https://github.com/software-2/nanoleaf
-
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.nanoleaf_aurora/
"""
@@ -15,9 +10,9 @@ import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR,
- SUPPORT_EFFECT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP,
- SUPPORT_COLOR, PLATFORM_SCHEMA, Light)
-from homeassistant.const import CONF_HOST, CONF_TOKEN, CONF_NAME
+ PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP,
+ SUPPORT_EFFECT, Light)
+from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN
import homeassistant.helpers.config_validation as cv
from homeassistant.util import color as color_util
from homeassistant.util.color import \
@@ -25,20 +20,24 @@ from homeassistant.util.color import \
REQUIREMENTS = ['nanoleaf==0.4.1']
+_LOGGER = logging.getLogger(__name__)
+
+DEFAULT_NAME = 'Aurora'
+
+ICON = 'mdi:triangle-outline'
+
SUPPORT_AURORA = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT |
SUPPORT_COLOR)
-_LOGGER = logging.getLogger(__name__)
-
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_TOKEN): cv.string,
- vol.Optional(CONF_NAME, default='Aurora'): cv.string,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
- """Setup Nanoleaf Aurora device."""
+ """Set up the Nanoleaf Aurora device."""
import nanoleaf
host = config.get(CONF_HOST)
name = config.get(CONF_NAME)
@@ -47,8 +46,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
aurora_light.hass_name = name
if aurora_light.on is None:
- _LOGGER.error("Could not connect to \
- Nanoleaf Aurora: %s on %s", name, host)
+ _LOGGER.error(
+ "Could not connect to Nanoleaf Aurora: %s on %s", name, host)
+ return
+
add_devices([AuroraLight(aurora_light)], True)
@@ -56,7 +57,7 @@ class AuroraLight(Light):
"""Representation of a Nanoleaf Aurora."""
def __init__(self, light):
- """Initialize an Aurora."""
+ """Initialize an Aurora light."""
self._brightness = None
self._color_temp = None
self._effect = None
@@ -99,7 +100,7 @@ class AuroraLight(Light):
@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
- return "mdi:triangle-outline"
+ return ICON
@property
def is_on(self):
@@ -141,10 +142,7 @@ class AuroraLight(Light):
self._light.on = False
def update(self):
- """Fetch new state data for this light.
-
- This is the only method that should fetch new data for Home Assistant.
- """
+ """Fetch new state data for this light."""
self._brightness = self._light.brightness
self._color_temp = self._light.color_temperature
self._effect = self._light.effect
diff --git a/homeassistant/components/light/qwikswitch.py b/homeassistant/components/light/qwikswitch.py
index 26741525b8f..528f4f73c53 100644
--- a/homeassistant/components/light/qwikswitch.py
+++ b/homeassistant/components/light/qwikswitch.py
@@ -27,9 +27,9 @@ class QSLight(QSToggleEntity, Light):
@property
def brightness(self):
"""Return the brightness of this light (0-255)."""
- return self._qsusb[self.qsid, 1] if self._dim else None
+ return self.device.value if self.device.is_dimmer else None
@property
def supported_features(self):
"""Flag supported features."""
- return SUPPORT_BRIGHTNESS if self._dim else 0
+ return SUPPORT_BRIGHTNESS if self.device.is_dimmer else 0
diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py
index 7061c24aac6..d6d860cbd9e 100644
--- a/homeassistant/components/light/yeelight.py
+++ b/homeassistant/components/light/yeelight.py
@@ -24,6 +24,14 @@ REQUIREMENTS = ['yeelight==0.4.0']
_LOGGER = logging.getLogger(__name__)
+LEGACY_DEVICE_TYPE_MAP = {
+ 'color1': 'rgb',
+ 'mono1': 'white',
+ 'strip1': 'strip',
+ 'bslamp1': 'bedside',
+ 'ceiling1': 'ceiling',
+}
+
CONF_TRANSITION = 'transition'
DEFAULT_TRANSITION = 350
@@ -122,8 +130,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if discovery_info is not None:
_LOGGER.debug("Adding autodetected %s", discovery_info['hostname'])
+ device_type = discovery_info['device_type']
+ device_type = LEGACY_DEVICE_TYPE_MAP.get(device_type, device_type)
+
# Not using hostname, as it seems to vary.
- name = "yeelight_%s_%s" % (discovery_info['device_type'],
+ name = "yeelight_%s_%s" % (device_type,
discovery_info['properties']['mac'])
device = {'name': name, 'ipaddr': discovery_info['host']}
diff --git a/homeassistant/components/lock/bmw_connected_drive.py b/homeassistant/components/lock/bmw_connected_drive.py
index c992bf1225a..52734b1259c 100644
--- a/homeassistant/components/lock/bmw_connected_drive.py
+++ b/homeassistant/components/lock/bmw_connected_drive.py
@@ -38,6 +38,7 @@ class BMWLock(LockDevice):
self._vehicle = vehicle
self._attribute = attribute
self._name = '{} {}'.format(self._vehicle.name, self._attribute)
+ self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute)
self._sensor_name = sensor_name
self._state = None
@@ -49,6 +50,11 @@ class BMWLock(LockDevice):
"""
return False
+ @property
+ def unique_id(self):
+ """Return the unique ID of the lock."""
+ return self._unique_id
+
@property
def name(self):
"""Return the name of the lock."""
diff --git a/homeassistant/components/lutron_caseta.py b/homeassistant/components/lutron_caseta.py
index 63f0315f35c..7b1b7417cfd 100644
--- a/homeassistant/components/lutron_caseta.py
+++ b/homeassistant/components/lutron_caseta.py
@@ -14,7 +14,7 @@ from homeassistant.const import CONF_HOST
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['pylutron-caseta==0.3.0']
+REQUIREMENTS = ['pylutron-caseta==0.5.0']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/maxcube.py b/homeassistant/components/maxcube.py
index a0a8db6ba4d..cf5091fc308 100644
--- a/homeassistant/components/maxcube.py
+++ b/homeassistant/components/maxcube.py
@@ -22,12 +22,22 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_PORT = 62910
DOMAIN = 'maxcube'
-MAXCUBE_HANDLE = 'maxcube'
+DATA_KEY = 'maxcube'
+
+NOTIFICATION_ID = 'maxcube_notification'
+NOTIFICATION_TITLE = 'Max!Cube gateway setup'
+
+CONF_GATEWAYS = 'gateways'
+
+CONFIG_GATEWAY = vol.Schema({
+ vol.Required(CONF_HOST): cv.string,
+ vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
+})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
- vol.Required(CONF_HOST): cv.string,
- vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
+ vol.Required(CONF_GATEWAYS, default={}):
+ vol.All(cv.ensure_list, [CONFIG_GATEWAY])
}),
}, extra=vol.ALLOW_EXTRA)
@@ -36,19 +46,31 @@ def setup(hass, config):
"""Establish connection to MAX! Cube."""
from maxcube.connection import MaxCubeConnection
from maxcube.cube import MaxCube
+ if DATA_KEY not in hass.data:
+ hass.data[DATA_KEY] = {}
- host = config.get(DOMAIN).get(CONF_HOST)
- port = config.get(DOMAIN).get(CONF_PORT)
+ connection_failed = 0
+ gateways = config[DOMAIN][CONF_GATEWAYS]
+ for gateway in gateways:
+ host = gateway[CONF_HOST]
+ port = gateway[CONF_PORT]
- try:
- cube = MaxCube(MaxCubeConnection(host, port))
- except timeout:
- _LOGGER.error("Connection to Max!Cube could not be established")
- cube = None
+ try:
+ cube = MaxCube(MaxCubeConnection(host, port))
+ hass.data[DATA_KEY][host] = MaxCubeHandle(cube)
+ except timeout as ex:
+ _LOGGER.error("Unable to connect to Max!Cube gateway: %s", str(ex))
+ hass.components.persistent_notification.create(
+ 'Error: {}
'
+ 'You will need to restart Home Assistant after fixing.'
+ ''.format(ex),
+ title=NOTIFICATION_TITLE,
+ notification_id=NOTIFICATION_ID)
+ connection_failed += 1
+
+ if connection_failed >= len(gateways):
return False
- hass.data[MAXCUBE_HANDLE] = MaxCubeHandle(cube)
-
load_platform(hass, 'climate', DOMAIN)
load_platform(hass, 'binary_sensor', DOMAIN)
diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py
index 85c569789a2..b5fd26b0bcb 100644
--- a/homeassistant/components/media_extractor.py
+++ b/homeassistant/components/media_extractor.py
@@ -14,7 +14,7 @@ from homeassistant.components.media_player import (
SERVICE_PLAY_MEDIA)
from homeassistant.helpers import config_validation as cv
-REQUIREMENTS = ['youtube_dl==2018.04.03']
+REQUIREMENTS = ['youtube_dl==2018.04.16']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/media_player/blackbird.py b/homeassistant/components/media_player/blackbird.py
new file mode 100644
index 00000000000..37b3c0ff819
--- /dev/null
+++ b/homeassistant/components/media_player/blackbird.py
@@ -0,0 +1,213 @@
+"""
+Support for interfacing with Monoprice Blackbird 4k 8x8 HDBaseT Matrix.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/media_player.blackbird
+"""
+import logging
+
+import voluptuous as vol
+
+from homeassistant.components.media_player import (
+ DOMAIN, MEDIA_PLAYER_SCHEMA, PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE,
+ SUPPORT_TURN_OFF, SUPPORT_TURN_ON, MediaPlayerDevice)
+from homeassistant.const import (
+ ATTR_ENTITY_ID, CONF_NAME, CONF_HOST, CONF_PORT, STATE_OFF, STATE_ON)
+import homeassistant.helpers.config_validation as cv
+
+REQUIREMENTS = ['pyblackbird==0.5']
+
+_LOGGER = logging.getLogger(__name__)
+
+SUPPORT_BLACKBIRD = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \
+ SUPPORT_SELECT_SOURCE
+
+ZONE_SCHEMA = vol.Schema({
+ vol.Required(CONF_NAME): cv.string,
+})
+
+SOURCE_SCHEMA = vol.Schema({
+ vol.Required(CONF_NAME): cv.string,
+})
+
+CONF_ZONES = 'zones'
+CONF_SOURCES = 'sources'
+CONF_TYPE = 'type'
+
+DATA_BLACKBIRD = 'blackbird'
+
+SERVICE_SETALLZONES = 'blackbird_set_all_zones'
+ATTR_SOURCE = 'source'
+
+BLACKBIRD_SETALLZONES_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
+ vol.Required(ATTR_SOURCE): cv.string
+})
+
+
+# Valid zone ids: 1-8
+ZONE_IDS = vol.All(vol.Coerce(int), vol.Range(min=1, max=8))
+
+# Valid source ids: 1-8
+SOURCE_IDS = vol.All(vol.Coerce(int), vol.Range(min=1, max=8))
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_TYPE): vol.In(['serial', 'socket']),
+ vol.Optional(CONF_PORT): cv.string,
+ vol.Optional(CONF_HOST): cv.string,
+ vol.Required(CONF_ZONES): vol.Schema({ZONE_IDS: ZONE_SCHEMA}),
+ vol.Required(CONF_SOURCES): vol.Schema({SOURCE_IDS: SOURCE_SCHEMA}),
+})
+
+
+# pylint: disable=unused-argument
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the Monoprice Blackbird 4k 8x8 HDBaseT Matrix platform."""
+ port = config.get(CONF_PORT)
+ host = config.get(CONF_HOST)
+ device_type = config.get(CONF_TYPE)
+
+ import socket
+ from pyblackbird import get_blackbird
+ from serial import SerialException
+
+ if device_type == 'serial':
+ if port is None:
+ _LOGGER.error("No port configured")
+ return
+ try:
+ blackbird = get_blackbird(port)
+ except SerialException:
+ _LOGGER.error("Error connecting to the Blackbird controller")
+ return
+
+ elif device_type == 'socket':
+ try:
+ if host is None:
+ _LOGGER.error("No host configured")
+ return
+ blackbird = get_blackbird(host, False)
+ except socket.timeout:
+ _LOGGER.error("Error connecting to the Blackbird controller")
+ return
+
+ else:
+ _LOGGER.error("Incorrect device type specified")
+ return
+
+ sources = {source_id: extra[CONF_NAME] for source_id, extra
+ in config[CONF_SOURCES].items()}
+
+ hass.data[DATA_BLACKBIRD] = []
+ for zone_id, extra in config[CONF_ZONES].items():
+ _LOGGER.info("Adding zone %d - %s", zone_id, extra[CONF_NAME])
+ hass.data[DATA_BLACKBIRD].append(BlackbirdZone(
+ blackbird, sources, zone_id, extra[CONF_NAME]))
+
+ add_devices(hass.data[DATA_BLACKBIRD], True)
+
+ def service_handle(service):
+ """Handle for services."""
+ entity_ids = service.data.get(ATTR_ENTITY_ID)
+ source = service.data.get(ATTR_SOURCE)
+ if entity_ids:
+ devices = [device for device in hass.data[DATA_BLACKBIRD]
+ if device.entity_id in entity_ids]
+
+ else:
+ devices = hass.data[DATA_BLACKBIRD]
+
+ for device in devices:
+ if service.service == SERVICE_SETALLZONES:
+ device.set_all_zones(source)
+
+ hass.services.register(DOMAIN, SERVICE_SETALLZONES, service_handle,
+ schema=BLACKBIRD_SETALLZONES_SCHEMA)
+
+
+class BlackbirdZone(MediaPlayerDevice):
+ """Representation of a Blackbird matrix zone."""
+
+ def __init__(self, blackbird, sources, zone_id, zone_name):
+ """Initialize new zone."""
+ self._blackbird = blackbird
+ # dict source_id -> source name
+ self._source_id_name = sources
+ # dict source name -> source_id
+ self._source_name_id = {v: k for k, v in sources.items()}
+ # ordered list of all source names
+ self._source_names = sorted(self._source_name_id.keys(),
+ key=lambda v: self._source_name_id[v])
+ self._zone_id = zone_id
+ self._name = zone_name
+ self._state = None
+ self._source = None
+
+ def update(self):
+ """Retrieve latest state."""
+ state = self._blackbird.zone_status(self._zone_id)
+ if not state:
+ return False
+ self._state = STATE_ON if state.power else STATE_OFF
+ idx = state.av
+ if idx in self._source_id_name:
+ self._source = self._source_id_name[idx]
+ else:
+ self._source = None
+ return True
+
+ @property
+ def name(self):
+ """Return the name of the zone."""
+ return self._name
+
+ @property
+ def state(self):
+ """Return the state of the zone."""
+ return self._state
+
+ @property
+ def supported_features(self):
+ """Return flag of media commands that are supported."""
+ return SUPPORT_BLACKBIRD
+
+ @property
+ def media_title(self):
+ """Return the current source as media title."""
+ return self._source
+
+ @property
+ def source(self):
+ """Return the current input source of the device."""
+ return self._source
+
+ @property
+ def source_list(self):
+ """List of available input sources."""
+ return self._source_names
+
+ def set_all_zones(self, source):
+ """Set all zones to one source."""
+ _LOGGER.debug("Setting all zones")
+ if source not in self._source_name_id:
+ return
+ idx = self._source_name_id[source]
+ _LOGGER.debug("Setting all zones source to %s", idx)
+ self._blackbird.set_all_zone_source(idx)
+
+ def select_source(self, source):
+ """Set input source."""
+ if source not in self._source_name_id:
+ return
+ idx = self._source_name_id[source]
+ _LOGGER.debug("Setting zone %d source to %s", self._zone_id, idx)
+ self._blackbird.set_zone_source(self._zone_id, idx)
+
+ def turn_on(self):
+ """Turn the media player on."""
+ _LOGGER.debug("Turning zone %d on", self._zone_id)
+ self._blackbird.set_zone_power(self._zone_id, True)
+
+ def turn_off(self):
+ """Turn the media player off."""
+ _LOGGER.debug("Turning zone %d off", self._zone_id)
+ self._blackbird.set_zone_power(self._zone_id, False)
diff --git a/homeassistant/components/media_player/bluesound.py b/homeassistant/components/media_player/bluesound.py
index 1b6310d4cab..283c4af032e 100644
--- a/homeassistant/components/media_player/bluesound.py
+++ b/homeassistant/components/media_player/bluesound.py
@@ -37,30 +37,30 @@ REQUIREMENTS = ['xmltodict==0.11.0']
_LOGGER = logging.getLogger(__name__)
-STATE_GROUPED = 'grouped'
-
ATTR_MASTER = 'master'
-SERVICE_JOIN = 'bluesound_join'
-SERVICE_UNJOIN = 'bluesound_unjoin'
-SERVICE_SET_TIMER = 'bluesound_set_sleep_timer'
-SERVICE_CLEAR_TIMER = 'bluesound_clear_sleep_timer'
-
DATA_BLUESOUND = 'bluesound'
DEFAULT_PORT = 11000
-SYNC_STATUS_INTERVAL = timedelta(minutes=5)
-UPDATE_CAPTURE_INTERVAL = timedelta(minutes=30)
-UPDATE_SERVICES_INTERVAL = timedelta(minutes=30)
-UPDATE_PRESETS_INTERVAL = timedelta(minutes=30)
NODE_OFFLINE_CHECK_TIMEOUT = 180
NODE_RETRY_INITIATION = timedelta(minutes=3)
+SERVICE_CLEAR_TIMER = 'bluesound_clear_sleep_timer'
+SERVICE_JOIN = 'bluesound_join'
+SERVICE_SET_TIMER = 'bluesound_set_sleep_timer'
+SERVICE_UNJOIN = 'bluesound_unjoin'
+STATE_GROUPED = 'grouped'
+SYNC_STATUS_INTERVAL = timedelta(minutes=5)
+
+UPDATE_CAPTURE_INTERVAL = timedelta(minutes=30)
+UPDATE_PRESETS_INTERVAL = timedelta(minutes=30)
+UPDATE_SERVICES_INTERVAL = timedelta(minutes=30)
+
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOSTS): vol.All(cv.ensure_list, [{
vol.Required(CONF_HOST): cv.string,
- vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_NAME): cv.string,
+ vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
}])
})
@@ -131,8 +131,8 @@ def _add_player(hass, async_add_devices, host, port=None, name=None):
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _init_player)
-async def async_setup_platform(hass, config, async_add_devices,
- discovery_info=None):
+async def async_setup_platform(
+ hass, config, async_add_devices, discovery_info=None):
"""Set up the Bluesound platforms."""
if DATA_BLUESOUND not in hass.data:
hass.data[DATA_BLUESOUND] = []
@@ -202,6 +202,9 @@ class BluesoundPlayer(MediaPlayerDevice):
if self.port is None:
self.port = DEFAULT_PORT
+ class _TimeoutException(Exception):
+ pass
+
@staticmethod
def _try_get_index(string, search_string):
"""Get the index."""
@@ -258,7 +261,8 @@ class BluesoundPlayer(MediaPlayerDevice):
while True:
await self.async_update_status()
- except (asyncio.TimeoutError, ClientError):
+ except (asyncio.TimeoutError, ClientError,
+ BluesoundPlayer._TimeoutException):
_LOGGER.info("Node %s is offline, retrying later", self._name)
await asyncio.sleep(
NODE_OFFLINE_CHECK_TIMEOUT, loop=self._hass.loop)
@@ -293,8 +297,8 @@ class BluesoundPlayer(MediaPlayerDevice):
self._retry_remove = async_track_time_interval(
self._hass, self.async_init, NODE_RETRY_INITIATION)
except Exception:
- _LOGGER.exception("Unexpected when initiating error in %s",
- self.host)
+ _LOGGER.exception(
+ "Unexpected when initiating error in %s", self.host)
raise
async def async_update(self):
@@ -307,8 +311,8 @@ class BluesoundPlayer(MediaPlayerDevice):
await self.async_update_captures()
await self.async_update_services()
- async def send_bluesound_command(self, method, raise_timeout=False,
- allow_offline=False):
+ async def send_bluesound_command(
+ self, method, raise_timeout=False, allow_offline=False):
"""Send command to the player."""
import xmltodict
@@ -321,6 +325,7 @@ class BluesoundPlayer(MediaPlayerDevice):
_LOGGER.debug("Calling URL: %s", url)
response = None
+
try:
websession = async_get_clientsession(self._hass)
with async_timeout.timeout(10, loop=self._hass.loop):
@@ -332,6 +337,9 @@ class BluesoundPlayer(MediaPlayerDevice):
data = None
else:
data = xmltodict.parse(result)
+ elif response.status == 595:
+ _LOGGER.info("Status 595 returned, treating as timeout")
+ raise BluesoundPlayer._TimeoutException()
else:
_LOGGER.error("Error %s on %s", response.status, url)
return None
@@ -366,13 +374,9 @@ class BluesoundPlayer(MediaPlayerDevice):
with async_timeout.timeout(125, loop=self._hass.loop):
response = await self._polling_session.get(
- url,
- headers={CONNECTION: KEEP_ALIVE})
+ url, headers={CONNECTION: KEEP_ALIVE})
- if response.status != 200:
- _LOGGER.error("Error %s on %s. Trying one more time.",
- response.status, url)
- else:
+ if response.status == 200:
result = await response.text()
self._is_online = True
self._last_status_update = dt_util.utcnow()
@@ -380,8 +384,8 @@ class BluesoundPlayer(MediaPlayerDevice):
group_name = self._status.get('groupName', None)
if group_name != self._group_name:
- _LOGGER.debug('Group name change detected on device: %s',
- self.host)
+ _LOGGER.debug(
+ "Group name change detected on device: %s", self.host)
self._group_name = group_name
# the sleep is needed to make sure that the
# devices is synced
@@ -398,14 +402,20 @@ class BluesoundPlayer(MediaPlayerDevice):
await self.force_update_sync_status()
self.async_schedule_update_ha_state()
+ elif response.status == 595:
+ _LOGGER.info("Status 595 returned, treating as timeout")
+ raise BluesoundPlayer._TimeoutException()
+ else:
+ _LOGGER.error("Error %s on %s. Trying one more time",
+ response.status, url)
except (asyncio.TimeoutError, ClientError):
self._is_online = False
self._last_status_update = None
self._status = None
self.async_schedule_update_ha_state()
- _LOGGER.info("Client connection error, marking %s as offline",
- self._name)
+ _LOGGER.info(
+ "Client connection error, marking %s as offline", self._name)
raise
async def async_trigger_sync_on_all(self):
@@ -416,8 +426,8 @@ class BluesoundPlayer(MediaPlayerDevice):
await player.force_update_sync_status()
@Throttle(SYNC_STATUS_INTERVAL)
- async def async_update_sync_status(self, on_updated_cb=None,
- raise_timeout=False):
+ async def async_update_sync_status(
+ self, on_updated_cb=None, raise_timeout=False):
"""Update sync status."""
await self.force_update_sync_status(
on_updated_cb, raise_timeout=False)
@@ -465,7 +475,7 @@ class BluesoundPlayer(MediaPlayerDevice):
'image': item.get('@image', ''),
'is_raw_url': True,
'url2': item.get('@url', ''),
- 'url': 'Preset?id=' + item.get('@id', '')
+ 'url': 'Preset?id={}'.format(item.get('@id', ''))
})
if 'presets' in resp and 'preset' in resp['presets']:
@@ -503,11 +513,6 @@ class BluesoundPlayer(MediaPlayerDevice):
return self._services_items
- @property
- def should_poll(self):
- """No need to poll information."""
- return True
-
@property
def media_content_type(self):
"""Content type of current playing media."""
@@ -803,22 +808,22 @@ class BluesoundPlayer(MediaPlayerDevice):
async def async_add_slave(self, slave_device):
"""Add slave to master."""
- return self.send_bluesound_command('/AddSlave?slave={}&port={}'
- .format(slave_device.host,
- slave_device.port))
+ return await self.send_bluesound_command(
+ '/AddSlave?slave={}&port={}'.format(
+ slave_device.host, slave_device.port))
async def async_remove_slave(self, slave_device):
"""Remove slave to master."""
- return self.send_bluesound_command('/RemoveSlave?slave={}&port={}'
- .format(slave_device.host,
- slave_device.port))
+ return await self.send_bluesound_command(
+ '/RemoveSlave?slave={}&port={}'.format(
+ slave_device.host, slave_device.port))
async def async_increase_timer(self):
"""Increase sleep time on player."""
sleep_time = await self.send_bluesound_command('/Sleep')
if sleep_time is None:
- _LOGGER.error('Error while increasing sleep time on player: %s',
- self.host)
+ _LOGGER.error(
+ "Error while increasing sleep time on player: %s", self.host)
return 0
return int(sleep_time.get('sleep', '0'))
@@ -831,8 +836,9 @@ class BluesoundPlayer(MediaPlayerDevice):
async def async_set_shuffle(self, shuffle):
"""Enable or disable shuffle mode."""
- return self.send_bluesound_command('/Shuffle?state={}'
- .format('1' if shuffle else '0'))
+ value = '1' if shuffle else '0'
+ return await self.send_bluesound_command(
+ '/Shuffle?state={}'.format(value))
async def async_select_source(self, source):
"""Select input source."""
@@ -856,14 +862,14 @@ class BluesoundPlayer(MediaPlayerDevice):
if 'is_raw_url' in selected_source and selected_source['is_raw_url']:
url = selected_source['url']
- return self.send_bluesound_command(url)
+ return await self.send_bluesound_command(url)
async def async_clear_playlist(self):
"""Clear players playlist."""
if self.is_grouped and not self.is_master:
return
- return self.send_bluesound_command('Clear')
+ return await self.send_bluesound_command('Clear')
async def async_media_next_track(self):
"""Send media_next command to media player."""
@@ -877,7 +883,7 @@ class BluesoundPlayer(MediaPlayerDevice):
action['@name'] == 'skip'):
cmd = action['@url']
- return self.send_bluesound_command(cmd)
+ return await self.send_bluesound_command(cmd)
async def async_media_previous_track(self):
"""Send media_previous command to media player."""
@@ -891,35 +897,36 @@ class BluesoundPlayer(MediaPlayerDevice):
action['@name'] == 'back'):
cmd = action['@url']
- return self.send_bluesound_command(cmd)
+ return await self.send_bluesound_command(cmd)
async def async_media_play(self):
"""Send media_play command to media player."""
if self.is_grouped and not self.is_master:
return
- return self.send_bluesound_command('Play')
+ return await self.send_bluesound_command('Play')
async def async_media_pause(self):
"""Send media_pause command to media player."""
if self.is_grouped and not self.is_master:
return
- return self.send_bluesound_command('Pause')
+ return await self.send_bluesound_command('Pause')
async def async_media_stop(self):
"""Send stop command."""
if self.is_grouped and not self.is_master:
return
- return self.send_bluesound_command('Pause')
+ return await self.send_bluesound_command('Pause')
async def async_media_seek(self, position):
"""Send media_seek command to media player."""
if self.is_grouped and not self.is_master:
return
- return self.send_bluesound_command('Play?seek=' + str(float(position)))
+ return await self.send_bluesound_command(
+ 'Play?seek={}'.format(float(position)))
async def async_play_media(self, media_type, media_id, **kwargs):
"""
@@ -933,9 +940,9 @@ class BluesoundPlayer(MediaPlayerDevice):
url = 'Play?url={}'.format(media_id)
if kwargs.get(ATTR_MEDIA_ENQUEUE):
- return self.send_bluesound_command(url)
+ return await self.send_bluesound_command(url)
- return self.send_bluesound_command(url)
+ return await self.send_bluesound_command(url)
async def async_volume_up(self):
"""Volume up the media player."""
@@ -957,7 +964,7 @@ class BluesoundPlayer(MediaPlayerDevice):
volume = 0
elif volume > 1:
volume = 1
- return self.send_bluesound_command(
+ return await self.send_bluesound_command(
'Volume?level=' + str(float(volume) * 100))
async def async_mute_volume(self, mute):
@@ -966,7 +973,7 @@ class BluesoundPlayer(MediaPlayerDevice):
volume = self.volume_level
if volume > 0:
self._lastvol = volume
- return self.send_bluesound_command('Volume?level=0')
+ return await self.send_bluesound_command('Volume?level=0')
else:
- return self.send_bluesound_command(
+ return await self.send_bluesound_command(
'Volume?level=' + str(float(self._lastvol) * 100))
diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py
index 2edda0645b0..30d4bd166d0 100644
--- a/homeassistant/components/media_player/cast.py
+++ b/homeassistant/components/media_player/cast.py
@@ -288,7 +288,8 @@ class CastDevice(MediaPlayerDevice):
self._chromecast = None # type: Optional[pychromecast.Chromecast]
self.cast_status = None
self.media_status = None
- self.media_status_received = None
+ self.media_status_position = None
+ self.media_status_position_received = None
self._available = False # type: bool
self._status_listener = None # type: Optional[CastStatusListener]
@@ -361,7 +362,8 @@ class CastDevice(MediaPlayerDevice):
self._chromecast = None
self.cast_status = None
self.media_status = None
- self.media_status_received = None
+ self.media_status_position = None
+ self.media_status_position_received = None
self._status_listener.invalidate()
self._status_listener = None
@@ -388,8 +390,36 @@ 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):
@@ -595,13 +625,7 @@ class CastDevice(MediaPlayerDevice):
@property
def media_position(self):
"""Position of current playing media in seconds."""
- 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
+ return self.media_status_position
@property
def media_position_updated_at(self):
@@ -609,7 +633,7 @@ class CastDevice(MediaPlayerDevice):
Returns value from homeassistant.util.dt.utcnow().
"""
- return self.media_status_received
+ return self.media_status_position_received
@property
def unique_id(self) -> Optional[str]:
diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py
index 9f2a653b8ee..770d57b5b8e 100644
--- a/homeassistant/components/media_player/kodi.py
+++ b/homeassistant/components/media_player/kodi.py
@@ -8,6 +8,7 @@ import asyncio
from collections import OrderedDict
from functools import wraps
import logging
+import socket
import urllib
import re
@@ -157,13 +158,29 @@ def _check_deprecated_turn_off(hass, turn_off_action):
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Kodi platform."""
if DATA_KODI not in hass.data:
- hass.data[DATA_KODI] = []
- name = config.get(CONF_NAME)
- host = config.get(CONF_HOST)
- port = config.get(CONF_PORT)
- tcp_port = config.get(CONF_TCP_PORT)
- encryption = config.get(CONF_PROXY_SSL)
- websocket = config.get(CONF_ENABLE_WEBSOCKET)
+ hass.data[DATA_KODI] = dict()
+
+ # Is this a manual configuration?
+ if discovery_info is None:
+ name = config.get(CONF_NAME)
+ host = config.get(CONF_HOST)
+ port = config.get(CONF_PORT)
+ tcp_port = config.get(CONF_TCP_PORT)
+ encryption = config.get(CONF_PROXY_SSL)
+ websocket = config.get(CONF_ENABLE_WEBSOCKET)
+ else:
+ name = "{} ({})".format(DEFAULT_NAME, discovery_info.get('hostname'))
+ host = discovery_info.get('host')
+ port = discovery_info.get('port')
+ tcp_port = DEFAULT_TCP_PORT
+ encryption = DEFAULT_PROXY_SSL
+ websocket = DEFAULT_ENABLE_WEBSOCKET
+
+ # Only add a device once, so discovered devices do not override manual
+ # config.
+ ip_addr = socket.gethostbyname(host)
+ if ip_addr in hass.data[DATA_KODI]:
+ return
entity = KodiDevice(
hass,
@@ -175,7 +192,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
turn_off_action=config.get(CONF_TURN_OFF_ACTION),
timeout=config.get(CONF_TIMEOUT), websocket=websocket)
- hass.data[DATA_KODI].append(entity)
+ hass.data[DATA_KODI][ip_addr] = entity
async_add_devices([entity], update_before_add=True)
@asyncio.coroutine
@@ -189,10 +206,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if key != 'entity_id'}
entity_ids = service.data.get('entity_id')
if entity_ids:
- target_players = [player for player in hass.data[DATA_KODI]
+ target_players = [player
+ for player in hass.data[DATA_KODI].values()
if player.entity_id in entity_ids]
else:
- target_players = hass.data[DATA_KODI]
+ target_players = hass.data[DATA_KODI].values()
update_tasks = []
for player in target_players:
diff --git a/homeassistant/components/media_player/mediaroom.py b/homeassistant/components/media_player/mediaroom.py
index a6d5841bb0f..f5b7567aa34 100644
--- a/homeassistant/components/media_player/mediaroom.py
+++ b/homeassistant/components/media_player/mediaroom.py
@@ -8,6 +8,7 @@ import logging
import voluptuous as vol
+from homeassistant.core import callback
from homeassistant.components.media_player import (
MEDIA_TYPE_CHANNEL, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_TURN_OFF,
SUPPORT_TURN_ON, SUPPORT_STOP, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK,
@@ -20,11 +21,11 @@ from homeassistant.helpers.dispatcher import (
from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_OPTIMISTIC, STATE_OFF,
CONF_TIMEOUT, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY,
- STATE_UNAVAILABLE
+ STATE_UNAVAILABLE, EVENT_HOMEASSISTANT_STOP
)
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['pymediaroom==0.6']
+REQUIREMENTS = ['pymediaroom==0.6.3']
_LOGGER = logging.getLogger(__name__)
@@ -81,12 +82,21 @@ async def async_setup_platform(hass, config, async_add_devices,
if not config[CONF_OPTIMISTIC]:
from pymediaroom import install_mediaroom_protocol
- already_installed = hass.data.get(DISCOVERY_MEDIAROOM, False)
+ already_installed = hass.data.get(DISCOVERY_MEDIAROOM, None)
if not already_installed:
- await install_mediaroom_protocol(
+ hass.data[DISCOVERY_MEDIAROOM] = await install_mediaroom_protocol(
responses_callback=callback_notify)
+
+ @callback
+ def stop_discovery(event):
+ """Stop discovery of new mediaroom STB's."""
+ _LOGGER.debug("Stopping internal pymediaroom discovery.")
+ hass.data[DISCOVERY_MEDIAROOM].close()
+
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
+ stop_discovery)
+
_LOGGER.debug("Auto discovery installed")
- hass.data[DISCOVERY_MEDIAROOM] = True
class MediaroomDevice(MediaPlayerDevice):
@@ -120,7 +130,7 @@ class MediaroomDevice(MediaPlayerDevice):
self._channel = None
self._optimistic = optimistic
self._state = STATE_PLAYING if optimistic else STATE_STANDBY
- self._name = 'Mediaroom {}'.format(device_id)
+ self._name = 'Mediaroom {}'.format(device_id if device_id else host)
self._available = True
if device_id:
self._unique_id = device_id
diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py
index 81a18ab93c5..04dd1ac5f2e 100644
--- a/homeassistant/components/media_player/mpd.py
+++ b/homeassistant/components/media_player/mpd.py
@@ -23,7 +23,7 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
-REQUIREMENTS = ['python-mpd2==0.5.5']
+REQUIREMENTS = ['python-mpd2==1.0.0']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py
index 432d9ce108f..58703165385 100644
--- a/homeassistant/components/media_player/onkyo.py
+++ b/homeassistant/components/media_player/onkyo.py
@@ -22,6 +22,7 @@ REQUIREMENTS = ['onkyo-eiscp==1.2.4']
_LOGGER = logging.getLogger(__name__)
CONF_SOURCES = 'sources'
+CONF_ZONE2 = 'zone2'
DEFAULT_NAME = 'Onkyo Receiver'
@@ -40,6 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SOURCES, default=DEFAULT_SOURCES):
{cv.string: cv.string},
+ vol.Optional(CONF_ZONE2, default=False): cv.boolean,
})
@@ -57,6 +59,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
eiscp.eISCP(host), config.get(CONF_SOURCES),
name=config.get(CONF_NAME)))
KNOWN_HOSTS.append(host)
+
+ # Add Zone2 if configured
+ if config.get(CONF_ZONE2):
+ _LOGGER.debug("Setting up zone 2")
+ hosts.append(OnkyoDeviceZone2(eiscp.eISCP(host),
+ config.get(CONF_SOURCES),
+ name=config.get(CONF_NAME) +
+ " Zone 2"))
except OSError:
_LOGGER.error("Unable to connect to receiver at %s", host)
else:
@@ -98,8 +108,9 @@ class OnkyoDevice(MediaPlayerDevice):
return result
def update(self):
- """Get the latest details from the device."""
+ """Get the latest state from the device."""
status = self.command('system-power query')
+
if not status:
return
if status[1] == 'on':
@@ -107,9 +118,11 @@ class OnkyoDevice(MediaPlayerDevice):
else:
self._pwstate = STATE_OFF
return
+
volume_raw = self.command('volume query')
mute_raw = self.command('audio-muting query')
current_source_raw = self.command('input-selector query')
+
if not (volume_raw and mute_raw and current_source_raw):
return
@@ -147,12 +160,12 @@ class OnkyoDevice(MediaPlayerDevice):
@property
def is_volume_muted(self):
- """Boolean if volume is currently muted."""
+ """Return boolean indicating mute status."""
return self._muted
@property
def supported_features(self):
- """Flag media player features that are supported."""
+ """Return media player features that are supported."""
return SUPPORT_ONKYO
@property
@@ -166,7 +179,7 @@ class OnkyoDevice(MediaPlayerDevice):
return self._source_list
def turn_off(self):
- """Turn off media player."""
+ """Turn the media player off."""
self.command('system-power standby')
def set_volume_level(self, volume):
@@ -189,3 +202,68 @@ class OnkyoDevice(MediaPlayerDevice):
if source in self._source_list:
source = self._reverse_mapping[source]
self.command('input-selector {}'.format(source))
+
+
+class OnkyoDeviceZone2(OnkyoDevice):
+ """Representation of an Onkyo device's zone 2."""
+
+ def update(self):
+ """Get the latest state from the device."""
+ status = self.command('zone2.power=query')
+
+ if not status:
+ return
+ if status[1] == 'on':
+ self._pwstate = STATE_ON
+ else:
+ self._pwstate = STATE_OFF
+ return
+
+ volume_raw = self.command('zone2.volume=query')
+ mute_raw = self.command('zone2.muting=query')
+ current_source_raw = self.command('zone2.selector=query')
+
+ if not (volume_raw and mute_raw and current_source_raw):
+ return
+
+ # eiscp can return string or tuple. Make everything tuples.
+ if isinstance(current_source_raw[1], str):
+ current_source_tuples = \
+ (current_source_raw[0], (current_source_raw[1],))
+ else:
+ current_source_tuples = current_source_raw
+
+ for source in current_source_tuples[1]:
+ if source in self._source_mapping:
+ self._current_source = self._source_mapping[source]
+ break
+ else:
+ self._current_source = '_'.join(
+ [i for i in current_source_tuples[1]])
+ self._muted = bool(mute_raw[1] == 'on')
+ self._volume = volume_raw[1] / 80.0
+
+ def turn_off(self):
+ """Turn the media player off."""
+ self.command('zone2.power=standby')
+
+ def set_volume_level(self, volume):
+ """Set volume level, input is range 0..1. Onkyo ranges from 1-80."""
+ self.command('zone2.volume={}'.format(int(volume*80)))
+
+ def mute_volume(self, mute):
+ """Mute (true) or unmute (false) media player."""
+ if mute:
+ self.command('zone2.muting=on')
+ else:
+ self.command('zone2.muting=off')
+
+ def turn_on(self):
+ """Turn the media player on."""
+ self.command('zone2.power=on')
+
+ def select_source(self, source):
+ """Set the input source."""
+ if source in self._source_list:
+ source = self._reverse_mapping[source]
+ self.command('zone2.selector={}'.format(source))
diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml
index 95072f0270c..0a6c413a688 100644
--- a/homeassistant/components/media_player/services.yaml
+++ b/homeassistant/components/media_player/services.yaml
@@ -402,3 +402,13 @@ songpal_set_sound_setting:
value:
description: Value to set.
example: 'on'
+
+blackbird_set_all_zones:
+ description: Set all Blackbird zones to a single source.
+ fields:
+ entity_id:
+ description: Name of any blackbird zone.
+ example: 'media_player.zone_1'
+ source:
+ description: Name of source to switch to.
+ example: 'Source 1'
diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py
index 86b4087ca81..371ad890364 100644
--- a/homeassistant/components/media_player/squeezebox.py
+++ b/homeassistant/components/media_player/squeezebox.py
@@ -266,6 +266,8 @@ class SqueezeBoxDevice(MediaPlayerDevice):
if response is False:
return
+ last_media_position = self.media_position
+
self._status = {}
try:
@@ -278,7 +280,11 @@ class SqueezeBoxDevice(MediaPlayerDevice):
pass
self._status.update(response)
- self._last_update = utcnow()
+
+ if self.media_position != last_media_position:
+ _LOGGER.debug('Media position updated for %s: %s',
+ self, self.media_position)
+ self._last_update = utcnow()
@property
def volume_level(self):
diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py
index ae9d259a47c..d7682a611b9 100644
--- a/homeassistant/components/media_player/webostv.py
+++ b/homeassistant/components/media_player/webostv.py
@@ -344,6 +344,42 @@ class LgWebOSDevice(MediaPlayerDevice):
self._current_source = source_dict['label']
self._client.set_input(source_dict['id'])
+ def play_media(self, media_type, media_id, **kwargs):
+ """Play a piece of media."""
+ _LOGGER.debug(
+ "Call play media type <%s>, Id <%s>", media_type, media_id)
+
+ if media_type == MEDIA_TYPE_CHANNEL:
+ _LOGGER.debug("Searching channel...")
+ partial_match_channel_id = None
+
+ for channel in self._client.get_channels():
+ _LOGGER.debug(
+ "Checking channel number <%s>, name <%s>, id <%s>...",
+ channel['channelNumber'],
+ channel['channelName'],
+ channel['channelId'])
+ if media_id == channel['channelNumber']:
+ _LOGGER.debug(
+ "Perfect match on channel number: switching!")
+ self._client.set_channel(channel['channelId'])
+ return
+ elif media_id.lower() == channel['channelName'].lower():
+ _LOGGER.debug(
+ "Perfect match on channel name: switching!")
+ self._client.set_channel(channel['channelId'])
+ return
+ elif media_id.lower() in channel['channelName'].lower():
+ _LOGGER.debug(
+ "Partial match on channel name: saving it...")
+ partial_match_channel_id = channel['channelId']
+
+ if partial_match_channel_id is not None:
+ _LOGGER.debug(
+ "Using partial match on channel name: switching!")
+ self._client.set_channel(partial_match_channel_id)
+ return
+
def media_play(self):
"""Send play command."""
self._playing = True
diff --git a/homeassistant/components/notify/clicksend.py b/homeassistant/components/notify/clicksend.py
index 2b2cb4e7f22..c028da2c579 100644
--- a/homeassistant/components/notify/clicksend.py
+++ b/homeassistant/components/notify/clicksend.py
@@ -37,7 +37,8 @@ PLATFORM_SCHEMA = vol.Schema(
vol.All(PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_API_KEY): cv.string,
- vol.Required(CONF_RECIPIENT): cv.string,
+ vol.Required(CONF_RECIPIENT, default=[]):
+ vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_SENDER): cv.string,
}), validate_sender))
@@ -59,21 +60,19 @@ class ClicksendNotificationService(BaseNotificationService):
"""Initialize the service."""
self.username = config.get(CONF_USERNAME)
self.api_key = config.get(CONF_API_KEY)
- self.recipient = config.get(CONF_RECIPIENT)
+ self.recipients = config.get(CONF_RECIPIENT)
self.sender = config.get(CONF_SENDER, CONF_RECIPIENT)
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
- data = ({
- 'messages': [
- {
- 'source': 'hass.notify',
- 'from': self.sender,
- 'to': self.recipient,
- 'body': message,
- }
- ]
- })
+ data = {"messages": []}
+ for recipient in self.recipients:
+ data["messages"].append({
+ 'source': 'hass.notify',
+ 'from': self.sender,
+ 'to': recipient,
+ 'body': message,
+ })
api_url = "{}/sms/send".format(BASE_API_URL)
diff --git a/homeassistant/components/notify/facebook.py b/homeassistant/components/notify/facebook.py
index 791440fdb5b..b73f845ea17 100644
--- a/homeassistant/components/notify/facebook.py
+++ b/homeassistant/components/notify/facebook.py
@@ -4,6 +4,7 @@ Facebook platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.facebook/
"""
+import json
import logging
from aiohttp.hdrs import CONTENT_TYPE
@@ -19,6 +20,8 @@ _LOGGER = logging.getLogger(__name__)
CONF_PAGE_ACCESS_TOKEN = 'page_access_token'
BASE_URL = 'https://graph.facebook.com/v2.6/me/messages'
+CREATE_BROADCAST_URL = 'https://graph.facebook.com/v2.11/me/message_creatives'
+SEND_BROADCAST_URL = 'https://graph.facebook.com/v2.11/me/broadcast_messages'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PAGE_ACCESS_TOKEN): cv.string,
@@ -55,27 +58,60 @@ class FacebookNotificationService(BaseNotificationService):
_LOGGER.error("At least 1 target is required")
return
- for target in targets:
- # If the target starts with a "+", we suppose it's a phone number,
- # otherwise it's a user id.
- if target.startswith('+'):
- recipient = {"phone_number": target}
- else:
- recipient = {"id": target}
+ # broadcast message
+ if targets[0].lower() == 'broadcast':
+ broadcast_create_body = {"messages": [body_message]}
+ _LOGGER.debug("Broadcast body %s : ", broadcast_create_body)
- body = {
- "recipient": recipient,
- "message": body_message
+ resp = requests.post(CREATE_BROADCAST_URL,
+ data=json.dumps(broadcast_create_body),
+ params=payload,
+ headers={CONTENT_TYPE: CONTENT_TYPE_JSON},
+ timeout=10)
+ _LOGGER.debug("FB Messager broadcast id %s : ", resp.json())
+
+ # at this point we get broadcast id
+ broadcast_body = {
+ "message_creative_id": resp.json().get('message_creative_id'),
+ "notification_type": "REGULAR",
}
- import json
- resp = requests.post(BASE_URL, data=json.dumps(body),
+
+ resp = requests.post(SEND_BROADCAST_URL,
+ data=json.dumps(broadcast_body),
params=payload,
headers={CONTENT_TYPE: CONTENT_TYPE_JSON},
timeout=10)
if resp.status_code != 200:
- obj = resp.json()
- error_message = obj['error']['message']
- error_code = obj['error']['code']
- _LOGGER.error(
- "Error %s : %s (Code %s)", resp.status_code, error_message,
- error_code)
+ log_error(resp)
+
+ # non-broadcast message
+ else:
+ for target in targets:
+ # If the target starts with a "+", it's a phone number,
+ # otherwise it's a user id.
+ if target.startswith('+'):
+ recipient = {"phone_number": target}
+ else:
+ recipient = {"id": target}
+
+ body = {
+ "recipient": recipient,
+ "message": body_message
+ }
+ resp = requests.post(BASE_URL, data=json.dumps(body),
+ params=payload,
+ headers={CONTENT_TYPE: CONTENT_TYPE_JSON},
+ timeout=10)
+ if resp.status_code != 200:
+ log_error(resp)
+
+
+def log_error(response):
+ """Log error message."""
+ obj = response.json()
+ error_message = obj['error']['message']
+ error_code = obj['error']['code']
+
+ _LOGGER.error(
+ "Error %s : %s (Code %s)", response.status_code, error_message,
+ error_code)
diff --git a/homeassistant/components/notify/xmpp.py b/homeassistant/components/notify/xmpp.py
index 806acdb6d09..12ddf49fca8 100644
--- a/homeassistant/components/notify/xmpp.py
+++ b/homeassistant/components/notify/xmpp.py
@@ -76,8 +76,6 @@ def send_message(sender, password, recipient, use_tls,
"""Initialize the Jabber Bot."""
super(SendNotificationBot, self).__init__(sender, password)
- logging.basicConfig(level=logging.ERROR)
-
self.use_tls = use_tls
self.use_ipv6 = False
self.add_event_handler('failed_auth', self.check_credentials)
diff --git a/homeassistant/components/prometheus.py b/homeassistant/components/prometheus.py
index f9629ca726a..dc1cbd945a7 100644
--- a/homeassistant/components/prometheus.py
+++ b/homeassistant/components/prometheus.py
@@ -185,6 +185,9 @@ class Metrics(object):
unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
metric = state.entity_id.split(".")[1]
+ if '_' not in str(metric):
+ metric = state.entity_id.replace('.', '_')
+
try:
int(metric.split("_")[-1])
metric = "_".join(metric.split("_")[:-1])
diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py
index 708eff7cf11..3dc16f513dc 100644
--- a/homeassistant/components/qwikswitch.py
+++ b/homeassistant/components/qwikswitch.py
@@ -18,7 +18,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.components.light import ATTR_BRIGHTNESS
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['pyqwikswitch==0.6']
+REQUIREMENTS = ['pyqwikswitch==0.71']
_LOGGER = logging.getLogger(__name__)
@@ -34,17 +34,54 @@ CONFIG_SCHEMA = vol.Schema({
vol.Coerce(str),
vol.Optional(CONF_DIMMER_ADJUST, default=1): CV_DIM_VALUE,
vol.Optional(CONF_BUTTON_EVENTS, default=[]): cv.ensure_list_csv,
- vol.Optional(CONF_SENSORS, default={}): vol.Schema({cv.slug: str}),
+ vol.Optional(CONF_SENSORS, default=[]): vol.All(
+ cv.ensure_list, [vol.Schema({
+ vol.Required('id'): str,
+ vol.Optional('channel', default=1): int,
+ vol.Required('name'): str,
+ vol.Required('type'): str,
+ })]),
vol.Optional(CONF_SWITCHES, default=[]): vol.All(
cv.ensure_list, [str])
})}, extra=vol.ALLOW_EXTRA)
-class QSToggleEntity(Entity):
- """Representation of a Qwikswitch Entity.
+class QSEntity(Entity):
+ """Qwikswitch Entity base."""
- Implement base QS methods. Modeled around HA ToggleEntity[1] & should only
- be used in a class that extends both QSToggleEntity *and* ToggleEntity.
+ def __init__(self, qsid, name):
+ """Initialize the QSEntity."""
+ self._name = name
+ self.qsid = qsid
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def poll(self):
+ """QS sensors gets packets in update_packet."""
+ return False
+
+ @property
+ def unique_id(self):
+ """Return a unique identifier for this sensor."""
+ return "qs{}".format(self.qsid)
+
+ @callback
+ def update_packet(self, packet):
+ """Receive update packet from QSUSB. Match dispather_send signature."""
+ self.async_schedule_update_ha_state()
+
+ async def async_added_to_hass(self):
+ """Listen for updates from QSUSb via dispatcher."""
+ self.hass.helpers.dispatcher.async_dispatcher_connect(
+ self.qsid, self.update_packet)
+
+
+class QSToggleEntity(QSEntity):
+ """Representation of a Qwikswitch Toggle Entity.
Implemented:
- QSLight extends QSToggleEntity and Light[2] (ToggleEntity[1])
@@ -57,52 +94,28 @@ class QSToggleEntity(Entity):
def __init__(self, qsid, qsusb):
"""Initialize the ToggleEntity."""
- from pyqwikswitch import (QS_NAME, QSDATA, QS_TYPE, QSType)
- self.qsid = qsid
- self._qsusb = qsusb.devices
- dev = qsusb.devices[qsid]
- self._dim = dev[QS_TYPE] == QSType.dimmer
- self._name = dev[QSDATA][QS_NAME]
-
- @property
- def should_poll(self):
- """No polling needed."""
- return False
-
- @property
- def name(self):
- """Return the name of the light."""
- return self._name
+ self.device = qsusb.devices[qsid]
+ super().__init__(qsid, self.device.name)
@property
def is_on(self):
"""Check if device is on (non-zero)."""
- return self._qsusb[self.qsid, 1] > 0
+ return self.device.value > 0
async def async_turn_on(self, **kwargs):
"""Turn the device on."""
new = kwargs.get(ATTR_BRIGHTNESS, 255)
- self._qsusb.set_value(self.qsid, new)
+ self.hass.data[DOMAIN].devices.set_value(self.qsid, new)
async def async_turn_off(self, **_):
"""Turn the device off."""
- self._qsusb.set_value(self.qsid, 0)
-
- def _update(self, _packet=None):
- """Schedule an update - match dispather_send signature."""
- self.async_schedule_update_ha_state()
-
- async def async_added_to_hass(self):
- """Listen for updates from QSUSb via dispatcher."""
- self.hass.helpers.dispatcher.async_dispatcher_connect(
- self.qsid, self._update)
+ self.hass.data[DOMAIN].devices.set_value(self.qsid, 0)
async def async_setup(hass, config):
"""Qwiskswitch component setup."""
from pyqwikswitch.async_ import QSUsb
- from pyqwikswitch import (
- CMD_BUTTONS, QS_CMD, QS_ID, QS_TYPE, QSType)
+ from pyqwikswitch import CMD_BUTTONS, QS_CMD, QS_ID, QSType
# Add cmd's to in /&listen packets will fire events
# By default only buttons of type [TOGGLE,SCENE EXE,LEVEL]
@@ -112,8 +125,8 @@ async def async_setup(hass, config):
url = config[DOMAIN][CONF_URL]
dimmer_adjust = config[DOMAIN][CONF_DIMMER_ADJUST]
- sensors = config[DOMAIN]['sensors']
- switches = config[DOMAIN]['switches']
+ sensors = config[DOMAIN][CONF_SENSORS]
+ switches = config[DOMAIN][CONF_SWITCHES]
def callback_value_changed(_qsd, qsid, _val):
"""Update entity values based on device change."""
@@ -131,17 +144,17 @@ async def async_setup(hass, config):
hass.data[DOMAIN] = qsusb
_new = {'switch': [], 'light': [], 'sensor': sensors}
- for _id, item in qsusb.devices:
- if _id in switches:
- if item[QS_TYPE] != QSType.relay:
+ 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", _id)
+ "You specified a switch that is not a relay %s", qsid)
continue
- _new['switch'].append(_id)
- elif item[QS_TYPE] in [QSType.relay, QSType.dimmer]:
- _new['light'].append(_id)
+ _new['switch'].append(qsid)
+ elif dev.qstype in (QSType.relay, QSType.dimmer):
+ _new['light'].append(qsid)
else:
- _LOGGER.warning("Ignored unknown QSUSB device: %s", item)
+ _LOGGER.warning("Ignored unknown QSUSB device: %s", dev)
continue
# Load platforms
@@ -149,24 +162,21 @@ async def async_setup(hass, config):
if comp_conf:
load_platform(hass, comp_name, DOMAIN, {DOMAIN: comp_conf}, config)
- def callback_qs_listen(item):
+ def callback_qs_listen(qspacket):
"""Typically a button press or update signal."""
# If button pressed, fire a hass event
- if QS_ID in item:
- if item.get(QS_CMD, '') in cmd_buttons:
+ if QS_ID in qspacket:
+ if qspacket.get(QS_CMD, '') in cmd_buttons:
hass.bus.async_fire(
- 'qwikswitch.button.{}'.format(item[QS_ID]), item)
+ 'qwikswitch.button.{}'.format(qspacket[QS_ID]), qspacket)
return
- # Private method due to bad __iter__ design in qsusb
- # qsusb.devices returns a list of tuples
- if item[QS_ID] not in \
- qsusb.devices._data: # pylint: disable=protected-access
+ if qspacket[QS_ID] not in qsusb.devices:
# Not a standard device in, component can handle packet
# i.e. sensors
- _LOGGER.debug("Dispatch %s ((%s))", item[QS_ID], item)
+ _LOGGER.debug("Dispatch %s ((%s))", qspacket[QS_ID], qspacket)
hass.helpers.dispatcher.async_dispatcher_send(
- item[QS_ID], item)
+ qspacket[QS_ID], qspacket)
# Update all ha_objects
hass.async_add_job(qsusb.update_from_devices)
diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py
index f10e0fc75d7..64e2b85f611 100644
--- a/homeassistant/components/recorder/__init__.py
+++ b/homeassistant/components/recorder/__init__.py
@@ -35,7 +35,7 @@ from . import migration, purge
from .const import DATA_INSTANCE
from .util import session_scope
-REQUIREMENTS = ['sqlalchemy==1.2.5']
+REQUIREMENTS = ['sqlalchemy==1.2.6']
_LOGGER = logging.getLogger(__name__)
@@ -47,9 +47,8 @@ ATTR_KEEP_DAYS = 'keep_days'
ATTR_REPACK = 'repack'
SERVICE_PURGE_SCHEMA = vol.Schema({
- vol.Optional(ATTR_KEEP_DAYS):
- vol.All(vol.Coerce(int), vol.Range(min=0)),
- vol.Optional(ATTR_REPACK, default=False): cv.boolean
+ vol.Optional(ATTR_KEEP_DAYS): vol.All(vol.Coerce(int), vol.Range(min=0)),
+ vol.Optional(ATTR_REPACK, default=False): cv.boolean,
})
DEFAULT_URL = 'sqlite:///{hass_config_path}'
diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py
index e0bf3c86b05..2bc35a034f4 100644
--- a/homeassistant/components/sensor/__init__.py
+++ b/homeassistant/components/sensor/__init__.py
@@ -8,6 +8,8 @@ https://home-assistant.io/components/sensor/
from datetime import timedelta
import logging
+import voluptuous as vol
+
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
@@ -18,6 +20,13 @@ DOMAIN = 'sensor'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
SCAN_INTERVAL = timedelta(seconds=30)
+DEVICE_CLASSES = [
+ 'battery', # % of battery that is left
+ 'humidity', # % of humidity in the air
+ 'temperature', # temperature (C/F)
+]
+
+DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
async def async_setup(hass, config):
diff --git a/homeassistant/components/sensor/alpha_vantage.py b/homeassistant/components/sensor/alpha_vantage.py
index 896497a93d5..77d8ba9322f 100644
--- a/homeassistant/components/sensor/alpha_vantage.py
+++ b/homeassistant/components/sensor/alpha_vantage.py
@@ -15,7 +15,7 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['alpha_vantage==1.9.0']
+REQUIREMENTS = ['alpha_vantage==2.0.0']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/sensor/bmw_connected_drive.py b/homeassistant/components/sensor/bmw_connected_drive.py
index bd582da1ef4..ed75520c179 100644
--- a/homeassistant/components/sensor/bmw_connected_drive.py
+++ b/homeassistant/components/sensor/bmw_connected_drive.py
@@ -52,6 +52,7 @@ class BMWConnectedDriveSensor(Entity):
self._state = None
self._unit_of_measurement = None
self._name = '{} {}'.format(self._vehicle.name, self._attribute)
+ self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute)
self._sensor_name = sensor_name
self._icon = icon
@@ -60,6 +61,11 @@ class BMWConnectedDriveSensor(Entity):
"""Data update is triggered from BMWConnectedDriveEntity."""
return False
+ @property
+ def unique_id(self):
+ """Return the unique ID of the sensor."""
+ return self._unique_id
+
@property
def name(self) -> str:
"""Return the name of the sensor."""
@@ -86,7 +92,7 @@ class BMWConnectedDriveSensor(Entity):
@property
def device_state_attributes(self):
- """Return the state attributes of the binary sensor."""
+ """Return the state attributes of the sensor."""
return {
'car': self._vehicle.name
}
diff --git a/homeassistant/components/sensor/broadlink.py b/homeassistant/components/sensor/broadlink.py
index 044b77ebfe8..5182ba4530e 100644
--- a/homeassistant/components/sensor/broadlink.py
+++ b/homeassistant/components/sensor/broadlink.py
@@ -56,9 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
name = config.get(CONF_NAME)
timeout = config.get(CONF_TIMEOUT)
update_interval = config.get(CONF_UPDATE_INTERVAL)
-
broadlink_data = BroadlinkData(update_interval, host, mac_addr, timeout)
-
dev = []
for variable in config[CONF_MONITORED_CONDITIONS]:
dev.append(BroadlinkSensor(name, broadlink_data, variable))
@@ -104,10 +102,11 @@ class BroadlinkData(object):
def __init__(self, interval, ip_addr, mac_addr, timeout):
"""Initialize the data object."""
- import broadlink
self.data = None
- self._device = broadlink.a1((ip_addr, 80), mac_addr, None)
- self._device.timeout = timeout
+ self.ip_addr = ip_addr
+ self.mac_addr = mac_addr
+ self.timeout = timeout
+ self._connect()
self._schema = vol.Schema({
vol.Optional('temperature'): vol.Range(min=-50, max=150),
vol.Optional('humidity'): vol.Range(min=0, max=100),
@@ -119,6 +118,11 @@ class BroadlinkData(object):
if not self._auth():
_LOGGER.warning("Failed to connect to device")
+ def _connect(self):
+ import broadlink
+ self._device = broadlink.a1((self.ip_addr, 80), self.mac_addr, None)
+ self._device.timeout = self.timeout
+
def _update(self, retry=3):
try:
data = self._device.check_sensors_raw()
@@ -140,5 +144,6 @@ class BroadlinkData(object):
except socket.timeout:
auth = False
if not auth and retry > 0:
+ self._connect()
return self._auth(retry-1)
return auth
diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py
index 081b304dc55..e569c5578ac 100644
--- a/homeassistant/components/sensor/deconz.py
+++ b/homeassistant/components/sensor/deconz.py
@@ -16,6 +16,7 @@ from homeassistant.util import slugify
DEPENDENCIES = ['deconz']
ATTR_CURRENT = 'current'
+ATTR_DAYLIGHT = 'daylight'
ATTR_EVENT_ID = 'event_id'
@@ -113,6 +114,8 @@ class DeconzSensor(Entity):
if self.unit_of_measurement == 'Watts':
attr[ATTR_CURRENT] = self._sensor.current
attr[ATTR_VOLTAGE] = self._sensor.voltage
+ if self._sensor.sensor_class == 'daylight':
+ attr[ATTR_DAYLIGHT] = self._sensor.daylight
return attr
diff --git a/homeassistant/components/sensor/ebox.py b/homeassistant/components/sensor/ebox.py
index eee959fceba..aca2d7bdb9a 100644
--- a/homeassistant/components/sensor/ebox.py
+++ b/homeassistant/components/sensor/ebox.py
@@ -19,7 +19,8 @@ from homeassistant.const import (
CONF_NAME, CONF_MONITORED_VARIABLES)
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['pyebox==0.1.0']
+# pylint: disable=import-error
+REQUIREMENTS = [] # ['pyebox==0.1.0'] - disabled because it breaks pip10
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/sensor/ecobee.py b/homeassistant/components/sensor/ecobee.py
index dad770d5bab..7274f421f15 100644
--- a/homeassistant/components/sensor/ecobee.py
+++ b/homeassistant/components/sensor/ecobee.py
@@ -52,6 +52,13 @@ class EcobeeSensor(Entity):
"""Return the name of the Ecobee sensor."""
return self._name
+ @property
+ def device_class(self):
+ """Return the device class of the sensor."""
+ if self.type in ('temperature', 'humidity'):
+ return self.type
+ return None
+
@property
def state(self):
"""Return the state of the sensor."""
diff --git a/homeassistant/components/sensor/eddystone_temperature.py b/homeassistant/components/sensor/eddystone_temperature.py
index 06accb26eb6..2c8ad4781d0 100644
--- a/homeassistant/components/sensor/eddystone_temperature.py
+++ b/homeassistant/components/sensor/eddystone_temperature.py
@@ -18,7 +18,7 @@ from homeassistant.const import (
CONF_NAME, TEMP_CELSIUS, STATE_UNKNOWN, EVENT_HOMEASSISTANT_STOP,
EVENT_HOMEASSISTANT_START)
-REQUIREMENTS = ['beacontools[scan]==1.2.1', 'construct==2.9.41']
+REQUIREMENTS = ['beacontools[scan]==1.2.3', 'construct==2.9.41']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py
index 3faf51a5f47..5b28faf78ca 100644
--- a/homeassistant/components/sensor/filter.py
+++ b/homeassistant/components/sensor/filter.py
@@ -8,6 +8,9 @@ import logging
import statistics
from collections import deque, Counter
from numbers import Number
+from functools import partial
+from copy import copy
+from datetime import timedelta
import voluptuous as vol
@@ -20,6 +23,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.util.decorator import Registry
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_state_change
+import homeassistant.components.history as history
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
@@ -40,6 +44,9 @@ CONF_TIME_SMA_TYPE = 'type'
TIME_SMA_LAST = 'last'
+WINDOW_SIZE_UNIT_NUMBER_EVENTS = 1
+WINDOW_SIZE_UNIT_TIME = 2
+
DEFAULT_WINDOW_SIZE = 1
DEFAULT_PRECISION = 2
DEFAULT_FILTER_RADIUS = 2.0
@@ -123,21 +130,22 @@ class SensorFilter(Entity):
async def async_added_to_hass(self):
"""Register callbacks."""
@callback
- def filter_sensor_state_listener(entity, old_state, new_state):
+ def filter_sensor_state_listener(entity, old_state, new_state,
+ update_ha=True):
"""Handle device state changes."""
if new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]:
return
- temp_state = new_state.state
+ temp_state = new_state
try:
for filt in self._filters:
- filtered_state = filt.filter_state(temp_state)
+ filtered_state = filt.filter_state(copy(temp_state))
_LOGGER.debug("%s(%s=%s) -> %s", filt.name,
self._entity,
- temp_state,
+ temp_state.state,
"skip" if filt.skip_processing else
- filtered_state)
+ filtered_state.state)
if filt.skip_processing:
return
temp_state = filtered_state
@@ -146,7 +154,7 @@ class SensorFilter(Entity):
self._state)
return
- self._state = temp_state
+ self._state = temp_state.state
if self._icon is None:
self._icon = new_state.attributes.get(
@@ -156,7 +164,50 @@ class SensorFilter(Entity):
self._unit_of_measurement = new_state.attributes.get(
ATTR_UNIT_OF_MEASUREMENT)
- self.async_schedule_update_ha_state()
+ if update_ha:
+ self.async_schedule_update_ha_state()
+
+ if 'recorder' in self.hass.config.components:
+ history_list = []
+ largest_window_items = 0
+ largest_window_time = timedelta(0)
+
+ # Determine the largest window_size by type
+ for filt in self._filters:
+ if filt.window_unit == WINDOW_SIZE_UNIT_NUMBER_EVENTS\
+ and largest_window_items < filt.window_size:
+ largest_window_items = filt.window_size
+ elif filt.window_unit == WINDOW_SIZE_UNIT_TIME\
+ and largest_window_time < filt.window_size:
+ largest_window_time = filt.window_size
+
+ # Retrieve the largest window_size of each type
+ if largest_window_items > 0:
+ filter_history = await self.hass.async_add_job(partial(
+ history.get_last_state_changes, self.hass,
+ largest_window_items, entity_id=self._entity))
+ history_list.extend(
+ [state for state in filter_history[self._entity]])
+ if largest_window_time > timedelta(seconds=0):
+ start = dt_util.utcnow() - largest_window_time
+ filter_history = await self.hass.async_add_job(partial(
+ history.state_changes_during_period, self.hass,
+ start, entity_id=self._entity))
+ history_list.extend(
+ [state for state in filter_history[self._entity]
+ if state not in history_list])
+
+ # Sort the window states
+ history_list = sorted(history_list, key=lambda s: s.last_updated)
+ _LOGGER.debug("Loading from history: %s",
+ [(s.state, s.last_updated) for s in history_list])
+
+ # Replay history through the filter chain
+ prev_state = None
+ for state in history_list:
+ filter_sensor_state_listener(
+ self._entity, prev_state, state, False)
+ prev_state = state
async_track_state_change(
self.hass, self._entity, filter_sensor_state_listener)
@@ -195,6 +246,31 @@ class SensorFilter(Entity):
return state_attr
+class FilterState(object):
+ """State abstraction for filter usage."""
+
+ def __init__(self, state):
+ """Initialize with HA State object."""
+ self.timestamp = state.last_updated
+ try:
+ self.state = float(state.state)
+ except ValueError:
+ self.state = state.state
+
+ def set_precision(self, precision):
+ """Set precision of Number based states."""
+ if isinstance(self.state, Number):
+ self.state = round(float(self.state), precision)
+
+ def __str__(self):
+ """Return state as the string representation of FilterState."""
+ return str(self.state)
+
+ def __repr__(self):
+ """Return timestamp and state as the representation of FilterState."""
+ return "{} : {}".format(self.timestamp, self.state)
+
+
class Filter(object):
"""Filter skeleton.
@@ -207,11 +283,22 @@ class Filter(object):
def __init__(self, name, window_size=1, precision=None, entity=None):
"""Initialize common attributes."""
- self.states = deque(maxlen=window_size)
+ if isinstance(window_size, int):
+ self.states = deque(maxlen=window_size)
+ self.window_unit = WINDOW_SIZE_UNIT_NUMBER_EVENTS
+ else:
+ self.states = deque(maxlen=0)
+ self.window_unit = WINDOW_SIZE_UNIT_TIME
self.precision = precision
self._name = name
self._entity = entity
self._skip_processing = False
+ self._window_size = window_size
+
+ @property
+ def window_size(self):
+ """Return window size."""
+ return self._window_size
@property
def name(self):
@@ -229,11 +316,11 @@ class Filter(object):
def filter_state(self, new_state):
"""Implement a common interface for filters."""
- filtered = self._filter_state(new_state)
- if isinstance(filtered, Number):
- filtered = round(float(filtered), self.precision)
- self.states.append(filtered)
- return filtered
+ filtered = self._filter_state(FilterState(new_state))
+ filtered.set_precision(self.precision)
+ self.states.append(copy(filtered))
+ new_state.state = filtered.state
+ return new_state
@FILTERS.register(FILTER_NAME_OUTLIER)
@@ -254,11 +341,10 @@ class OutlierFilter(Filter):
def _filter_state(self, new_state):
"""Implement the outlier filter."""
- new_state = float(new_state)
-
- if (self.states and
- abs(new_state - statistics.median(self.states))
- > self._radius):
+ if (len(self.states) == self.states.maxlen and
+ abs(new_state.state -
+ statistics.median([s.state for s in self.states])) >
+ self._radius):
self._stats_internal['erasures'] += 1
@@ -284,16 +370,15 @@ class LowPassFilter(Filter):
def _filter_state(self, new_state):
"""Implement the low pass filter."""
- new_state = float(new_state)
-
if not self.states:
return new_state
new_weight = 1.0 / self._time_constant
prev_weight = 1.0 - new_weight
- filtered = prev_weight * self.states[-1] + new_weight * new_state
+ new_state.state = prev_weight * self.states[-1].state +\
+ new_weight * new_state.state
- return filtered
+ return new_state
@FILTERS.register(FILTER_NAME_TIME_SMA)
@@ -308,35 +393,36 @@ class TimeSMAFilter(Filter):
def __init__(self, window_size, precision, entity, type):
"""Initialize Filter."""
- super().__init__(FILTER_NAME_TIME_SMA, 0, precision, entity)
- self._time_window = int(window_size.total_seconds())
+ super().__init__(FILTER_NAME_TIME_SMA, window_size, precision, entity)
+ self._time_window = window_size
self.last_leak = None
self.queue = deque()
- def _leak(self, now):
+ def _leak(self, left_boundary):
"""Remove timeouted elements."""
while self.queue:
- timestamp, _ = self.queue[0]
- if timestamp + self._time_window <= now:
+ if self.queue[0].timestamp + self._time_window <= left_boundary:
self.last_leak = self.queue.popleft()
else:
return
def _filter_state(self, new_state):
- now = int(dt_util.utcnow().timestamp())
+ """Implement the Simple Moving Average filter."""
+ self._leak(new_state.timestamp)
+ self.queue.append(copy(new_state))
- self._leak(now)
- self.queue.append((now, float(new_state)))
moving_sum = 0
- start = now - self._time_window
- _, prev_val = self.last_leak or (0, float(new_state))
+ start = new_state.timestamp - self._time_window
+ prev_state = self.last_leak or self.queue[0]
+ for state in self.queue:
+ moving_sum += (state.timestamp-start).total_seconds()\
+ * prev_state.state
+ start = state.timestamp
+ prev_state = state
- for timestamp, val in self.queue:
- moving_sum += (timestamp-start)*prev_val
- start, prev_val = timestamp, val
- moving_sum += (now-start)*prev_val
+ new_state.state = moving_sum / self._time_window.total_seconds()
- return moving_sum/self._time_window
+ return new_state
@FILTERS.register(FILTER_NAME_THROTTLE)
diff --git a/homeassistant/components/sensor/folder.py b/homeassistant/components/sensor/folder.py
index a185cd1e825..2b5f3dd4309 100644
--- a/homeassistant/components/sensor/folder.py
+++ b/homeassistant/components/sensor/folder.py
@@ -38,7 +38,7 @@ def get_files_list(folder_path, filter_term):
def get_size(files_list):
"""Return the sum of the size in bytes of files in the list."""
- size_list = [os.stat(f).st_size for f in files_list]
+ size_list = [os.stat(f).st_size for f in files_list if os.path.isfile(f)]
return sum(size_list)
diff --git a/homeassistant/components/sensor/foobot.py b/homeassistant/components/sensor/foobot.py
index 8f65a335872..d247a90e93a 100644
--- a/homeassistant/components/sensor/foobot.py
+++ b/homeassistant/components/sensor/foobot.py
@@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
-REQUIREMENTS = ['foobot_async==0.3.0']
+REQUIREMENTS = ['foobot_async==0.3.1']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/sensor/fritzbox_netmonitor.py b/homeassistant/components/sensor/fritzbox_netmonitor.py
index f4f774cad1e..857e6cc4a07 100644
--- a/homeassistant/components/sensor/fritzbox_netmonitor.py
+++ b/homeassistant/components/sensor/fritzbox_netmonitor.py
@@ -11,7 +11,7 @@ from requests.exceptions import RequestException
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import (CONF_HOST, STATE_UNAVAILABLE)
+from homeassistant.const import (CONF_NAME, CONF_HOST, STATE_UNAVAILABLE)
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
@@ -20,6 +20,7 @@ REQUIREMENTS = ['fritzconnection==0.6.5']
_LOGGER = logging.getLogger(__name__)
+CONF_DEFAULT_NAME = 'fritz_netmonitor'
CONF_DEFAULT_IP = '169.254.1.1' # This IP is valid for all FRITZ!Box routers.
ATTR_BYTES_RECEIVED = 'bytes_received'
@@ -42,6 +43,7 @@ STATE_OFFLINE = 'offline'
ICON = 'mdi:web'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Optional(CONF_NAME, default=CONF_DEFAULT_NAME): cv.string,
vol.Optional(CONF_HOST, default=CONF_DEFAULT_IP): cv.string,
})
@@ -52,6 +54,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
import fritzconnection as fc
from fritzconnection.fritzconnection import FritzConnectionException
+ name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
try:
@@ -65,15 +68,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
else:
_LOGGER.info("Successfully connected to FRITZ!Box")
- add_devices([FritzboxMonitorSensor(fstatus)], True)
+ add_devices([FritzboxMonitorSensor(name, fstatus)], True)
class FritzboxMonitorSensor(Entity):
"""Implementation of a fritzbox monitor sensor."""
- def __init__(self, fstatus):
+ def __init__(self, name, fstatus):
"""Initialize the sensor."""
- self._name = 'fritz_netmonitor'
+ self._name = name
self._fstatus = fstatus
self._state = STATE_UNAVAILABLE
self._is_linked = self._is_connected = self._wan_access_type = None
diff --git a/homeassistant/components/sensor/hive.py b/homeassistant/components/sensor/hive.py
index cae2eaf7437..8f8ce2d1681 100644
--- a/homeassistant/components/sensor/hive.py
+++ b/homeassistant/components/sensor/hive.py
@@ -4,11 +4,17 @@ Support for the Hive devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.hive/
"""
+from homeassistant.const import TEMP_CELSIUS
from homeassistant.components.hive import DATA_HIVE
from homeassistant.helpers.entity import Entity
DEPENDENCIES = ['hive']
+FRIENDLY_NAMES = {'Hub_OnlineStatus': 'Hub Status',
+ 'Hive_OutsideTemperature': 'Outside Temperature'}
+DEVICETYPE_ICONS = {'Hub_OnlineStatus': 'mdi:switch',
+ 'Hive_OutsideTemperature': 'mdi:thermometer'}
+
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Hive sensor devices."""
@@ -16,7 +22,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return
session = hass.data.get(DATA_HIVE)
- if discovery_info["HA_DeviceType"] == "Hub_OnlineStatus":
+ if (discovery_info["HA_DeviceType"] == "Hub_OnlineStatus" or
+ discovery_info["HA_DeviceType"] == "Hive_OutsideTemperature"):
add_devices([HiveSensorEntity(session, discovery_info)])
@@ -27,6 +34,7 @@ class HiveSensorEntity(Entity):
"""Initialize the sensor."""
self.node_id = hivedevice["Hive_NodeID"]
self.device_type = hivedevice["HA_DeviceType"]
+ self.node_device_type = hivedevice["Hive_DeviceType"]
self.session = hivesession
self.data_updatesource = '{}.{}'.format(self.device_type,
self.node_id)
@@ -40,13 +48,29 @@ class HiveSensorEntity(Entity):
@property
def name(self):
"""Return the name of the sensor."""
- return "Hive hub status"
+ return FRIENDLY_NAMES.get(self.device_type)
@property
def state(self):
"""Return the state of the sensor."""
- return self.session.sensor.hub_online_status(self.node_id)
+ if self.device_type == "Hub_OnlineStatus":
+ return self.session.sensor.hub_online_status(self.node_id)
+ elif self.device_type == "Hive_OutsideTemperature":
+ return self.session.weather.temperature()
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ if self.device_type == "Hive_OutsideTemperature":
+ return TEMP_CELSIUS
+
+ @property
+ def icon(self):
+ """Return the icon to use."""
+ return DEVICETYPE_ICONS.get(self.device_type)
def update(self):
- """Update all Node data from Hive."""
- self.session.core.update_data(self.node_id)
+ """Update all Node data frome Hive."""
+ if self.session.core.update_data(self.node_id):
+ for entity in self.session.entities:
+ entity.handle_update(self.data_updatesource)
diff --git a/homeassistant/components/sensor/linux_battery.py b/homeassistant/components/sensor/linux_battery.py
index 3d28c44d606..1f0e3e89e5c 100644
--- a/homeassistant/components/sensor/linux_battery.py
+++ b/homeassistant/components/sensor/linux_battery.py
@@ -94,6 +94,11 @@ class LinuxBatterySensor(Entity):
"""Return the name of the sensor."""
return self._name
+ @property
+ def device_class(self):
+ """Return the device class of the sensor."""
+ return 'battery'
+
@property
def state(self):
"""Return the state of the sensor."""
diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py
index 37976151190..98cc7731d4d 100644
--- a/homeassistant/components/sensor/miflora.py
+++ b/homeassistant/components/sensor/miflora.py
@@ -16,7 +16,7 @@ from homeassistant.const import (
)
-REQUIREMENTS = ['miflora==0.3.0']
+REQUIREMENTS = ['miflora==0.4.0']
_LOGGER = logging.getLogger(__name__)
@@ -63,10 +63,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
from miflora import miflora_poller
try:
import bluepy.btle # noqa: F401 # pylint: disable=unused-variable
- from miflora.backends.bluepy import BluepyBackend
+ from btlewrap import BluepyBackend
backend = BluepyBackend
except ImportError:
- from miflora.backends.gatttool import GatttoolBackend
+ from btlewrap import GatttoolBackend
backend = GatttoolBackend
_LOGGER.debug('Miflora is using %s backend.', backend.__name__)
@@ -138,7 +138,7 @@ class MiFloraSensor(Entity):
This uses a rolling median over 3 values to filter out outliers.
"""
- from miflora.backends import BluetoothBackendException
+ from btlewrap import BluetoothBackendException
try:
_LOGGER.debug("Polling data for %s", self.name)
data = self.poller.parameter_value(self.parameter)
diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py
index d191b9a22e8..c4f64e9e015 100644
--- a/homeassistant/components/sensor/mqtt.py
+++ b/homeassistant/components/sensor/mqtt.py
@@ -8,6 +8,7 @@ import asyncio
import logging
import json
from datetime import timedelta
+from typing import Optional
import voluptuous as vol
@@ -28,6 +29,7 @@ _LOGGER = logging.getLogger(__name__)
CONF_EXPIRE_AFTER = 'expire_after'
CONF_JSON_ATTRS = 'json_attributes'
+CONF_UNIQUE_ID = 'unique_id'
DEFAULT_NAME = 'MQTT Sensor'
DEFAULT_FORCE_UPDATE = False
@@ -40,6 +42,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv,
vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int,
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
+ # Integrations shouldn't never expose unique_id through configuration
+ # this here is an exception because MQTT is a msg transport, not a protocol
+ vol.Optional(CONF_UNIQUE_ID): cv.string,
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@@ -63,6 +68,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
config.get(CONF_ICON),
value_template,
config.get(CONF_JSON_ATTRS),
+ config.get(CONF_UNIQUE_ID),
config.get(CONF_AVAILABILITY_TOPIC),
config.get(CONF_PAYLOAD_AVAILABLE),
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
@@ -74,7 +80,8 @@ class MqttSensor(MqttAvailability, Entity):
def __init__(self, name, state_topic, qos, unit_of_measurement,
force_update, expire_after, icon, value_template,
- json_attributes, availability_topic, payload_available,
+ json_attributes, unique_id: Optional[str],
+ availability_topic, payload_available,
payload_not_available):
"""Initialize the sensor."""
super().__init__(availability_topic, qos, payload_available,
@@ -90,6 +97,7 @@ class MqttSensor(MqttAvailability, Entity):
self._icon = icon
self._expiration_trigger = None
self._json_attributes = set(json_attributes)
+ self._unique_id = unique_id
self._attributes = None
@asyncio.coroutine
@@ -174,6 +182,11 @@ class MqttSensor(MqttAvailability, Entity):
"""Return the state attributes."""
return self._attributes
+ @property
+ def unique_id(self):
+ """Return a unique ID."""
+ return self._unique_id
+
@property
def icon(self):
"""Return the icon."""
diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py
index e2567fdf4ca..5ee4f738051 100644
--- a/homeassistant/components/sensor/nest.py
+++ b/homeassistant/components/sensor/nest.py
@@ -140,6 +140,11 @@ class NestTempSensor(NestSensor):
"""Return the state of the sensor."""
return self._state
+ @property
+ def device_class(self):
+ """Return the device class of the sensor."""
+ return 'temperature'
+
def update(self):
"""Retrieve latest state."""
if self.device.temperature_scale == 'C':
diff --git a/homeassistant/components/sensor/nut.py b/homeassistant/components/sensor/nut.py
index e0d5b7250e9..b8917080efc 100644
--- a/homeassistant/components/sensor/nut.py
+++ b/homeassistant/components/sensor/nut.py
@@ -113,6 +113,7 @@ STATE_TYPES = {
'HB': 'High Battery',
'RB': 'Battery Needs Replaced',
'CHRG': 'Battery Charging',
+ 'DISCHRG': 'Battery Discharging',
'BYPASS': 'Bypass Active',
'CAL': 'Runtime Calibration',
'OFF': 'Offline',
diff --git a/homeassistant/components/sensor/qwikswitch.py b/homeassistant/components/sensor/qwikswitch.py
index 19b32e93670..ebd5f5254d4 100644
--- a/homeassistant/components/sensor/qwikswitch.py
+++ b/homeassistant/components/sensor/qwikswitch.py
@@ -6,8 +6,8 @@ https://home-assistant.io/components/sensor.qwikswitch/
"""
import logging
-from homeassistant.components.qwikswitch import DOMAIN as QWIKSWITCH
-from homeassistant.helpers.entity import Entity
+from homeassistant.components.qwikswitch import DOMAIN as QWIKSWITCH, QSEntity
+from homeassistant.core import callback
DEPENDENCIES = [QWIKSWITCH]
@@ -15,55 +15,54 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_platform(hass, _, add_devices, discovery_info=None):
- """Add lights from the main Qwikswitch component."""
+ """Add sensor from the main Qwikswitch component."""
if discovery_info is None:
return
qsusb = hass.data[QWIKSWITCH]
_LOGGER.debug("Setup qwikswitch.sensor %s, %s", qsusb, discovery_info)
- devs = [QSSensor(name, qsid)
- for name, qsid in discovery_info[QWIKSWITCH].items()]
+ devs = [QSSensor(sensor) for sensor in discovery_info[QWIKSWITCH]]
add_devices(devs)
-class QSSensor(Entity):
+class QSSensor(QSEntity):
"""Sensor based on a Qwikswitch relay/dimmer module."""
- _val = {}
+ _val = None
- def __init__(self, sensor_name, sensor_id):
+ def __init__(self, sensor):
"""Initialize the sensor."""
- self._name = sensor_name
- self.qsid = sensor_id
+ from pyqwikswitch import SENSORS
+ super().__init__(sensor['id'], sensor['name'])
+ self.channel = sensor['channel']
+ self.sensor_type = sensor['type']
+
+ self._decode, self.unit = SENSORS[self.sensor_type]
+ if isinstance(self.unit, type):
+ self.unit = "{}:{}".format(self.sensor_type, self.channel)
+
+ @callback
def update_packet(self, packet):
"""Receive update packet from QSUSB."""
- _LOGGER.debug("Update %s (%s): %s", self.entity_id, self.qsid, packet)
- self._val = packet
- self.async_schedule_update_ha_state()
+ 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)
+ if val is not None:
+ self._val = val
+ self.async_schedule_update_ha_state()
@property
def state(self):
"""Return the value of the sensor."""
- return self._val.get('data', 0)
+ return str(self._val)
@property
- def device_state_attributes(self):
- """Return the state attributes of the sensor."""
- return self._val
+ def unique_id(self):
+ """Return a unique identifier for this sensor."""
+ return "qs{}:{}".format(self.qsid, self.channel)
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
- return None
-
- @property
- def poll(self):
- """QS sensors gets packets in update_packet."""
- return False
-
- async def async_added_to_hass(self):
- """Listen for updates from QSUSb via dispatcher."""
- # Part of Entity/ToggleEntity
- self.hass.helpers.dispatcher.async_dispatcher_connect(
- self.qsid, self.update_packet)
+ return self.unit
diff --git a/homeassistant/components/sensor/sht31.py b/homeassistant/components/sensor/sht31.py
new file mode 100644
index 00000000000..e1a7f3c9e5f
--- /dev/null
+++ b/homeassistant/components/sensor/sht31.py
@@ -0,0 +1,152 @@
+"""
+Support for Sensirion SHT31 temperature and humidity sensor.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.sht31/
+"""
+
+from datetime import timedelta
+import logging
+import math
+
+import voluptuous as vol
+
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import (
+ TEMP_CELSIUS, CONF_NAME, CONF_MONITORED_CONDITIONS)
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.temperature import display_temp
+from homeassistant.const import PRECISION_TENTHS
+from homeassistant.util import Throttle
+
+
+REQUIREMENTS = ['Adafruit-GPIO==1.0.3',
+ 'Adafruit-SHT31==1.0.2']
+
+_LOGGER = logging.getLogger(__name__)
+
+CONF_I2C_ADDRESS = 'i2c_address'
+
+DEFAULT_NAME = 'SHT31'
+DEFAULT_I2C_ADDRESS = 0x44
+
+SENSOR_TEMPERATURE = 'temperature'
+SENSOR_HUMIDITY = 'humidity'
+SENSOR_TYPES = (SENSOR_TEMPERATURE, SENSOR_HUMIDITY)
+
+MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10)
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS):
+ vol.All(vol.Coerce(int), vol.Range(min=0x44, max=0x45)),
+ vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)):
+ vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+})
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Setup the sensor platform."""
+ from Adafruit_SHT31 import SHT31
+
+ i2c_address = config.get(CONF_I2C_ADDRESS)
+ sensor = SHT31(address=i2c_address)
+
+ try:
+ if sensor.read_status() is None:
+ raise ValueError("CRC error while reading SHT31 status")
+ except (OSError, ValueError):
+ _LOGGER.error(
+ "SHT31 sensor not detected at address %s", hex(i2c_address))
+ return
+ sensor_client = SHTClient(sensor)
+
+ sensor_classes = {
+ SENSOR_TEMPERATURE: SHTSensorTemperature,
+ SENSOR_HUMIDITY: SHTSensorHumidity
+ }
+
+ devs = []
+ for sensor_type, sensor_class in sensor_classes.items():
+ name = "{} {}".format(config.get(CONF_NAME), sensor_type.capitalize())
+ devs.append(sensor_class(sensor_client, name))
+
+ add_devices(devs)
+
+
+class SHTClient(object):
+ """Get the latest data from the SHT sensor."""
+
+ def __init__(self, adafruit_sht):
+ """Initialize the sensor."""
+ self.adafruit_sht = adafruit_sht
+ self.temperature = None
+ self.humidity = None
+
+ @Throttle(MIN_TIME_BETWEEN_UPDATES)
+ def update(self):
+ """Get the latest data from the SHT sensor."""
+ temperature, humidity = self.adafruit_sht.read_temperature_humidity()
+ if math.isnan(temperature) or math.isnan(humidity):
+ _LOGGER.warning("Bad sample from sensor SHT31")
+ return
+ self.temperature = temperature
+ self.humidity = humidity
+
+
+class SHTSensor(Entity):
+ """An abstract SHTSensor, can be either temperature or humidity."""
+
+ def __init__(self, sensor, name):
+ """Initialize the sensor."""
+ self._sensor = sensor
+ self._name = name
+ self._state = None
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ def update(self):
+ """Fetch temperature and humidity from the sensor."""
+ self._sensor.update()
+
+
+class SHTSensorTemperature(SHTSensor):
+ """Representation of a temperature sensor."""
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return self.hass.config.units.temperature_unit
+
+ def update(self):
+ """Fetch temperature from the sensor."""
+ super().update()
+ temp_celsius = self._sensor.temperature
+ if temp_celsius is not None:
+ self._state = display_temp(self.hass, temp_celsius,
+ TEMP_CELSIUS, PRECISION_TENTHS)
+
+
+class SHTSensorHumidity(SHTSensor):
+ """Representation of a humidity sensor."""
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return '%'
+
+ def update(self):
+ """Fetch humidity from the sensor."""
+ super().update()
+ humidity = self._sensor.humidity
+ if humidity is not None:
+ self._state = round(humidity)
diff --git a/homeassistant/components/sensor/sigfox.py b/homeassistant/components/sensor/sigfox.py
new file mode 100644
index 00000000000..ef47132eefc
--- /dev/null
+++ b/homeassistant/components/sensor/sigfox.py
@@ -0,0 +1,161 @@
+"""
+Sensor for SigFox devices.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.sigfox/
+"""
+import logging
+import datetime
+import json
+from urllib.parse import urljoin
+
+import requests
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import CONF_NAME
+from homeassistant.helpers.entity import Entity
+
+_LOGGER = logging.getLogger(__name__)
+
+SCAN_INTERVAL = datetime.timedelta(seconds=30)
+API_URL = 'https://backend.sigfox.com/api/'
+CONF_API_LOGIN = 'api_login'
+CONF_API_PASSWORD = 'api_password'
+DEFAULT_NAME = 'sigfox'
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_API_LOGIN): cv.string,
+ vol.Required(CONF_API_PASSWORD): cv.string,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+})
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the sigfox sensor."""
+ api_login = config[CONF_API_LOGIN]
+ api_password = config[CONF_API_PASSWORD]
+ name = config[CONF_NAME]
+ try:
+ sigfox = SigfoxAPI(api_login, api_password)
+ except ValueError:
+ return False
+ auth = sigfox.auth
+ devices = sigfox.devices
+
+ sensors = []
+ for device in devices:
+ sensors.append(SigfoxDevice(device, auth, name))
+ add_devices(sensors, True)
+
+
+def epoch_to_datetime(epoch_time):
+ """Take an ms since epoch and return datetime string."""
+ return datetime.datetime.fromtimestamp(epoch_time).isoformat()
+
+
+class SigfoxAPI(object):
+ """Class for interacting with the SigFox API."""
+
+ def __init__(self, api_login, api_password):
+ """Initialise the API object."""
+ self._auth = requests.auth.HTTPBasicAuth(api_login, api_password)
+ if self.check_credentials():
+ device_types = self.get_device_types()
+ self._devices = self.get_devices(device_types)
+
+ def check_credentials(self):
+ """"Check API credentials are valid."""
+ url = urljoin(API_URL, 'devicetypes')
+ response = requests.get(url, auth=self._auth, timeout=10)
+ if response.status_code != 200:
+ if response.status_code == 401:
+ _LOGGER.error(
+ "Invalid credentials for Sigfox API")
+ else:
+ _LOGGER.error(
+ "Unable to login to Sigfox API, error code %s", str(
+ response.status_code))
+ raise ValueError('Sigfox component not setup')
+ return True
+
+ def get_device_types(self):
+ """Get a list of device types."""
+ url = urljoin(API_URL, 'devicetypes')
+ response = requests.get(url, auth=self._auth, timeout=10)
+ device_types = []
+ for device in json.loads(response.text)['data']:
+ device_types.append(device['id'])
+ return device_types
+
+ def get_devices(self, device_types):
+ """Get the device_id of each device registered."""
+ devices = []
+ for unique_type in device_types:
+ location_url = 'devicetypes/{}/devices'.format(unique_type)
+ url = urljoin(API_URL, location_url)
+ response = requests.get(url, auth=self._auth, timeout=10)
+ devices_data = json.loads(response.text)['data']
+ for device in devices_data:
+ devices.append(device['id'])
+ return devices
+
+ @property
+ def auth(self):
+ """Return the API authentification."""
+ return self._auth
+
+ @property
+ def devices(self):
+ """Return the list of device_id."""
+ return self._devices
+
+
+class SigfoxDevice(Entity):
+ """Class for single sigfox device."""
+
+ def __init__(self, device_id, auth, name):
+ """Initialise the device object."""
+ self._device_id = device_id
+ self._auth = auth
+ self._message_data = {}
+ self._name = '{}_{}'.format(name, device_id)
+ self._state = None
+
+ def get_last_message(self):
+ """Return the last message from a device."""
+ device_url = 'devices/{}/messages?limit=1'.format(self._device_id)
+ url = urljoin(API_URL, device_url)
+ response = requests.get(url, auth=self._auth, timeout=10)
+ data = json.loads(response.text)['data'][0]
+ payload = bytes.fromhex(data['data']).decode('utf-8')
+ lat = data['rinfos'][0]['lat']
+ lng = data['rinfos'][0]['lng']
+ snr = data['snr']
+ epoch_time = data['time']
+ return {'lat': lat,
+ 'lng': lng,
+ 'payload': payload,
+ 'snr': snr,
+ 'time': epoch_to_datetime(epoch_time)}
+
+ def update(self):
+ """Fetch the latest device message."""
+ self._message_data = self.get_last_message()
+ self._state = self._message_data['payload']
+
+ @property
+ def name(self):
+ """Return the HA name of the sensor."""
+ return self._name
+
+ @property
+ def state(self):
+ """Return the payload of the last message."""
+ return self._state
+
+ @property
+ def device_state_attributes(self):
+ """Return other details about the last message."""
+ return self._message_data
diff --git a/homeassistant/components/sensor/smappee.py b/homeassistant/components/sensor/smappee.py
index c59798d16d7..5b84962144d 100644
--- a/homeassistant/components/sensor/smappee.py
+++ b/homeassistant/components/sensor/smappee.py
@@ -31,7 +31,19 @@ SENSOR_TYPES = {
'solar_today':
['Solar Today', 'mdi:white-balance-sunny', 'remote', 'kWh', 'solar'],
'power_today':
- ['Power Today', 'mdi:power-plug', 'remote', 'kWh', 'consumption']
+ ['Power Today', 'mdi:power-plug', 'remote', 'kWh', 'consumption'],
+ 'water_sensor_1':
+ ['Water Sensor 1', 'mdi:water', 'water', 'm3', 'value1'],
+ 'water_sensor_2':
+ ['Water Sensor 2', 'mdi:water', 'water', 'm3', 'value2'],
+ 'water_sensor_temperature':
+ ['Water Sensor Temperature', 'mdi:temperature-celsius',
+ 'water', '°', 'temperature'],
+ 'water_sensor_humidity':
+ ['Water Sensor Humidity', 'mdi:water-percent', 'water',
+ '%', 'humidity'],
+ 'water_sensor_battery':
+ ['Water Sensor Battery', 'mdi:battery', 'water', '%', 'battery'],
}
SCAN_INTERVAL = timedelta(seconds=30)
@@ -43,36 +55,50 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
dev = []
if smappee.is_remote_active:
- for sensor in SENSOR_TYPES:
- if 'remote' in SENSOR_TYPES[sensor]:
- for location_id in smappee.locations.keys():
- dev.append(SmappeeSensor(smappee, location_id, sensor))
+ for location_id in smappee.locations.keys():
+ for sensor in SENSOR_TYPES:
+ if 'remote' in SENSOR_TYPES[sensor]:
+ dev.append(SmappeeSensor(smappee, location_id,
+ sensor,
+ SENSOR_TYPES[sensor]))
+ elif 'water' in SENSOR_TYPES[sensor]:
+ for items in smappee.info[location_id].get('sensors'):
+ dev.append(SmappeeSensor(
+ smappee,
+ location_id,
+ '{}:{}'.format(sensor, items.get('id')),
+ SENSOR_TYPES[sensor]))
if smappee.is_local_active:
- for sensor in SENSOR_TYPES:
- if 'local' in SENSOR_TYPES[sensor]:
- if smappee.is_remote_active:
- for location_id in smappee.locations.keys():
- dev.append(SmappeeSensor(smappee, location_id, sensor))
- else:
- dev.append(SmappeeSensor(smappee, None, sensor))
+ for location_id in smappee.locations.keys():
+ for sensor in SENSOR_TYPES:
+ if 'local' in SENSOR_TYPES[sensor]:
+ if smappee.is_remote_active:
+ dev.append(SmappeeSensor(smappee, location_id, sensor,
+ SENSOR_TYPES[sensor]))
+ else:
+ dev.append(SmappeeSensor(smappee, None, sensor,
+ SENSOR_TYPES[sensor]))
+
add_devices(dev, True)
class SmappeeSensor(Entity):
"""Implementation of a Smappee sensor."""
- def __init__(self, smappee, location_id, sensor):
- """Initialize the sensor."""
+ def __init__(self, smappee, location_id, sensor, attributes):
+ """Initialize the Smappee sensor."""
self._smappee = smappee
self._location_id = location_id
+ self._attributes = attributes
self._sensor = sensor
self.data = None
self._state = None
- self._name = SENSOR_TYPES[self._sensor][0]
- self._icon = SENSOR_TYPES[self._sensor][1]
- self._unit_of_measurement = SENSOR_TYPES[self._sensor][3]
- self._smappe_name = SENSOR_TYPES[self._sensor][4]
+ self._name = self._attributes[0]
+ self._icon = self._attributes[1]
+ self._type = self._attributes[2]
+ self._unit_of_measurement = self._attributes[3]
+ self._smappe_name = self._attributes[4]
@property
def name(self):
@@ -82,9 +108,7 @@ class SmappeeSensor(Entity):
else:
location_name = 'Local'
- return "{} {} {}".format(SENSOR_PREFIX,
- location_name,
- self._name)
+ return "{} {} {}".format(SENSOR_PREFIX, location_name, self._name)
@property
def icon(self):
@@ -160,3 +184,13 @@ class SmappeeSensor(Entity):
if i['key'].endswith('phase5ActivePower')]
power = sum(value1 + value2 + value3) / 1000
self._state = round(power, 2)
+ elif self._type == 'water':
+ sensor_name, sensor_id = self._sensor.split(":")
+ data = self._smappee.sensor_consumption[self._location_id]\
+ .get(int(sensor_id))
+ if data:
+ consumption = data.get('records')[-1]
+ _LOGGER.debug("%s (%s) %s",
+ sensor_name, sensor_id, consumption)
+ value = consumption.get(self._smappe_name)
+ self._state = value
diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py
index af9fa233d40..eeca31fa36b 100644
--- a/homeassistant/components/sensor/sql.py
+++ b/homeassistant/components/sensor/sql.py
@@ -19,7 +19,7 @@ from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
-REQUIREMENTS = ['sqlalchemy==1.2.5']
+REQUIREMENTS = ['sqlalchemy==1.2.6']
CONF_QUERIES = 'queries'
CONF_QUERY = 'query'
diff --git a/homeassistant/components/sensor/tibber.py b/homeassistant/components/sensor/tibber.py
index aaaa8366909..4fb378ac227 100644
--- a/homeassistant/components/sensor/tibber.py
+++ b/homeassistant/components/sensor/tibber.py
@@ -19,8 +19,9 @@ from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import Entity
from homeassistant.util import dt as dt_util
+from homeassistant.util import Throttle
-REQUIREMENTS = ['pyTibber==0.4.0']
+REQUIREMENTS = ['pyTibber==0.4.1']
_LOGGER = logging.getLogger(__name__)
@@ -30,6 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
ICON = 'mdi:currency-usd'
SCAN_INTERVAL = timedelta(minutes=1)
+MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
async def async_setup_platform(hass, config, async_add_devices,
@@ -58,7 +60,9 @@ class TibberSensor(Entity):
"""Initialize the sensor."""
self._tibber_home = tibber_home
self._last_updated = None
+ self._newest_data_timestamp = None
self._state = None
+ self._is_available = False
self._device_state_attributes = {}
self._unit_of_measurement = self._tibber_home.price_unit
self._name = 'Electricity price {}'.format(tibber_home.info['viewer']
@@ -68,50 +72,27 @@ class TibberSensor(Entity):
"""Get the latest data and updates the states."""
now = dt_util.utcnow()
if self._tibber_home.current_price_total and self._last_updated and \
- dt_util.as_utc(dt_util.parse_datetime(self._last_updated)).hour\
- == now.hour:
+ self._last_updated.hour == now.hour and self._newest_data_timestamp:
return
- def _find_current_price():
- state = None
- max_price = None
- min_price = None
- for key, price_total in self._tibber_home.price_total.items():
- price_time = dt_util.as_utc(dt_util.parse_datetime(key))
- price_total = round(price_total, 3)
- time_diff = (now - price_time).total_seconds()/60
- if time_diff >= 0 and time_diff < 60:
- state = price_total
- self._last_updated = key
- if now.date() == price_time.date():
- if max_price is None or price_total > max_price:
- max_price = price_total
- if min_price is None or price_total < min_price:
- min_price = price_total
- self._state = state
- self._device_state_attributes['max_price'] = max_price
- self._device_state_attributes['min_price'] = min_price
- return state is not None
+ if (not self._newest_data_timestamp or
+ (self._newest_data_timestamp - now).total_seconds()/3600 < 12
+ or not self._is_available):
+ _LOGGER.debug("Asking for new data.")
+ await self._fetch_data()
- if _find_current_price():
- return
-
- _LOGGER.debug("No cached data found, so asking for new data")
- await self._tibber_home.update_info()
- await self._tibber_home.update_price_info()
- data = self._tibber_home.info['viewer']['home']
- self._device_state_attributes['app_nickname'] = data['appNickname']
- self._device_state_attributes['grid_company'] =\
- data['meteringPointData']['gridCompany']
- self._device_state_attributes['estimated_annual_consumption'] =\
- data['meteringPointData']['estimatedAnnualConsumption']
- _find_current_price()
+ self._is_available = self._update_current_price()
@property
def device_state_attributes(self):
"""Return the state attributes."""
return self._device_state_attributes
+ @property
+ def available(self):
+ """Return True if entity is available."""
+ return self._is_available
+
@property
def name(self):
"""Return the name of the sensor."""
@@ -137,3 +118,42 @@ class TibberSensor(Entity):
"""Return a unique ID."""
home = self._tibber_home.info['viewer']['home']
return home['meteringPointData']['consumptionEan']
+
+ @Throttle(MIN_TIME_BETWEEN_UPDATES)
+ async def _fetch_data(self):
+ try:
+ await self._tibber_home.update_info()
+ await self._tibber_home.update_price_info()
+ except (asyncio.TimeoutError, aiohttp.ClientError):
+ return
+ data = self._tibber_home.info['viewer']['home']
+ self._device_state_attributes['app_nickname'] = data['appNickname']
+ self._device_state_attributes['grid_company'] = \
+ data['meteringPointData']['gridCompany']
+ self._device_state_attributes['estimated_annual_consumption'] = \
+ data['meteringPointData']['estimatedAnnualConsumption']
+
+ def _update_current_price(self):
+ state = None
+ max_price = None
+ min_price = None
+ now = dt_util.utcnow()
+ for key, price_total in self._tibber_home.price_total.items():
+ price_time = dt_util.as_utc(dt_util.parse_datetime(key))
+ price_total = round(price_total, 3)
+ time_diff = (now - price_time).total_seconds()/60
+ if (not self._newest_data_timestamp or
+ price_time > self._newest_data_timestamp):
+ self._newest_data_timestamp = price_time
+ if 0 <= time_diff < 60:
+ state = price_total
+ self._last_updated = price_time
+ if now.date() == price_time.date():
+ if max_price is None or price_total > max_price:
+ max_price = price_total
+ if min_price is None or price_total < min_price:
+ min_price = price_total
+ self._state = state
+ self._device_state_attributes['max_price'] = max_price
+ self._device_state_attributes['min_price'] = min_price
+ return state is not None
diff --git a/homeassistant/components/sensor/trafikverket_weatherstation.py b/homeassistant/components/sensor/trafikverket_weatherstation.py
index fba16c27c7e..77a2b0e7338 100644
--- a/homeassistant/components/sensor/trafikverket_weatherstation.py
+++ b/homeassistant/components/sensor/trafikverket_weatherstation.py
@@ -4,17 +4,17 @@ Weather information for air and road temperature, provided by Trafikverket.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.trafikverket_weatherstation/
"""
+from datetime import timedelta
import json
import logging
-from datetime import timedelta
import requests
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
- CONF_NAME, ATTR_ATTRIBUTION, TEMP_CELSIUS, CONF_API_KEY, CONF_TYPE)
+ ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME, CONF_TYPE, TEMP_CELSIUS)
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
@@ -25,6 +25,7 @@ CONF_ATTRIBUTION = "Data provided by Trafikverket API"
CONF_STATION = 'station'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
+
SCAN_INTERVAL = timedelta(seconds=300)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@@ -36,7 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
- """Setup the sensor platform."""
+ """Set up the Trafikverket sensor platform."""
sensor_name = config.get(CONF_NAME)
sensor_api = config.get(CONF_API_KEY)
sensor_station = config.get(CONF_STATION)
@@ -47,10 +48,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class TrafikverketWeatherStation(Entity):
- """Representation of a Sensor."""
+ """Representation of a Trafikverket sensor."""
def __init__(self, sensor_name, sensor_api, sensor_station, sensor_type):
- """Initialize the sensor."""
+ """Initialize the Trafikverket sensor."""
self._name = sensor_name
self._api = sensor_api
self._station = sensor_station
@@ -82,10 +83,7 @@ class TrafikverketWeatherStation(Entity):
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
- """Fetch new state data for the sensor.
-
- This is the only method that should fetch new data for Home Assistant.
- """
+ """Fetch new state data for the sensor."""
url = 'http://api.trafikinfo.trafikverket.se/v1.3/data.json'
if self._type == 'road':
@@ -117,7 +115,7 @@ class TrafikverketWeatherStation(Entity):
result = data["RESPONSE"]["RESULT"][0]
final = result["WeatherStation"][0]["Measurement"]
except KeyError:
- _LOGGER.error("Incorrect weather station or API key.")
+ _LOGGER.error("Incorrect weather station or API key")
return
# air_vs_road contains "Air" or "Road" depending on user input.
diff --git a/homeassistant/components/sensor/upnp.py b/homeassistant/components/sensor/upnp.py
index e5acae67916..e0c57ca9ac6 100644
--- a/homeassistant/components/sensor/upnp.py
+++ b/homeassistant/components/sensor/upnp.py
@@ -6,38 +6,44 @@ https://home-assistant.io/components/sensor.upnp/
"""
import logging
-from homeassistant.components.upnp import DATA_UPNP, UNITS
+from homeassistant.components.upnp import DATA_UPNP, UNITS, CIC_SERVICE
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
+BYTES_RECEIVED = 1
+BYTES_SENT = 2
+PACKETS_RECEIVED = 3
+PACKETS_SENT = 4
+
# sensor_type: [friendly_name, convert_unit, icon]
SENSOR_TYPES = {
- 'byte_received': ['received bytes', True, 'mdi:server-network'],
- 'byte_sent': ['sent bytes', True, 'mdi:server-network'],
- 'packets_in': ['packets received', False, 'mdi:server-network'],
- 'packets_out': ['packets sent', False, 'mdi:server-network'],
+ BYTES_RECEIVED: ['received bytes', True, 'mdi:server-network'],
+ BYTES_SENT: ['sent bytes', True, 'mdi:server-network'],
+ PACKETS_RECEIVED: ['packets received', False, 'mdi:server-network'],
+ PACKETS_SENT: ['packets sent', False, 'mdi:server-network'],
}
-def setup_platform(hass, config, add_devices, discovery_info=None):
+async def async_setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the IGD sensors."""
- upnp = hass.data[DATA_UPNP]
+ device = hass.data[DATA_UPNP]
+ service = device.find_first_service(CIC_SERVICE)
unit = discovery_info['unit']
add_devices([
- IGDSensor(upnp, t, unit if SENSOR_TYPES[t][1] else None)
+ IGDSensor(service, t, unit if SENSOR_TYPES[t][1] else '#')
for t in SENSOR_TYPES], True)
class IGDSensor(Entity):
"""Representation of a UPnP IGD sensor."""
- def __init__(self, upnp, sensor_type, unit=""):
+ def __init__(self, service, sensor_type, unit=None):
"""Initialize the IGD sensor."""
- self._upnp = upnp
+ self._service = service
self.type = sensor_type
self.unit = unit
- self.unit_factor = UNITS[unit] if unit is not None else 1
+ self.unit_factor = UNITS[unit] if unit in UNITS else 1
self._name = 'IGD {}'.format(SENSOR_TYPES[sensor_type][0])
self._state = None
@@ -49,9 +55,9 @@ class IGDSensor(Entity):
@property
def state(self):
"""Return the state of the device."""
- if self._state is None:
- return None
- return format(self._state / self.unit_factor, '.1f')
+ if self._state:
+ return format(float(self._state) / self.unit_factor, '.1f')
+ return self._state
@property
def icon(self):
@@ -63,13 +69,13 @@ class IGDSensor(Entity):
"""Return the unit of measurement of this entity, if any."""
return self.unit
- def update(self):
+ async def async_update(self):
"""Get the latest information from the IGD."""
- if self.type == "byte_received":
- self._state = self._upnp.totalbytereceived()
- elif self.type == "byte_sent":
- self._state = self._upnp.totalbytesent()
- elif self.type == "packets_in":
- self._state = self._upnp.totalpacketreceived()
- elif self.type == "packets_out":
- self._state = self._upnp.totalpacketsent()
+ if self.type == BYTES_RECEIVED:
+ self._state = await self._service.get_total_bytes_received()
+ elif self.type == BYTES_SENT:
+ self._state = await self._service.get_total_bytes_sent()
+ elif self.type == PACKETS_RECEIVED:
+ self._state = await self._service.get_total_packets_received()
+ elif self.type == PACKETS_SENT:
+ self._state = await self._service.get_total_packets_sent()
diff --git a/homeassistant/components/sensor/uscis.py b/homeassistant/components/sensor/uscis.py
new file mode 100644
index 00000000000..ed3c9ca8587
--- /dev/null
+++ b/homeassistant/components/sensor/uscis.py
@@ -0,0 +1,87 @@
+"""
+Support for USCIS Case Status.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.uscis/
+"""
+
+import logging
+from datetime import timedelta
+import voluptuous as vol
+
+from homeassistant.helpers.entity import Entity
+from homeassistant.util import Throttle
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.helpers import config_validation as cv
+from homeassistant.const import CONF_FRIENDLY_NAME
+
+
+_LOGGER = logging.getLogger(__name__)
+
+REQUIREMENTS = ['uscisstatus==0.1.1']
+
+DEFAULT_NAME = "USCIS"
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Optional(CONF_FRIENDLY_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Required('case_id'): cv.string,
+})
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Setting the platform in HASS and Case Information."""
+ uscis = UscisSensor(config['case_id'], config[CONF_FRIENDLY_NAME])
+ uscis.update()
+ if uscis.valid_case_id:
+ add_devices([uscis])
+ else:
+ _LOGGER.error("Setup USCIS Sensor Fail"
+ " check if your Case ID is Valid")
+
+
+class UscisSensor(Entity):
+ """USCIS Sensor will check case status on daily basis."""
+
+ MIN_TIME_BETWEEN_UPDATES = timedelta(hours=24)
+
+ CURRENT_STATUS = "current_status"
+ LAST_CASE_UPDATE = "last_update_date"
+
+ def __init__(self, case, name):
+ """Initialize the sensor."""
+ self._state = None
+ self._case_id = case
+ self._attributes = None
+ self.valid_case_id = None
+ self._name = name
+
+ @property
+ def name(self):
+ """Return the name."""
+ return self._name
+
+ @property
+ def state(self):
+ """Return the state."""
+ return self._state
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ return self._attributes
+
+ @Throttle(MIN_TIME_BETWEEN_UPDATES)
+ def update(self):
+ """Using Request to access USCIS website and fetch data."""
+ import uscisstatus
+ try:
+ status = uscisstatus.get_case_status(self._case_id)
+ self._attributes = {
+ self.CURRENT_STATUS: status['status']
+ }
+ self._state = status['date']
+ self.valid_case_id = True
+
+ except ValueError:
+ _LOGGER("Please Check that you have valid USCIS case id")
+ self.valid_case_id = False
diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml
index 519d3b98704..746c3c7f483 100644
--- a/homeassistant/components/services.yaml
+++ b/homeassistant/components/services.yaml
@@ -395,6 +395,18 @@ snips:
intent_filter:
description: Optional Array of Strings - A list of intents names to restrict the NLU resolution to on the first query.
example: turnOnLights, turnOffLights
+ feedback_on:
+ description: Turns feedback sounds on.
+ fields:
+ site_id:
+ description: Site to turn sounds on, defaults to all sites (optional)
+ example: bedroom
+ feedback_off:
+ description: Turns feedback sounds off.
+ fields:
+ site_id:
+ description: Site to turn sounds on, defaults to all sites (optional)
+ example: bedroom
input_boolean:
toggle:
diff --git a/homeassistant/components/skybell.py b/homeassistant/components/skybell.py
index 854abdda7bc..3f27c91e7c5 100644
--- a/homeassistant/components/skybell.py
+++ b/homeassistant/components/skybell.py
@@ -14,7 +14,7 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['skybellpy==0.1.1']
+REQUIREMENTS = ['skybellpy==0.1.2']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/smappee.py b/homeassistant/components/smappee.py
index 1241679770b..b35cd8cf5a8 100644
--- a/homeassistant/components/smappee.py
+++ b/homeassistant/components/smappee.py
@@ -110,6 +110,7 @@ class Smappee(object):
self.locations = {}
self.info = {}
self.consumption = {}
+ self.sensor_consumption = {}
self.instantaneous = {}
if self._remote_active or self._local_active:
@@ -124,11 +125,22 @@ class Smappee(object):
for location in service_locations:
location_id = location.get('serviceLocationId')
if location_id is not None:
+ self.sensor_consumption[location_id] = {}
self.locations[location_id] = location.get('name')
self.info[location_id] = self._smappy \
.get_service_location_info(location_id)
_LOGGER.debug("Remote info %s %s",
- self.locations, self.info)
+ self.locations, self.info[location_id])
+
+ for sensors in self.info[location_id].get('sensors'):
+ sensor_id = sensors.get('id')
+ self.sensor_consumption[location_id]\
+ .update({sensor_id: self.get_sensor_consumption(
+ location_id, sensor_id,
+ aggregation=3, delta=1440)})
+ _LOGGER.debug("Remote sensors %s %s",
+ self.locations,
+ self.sensor_consumption[location_id])
self.consumption[location_id] = self.get_consumption(
location_id, aggregation=3, delta=1440)
@@ -190,7 +202,8 @@ class Smappee(object):
"Error getting comsumption from Smappee cloud. (%s)",
error)
- def get_sensor_consumption(self, location_id, sensor_id):
+ def get_sensor_consumption(self, location_id, sensor_id,
+ aggregation, delta):
"""Update data from Smappee."""
# Start & End accept epoch (in milliseconds),
# datetime and pandas timestamps
@@ -203,13 +216,13 @@ class Smappee(object):
if not self.is_remote_active:
return
- start = datetime.utcnow() - timedelta(minutes=30)
end = datetime.utcnow()
+ start = end - timedelta(minutes=delta)
try:
return self._smappy.get_sensor_consumption(location_id,
sensor_id,
start,
- end, 1)
+ end, aggregation)
except RequestException as error:
_LOGGER.error(
"Error getting comsumption from Smappee cloud. (%s)",
diff --git a/homeassistant/components/snips.py b/homeassistant/components/snips.py
index d085b1279cb..812906e7be9 100644
--- a/homeassistant/components/snips.py
+++ b/homeassistant/components/snips.py
@@ -4,13 +4,13 @@ Support for Snips on-device ASR and NLU.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/snips/
"""
-import asyncio
import json
import logging
from datetime import timedelta
import voluptuous as vol
+from homeassistant.core import callback
from homeassistant.helpers import intent, config_validation as cv
import homeassistant.components.mqtt as mqtt
@@ -19,11 +19,18 @@ DEPENDENCIES = ['mqtt']
CONF_INTENTS = 'intents'
CONF_ACTION = 'action'
+CONF_FEEDBACK = 'feedback_sounds'
+CONF_PROBABILITY = 'probability_threshold'
+CONF_SITE_IDS = 'site_ids'
SERVICE_SAY = 'say'
SERVICE_SAY_ACTION = 'say_action'
+SERVICE_FEEDBACK_ON = 'feedback_on'
+SERVICE_FEEDBACK_OFF = 'feedback_off'
INTENT_TOPIC = 'hermes/intent/#'
+FEEDBACK_ON_TOPIC = 'hermes/feedback/sound/toggleOn'
+FEEDBACK_OFF_TOPIC = 'hermes/feedback/sound/toggleOff'
ATTR_TEXT = 'text'
ATTR_SITE_ID = 'site_id'
@@ -34,7 +41,12 @@ ATTR_INTENT_FILTER = 'intent_filter'
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema({
- DOMAIN: {}
+ DOMAIN: vol.Schema({
+ vol.Optional(CONF_FEEDBACK): cv.boolean,
+ vol.Optional(CONF_PROBABILITY, default=0): vol.Coerce(float),
+ vol.Optional(CONF_SITE_IDS, default=['default']):
+ vol.All(cv.ensure_list, [cv.string]),
+ }),
}, extra=vol.ALLOW_EXTRA)
INTENT_SCHEMA = vol.Schema({
@@ -57,7 +69,6 @@ SERVICE_SCHEMA_SAY = vol.Schema({
vol.Optional(ATTR_SITE_ID, default='default'): str,
vol.Optional(ATTR_CUSTOM_DATA, default=''): str
})
-
SERVICE_SCHEMA_SAY_ACTION = vol.Schema({
vol.Required(ATTR_TEXT): str,
vol.Optional(ATTR_SITE_ID, default='default'): str,
@@ -65,13 +76,31 @@ SERVICE_SCHEMA_SAY_ACTION = vol.Schema({
vol.Optional(ATTR_CAN_BE_ENQUEUED, default=True): cv.boolean,
vol.Optional(ATTR_INTENT_FILTER): vol.All(cv.ensure_list),
})
+SERVICE_SCHEMA_FEEDBACK = vol.Schema({
+ vol.Optional(ATTR_SITE_ID, default='default'): str
+})
-@asyncio.coroutine
-def async_setup(hass, config):
+async def async_setup(hass, config):
"""Activate Snips component."""
- @asyncio.coroutine
- def message_received(topic, payload, qos):
+ @callback
+ def async_set_feedback(site_ids, state):
+ """Set Feedback sound state."""
+ site_ids = (site_ids if site_ids
+ else config[DOMAIN].get(CONF_SITE_IDS))
+ topic = (FEEDBACK_ON_TOPIC if state
+ else FEEDBACK_OFF_TOPIC)
+ for site_id in site_ids:
+ payload = json.dumps({'siteId': site_id})
+ hass.components.mqtt.async_publish(
+ FEEDBACK_ON_TOPIC, None, qos=0, retain=False)
+ hass.components.mqtt.async_publish(
+ topic, payload, qos=int(state), retain=state)
+
+ if CONF_FEEDBACK in config[DOMAIN]:
+ async_set_feedback(None, config[DOMAIN][CONF_FEEDBACK])
+
+ async def message_received(topic, payload, qos):
"""Handle new messages on MQTT."""
_LOGGER.debug("New intent: %s", payload)
@@ -81,6 +110,13 @@ def async_setup(hass, config):
_LOGGER.error('Received invalid JSON: %s', payload)
return
+ if (request['intent']['probability']
+ < config[DOMAIN].get(CONF_PROBABILITY)):
+ _LOGGER.warning("Intent below probaility threshold %s < %s",
+ request['intent']['probability'],
+ config[DOMAIN].get(CONF_PROBABILITY))
+ return
+
try:
request = INTENT_SCHEMA(request)
except vol.Invalid as err:
@@ -97,7 +133,7 @@ def async_setup(hass, config):
slots[slot['slotName']] = {'value': resolve_slot_values(slot)}
try:
- intent_response = yield from intent.async_handle(
+ intent_response = await intent.async_handle(
hass, DOMAIN, intent_type, slots, request['input'])
if 'plain' in intent_response.speech:
snips_response = intent_response.speech['plain']['speech']
@@ -115,11 +151,10 @@ def async_setup(hass, config):
mqtt.async_publish(hass, 'hermes/dialogueManager/endSession',
json.dumps(notification))
- yield from hass.components.mqtt.async_subscribe(
+ await hass.components.mqtt.async_subscribe(
INTENT_TOPIC, message_received)
- @asyncio.coroutine
- def snips_say(call):
+ async def snips_say(call):
"""Send a Snips notification message."""
notification = {'siteId': call.data.get(ATTR_SITE_ID, 'default'),
'customData': call.data.get(ATTR_CUSTOM_DATA, ''),
@@ -129,8 +164,7 @@ def async_setup(hass, config):
json.dumps(notification))
return
- @asyncio.coroutine
- def snips_say_action(call):
+ async def snips_say_action(call):
"""Send a Snips action message."""
notification = {'siteId': call.data.get(ATTR_SITE_ID, 'default'),
'customData': call.data.get(ATTR_CUSTOM_DATA, ''),
@@ -144,12 +178,26 @@ def async_setup(hass, config):
json.dumps(notification))
return
+ async def feedback_on(call):
+ """Turn feedback sounds on."""
+ async_set_feedback(call.data.get(ATTR_SITE_ID), True)
+
+ async def feedback_off(call):
+ """Turn feedback sounds off."""
+ async_set_feedback(call.data.get(ATTR_SITE_ID), False)
+
hass.services.async_register(
DOMAIN, SERVICE_SAY, snips_say,
schema=SERVICE_SCHEMA_SAY)
hass.services.async_register(
DOMAIN, SERVICE_SAY_ACTION, snips_say_action,
schema=SERVICE_SCHEMA_SAY_ACTION)
+ hass.services.async_register(
+ DOMAIN, SERVICE_FEEDBACK_ON, feedback_on,
+ schema=SERVICE_SCHEMA_FEEDBACK)
+ hass.services.async_register(
+ DOMAIN, SERVICE_FEEDBACK_OFF, feedback_off,
+ schema=SERVICE_SCHEMA_FEEDBACK)
return True
diff --git a/homeassistant/components/switch/edimax.py b/homeassistant/components/switch/edimax.py
index 49eb5d32110..40ebb54b603 100644
--- a/homeassistant/components/switch/edimax.py
+++ b/homeassistant/components/switch/edimax.py
@@ -84,12 +84,12 @@ class SmartPlugSwitch(SwitchDevice):
"""Update edimax switch."""
try:
self._now_power = float(self.smartplug.now_power)
- except ValueError:
+ except (TypeError, ValueError):
self._now_power = None
try:
self._now_energy_day = float(self.smartplug.now_energy_day)
- except ValueError:
+ except (TypeError, ValueError):
self._now_energy_day = None
self._state = self.smartplug.state == 'ON'
diff --git a/homeassistant/components/switch/eufy.py b/homeassistant/components/switch/eufy.py
new file mode 100644
index 00000000000..891525d3979
--- /dev/null
+++ b/homeassistant/components/switch/eufy.py
@@ -0,0 +1,73 @@
+"""
+Support for Eufy switches.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/switch.eufy/
+"""
+import logging
+
+from homeassistant.components.switch import SwitchDevice
+
+DEPENDENCIES = ['eufy']
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up Eufy switches."""
+ if discovery_info is None:
+ return
+ add_devices([EufySwitch(discovery_info)], True)
+
+
+class EufySwitch(SwitchDevice):
+ """Representation of a Eufy switch."""
+
+ def __init__(self, device):
+ """Initialize the light."""
+ # pylint: disable=import-error
+ import lakeside
+
+ self._state = None
+ self._name = device['name']
+ self._address = device['address']
+ self._code = device['code']
+ self._type = device['type']
+ self._switch = lakeside.switch(self._address, self._code, self._type)
+ self._switch.connect()
+
+ def update(self):
+ """Synchronise state from the switch."""
+ self._switch.update()
+ self._state = self._switch.power
+
+ @property
+ def unique_id(self):
+ """Return the ID of this light."""
+ return self._address
+
+ @property
+ def name(self):
+ """Return the name of the device if any."""
+ return self._name
+
+ @property
+ def is_on(self):
+ """Return true if device is on."""
+ return self._state
+
+ def turn_on(self, **kwargs):
+ """Turn the specified switch on."""
+ try:
+ self._switch.set_state(True)
+ except BrokenPipeError:
+ self._switch.connect()
+ self._switch.set_state(power=True)
+
+ def turn_off(self, **kwargs):
+ """Turn the specified switch off."""
+ try:
+ self._switch.set_state(False)
+ except BrokenPipeError:
+ self._switch.connect()
+ self._switch.set_state(False)
diff --git a/homeassistant/components/switch/fritzbox.py b/homeassistant/components/switch/fritzbox.py
new file mode 100755
index 00000000000..c8313b0dfef
--- /dev/null
+++ b/homeassistant/components/switch/fritzbox.py
@@ -0,0 +1,104 @@
+"""
+Support for AVM Fritz!Box smarthome switch devices.
+
+For more details about this component, please refer to the documentation at
+http://home-assistant.io/components/switch.fritzbox/
+"""
+import logging
+
+import requests
+
+from homeassistant.components.fritzbox import DOMAIN as FRITZBOX_DOMAIN
+from homeassistant.components.fritzbox import (
+ ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_LOCKED)
+from homeassistant.components.switch import SwitchDevice
+from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
+
+DEPENDENCIES = ['fritzbox']
+
+_LOGGER = logging.getLogger(__name__)
+
+ATTR_TOTAL_CONSUMPTION = 'total_consumption'
+ATTR_TOTAL_CONSUMPTION_UNIT = 'total_consumption_unit'
+ATTR_TOTAL_CONSUMPTION_UNIT_VALUE = 'kWh'
+
+ATTR_TEMPERATURE_UNIT = 'temperature_unit'
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the Fritzbox smarthome switch platform."""
+ devices = []
+ fritz_list = hass.data[FRITZBOX_DOMAIN]
+
+ for fritz in fritz_list:
+ device_list = fritz.get_devices()
+ for device in device_list:
+ if device.has_switch:
+ devices.append(FritzboxSwitch(device, fritz))
+
+ add_devices(devices)
+
+
+class FritzboxSwitch(SwitchDevice):
+ """The switch class for Fritzbox switches."""
+
+ def __init__(self, device, fritz):
+ """Initialize the switch."""
+ self._device = device
+ self._fritz = fritz
+
+ @property
+ def available(self):
+ """Return if switch is available."""
+ return self._device.present
+
+ @property
+ def name(self):
+ """Return the name of the device."""
+ return self._device.name
+
+ @property
+ def is_on(self):
+ """Return true if the switch is on."""
+ return self._device.switch_state
+
+ def turn_on(self, **kwargs):
+ """Turn the switch on."""
+ self._device.set_switch_state_on()
+
+ def turn_off(self, **kwargs):
+ """Turn the switch off."""
+ self._device.set_switch_state_off()
+
+ def update(self):
+ """Get latest data and states from the device."""
+ try:
+ self._device.update()
+ except requests.exceptions.HTTPError as ex:
+ _LOGGER.warning("Fritzhome connection error: %s", ex)
+ self._fritz.login()
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes of the device."""
+ attrs = {}
+ attrs[ATTR_STATE_DEVICE_LOCKED] = self._device.device_lock
+ attrs[ATTR_STATE_LOCKED] = self._device.lock
+
+ if self._device.has_powermeter:
+ attrs[ATTR_TOTAL_CONSUMPTION] = "{:.3f}".format(
+ (self._device.energy or 0.0) / 100000)
+ attrs[ATTR_TOTAL_CONSUMPTION_UNIT] = \
+ ATTR_TOTAL_CONSUMPTION_UNIT_VALUE
+ if self._device.has_temperature_sensor:
+ attrs[ATTR_TEMPERATURE] = \
+ str(self.hass.config.units.temperature(
+ self._device.temperature, TEMP_CELSIUS))
+ attrs[ATTR_TEMPERATURE_UNIT] = \
+ self.hass.config.units.temperature_unit
+ return attrs
+
+ @property
+ def current_power_w(self):
+ """Return the current power usage in W."""
+ return self._device.power / 1000
diff --git a/homeassistant/components/switch/hive.py b/homeassistant/components/switch/hive.py
index 67ebe95ba8e..49fc9696b5e 100644
--- a/homeassistant/components/switch/hive.py
+++ b/homeassistant/components/switch/hive.py
@@ -28,6 +28,7 @@ class HiveDevicePlug(SwitchDevice):
self.node_name = hivedevice["Hive_NodeName"]
self.device_type = hivedevice["HA_DeviceType"]
self.session = hivesession
+ self.attributes = {}
self.data_updatesource = '{}.{}'.format(self.device_type,
self.node_id)
self.session.entities.append(self)
@@ -42,6 +43,11 @@ class HiveDevicePlug(SwitchDevice):
"""Return the name of this Switch device if any."""
return self.node_name
+ @property
+ def device_state_attributes(self):
+ """Show Device Attributes."""
+ return self.attributes
+
@property
def current_power_w(self):
"""Return the current power usage in W."""
@@ -67,3 +73,5 @@ class HiveDevicePlug(SwitchDevice):
def update(self):
"""Update all Node data from Hive."""
self.session.core.update_data(self.node_id)
+ self.attributes = self.session.attributes.state_attributes(
+ self.node_id)
diff --git a/homeassistant/components/switch/homekit_controller.py b/homeassistant/components/switch/homekit_controller.py
new file mode 100644
index 00000000000..6b97200ba49
--- /dev/null
+++ b/homeassistant/components/switch/homekit_controller.py
@@ -0,0 +1,68 @@
+"""
+Support for Homekit switches.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/switch.homekit_controller/
+"""
+import json
+import logging
+
+from homeassistant.components.homekit_controller import (HomeKitEntity,
+ KNOWN_ACCESSORIES)
+from homeassistant.components.switch import SwitchDevice
+
+DEPENDENCIES = ['homekit_controller']
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up Homekit switch support."""
+ if discovery_info is not None:
+ accessory = hass.data[KNOWN_ACCESSORIES][discovery_info['serial']]
+ add_devices([HomeKitSwitch(accessory, discovery_info)], True)
+
+
+class HomeKitSwitch(HomeKitEntity, SwitchDevice):
+ """Representation of a Homekit switch."""
+
+ def __init__(self, *args):
+ """Initialise the switch."""
+ super().__init__(*args)
+ self._on = None
+
+ def update_characteristics(self, characteristics):
+ """Synchronise the switch state with Home Assistant."""
+ # pylint: disable=import-error
+ import homekit
+
+ for characteristic in characteristics:
+ ctype = characteristic['type']
+ ctype = homekit.CharacteristicsTypes.get_short(ctype)
+ if ctype == "on":
+ self._chars['on'] = characteristic['iid']
+ self._on = characteristic['value']
+ elif ctype == "outlet-in-use":
+ self._chars['outlet-in-use'] = characteristic['iid']
+
+ @property
+ def is_on(self):
+ """Return true if device is on."""
+ return self._on
+
+ def turn_on(self, **kwargs):
+ """Turn the specified switch on."""
+ self._on = True
+ characteristics = [{'aid': self._aid,
+ 'iid': self._chars['on'],
+ 'value': True}]
+ body = json.dumps({'characteristics': characteristics})
+ self._securecon.put('/characteristics', body)
+
+ def turn_off(self, **kwargs):
+ """Turn the specified switch off."""
+ characteristics = [{'aid': self._aid,
+ 'iid': self._chars['on'],
+ 'value': False}]
+ body = json.dumps({'characteristics': characteristics})
+ self._securecon.put('/characteristics', body)
diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py
index 17aa66ea825..999b584360c 100644
--- a/homeassistant/components/tts/__init__.py
+++ b/homeassistant/components/tts/__init__.py
@@ -37,6 +37,7 @@ ATTR_CACHE = 'cache'
ATTR_LANGUAGE = 'language'
ATTR_MESSAGE = 'message'
ATTR_OPTIONS = 'options'
+ATTR_PLATFORM = 'platform'
CONF_CACHE = 'cache'
CONF_CACHE_DIR = 'cache_dir'
@@ -77,8 +78,7 @@ SCHEMA_SERVICE_SAY = vol.Schema({
SCHEMA_SERVICE_CLEAR_CACHE = vol.Schema({})
-@asyncio.coroutine
-def async_setup(hass, config):
+async def async_setup(hass, config):
"""Set up TTS."""
tts = SpeechManager(hass)
@@ -88,27 +88,27 @@ def async_setup(hass, config):
cache_dir = conf.get(CONF_CACHE_DIR, DEFAULT_CACHE_DIR)
time_memory = conf.get(CONF_TIME_MEMORY, DEFAULT_TIME_MEMORY)
- yield from tts.async_init_cache(use_cache, cache_dir, time_memory)
+ await tts.async_init_cache(use_cache, cache_dir, time_memory)
except (HomeAssistantError, KeyError) as err:
_LOGGER.error("Error on cache init %s", err)
return False
hass.http.register_view(TextToSpeechView(tts))
+ hass.http.register_view(TextToSpeechUrlView(tts))
- @asyncio.coroutine
- def async_setup_platform(p_type, p_config, disc_info=None):
+ async def async_setup_platform(p_type, p_config, disc_info=None):
"""Set up a TTS platform."""
- platform = yield from async_prepare_setup_platform(
+ platform = await async_prepare_setup_platform(
hass, config, DOMAIN, p_type)
if platform is None:
return
try:
if hasattr(platform, 'async_get_engine'):
- provider = yield from platform.async_get_engine(
+ provider = await platform.async_get_engine(
hass, p_config)
else:
- provider = yield from hass.async_add_job(
+ provider = await hass.async_add_job(
platform.get_engine, hass, p_config)
if provider is None:
@@ -120,8 +120,7 @@ def async_setup(hass, config):
_LOGGER.exception("Error setting up platform %s", p_type)
return
- @asyncio.coroutine
- def async_say_handle(service):
+ async def async_say_handle(service):
"""Service handle for say."""
entity_ids = service.data.get(ATTR_ENTITY_ID)
message = service.data.get(ATTR_MESSAGE)
@@ -130,7 +129,7 @@ def async_setup(hass, config):
options = service.data.get(ATTR_OPTIONS)
try:
- url = yield from tts.async_get_url(
+ url = await tts.async_get_url(
p_type, message, cache=cache, language=language,
options=options
)
@@ -146,7 +145,7 @@ def async_setup(hass, config):
if entity_ids:
data[ATTR_ENTITY_ID] = entity_ids
- yield from hass.services.async_call(
+ await hass.services.async_call(
DOMAIN_MP, SERVICE_PLAY_MEDIA, data, blocking=True)
hass.services.async_register(
@@ -157,12 +156,11 @@ def async_setup(hass, config):
in config_per_platform(config, DOMAIN)]
if setup_tasks:
- yield from asyncio.wait(setup_tasks, loop=hass.loop)
+ await asyncio.wait(setup_tasks, loop=hass.loop)
- @asyncio.coroutine
- def async_clear_cache_handle(service):
+ async def async_clear_cache_handle(service):
"""Handle clear cache service call."""
- yield from tts.async_clear_cache()
+ await tts.async_clear_cache()
hass.services.async_register(
DOMAIN, SERVICE_CLEAR_CACHE, async_clear_cache_handle,
@@ -185,8 +183,7 @@ class SpeechManager(object):
self.file_cache = {}
self.mem_cache = {}
- @asyncio.coroutine
- def async_init_cache(self, use_cache, cache_dir, time_memory):
+ async def async_init_cache(self, use_cache, cache_dir, time_memory):
"""Init config folder and load file cache."""
self.use_cache = use_cache
self.time_memory = time_memory
@@ -201,7 +198,7 @@ class SpeechManager(object):
return cache_dir
try:
- self.cache_dir = yield from self.hass.async_add_job(
+ self.cache_dir = await self.hass.async_add_job(
init_tts_cache_dir, cache_dir)
except OSError as err:
raise HomeAssistantError("Can't init cache dir {}".format(err))
@@ -222,15 +219,14 @@ class SpeechManager(object):
return cache
try:
- cache_files = yield from self.hass.async_add_job(get_cache_files)
+ cache_files = await self.hass.async_add_job(get_cache_files)
except OSError as err:
raise HomeAssistantError("Can't read cache dir {}".format(err))
if cache_files:
self.file_cache.update(cache_files)
- @asyncio.coroutine
- def async_clear_cache(self):
+ async def async_clear_cache(self):
"""Read file cache and delete files."""
self.mem_cache = {}
@@ -243,7 +239,7 @@ class SpeechManager(object):
_LOGGER.warning(
"Can't remove cache file '%s': %s", filename, err)
- yield from self.hass.async_add_job(remove_files)
+ await self.hass.async_add_job(remove_files)
self.file_cache = {}
@callback
@@ -254,9 +250,8 @@ class SpeechManager(object):
provider.name = engine
self.providers[engine] = provider
- @asyncio.coroutine
- def async_get_url(self, engine, message, cache=None, language=None,
- options=None):
+ async def async_get_url(self, engine, message, cache=None, language=None,
+ options=None):
"""Get URL for play message.
This method is a coroutine.
@@ -301,21 +296,20 @@ class SpeechManager(object):
self.hass.async_add_job(self.async_file_to_mem(key))
# Load speech from provider into memory
else:
- filename = yield from self.async_get_tts_audio(
+ filename = await self.async_get_tts_audio(
engine, key, message, use_cache, language, options)
return "{}/api/tts_proxy/{}".format(
self.hass.config.api.base_url, filename)
- @asyncio.coroutine
- def async_get_tts_audio(self, engine, key, message, cache, language,
- options):
+ async def async_get_tts_audio(self, engine, key, message, cache, language,
+ options):
"""Receive TTS and store for view in cache.
This method is a coroutine.
"""
provider = self.providers[engine]
- extension, data = yield from provider.async_get_tts_audio(
+ extension, data = await provider.async_get_tts_audio(
message, language, options)
if data is None or extension is None:
@@ -337,8 +331,7 @@ class SpeechManager(object):
return filename
- @asyncio.coroutine
- def async_save_tts_audio(self, key, filename, data):
+ async def async_save_tts_audio(self, key, filename, data):
"""Store voice data to file and file_cache.
This method is a coroutine.
@@ -351,13 +344,12 @@ class SpeechManager(object):
speech.write(data)
try:
- yield from self.hass.async_add_job(save_speech)
+ await self.hass.async_add_job(save_speech)
self.file_cache[key] = filename
except OSError:
_LOGGER.error("Can't write %s", filename)
- @asyncio.coroutine
- def async_file_to_mem(self, key):
+ async def async_file_to_mem(self, key):
"""Load voice from file cache into memory.
This method is a coroutine.
@@ -374,7 +366,7 @@ class SpeechManager(object):
return speech.read()
try:
- data = yield from self.hass.async_add_job(load_speech)
+ data = await self.hass.async_add_job(load_speech)
except OSError:
del self.file_cache[key]
raise HomeAssistantError("Can't read {}".format(voice_file))
@@ -396,8 +388,7 @@ class SpeechManager(object):
self.hass.loop.call_later(self.time_memory, async_remove_from_mem)
- @asyncio.coroutine
- def async_read_tts(self, filename):
+ async def async_read_tts(self, filename):
"""Read a voice file and return binary.
This method is a coroutine.
@@ -412,7 +403,7 @@ class SpeechManager(object):
if key not in self.mem_cache:
if key not in self.file_cache:
raise HomeAssistantError("{} not in cache!".format(key))
- yield from self.async_file_to_mem(key)
+ await self.async_file_to_mem(key)
content, _ = mimetypes.guess_type(filename)
return (content, self.mem_cache[key][MEM_CACHE_VOICE])
@@ -490,6 +481,45 @@ class Provider(object):
ft.partial(self.get_tts_audio, message, language, options=options))
+class TextToSpeechUrlView(HomeAssistantView):
+ """TTS view to get a url to a generated speech file."""
+
+ requires_auth = True
+ url = '/api/tts_get_url'
+ name = 'api:tts:geturl'
+
+ def __init__(self, tts):
+ """Initialize a tts view."""
+ self.tts = tts
+
+ async def post(self, request):
+ """Generate speech and provide url."""
+ try:
+ data = await request.json()
+ except ValueError:
+ return self.json_message('Invalid JSON specified', 400)
+ if not data.get(ATTR_PLATFORM) and data.get(ATTR_MESSAGE):
+ return self.json_message('Must specify platform and message', 400)
+
+ p_type = data[ATTR_PLATFORM]
+ message = data[ATTR_MESSAGE]
+ cache = data.get(ATTR_CACHE)
+ language = data.get(ATTR_LANGUAGE)
+ options = data.get(ATTR_OPTIONS)
+
+ try:
+ url = await self.tts.async_get_url(
+ p_type, message, cache=cache, language=language,
+ options=options
+ )
+ resp = self.json({'url': url}, 200)
+ except HomeAssistantError as err:
+ _LOGGER.error("Error on init tts: %s", err)
+ resp = self.json({'error': err}, 400)
+
+ return resp
+
+
class TextToSpeechView(HomeAssistantView):
"""TTS view to serve a speech audio."""
@@ -501,11 +531,10 @@ class TextToSpeechView(HomeAssistantView):
"""Initialize a tts view."""
self.tts = tts
- @asyncio.coroutine
- def get(self, request, filename):
+ async def get(self, request, filename):
"""Start a get request."""
try:
- content, data = yield from self.tts.async_read_tts(filename)
+ content, data = await self.tts.async_read_tts(filename)
except HomeAssistantError as err:
_LOGGER.error("Error on load tts: %s", err)
return web.Response(status=404)
diff --git a/homeassistant/components/upnp.py b/homeassistant/components/upnp.py
index 960d8f3780e..dd611090c22 100644
--- a/homeassistant/components/upnp.py
+++ b/homeassistant/components/upnp.py
@@ -6,6 +6,7 @@ https://home-assistant.io/components/upnp/
"""
from ipaddress import ip_address
import logging
+import asyncio
import voluptuous as vol
@@ -14,7 +15,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.util import get_local_ip
-REQUIREMENTS = ['miniupnpc==2.0.2']
+REQUIREMENTS = ['pyupnp-async==0.1.0.1']
DEPENDENCIES = ['http']
_LOGGER = logging.getLogger(__name__)
@@ -22,7 +23,7 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['api']
DOMAIN = 'upnp'
-DATA_UPNP = 'UPNP'
+DATA_UPNP = 'upnp_device'
CONF_LOCAL_IP = 'local_ip'
CONF_ENABLE_PORT_MAPPING = 'port_mapping'
@@ -33,6 +34,11 @@ CONF_HASS = 'hass'
NOTIFICATION_ID = 'upnp_notification'
NOTIFICATION_TITLE = 'UPnP Setup'
+IGD_DEVICE = 'urn:schemas-upnp-org:device:InternetGatewayDevice:1'
+PPP_SERVICE = 'urn:schemas-upnp-org:service:WANPPPConnection:1'
+IP_SERVICE = 'urn:schemas-upnp-org:service:WANIPConnection:1'
+CIC_SERVICE = 'urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1'
+
UNITS = {
"Bytes": 1,
"KBytes": 1024,
@@ -51,8 +57,7 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
-# pylint: disable=import-error, no-member, broad-except, c-extension-no-member
-def setup(hass, config):
+async def async_setup(hass, config):
"""Register a port mapping for Home Assistant via UPnP."""
config = config[DOMAIN]
host = config.get(CONF_LOCAL_IP)
@@ -67,21 +72,35 @@ def setup(hass, config):
'Unable to determine local IP. Add it to your configuration.')
return False
- import miniupnpc
+ import pyupnp_async
+ from pyupnp_async.error import UpnpSoapError
- upnp = miniupnpc.UPnP()
- hass.data[DATA_UPNP] = upnp
-
- upnp.discoverdelay = 200
- upnp.discover()
- try:
- upnp.selectigd()
- except Exception:
- _LOGGER.exception("Error when attempting to discover an UPnP IGD")
+ service = None
+ resp = await pyupnp_async.msearch_first(search_target=IGD_DEVICE)
+ if not resp:
return False
- unit = config.get(CONF_UNITS)
- discovery.load_platform(hass, 'sensor', DOMAIN, {'unit': unit}, config)
+ try:
+ device = await resp.get_device()
+ hass.data[DATA_UPNP] = device
+ for _service in device.services:
+ if _service['serviceType'] == PPP_SERVICE:
+ service = device.find_first_service(PPP_SERVICE)
+ if _service['serviceType'] == IP_SERVICE:
+ service = device.find_first_service(IP_SERVICE)
+ if _service['serviceType'] == CIC_SERVICE:
+ unit = config.get(CONF_UNITS)
+ discovery.load_platform(hass, 'sensor',
+ DOMAIN,
+ {'unit': unit},
+ config)
+ except UpnpSoapError as error:
+ _LOGGER.error(error)
+ return False
+
+ if not service:
+ _LOGGER.warning("Could not find any UPnP IGD")
+ return False
port_mapping = config.get(CONF_ENABLE_PORT_MAPPING)
if not port_mapping:
@@ -98,12 +117,12 @@ def setup(hass, config):
if internal == CONF_HASS:
internal = internal_port
try:
- upnp.addportmapping(
- external, 'TCP', host, internal, 'Home Assistant', '')
+ await service.add_port_mapping(internal, external, host, 'TCP',
+ desc='Home Assistant')
registered.append(external)
- except Exception:
- _LOGGER.exception("UPnP failed to configure port mapping for %s",
- external)
+ _LOGGER.debug("external %s -> %s @ %s", external, internal, host)
+ except UpnpSoapError as error:
+ _LOGGER.error(error)
hass.components.persistent_notification.create(
'ERROR: tcp port {} is already mapped in your router.'
'
Please disable port_mapping in the upnp '
@@ -113,11 +132,13 @@ def setup(hass, config):
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
- def deregister_port(event):
+ async def deregister_port(event):
"""De-register the UPnP port mapping."""
- for external in registered:
- upnp.deleteportmapping(external, 'TCP')
+ tasks = [service.delete_port_mapping(external, 'TCP')
+ for external in registered]
+ if tasks:
+ await asyncio.wait(tasks)
- hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, deregister_port)
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deregister_port)
return True
diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py
index 095e8bfb124..1b7d5685231 100644
--- a/homeassistant/components/vacuum/__init__.py
+++ b/homeassistant/components/vacuum/__init__.py
@@ -57,7 +57,7 @@ VACUUM_SET_FAN_SPEED_SERVICE_SCHEMA = VACUUM_SERVICE_SCHEMA.extend({
VACUUM_SEND_COMMAND_SERVICE_SCHEMA = VACUUM_SERVICE_SCHEMA.extend({
vol.Required(ATTR_COMMAND): cv.string,
- vol.Optional(ATTR_PARAMS): cv.Dict,
+ vol.Optional(ATTR_PARAMS): vol.Any(cv.Dict, cv.ensure_list),
})
SERVICE_TO_METHOD = {
@@ -76,7 +76,6 @@ SERVICE_TO_METHOD = {
}
DEFAULT_NAME = 'Vacuum cleaner robot'
-DEFAULT_ICON = 'mdi:roomba'
SUPPORT_TURN_ON = 1
SUPPORT_TURN_OFF = 2
diff --git a/homeassistant/components/vacuum/demo.py b/homeassistant/components/vacuum/demo.py
index 668e3ca37e6..bd501167ffa 100644
--- a/homeassistant/components/vacuum/demo.py
+++ b/homeassistant/components/vacuum/demo.py
@@ -7,7 +7,7 @@ https://home-assistant.io/components/demo/
import logging
from homeassistant.components.vacuum import (
- ATTR_CLEANED_AREA, DEFAULT_ICON, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT,
+ ATTR_CLEANED_AREA, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT,
SUPPORT_FAN_SPEED, SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME,
SUPPORT_SEND_COMMAND, SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF,
SUPPORT_TURN_ON, VacuumDevice)
@@ -66,11 +66,6 @@ class DemoVacuum(VacuumDevice):
"""Return the name of the vacuum."""
return self._name
- @property
- def icon(self):
- """Return the icon for the vacuum."""
- return DEFAULT_ICON
-
@property
def should_poll(self):
"""No polling needed for a demo vacuum."""
diff --git a/homeassistant/components/vacuum/dyson.py b/homeassistant/components/vacuum/dyson.py
index aa05d004a35..d423a8dacf5 100644
--- a/homeassistant/components/vacuum/dyson.py
+++ b/homeassistant/components/vacuum/dyson.py
@@ -24,8 +24,6 @@ DEPENDENCIES = ['dyson']
DYSON_360_EYE_DEVICES = "dyson_360_eye_devices"
-ICON = 'mdi:roomba'
-
SUPPORT_DYSON = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PAUSE | \
SUPPORT_RETURN_HOME | SUPPORT_FAN_SPEED | SUPPORT_STATUS | \
SUPPORT_BATTERY | SUPPORT_STOP
@@ -56,7 +54,6 @@ class Dyson360EyeDevice(VacuumDevice):
"""Dyson 360 Eye robot vacuum device."""
_LOGGER.debug("Creating device %s", device.name)
self._device = device
- self._icon = ICON
@asyncio.coroutine
def async_added_to_hass(self):
@@ -82,11 +79,6 @@ class Dyson360EyeDevice(VacuumDevice):
"""Return the name of the device."""
return self._device.name
- @property
- def icon(self):
- """Return the icon to use for device."""
- return self._icon
-
@property
def status(self):
"""Return the status of the vacuum cleaner."""
diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/vacuum/mqtt.py
index f4c640f1fc7..ef3bb0f636b 100644
--- a/homeassistant/components/vacuum/mqtt.py
+++ b/homeassistant/components/vacuum/mqtt.py
@@ -12,7 +12,7 @@ import voluptuous as vol
import homeassistant.components.mqtt as mqtt
from homeassistant.components.mqtt import MqttAvailability
from homeassistant.components.vacuum import (
- DEFAULT_ICON, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED,
+ SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED,
SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND,
SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
VacuumDevice)
@@ -340,11 +340,6 @@ class MqttVacuum(MqttAvailability, VacuumDevice):
"""Return the name of the vacuum."""
return self._name
- @property
- def icon(self):
- """Return the icon for the vacuum."""
- return DEFAULT_ICON
-
@property
def should_poll(self):
"""No polling needed for an MQTT vacuum."""
diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/vacuum/neato.py
index 2a4eb2d5e7f..9eba34cea32 100644
--- a/homeassistant/components/vacuum/neato.py
+++ b/homeassistant/components/vacuum/neato.py
@@ -24,8 +24,6 @@ SUPPORT_NEATO = SUPPORT_BATTERY | SUPPORT_PAUSE | SUPPORT_RETURN_HOME | \
SUPPORT_STOP | SUPPORT_TURN_OFF | SUPPORT_TURN_ON | \
SUPPORT_STATUS | SUPPORT_MAP
-ICON = 'mdi:roomba'
-
ATTR_CLEAN_START = 'clean_start'
ATTR_CLEAN_STOP = 'clean_stop'
ATTR_CLEAN_AREA = 'clean_area'
@@ -131,11 +129,6 @@ class NeatoConnectedVacuum(VacuumDevice):
"""Return the name of the device."""
return self._name
- @property
- def icon(self):
- """Return the icon to use for device."""
- return ICON
-
@property
def supported_features(self):
"""Flag vacuum cleaner robot features that are supported."""
diff --git a/homeassistant/components/vacuum/roomba.py b/homeassistant/components/vacuum/roomba.py
index b983b20bd0c..44d22e03f41 100644
--- a/homeassistant/components/vacuum/roomba.py
+++ b/homeassistant/components/vacuum/roomba.py
@@ -43,7 +43,6 @@ DEFAULT_CERT = '/etc/ssl/certs/ca-certificates.crt'
DEFAULT_CONTINUOUS = True
DEFAULT_NAME = 'Roomba'
-ICON = 'mdi:roomba'
PLATFORM = 'roomba'
FAN_SPEED_AUTOMATIC = 'Automatic'
@@ -165,11 +164,6 @@ class RoombaVacuum(VacuumDevice):
"""Return the name of the device."""
return self._name
- @property
- def icon(self):
- """Return the icon to use for device."""
- return ICON
-
@property
def device_state_attributes(self):
"""Return the state attributes of the device."""
diff --git a/homeassistant/components/vacuum/services.yaml b/homeassistant/components/vacuum/services.yaml
index fea365ac7c7..863157074bc 100644
--- a/homeassistant/components/vacuum/services.yaml
+++ b/homeassistant/components/vacuum/services.yaml
@@ -4,93 +4,93 @@ turn_on:
description: Start a new cleaning task.
fields:
entity_id:
- description: Name of the botvac entity.
+ description: Name of the vacuum entity.
example: 'vacuum.xiaomi_vacuum_cleaner'
turn_off:
description: Stop the current cleaning task and return to home.
fields:
entity_id:
- description: Name of the botvac entity.
+ description: Name of the vacuum entity.
example: 'vacuum.xiaomi_vacuum_cleaner'
stop:
description: Stop the current cleaning task.
fields:
entity_id:
- description: Name of the botvac entity.
+ description: Name of the vacuum entity.
example: 'vacuum.xiaomi_vacuum_cleaner'
locate:
description: Locate the vacuum cleaner robot.
fields:
entity_id:
- description: Name of the botvac entity.
+ description: Name of the vacuum entity.
example: 'vacuum.xiaomi_vacuum_cleaner'
start_pause:
description: Start, pause, or resume the cleaning task.
fields:
entity_id:
- description: Name of the botvac entity.
+ description: Name of the vacuum entity.
example: 'vacuum.xiaomi_vacuum_cleaner'
return_to_base:
description: Tell the vacuum cleaner to return to its dock.
fields:
entity_id:
- description: Name of the botvac entity.
+ description: Name of the vacuum entity.
example: 'vacuum.xiaomi_vacuum_cleaner'
clean_spot:
description: Tell the vacuum cleaner to do a spot clean-up.
fields:
entity_id:
- description: Name of the botvac entity.
+ description: Name of the vacuum entity.
example: 'vacuum.xiaomi_vacuum_cleaner'
send_command:
description: Send a raw command to the vacuum cleaner.
fields:
entity_id:
- description: Name of the botvac entity.
+ description: Name of the vacuum entity.
example: 'vacuum.xiaomi_vacuum_cleaner'
command:
description: Command to execute.
example: 'set_dnd_timer'
params:
description: Parameters for the command.
- example: '[22,0,6,0]'
+ example: '{ "key": "value" }'
set_fan_speed:
description: Set the fan speed of the vacuum cleaner.
fields:
entity_id:
- description: Name of the botvac entity.
+ description: Name of the vacuum entity.
example: 'vacuum.xiaomi_vacuum_cleaner'
fan_speed:
- description: Platform dependent vacuum cleaner fan speed, with speed steps, like 'medium', or by percentage, between 0 and 100.
+ description: Platform dependent vacuum cleaner fan speed, with speed steps, like 'medium' or by percentage, between 0 and 100.
example: 'low'
xiaomi_remote_control_start:
description: Start remote control of the vacuum cleaner. You can then move it with `remote_control_move`, when done call `remote_control_stop`.
fields:
entity_id:
- description: Name of the botvac entity.
+ description: Name of the vacuum entity.
example: 'vacuum.xiaomi_vacuum_cleaner'
xiaomi_remote_control_stop:
description: Stop remote control mode of the vacuum cleaner.
fields:
entity_id:
- description: Name of the botvac entity.
+ description: Name of the vacuum entity.
example: 'vacuum.xiaomi_vacuum_cleaner'
xiaomi_remote_control_move:
description: Remote control the vacuum cleaner, make sure you first set it in remote control mode with `remote_control_start`.
fields:
entity_id:
- description: Name of the botvac entity.
+ description: Name of the vacuum entity.
example: 'vacuum.xiaomi_vacuum_cleaner'
velocity:
description: Speed, between -0.29 and 0.29.
@@ -106,7 +106,7 @@ xiaomi_remote_control_move_step:
description: Remote control the vacuum cleaner, only makes one move and then stops.
fields:
entity_id:
- description: Name of the botvac entity.
+ description: Name of the vacuum entity.
example: 'vacuum.xiaomi_vacuum_cleaner'
velocity:
description: Speed, between -0.29 and 0.29.
diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py
index b2451ed495c..620014a1bae 100644
--- a/homeassistant/components/vacuum/xiaomi_miio.py
+++ b/homeassistant/components/vacuum/xiaomi_miio.py
@@ -24,7 +24,6 @@ REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Xiaomi Vacuum cleaner'
-ICON = 'mdi:roomba'
DATA_KEY = 'vacuum.xiaomi_miio'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@@ -142,7 +141,6 @@ class MiroboVacuum(VacuumDevice):
def __init__(self, name, vacuum):
"""Initialize the Xiaomi vacuum cleaner robot handler."""
self._name = name
- self._icon = ICON
self._vacuum = vacuum
self.vacuum_state = None
@@ -158,11 +156,6 @@ class MiroboVacuum(VacuumDevice):
"""Return the name of the device."""
return self._name
- @property
- def icon(self):
- """Return the icon to use for device."""
- return self._icon
-
@property
def status(self):
"""Return the status of the vacuum cleaner."""
diff --git a/homeassistant/components/weather/yweather.py b/homeassistant/components/weather/yweather.py
index f9610e469b2..5987cf7621f 100644
--- a/homeassistant/components/weather/yweather.py
+++ b/homeassistant/components/weather/yweather.py
@@ -32,6 +32,7 @@ DEFAULT_NAME = 'Yweather'
SCAN_INTERVAL = timedelta(minutes=10)
CONDITION_CLASSES = {
+ 'clear-night': [31],
'cloudy': [26, 27, 28, 29, 30],
'fog': [19, 20, 21, 22, 23],
'hail': [17, 18, 35],
diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py
index 39419034545..73c1fdf9075 100644
--- a/homeassistant/components/zha/__init__.py
+++ b/homeassistant/components/zha/__init__.py
@@ -16,7 +16,7 @@ from homeassistant.helpers import discovery, entity
from homeassistant.util import slugify
REQUIREMENTS = [
- 'bellows==0.5.1',
+ 'bellows==0.5.2',
'zigpy==0.0.3',
'zigpy-xbee==0.0.2',
]
diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py
index 69491af1aad..46bb2f7bfe2 100644
--- a/homeassistant/config_entries.py
+++ b/homeassistant/config_entries.py
@@ -27,7 +27,7 @@ At a minimum, each config flow will have to define a version number and the
'init' step.
@config_entries.HANDLERS.register(DOMAIN)
- class ExampleConfigFlow(config_entries.ConfigFlowHandler):
+ class ExampleConfigFlow(config_entries.FlowHandler):
VERSION = 1
@@ -115,6 +115,7 @@ import logging
import os
import uuid
+from . import data_entry_flow
from .core import callback
from .exceptions import HomeAssistantError
from .setup import async_setup_component, async_process_deps_reqs
@@ -130,17 +131,11 @@ FLOWS = [
'hue',
]
-SOURCE_USER = 'user'
-SOURCE_DISCOVERY = 'discovery'
PATH_CONFIG = '.config_entries.json'
SAVE_DELAY = 1
-RESULT_TYPE_FORM = 'form'
-RESULT_TYPE_CREATE_ENTRY = 'create_entry'
-RESULT_TYPE_ABORT = 'abort'
-
ENTRY_STATE_LOADED = 'loaded'
ENTRY_STATE_SETUP_ERROR = 'setup_error'
ENTRY_STATE_NOT_LOADED = 'not_loaded'
@@ -187,24 +182,29 @@ class ConfigEntry:
if not isinstance(result, bool):
_LOGGER.error('%s.async_config_entry did not return boolean',
- self.domain)
+ component.DOMAIN)
result = False
except Exception: # pylint: disable=broad-except
_LOGGER.exception('Error setting up entry %s for %s',
- self.title, self.domain)
+ self.title, component.DOMAIN)
result = False
+ # Only store setup result as state if it was not forwarded.
+ if self.domain != component.DOMAIN:
+ return
+
if result:
self.state = ENTRY_STATE_LOADED
else:
self.state = ENTRY_STATE_SETUP_ERROR
- async def async_unload(self, hass):
+ async def async_unload(self, hass, *, component=None):
"""Unload an entry.
Returns if unload is possible and was successful.
"""
- component = getattr(hass.components, self.domain)
+ if component is None:
+ component = getattr(hass.components, self.domain)
supports_unload = hasattr(component, 'async_unload_entry')
@@ -216,13 +216,13 @@ class ConfigEntry:
if not isinstance(result, bool):
_LOGGER.error('%s.async_unload_entry did not return boolean',
- self.domain)
+ component.DOMAIN)
result = False
return result
except Exception: # pylint: disable=broad-except
_LOGGER.exception('Error unloading entry %s for %s',
- self.title, self.domain)
+ self.title, component.DOMAIN)
self.state = ENTRY_STATE_FAILED_UNLOAD
return False
@@ -246,18 +246,6 @@ class UnknownEntry(ConfigError):
"""Unknown entry specified."""
-class UnknownHandler(ConfigError):
- """Unknown handler specified."""
-
-
-class UnknownFlow(ConfigError):
- """Uknown flow specified."""
-
-
-class UnknownStep(ConfigError):
- """Unknown step specified."""
-
-
class ConfigEntries:
"""Manage the configuration entries.
@@ -267,7 +255,8 @@ class ConfigEntries:
def __init__(self, hass, hass_config):
"""Initialize the entry manager."""
self.hass = hass
- self.flow = FlowManager(hass, hass_config, self._async_add_entry)
+ self.flow = data_entry_flow.FlowManager(
+ hass, self._async_create_flow, self._async_save_entry)
self._hass_config = hass_config
self._entries = None
self._sched_save = None
@@ -322,8 +311,45 @@ class ConfigEntries:
entries = await self.hass.async_add_job(load_json, path)
self._entries = [ConfigEntry(**entry) for entry in entries]
- async def _async_add_entry(self, entry):
+ async def async_forward_entry_setup(self, entry, component):
+ """Forward the setup of an entry to a different component.
+
+ By default an entry is setup with the component it belongs to. If that
+ component also has related platforms, the component will have to
+ forward the entry to be setup by that component.
+
+ You don't want to await this coroutine if it is called as part of the
+ setup of a component, because it can cause a deadlock.
+ """
+ # Setup Component if not set up yet
+ if component not in self.hass.config.components:
+ result = await async_setup_component(
+ self.hass, component, self._hass_config)
+
+ if not result:
+ return False
+
+ await entry.async_setup(
+ self.hass, component=getattr(self.hass.components, component))
+
+ async def async_forward_entry_unload(self, entry, component):
+ """Forward the unloading of an entry to a different component."""
+ # It was never loaded.
+ if component not in self.hass.config.components:
+ return True
+
+ return await entry.async_unload(
+ self.hass, component=getattr(self.hass.components, component))
+
+ async def _async_save_entry(self, result):
"""Add an entry."""
+ entry = ConfigEntry(
+ version=result['version'],
+ domain=result['handler'],
+ title=result['title'],
+ data=result['data'],
+ source=result['source'],
+ )
self._entries.append(entry)
self._async_schedule_save()
@@ -336,6 +362,25 @@ class ConfigEntries:
await async_setup_component(
self.hass, entry.domain, self._hass_config)
+ return entry
+
+ async def _async_create_flow(self, handler):
+ """Create a flow for specified handler.
+
+ Handler key is the domain of the component that we want to setup.
+ """
+ component = getattr(self.hass.components, handler)
+ handler = HANDLERS.get(handler)
+
+ if handler is None:
+ raise data_entry_flow.UnknownHandler
+
+ # Make sure requirements and dependencies of component are resolved
+ await async_process_deps_reqs(
+ self.hass, self._hass_config, handler, component)
+
+ return handler()
+
@callback
def _async_schedule_save(self):
"""Schedule saving the entity registry."""
@@ -353,157 +398,3 @@ class ConfigEntries:
await self.hass.async_add_job(
save_json, self.hass.config.path(PATH_CONFIG), data)
-
-
-class FlowManager:
- """Manage all the config flows that are in progress."""
-
- def __init__(self, hass, hass_config, async_add_entry):
- """Initialize the flow manager."""
- self.hass = hass
- self._hass_config = hass_config
- self._progress = {}
- self._async_add_entry = async_add_entry
-
- @callback
- def async_progress(self):
- """Return the flows in progress."""
- return [{
- 'flow_id': flow.flow_id,
- 'domain': flow.domain,
- 'source': flow.source,
- } for flow in self._progress.values()]
-
- async def async_init(self, domain, *, source=SOURCE_USER, data=None):
- """Start a configuration flow."""
- handler = HANDLERS.get(domain)
-
- if handler is None:
- # This will load the component and thus register the handler
- component = getattr(self.hass.components, domain)
- handler = HANDLERS.get(domain)
-
- if handler is None:
- raise UnknownHandler
-
- # Make sure requirements and dependencies of component are resolved
- await async_process_deps_reqs(
- self.hass, self._hass_config, domain, component)
-
- flow_id = uuid.uuid4().hex
- flow = self._progress[flow_id] = handler()
- flow.hass = self.hass
- flow.domain = domain
- flow.flow_id = flow_id
- flow.source = source
-
- if source == SOURCE_USER:
- step = 'init'
- else:
- step = source
-
- return await self._async_handle_step(flow, step, data)
-
- async def async_configure(self, flow_id, user_input=None):
- """Start or continue a configuration flow."""
- flow = self._progress.get(flow_id)
-
- if flow is None:
- raise UnknownFlow
-
- step_id, data_schema = flow.cur_step
-
- if data_schema is not None and user_input is not None:
- user_input = data_schema(user_input)
-
- return await self._async_handle_step(
- flow, step_id, user_input)
-
- @callback
- def async_abort(self, flow_id):
- """Abort a flow."""
- if self._progress.pop(flow_id, None) is None:
- raise UnknownFlow
-
- async def _async_handle_step(self, flow, step_id, user_input):
- """Handle a step of a flow."""
- method = "async_step_{}".format(step_id)
-
- if not hasattr(flow, method):
- self._progress.pop(flow.flow_id)
- raise UnknownStep("Handler {} doesn't support step {}".format(
- flow.__class__.__name__, step_id))
-
- result = await getattr(flow, method)(user_input)
-
- if result['type'] not in (RESULT_TYPE_FORM, RESULT_TYPE_CREATE_ENTRY,
- RESULT_TYPE_ABORT):
- raise ValueError(
- 'Handler returned incorrect type: {}'.format(result['type']))
-
- if result['type'] == RESULT_TYPE_FORM:
- flow.cur_step = (result['step_id'], result['data_schema'])
- return result
-
- # Abort and Success results both finish the flow
- self._progress.pop(flow.flow_id)
-
- if result['type'] == RESULT_TYPE_ABORT:
- return result
-
- entry = ConfigEntry(
- version=flow.VERSION,
- domain=flow.domain,
- title=result['title'],
- data=result.pop('data'),
- source=flow.source
- )
- await self._async_add_entry(entry)
- return result
-
-
-class ConfigFlowHandler:
- """Handle the configuration flow of a component."""
-
- # Set by flow manager
- flow_id = None
- hass = None
- domain = None
- source = SOURCE_USER
- cur_step = None
-
- # Set by dev
- # VERSION
-
- @callback
- def async_show_form(self, *, step_id, data_schema=None, errors=None):
- """Return the definition of a form to gather user input."""
- return {
- 'type': RESULT_TYPE_FORM,
- 'flow_id': self.flow_id,
- 'domain': self.domain,
- 'step_id': step_id,
- 'data_schema': data_schema,
- 'errors': errors,
- }
-
- @callback
- def async_create_entry(self, *, title, data):
- """Finish config flow and create a config entry."""
- return {
- 'type': RESULT_TYPE_CREATE_ENTRY,
- 'flow_id': self.flow_id,
- 'domain': self.domain,
- 'title': title,
- 'data': data,
- }
-
- @callback
- def async_abort(self, *, reason):
- """Abort the config flow."""
- return {
- 'type': RESULT_TYPE_ABORT,
- 'flow_id': self.flow_id,
- 'domain': self.domain,
- 'reason': reason
- }
diff --git a/homeassistant/const.py b/homeassistant/const.py
index 4b8d7bcd3bc..43380d00a2d 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -1,8 +1,8 @@
# coding: utf-8
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
-MINOR_VERSION = 67
-PATCH_VERSION = '1'
+MINOR_VERSION = 68
+PATCH_VERSION = '0.dev0'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 5, 3)
diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py
new file mode 100644
index 00000000000..cadec3f3d69
--- /dev/null
+++ b/homeassistant/data_entry_flow.py
@@ -0,0 +1,167 @@
+"""Classes to help gather user submissions."""
+import logging
+import uuid
+
+from .core import callback
+from .exceptions import HomeAssistantError
+
+_LOGGER = logging.getLogger(__name__)
+
+SOURCE_USER = 'user'
+SOURCE_DISCOVERY = 'discovery'
+
+RESULT_TYPE_FORM = 'form'
+RESULT_TYPE_CREATE_ENTRY = 'create_entry'
+RESULT_TYPE_ABORT = 'abort'
+
+
+class FlowError(HomeAssistantError):
+ """Error while configuring an account."""
+
+
+class UnknownHandler(FlowError):
+ """Unknown handler specified."""
+
+
+class UnknownFlow(FlowError):
+ """Uknown flow specified."""
+
+
+class UnknownStep(FlowError):
+ """Unknown step specified."""
+
+
+class FlowManager:
+ """Manage all the flows that are in progress."""
+
+ def __init__(self, hass, async_create_flow, async_finish_flow):
+ """Initialize the flow manager."""
+ self.hass = hass
+ self._progress = {}
+ self._async_create_flow = async_create_flow
+ self._async_finish_flow = async_finish_flow
+
+ @callback
+ def async_progress(self):
+ """Return the flows in progress."""
+ return [{
+ 'flow_id': flow.flow_id,
+ 'handler': flow.handler,
+ 'source': flow.source,
+ } for flow in self._progress.values()]
+
+ async def async_init(self, handler, *, source=SOURCE_USER, data=None):
+ """Start a configuration flow."""
+ flow = await self._async_create_flow(handler)
+ flow.hass = self.hass
+ flow.handler = handler
+ flow.flow_id = uuid.uuid4().hex
+ flow.source = source
+ self._progress[flow.flow_id] = flow
+
+ if source == SOURCE_USER:
+ step = 'init'
+ else:
+ step = source
+
+ return await self._async_handle_step(flow, step, data)
+
+ async def async_configure(self, flow_id, user_input=None):
+ """Start or continue a configuration flow."""
+ flow = self._progress.get(flow_id)
+
+ if flow is None:
+ raise UnknownFlow
+
+ step_id, data_schema = flow.cur_step
+
+ if data_schema is not None and user_input is not None:
+ user_input = data_schema(user_input)
+
+ return await self._async_handle_step(
+ flow, step_id, user_input)
+
+ @callback
+ def async_abort(self, flow_id):
+ """Abort a flow."""
+ if self._progress.pop(flow_id, None) is None:
+ raise UnknownFlow
+
+ async def _async_handle_step(self, flow, step_id, user_input):
+ """Handle a step of a flow."""
+ method = "async_step_{}".format(step_id)
+
+ if not hasattr(flow, method):
+ self._progress.pop(flow.flow_id)
+ raise UnknownStep("Handler {} doesn't support step {}".format(
+ flow.__class__.__name__, step_id))
+
+ result = await getattr(flow, method)(user_input)
+
+ if result['type'] not in (RESULT_TYPE_FORM, RESULT_TYPE_CREATE_ENTRY,
+ RESULT_TYPE_ABORT):
+ raise ValueError(
+ 'Handler returned incorrect type: {}'.format(result['type']))
+
+ if result['type'] == RESULT_TYPE_FORM:
+ flow.cur_step = (result['step_id'], result['data_schema'])
+ return result
+
+ # Abort and Success results both finish the flow
+ self._progress.pop(flow.flow_id)
+
+ if result['type'] == RESULT_TYPE_ABORT:
+ return result
+
+ # We pass a copy of the result because we're mutating our version
+ result['result'] = await self._async_finish_flow(dict(result))
+ return result
+
+
+class FlowHandler:
+ """Handle the configuration flow of a component."""
+
+ # Set by flow manager
+ flow_id = None
+ hass = None
+ handler = None
+ source = SOURCE_USER
+ cur_step = None
+
+ # Set by developer
+ VERSION = 1
+
+ @callback
+ def async_show_form(self, *, step_id, data_schema=None, errors=None):
+ """Return the definition of a form to gather user input."""
+ return {
+ 'type': RESULT_TYPE_FORM,
+ 'flow_id': self.flow_id,
+ 'handler': self.handler,
+ 'step_id': step_id,
+ 'data_schema': data_schema,
+ 'errors': errors,
+ }
+
+ @callback
+ def async_create_entry(self, *, title, data):
+ """Finish config flow and create a config entry."""
+ return {
+ 'version': self.VERSION,
+ 'type': RESULT_TYPE_CREATE_ENTRY,
+ 'flow_id': self.flow_id,
+ 'handler': self.handler,
+ 'title': title,
+ 'data': data,
+ 'source': self.source,
+ }
+
+ @callback
+ def async_abort(self, *, reason):
+ """Abort the config flow."""
+ return {
+ 'type': RESULT_TYPE_ABORT,
+ 'flow_id': self.flow_id,
+ 'handler': self.handler,
+ 'reason': reason
+ }
diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py
new file mode 100644
index 00000000000..a8aca2fd2e9
--- /dev/null
+++ b/homeassistant/helpers/data_entry_flow.py
@@ -0,0 +1,106 @@
+"""Helpers for the data entry flow."""
+
+import voluptuous as vol
+
+from homeassistant import data_entry_flow
+from homeassistant.components.http import HomeAssistantView
+from homeassistant.components.http.data_validator import RequestDataValidator
+
+
+def _prepare_json(result):
+ """Convert result for JSON."""
+ if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
+ data = result.copy()
+ data.pop('result')
+ data.pop('data')
+ return data
+
+ elif result['type'] != data_entry_flow.RESULT_TYPE_FORM:
+ return result
+
+ import voluptuous_serialize
+
+ data = result.copy()
+
+ schema = data['data_schema']
+ if schema is None:
+ data['data_schema'] = []
+ else:
+ data['data_schema'] = voluptuous_serialize.convert(schema)
+
+ return data
+
+
+class FlowManagerIndexView(HomeAssistantView):
+ """View to create config flows."""
+
+ def __init__(self, flow_mgr):
+ """Initialize the flow manager index view."""
+ self._flow_mgr = flow_mgr
+
+ async def get(self, request):
+ """List flows that are in progress."""
+ return self.json(self._flow_mgr.async_progress())
+
+ @RequestDataValidator(vol.Schema({
+ vol.Required('handler'): vol.Any(str, list),
+ }))
+ async def post(self, request, data):
+ """Handle a POST request."""
+ if isinstance(data['handler'], list):
+ handler = tuple(data['handler'])
+ else:
+ handler = data['handler']
+
+ try:
+ result = await self._flow_mgr.async_init(handler)
+ except data_entry_flow.UnknownHandler:
+ return self.json_message('Invalid handler specified', 404)
+ except data_entry_flow.UnknownStep:
+ return self.json_message('Handler does not support init', 400)
+
+ result = _prepare_json(result)
+
+ return self.json(result)
+
+
+class FlowManagerResourceView(HomeAssistantView):
+ """View to interact with the flow manager."""
+
+ def __init__(self, flow_mgr):
+ """Initialize the flow manager resource view."""
+ self._flow_mgr = flow_mgr
+
+ async def get(self, request, flow_id):
+ """Get the current state of a data_entry_flow."""
+ try:
+ result = await self._flow_mgr.async_configure(flow_id)
+ except data_entry_flow.UnknownFlow:
+ return self.json_message('Invalid flow specified', 404)
+
+ result = _prepare_json(result)
+
+ return self.json(result)
+
+ @RequestDataValidator(vol.Schema(dict), allow_empty=True)
+ async def post(self, request, flow_id, data):
+ """Handle a POST request."""
+ try:
+ result = await self._flow_mgr.async_configure(flow_id, data)
+ except data_entry_flow.UnknownFlow:
+ return self.json_message('Invalid flow specified', 404)
+ except vol.Invalid:
+ return self.json_message('User input malformed', 400)
+
+ result = _prepare_json(result)
+
+ return self.json(result)
+
+ async def delete(self, request, flow_id):
+ """Cancel a flow in progress."""
+ try:
+ self._flow_mgr.async_abort(flow_id)
+ except data_entry_flow.UnknownFlow:
+ return self.json_message('Invalid flow specified', 404)
+
+ return self.json_message('Flow aborted')
diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py
index f086437c10d..c82ae2a46f0 100644
--- a/homeassistant/helpers/entity_component.py
+++ b/homeassistant/helpers/entity_component.py
@@ -40,16 +40,7 @@ class EntityComponent(object):
self.config = None
self._platforms = {
- domain: EntityPlatform(
- hass=hass,
- logger=logger,
- domain=domain,
- platform_name=domain,
- scan_interval=self.scan_interval,
- parallel_updates=0,
- entity_namespace=None,
- async_entities_added_callback=self._async_update_group,
- )
+ domain: self._async_init_entity_platform(domain, None)
}
self.async_add_entities = self._platforms[domain].async_add_entities
self.add_entities = self._platforms[domain].add_entities
@@ -102,6 +93,38 @@ class EntityComponent(object):
discovery.async_listen_platform(
self.hass, self.domain, component_platform_discovered)
+ async def async_setup_entry(self, config_entry):
+ """Setup a config entry."""
+ platform_type = config_entry.domain
+ platform = await async_prepare_setup_platform(
+ self.hass, self.config, self.domain, platform_type)
+
+ if platform is None:
+ return False
+
+ key = config_entry.entry_id
+
+ if key in self._platforms:
+ raise ValueError('Config entry has already been setup!')
+
+ self._platforms[key] = self._async_init_entity_platform(
+ platform_type, platform
+ )
+
+ return await self._platforms[key].async_setup_entry(config_entry)
+
+ async def async_unload_entry(self, config_entry):
+ """Unload a config entry."""
+ key = config_entry.entry_id
+
+ platform = self._platforms.pop(key, None)
+
+ if platform is None:
+ raise ValueError('Config entry was never loaded!')
+
+ await platform.async_reset()
+ return True
+
@callback
def async_extract_from_service(self, service, expand_group=True):
"""Extract all known and available entities from a service call.
@@ -127,34 +150,19 @@ class EntityComponent(object):
if platform is None:
return
- # Config > Platform > Component
- scan_interval = (
- platform_config.get(CONF_SCAN_INTERVAL) or
- getattr(platform, 'SCAN_INTERVAL', None) or self.scan_interval)
- parallel_updates = getattr(
- platform, 'PARALLEL_UPDATES',
- int(not hasattr(platform, 'async_setup_platform')))
-
+ # Use config scan interval, fallback to platform if none set
+ scan_interval = platform_config.get(
+ CONF_SCAN_INTERVAL, getattr(platform, 'SCAN_INTERVAL', None))
entity_namespace = platform_config.get(CONF_ENTITY_NAMESPACE)
key = (platform_type, scan_interval, entity_namespace)
if key not in self._platforms:
- entity_platform = self._platforms[key] = EntityPlatform(
- hass=self.hass,
- logger=self.logger,
- domain=self.domain,
- platform_name=platform_type,
- scan_interval=scan_interval,
- parallel_updates=parallel_updates,
- entity_namespace=entity_namespace,
- async_entities_added_callback=self._async_update_group,
+ self._platforms[key] = self._async_init_entity_platform(
+ platform_type, platform, scan_interval, entity_namespace
)
- else:
- entity_platform = self._platforms[key]
- await entity_platform.async_setup(
- platform, platform_config, discovery_info)
+ await self._platforms[key].async_setup(platform_config, discovery_info)
@callback
def _async_update_group(self):
@@ -219,3 +227,20 @@ class EntityComponent(object):
await self._async_reset()
return conf
+
+ def _async_init_entity_platform(self, platform_type, platform,
+ scan_interval=None, entity_namespace=None):
+ """Helper to initialize an entity platform."""
+ if scan_interval is None:
+ scan_interval = self.scan_interval
+
+ return EntityPlatform(
+ hass=self.hass,
+ logger=self.logger,
+ domain=self.domain,
+ platform_name=platform_type,
+ platform=platform,
+ scan_interval=scan_interval,
+ entity_namespace=entity_namespace,
+ async_entities_added_callback=self._async_update_group,
+ )
diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py
index 501ab5057a3..00a7e49840e 100644
--- a/homeassistant/helpers/entity_platform.py
+++ b/homeassistant/helpers/entity_platform.py
@@ -1,15 +1,13 @@
"""Class to manage the entities for a single platform."""
import asyncio
-from datetime import timedelta
from homeassistant.const import DEVICE_DEFAULT_NAME
from homeassistant.core import callback, valid_entity_id, split_entity_id
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
from homeassistant.util.async_ import (
run_callback_threadsafe, run_coroutine_threadsafe)
-import homeassistant.util.dt as dt_util
-from .event import async_track_time_interval, async_track_point_in_time
+from .event import async_track_time_interval, async_call_later
from .entity_registry import async_get_registry
SLOW_SETUP_WARNING = 10
@@ -20,8 +18,8 @@ PLATFORM_NOT_READY_RETRIES = 10
class EntityPlatform(object):
"""Manage the entities for a single platform."""
- def __init__(self, *, hass, logger, domain, platform_name, scan_interval,
- parallel_updates, entity_namespace,
+ def __init__(self, *, hass, logger, domain, platform_name, platform,
+ scan_interval, entity_namespace,
async_entities_added_callback):
"""Initialize the entity platform.
@@ -38,22 +36,81 @@ class EntityPlatform(object):
self.logger = logger
self.domain = domain
self.platform_name = platform_name
+ self.platform = platform
self.scan_interval = scan_interval
- self.parallel_updates = None
self.entity_namespace = entity_namespace
self.async_entities_added_callback = async_entities_added_callback
+ self.config_entry = None
self.entities = {}
self._tasks = []
+ # Method to cancel the state change listener
self._async_unsub_polling = None
+ # Method to cancel the retry of setup
+ self._async_cancel_retry_setup = None
self._process_updates = asyncio.Lock(loop=hass.loop)
+ # Platform is None for the EntityComponent "catch-all" EntityPlatform
+ # which powers entity_component.add_entities
+ if platform is None:
+ self.parallel_updates = None
+ return
+
+ # Async platforms do all updates in parallel by default
+ if hasattr(platform, 'async_setup_platform'):
+ default_parallel_updates = 0
+ else:
+ default_parallel_updates = 1
+
+ parallel_updates = getattr(platform, 'PARALLEL_UPDATES',
+ default_parallel_updates)
+
if parallel_updates:
self.parallel_updates = asyncio.Semaphore(
parallel_updates, loop=hass.loop)
+ else:
+ self.parallel_updates = None
- async def async_setup(self, platform, platform_config, discovery_info=None,
- tries=0):
- """Setup the platform."""
+ async def async_setup(self, platform_config, discovery_info=None):
+ """Setup the platform from a config file."""
+ platform = self.platform
+ hass = self.hass
+
+ @callback
+ def async_create_setup_task():
+ """Get task to setup platform."""
+ if getattr(platform, 'async_setup_platform', None):
+ return platform.async_setup_platform(
+ hass, platform_config,
+ self._async_schedule_add_entities, discovery_info
+ )
+
+ # This should not be replaced with hass.async_add_job because
+ # we don't want to track this task in case it blocks startup.
+ return hass.loop.run_in_executor(
+ None, platform.setup_platform, hass, platform_config,
+ self._schedule_add_entities, discovery_info
+ )
+ await self._async_setup_platform(async_create_setup_task)
+
+ async def async_setup_entry(self, config_entry):
+ """Setup the platform from a config entry."""
+ # Store it so that we can save config entry ID in entity registry
+ self.config_entry = config_entry
+ platform = self.platform
+
+ @callback
+ def async_create_setup_task():
+ """Get task to setup platform."""
+ return platform.async_setup_entry(
+ self.hass, config_entry, self._async_schedule_add_entities)
+
+ return await self._async_setup_platform(async_create_setup_task)
+
+ async def _async_setup_platform(self, async_create_setup_task, tries=0):
+ """Helper to setup a platform via config file or config entry.
+
+ async_create_setup_task creates a coroutine that sets up platform.
+ """
logger = self.logger
hass = self.hass
full_name = '{}.{}'.format(self.domain, self.platform_name)
@@ -65,18 +122,8 @@ class EntityPlatform(object):
self.platform_name, SLOW_SETUP_WARNING)
try:
- if getattr(platform, 'async_setup_platform', None):
- task = platform.async_setup_platform(
- hass, platform_config,
- self._async_schedule_add_entities, discovery_info
- )
- else:
- # This should not be replaced with hass.async_add_job because
- # we don't want to track this task in case it blocks startup.
- task = hass.loop.run_in_executor(
- None, platform.setup_platform, hass, platform_config,
- self._schedule_add_entities, discovery_info
- )
+ task = async_create_setup_task()
+
await asyncio.wait_for(
asyncio.shield(task, loop=hass.loop),
SLOW_SETUP_MAX_WAIT, loop=hass.loop)
@@ -91,24 +138,33 @@ class EntityPlatform(object):
pending, loop=self.hass.loop)
hass.config.components.add(full_name)
+ return True
except PlatformNotReady:
tries += 1
wait_time = min(tries, 6) * 30
logger.warning(
'Platform %s not ready yet. Retrying in %d seconds.',
self.platform_name, wait_time)
- async_track_point_in_time(
- hass, self.async_setup(
- platform, platform_config, discovery_info, tries),
- dt_util.utcnow() + timedelta(seconds=wait_time))
+
+ async def setup_again(now):
+ """Run setup again."""
+ self._async_cancel_retry_setup = None
+ await self._async_setup_platform(
+ async_create_setup_task, tries)
+
+ self._async_cancel_retry_setup = \
+ async_call_later(hass, wait_time, setup_again)
+ return False
except asyncio.TimeoutError:
logger.error(
"Setup of platform %s is taking longer than %s seconds."
" Startup will proceed without waiting any longer.",
self.platform_name, SLOW_SETUP_MAX_WAIT)
+ return False
except Exception: # pylint: disable=broad-except
logger.exception(
"Error while setting up platform %s", self.platform_name)
+ return False
finally:
warn_task.cancel()
@@ -264,6 +320,10 @@ class EntityPlatform(object):
This method must be run in the event loop.
"""
+ if self._async_cancel_retry_setup is not None:
+ self._async_cancel_retry_setup()
+ self._async_cancel_retry_setup = None
+
if not self.entities:
return
diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py
index 353fda28875..3a24de6b39c 100644
--- a/homeassistant/helpers/template.py
+++ b/homeassistant/helpers/template.py
@@ -452,6 +452,38 @@ def logarithm(value, base=math.e):
return value
+def sine(value):
+ """Filter to get sine of the value."""
+ try:
+ return math.sin(float(value))
+ except (ValueError, TypeError):
+ return value
+
+
+def cosine(value):
+ """Filter to get cosine of the value."""
+ try:
+ return math.cos(float(value))
+ except (ValueError, TypeError):
+ return value
+
+
+def tangent(value):
+ """Filter to get tangent of the value."""
+ try:
+ return math.tan(float(value))
+ except (ValueError, TypeError):
+ return value
+
+
+def square_root(value):
+ """Filter to get square root of the value."""
+ try:
+ return math.sqrt(float(value))
+ except (ValueError, TypeError):
+ return value
+
+
def timestamp_custom(value, date_format=DATE_STR_FORMAT, local=True):
"""Filter to convert given timestamp to format."""
try:
@@ -571,6 +603,10 @@ ENV = TemplateEnvironment()
ENV.filters['round'] = forgiving_round
ENV.filters['multiply'] = multiply
ENV.filters['log'] = logarithm
+ENV.filters['sin'] = sine
+ENV.filters['cos'] = cosine
+ENV.filters['tan'] = tangent
+ENV.filters['sqrt'] = square_root
ENV.filters['timestamp_custom'] = timestamp_custom
ENV.filters['timestamp_local'] = timestamp_local
ENV.filters['timestamp_utc'] = timestamp_utc
@@ -583,6 +619,13 @@ ENV.filters['regex_replace'] = regex_replace
ENV.filters['regex_search'] = regex_search
ENV.filters['regex_findall_index'] = regex_findall_index
ENV.globals['log'] = logarithm
+ENV.globals['sin'] = sine
+ENV.globals['cos'] = cosine
+ENV.globals['tan'] = tangent
+ENV.globals['sqrt'] = square_root
+ENV.globals['pi'] = math.pi
+ENV.globals['tau'] = math.pi * 2
+ENV.globals['e'] = math.e
ENV.globals['float'] = forgiving_float
ENV.globals['now'] = dt_util.now
ENV.globals['utcnow'] = dt_util.utcnow
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index 85f8d5dcf12..6de885942fb 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -5,7 +5,7 @@ pip>=8.0.3
jinja2>=2.10
voluptuous==0.11.1
typing>=3,<4
-aiohttp==3.1.1
+aiohttp==3.1.3
async_timeout==2.0.1
astral==1.6
certifi>=2017.4.17
diff --git a/requirements_all.txt b/requirements_all.txt
index 36a8f30502f..aeb5b84811e 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -6,7 +6,7 @@ pip>=8.0.3
jinja2>=2.10
voluptuous==0.11.1
typing>=3,<4
-aiohttp==3.1.1
+aiohttp==3.1.3
async_timeout==2.0.1
astral==1.6
certifi>=2017.4.17
@@ -15,6 +15,12 @@ attrs==17.4.0
# homeassistant.components.nuimo_controller
--only-binary=all https://github.com/getSenic/nuimo-linux-python/archive/29fc42987f74d8090d0e2382e8f248ff5990b8c9.zip#nuimo==1.0.0
+# homeassistant.components.sensor.sht31
+Adafruit-GPIO==1.0.3
+
+# homeassistant.components.sensor.sht31
+Adafruit-SHT31==1.0.2
+
# homeassistant.components.bbb_gpio
# Adafruit_BBIO==1.0.0
@@ -22,7 +28,7 @@ attrs==17.4.0
DoorBirdPy==0.1.3
# homeassistant.components.homekit
-HAP-python==1.1.8
+HAP-python==1.1.9
# homeassistant.components.notify.mastodon
Mastodon.py==1.2.2
@@ -64,7 +70,7 @@ WazeRouteCalculator==0.5
YesssSMS==0.1.1b3
# homeassistant.components.abode
-abodepy==0.12.3
+abodepy==0.13.1
# homeassistant.components.media_player.frontier_silicon
afsapi==0.0.3
@@ -98,7 +104,7 @@ aiopvapi==1.5.4
alarmdecoder==1.13.2
# homeassistant.components.sensor.alpha_vantage
-alpha_vantage==1.9.0
+alpha_vantage==2.0.0
# homeassistant.components.amcrest
amcrest==1.2.2
@@ -131,7 +137,7 @@ basicmodem==0.7
batinfo==0.4.2
# homeassistant.components.sensor.eddystone_temperature
-# beacontools[scan]==1.2.1
+# beacontools[scan]==1.2.3
# homeassistant.components.device_tracker.linksys_ap
# homeassistant.components.sensor.geizhals
@@ -140,7 +146,7 @@ batinfo==0.4.2
beautifulsoup4==4.6.0
# homeassistant.components.zha
-bellows==0.5.1
+bellows==0.5.2
# homeassistant.components.bmw_connected_drive
bimmer_connected==0.5.0
@@ -203,6 +209,7 @@ colorlog==3.1.2
concord232==0.15
# homeassistant.components.climate.eq3btsmart
+# homeassistant.components.device_tracker.xiaomi_miio
# homeassistant.components.fan.xiaomi_miio
# homeassistant.components.light.xiaomi_miio
# homeassistant.components.remote.xiaomi_miio
@@ -311,7 +318,7 @@ fixerio==0.1.1
flux_led==0.21
# homeassistant.components.sensor.foobot
-foobot_async==0.3.0
+foobot_async==0.3.1
# homeassistant.components.notify.free_mobile
freesms==0.1.2
@@ -379,7 +386,10 @@ hipnotify==1.0.8
holidays==0.9.4
# homeassistant.components.frontend
-home-assistant-frontend==20180404.0
+home-assistant-frontend==20180420.0
+
+# homeassistant.components.homekit_controller
+# homekit==0.5
# homeassistant.components.homematicip_cloud
homematicip==0.8
@@ -454,6 +464,9 @@ keyring==12.0.0
# homeassistant.scripts.keyring
keyrings.alt==3.0
+# homeassistant.components.eufy
+lakeside==0.5
+
# homeassistant.components.device_tracker.owntracks
# homeassistant.components.device_tracker.owntracks_http
libnacl==1.6.1
@@ -490,7 +503,7 @@ liveboxplaytv==2.0.2
lmnotify==0.0.4
# homeassistant.components.device_tracker.google_maps
-locationsharinglib==0.4.0
+locationsharinglib==1.2.1
# homeassistant.components.sensor.luftdaten
luftdaten==0.1.3
@@ -512,10 +525,7 @@ messagebird==1.2.0
mficlient==0.3.0
# homeassistant.components.sensor.miflora
-miflora==0.3.0
-
-# homeassistant.components.upnp
-miniupnpc==2.0.2
+miflora==0.4.0
# homeassistant.components.sensor.mopar
motorparts==1.0.2
@@ -540,7 +550,7 @@ nad_receiver==0.0.9
nanoleaf==0.4.1
# homeassistant.components.discovery
-netdisco==1.3.0
+netdisco==1.3.1
# homeassistant.components.sensor.neurio_energy
neurio==0.3.1
@@ -670,7 +680,7 @@ pyHS100==0.3.0
pyRFXtrx==0.22.0
# homeassistant.components.sensor.tibber
-pyTibber==0.4.0
+pyTibber==0.4.1
# homeassistant.components.switch.dlink
pyW215==0.6.0
@@ -700,6 +710,9 @@ pyatv==0.3.9
# homeassistant.components.sensor.bbox
pybbox==0.0.5-alpha
+# homeassistant.components.media_player.blackbird
+pyblackbird==0.5
+
# homeassistant.components.device_tracker.bluetooth_tracker
# pybluez==0.22
@@ -726,7 +739,7 @@ pycsspeechtts==1.0.2
pydaikin==0.4
# homeassistant.components.deconz
-pydeconz==35
+pydeconz==36
# homeassistant.components.zwave
pydispatcher==2.0.5
@@ -734,9 +747,6 @@ pydispatcher==2.0.5
# homeassistant.components.android_ip_webcam
pydroid-ipcam==0.8
-# homeassistant.components.sensor.ebox
-pyebox==0.1.0
-
# homeassistant.components.climate.econet
pyeconet==0.0.5
@@ -761,6 +771,9 @@ pyfido==2.1.1
# homeassistant.components.climate.flexit
pyflexit==0.3
+# homeassistant.components.fritzbox
+pyfritzhome==0.3.7
+
# homeassistant.components.ifttt
pyfttt==0.3
@@ -774,10 +787,10 @@ pyharmony==1.0.20
pyhik==0.1.8
# homeassistant.components.hive
-pyhiveapi==0.2.11
+pyhiveapi==0.2.14
# homeassistant.components.homematic
-pyhomematic==0.1.40
+pyhomematic==0.1.41
# homeassistant.components.sensor.hydroquebec
pyhydroquebec==2.2.2
@@ -820,7 +833,7 @@ pylitejet==0.1
pyloopenergy==0.0.18
# homeassistant.components.lutron_caseta
-pylutron-caseta==0.3.0
+pylutron-caseta==0.5.0
# homeassistant.components.lutron
pylutron==0.1.0
@@ -829,7 +842,7 @@ pylutron==0.1.0
pymailgunner==1.4
# homeassistant.components.media_player.mediaroom
-pymediaroom==0.6
+pymediaroom==0.6.3
# homeassistant.components.media_player.xiaomi_tv
pymitv==1.0.0
@@ -885,7 +898,7 @@ pyowm==2.8.0
pypollencom==1.1.2
# homeassistant.components.qwikswitch
-pyqwikswitch==0.6
+pyqwikswitch==0.71
# homeassistant.components.rainbird
pyrainbird==0.1.3
@@ -939,7 +952,7 @@ python-clementine-remote==1.0.1
python-digitalocean==1.13.2
# homeassistant.components.ecobee
-python-ecobee-api==0.0.17
+python-ecobee-api==0.0.18
# homeassistant.components.climate.eq3btsmart
# python-eq3bt==0.1.9
@@ -977,7 +990,7 @@ python-juicenet==0.0.5
python-miio==0.3.9
# homeassistant.components.media_player.mpd
-python-mpd2==0.5.5
+python-mpd2==1.0.0
# homeassistant.components.light.mystrom
# homeassistant.components.switch.mystrom
@@ -1052,6 +1065,9 @@ pytradfri[async]==5.4.2
# homeassistant.components.device_tracker.unifi
pyunifi==2.13
+# homeassistant.components.upnp
+pyupnp-async==0.1.0.1
+
# homeassistant.components.keyboard
# pyuserinput==0.1.11
@@ -1159,7 +1175,7 @@ simplepush==1.1.4
simplisafe-python==1.0.5
# homeassistant.components.skybell
-skybellpy==0.1.1
+skybellpy==0.1.2
# homeassistant.components.notify.slack
slacker==0.9.65
@@ -1196,7 +1212,7 @@ spotcrime==1.0.3
# homeassistant.components.recorder
# homeassistant.scripts.db_migrator
# homeassistant.components.sensor.sql
-sqlalchemy==1.2.5
+sqlalchemy==1.2.6
# homeassistant.components.statsd
statsd==3.2.1
@@ -1262,6 +1278,9 @@ upcloud-api==0.4.2
# homeassistant.components.sensor.ups
upsmychoice==1.0.6
+# homeassistant.components.sensor.uscis
+uscisstatus==0.1.1
+
# homeassistant.components.camera.uvc
uvcclient==0.10.1
@@ -1339,7 +1358,7 @@ yeelight==0.4.0
yeelightsunflower==0.0.10
# homeassistant.components.media_extractor
-youtube_dl==2018.04.03
+youtube_dl==2018.04.16
# homeassistant.components.light.zengge
zengge==0.2
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index e44b0dc85d5..0d371996e36 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -19,7 +19,7 @@ requests_mock==1.4
# homeassistant.components.homekit
-HAP-python==1.1.8
+HAP-python==1.1.9
# homeassistant.components.notify.html5
PyJWT==1.6.0
@@ -63,7 +63,7 @@ evohomeclient==0.2.5
feedparser==5.2.1
# homeassistant.components.sensor.foobot
-foobot_async==0.3.0
+foobot_async==0.3.1
# homeassistant.components.tts.google
gTTS-token==1.1.1
@@ -81,7 +81,7 @@ hbmqtt==0.9.1
holidays==0.9.4
# homeassistant.components.frontend
-home-assistant-frontend==20180404.0
+home-assistant-frontend==20180420.0
# homeassistant.components.influxdb
# homeassistant.components.sensor.influxdb
@@ -129,8 +129,11 @@ pushbullet.py==0.11.0
# homeassistant.components.canary
py-canary==0.5.0
+# homeassistant.components.media_player.blackbird
+pyblackbird==0.5
+
# homeassistant.components.deconz
-pydeconz==35
+pydeconz==36
# homeassistant.components.zwave
pydispatcher==2.0.5
@@ -145,6 +148,9 @@ pymonoprice==0.3
# homeassistant.components.binary_sensor.nx584
pynx584==0.4
+# homeassistant.components.qwikswitch
+pyqwikswitch==0.71
+
# homeassistant.components.sensor.darksky
# homeassistant.components.weather.darksky
python-forecastio==1.4.0
@@ -155,6 +161,9 @@ pythonwhois==2.4.3
# homeassistant.components.device_tracker.unifi
pyunifi==2.13
+# homeassistant.components.upnp
+pyupnp-async==0.1.0.1
+
# homeassistant.components.notify.html5
pywebpush==1.6.0
@@ -179,7 +188,7 @@ somecomfort==0.5.2
# homeassistant.components.recorder
# homeassistant.scripts.db_migrator
# homeassistant.components.sensor.sql
-sqlalchemy==1.2.5
+sqlalchemy==1.2.6
# homeassistant.components.statsd
statsd==3.2.1
diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py
index d5bb2701e9b..b5b636dc874 100755
--- a/script/gen_requirements_all.py
+++ b/script/gen_requirements_all.py
@@ -33,6 +33,7 @@ COMMENT_REQUIREMENTS = (
'i2csense',
'credstash',
'bme680',
+ 'homekit',
)
TEST_REQUIREMENTS = (
@@ -67,14 +68,17 @@ TEST_REQUIREMENTS = (
'prometheus_client',
'pushbullet.py',
'py-canary',
+ 'pyblackbird',
'pydeconz',
'pydispatcher',
'PyJWT',
'pylitejet',
'pymonoprice',
'pynx584',
+ 'pyqwikswitch',
'python-forecastio',
'pyunifi',
+ 'pyupnp-async',
'pywebpush',
'restrictedpython',
'rflink',
diff --git a/script/lazytox.py b/script/lazytox.py
index 2639d640753..19af5560dfb 100755
--- a/script/lazytox.py
+++ b/script/lazytox.py
@@ -18,7 +18,7 @@ except ImportError:
RE_ASCII = re.compile(r"\033\[[^m]*m")
-Error = namedtuple('Error', ['file', 'line', 'col', 'msg'])
+Error = namedtuple('Error', ['file', 'line', 'col', 'msg', 'skip'])
PASS = 'green'
FAIL = 'bold_red'
@@ -109,8 +109,9 @@ async def pylint(files):
line = line.split(':')
if len(line) < 3:
continue
- res.append(Error(line[0].replace('\\', '/'),
- line[1], "", line[2].strip()))
+ _fn = line[0].replace('\\', '/')
+ res.append(Error(
+ _fn, line[1], '', line[2].strip(), _fn.startswith('tests/')))
return res
@@ -122,8 +123,8 @@ async def flake8(files):
line = line.split(':')
if len(line) < 4:
continue
- res.append(Error(line[0].replace('\\', '/'),
- line[1], line[2], line[3].strip()))
+ _fn = line[0].replace('\\', '/')
+ res.append(Error(_fn, line[1], line[2], line[3].strip(), False))
return res
@@ -144,7 +145,7 @@ async def lint(files):
err_msg = "{} {}:{} {}".format(err.file, err.line, err.col, err.msg)
# tests/* does not have to pass lint
- if err.file.startswith('tests/'):
+ if err.skip:
print(err_msg)
else:
printc(FAIL, err_msg)
diff --git a/script/translations_upload b/script/translations_upload
index 578cc8c0ccf..5bf9fe1e121 100755
--- a/script/translations_upload
+++ b/script/translations_upload
@@ -35,9 +35,10 @@ script/translations_upload_merge.py
docker run \
-v ${LOCAL_FILE}:/opt/src/${LOCAL_FILE} \
- lokalise/lokalise-cli@sha256:79b3108211ed1fcc9f7b09a011bfc53c240fc2f3b7fa7f0c8390f593271b4cd7 lokalise \
+ lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21 lokalise \
--token ${LOKALISE_TOKEN} \
import ${PROJECT_ID} \
--file /opt/src/${LOCAL_FILE} \
--lang_iso ${LANG_ISO} \
+ --convert_placeholders 0 \
--replace 1
diff --git a/setup.py b/setup.py
index db4b1f8df92..8815b0227ad 100755
--- a/setup.py
+++ b/setup.py
@@ -49,7 +49,7 @@ REQUIRES = [
'jinja2>=2.10',
'voluptuous==0.11.1',
'typing>=3,<4',
- 'aiohttp==3.1.1',
+ 'aiohttp==3.1.3',
'async_timeout==2.0.1',
'astral==1.6',
'certifi>=2017.4.17',
diff --git a/tests/common.py b/tests/common.py
index bc84b3493a8..67fd8bab23f 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -10,7 +10,7 @@ import logging
import threading
from contextlib import contextmanager
-from homeassistant import core as ha, loader, config_entries
+from homeassistant import core as ha, loader, data_entry_flow, config_entries
from homeassistant.setup import setup_component, async_setup_component
from homeassistant.config import async_process_component_config
from homeassistant.helpers import (
@@ -344,7 +344,8 @@ class MockPlatform(object):
# pylint: disable=invalid-name
def __init__(self, setup_platform=None, dependencies=None,
- platform_schema=None, async_setup_platform=None):
+ platform_schema=None, async_setup_platform=None,
+ async_setup_entry=None):
"""Initialize the platform."""
self.DEPENDENCIES = dependencies or []
@@ -358,6 +359,9 @@ class MockPlatform(object):
if async_setup_platform is not None:
self.async_setup_platform = async_setup_platform
+ if async_setup_entry is not None:
+ self.async_setup_entry = async_setup_entry
+
if setup_platform is None and async_setup_platform is None:
self.async_setup_platform = mock_coro_func()
@@ -370,19 +374,27 @@ class MockEntityPlatform(entity_platform.EntityPlatform):
logger=None,
domain='test_domain',
platform_name='test_platform',
+ platform=None,
scan_interval=timedelta(seconds=15),
- parallel_updates=0,
entity_namespace=None,
async_entities_added_callback=lambda: None
):
"""Initialize a mock entity platform."""
+ if logger is None:
+ logger = logging.getLogger('homeassistant.helpers.entity_platform')
+
+ # Otherwise the constructor will blow up.
+ if (isinstance(platform, Mock) and
+ isinstance(platform.PARALLEL_UPDATES, Mock)):
+ platform.PARALLEL_UPDATES = 0
+
super().__init__(
hass=hass,
logger=logger,
domain=domain,
platform_name=platform_name,
+ platform=platform,
scan_interval=scan_interval,
- parallel_updates=parallel_updates,
entity_namespace=entity_namespace,
async_entities_added_callback=async_entities_added_callback,
)
@@ -443,7 +455,7 @@ class MockConfigEntry(config_entries.ConfigEntry):
"""Helper for creating config entries that adds some defaults."""
def __init__(self, *, domain='test', data=None, version=0, entry_id=None,
- source=config_entries.SOURCE_USER, title='Mock Title',
+ source=data_entry_flow.SOURCE_USER, title='Mock Title',
state=None):
"""Initialize a mock config entry."""
kwargs = {
diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py
index dd404b7d57a..afa4d19b5d9 100644
--- a/tests/components/alexa/test_smart_home.py
+++ b/tests/components/alexa/test_smart_home.py
@@ -807,15 +807,23 @@ async def test_thermostat(hass):
'Alexa.ThermostatController', 'SetThermostatMode',
'climate#test_thermostat', 'climate.set_operation_mode',
hass,
- payload={'thermostatMode': 'HEAT'}
+ payload={'thermostatMode': {'value': 'HEAT'}}
)
assert call.data['operation_mode'] == 'heat'
+ call, _ = await assert_request_calls_service(
+ 'Alexa.ThermostatController', 'SetThermostatMode',
+ 'climate#test_thermostat', 'climate.set_operation_mode',
+ hass,
+ payload={'thermostatMode': 'HEAT'}
+ )
+
+ assert call.data['operation_mode'] == 'heat'
msg = await assert_request_fails(
'Alexa.ThermostatController', 'SetThermostatMode',
'climate#test_thermostat', 'climate.set_operation_mode',
hass,
- payload={'thermostatMode': 'INVALID'}
+ payload={'thermostatMode': {'value': 'INVALID'}}
)
assert msg['event']['payload']['type'] == 'UNSUPPORTED_THERMOSTAT_MODE'
diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py
index cfe6b12baac..f53be8818a3 100644
--- a/tests/components/config/test_config_entries.py
+++ b/tests/components/config/test_config_entries.py
@@ -8,7 +8,8 @@ import pytest
import voluptuous as vol
from homeassistant import config_entries as core_ce
-from homeassistant.config_entries import ConfigFlowHandler, HANDLERS
+from homeassistant.config_entries import HANDLERS
+from homeassistant.data_entry_flow import FlowHandler
from homeassistant.setup import async_setup_component
from homeassistant.components.config import config_entries
from homeassistant.loader import set_component
@@ -16,6 +17,12 @@ from homeassistant.loader import set_component
from tests.common import MockConfigEntry, MockModule, mock_coro_func
+@pytest.fixture(scope='session', autouse=True)
+def mock_test_component():
+ """Ensure a component called 'test' exists."""
+ set_component('test', MockModule('test'))
+
+
@pytest.fixture
def client(hass, aiohttp_client):
"""Fixture that can interact with the config manager API."""
@@ -93,7 +100,7 @@ def test_available_flows(hass, client):
@asyncio.coroutine
def test_initialize_flow(hass, client):
"""Test we can initialize a flow."""
- class TestFlow(ConfigFlowHandler):
+ class TestFlow(FlowHandler):
@asyncio.coroutine
def async_step_init(self, user_input=None):
schema = OrderedDict()
@@ -110,7 +117,7 @@ def test_initialize_flow(hass, client):
with patch.dict(HANDLERS, {'test': TestFlow}):
resp = yield from client.post('/api/config/config_entries/flow',
- json={'domain': 'test'})
+ json={'handler': 'test'})
assert resp.status == 200
data = yield from resp.json()
@@ -119,7 +126,7 @@ def test_initialize_flow(hass, client):
assert data == {
'type': 'form',
- 'domain': 'test',
+ 'handler': 'test',
'step_id': 'init',
'data_schema': [
{
@@ -142,20 +149,20 @@ def test_initialize_flow(hass, client):
@asyncio.coroutine
def test_abort(hass, client):
"""Test a flow that aborts."""
- class TestFlow(ConfigFlowHandler):
+ class TestFlow(FlowHandler):
@asyncio.coroutine
def async_step_init(self, user_input=None):
return self.async_abort(reason='bla')
with patch.dict(HANDLERS, {'test': TestFlow}):
resp = yield from client.post('/api/config/config_entries/flow',
- json={'domain': 'test'})
+ json={'handler': 'test'})
assert resp.status == 200
data = yield from resp.json()
data.pop('flow_id')
assert data == {
- 'domain': 'test',
+ 'handler': 'test',
'reason': 'bla',
'type': 'abort'
}
@@ -167,7 +174,7 @@ def test_create_account(hass, client):
set_component(
'test', MockModule('test', async_setup_entry=mock_coro_func(True)))
- class TestFlow(ConfigFlowHandler):
+ class TestFlow(FlowHandler):
VERSION = 1
@asyncio.coroutine
@@ -179,15 +186,17 @@ def test_create_account(hass, client):
with patch.dict(HANDLERS, {'test': TestFlow}):
resp = yield from client.post('/api/config/config_entries/flow',
- json={'domain': 'test'})
+ json={'handler': 'test'})
assert resp.status == 200
data = yield from resp.json()
data.pop('flow_id')
assert data == {
- 'domain': 'test',
+ 'handler': 'test',
'title': 'Test Entry',
- 'type': 'create_entry'
+ 'type': 'create_entry',
+ 'source': 'user',
+ 'version': 1,
}
@@ -197,7 +206,7 @@ def test_two_step_flow(hass, client):
set_component(
'test', MockModule('test', async_setup_entry=mock_coro_func(True)))
- class TestFlow(ConfigFlowHandler):
+ class TestFlow(FlowHandler):
VERSION = 1
@asyncio.coroutine
@@ -217,13 +226,13 @@ def test_two_step_flow(hass, client):
with patch.dict(HANDLERS, {'test': TestFlow}):
resp = yield from client.post('/api/config/config_entries/flow',
- json={'domain': 'test'})
+ json={'handler': 'test'})
assert resp.status == 200
data = yield from resp.json()
flow_id = data.pop('flow_id')
assert data == {
'type': 'form',
- 'domain': 'test',
+ 'handler': 'test',
'step_id': 'account',
'data_schema': [
{
@@ -242,16 +251,18 @@ def test_two_step_flow(hass, client):
data = yield from resp.json()
data.pop('flow_id')
assert data == {
- 'domain': 'test',
+ 'handler': 'test',
'type': 'create_entry',
'title': 'user-title',
+ 'version': 1,
+ 'source': 'user',
}
@asyncio.coroutine
def test_get_progress_index(hass, client):
"""Test querying for the flows that are in progress."""
- class TestFlow(ConfigFlowHandler):
+ class TestFlow(FlowHandler):
VERSION = 5
@asyncio.coroutine
@@ -274,7 +285,7 @@ def test_get_progress_index(hass, client):
assert data == [
{
'flow_id': form['flow_id'],
- 'domain': 'test',
+ 'handler': 'test',
'source': 'hassio'
}
]
@@ -283,7 +294,7 @@ def test_get_progress_index(hass, client):
@asyncio.coroutine
def test_get_progress_flow(hass, client):
"""Test we can query the API for same result as we get from init a flow."""
- class TestFlow(ConfigFlowHandler):
+ class TestFlow(FlowHandler):
@asyncio.coroutine
def async_step_init(self, user_input=None):
schema = OrderedDict()
@@ -300,7 +311,7 @@ def test_get_progress_flow(hass, client):
with patch.dict(HANDLERS, {'test': TestFlow}):
resp = yield from client.post('/api/config/config_entries/flow',
- json={'domain': 'test'})
+ json={'handler': 'test'})
assert resp.status == 200
data = yield from resp.json()
diff --git a/tests/components/deconz/__init__.py b/tests/components/deconz/__init__.py
new file mode 100644
index 00000000000..59b903e8900
--- /dev/null
+++ b/tests/components/deconz/__init__.py
@@ -0,0 +1 @@
+"""Tests for the deCONZ component."""
diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py
new file mode 100644
index 00000000000..d86475b35ef
--- /dev/null
+++ b/tests/components/deconz/test_config_flow.py
@@ -0,0 +1,225 @@
+"""Tests for deCONZ config flow."""
+from unittest.mock import patch
+import pytest
+
+import voluptuous as vol
+from homeassistant.components.deconz import config_flow
+from tests.common import MockConfigEntry
+
+import pydeconz
+
+
+async def test_flow_works(hass, aioclient_mock):
+ """Test that config flow works."""
+ aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[
+ {'id': 'id', 'internalipaddress': '1.2.3.4', 'internalport': 80}
+ ])
+ aioclient_mock.post('http://1.2.3.4:80/api', json=[
+ {"success": {"username": "1234567890ABCDEF"}}
+ ])
+
+ flow = config_flow.DeconzFlowHandler()
+ flow.hass = hass
+ await flow.async_step_init()
+ result = await flow.async_step_link(user_input={})
+
+ assert result['type'] == 'create_entry'
+ assert result['title'] == 'deCONZ-id'
+ assert result['data'] == {
+ 'bridgeid': 'id',
+ 'host': '1.2.3.4',
+ 'port': 80,
+ 'api_key': '1234567890ABCDEF'
+ }
+
+
+async def test_flow_already_registered_bridge(hass):
+ """Test config flow don't allow more than one bridge to be registered."""
+ MockConfigEntry(domain='deconz', data={
+ 'host': '1.2.3.4'
+ }).add_to_hass(hass)
+ flow = config_flow.DeconzFlowHandler()
+ flow.hass = hass
+
+ result = await flow.async_step_init()
+ assert result['type'] == 'abort'
+
+
+async def test_flow_no_discovered_bridges(hass, aioclient_mock):
+ """Test config flow discovers no bridges."""
+ aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[])
+ flow = config_flow.DeconzFlowHandler()
+ flow.hass = hass
+
+ result = await flow.async_step_init()
+ assert result['type'] == 'abort'
+
+
+async def test_flow_one_bridge_discovered(hass, aioclient_mock):
+ """Test config flow discovers one bridge."""
+ aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[
+ {'id': 'id', 'internalipaddress': '1.2.3.4', 'internalport': 80}
+ ])
+ flow = config_flow.DeconzFlowHandler()
+ flow.hass = hass
+
+ result = await flow.async_step_init()
+ assert result['type'] == 'form'
+ assert result['step_id'] == 'link'
+
+
+async def test_flow_two_bridges_discovered(hass, aioclient_mock):
+ """Test config flow discovers two bridges."""
+ aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[
+ {'id': 'id1', 'internalipaddress': '1.2.3.4', 'internalport': 80},
+ {'id': 'id2', 'internalipaddress': '5.6.7.8', 'internalport': 80}
+ ])
+ flow = config_flow.DeconzFlowHandler()
+ flow.hass = hass
+
+ result = await flow.async_step_init()
+ assert result['type'] == 'form'
+ assert result['step_id'] == 'init'
+
+ with pytest.raises(vol.Invalid):
+ assert result['data_schema']({'host': '0.0.0.0'})
+
+ result['data_schema']({'host': '1.2.3.4'})
+ result['data_schema']({'host': '5.6.7.8'})
+
+
+async def test_link_no_api_key(hass, aioclient_mock):
+ """Test config flow should abort if no API key was possible to retrieve."""
+ aioclient_mock.post('http://1.2.3.4:80/api', json=[])
+ flow = config_flow.DeconzFlowHandler()
+ flow.hass = hass
+ flow.deconz_config = {'host': '1.2.3.4', 'port': 80}
+
+ result = await flow.async_step_link(user_input={})
+ assert result['type'] == 'form'
+ assert result['step_id'] == 'link'
+ assert result['errors'] == {'base': 'no_key'}
+
+
+async def test_link_already_registered_bridge(hass):
+ """Test that link verifies to only allow one config entry to complete.
+
+ This is possible with discovery which will allow the user to complete
+ a second config entry and then complete the discovered config entry.
+ """
+ MockConfigEntry(domain='deconz', data={
+ 'host': '1.2.3.4'
+ }).add_to_hass(hass)
+ flow = config_flow.DeconzFlowHandler()
+ flow.hass = hass
+ flow.deconz_config = {'host': '1.2.3.4', 'port': 80}
+
+ result = await flow.async_step_link(user_input={})
+ assert result['type'] == 'abort'
+
+
+async def test_bridge_discovery(hass):
+ """Test a bridge being discovered with no additional config file."""
+ flow = config_flow.DeconzFlowHandler()
+ flow.hass = hass
+ with patch.object(config_flow, 'load_json', return_value={}):
+ result = await flow.async_step_discovery({
+ 'host': '1.2.3.4',
+ 'port': 80,
+ 'serial': 'id'
+ })
+
+ assert result['type'] == 'form'
+ assert result['step_id'] == 'link'
+
+
+async def test_bridge_discovery_config_file(hass):
+ """Test a bridge being discovered with a corresponding config file."""
+ flow = config_flow.DeconzFlowHandler()
+ flow.hass = hass
+ with patch.object(config_flow, 'load_json',
+ return_value={'host': '1.2.3.4',
+ 'port': 8080,
+ 'api_key': '1234567890ABCDEF'}):
+ result = await flow.async_step_discovery({
+ 'host': '1.2.3.4',
+ 'port': 80,
+ 'serial': 'id'
+ })
+
+ assert result['type'] == 'create_entry'
+ assert result['title'] == 'deCONZ-id'
+ assert result['data'] == {
+ 'bridgeid': 'id',
+ 'host': '1.2.3.4',
+ 'port': 80,
+ 'api_key': '1234567890ABCDEF'
+ }
+
+
+async def test_bridge_discovery_other_config_file(hass):
+ """Test a bridge being discovered with another bridges config file."""
+ flow = config_flow.DeconzFlowHandler()
+ flow.hass = hass
+ with patch.object(config_flow, 'load_json',
+ return_value={'host': '5.6.7.8', 'api_key': '5678'}):
+ result = await flow.async_step_discovery({
+ 'host': '1.2.3.4',
+ 'port': 80,
+ 'serial': 'id'
+ })
+
+ assert result['type'] == 'form'
+ assert result['step_id'] == 'link'
+
+
+async def test_bridge_discovery_already_configured(hass):
+ """Test if a discovered bridge has already been configured."""
+ MockConfigEntry(domain='deconz', data={
+ 'host': '1.2.3.4'
+ }).add_to_hass(hass)
+
+ flow = config_flow.DeconzFlowHandler()
+ flow.hass = hass
+
+ result = await flow.async_step_discovery({
+ 'host': '1.2.3.4',
+ 'serial': 'id'
+ })
+
+ assert result['type'] == 'abort'
+
+
+async def test_import_without_api_key(hass):
+ """Test importing a host without an API key."""
+ flow = config_flow.DeconzFlowHandler()
+ flow.hass = hass
+
+ result = await flow.async_step_import({
+ 'host': '1.2.3.4',
+ })
+
+ assert result['type'] == 'form'
+ assert result['step_id'] == 'link'
+
+
+async def test_import_with_api_key(hass):
+ """Test importing a host with an API key."""
+ flow = config_flow.DeconzFlowHandler()
+ flow.hass = hass
+
+ result = await flow.async_step_import({
+ 'bridgeid': 'id',
+ 'host': '1.2.3.4',
+ 'port': 80,
+ 'api_key': '1234567890ABCDEF'
+ })
+
+ assert result['type'] == 'create_entry'
+ assert result['title'] == 'deCONZ-id'
+ assert result['data'] == {
+ 'bridgeid': 'id',
+ 'host': '1.2.3.4',
+ 'port': 80,
+ 'api_key': '1234567890ABCDEF'
+ }
diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py
new file mode 100644
index 00000000000..cbc8a373972
--- /dev/null
+++ b/tests/components/deconz/test_init.py
@@ -0,0 +1,69 @@
+"""Test deCONZ component setup process."""
+from unittest.mock import patch
+
+from homeassistant.setup import async_setup_component
+from homeassistant.components import deconz
+
+
+async def test_config_with_host_passed_to_config_entry(hass):
+ """Test that configured options for a host are loaded via config entry."""
+ with patch.object(hass, 'config_entries') as mock_config_entries, \
+ patch.object(deconz, 'configured_hosts', return_value=[]), \
+ patch.object(deconz, 'load_json', return_value={}):
+ assert await async_setup_component(hass, deconz.DOMAIN, {
+ deconz.DOMAIN: {
+ deconz.CONF_HOST: '1.2.3.4',
+ deconz.CONF_PORT: 80
+ }
+ }) is True
+ # Import flow started
+ assert len(mock_config_entries.flow.mock_calls) == 2
+
+
+async def test_config_file_passed_to_config_entry(hass):
+ """Test that configuration file for a host are loaded via config entry."""
+ with patch.object(hass, 'config_entries') as mock_config_entries, \
+ patch.object(deconz, 'configured_hosts', return_value=[]), \
+ patch.object(deconz, 'load_json',
+ return_value={'host': '1.2.3.4'}):
+ assert await async_setup_component(hass, deconz.DOMAIN, {
+ deconz.DOMAIN: {}
+ }) is True
+ # Import flow started
+ assert len(mock_config_entries.flow.mock_calls) == 2
+
+
+async def test_config_without_host_not_passed_to_config_entry(hass):
+ """Test that a configuration without a host does not initiate an import."""
+ with patch.object(hass, 'config_entries') as mock_config_entries, \
+ patch.object(deconz, 'configured_hosts', return_value=[]), \
+ patch.object(deconz, 'load_json', return_value={}):
+ assert await async_setup_component(hass, deconz.DOMAIN, {
+ deconz.DOMAIN: {}
+ }) is True
+ # No flow started
+ assert len(mock_config_entries.flow.mock_calls) == 0
+
+
+async def test_config_already_registered_not_passed_to_config_entry(hass):
+ """Test that an already registered host does not initiate an import."""
+ with patch.object(hass, 'config_entries') as mock_config_entries, \
+ patch.object(deconz, 'configured_hosts',
+ return_value=['1.2.3.4']), \
+ patch.object(deconz, 'load_json', return_value={}):
+ assert await async_setup_component(hass, deconz.DOMAIN, {
+ deconz.DOMAIN: {
+ deconz.CONF_HOST: '1.2.3.4',
+ deconz.CONF_PORT: 80
+ }
+ }) is True
+ # No flow started
+ assert len(mock_config_entries.flow.mock_calls) == 0
+
+
+async def test_config_discovery(hass):
+ """Test that a discovered bridge does not initiate an import."""
+ with patch.object(hass, 'config_entries') as mock_config_entries:
+ assert await async_setup_component(hass, deconz.DOMAIN, {}) is True
+ # No flow started
+ assert len(mock_config_entries.flow.mock_calls) == 0
diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py
index a2facd826e4..f8e026483aa 100644
--- a/tests/components/homekit/test_accessories.py
+++ b/tests/components/homekit/test_accessories.py
@@ -2,21 +2,66 @@
This includes tests for all mock object types.
"""
+from datetime import datetime, timedelta
import unittest
from unittest.mock import call, patch, Mock
from homeassistant.components.homekit.accessories import (
add_preload_service, set_accessory_info,
- HomeAccessory, HomeBridge, HomeDriver)
+ debounce, HomeAccessory, HomeBridge, HomeDriver)
from homeassistant.components.homekit.const import (
- ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME,
- SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, CHAR_MODEL,
- CHAR_NAME, CHAR_SERIAL_NUMBER)
+ BRIDGE_MODEL, BRIDGE_NAME, SERV_ACCESSORY_INFO,
+ CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER)
+from homeassistant.const import ATTR_NOW, EVENT_TIME_CHANGED
+import homeassistant.util.dt as dt_util
+
+from tests.common import get_test_home_assistant
+
+
+def patch_debounce():
+ """Return patch for debounce method."""
+ return patch('homeassistant.components.homekit.accessories.debounce',
+ lambda f: lambda *args, **kwargs: f(*args, **kwargs))
class TestAccessories(unittest.TestCase):
"""Test pyhap adapter methods."""
+ def test_debounce(self):
+ """Test add_timeout decorator function."""
+ def demo_func(*args):
+ nonlocal arguments, counter
+ counter += 1
+ arguments = args
+
+ arguments = None
+ counter = 0
+ hass = get_test_home_assistant()
+ mock = Mock(hass=hass)
+
+ debounce_demo = debounce(demo_func)
+ self.assertEqual(debounce_demo.__name__, 'demo_func')
+ now = datetime(2018, 1, 1, 20, 0, 0, tzinfo=dt_util.UTC)
+
+ with patch('homeassistant.util.dt.utcnow', return_value=now):
+ debounce_demo(mock, 'value')
+ hass.bus.fire(
+ EVENT_TIME_CHANGED, {ATTR_NOW: now + timedelta(seconds=3)})
+ hass.block_till_done()
+ assert counter == 1
+ assert len(arguments) == 2
+
+ with patch('homeassistant.util.dt.utcnow', return_value=now):
+ debounce_demo(mock, 'value')
+ debounce_demo(mock, 'value')
+
+ hass.bus.fire(
+ EVENT_TIME_CHANGED, {ATTR_NOW: now + timedelta(seconds=3)})
+ hass.block_till_done()
+ assert counter == 2
+
+ hass.stop()
+
def test_add_preload_service(self):
"""Test add_preload_service without additional characteristics."""
acc = Mock()
@@ -46,7 +91,7 @@ class TestAccessories(unittest.TestCase):
def test_set_accessory_info(self):
"""Test setting the basic accessory information."""
# Test HomeAccessory
- acc = HomeAccessory()
+ acc = HomeAccessory('HA', 'Home Accessory', 'homekit.accessory', 2, '')
set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000')
serv = acc.get_service(SERV_ACCESSORY_INFO)
@@ -58,7 +103,7 @@ class TestAccessories(unittest.TestCase):
serv.get_characteristic(CHAR_SERIAL_NUMBER).value, '0000')
# Test HomeBridge
- acc = HomeBridge(None)
+ acc = HomeBridge('hass')
set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000')
serv = acc.get_service(SERV_ACCESSORY_INFO)
@@ -70,26 +115,37 @@ class TestAccessories(unittest.TestCase):
def test_home_accessory(self):
"""Test HomeAccessory class."""
- acc = HomeAccessory()
- self.assertEqual(acc.display_name, ACCESSORY_NAME)
+ hass = get_test_home_assistant()
+
+ acc = HomeAccessory(hass, 'Home Accessory', 'homekit.accessory', 2, '')
+ self.assertEqual(acc.hass, hass)
+ self.assertEqual(acc.display_name, 'Home Accessory')
self.assertEqual(acc.category, 1) # Category.OTHER
self.assertEqual(len(acc.services), 1)
serv = acc.services[0] # SERV_ACCESSORY_INFO
self.assertEqual(
- serv.get_characteristic(CHAR_MODEL).value, ACCESSORY_MODEL)
+ serv.get_characteristic(CHAR_MODEL).value, 'homekit.accessory')
- acc = HomeAccessory('test_name', 'test_model', 'FAN', aid=2)
+ hass.states.set('homekit.accessory', 'on')
+ hass.block_till_done()
+ acc.run()
+ hass.states.set('homekit.accessory', 'off')
+ hass.block_till_done()
+
+ acc = HomeAccessory('hass', 'test_name', 'test_model', 2, '')
self.assertEqual(acc.display_name, 'test_name')
- self.assertEqual(acc.category, 3) # Category.FAN
self.assertEqual(acc.aid, 2)
self.assertEqual(len(acc.services), 1)
serv = acc.services[0] # SERV_ACCESSORY_INFO
self.assertEqual(
serv.get_characteristic(CHAR_MODEL).value, 'test_model')
+ hass.stop()
+
def test_home_bridge(self):
"""Test HomeBridge class."""
- bridge = HomeBridge(None)
+ bridge = HomeBridge('hass')
+ self.assertEqual(bridge.hass, 'hass')
self.assertEqual(bridge.display_name, BRIDGE_NAME)
self.assertEqual(bridge.category, 2) # Category.BRIDGE
self.assertEqual(len(bridge.services), 1)
@@ -98,12 +154,10 @@ class TestAccessories(unittest.TestCase):
self.assertEqual(
serv.get_characteristic(CHAR_MODEL).value, BRIDGE_MODEL)
- bridge = HomeBridge('hass', 'test_name', 'test_model')
+ bridge = HomeBridge('hass', 'test_name')
self.assertEqual(bridge.display_name, 'test_name')
self.assertEqual(len(bridge.services), 1)
serv = bridge.services[0] # SERV_ACCESSORY_INFO
- self.assertEqual(
- serv.get_characteristic(CHAR_MODEL).value, 'test_model')
# setup_message
bridge.setup_message()
@@ -128,11 +182,11 @@ class TestAccessories(unittest.TestCase):
self.assertEqual(
mock_remove_paired_client.call_args, call('client_uuid'))
- self.assertEqual(mock_show_msg.call_args, call(bridge, 'hass'))
+ self.assertEqual(mock_show_msg.call_args, call('hass', bridge))
def test_home_driver(self):
"""Test HomeDriver class."""
- bridge = HomeBridge(None)
+ bridge = HomeBridge('hass')
ip_address = '127.0.0.1'
port = 51826
path = '.homekit.state'
diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py
index e29ed85b5fc..c26982e170b 100644
--- a/tests/components/homekit/test_get_accessories.py
+++ b/tests/components/homekit/test_get_accessories.py
@@ -4,12 +4,14 @@ import unittest
from unittest.mock import patch, Mock
from homeassistant.core import State
+from homeassistant.components.cover import (
+ SUPPORT_OPEN, SUPPORT_CLOSE)
from homeassistant.components.climate import (
SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW)
from homeassistant.components.homekit import get_accessory, TYPES
from homeassistant.const import (
ATTR_CODE, ATTR_UNIT_OF_MEASUREMENT, ATTR_SUPPORTED_FEATURES,
- TEMP_CELSIUS, TEMP_FAHRENHEIT)
+ TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_DEVICE_CLASS)
_LOGGER = logging.getLogger(__name__)
@@ -19,14 +21,14 @@ CONFIG = {}
def test_get_accessory_invalid_aid(caplog):
"""Test with unsupported component."""
assert get_accessory(None, State('light.demo', 'on'),
- aid=None, config=None) is None
+ None, config=None) is None
assert caplog.records[0].levelname == 'WARNING'
assert 'invalid aid' in caplog.records[0].msg
def test_not_supported():
"""Test if none is returned if entity isn't supported."""
- assert get_accessory(None, State('demo.demo', 'on'), aid=2, config=None) \
+ assert get_accessory(None, State('demo.demo', 'on'), 2, config=None) \
is None
@@ -41,6 +43,13 @@ class TestGetAccessories(unittest.TestCase):
"""Test if mock type was called."""
self.assertTrue(self.mock_type.called)
+ def test_sensor_temperature(self):
+ """Test temperature sensor with device class temperature."""
+ with patch.dict(TYPES, {'TemperatureSensor': self.mock_type}):
+ state = State('sensor.temperature', '23',
+ {ATTR_DEVICE_CLASS: 'temperature'})
+ get_accessory(None, state, 2, {})
+
def test_sensor_temperature_celsius(self):
"""Test temperature sensor with Celsius as unit."""
with patch.dict(TYPES, {'TemperatureSensor': self.mock_type}):
@@ -48,7 +57,6 @@ class TestGetAccessories(unittest.TestCase):
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
get_accessory(None, state, 2, {})
- # pylint: disable=invalid-name
def test_sensor_temperature_fahrenheit(self):
"""Test temperature sensor with Fahrenheit as unit."""
with patch.dict(TYPES, {'TemperatureSensor': self.mock_type}):
@@ -57,12 +65,88 @@ class TestGetAccessories(unittest.TestCase):
get_accessory(None, state, 2, {})
def test_sensor_humidity(self):
+ """Test humidity sensor with device class humidity."""
+ with patch.dict(TYPES, {'HumiditySensor': self.mock_type}):
+ state = State('sensor.humidity', '20',
+ {ATTR_DEVICE_CLASS: 'humidity'})
+ get_accessory(None, state, 2, {})
+
+ def test_sensor_humidity_unit(self):
"""Test humidity sensor with % as unit."""
with patch.dict(TYPES, {'HumiditySensor': self.mock_type}):
state = State('sensor.humidity', '20',
{ATTR_UNIT_OF_MEASUREMENT: '%'})
get_accessory(None, state, 2, {})
+ def test_air_quality_sensor(self):
+ """Test air quality sensor with pm25 class."""
+ with patch.dict(TYPES, {'AirQualitySensor': self.mock_type}):
+ state = State('sensor.air_quality', '40',
+ {ATTR_DEVICE_CLASS: 'pm25'})
+ get_accessory(None, state, 2, {})
+
+ def test_air_quality_sensor_entity_id(self):
+ """Test air quality sensor with entity_id contains pm25."""
+ with patch.dict(TYPES, {'AirQualitySensor': self.mock_type}):
+ state = State('sensor.air_quality_pm25', '40', {})
+ get_accessory(None, state, 2, {})
+
+ def test_co2_sensor(self):
+ """Test co2 sensor with device class co2."""
+ with patch.dict(TYPES, {'CarbonDioxideSensor': self.mock_type}):
+ state = State('sensor.airmeter', '500',
+ {ATTR_DEVICE_CLASS: 'co2'})
+ get_accessory(None, state, 2, {})
+
+ def test_co2_sensor_entity_id(self):
+ """Test co2 sensor with entity_id contains co2."""
+ with patch.dict(TYPES, {'CarbonDioxideSensor': self.mock_type}):
+ state = State('sensor.airmeter_co2', '500', {})
+ get_accessory(None, state, 2, {})
+
+ def test_light_sensor(self):
+ """Test light sensor with device class lux."""
+ with patch.dict(TYPES, {'LightSensor': self.mock_type}):
+ state = State('sensor.light', '900',
+ {ATTR_DEVICE_CLASS: 'light'})
+ get_accessory(None, state, 2, {})
+
+ def test_light_sensor_unit_lm(self):
+ """Test light sensor with lm as unit."""
+ with patch.dict(TYPES, {'LightSensor': self.mock_type}):
+ state = State('sensor.light', '900',
+ {ATTR_UNIT_OF_MEASUREMENT: 'lm'})
+ get_accessory(None, state, 2, {})
+
+ def test_light_sensor_unit_lux(self):
+ """Test light sensor with lux as unit."""
+ with patch.dict(TYPES, {'LightSensor': self.mock_type}):
+ state = State('sensor.light', '900',
+ {ATTR_UNIT_OF_MEASUREMENT: 'lux'})
+ get_accessory(None, state, 2, {})
+
+ def test_binary_sensor(self):
+ """Test binary sensor with opening class."""
+ with patch.dict(TYPES, {'BinarySensor': self.mock_type}):
+ state = State('binary_sensor.opening', 'on',
+ {ATTR_DEVICE_CLASS: 'opening'})
+ get_accessory(None, state, 2, {})
+
+ def test_device_tracker(self):
+ """Test binary sensor with opening class."""
+ with patch.dict(TYPES, {'BinarySensor': self.mock_type}):
+ state = State('device_tracker.someone', 'not_home', {})
+ get_accessory(None, state, 2, {})
+
+ def test_garage_door(self):
+ """Test cover with device_class: 'garage' and required features."""
+ with patch.dict(TYPES, {'GarageDoorOpener': self.mock_type}):
+ state = State('cover.garage_door', 'open', {
+ ATTR_DEVICE_CLASS: 'garage',
+ ATTR_SUPPORTED_FEATURES:
+ SUPPORT_OPEN | SUPPORT_CLOSE})
+ get_accessory(None, state, 2, {})
+
def test_cover_set_position(self):
"""Test cover with support for set_cover_position."""
with patch.dict(TYPES, {'WindowCovering': self.mock_type}):
@@ -70,6 +154,13 @@ class TestGetAccessories(unittest.TestCase):
{ATTR_SUPPORTED_FEATURES: 4})
get_accessory(None, state, 2, {})
+ def test_cover_open_close(self):
+ """Test cover with support for open and close."""
+ with patch.dict(TYPES, {'WindowCoveringBasic': self.mock_type}):
+ state = State('cover.open_window', 'open',
+ {ATTR_SUPPORTED_FEATURES: 3})
+ get_accessory(None, state, 2, {})
+
def test_alarm_control_panel(self):
"""Test alarm control panel."""
config = {ATTR_CODE: '1234'}
@@ -78,8 +169,9 @@ class TestGetAccessories(unittest.TestCase):
get_accessory(None, state, 2, config)
# pylint: disable=unsubscriptable-object
+ print(self.mock_type.call_args[1])
self.assertEqual(
- self.mock_type.call_args[1].get('alarm_code'), '1234')
+ self.mock_type.call_args[1]['config'][ATTR_CODE], '1234')
def test_climate(self):
"""Test climate devices."""
@@ -87,10 +179,6 @@ class TestGetAccessories(unittest.TestCase):
state = State('climate.test', 'auto')
get_accessory(None, state, 2, {})
- # pylint: disable=unsubscriptable-object
- self.assertEqual(
- self.mock_type.call_args[0][-1], False) # support_auto
-
def test_light(self):
"""Test light devices."""
with patch.dict(TYPES, {'Light': self.mock_type}):
@@ -106,10 +194,6 @@ class TestGetAccessories(unittest.TestCase):
SUPPORT_TARGET_TEMPERATURE_HIGH})
get_accessory(None, state, 2, {})
- # pylint: disable=unsubscriptable-object
- self.assertEqual(
- self.mock_type.call_args[0][-1], True) # support_auto
-
def test_switch(self):
"""Test switch."""
with patch.dict(TYPES, {'Switch': self.mock_type}):
@@ -127,3 +211,9 @@ class TestGetAccessories(unittest.TestCase):
with patch.dict(TYPES, {'Switch': self.mock_type}):
state = State('input_boolean.test', 'on')
get_accessory(None, state, 2, {})
+
+ def test_lock(self):
+ """Test lock."""
+ with patch.dict(TYPES, {'Lock': self.mock_type}):
+ state = State('lock.test', 'locked')
+ get_accessory(None, state, 2, {})
diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py
index c6d79545487..d1ad232d279 100644
--- a/tests/components/homekit/test_homekit.py
+++ b/tests/components/homekit/test_homekit.py
@@ -14,6 +14,7 @@ from homeassistant.const import (
CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
from tests.common import get_test_home_assistant
+from tests.components.homekit.test_accessories import patch_debounce
IP_ADDRESS = '127.0.0.1'
PATH_HOMEKIT = 'homeassistant.components.homekit'
@@ -22,6 +23,17 @@ PATH_HOMEKIT = 'homeassistant.components.homekit'
class TestHomeKit(unittest.TestCase):
"""Test setup of HomeKit component and HomeKit class."""
+ @classmethod
+ def setUpClass(cls):
+ """Setup debounce patcher."""
+ cls.patcher = patch_debounce()
+ cls.patcher.start()
+
+ @classmethod
+ def tearDownClass(cls):
+ """Stop debounce patcher."""
+ cls.patcher.stop()
+
def setUp(self):
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
@@ -161,7 +173,7 @@ class TestHomeKit(unittest.TestCase):
self.assertEqual(mock_add_bridge_acc.mock_calls, [call(state)])
self.assertEqual(mock_show_setup_msg.mock_calls, [
- call(homekit.bridge, self.hass)])
+ call(self.hass, homekit.bridge)])
self.assertEqual(homekit.driver.mock_calls, [call.start()])
self.assertTrue(homekit.started)
diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py
index 45631a76c98..2dcb48a4d4c 100644
--- a/tests/components/homekit/test_type_covers.py
+++ b/tests/components/homekit/test_type_covers.py
@@ -3,11 +3,13 @@ import unittest
from homeassistant.core import callback
from homeassistant.components.cover import (
- ATTR_POSITION, ATTR_CURRENT_POSITION)
-from homeassistant.components.homekit.type_covers import WindowCovering
+ ATTR_POSITION, ATTR_CURRENT_POSITION, SUPPORT_STOP)
+from homeassistant.components.homekit.type_covers import (
+ GarageDoorOpener, WindowCovering, WindowCoveringBasic)
from homeassistant.const import (
- STATE_UNKNOWN, STATE_OPEN,
- ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE)
+ STATE_CLOSED, STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_OPEN,
+ ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE,
+ ATTR_SUPPORTED_FEATURES)
from tests.common import get_test_home_assistant
@@ -31,11 +33,69 @@ class TestHomekitSensors(unittest.TestCase):
"""Stop down everything that was started."""
self.hass.stop()
+ def test_garage_door_open_close(self):
+ """Test if accessory and HA are updated accordingly."""
+ garage_door = 'cover.garage_door'
+
+ acc = GarageDoorOpener(self.hass, 'Cover', garage_door, 2, config=None)
+ acc.run()
+
+ self.assertEqual(acc.aid, 2)
+ self.assertEqual(acc.category, 4) # GarageDoorOpener
+
+ self.assertEqual(acc.char_current_state.value, 0)
+ self.assertEqual(acc.char_target_state.value, 0)
+
+ self.hass.states.set(garage_door, STATE_CLOSED)
+ self.hass.block_till_done()
+
+ self.assertEqual(acc.char_current_state.value, 1)
+ self.assertEqual(acc.char_target_state.value, 1)
+
+ self.hass.states.set(garage_door, STATE_OPEN)
+ self.hass.block_till_done()
+
+ self.assertEqual(acc.char_current_state.value, 0)
+ self.assertEqual(acc.char_target_state.value, 0)
+
+ self.hass.states.set(garage_door, STATE_UNAVAILABLE)
+ self.hass.block_till_done()
+
+ self.assertEqual(acc.char_current_state.value, 0)
+ self.assertEqual(acc.char_target_state.value, 0)
+
+ self.hass.states.set(garage_door, STATE_UNKNOWN)
+ self.hass.block_till_done()
+
+ self.assertEqual(acc.char_current_state.value, 0)
+ self.assertEqual(acc.char_target_state.value, 0)
+
+ # Set closed from HomeKit
+ acc.char_target_state.client_update_value(1)
+ self.hass.block_till_done()
+
+ self.assertEqual(acc.char_current_state.value, 2)
+ self.assertEqual(acc.char_target_state.value, 1)
+ self.assertEqual(
+ self.events[0].data[ATTR_SERVICE], 'close_cover')
+
+ self.hass.states.set(garage_door, STATE_CLOSED)
+ self.hass.block_till_done()
+
+ # Set open from HomeKit
+ acc.char_target_state.client_update_value(0)
+ self.hass.block_till_done()
+
+ self.assertEqual(acc.char_current_state.value, 3)
+ self.assertEqual(acc.char_target_state.value, 0)
+ self.assertEqual(
+ self.events[1].data[ATTR_SERVICE], 'open_cover')
+
def test_window_set_cover_position(self):
"""Test if accessory and HA are updated accordingly."""
window_cover = 'cover.window'
- acc = WindowCovering(self.hass, window_cover, 'Cover', aid=2)
+ acc = WindowCovering(self.hass, 'Cover', window_cover, 2, config=None)
acc.run()
self.assertEqual(acc.aid, 2)
@@ -43,7 +103,6 @@ class TestHomekitSensors(unittest.TestCase):
self.assertEqual(acc.char_current_position.value, 0)
self.assertEqual(acc.char_target_position.value, 0)
- self.assertEqual(acc.char_position_state.value, 0)
self.hass.states.set(window_cover, STATE_UNKNOWN,
{ATTR_CURRENT_POSITION: None})
@@ -51,7 +110,6 @@ class TestHomekitSensors(unittest.TestCase):
self.assertEqual(acc.char_current_position.value, 0)
self.assertEqual(acc.char_target_position.value, 0)
- self.assertEqual(acc.char_position_state.value, 0)
self.hass.states.set(window_cover, STATE_OPEN,
{ATTR_CURRENT_POSITION: 50})
@@ -59,10 +117,9 @@ class TestHomekitSensors(unittest.TestCase):
self.assertEqual(acc.char_current_position.value, 50)
self.assertEqual(acc.char_target_position.value, 50)
- self.assertEqual(acc.char_position_state.value, 2)
# Set from HomeKit
- acc.char_target_position.set_value(25)
+ acc.char_target_position.client_update_value(25)
self.hass.block_till_done()
self.assertEqual(
self.events[0].data[ATTR_SERVICE], 'set_cover_position')
@@ -71,16 +128,122 @@ class TestHomekitSensors(unittest.TestCase):
self.assertEqual(acc.char_current_position.value, 50)
self.assertEqual(acc.char_target_position.value, 25)
- self.assertEqual(acc.char_position_state.value, 0)
# Set from HomeKit
- acc.char_target_position.set_value(75)
+ acc.char_target_position.client_update_value(75)
self.hass.block_till_done()
self.assertEqual(
- self.events[0].data[ATTR_SERVICE], 'set_cover_position')
+ self.events[1].data[ATTR_SERVICE], 'set_cover_position')
self.assertEqual(
self.events[1].data[ATTR_SERVICE_DATA][ATTR_POSITION], 75)
self.assertEqual(acc.char_current_position.value, 50)
self.assertEqual(acc.char_target_position.value, 75)
- self.assertEqual(acc.char_position_state.value, 1)
+
+ def test_window_open_close(self):
+ """Test if accessory and HA are updated accordingly."""
+ window_cover = 'cover.window'
+
+ self.hass.states.set(window_cover, STATE_UNKNOWN,
+ {ATTR_SUPPORTED_FEATURES: 0})
+ acc = WindowCoveringBasic(self.hass, 'Cover', window_cover, 2,
+ config=None)
+ acc.run()
+
+ self.assertEqual(acc.aid, 2)
+ self.assertEqual(acc.category, 14) # WindowCovering
+
+ self.assertEqual(acc.char_current_position.value, 0)
+ self.assertEqual(acc.char_target_position.value, 0)
+ self.assertEqual(acc.char_position_state.value, 2)
+
+ self.hass.states.set(window_cover, STATE_UNKNOWN)
+ self.hass.block_till_done()
+
+ self.assertEqual(acc.char_current_position.value, 0)
+ self.assertEqual(acc.char_target_position.value, 0)
+ self.assertEqual(acc.char_position_state.value, 2)
+
+ self.hass.states.set(window_cover, STATE_OPEN)
+ self.hass.block_till_done()
+
+ self.assertEqual(acc.char_current_position.value, 100)
+ self.assertEqual(acc.char_target_position.value, 100)
+ self.assertEqual(acc.char_position_state.value, 2)
+
+ self.hass.states.set(window_cover, STATE_CLOSED)
+ self.hass.block_till_done()
+
+ self.assertEqual(acc.char_current_position.value, 0)
+ self.assertEqual(acc.char_target_position.value, 0)
+ self.assertEqual(acc.char_position_state.value, 2)
+
+ # Set from HomeKit
+ acc.char_target_position.client_update_value(25)
+ self.hass.block_till_done()
+ self.assertEqual(
+ self.events[0].data[ATTR_SERVICE], 'close_cover')
+
+ self.assertEqual(acc.char_current_position.value, 0)
+ self.assertEqual(acc.char_target_position.value, 0)
+ self.assertEqual(acc.char_position_state.value, 2)
+
+ # Set from HomeKit
+ acc.char_target_position.client_update_value(90)
+ self.hass.block_till_done()
+ self.assertEqual(
+ self.events[1].data[ATTR_SERVICE], 'open_cover')
+
+ self.assertEqual(acc.char_current_position.value, 100)
+ self.assertEqual(acc.char_target_position.value, 100)
+ self.assertEqual(acc.char_position_state.value, 2)
+
+ # Set from HomeKit
+ acc.char_target_position.client_update_value(55)
+ self.hass.block_till_done()
+ self.assertEqual(
+ self.events[2].data[ATTR_SERVICE], 'open_cover')
+
+ self.assertEqual(acc.char_current_position.value, 100)
+ self.assertEqual(acc.char_target_position.value, 100)
+ self.assertEqual(acc.char_position_state.value, 2)
+
+ def test_window_open_close_stop(self):
+ """Test if accessory and HA are updated accordingly."""
+ window_cover = 'cover.window'
+
+ self.hass.states.set(window_cover, STATE_UNKNOWN,
+ {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP})
+ acc = WindowCoveringBasic(self.hass, 'Cover', window_cover, 2,
+ config=None)
+ acc.run()
+
+ # Set from HomeKit
+ acc.char_target_position.client_update_value(25)
+ self.hass.block_till_done()
+ self.assertEqual(
+ self.events[0].data[ATTR_SERVICE], 'close_cover')
+
+ self.assertEqual(acc.char_current_position.value, 0)
+ self.assertEqual(acc.char_target_position.value, 0)
+ self.assertEqual(acc.char_position_state.value, 2)
+
+ # Set from HomeKit
+ acc.char_target_position.client_update_value(90)
+ self.hass.block_till_done()
+ self.assertEqual(
+ self.events[1].data[ATTR_SERVICE], 'open_cover')
+
+ self.assertEqual(acc.char_current_position.value, 100)
+ self.assertEqual(acc.char_target_position.value, 100)
+ self.assertEqual(acc.char_position_state.value, 2)
+
+ # Set from HomeKit
+ acc.char_target_position.client_update_value(55)
+ self.hass.block_till_done()
+ self.assertEqual(
+ self.events[2].data[ATTR_SERVICE], 'stop_cover')
+
+ self.assertEqual(acc.char_current_position.value, 50)
+ self.assertEqual(acc.char_target_position.value, 50)
+ self.assertEqual(acc.char_position_state.value, 2)
diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py
index 1cfb926c4ce..10bf469c08d 100644
--- a/tests/components/homekit/test_type_lights.py
+++ b/tests/components/homekit/test_type_lights.py
@@ -2,7 +2,6 @@
import unittest
from homeassistant.core import callback
-from homeassistant.components.homekit.type_lights import Light
from homeassistant.components.light import (
DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP,
ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR)
@@ -12,11 +11,26 @@ from homeassistant.const import (
SERVICE_TURN_OFF, STATE_ON, STATE_OFF, STATE_UNKNOWN)
from tests.common import get_test_home_assistant
+from tests.components.homekit.test_accessories import patch_debounce
class TestHomekitLights(unittest.TestCase):
"""Test class for all accessory types regarding lights."""
+ @classmethod
+ def setUpClass(cls):
+ """Setup Light class import and debounce patcher."""
+ cls.patcher = patch_debounce()
+ cls.patcher.start()
+ _import = __import__('homeassistant.components.homekit.type_lights',
+ fromlist=['Light'])
+ cls.light_cls = _import.Light
+
+ @classmethod
+ def tearDownClass(cls):
+ """Stop debounce patcher."""
+ cls.patcher.stop()
+
def setUp(self):
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
@@ -36,9 +50,11 @@ class TestHomekitLights(unittest.TestCase):
def test_light_basic(self):
"""Test light with char state."""
entity_id = 'light.demo'
+
self.hass.states.set(entity_id, STATE_ON,
{ATTR_SUPPORTED_FEATURES: 0})
- acc = Light(self.hass, entity_id, 'Light', aid=2)
+ self.hass.block_till_done()
+ acc = self.light_cls(self.hass, 'Light', entity_id, 2, config=None)
self.assertEqual(acc.aid, 2)
self.assertEqual(acc.category, 5) # Lightbulb
self.assertEqual(acc.char_on.value, 0)
@@ -57,7 +73,7 @@ class TestHomekitLights(unittest.TestCase):
self.assertEqual(acc.char_on.value, 0)
# Set from HomeKit
- acc.char_on.set_value(1)
+ acc.char_on.client_update_value(1)
self.hass.block_till_done()
self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN)
self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON)
@@ -65,7 +81,7 @@ class TestHomekitLights(unittest.TestCase):
self.hass.states.set(entity_id, STATE_ON)
self.hass.block_till_done()
- acc.char_on.set_value(0)
+ acc.char_on.client_update_value(0)
self.hass.block_till_done()
self.assertEqual(self.events[1].data[ATTR_DOMAIN], DOMAIN)
self.assertEqual(self.events[1].data[ATTR_SERVICE], SERVICE_TURN_OFF)
@@ -80,9 +96,11 @@ class TestHomekitLights(unittest.TestCase):
def test_light_brightness(self):
"""Test light with brightness."""
entity_id = 'light.demo'
+
self.hass.states.set(entity_id, STATE_ON, {
ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255})
- acc = Light(self.hass, entity_id, 'Light', aid=2)
+ self.hass.block_till_done()
+ acc = self.light_cls(self.hass, 'Light', entity_id, 2, config=None)
self.assertEqual(acc.char_brightness.value, 0)
acc.run()
@@ -94,8 +112,8 @@ class TestHomekitLights(unittest.TestCase):
self.assertEqual(acc.char_brightness.value, 40)
# Set from HomeKit
- acc.char_brightness.set_value(20)
- acc.char_on.set_value(1)
+ acc.char_brightness.client_update_value(20)
+ acc.char_on.client_update_value(1)
self.hass.block_till_done()
self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN)
self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON)
@@ -103,8 +121,8 @@ class TestHomekitLights(unittest.TestCase):
self.events[0].data[ATTR_SERVICE_DATA], {
ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 20})
- acc.char_on.set_value(1)
- acc.char_brightness.set_value(40)
+ acc.char_on.client_update_value(1)
+ acc.char_brightness.client_update_value(40)
self.hass.block_till_done()
self.assertEqual(self.events[1].data[ATTR_DOMAIN], DOMAIN)
self.assertEqual(self.events[1].data[ATTR_SERVICE], SERVICE_TURN_ON)
@@ -112,8 +130,8 @@ class TestHomekitLights(unittest.TestCase):
self.events[1].data[ATTR_SERVICE_DATA], {
ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 40})
- acc.char_on.set_value(1)
- acc.char_brightness.set_value(0)
+ acc.char_on.client_update_value(1)
+ acc.char_brightness.client_update_value(0)
self.hass.block_till_done()
self.assertEqual(self.events[2].data[ATTR_DOMAIN], DOMAIN)
self.assertEqual(self.events[2].data[ATTR_SERVICE], SERVICE_TURN_OFF)
@@ -121,10 +139,12 @@ class TestHomekitLights(unittest.TestCase):
def test_light_color_temperature(self):
"""Test light with color temperature."""
entity_id = 'light.demo'
+
self.hass.states.set(entity_id, STATE_ON, {
ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP,
ATTR_COLOR_TEMP: 190})
- acc = Light(self.hass, entity_id, 'Light', aid=2)
+ self.hass.block_till_done()
+ acc = self.light_cls(self.hass, 'Light', entity_id, 2, config=None)
self.assertEqual(acc.char_color_temperature.value, 153)
acc.run()
@@ -132,7 +152,7 @@ class TestHomekitLights(unittest.TestCase):
self.assertEqual(acc.char_color_temperature.value, 190)
# Set from HomeKit
- acc.char_color_temperature.set_value(250)
+ acc.char_color_temperature.client_update_value(250)
self.hass.block_till_done()
self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN)
self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON)
@@ -143,10 +163,12 @@ class TestHomekitLights(unittest.TestCase):
def test_light_rgb_color(self):
"""Test light with rgb_color."""
entity_id = 'light.demo'
+
self.hass.states.set(entity_id, STATE_ON, {
ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR,
ATTR_HS_COLOR: (260, 90)})
- acc = Light(self.hass, entity_id, 'Light', aid=2)
+ self.hass.block_till_done()
+ acc = self.light_cls(self.hass, 'Light', entity_id, 2, config=None)
self.assertEqual(acc.char_hue.value, 0)
self.assertEqual(acc.char_saturation.value, 75)
@@ -156,8 +178,8 @@ class TestHomekitLights(unittest.TestCase):
self.assertEqual(acc.char_saturation.value, 90)
# Set from HomeKit
- acc.char_hue.set_value(145)
- acc.char_saturation.set_value(75)
+ acc.char_hue.client_update_value(145)
+ acc.char_saturation.client_update_value(75)
self.hass.block_till_done()
self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN)
self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON)
diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py
new file mode 100644
index 00000000000..b2053116060
--- /dev/null
+++ b/tests/components/homekit/test_type_locks.py
@@ -0,0 +1,77 @@
+"""Test different accessory types: Locks."""
+import unittest
+
+from homeassistant.core import callback
+from homeassistant.components.homekit.type_locks import Lock
+from homeassistant.const import (
+ STATE_UNKNOWN, STATE_UNLOCKED, STATE_LOCKED,
+ ATTR_SERVICE, EVENT_CALL_SERVICE)
+
+from tests.common import get_test_home_assistant
+
+
+class TestHomekitSensors(unittest.TestCase):
+ """Test class for all accessory types regarding covers."""
+
+ def setUp(self):
+ """Setup things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+ self.events = []
+
+ @callback
+ def record_event(event):
+ """Track called event."""
+ self.events.append(event)
+
+ self.hass.bus.listen(EVENT_CALL_SERVICE, record_event)
+
+ def tearDown(self):
+ """Stop down everything that was started."""
+ self.hass.stop()
+
+ def test_lock_unlock(self):
+ """Test if accessory and HA are updated accordingly."""
+ kitchen_lock = 'lock.kitchen_door'
+
+ acc = Lock(self.hass, 'Lock', kitchen_lock, 2, config=None)
+ acc.run()
+
+ self.assertEqual(acc.aid, 2)
+ self.assertEqual(acc.category, 6) # DoorLock
+
+ self.assertEqual(acc.char_current_state.value, 3)
+ self.assertEqual(acc.char_target_state.value, 1)
+
+ self.hass.states.set(kitchen_lock, STATE_LOCKED)
+ self.hass.block_till_done()
+
+ self.assertEqual(acc.char_current_state.value, 1)
+ self.assertEqual(acc.char_target_state.value, 1)
+
+ self.hass.states.set(kitchen_lock, STATE_UNLOCKED)
+ self.hass.block_till_done()
+
+ self.assertEqual(acc.char_current_state.value, 0)
+ self.assertEqual(acc.char_target_state.value, 0)
+
+ self.hass.states.set(kitchen_lock, STATE_UNKNOWN)
+ self.hass.block_till_done()
+
+ self.assertEqual(acc.char_current_state.value, 3)
+ self.assertEqual(acc.char_target_state.value, 0)
+
+ # Set from HomeKit
+ acc.char_target_state.client_update_value(1)
+ self.hass.block_till_done()
+ self.assertEqual(
+ self.events[0].data[ATTR_SERVICE], 'lock')
+ self.assertEqual(acc.char_target_state.value, 1)
+
+ acc.char_target_state.client_update_value(0)
+ self.hass.block_till_done()
+ self.assertEqual(
+ self.events[1].data[ATTR_SERVICE], 'unlock')
+ self.assertEqual(acc.char_target_state.value, 0)
+
+ self.hass.states.remove(kitchen_lock)
+ self.hass.block_till_done()
diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py
index c689a73bac2..ec538ce4b50 100644
--- a/tests/components/homekit/test_type_security_systems.py
+++ b/tests/components/homekit/test_type_security_systems.py
@@ -35,8 +35,8 @@ class TestHomekitSecuritySystems(unittest.TestCase):
"""Test if accessory and HA are updated accordingly."""
acp = 'alarm_control_panel.test'
- acc = SecuritySystem(self.hass, acp, 'SecuritySystem',
- alarm_code='1234', aid=2)
+ acc = SecuritySystem(self.hass, 'SecuritySystem', acp,
+ 2, config={ATTR_CODE: '1234'})
acc.run()
self.assertEqual(acc.aid, 2)
@@ -71,7 +71,7 @@ class TestHomekitSecuritySystems(unittest.TestCase):
self.assertEqual(acc.char_current_state.value, 3)
# Set from HomeKit
- acc.char_target_state.set_value(0)
+ acc.char_target_state.client_update_value(0)
self.hass.block_till_done()
self.assertEqual(
self.events[0].data[ATTR_SERVICE], 'alarm_arm_home')
@@ -79,7 +79,7 @@ class TestHomekitSecuritySystems(unittest.TestCase):
self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234')
self.assertEqual(acc.char_target_state.value, 0)
- acc.char_target_state.set_value(1)
+ acc.char_target_state.client_update_value(1)
self.hass.block_till_done()
self.assertEqual(
self.events[1].data[ATTR_SERVICE], 'alarm_arm_away')
@@ -87,7 +87,7 @@ class TestHomekitSecuritySystems(unittest.TestCase):
self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234')
self.assertEqual(acc.char_target_state.value, 1)
- acc.char_target_state.set_value(2)
+ acc.char_target_state.client_update_value(2)
self.hass.block_till_done()
self.assertEqual(
self.events[2].data[ATTR_SERVICE], 'alarm_arm_night')
@@ -95,7 +95,7 @@ class TestHomekitSecuritySystems(unittest.TestCase):
self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234')
self.assertEqual(acc.char_target_state.value, 2)
- acc.char_target_state.set_value(3)
+ acc.char_target_state.client_update_value(3)
self.hass.block_till_done()
self.assertEqual(
self.events[3].data[ATTR_SERVICE], 'alarm_disarm')
@@ -107,12 +107,12 @@ class TestHomekitSecuritySystems(unittest.TestCase):
"""Test accessory if security_system doesn't require a alarm_code."""
acp = 'alarm_control_panel.test'
- acc = SecuritySystem(self.hass, acp, 'SecuritySystem',
- alarm_code=None, aid=2)
+ acc = SecuritySystem(self.hass, 'SecuritySystem', acp,
+ 2, config={ATTR_CODE: None})
acc.run()
# Set from HomeKit
- acc.char_target_state.set_value(0)
+ acc.char_target_state.client_update_value(0)
self.hass.block_till_done()
self.assertEqual(
self.events[0].data[ATTR_SERVICE], 'alarm_arm_home')
diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py
index c04c250613d..77bfc0c8901 100644
--- a/tests/components/homekit/test_type_sensors.py
+++ b/tests/components/homekit/test_type_sensors.py
@@ -3,9 +3,11 @@ import unittest
from homeassistant.components.homekit.const import PROP_CELSIUS
from homeassistant.components.homekit.type_sensors import (
- TemperatureSensor, HumiditySensor)
+ TemperatureSensor, HumiditySensor, AirQualitySensor, CarbonDioxideSensor,
+ LightSensor, BinarySensor, BINARY_SENSOR_SERVICE_MAP)
from homeassistant.const import (
- ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT)
+ ATTR_UNIT_OF_MEASUREMENT, ATTR_DEVICE_CLASS, STATE_UNKNOWN, STATE_ON,
+ STATE_OFF, STATE_HOME, STATE_NOT_HOME, TEMP_CELSIUS, TEMP_FAHRENHEIT)
from tests.common import get_test_home_assistant
@@ -25,7 +27,8 @@ class TestHomekitSensors(unittest.TestCase):
"""Test if accessory is updated after state change."""
entity_id = 'sensor.temperature'
- acc = TemperatureSensor(self.hass, entity_id, 'Temperature', aid=2)
+ acc = TemperatureSensor(self.hass, 'Temperature', entity_id,
+ 2, config=None)
acc.run()
self.assertEqual(acc.aid, 2)
@@ -38,6 +41,7 @@ class TestHomekitSensors(unittest.TestCase):
self.hass.states.set(entity_id, STATE_UNKNOWN,
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
self.hass.block_till_done()
+ self.assertEqual(acc.char_temp.value, 0.0)
self.hass.states.set(entity_id, '20',
{ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
@@ -53,7 +57,7 @@ class TestHomekitSensors(unittest.TestCase):
"""Test if accessory is updated after state change."""
entity_id = 'sensor.humidity'
- acc = HumiditySensor(self.hass, entity_id, 'Humidity', aid=2)
+ acc = HumiditySensor(self.hass, 'Humidity', entity_id, 2, config=None)
acc.run()
self.assertEqual(acc.aid, 2)
@@ -61,10 +65,145 @@ class TestHomekitSensors(unittest.TestCase):
self.assertEqual(acc.char_humidity.value, 0)
- self.hass.states.set(entity_id, STATE_UNKNOWN,
- {ATTR_UNIT_OF_MEASUREMENT: "%"})
+ self.hass.states.set(entity_id, STATE_UNKNOWN)
self.hass.block_till_done()
+ self.assertEqual(acc.char_humidity.value, 0)
- self.hass.states.set(entity_id, '20', {ATTR_UNIT_OF_MEASUREMENT: "%"})
+ self.hass.states.set(entity_id, '20')
self.hass.block_till_done()
self.assertEqual(acc.char_humidity.value, 20)
+
+ def test_air_quality(self):
+ """Test if accessory is updated after state change."""
+ entity_id = 'sensor.air_quality'
+
+ acc = AirQualitySensor(self.hass, 'Air Quality', entity_id,
+ 2, config=None)
+ acc.run()
+
+ self.assertEqual(acc.aid, 2)
+ self.assertEqual(acc.category, 10) # Sensor
+
+ self.assertEqual(acc.char_density.value, 0)
+ self.assertEqual(acc.char_quality.value, 0)
+
+ self.hass.states.set(entity_id, STATE_UNKNOWN)
+ self.hass.block_till_done()
+ self.assertEqual(acc.char_density.value, 0)
+ self.assertEqual(acc.char_quality.value, 0)
+
+ self.hass.states.set(entity_id, '34')
+ self.hass.block_till_done()
+ self.assertEqual(acc.char_density.value, 34)
+ self.assertEqual(acc.char_quality.value, 1)
+
+ self.hass.states.set(entity_id, '200')
+ self.hass.block_till_done()
+ self.assertEqual(acc.char_density.value, 200)
+ self.assertEqual(acc.char_quality.value, 5)
+
+ def test_co2(self):
+ """Test if accessory is updated after state change."""
+ entity_id = 'sensor.co2'
+
+ acc = CarbonDioxideSensor(self.hass, 'CO2', entity_id, 2, config=None)
+ acc.run()
+
+ self.assertEqual(acc.aid, 2)
+ self.assertEqual(acc.category, 10) # Sensor
+
+ self.assertEqual(acc.char_co2.value, 0)
+ self.assertEqual(acc.char_peak.value, 0)
+ self.assertEqual(acc.char_detected.value, 0)
+
+ self.hass.states.set(entity_id, STATE_UNKNOWN)
+ self.hass.block_till_done()
+ self.assertEqual(acc.char_co2.value, 0)
+ self.assertEqual(acc.char_peak.value, 0)
+ self.assertEqual(acc.char_detected.value, 0)
+
+ self.hass.states.set(entity_id, '1100')
+ self.hass.block_till_done()
+ self.assertEqual(acc.char_co2.value, 1100)
+ self.assertEqual(acc.char_peak.value, 1100)
+ self.assertEqual(acc.char_detected.value, 1)
+
+ self.hass.states.set(entity_id, '800')
+ self.hass.block_till_done()
+ self.assertEqual(acc.char_co2.value, 800)
+ self.assertEqual(acc.char_peak.value, 1100)
+ self.assertEqual(acc.char_detected.value, 0)
+
+ def test_light(self):
+ """Test if accessory is updated after state change."""
+ entity_id = 'sensor.light'
+
+ acc = LightSensor(self.hass, 'Light', entity_id, 2, config=None)
+ acc.run()
+
+ self.assertEqual(acc.aid, 2)
+ self.assertEqual(acc.category, 10) # Sensor
+
+ self.assertEqual(acc.char_light.value, 0.0001)
+
+ self.hass.states.set(entity_id, STATE_UNKNOWN)
+ self.hass.block_till_done()
+ self.assertEqual(acc.char_light.value, 0.0001)
+
+ self.hass.states.set(entity_id, '300')
+ self.hass.block_till_done()
+ self.assertEqual(acc.char_light.value, 300)
+
+ def test_binary(self):
+ """Test if accessory is updated after state change."""
+ entity_id = 'binary_sensor.opening'
+
+ self.hass.states.set(entity_id, STATE_UNKNOWN,
+ {ATTR_DEVICE_CLASS: "opening"})
+ self.hass.block_till_done()
+
+ acc = BinarySensor(self.hass, 'Window Opening', entity_id,
+ 2, config=None)
+ acc.run()
+
+ self.assertEqual(acc.aid, 2)
+ self.assertEqual(acc.category, 10) # Sensor
+
+ self.assertEqual(acc.char_detected.value, 0)
+
+ self.hass.states.set(entity_id, STATE_ON,
+ {ATTR_DEVICE_CLASS: "opening"})
+ self.hass.block_till_done()
+ self.assertEqual(acc.char_detected.value, 1)
+
+ self.hass.states.set(entity_id, STATE_OFF,
+ {ATTR_DEVICE_CLASS: "opening"})
+ self.hass.block_till_done()
+ self.assertEqual(acc.char_detected.value, 0)
+
+ self.hass.states.set(entity_id, STATE_HOME,
+ {ATTR_DEVICE_CLASS: "opening"})
+ self.hass.block_till_done()
+ self.assertEqual(acc.char_detected.value, 1)
+
+ self.hass.states.set(entity_id, STATE_NOT_HOME,
+ {ATTR_DEVICE_CLASS: "opening"})
+ self.hass.block_till_done()
+ self.assertEqual(acc.char_detected.value, 0)
+
+ self.hass.states.remove(entity_id)
+ self.hass.block_till_done()
+
+ def test_binary_device_classes(self):
+ """Test if services and characteristics are assigned correctly."""
+ entity_id = 'binary_sensor.demo'
+
+ for device_class, (service, char) in BINARY_SENSOR_SERVICE_MAP.items():
+ self.hass.states.set(entity_id, STATE_OFF,
+ {ATTR_DEVICE_CLASS: device_class})
+ self.hass.block_till_done()
+
+ acc = BinarySensor(self.hass, 'Binary Sensor', entity_id,
+ 2, config=None)
+ self.assertEqual(acc.get_service(service).display_name, service)
+ self.assertEqual(acc.char_detected.display_name, char)
diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py
index 21d7583152e..65b107e24cd 100644
--- a/tests/components/homekit/test_type_switches.py
+++ b/tests/components/homekit/test_type_switches.py
@@ -34,7 +34,7 @@ class TestHomekitSwitches(unittest.TestCase):
entity_id = 'switch.test'
domain = split_entity_id(entity_id)[0]
- acc = Switch(self.hass, entity_id, 'Switch', aid=2)
+ acc = Switch(self.hass, 'Switch', entity_id, 2, config=None)
acc.run()
self.assertEqual(acc.aid, 2)
@@ -51,14 +51,14 @@ class TestHomekitSwitches(unittest.TestCase):
self.assertEqual(acc.char_on.value, False)
# Set from HomeKit
- acc.char_on.set_value(True)
+ acc.char_on.client_update_value(True)
self.hass.block_till_done()
self.assertEqual(
self.events[0].data[ATTR_DOMAIN], domain)
self.assertEqual(
self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON)
- acc.char_on.set_value(False)
+ acc.char_on.client_update_value(False)
self.hass.block_till_done()
self.assertEqual(
self.events[1].data[ATTR_DOMAIN], domain)
@@ -70,13 +70,13 @@ class TestHomekitSwitches(unittest.TestCase):
entity_id = 'remote.test'
domain = split_entity_id(entity_id)[0]
- acc = Switch(self.hass, entity_id, 'Switch', aid=2)
+ acc = Switch(self.hass, 'Switch', entity_id, 2, config=None)
acc.run()
self.assertEqual(acc.char_on.value, False)
# Set from HomeKit
- acc.char_on.set_value(True)
+ acc.char_on.client_update_value(True)
self.hass.block_till_done()
self.assertEqual(
self.events[0].data[ATTR_DOMAIN], domain)
@@ -89,13 +89,13 @@ class TestHomekitSwitches(unittest.TestCase):
entity_id = 'input_boolean.test'
domain = split_entity_id(entity_id)[0]
- acc = Switch(self.hass, entity_id, 'Switch', aid=2)
+ acc = Switch(self.hass, 'Switch', entity_id, 2, config=None)
acc.run()
self.assertEqual(acc.char_on.value, False)
# Set from HomeKit
- acc.char_on.set_value(True)
+ acc.char_on.client_update_value(True)
self.hass.block_till_done()
self.assertEqual(
self.events[0].data[ATTR_DOMAIN], domain)
diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py
index e1511163f2f..adc3fb018f8 100644
--- a/tests/components/homekit/test_type_thermostats.py
+++ b/tests/components/homekit/test_type_thermostats.py
@@ -6,17 +6,33 @@ from homeassistant.components.climate import (
ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE,
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE,
ATTR_OPERATION_LIST, STATE_COOL, STATE_HEAT, STATE_AUTO)
-from homeassistant.components.homekit.type_thermostats import Thermostat
from homeassistant.const import (
- ATTR_SERVICE, EVENT_CALL_SERVICE, ATTR_SERVICE_DATA,
- ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
+ ATTR_SERVICE, ATTR_SERVICE_DATA, ATTR_SUPPORTED_FEATURES,
+ ATTR_UNIT_OF_MEASUREMENT, EVENT_CALL_SERVICE,
+ STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT)
from tests.common import get_test_home_assistant
+from tests.components.homekit.test_accessories import patch_debounce
class TestHomekitThermostats(unittest.TestCase):
"""Test class for all accessory types regarding thermostats."""
+ @classmethod
+ def setUpClass(cls):
+ """Setup Thermostat class import and debounce patcher."""
+ cls.patcher = patch_debounce()
+ cls.patcher.start()
+ _import = __import__(
+ 'homeassistant.components.homekit.type_thermostats',
+ fromlist=['Thermostat'])
+ cls.thermostat_cls = _import.Thermostat
+
+ @classmethod
+ def tearDownClass(cls):
+ """Stop debounce patcher."""
+ cls.patcher.stop()
+
def setUp(self):
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
@@ -37,7 +53,10 @@ class TestHomekitThermostats(unittest.TestCase):
"""Test if accessory and HA are updated accordingly."""
climate = 'climate.test'
- acc = Thermostat(self.hass, climate, 'Climate', False, aid=2)
+ self.hass.states.set(climate, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 0})
+ self.hass.block_till_done()
+ acc = self.thermostat_cls(self.hass, 'Climate', climate,
+ 2, config=None)
acc.run()
self.assertEqual(acc.aid, 2)
@@ -151,7 +170,7 @@ class TestHomekitThermostats(unittest.TestCase):
self.assertEqual(acc.char_display_units.value, 0)
# Set from HomeKit
- acc.char_target_temp.set_value(19.0)
+ acc.char_target_temp.client_update_value(19.0)
self.hass.block_till_done()
self.assertEqual(
self.events[0].data[ATTR_SERVICE], 'set_temperature')
@@ -159,7 +178,7 @@ class TestHomekitThermostats(unittest.TestCase):
self.events[0].data[ATTR_SERVICE_DATA][ATTR_TEMPERATURE], 19.0)
self.assertEqual(acc.char_target_temp.value, 19.0)
- acc.char_target_heat_cool.set_value(1)
+ acc.char_target_heat_cool.client_update_value(1)
self.hass.block_till_done()
self.assertEqual(
self.events[1].data[ATTR_SERVICE], 'set_operation_mode')
@@ -172,7 +191,11 @@ class TestHomekitThermostats(unittest.TestCase):
"""Test if accessory and HA are updated accordingly."""
climate = 'climate.test'
- acc = Thermostat(self.hass, climate, 'Climate', True)
+ # support_auto = True
+ self.hass.states.set(climate, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6})
+ self.hass.block_till_done()
+ acc = self.thermostat_cls(self.hass, 'Climate', climate,
+ 2, config=None)
acc.run()
self.assertEqual(acc.char_cooling_thresh_temp.value, 23.0)
@@ -221,7 +244,7 @@ class TestHomekitThermostats(unittest.TestCase):
self.assertEqual(acc.char_display_units.value, 0)
# Set from HomeKit
- acc.char_heating_thresh_temp.set_value(20.0)
+ acc.char_heating_thresh_temp.client_update_value(20.0)
self.hass.block_till_done()
self.assertEqual(
self.events[0].data[ATTR_SERVICE], 'set_temperature')
@@ -229,7 +252,7 @@ class TestHomekitThermostats(unittest.TestCase):
self.events[0].data[ATTR_SERVICE_DATA][ATTR_TARGET_TEMP_LOW], 20.0)
self.assertEqual(acc.char_heating_thresh_temp.value, 20.0)
- acc.char_cooling_thresh_temp.set_value(25.0)
+ acc.char_cooling_thresh_temp.client_update_value(25.0)
self.hass.block_till_done()
self.assertEqual(
self.events[1].data[ATTR_SERVICE], 'set_temperature')
@@ -242,7 +265,11 @@ class TestHomekitThermostats(unittest.TestCase):
"""Test if accessory and HA are updated accordingly."""
climate = 'climate.test'
- acc = Thermostat(self.hass, climate, 'Climate', True)
+ # support_auto = True
+ self.hass.states.set(climate, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6})
+ self.hass.block_till_done()
+ acc = self.thermostat_cls(self.hass, 'Climate', climate,
+ 2, config=None)
acc.run()
self.hass.states.set(climate, STATE_AUTO,
@@ -260,19 +287,19 @@ class TestHomekitThermostats(unittest.TestCase):
self.assertEqual(acc.char_display_units.value, 1)
# Set from HomeKit
- acc.char_cooling_thresh_temp.set_value(23)
+ acc.char_cooling_thresh_temp.client_update_value(23)
self.hass.block_till_done()
service_data = self.events[-1].data[ATTR_SERVICE_DATA]
self.assertEqual(service_data[ATTR_TARGET_TEMP_HIGH], 73.4)
self.assertEqual(service_data[ATTR_TARGET_TEMP_LOW], 68)
- acc.char_heating_thresh_temp.set_value(22)
+ acc.char_heating_thresh_temp.client_update_value(22)
self.hass.block_till_done()
service_data = self.events[-1].data[ATTR_SERVICE_DATA]
self.assertEqual(service_data[ATTR_TARGET_TEMP_HIGH], 73.4)
self.assertEqual(service_data[ATTR_TARGET_TEMP_LOW], 71.6)
- acc.char_target_temp.set_value(24.0)
+ acc.char_target_temp.client_update_value(24.0)
self.hass.block_till_done()
service_data = self.events[-1].data[ATTR_SERVICE_DATA]
self.assertEqual(service_data[ATTR_TEMPERATURE], 75.2)
diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py
index d6ef5856f85..4a9521384bd 100644
--- a/tests/components/homekit/test_util.py
+++ b/tests/components/homekit/test_util.py
@@ -2,13 +2,15 @@
import unittest
import voluptuous as vol
+import pytest
from homeassistant.core import callback
from homeassistant.components.homekit.accessories import HomeBridge
from homeassistant.components.homekit.const import HOMEKIT_NOTIFY_ID
from homeassistant.components.homekit.util import (
show_setup_message, dismiss_setup_message, convert_to_float,
- temperature_to_homekit, temperature_to_states, ATTR_CODE)
+ temperature_to_homekit, temperature_to_states, ATTR_CODE,
+ density_to_air_quality)
from homeassistant.components.homekit.util import validate_entity_config \
as vec
from homeassistant.components.persistent_notification import (
@@ -20,6 +22,52 @@ from homeassistant.const import (
from tests.common import get_test_home_assistant
+def test_validate_entity_config():
+ """Test validate entities."""
+ configs = [{'invalid_entity_id': {}}, {'demo.test': 1},
+ {'demo.test': 'test'}, {'demo.test': [1, 2]},
+ {'demo.test': None}]
+
+ for conf in configs:
+ with pytest.raises(vol.Invalid):
+ vec(conf)
+
+ assert vec({}) == {}
+ assert vec({'alarm_control_panel.demo': {ATTR_CODE: '1234'}}) == \
+ {'alarm_control_panel.demo': {ATTR_CODE: '1234'}}
+
+
+def test_convert_to_float():
+ """Test convert_to_float method."""
+ assert convert_to_float(12) == 12
+ assert convert_to_float(12.4) == 12.4
+ assert convert_to_float(STATE_UNKNOWN) is None
+ assert convert_to_float(None) is None
+
+
+def test_temperature_to_homekit():
+ """Test temperature conversion from HA to HomeKit."""
+ assert temperature_to_homekit(20.46, TEMP_CELSIUS) == 20.5
+ assert temperature_to_homekit(92.1, TEMP_FAHRENHEIT) == 33.4
+
+
+def test_temperature_to_states():
+ """Test temperature conversion from HomeKit to HA."""
+ assert temperature_to_states(20, TEMP_CELSIUS) == 20.0
+ assert temperature_to_states(20.2, TEMP_FAHRENHEIT) == 68.4
+
+
+def test_density_to_air_quality():
+ """Test map PM2.5 density to HomeKit AirQuality level."""
+ assert density_to_air_quality(0) == 1
+ assert density_to_air_quality(35) == 1
+ assert density_to_air_quality(35.1) == 2
+ assert density_to_air_quality(75) == 2
+ assert density_to_air_quality(115) == 3
+ assert density_to_air_quality(150) == 4
+ assert density_to_air_quality(300) == 5
+
+
class TestUtil(unittest.TestCase):
"""Test all HomeKit util methods."""
@@ -39,26 +87,11 @@ class TestUtil(unittest.TestCase):
"""Stop down everything that was started."""
self.hass.stop()
- def test_validate_entity_config(self):
- """Test validate entities."""
- configs = [{'invalid_entity_id': {}}, {'demo.test': 1},
- {'demo.test': 'test'}, {'demo.test': [1, 2]},
- {'demo.test': None}]
-
- for conf in configs:
- with self.assertRaises(vol.Invalid):
- vec(conf)
-
- self.assertEqual(vec({}), {})
- self.assertEqual(
- vec({'alarm_control_panel.demo': {ATTR_CODE: '1234'}}),
- {'alarm_control_panel.demo': {ATTR_CODE: '1234'}})
-
def test_show_setup_msg(self):
"""Test show setup message as persistence notification."""
bridge = HomeBridge(self.hass)
- show_setup_message(bridge, self.hass)
+ show_setup_message(self.hass, bridge)
self.hass.block_till_done()
data = self.events[0].data
@@ -83,20 +116,3 @@ class TestUtil(unittest.TestCase):
self.assertEqual(
data[ATTR_SERVICE_DATA].get(ATTR_NOTIFICATION_ID, None),
HOMEKIT_NOTIFY_ID)
-
- def test_convert_to_float(self):
- """Test convert_to_float method."""
- self.assertEqual(convert_to_float(12), 12)
- self.assertEqual(convert_to_float(12.4), 12.4)
- self.assertIsNone(convert_to_float(STATE_UNKNOWN))
- self.assertIsNone(convert_to_float(None))
-
- def test_temperature_to_homekit(self):
- """Test temperature conversion from HA to HomeKit."""
- self.assertEqual(temperature_to_homekit(20.46, TEMP_CELSIUS), 20.5)
- self.assertEqual(temperature_to_homekit(92.1, TEMP_FAHRENHEIT), 33.4)
-
- def test_temperature_to_states(self):
- """Test temperature conversion from HomeKit to HA."""
- self.assertEqual(temperature_to_states(20, TEMP_CELSIUS), 20.0)
- self.assertEqual(temperature_to_states(20.2, TEMP_FAHRENHEIT), 68.4)
diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py
index 0845aa2f077..c20cee0d0e8 100644
--- a/tests/components/hue/test_bridge.py
+++ b/tests/components/hue/test_bridge.py
@@ -18,10 +18,9 @@ async def test_bridge_setup():
assert await hue_bridge.async_setup() is True
assert hue_bridge.api is api
- assert len(hass.helpers.discovery.async_load_platform.mock_calls) == 1
- assert hass.helpers.discovery.async_load_platform.mock_calls[0][1][2] == {
- 'host': '1.2.3.4'
- }
+ assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 1
+ assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == \
+ (entry, 'light')
async def test_bridge_setup_invalid_username():
@@ -55,3 +54,60 @@ async def test_bridge_setup_timeout(hass):
assert len(hass.helpers.event.async_call_later.mock_calls) == 1
# Assert we are going to wait 2 seconds
assert hass.helpers.event.async_call_later.mock_calls[0][1][0] == 2
+
+
+async def test_reset_cancels_retry_setup():
+ """Test resetting a bridge while we're waiting to retry setup."""
+ hass = Mock()
+ entry = Mock()
+ entry.data = {'host': '1.2.3.4', 'username': 'mock-username'}
+ hue_bridge = bridge.HueBridge(hass, entry, False, False)
+
+ with patch.object(bridge, 'get_bridge', side_effect=errors.CannotConnect):
+ assert await hue_bridge.async_setup() is False
+
+ mock_call_later = hass.helpers.event.async_call_later
+
+ assert len(mock_call_later.mock_calls) == 1
+
+ assert await hue_bridge.async_reset()
+
+ assert len(mock_call_later.mock_calls) == 2
+ assert len(mock_call_later.return_value.mock_calls) == 1
+
+
+async def test_reset_if_entry_had_wrong_auth():
+ """Test calling reset when the entry contained wrong auth."""
+ hass = Mock()
+ entry = Mock()
+ entry.data = {'host': '1.2.3.4', 'username': 'mock-username'}
+ hue_bridge = bridge.HueBridge(hass, entry, False, False)
+
+ with patch.object(bridge, 'get_bridge',
+ side_effect=errors.AuthenticationRequired):
+ assert await hue_bridge.async_setup() is False
+
+ assert len(hass.async_add_job.mock_calls) == 1
+
+ assert await hue_bridge.async_reset()
+
+
+async def test_reset_unloads_entry_if_setup():
+ """Test calling reset while the entry has been setup."""
+ hass = Mock()
+ entry = Mock()
+ entry.data = {'host': '1.2.3.4', 'username': 'mock-username'}
+ hue_bridge = bridge.HueBridge(hass, entry, False, False)
+
+ with patch.object(bridge, 'get_bridge', return_value=mock_coro(Mock())):
+ assert await hue_bridge.async_setup() is True
+
+ assert len(hass.services.async_register.mock_calls) == 1
+ assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 1
+
+ hass.config_entries.async_forward_entry_unload.return_value = \
+ mock_coro(True)
+ assert await hue_bridge.async_reset()
+
+ assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 1
+ assert len(hass.services.async_remove.mock_calls) == 1
diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py
index 47e74b70e83..ea656ba8fc6 100644
--- a/tests/components/hue/test_init.py
+++ b/tests/components/hue/test_init.py
@@ -167,3 +167,22 @@ async def test_config_passed_to_config_entry(hass):
assert p_entry is entry
assert p_allow_unreachable is True
assert p_allow_groups is False
+
+
+async def test_unload_entry(hass):
+ """Test being able to unload an entry."""
+ entry = MockConfigEntry(domain=hue.DOMAIN, data={
+ 'host': '0.0.0.0',
+ })
+ entry.add_to_hass(hass)
+
+ with patch.object(hue, 'HueBridge') as mock_bridge:
+ mock_bridge.return_value.async_setup.return_value = mock_coro(True)
+ assert await async_setup_component(hass, hue.DOMAIN, {}) is True
+
+ assert len(mock_bridge.return_value.mock_calls) == 1
+
+ mock_bridge.return_value.async_reset.return_value = mock_coro(True)
+ assert await hue.async_unload_entry(hass, entry)
+ assert len(mock_bridge.return_value.async_reset.mock_calls) == 1
+ assert hass.data[hue.DOMAIN] == {}
diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py
index 7b6c3a21a79..712cd17a7c7 100644
--- a/tests/components/light/test_hue.py
+++ b/tests/components/light/test_hue.py
@@ -9,6 +9,7 @@ from aiohue.lights import Lights
from aiohue.groups import Groups
import pytest
+from homeassistant import config_entries
from homeassistant.components import hue
import homeassistant.components.light.hue as hue_light
from homeassistant.util import color
@@ -196,9 +197,11 @@ async def setup_bridge(hass, mock_bridge):
"""Load the Hue light platform with the provided bridge."""
hass.config.components.add(hue.DOMAIN)
hass.data[hue.DOMAIN] = {'mock-host': mock_bridge}
- await hass.helpers.discovery.async_load_platform('light', 'hue', {
+ config_entry = config_entries.ConfigEntry(1, hue.DOMAIN, 'Mock Title', {
'host': 'mock-host'
- })
+ }, 'test')
+ await hass.config_entries.async_forward_entry_setup(config_entry, 'light')
+ # To flush out the service call to update the group
await hass.async_block_till_done()
diff --git a/tests/components/media_player/test_blackbird.py b/tests/components/media_player/test_blackbird.py
new file mode 100644
index 00000000000..86bfdfb52c4
--- /dev/null
+++ b/tests/components/media_player/test_blackbird.py
@@ -0,0 +1,328 @@
+"""The tests for the Monoprice Blackbird media player platform."""
+import unittest
+from unittest import mock
+import voluptuous as vol
+
+from collections import defaultdict
+from homeassistant.components.media_player import (
+ DOMAIN, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_SELECT_SOURCE)
+from homeassistant.const import STATE_ON, STATE_OFF
+
+import tests.common
+from homeassistant.components.media_player.blackbird import (
+ DATA_BLACKBIRD, PLATFORM_SCHEMA, SERVICE_SETALLZONES, setup_platform)
+
+
+class AttrDict(dict):
+ """Helper clas for mocking attributes."""
+
+ def __setattr__(self, name, value):
+ """Set attribute."""
+ self[name] = value
+
+ def __getattr__(self, item):
+ """Get attribute."""
+ return self[item]
+
+
+class MockBlackbird(object):
+ """Mock for pyblackbird object."""
+
+ def __init__(self):
+ """Init mock object."""
+ self.zones = defaultdict(lambda: AttrDict(power=True,
+ av=1))
+
+ def zone_status(self, zone_id):
+ """Get zone status."""
+ status = self.zones[zone_id]
+ status.zone = zone_id
+ return AttrDict(status)
+
+ def set_zone_source(self, zone_id, source_idx):
+ """Set source for zone."""
+ self.zones[zone_id].av = source_idx
+
+ def set_zone_power(self, zone_id, power):
+ """Turn zone on/off."""
+ self.zones[zone_id].power = power
+
+ def set_all_zone_source(self, source_idx):
+ """Set source for all zones."""
+ self.zones[3].av = source_idx
+
+
+class TestBlackbirdSchema(unittest.TestCase):
+ """Test Blackbird schema."""
+
+ def test_valid_serial_schema(self):
+ """Test valid schema."""
+ valid_schema = {
+ 'platform': 'blackbird',
+ 'type': 'serial',
+ 'port': '/dev/ttyUSB0',
+ 'zones': {1: {'name': 'a'},
+ 2: {'name': 'a'},
+ 3: {'name': 'a'},
+ 4: {'name': 'a'},
+ 5: {'name': 'a'},
+ 6: {'name': 'a'},
+ 7: {'name': 'a'},
+ 8: {'name': 'a'},
+ },
+ 'sources': {
+ 1: {'name': 'a'},
+ 2: {'name': 'a'},
+ 3: {'name': 'a'},
+ 4: {'name': 'a'},
+ 5: {'name': 'a'},
+ 6: {'name': 'a'},
+ 7: {'name': 'a'},
+ 8: {'name': 'a'},
+ }
+ }
+ PLATFORM_SCHEMA(valid_schema)
+
+ def test_valid_socket_schema(self):
+ """Test valid schema."""
+ valid_schema = {
+ 'platform': 'blackbird',
+ 'type': 'socket',
+ 'port': '192.168.1.50',
+ 'zones': {1: {'name': 'a'},
+ 2: {'name': 'a'},
+ 3: {'name': 'a'},
+ 4: {'name': 'a'},
+ 5: {'name': 'a'},
+ },
+ 'sources': {
+ 1: {'name': 'a'},
+ 2: {'name': 'a'},
+ 3: {'name': 'a'},
+ 4: {'name': 'a'},
+ }
+ }
+ PLATFORM_SCHEMA(valid_schema)
+
+ def test_invalid_schemas(self):
+ """Test invalid schemas."""
+ schemas = (
+ {}, # Empty
+ None, # None
+ # Missing type
+ {
+ 'platform': 'blackbird',
+ 'port': 'aaa',
+ 'name': 'Name',
+ 'zones': {1: {'name': 'a'}},
+ 'sources': {1: {'name': 'b'}},
+ },
+ # Invalid zone number
+ {
+ 'platform': 'blackbird',
+ 'type': 'serial',
+ 'port': 'aaa',
+ 'name': 'Name',
+ 'zones': {11: {'name': 'a'}},
+ 'sources': {1: {'name': 'b'}},
+ },
+ # Invalid source number
+ {
+ 'platform': 'blackbird',
+ 'type': 'serial',
+ 'port': 'aaa',
+ 'name': 'Name',
+ 'zones': {1: {'name': 'a'}},
+ 'sources': {9: {'name': 'b'}},
+ },
+ # Zone missing name
+ {
+ 'platform': 'blackbird',
+ 'type': 'serial',
+ 'port': 'aaa',
+ 'name': 'Name',
+ 'zones': {1: {}},
+ 'sources': {1: {'name': 'b'}},
+ },
+ # Source missing name
+ {
+ 'platform': 'blackbird',
+ 'type': 'serial',
+ 'port': 'aaa',
+ 'name': 'Name',
+ 'zones': {1: {'name': 'a'}},
+ 'sources': {1: {}},
+ },
+ # Invalid type
+ {
+ 'platform': 'blackbird',
+ 'type': 'aaa',
+ 'port': 'aaa',
+ 'name': 'Name',
+ 'zones': {1: {'name': 'a'}},
+ 'sources': {1: {'name': 'b'}},
+ },
+ )
+ for value in schemas:
+ with self.assertRaises(vol.MultipleInvalid):
+ PLATFORM_SCHEMA(value)
+
+
+class TestBlackbirdMediaPlayer(unittest.TestCase):
+ """Test the media_player module."""
+
+ def setUp(self):
+ """Set up the test case."""
+ self.blackbird = MockBlackbird()
+ self.hass = tests.common.get_test_home_assistant()
+ self.hass.start()
+ # Note, source dictionary is unsorted!
+ with mock.patch('pyblackbird.get_blackbird',
+ new=lambda *a: self.blackbird):
+ setup_platform(self.hass, {
+ 'platform': 'blackbird',
+ 'type': 'serial',
+ 'port': '/dev/ttyUSB0',
+ 'zones': {3: {'name': 'Zone name'}},
+ 'sources': {1: {'name': 'one'},
+ 3: {'name': 'three'},
+ 2: {'name': 'two'}},
+ }, lambda *args, **kwargs: None, {})
+ self.hass.block_till_done()
+ self.media_player = self.hass.data[DATA_BLACKBIRD][0]
+ self.media_player.hass = self.hass
+ self.media_player.entity_id = 'media_player.zone_3'
+
+ def tearDown(self):
+ """Tear down the test case."""
+ self.hass.stop()
+
+ def test_setup_platform(self, *args):
+ """Test setting up platform."""
+ # One service must be registered
+ self.assertTrue(self.hass.services.has_service(DOMAIN,
+ SERVICE_SETALLZONES))
+ self.assertEqual(len(self.hass.data[DATA_BLACKBIRD]), 1)
+ self.assertEqual(self.hass.data[DATA_BLACKBIRD][0].name, 'Zone name')
+
+ def test_setallzones_service_call_with_entity_id(self):
+ """Test set all zone source service call with entity id."""
+ self.media_player.update()
+ self.assertEqual('Zone name', self.media_player.name)
+ self.assertEqual(STATE_ON, self.media_player.state)
+ self.assertEqual('one', self.media_player.source)
+
+ # Call set all zones service
+ self.hass.services.call(DOMAIN, SERVICE_SETALLZONES,
+ {'entity_id': 'media_player.zone_3',
+ 'source': 'three'},
+ blocking=True)
+
+ # Check that source was changed
+ self.assertEqual(3, self.blackbird.zones[3].av)
+ self.media_player.update()
+ self.assertEqual('three', self.media_player.source)
+
+ def test_setallzones_service_call_without_entity_id(self):
+ """Test set all zone source service call without entity id."""
+ self.media_player.update()
+ self.assertEqual('Zone name', self.media_player.name)
+ self.assertEqual(STATE_ON, self.media_player.state)
+ self.assertEqual('one', self.media_player.source)
+
+ # Call set all zones service
+ self.hass.services.call(DOMAIN, SERVICE_SETALLZONES,
+ {'source': 'three'}, blocking=True)
+
+ # Check that source was changed
+ self.assertEqual(3, self.blackbird.zones[3].av)
+ self.media_player.update()
+ self.assertEqual('three', self.media_player.source)
+
+ def test_update(self):
+ """Test updating values from blackbird."""
+ self.assertIsNone(self.media_player.state)
+ self.assertIsNone(self.media_player.source)
+
+ self.media_player.update()
+
+ self.assertEqual(STATE_ON, self.media_player.state)
+ self.assertEqual('one', self.media_player.source)
+
+ def test_name(self):
+ """Test name property."""
+ self.assertEqual('Zone name', self.media_player.name)
+
+ def test_state(self):
+ """Test state property."""
+ self.assertIsNone(self.media_player.state)
+
+ self.media_player.update()
+ self.assertEqual(STATE_ON, self.media_player.state)
+
+ self.blackbird.zones[3].power = False
+ self.media_player.update()
+ self.assertEqual(STATE_OFF, self.media_player.state)
+
+ def test_supported_features(self):
+ """Test supported features property."""
+ self.assertEqual(SUPPORT_TURN_ON | SUPPORT_TURN_OFF |
+ SUPPORT_SELECT_SOURCE,
+ self.media_player.supported_features)
+
+ def test_source(self):
+ """Test source property."""
+ self.assertIsNone(self.media_player.source)
+ self.media_player.update()
+ self.assertEqual('one', self.media_player.source)
+
+ def test_media_title(self):
+ """Test media title property."""
+ self.assertIsNone(self.media_player.media_title)
+ self.media_player.update()
+ self.assertEqual('one', self.media_player.media_title)
+
+ def test_source_list(self):
+ """Test source list property."""
+ # Note, the list is sorted!
+ self.assertEqual(['one', 'two', 'three'],
+ self.media_player.source_list)
+
+ def test_select_source(self):
+ """Test source selection methods."""
+ self.media_player.update()
+
+ self.assertEqual('one', self.media_player.source)
+
+ self.media_player.select_source('two')
+ self.assertEqual(2, self.blackbird.zones[3].av)
+ self.media_player.update()
+ self.assertEqual('two', self.media_player.source)
+
+ # Trying to set unknown source.
+ self.media_player.select_source('no name')
+ self.assertEqual(2, self.blackbird.zones[3].av)
+ self.media_player.update()
+ self.assertEqual('two', self.media_player.source)
+
+ def test_turn_on(self):
+ """Testing turning on the zone."""
+ self.blackbird.zones[3].power = False
+ self.media_player.update()
+ self.assertEqual(STATE_OFF, self.media_player.state)
+
+ self.media_player.turn_on()
+ self.assertTrue(self.blackbird.zones[3].power)
+ self.media_player.update()
+ self.assertEqual(STATE_ON, self.media_player.state)
+
+ def test_turn_off(self):
+ """Testing turning off the zone."""
+ self.blackbird.zones[3].power = True
+ self.media_player.update()
+ self.assertEqual(STATE_ON, self.media_player.state)
+
+ self.media_player.turn_off()
+ self.assertFalse(self.blackbird.zones[3].power)
+ self.media_player.update()
+ self.assertEqual(STATE_OFF, self.media_player.state)
diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py
index ee69ec1c85d..0c0f3906dc2 100644
--- a/tests/components/media_player/test_cast.py
+++ b/tests/components/media_player/test_cast.py
@@ -1,6 +1,7 @@
"""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
@@ -14,7 +15,8 @@ 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
+from homeassistant.components.media_player import cast, \
+ ATTR_MEDIA_POSITION, ATTR_MEDIA_POSITION_UPDATED_AT
from homeassistant.setup import async_setup_component
@@ -286,6 +288,8 @@ 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()
@@ -320,6 +324,85 @@ 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()
diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py
index 0d4082731ab..43432f3304c 100644
--- a/tests/components/sensor/test_filter.py
+++ b/tests/components/sensor/test_filter.py
@@ -7,7 +7,9 @@ from homeassistant.components.sensor.filter import (
LowPassFilter, OutlierFilter, ThrottleFilter, TimeSMAFilter)
import homeassistant.util.dt as dt_util
from homeassistant.setup import setup_component
-from tests.common import get_test_home_assistant, assert_setup_component
+import homeassistant.core as ha
+from tests.common import (get_test_home_assistant, assert_setup_component,
+ init_recorder_component)
class TestFilterSensor(unittest.TestCase):
@@ -16,12 +18,24 @@ class TestFilterSensor(unittest.TestCase):
def setup_method(self, method):
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
- self.values = [20, 19, 18, 21, 22, 0]
+ raw_values = [20, 19, 18, 21, 22, 0]
+ self.values = []
+
+ timestamp = dt_util.utcnow()
+ for val in raw_values:
+ self.values.append(ha.State('sensor.test_monitored',
+ val, last_updated=timestamp))
+ timestamp += timedelta(minutes=1)
def teardown_method(self, method):
"""Stop everything that was started."""
self.hass.stop()
+ def init_recorder(self):
+ """Initialize the recorder."""
+ init_recorder_component(self.hass)
+ self.hass.start()
+
def test_setup_fail(self):
"""Test if filter doesn't exist."""
config = {
@@ -36,41 +50,73 @@ class TestFilterSensor(unittest.TestCase):
def test_chain(self):
"""Test if filter chaining works."""
+ self.init_recorder()
config = {
+ 'history': {
+ },
'sensor': {
'platform': 'filter',
'name': 'test',
'entity_id': 'sensor.test_monitored',
+ 'history_period': '00:05',
'filters': [{
'filter': 'outlier',
+ 'window_size': 10,
'radius': 4.0
}, {
'filter': 'lowpass',
- 'window_size': 4,
'time_constant': 10,
'precision': 2
}]
}
}
- with assert_setup_component(1):
- assert setup_component(self.hass, 'sensor', config)
+ t_0 = dt_util.utcnow() - timedelta(minutes=1)
+ t_1 = dt_util.utcnow() - timedelta(minutes=2)
+ t_2 = dt_util.utcnow() - timedelta(minutes=3)
- for value in self.values:
- self.hass.states.set(config['sensor']['entity_id'], value)
- self.hass.block_till_done()
+ fake_states = {
+ 'sensor.test_monitored': [
+ ha.State('sensor.test_monitored', 18.0, last_changed=t_0),
+ ha.State('sensor.test_monitored', 19.0, last_changed=t_1),
+ ha.State('sensor.test_monitored', 18.2, last_changed=t_2),
+ ]
+ }
- state = self.hass.states.get('sensor.test')
- self.assertEqual('20.25', state.state)
+ with patch('homeassistant.components.history.'
+ 'state_changes_during_period', return_value=fake_states):
+ with patch('homeassistant.components.history.'
+ 'get_last_state_changes', return_value=fake_states):
+ with assert_setup_component(1, 'sensor'):
+ assert setup_component(self.hass, 'sensor', config)
+
+ for value in self.values:
+ self.hass.states.set(
+ config['sensor']['entity_id'], value.state)
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('sensor.test')
+ self.assertEqual('17.05', state.state)
def test_outlier(self):
"""Test if outlier filter works."""
- filt = OutlierFilter(window_size=10,
+ filt = OutlierFilter(window_size=3,
precision=2,
entity=None,
radius=4.0)
for state in self.values:
filtered = filt.filter_state(state)
- self.assertEqual(22, filtered)
+ self.assertEqual(22, filtered.state)
+
+ def test_initial_outlier(self):
+ """Test issue #13363."""
+ filt = OutlierFilter(window_size=3,
+ precision=2,
+ entity=None,
+ radius=4.0)
+ out = ha.State('sensor.test_monitored', 4000)
+ for state in [out]+self.values:
+ filtered = filt.filter_state(state)
+ self.assertEqual(22, filtered.state)
def test_lowpass(self):
"""Test if lowpass filter works."""
@@ -80,7 +126,7 @@ class TestFilterSensor(unittest.TestCase):
time_constant=10)
for state in self.values:
filtered = filt.filter_state(state)
- self.assertEqual(18.05, filtered)
+ self.assertEqual(18.05, filtered.state)
def test_throttle(self):
"""Test if lowpass filter works."""
@@ -92,7 +138,7 @@ class TestFilterSensor(unittest.TestCase):
new_state = filt.filter_state(state)
if not filt.skip_processing:
filtered.append(new_state)
- self.assertEqual([20, 21], filtered)
+ self.assertEqual([20, 21], [f.state for f in filtered])
def test_time_sma(self):
"""Test if time_sma filter works."""
@@ -100,9 +146,6 @@ class TestFilterSensor(unittest.TestCase):
precision=2,
entity=None,
type='last')
- past = dt_util.utcnow() - timedelta(minutes=5)
for state in self.values:
- with patch('homeassistant.util.dt.utcnow', return_value=past):
- filtered = filt.filter_state(state)
- past += timedelta(minutes=1)
- self.assertEqual(21.5, filtered)
+ filtered = filt.filter_state(state)
+ self.assertEqual(21.5, filtered.state)
diff --git a/tests/components/sensor/test_mqtt.py b/tests/components/sensor/test_mqtt.py
index b23d89e3057..88e74e11008 100644
--- a/tests/components/sensor/test_mqtt.py
+++ b/tests/components/sensor/test_mqtt.py
@@ -329,3 +329,24 @@ class TestSensorMQTT(unittest.TestCase):
self.assertEqual('100',
state.attributes.get('val'))
self.assertEqual('100', state.state)
+
+ def test_unique_id(self):
+ """Test unique id option only creates one sensor per unique_id."""
+ assert setup_component(self.hass, sensor.DOMAIN, {
+ sensor.DOMAIN: [{
+ 'platform': 'mqtt',
+ 'name': 'Test 1',
+ 'state_topic': 'test-topic',
+ 'unique_id': 'TOTALLY_UNIQUE'
+ }, {
+ 'platform': 'mqtt',
+ 'name': 'Test 2',
+ 'state_topic': 'test-topic',
+ 'unique_id': 'TOTALLY_UNIQUE'
+ }]
+ })
+
+ fire_mqtt_message(self.hass, 'test-topic', 'payload')
+ self.hass.block_till_done()
+
+ assert len(self.hass.states.all()) == 1
diff --git a/tests/components/sensor/test_qwikswitch.py b/tests/components/sensor/test_qwikswitch.py
new file mode 100644
index 00000000000..d9dfe072fc0
--- /dev/null
+++ b/tests/components/sensor/test_qwikswitch.py
@@ -0,0 +1,87 @@
+"""Test qwikswitch sensors."""
+import logging
+
+import pytest
+
+from homeassistant.const import EVENT_HOMEASSISTANT_START
+from homeassistant.components.qwikswitch import DOMAIN as QWIKSWITCH
+from homeassistant.bootstrap import async_setup_component
+from tests.test_util.aiohttp import mock_aiohttp_client
+
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class AiohttpClientMockResponseList(list):
+ """List that fires an event on empty pop, for aiohttp Mocker."""
+
+ def decode(self, _):
+ """Return next item from list."""
+ try:
+ res = list.pop(self)
+ _LOGGER.debug("MockResponseList popped %s: %s", res, self)
+ return res
+ except IndexError:
+ _LOGGER.debug("MockResponseList empty")
+ return ""
+
+ async def wait_till_empty(self, hass):
+ """Wait until empty."""
+ while self:
+ await hass.async_block_till_done()
+ await hass.async_block_till_done()
+
+
+LISTEN = AiohttpClientMockResponseList()
+
+
+@pytest.fixture
+def aioclient_mock():
+ """HTTP client listen and devices."""
+ devices = """[
+ {"id":"@000001","name":"Switch 1","type":"rel","val":"OFF",
+ "time":"1522777506","rssi":"51%"},
+ {"id":"@000002","name":"Light 2","type":"rel","val":"ON",
+ "time":"1522777507","rssi":"45%"},
+ {"id":"@000003","name":"Dim 3","type":"dim","val":"280c00",
+ "time":"1522777544","rssi":"62%"}]"""
+
+ with mock_aiohttp_client() as mock_session:
+ mock_session.get("http://127.0.0.1:2020/&listen", content=LISTEN)
+ mock_session.get("http://127.0.0.1:2020/&device", text=devices)
+ yield mock_session
+
+
+async def test_sensor_device(hass, aioclient_mock):
+ """Test a sensor device."""
+ config = {
+ 'qwikswitch': {
+ 'sensors': {
+ 'name': 's1',
+ 'id': '@a00001',
+ 'channel': 1,
+ 'type': 'imod',
+ }
+ }
+ }
+ 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'
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
+
+ LISTEN.append( # Close
+ """{"id":"@a00001","cmd":"","data":"4e0e1601","rssi":"61%"}""")
+ await hass.async_block_till_done()
+ state_obj = hass.states.get('sensor.s1')
+ assert state_obj.state == 'True'
+
+ # 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'
diff --git a/tests/components/sensor/test_sigfox.py b/tests/components/sensor/test_sigfox.py
new file mode 100644
index 00000000000..dcdeef56b98
--- /dev/null
+++ b/tests/components/sensor/test_sigfox.py
@@ -0,0 +1,68 @@
+"""Tests for the sigfox sensor."""
+import re
+import requests_mock
+import unittest
+
+from homeassistant.components.sensor.sigfox import (
+ API_URL, CONF_API_LOGIN, CONF_API_PASSWORD)
+from homeassistant.setup import setup_component
+from tests.common import get_test_home_assistant
+
+TEST_API_LOGIN = 'foo'
+TEST_API_PASSWORD = 'ebcd1234'
+
+VALID_CONFIG = {
+ 'sensor': {
+ 'platform': 'sigfox',
+ CONF_API_LOGIN: TEST_API_LOGIN,
+ CONF_API_PASSWORD: TEST_API_PASSWORD}}
+
+VALID_MESSAGE = """
+{"data":[{
+"time":1521879720,
+"data":"7061796c6f6164",
+"rinfos":[{"lat":"0.0","lng":"0.0"}],
+"snr":"50.0"}]}
+"""
+
+
+class TestSigfoxSensor(unittest.TestCase):
+ """Test the sigfox platform."""
+
+ def setUp(self):
+ """Initialize values for this testcase class."""
+ self.hass = get_test_home_assistant()
+
+ def tearDown(self):
+ """Stop everything that was started."""
+ self.hass.stop()
+
+ def test_invalid_credentials(self):
+ """Test for a invalid credentials."""
+ with requests_mock.Mocker() as mock_req:
+ url = re.compile(API_URL + 'devicetypes')
+ mock_req.get(url, text='{}', status_code=401)
+ self.assertTrue(
+ setup_component(self.hass, 'sensor', VALID_CONFIG))
+ assert len(self.hass.states.entity_ids()) == 0
+
+ def test_valid_credentials(self):
+ """Test for a valid credentials."""
+ with requests_mock.Mocker() as mock_req:
+ url1 = re.compile(API_URL + 'devicetypes')
+ mock_req.get(url1, text='{"data":[{"id":"fake_type"}]}',
+ status_code=200)
+
+ url2 = re.compile(API_URL + 'devicetypes/fake_type/devices')
+ mock_req.get(url2, text='{"data":[{"id":"fake_id"}]}')
+
+ url3 = re.compile(API_URL + 'devices/fake_id/messages*')
+ mock_req.get(url3, text=VALID_MESSAGE)
+
+ self.assertTrue(
+ setup_component(self.hass, 'sensor', VALID_CONFIG))
+
+ assert len(self.hass.states.entity_ids()) == 1
+ state = self.hass.states.get('sensor.sigfox_fake_id')
+ assert state.state == 'payload'
+ assert state.attributes.get('snr') == '50.0'
diff --git a/tests/components/test_deconz.py b/tests/components/test_deconz.py
deleted file mode 100644
index 2c7c656d560..00000000000
--- a/tests/components/test_deconz.py
+++ /dev/null
@@ -1,97 +0,0 @@
-"""Tests for deCONZ config flow."""
-import pytest
-
-import voluptuous as vol
-
-import homeassistant.components.deconz as deconz
-import pydeconz
-
-
-async def test_flow_works(hass, aioclient_mock):
- """Test config flow."""
- aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[
- {'id': 'id', 'internalipaddress': '1.2.3.4', 'internalport': '80'}
- ])
- aioclient_mock.post('http://1.2.3.4:80/api', json=[
- {"success": {"username": "1234567890ABCDEF"}}
- ])
-
- flow = deconz.DeconzFlowHandler()
- flow.hass = hass
- await flow.async_step_init()
- result = await flow.async_step_link(user_input={})
-
- assert result['type'] == 'create_entry'
- assert result['title'] == 'deCONZ'
- assert result['data'] == {
- 'bridgeid': 'id',
- 'host': '1.2.3.4',
- 'port': '80',
- 'api_key': '1234567890ABCDEF'
- }
-
-
-async def test_flow_already_registered_bridge(hass, aioclient_mock):
- """Test config flow don't allow more than one bridge to be registered."""
- flow = deconz.DeconzFlowHandler()
- flow.hass = hass
- flow.hass.data[deconz.DOMAIN] = True
-
- result = await flow.async_step_init()
- assert result['type'] == 'abort'
-
-
-async def test_flow_no_discovered_bridges(hass, aioclient_mock):
- """Test config flow discovers no bridges."""
- aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[])
- flow = deconz.DeconzFlowHandler()
- flow.hass = hass
-
- result = await flow.async_step_init()
- assert result['type'] == 'abort'
-
-
-async def test_flow_one_bridge_discovered(hass, aioclient_mock):
- """Test config flow discovers one bridge."""
- aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[
- {'id': 'id', 'internalipaddress': '1.2.3.4', 'internalport': '80'}
- ])
- flow = deconz.DeconzFlowHandler()
- flow.hass = hass
-
- result = await flow.async_step_init()
- assert result['type'] == 'form'
- assert result['step_id'] == 'link'
-
-
-async def test_flow_two_bridges_discovered(hass, aioclient_mock):
- """Test config flow discovers two bridges."""
- aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[
- {'id': 'id1', 'internalipaddress': '1.2.3.4', 'internalport': '80'},
- {'id': 'id2', 'internalipaddress': '5.6.7.8', 'internalport': '80'}
- ])
- flow = deconz.DeconzFlowHandler()
- flow.hass = hass
-
- result = await flow.async_step_init()
- assert result['type'] == 'form'
- assert result['step_id'] == 'init'
-
- with pytest.raises(vol.Invalid):
- assert result['data_schema']({'host': '0.0.0.0'})
-
- result['data_schema']({'host': '1.2.3.4'})
- result['data_schema']({'host': '5.6.7.8'})
-
-
-async def test_flow_no_api_key(hass, aioclient_mock):
- """Test config flow discovers no bridges."""
- aioclient_mock.post('http://1.2.3.4:80/api', json=[])
- flow = deconz.DeconzFlowHandler()
- flow.hass = hass
- flow.deconz_config = {'host': '1.2.3.4', 'port': 80}
-
- result = await flow.async_step_link(user_input={})
- assert result['type'] == 'form'
- assert result['step_id'] == 'link'
- assert result['errors'] == {'base': 'no_key'}
diff --git a/tests/components/test_discovery.py b/tests/components/test_discovery.py
index b4c80bf3210..dd22c87cb18 100644
--- a/tests/components/test_discovery.py
+++ b/tests/components/test_discovery.py
@@ -5,7 +5,7 @@ from unittest.mock import patch, MagicMock
import pytest
-from homeassistant import config_entries
+from homeassistant import data_entry_flow
from homeassistant.bootstrap import async_setup_component
from homeassistant.components import discovery
from homeassistant.util.dt import utcnow
@@ -25,7 +25,8 @@ UNKNOWN_SERVICE = 'this_service_will_never_be_supported'
BASE_CONFIG = {
discovery.DOMAIN: {
- 'ignore': []
+ 'ignore': [],
+ 'enable': []
}
}
@@ -168,11 +169,11 @@ async def test_discover_config_flow(hass):
with patch.dict(discovery.CONFIG_ENTRY_HANDLERS, {
'mock-service': 'mock-component'}), patch(
- 'homeassistant.config_entries.FlowManager.async_init') as m_init:
+ 'homeassistant.data_entry_flow.FlowManager.async_init') as m_init:
await mock_discovery(hass, discover)
assert len(m_init.mock_calls) == 1
args, kwargs = m_init.mock_calls[0][1:]
assert args == ('mock-component',)
- assert kwargs['source'] == config_entries.SOURCE_DISCOVERY
+ assert kwargs['source'] == data_entry_flow.SOURCE_DISCOVERY
assert kwargs['data'] == discovery_info
diff --git a/tests/components/test_google.py b/tests/components/test_google.py
index fd45cfc59a9..0ee066fcfee 100644
--- a/tests/components/test_google.py
+++ b/tests/components/test_google.py
@@ -58,6 +58,7 @@ class TestGoogle(unittest.TestCase):
'device_id': 'we_are_we_are_a_test_calendar',
'name': 'We are, we are, a... Test Calendar',
'track': True,
+ 'ignore_availability': True,
}]
})
diff --git a/tests/components/test_history.py b/tests/components/test_history.py
index bea2af396cb..5d909492380 100644
--- a/tests/components/test_history.py
+++ b/tests/components/test_history.py
@@ -131,6 +131,39 @@ class TestComponentHistory(unittest.TestCase):
self.assertEqual(states, hist[entity_id])
+ def test_get_last_state_changes(self):
+ """Test number of state changes."""
+ self.init_recorder()
+ entity_id = 'sensor.test'
+
+ def set_state(state):
+ """Set the state."""
+ self.hass.states.set(entity_id, state)
+ self.wait_recording_done()
+ return self.hass.states.get(entity_id)
+
+ start = dt_util.utcnow() - timedelta(minutes=2)
+ point = start + timedelta(minutes=1)
+ point2 = point + timedelta(minutes=1)
+
+ with patch('homeassistant.components.recorder.dt_util.utcnow',
+ return_value=start):
+ set_state('1')
+
+ states = []
+ with patch('homeassistant.components.recorder.dt_util.utcnow',
+ return_value=point):
+ states.append(set_state('2'))
+
+ with patch('homeassistant.components.recorder.dt_util.utcnow',
+ return_value=point2):
+ states.append(set_state('3'))
+
+ hist = history.get_last_state_changes(
+ self.hass, 2, entity_id)
+
+ self.assertEqual(states, hist[entity_id])
+
def test_get_significant_states(self):
"""Test that only significant states are returned.
diff --git a/tests/components/test_snips.py b/tests/components/test_snips.py
index f37beef7960..2342e897708 100644
--- a/tests/components/test_snips.py
+++ b/tests/components/test_snips.py
@@ -1,20 +1,92 @@
"""Test the Snips component."""
-import asyncio
import json
import logging
-from homeassistant.core import callback
from homeassistant.bootstrap import async_setup_component
+from homeassistant.components.mqtt import MQTT_PUBLISH_SCHEMA
+import homeassistant.components.snips as snips
from tests.common import (async_fire_mqtt_message, async_mock_intent,
async_mock_service)
-from homeassistant.components.snips import (SERVICE_SCHEMA_SAY,
- SERVICE_SCHEMA_SAY_ACTION)
-@asyncio.coroutine
-def test_snips_intent(hass, mqtt_mock):
+async def test_snips_config(hass, mqtt_mock):
+ """Test Snips Config."""
+ result = await async_setup_component(hass, "snips", {
+ "snips": {
+ "feedback_sounds": True,
+ "probability_threshold": .5,
+ "site_ids": ["default", "remote"]
+ },
+ })
+ assert result
+
+
+async def test_snips_bad_config(hass, mqtt_mock):
+ """Test Snips bad config."""
+ result = await async_setup_component(hass, "snips", {
+ "snips": {
+ "feedback_sounds": "on",
+ "probability": "none",
+ "site_ids": "default"
+ },
+ })
+ assert not result
+
+
+async def test_snips_config_feedback_on(hass, mqtt_mock):
+ """Test Snips Config."""
+ calls = async_mock_service(hass, 'mqtt', 'publish', MQTT_PUBLISH_SCHEMA)
+ result = await async_setup_component(hass, "snips", {
+ "snips": {
+ "feedback_sounds": True
+ },
+ })
+ assert result
+ await hass.async_block_till_done()
+
+ assert len(calls) == 2
+ topic = calls[0].data['topic']
+ assert topic == 'hermes/feedback/sound/toggleOn'
+ topic = calls[1].data['topic']
+ assert topic == 'hermes/feedback/sound/toggleOn'
+ assert calls[1].data['qos'] == 1
+ assert calls[1].data['retain']
+
+
+async def test_snips_config_feedback_off(hass, mqtt_mock):
+ """Test Snips Config."""
+ calls = async_mock_service(hass, 'mqtt', 'publish', MQTT_PUBLISH_SCHEMA)
+ result = await async_setup_component(hass, "snips", {
+ "snips": {
+ "feedback_sounds": False
+ },
+ })
+ assert result
+ await hass.async_block_till_done()
+
+ assert len(calls) == 2
+ topic = calls[0].data['topic']
+ assert topic == 'hermes/feedback/sound/toggleOn'
+ topic = calls[1].data['topic']
+ assert topic == 'hermes/feedback/sound/toggleOff'
+ assert calls[1].data['qos'] == 0
+ assert not calls[1].data['retain']
+
+
+async def test_snips_config_no_feedback(hass, mqtt_mock):
+ """Test Snips Config."""
+ calls = async_mock_service(hass, 'snips', 'say')
+ result = await async_setup_component(hass, "snips", {
+ "snips": {},
+ })
+ assert result
+ await hass.async_block_till_done()
+ assert len(calls) == 0
+
+
+async def test_snips_intent(hass, mqtt_mock):
"""Test intent via Snips."""
- result = yield from async_setup_component(hass, "snips", {
+ result = await async_setup_component(hass, "snips", {
"snips": {},
})
assert result
@@ -41,7 +113,7 @@ def test_snips_intent(hass, mqtt_mock):
async_fire_mqtt_message(hass, 'hermes/intent/Lights',
payload)
- yield from hass.async_block_till_done()
+ await hass.async_block_till_done()
assert len(intents) == 1
intent = intents[0]
assert intent.platform == 'snips'
@@ -50,10 +122,9 @@ def test_snips_intent(hass, mqtt_mock):
assert intent.text_input == 'turn the lights green'
-@asyncio.coroutine
-def test_snips_intent_with_duration(hass, mqtt_mock):
+async def test_snips_intent_with_duration(hass, mqtt_mock):
"""Test intent with Snips duration."""
- result = yield from async_setup_component(hass, "snips", {
+ result = await async_setup_component(hass, "snips", {
"snips": {},
})
assert result
@@ -61,7 +132,8 @@ def test_snips_intent_with_duration(hass, mqtt_mock):
{
"input": "set a timer of five minutes",
"intent": {
- "intentName": "SetTimer"
+ "intentName": "SetTimer",
+ "probability": 1
},
"slots": [
{
@@ -92,7 +164,7 @@ def test_snips_intent_with_duration(hass, mqtt_mock):
async_fire_mqtt_message(hass, 'hermes/intent/SetTimer',
payload)
- yield from hass.async_block_till_done()
+ await hass.async_block_till_done()
assert len(intents) == 1
intent = intents[0]
assert intent.platform == 'snips'
@@ -100,22 +172,14 @@ def test_snips_intent_with_duration(hass, mqtt_mock):
assert intent.slots == {'timer_duration': {'value': 300}}
-@asyncio.coroutine
-def test_intent_speech_response(hass, mqtt_mock):
+async def test_intent_speech_response(hass, mqtt_mock):
"""Test intent speech response via Snips."""
- event = 'call_service'
- events = []
-
- @callback
- def record_event(event):
- """Add recorded event to set."""
- events.append(event)
-
- result = yield from async_setup_component(hass, "snips", {
+ calls = async_mock_service(hass, 'mqtt', 'publish', MQTT_PUBLISH_SCHEMA)
+ result = await async_setup_component(hass, "snips", {
"snips": {},
})
assert result
- result = yield from async_setup_component(hass, "intent_script", {
+ result = await async_setup_component(hass, "intent_script", {
"intent_script": {
"spokenIntent": {
"speech": {
@@ -131,31 +195,28 @@ def test_intent_speech_response(hass, mqtt_mock):
"input": "speak to me",
"sessionId": "abcdef0123456789",
"intent": {
- "intentName": "spokenIntent"
+ "intentName": "spokenIntent",
+ "probability": 1
},
"slots": []
}
"""
- hass.bus.async_listen(event, record_event)
async_fire_mqtt_message(hass, 'hermes/intent/spokenIntent',
payload)
- yield from hass.async_block_till_done()
+ await hass.async_block_till_done()
- assert len(events) == 1
- assert events[0].data['domain'] == 'mqtt'
- assert events[0].data['service'] == 'publish'
- payload = json.loads(events[0].data['service_data']['payload'])
- topic = events[0].data['service_data']['topic']
+ assert len(calls) == 1
+ payload = json.loads(calls[0].data['payload'])
+ topic = calls[0].data['topic']
assert payload['sessionId'] == 'abcdef0123456789'
assert payload['text'] == 'I am speaking to you'
assert topic == 'hermes/dialogueManager/endSession'
-@asyncio.coroutine
-def test_unknown_intent(hass, mqtt_mock, caplog):
+async def test_unknown_intent(hass, mqtt_mock, caplog):
"""Test unknown intent."""
caplog.set_level(logging.WARNING)
- result = yield from async_setup_component(hass, "snips", {
+ result = await async_setup_component(hass, "snips", {
"snips": {},
})
assert result
@@ -164,21 +225,21 @@ def test_unknown_intent(hass, mqtt_mock, caplog):
"input": "I don't know what I am supposed to do",
"sessionId": "abcdef1234567890",
"intent": {
- "intentName": "unknownIntent"
+ "intentName": "unknownIntent",
+ "probability": 1
},
"slots": []
}
"""
async_fire_mqtt_message(hass,
'hermes/intent/unknownIntent', payload)
- yield from hass.async_block_till_done()
+ await hass.async_block_till_done()
assert 'Received unknown intent unknownIntent' in caplog.text
-@asyncio.coroutine
-def test_snips_intent_user(hass, mqtt_mock):
+async def test_snips_intent_user(hass, mqtt_mock):
"""Test intentName format user_XXX__intentName."""
- result = yield from async_setup_component(hass, "snips", {
+ result = await async_setup_component(hass, "snips", {
"snips": {},
})
assert result
@@ -186,7 +247,8 @@ def test_snips_intent_user(hass, mqtt_mock):
{
"input": "what to do",
"intent": {
- "intentName": "user_ABCDEF123__Lights"
+ "intentName": "user_ABCDEF123__Lights",
+ "probability": 1
},
"slots": []
}
@@ -194,7 +256,7 @@ def test_snips_intent_user(hass, mqtt_mock):
intents = async_mock_intent(hass, 'Lights')
async_fire_mqtt_message(hass, 'hermes/intent/user_ABCDEF123__Lights',
payload)
- yield from hass.async_block_till_done()
+ await hass.async_block_till_done()
assert len(intents) == 1
intent = intents[0]
@@ -202,10 +264,9 @@ def test_snips_intent_user(hass, mqtt_mock):
assert intent.intent_type == 'Lights'
-@asyncio.coroutine
-def test_snips_intent_username(hass, mqtt_mock):
+async def test_snips_intent_username(hass, mqtt_mock):
"""Test intentName format username:intentName."""
- result = yield from async_setup_component(hass, "snips", {
+ result = await async_setup_component(hass, "snips", {
"snips": {},
})
assert result
@@ -213,7 +274,8 @@ def test_snips_intent_username(hass, mqtt_mock):
{
"input": "what to do",
"intent": {
- "intentName": "username:Lights"
+ "intentName": "username:Lights",
+ "probability": 1
},
"slots": []
}
@@ -221,7 +283,7 @@ def test_snips_intent_username(hass, mqtt_mock):
intents = async_mock_intent(hass, 'Lights')
async_fire_mqtt_message(hass, 'hermes/intent/username:Lights',
payload)
- yield from hass.async_block_till_done()
+ await hass.async_block_till_done()
assert len(intents) == 1
intent = intents[0]
@@ -229,15 +291,41 @@ def test_snips_intent_username(hass, mqtt_mock):
assert intent.intent_type == 'Lights'
-@asyncio.coroutine
-def test_snips_say(hass, caplog):
+async def test_snips_low_probability(hass, mqtt_mock, caplog):
+ """Test intent via Snips."""
+ caplog.set_level(logging.WARNING)
+ result = await async_setup_component(hass, "snips", {
+ "snips": {
+ "probability_threshold": 0.5
+ },
+ })
+ assert result
+ payload = """
+ {
+ "input": "I am not sure what to say",
+ "intent": {
+ "intentName": "LightsMaybe",
+ "probability": 0.49
+ },
+ "slots": []
+ }
+ """
+
+ async_mock_intent(hass, 'LightsMaybe')
+ async_fire_mqtt_message(hass, 'hermes/intent/LightsMaybe',
+ payload)
+ await hass.async_block_till_done()
+ assert 'Intent below probaility threshold 0.49 < 0.5' in caplog.text
+
+
+async def test_snips_say(hass, caplog):
"""Test snips say with invalid config."""
calls = async_mock_service(hass, 'snips', 'say',
- SERVICE_SCHEMA_SAY)
+ snips.SERVICE_SCHEMA_SAY)
data = {'text': 'Hello'}
- yield from hass.services.async_call('snips', 'say', data)
- yield from hass.async_block_till_done()
+ await hass.services.async_call('snips', 'say', data)
+ await hass.async_block_till_done()
assert len(calls) == 1
assert calls[0].domain == 'snips'
@@ -245,15 +333,14 @@ def test_snips_say(hass, caplog):
assert calls[0].data['text'] == 'Hello'
-@asyncio.coroutine
-def test_snips_say_action(hass, caplog):
+async def test_snips_say_action(hass, caplog):
"""Test snips say_action with invalid config."""
calls = async_mock_service(hass, 'snips', 'say_action',
- SERVICE_SCHEMA_SAY_ACTION)
+ snips.SERVICE_SCHEMA_SAY_ACTION)
data = {'text': 'Hello', 'intent_filter': ['myIntent']}
- yield from hass.services.async_call('snips', 'say_action', data)
- yield from hass.async_block_till_done()
+ await hass.services.async_call('snips', 'say_action', data)
+ await hass.async_block_till_done()
assert len(calls) == 1
assert calls[0].domain == 'snips'
@@ -262,31 +349,71 @@ def test_snips_say_action(hass, caplog):
assert calls[0].data['intent_filter'] == ['myIntent']
-@asyncio.coroutine
-def test_snips_say_invalid_config(hass, caplog):
+async def test_snips_say_invalid_config(hass, caplog):
"""Test snips say with invalid config."""
calls = async_mock_service(hass, 'snips', 'say',
- SERVICE_SCHEMA_SAY)
+ snips.SERVICE_SCHEMA_SAY)
data = {'text': 'Hello', 'badKey': 'boo'}
- yield from hass.services.async_call('snips', 'say', data)
- yield from hass.async_block_till_done()
+ await hass.services.async_call('snips', 'say', data)
+ await hass.async_block_till_done()
assert len(calls) == 0
assert 'ERROR' in caplog.text
assert 'Invalid service data' in caplog.text
-@asyncio.coroutine
-def test_snips_say_action_invalid_config(hass, caplog):
+async def test_snips_say_action_invalid(hass, caplog):
"""Test snips say_action with invalid config."""
calls = async_mock_service(hass, 'snips', 'say_action',
- SERVICE_SCHEMA_SAY_ACTION)
+ snips.SERVICE_SCHEMA_SAY_ACTION)
data = {'text': 'Hello', 'can_be_enqueued': 'notabool'}
- yield from hass.services.async_call('snips', 'say_action', data)
- yield from hass.async_block_till_done()
+ await hass.services.async_call('snips', 'say_action', data)
+ await hass.async_block_till_done()
assert len(calls) == 0
assert 'ERROR' in caplog.text
assert 'Invalid service data' in caplog.text
+
+
+async def test_snips_feedback_on(hass, caplog):
+ """Test snips say with invalid config."""
+ calls = async_mock_service(hass, 'snips', 'feedback_on',
+ snips.SERVICE_SCHEMA_FEEDBACK)
+
+ data = {'site_id': 'remote'}
+ await hass.services.async_call('snips', 'feedback_on', data)
+ await hass.async_block_till_done()
+
+ assert len(calls) == 1
+ assert calls[0].domain == 'snips'
+ assert calls[0].service == 'feedback_on'
+ assert calls[0].data['site_id'] == 'remote'
+
+
+async def test_snips_feedback_off(hass, caplog):
+ """Test snips say with invalid config."""
+ calls = async_mock_service(hass, 'snips', 'feedback_off',
+ snips.SERVICE_SCHEMA_FEEDBACK)
+
+ data = {'site_id': 'remote'}
+ await hass.services.async_call('snips', 'feedback_off', data)
+ await hass.async_block_till_done()
+
+ assert len(calls) == 1
+ assert calls[0].domain == 'snips'
+ assert calls[0].service == 'feedback_off'
+ assert calls[0].data['site_id'] == 'remote'
+
+
+async def test_snips_feedback_config(hass, caplog):
+ """Test snips say with invalid config."""
+ calls = async_mock_service(hass, 'snips', 'feedback_on',
+ snips.SERVICE_SCHEMA_FEEDBACK)
+
+ data = {'site_id': 'remote', 'test': 'test'}
+ await hass.services.async_call('snips', 'feedback_on', data)
+ await hass.async_block_till_done()
+
+ assert len(calls) == 0
diff --git a/tests/components/test_upnp.py b/tests/components/test_upnp.py
index e2096d28e58..4956b8a6278 100644
--- a/tests/components/test_upnp.py
+++ b/tests/components/test_upnp.py
@@ -1,5 +1,4 @@
"""Test the UPNP component."""
-import asyncio
from collections import OrderedDict
from unittest.mock import patch, MagicMock
@@ -7,15 +6,64 @@ import pytest
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.setup import async_setup_component
+from homeassistant.components.upnp import IP_SERVICE, DATA_UPNP
+
+
+class MockService(MagicMock):
+ """Mock upnp IP service."""
+
+ async def add_port_mapping(self, *args, **kwargs):
+ """Original function."""
+ self.mock_add_port_mapping(*args, **kwargs)
+
+ async def delete_port_mapping(self, *args, **kwargs):
+ """Original function."""
+ self.mock_delete_port_mapping(*args, **kwargs)
+
+
+class MockDevice(MagicMock):
+ """Mock upnp device."""
+
+ def find_first_service(self, *args, **kwargs):
+ """Original function."""
+ self._service = MockService()
+ return self._service
+
+ def peep_first_service(self):
+ """Access Mock first service."""
+ return self._service
+
+
+class MockResp(MagicMock):
+ """Mock upnp msearch response."""
+
+ async def get_device(self, *args, **kwargs):
+ """Original function."""
+ device = MockDevice()
+ service = {'serviceType': IP_SERVICE}
+ device.services = [service]
+ return device
@pytest.fixture
-def mock_miniupnpc():
- """Mock miniupnpc."""
- mock = MagicMock()
+def mock_msearch_first(*args, **kwargs):
+ """Wrapper to async mock function."""
+ async def async_mock_msearch_first(*args, **kwargs):
+ """Mock msearch_first."""
+ return MockResp(*args, **kwargs)
- with patch.dict('sys.modules', {'miniupnpc': mock}):
- yield mock.UPnP()
+ with patch('pyupnp_async.msearch_first', new=async_mock_msearch_first):
+ yield
+
+
+@pytest.fixture
+def mock_async_exception(*args, **kwargs):
+ """Wrapper to async mock function with exception."""
+ async def async_mock_exception(*args, **kwargs):
+ return Exception
+
+ with patch('pyupnp_async.msearch_first', new=async_mock_exception):
+ yield
@pytest.fixture
@@ -26,75 +74,66 @@ def mock_local_ip():
yield
-@pytest.fixture(autouse=True)
-def mock_discovery():
- """Mock discovery of upnp sensor."""
- with patch('homeassistant.components.upnp.discovery'):
- yield
-
-
-@asyncio.coroutine
-def test_setup_fail_if_no_ip(hass):
+async def test_setup_fail_if_no_ip(hass):
"""Test setup fails if we can't find a local IP."""
with patch('homeassistant.components.upnp.get_local_ip',
return_value='127.0.0.1'):
- result = yield from async_setup_component(hass, 'upnp', {
+ result = await async_setup_component(hass, 'upnp', {
'upnp': {}
})
assert not result
-@asyncio.coroutine
-def test_setup_fail_if_cannot_select_igd(hass, mock_local_ip, mock_miniupnpc):
+async def test_setup_fail_if_cannot_select_igd(hass,
+ mock_local_ip,
+ mock_async_exception):
"""Test setup fails if we can't find an UPnP IGD."""
- mock_miniupnpc.selectigd.side_effect = Exception
-
- result = yield from async_setup_component(hass, 'upnp', {
+ result = await async_setup_component(hass, 'upnp', {
'upnp': {}
})
assert not result
-@asyncio.coroutine
-def test_setup_succeeds_if_specify_ip(hass, mock_miniupnpc):
+async def test_setup_succeeds_if_specify_ip(hass, mock_msearch_first):
"""Test setup succeeds if we specify IP and can't find a local IP."""
with patch('homeassistant.components.upnp.get_local_ip',
return_value='127.0.0.1'):
- result = yield from async_setup_component(hass, 'upnp', {
+ result = await async_setup_component(hass, 'upnp', {
'upnp': {
'local_ip': '192.168.0.10'
}
})
assert result
+ mock_service = hass.data[DATA_UPNP].peep_first_service()
+ assert len(mock_service.mock_add_port_mapping.mock_calls) == 1
+ mock_service.mock_add_port_mapping.assert_called_once_with(
+ 8123, 8123, '192.168.0.10', 'TCP', desc='Home Assistant')
-@asyncio.coroutine
-def test_no_config_maps_hass_local_to_remote_port(hass, mock_miniupnpc):
+async def test_no_config_maps_hass_local_to_remote_port(hass,
+ mock_local_ip,
+ mock_msearch_first):
"""Test by default we map local to remote port."""
- result = yield from async_setup_component(hass, 'upnp', {
- 'upnp': {
- 'local_ip': '192.168.0.10'
- }
+ result = await async_setup_component(hass, 'upnp', {
+ 'upnp': {}
})
assert result
- assert len(mock_miniupnpc.addportmapping.mock_calls) == 1
- external, _, host, internal, _, _ = \
- mock_miniupnpc.addportmapping.mock_calls[0][1]
- assert host == '192.168.0.10'
- assert external == 8123
- assert internal == 8123
+ mock_service = hass.data[DATA_UPNP].peep_first_service()
+ assert len(mock_service.mock_add_port_mapping.mock_calls) == 1
+ mock_service.mock_add_port_mapping.assert_called_once_with(
+ 8123, 8123, '192.168.0.10', 'TCP', desc='Home Assistant')
-@asyncio.coroutine
-def test_map_hass_to_remote_port(hass, mock_miniupnpc):
+async def test_map_hass_to_remote_port(hass,
+ mock_local_ip,
+ mock_msearch_first):
"""Test mapping hass to remote port."""
- result = yield from async_setup_component(hass, 'upnp', {
+ result = await async_setup_component(hass, 'upnp', {
'upnp': {
- 'local_ip': '192.168.0.10',
'ports': {
'hass': 1000
}
@@ -102,41 +141,38 @@ def test_map_hass_to_remote_port(hass, mock_miniupnpc):
})
assert result
- assert len(mock_miniupnpc.addportmapping.mock_calls) == 1
- external, _, host, internal, _, _ = \
- mock_miniupnpc.addportmapping.mock_calls[0][1]
- assert external == 1000
- assert internal == 8123
+ mock_service = hass.data[DATA_UPNP].peep_first_service()
+ assert len(mock_service.mock_add_port_mapping.mock_calls) == 1
+ mock_service.mock_add_port_mapping.assert_called_once_with(
+ 8123, 1000, '192.168.0.10', 'TCP', desc='Home Assistant')
-@asyncio.coroutine
-def test_map_internal_to_remote_ports(hass, mock_miniupnpc):
+async def test_map_internal_to_remote_ports(hass,
+ mock_local_ip,
+ mock_msearch_first):
"""Test mapping local to remote ports."""
ports = OrderedDict()
ports['hass'] = 1000
ports[1883] = 3883
- result = yield from async_setup_component(hass, 'upnp', {
+ result = await async_setup_component(hass, 'upnp', {
'upnp': {
- 'local_ip': '192.168.0.10',
'ports': ports
}
})
assert result
- assert len(mock_miniupnpc.addportmapping.mock_calls) == 2
- external, _, host, internal, _, _ = \
- mock_miniupnpc.addportmapping.mock_calls[0][1]
- assert external == 1000
- assert internal == 8123
+ mock_service = hass.data[DATA_UPNP].peep_first_service()
+ assert len(mock_service.mock_add_port_mapping.mock_calls) == 2
- external, _, host, internal, _, _ = \
- mock_miniupnpc.addportmapping.mock_calls[1][1]
- assert external == 3883
- assert internal == 1883
+ mock_service.mock_add_port_mapping.assert_any_call(
+ 8123, 1000, '192.168.0.10', 'TCP', desc='Home Assistant')
+ mock_service.mock_add_port_mapping.assert_any_call(
+ 1883, 3883, '192.168.0.10', 'TCP', desc='Home Assistant')
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
- yield from hass.async_block_till_done()
- assert len(mock_miniupnpc.deleteportmapping.mock_calls) == 2
- assert mock_miniupnpc.deleteportmapping.mock_calls[0][1][0] == 1000
- assert mock_miniupnpc.deleteportmapping.mock_calls[1][1][0] == 3883
+ await hass.async_block_till_done()
+ assert len(mock_service.mock_delete_port_mapping.mock_calls) == 2
+
+ mock_service.mock_delete_port_mapping.assert_any_call(1000, 'TCP')
+ mock_service.mock_delete_port_mapping.assert_any_call(3883, 'TCP')
diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py
index 7a15ed28f97..b6bfa430fd2 100644
--- a/tests/components/tts/test_init.py
+++ b/tests/components/tts/test_init.py
@@ -2,6 +2,7 @@
import ctypes
import os
import shutil
+import json
from unittest.mock import patch, PropertyMock
import pytest
@@ -353,7 +354,7 @@ class TestTTS(object):
demo_data = tts.SpeechManager.write_tags(
"265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3",
demo_data, self.demo_provider,
- "I person is on front of your door.", 'en', None)
+ "AI person is in front of your door.", 'en', None)
assert req.status_code == 200
assert req.content == demo_data
@@ -562,3 +563,46 @@ class TestTTS(object):
req = requests.get(url)
assert req.status_code == 200
assert req.content == demo_data
+
+ def test_setup_component_and_web_get_url(self):
+ """Setup the demo platform and receive wrong file from web."""
+ config = {
+ tts.DOMAIN: {
+ 'platform': 'demo',
+ }
+ }
+
+ with assert_setup_component(1, tts.DOMAIN):
+ setup_component(self.hass, tts.DOMAIN, config)
+
+ self.hass.start()
+
+ url = ("{}/api/tts_get_url").format(self.hass.config.api.base_url)
+ data = {'platform': 'demo',
+ 'message': "I person is on front of your door."}
+
+ req = requests.post(url, data=json.dumps(data))
+ assert req.status_code == 200
+ response = json.loads(req.text)
+ assert response.get('url') == (("{}/api/tts_proxy/265944c108cbb00b2a62"
+ "1be5930513e03a0bb2cd_en_-_demo.mp3")
+ .format(self.hass.config.api.base_url))
+
+ def test_setup_component_and_web_get_url_bad_config(self):
+ """Setup the demo platform and receive wrong file from web."""
+ config = {
+ tts.DOMAIN: {
+ 'platform': 'demo',
+ }
+ }
+
+ with assert_setup_component(1, tts.DOMAIN):
+ setup_component(self.hass, tts.DOMAIN, config)
+
+ self.hass.start()
+
+ url = ("{}/api/tts_get_url").format(self.hass.config.api.base_url)
+ data = {'message': "I person is on front of your door."}
+
+ req = requests.post(url, data=data)
+ assert req.status_code == 400
diff --git a/tests/components/vacuum/test_dyson.py b/tests/components/vacuum/test_dyson.py
index 186a2271a73..8a4e6d57b91 100644
--- a/tests/components/vacuum/test_dyson.py
+++ b/tests/components/vacuum/test_dyson.py
@@ -118,7 +118,6 @@ class DysonTest(unittest.TestCase):
component3 = Dyson360EyeDevice(device3)
self.assertEqual(component.name, "Device_Vacuum")
self.assertTrue(component.is_on)
- self.assertEqual(component.icon, "mdi:roomba")
self.assertEqual(component.status, "Cleaning")
self.assertEqual(component2.status, "Unknown")
self.assertEqual(component.battery_level, 85)
diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py
index d8dac11f6a0..0bc6a7601dc 100644
--- a/tests/helpers/test_entity_component.py
+++ b/tests/helpers/test_entity_component.py
@@ -7,6 +7,8 @@ import unittest
from unittest.mock import patch, Mock
from datetime import timedelta
+import pytest
+
import homeassistant.core as ha
import homeassistant.loader as loader
from homeassistant.exceptions import PlatformNotReady
@@ -19,7 +21,7 @@ import homeassistant.util.dt as dt_util
from tests.common import (
get_test_home_assistant, MockPlatform, MockModule, mock_coro,
- async_fire_time_changed, MockEntity)
+ async_fire_time_changed, MockEntity, MockConfigEntry)
_LOGGER = logging.getLogger(__name__)
DOMAIN = "test_domain"
@@ -333,3 +335,75 @@ def test_setup_dependencies_platform(hass):
assert 'test_component' in hass.config.components
assert 'test_component2' in hass.config.components
assert 'test_domain.test_component' in hass.config.components
+
+
+async def test_setup_entry(hass):
+ """Test setup entry calls async_setup_entry on platform."""
+ mock_setup_entry = Mock(return_value=mock_coro(True))
+ loader.set_component(
+ 'test_domain.entry_domain',
+ MockPlatform(async_setup_entry=mock_setup_entry))
+
+ component = EntityComponent(_LOGGER, DOMAIN, hass)
+ entry = MockConfigEntry(domain='entry_domain')
+
+ assert await component.async_setup_entry(entry)
+ assert len(mock_setup_entry.mock_calls) == 1
+ p_hass, p_entry, p_add_entities = mock_setup_entry.mock_calls[0][1]
+ assert p_hass is hass
+ assert p_entry is entry
+
+
+async def test_setup_entry_platform_not_exist(hass):
+ """Test setup entry fails if platform doesnt exist."""
+ component = EntityComponent(_LOGGER, DOMAIN, hass)
+ entry = MockConfigEntry(domain='non_existing')
+
+ assert (await component.async_setup_entry(entry)) is False
+
+
+async def test_setup_entry_fails_duplicate(hass):
+ """Test we don't allow setting up a config entry twice."""
+ mock_setup_entry = Mock(return_value=mock_coro(True))
+ loader.set_component(
+ 'test_domain.entry_domain',
+ MockPlatform(async_setup_entry=mock_setup_entry))
+
+ component = EntityComponent(_LOGGER, DOMAIN, hass)
+ entry = MockConfigEntry(domain='entry_domain')
+
+ assert await component.async_setup_entry(entry)
+
+ with pytest.raises(ValueError):
+ await component.async_setup_entry(entry)
+
+
+async def test_unload_entry_resets_platform(hass):
+ """Test unloading an entry removes all entities."""
+ mock_setup_entry = Mock(return_value=mock_coro(True))
+ loader.set_component(
+ 'test_domain.entry_domain',
+ MockPlatform(async_setup_entry=mock_setup_entry))
+
+ component = EntityComponent(_LOGGER, DOMAIN, hass)
+ entry = MockConfigEntry(domain='entry_domain')
+
+ assert await component.async_setup_entry(entry)
+ assert len(mock_setup_entry.mock_calls) == 1
+ add_entities = mock_setup_entry.mock_calls[0][1][2]
+ add_entities([MockEntity()])
+ await hass.async_block_till_done()
+
+ assert len(hass.states.async_entity_ids()) == 1
+
+ assert await component.async_unload_entry(entry)
+ assert len(hass.states.async_entity_ids()) == 0
+
+
+async def test_unload_entry_fails_if_never_loaded(hass):
+ """."""
+ component = EntityComponent(_LOGGER, DOMAIN, hass)
+ entry = MockConfigEntry(domain='entry_domain')
+
+ with pytest.raises(ValueError):
+ await component.async_unload_entry(entry)
diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py
index 8c085e4abb1..2018cb27541 100644
--- a/tests/helpers/test_entity_platform.py
+++ b/tests/helpers/test_entity_platform.py
@@ -5,6 +5,7 @@ import unittest
from unittest.mock import patch, Mock, MagicMock
from datetime import timedelta
+from homeassistant.exceptions import PlatformNotReady
import homeassistant.loader as loader
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.entity_component import (
@@ -15,7 +16,7 @@ import homeassistant.util.dt as dt_util
from tests.common import (
get_test_home_assistant, MockPlatform, fire_time_changed, mock_registry,
- MockEntity, MockEntityPlatform)
+ MockEntity, MockEntityPlatform, MockConfigEntry, mock_coro)
_LOGGER = logging.getLogger(__name__)
DOMAIN = "test_domain"
@@ -511,3 +512,72 @@ async def test_entity_registry_updates(hass):
state = hass.states.get('test_domain.world')
assert state.name == 'after update'
+
+
+async def test_setup_entry(hass):
+ """Test we can setup an entry."""
+ async_setup_entry = Mock(return_value=mock_coro(True))
+ platform = MockPlatform(
+ async_setup_entry=async_setup_entry
+ )
+ config_entry = MockConfigEntry()
+ entity_platform = MockEntityPlatform(
+ hass,
+ platform_name=config_entry.domain,
+ platform=platform
+ )
+
+ assert await entity_platform.async_setup_entry(config_entry)
+
+ full_name = '{}.{}'.format(entity_platform.domain, config_entry.domain)
+ assert full_name in hass.config.components
+ assert len(async_setup_entry.mock_calls) == 1
+
+
+async def test_setup_entry_platform_not_ready(hass, caplog):
+ """Test when an entry is not ready yet."""
+ async_setup_entry = Mock(side_effect=PlatformNotReady)
+ platform = MockPlatform(
+ async_setup_entry=async_setup_entry
+ )
+ config_entry = MockConfigEntry()
+ ent_platform = MockEntityPlatform(
+ hass,
+ platform_name=config_entry.domain,
+ platform=platform
+ )
+
+ with patch.object(entity_platform, 'async_call_later') as mock_call_later:
+ assert not await ent_platform.async_setup_entry(config_entry)
+
+ full_name = '{}.{}'.format(ent_platform.domain, config_entry.domain)
+ assert full_name not in hass.config.components
+ assert len(async_setup_entry.mock_calls) == 1
+ assert 'Platform test not ready yet' in caplog.text
+ assert len(mock_call_later.mock_calls) == 1
+
+
+async def test_reset_cancels_retry_setup(hass):
+ """Test that resetting a platform will cancel scheduled a setup retry."""
+ async_setup_entry = Mock(side_effect=PlatformNotReady)
+ platform = MockPlatform(
+ async_setup_entry=async_setup_entry
+ )
+ config_entry = MockConfigEntry()
+ ent_platform = MockEntityPlatform(
+ hass,
+ platform_name=config_entry.domain,
+ platform=platform
+ )
+
+ with patch.object(entity_platform, 'async_call_later') as mock_call_later:
+ assert not await ent_platform.async_setup_entry(config_entry)
+
+ assert len(mock_call_later.mock_calls) == 1
+ assert len(mock_call_later.return_value.mock_calls) == 0
+ assert ent_platform._async_cancel_retry_setup is not None
+
+ await ent_platform.async_reset()
+
+ assert len(mock_call_later.return_value.mock_calls) == 1
+ assert ent_platform._async_cancel_retry_setup is None
diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py
index 650b98509d0..2dfcb2a58e5 100644
--- a/tests/helpers/test_template.py
+++ b/tests/helpers/test_template.py
@@ -149,6 +149,74 @@ class TestHelpersTemplate(unittest.TestCase):
'{{ log(%s, %s) | round(1) }}' % (value, base),
self.hass).render())
+ def test_sine(self):
+ """Test sine."""
+ tests = [
+ (0, '0.0'),
+ (math.pi / 2, '1.0'),
+ (math.pi, '0.0'),
+ (math.pi * 1.5, '-1.0'),
+ (math.pi / 10, '0.309')
+ ]
+
+ for value, expected in tests:
+ self.assertEqual(
+ expected,
+ template.Template(
+ '{{ %s | sin | round(3) }}' % value,
+ self.hass).render())
+
+ def test_cos(self):
+ """Test cosine."""
+ tests = [
+ (0, '1.0'),
+ (math.pi / 2, '0.0'),
+ (math.pi, '-1.0'),
+ (math.pi * 1.5, '-0.0'),
+ (math.pi / 10, '0.951')
+ ]
+
+ for value, expected in tests:
+ self.assertEqual(
+ expected,
+ template.Template(
+ '{{ %s | cos | round(3) }}' % value,
+ self.hass).render())
+
+ def test_tan(self):
+ """Test tangent."""
+ tests = [
+ (0, '0.0'),
+ (math.pi, '-0.0'),
+ (math.pi / 180 * 45, '1.0'),
+ (math.pi / 180 * 90, '1.633123935319537e+16'),
+ (math.pi / 180 * 135, '-1.0')
+ ]
+
+ for value, expected in tests:
+ self.assertEqual(
+ expected,
+ template.Template(
+ '{{ %s | tan | round(3) }}' % value,
+ self.hass).render())
+
+ def test_sqrt(self):
+ """Test square root."""
+ tests = [
+ (0, '0.0'),
+ (1, '1.0'),
+ (2, '1.414'),
+ (10, '3.162'),
+ (100, '10.0'),
+ ]
+
+ for value, expected in tests:
+ self.assertEqual(
+ expected,
+ template.Template(
+ '{{ %s | sqrt | round(3) }}' % value,
+ self.hass).render())
+
def test_strptime(self):
"""Test the parse timestamp method."""
tests = [
diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py
index 5b1ec3b8ec0..94b1dcb47da 100644
--- a/tests/test_config_entries.py
+++ b/tests/test_config_entries.py
@@ -3,9 +3,8 @@ import asyncio
from unittest.mock import MagicMock, patch, mock_open
import pytest
-import voluptuous as vol
-from homeassistant import config_entries, loader
+from homeassistant import config_entries, loader, data_entry_flow
from homeassistant.setup import async_setup_component
from tests.common import MockModule, mock_coro, MockConfigEntry
@@ -100,7 +99,7 @@ def test_add_entry_calls_setup_entry(hass, manager):
'comp',
MockModule('comp', async_setup_entry=mock_setup_entry))
- class TestFlow(config_entries.ConfigFlowHandler):
+ class TestFlow(data_entry_flow.FlowHandler):
VERSION = 1
@@ -112,7 +111,7 @@ def test_add_entry_calls_setup_entry(hass, manager):
'token': 'supersecret'
})
- with patch.dict(config_entries.HANDLERS, {'comp': TestFlow}):
+ with patch.dict(config_entries.HANDLERS, {'comp': TestFlow, 'beer': 5}):
yield from manager.flow.async_init('comp')
yield from hass.async_block_till_done()
@@ -152,7 +151,7 @@ def test_domains_gets_uniques(manager):
@asyncio.coroutine
def test_saving_and_loading(hass):
"""Test that we're saving and loading correctly."""
- class TestFlow(config_entries.ConfigFlowHandler):
+ class TestFlow(data_entry_flow.FlowHandler):
VERSION = 5
@asyncio.coroutine
@@ -167,7 +166,7 @@ def test_saving_and_loading(hass):
with patch.dict(config_entries.HANDLERS, {'test': TestFlow}):
yield from hass.config_entries.flow.async_init('test')
- class Test2Flow(config_entries.ConfigFlowHandler):
+ class Test2Flow(data_entry_flow.FlowHandler):
VERSION = 3
@asyncio.coroutine
@@ -212,180 +211,37 @@ def test_saving_and_loading(hass):
assert orig.source == loaded.source
-#######################
-# FLOW MANAGER TESTS #
-#######################
+async def test_forward_entry_sets_up_component(hass):
+ """Test we setup the component entry is forwarded to."""
+ entry = MockConfigEntry(domain='original')
-@asyncio.coroutine
-def test_configure_reuses_handler_instance(manager):
- """Test that we reuse instances."""
- class TestFlow(config_entries.ConfigFlowHandler):
- handle_count = 0
+ mock_original_setup_entry = MagicMock(return_value=mock_coro(True))
+ loader.set_component(
+ 'original',
+ MockModule('original', async_setup_entry=mock_original_setup_entry))
- @asyncio.coroutine
- def async_step_init(self, user_input=None):
- self.handle_count += 1
- return self.async_show_form(
- errors={'base': str(self.handle_count)},
- step_id='init')
+ mock_forwarded_setup_entry = MagicMock(return_value=mock_coro(True))
+ loader.set_component(
+ 'forwarded',
+ MockModule('forwarded', async_setup_entry=mock_forwarded_setup_entry))
- with patch.dict(config_entries.HANDLERS, {'test': TestFlow}):
- form = yield from manager.flow.async_init('test')
- assert form['errors']['base'] == '1'
- form = yield from manager.flow.async_configure(form['flow_id'])
- assert form['errors']['base'] == '2'
- assert len(manager.flow.async_progress()) == 1
- assert len(manager.async_entries()) == 0
+ await hass.config_entries.async_forward_entry_setup(entry, 'forwarded')
+ assert len(mock_original_setup_entry.mock_calls) == 0
+ assert len(mock_forwarded_setup_entry.mock_calls) == 1
-@asyncio.coroutine
-def test_configure_two_steps(manager):
- """Test that we reuse instances."""
- class TestFlow(config_entries.ConfigFlowHandler):
- VERSION = 1
+async def test_forward_entry_does_not_setup_entry_if_setup_fails(hass):
+ """Test we do not setup entry if component setup fails."""
+ entry = MockConfigEntry(domain='original')
- @asyncio.coroutine
- def async_step_init(self, user_input=None):
- if user_input is not None:
- self.init_data = user_input
- return self.async_step_second()
- return self.async_show_form(
- step_id='init',
- data_schema=vol.Schema([str])
- )
+ mock_setup = MagicMock(return_value=mock_coro(False))
+ mock_setup_entry = MagicMock()
+ loader.set_component('forwarded', MockModule(
+ 'forwarded',
+ async_setup=mock_setup,
+ async_setup_entry=mock_setup_entry,
+ ))
- @asyncio.coroutine
- def async_step_second(self, user_input=None):
- if user_input is not None:
- return self.async_create_entry(
- title='Test Entry',
- data=self.init_data + user_input
- )
- return self.async_show_form(
- step_id='second',
- data_schema=vol.Schema([str])
- )
-
- with patch.dict(config_entries.HANDLERS, {'test': TestFlow}):
- form = yield from manager.flow.async_init('test')
-
- with pytest.raises(vol.Invalid):
- form = yield from manager.flow.async_configure(
- form['flow_id'], 'INCORRECT-DATA')
-
- form = yield from manager.flow.async_configure(
- form['flow_id'], ['INIT-DATA'])
- form = yield from manager.flow.async_configure(
- form['flow_id'], ['SECOND-DATA'])
- assert form['type'] == config_entries.RESULT_TYPE_CREATE_ENTRY
- assert len(manager.flow.async_progress()) == 0
- assert len(manager.async_entries()) == 1
- entry = manager.async_entries()[0]
- assert entry.domain == 'test'
- assert entry.data == ['INIT-DATA', 'SECOND-DATA']
-
-
-@asyncio.coroutine
-def test_show_form(manager):
- """Test that abort removes the flow from progress."""
- schema = vol.Schema({
- vol.Required('username'): str,
- vol.Required('password'): str
- })
-
- class TestFlow(config_entries.ConfigFlowHandler):
- @asyncio.coroutine
- def async_step_init(self, user_input=None):
- return self.async_show_form(
- step_id='init',
- data_schema=schema,
- errors={
- 'username': 'Should be unique.'
- }
- )
-
- with patch.dict(config_entries.HANDLERS, {'test': TestFlow}):
- form = yield from manager.flow.async_init('test')
- assert form['type'] == 'form'
- assert form['data_schema'] is schema
- assert form['errors'] == {
- 'username': 'Should be unique.'
- }
-
-
-@asyncio.coroutine
-def test_abort_removes_instance(manager):
- """Test that abort removes the flow from progress."""
- class TestFlow(config_entries.ConfigFlowHandler):
- is_new = True
-
- @asyncio.coroutine
- def async_step_init(self, user_input=None):
- old = self.is_new
- self.is_new = False
- return self.async_abort(reason=str(old))
-
- with patch.dict(config_entries.HANDLERS, {'test': TestFlow}):
- form = yield from manager.flow.async_init('test')
- assert form['reason'] == 'True'
- assert len(manager.flow.async_progress()) == 0
- assert len(manager.async_entries()) == 0
- form = yield from manager.flow.async_init('test')
- assert form['reason'] == 'True'
- assert len(manager.flow.async_progress()) == 0
- assert len(manager.async_entries()) == 0
-
-
-@asyncio.coroutine
-def test_create_saves_data(manager):
- """Test creating a config entry."""
- class TestFlow(config_entries.ConfigFlowHandler):
- VERSION = 5
-
- @asyncio.coroutine
- def async_step_init(self, user_input=None):
- return self.async_create_entry(
- title='Test Title',
- data='Test Data'
- )
-
- with patch.dict(config_entries.HANDLERS, {'test': TestFlow}):
- yield from manager.flow.async_init('test')
- assert len(manager.flow.async_progress()) == 0
- assert len(manager.async_entries()) == 1
-
- entry = manager.async_entries()[0]
- assert entry.version == 5
- assert entry.domain == 'test'
- assert entry.title == 'Test Title'
- assert entry.data == 'Test Data'
- assert entry.source == config_entries.SOURCE_USER
-
-
-@asyncio.coroutine
-def test_discovery_init_flow(manager):
- """Test a flow initialized by discovery."""
- class TestFlow(config_entries.ConfigFlowHandler):
- VERSION = 5
-
- @asyncio.coroutine
- def async_step_discovery(self, info):
- return self.async_create_entry(title=info['id'], data=info)
-
- data = {
- 'id': 'hello',
- 'token': 'secret'
- }
-
- with patch.dict(config_entries.HANDLERS, {'test': TestFlow}):
- yield from manager.flow.async_init(
- 'test', source=config_entries.SOURCE_DISCOVERY, data=data)
- assert len(manager.flow.async_progress()) == 0
- assert len(manager.async_entries()) == 1
-
- entry = manager.async_entries()[0]
- assert entry.version == 5
- assert entry.domain == 'test'
- assert entry.title == 'hello'
- assert entry.data == data
- assert entry.source == config_entries.SOURCE_DISCOVERY
+ await hass.config_entries.async_forward_entry_setup(entry, 'forwarded')
+ assert len(mock_setup.mock_calls) == 1
+ assert len(mock_setup_entry.mock_calls) == 0
diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py
new file mode 100644
index 00000000000..2767e206c30
--- /dev/null
+++ b/tests/test_data_entry_flow.py
@@ -0,0 +1,192 @@
+"""Test the flow classes."""
+import pytest
+import voluptuous as vol
+
+from homeassistant import data_entry_flow
+from homeassistant.util.decorator import Registry
+
+
+@pytest.fixture
+def manager():
+ """Return a flow manager."""
+ handlers = Registry()
+ entries = []
+
+ async def async_create_flow(handler_name):
+ handler = handlers.get(handler_name)
+
+ if handler is None:
+ raise data_entry_flow.UnknownHandler
+
+ return handler()
+
+ async def async_add_entry(result):
+ entries.append(result)
+
+ manager = data_entry_flow.FlowManager(
+ None, async_create_flow, async_add_entry)
+ manager.mock_created_entries = entries
+ manager.mock_reg_handler = handlers.register
+ return manager
+
+
+async def test_configure_reuses_handler_instance(manager):
+ """Test that we reuse instances."""
+ @manager.mock_reg_handler('test')
+ class TestFlow(data_entry_flow.FlowHandler):
+ handle_count = 0
+
+ async def async_step_init(self, user_input=None):
+ self.handle_count += 1
+ return self.async_show_form(
+ errors={'base': str(self.handle_count)},
+ step_id='init')
+
+ form = await manager.async_init('test')
+ assert form['errors']['base'] == '1'
+ form = await manager.async_configure(form['flow_id'])
+ assert form['errors']['base'] == '2'
+ assert len(manager.async_progress()) == 1
+ assert len(manager.mock_created_entries) == 0
+
+
+async def test_configure_two_steps(manager):
+ """Test that we reuse instances."""
+ @manager.mock_reg_handler('test')
+ class TestFlow(data_entry_flow.FlowHandler):
+ VERSION = 1
+
+ async def async_step_init(self, user_input=None):
+ if user_input is not None:
+ self.init_data = user_input
+ return await self.async_step_second()
+ return self.async_show_form(
+ step_id='init',
+ data_schema=vol.Schema([str])
+ )
+
+ async def async_step_second(self, user_input=None):
+ if user_input is not None:
+ return self.async_create_entry(
+ title='Test Entry',
+ data=self.init_data + user_input
+ )
+ return self.async_show_form(
+ step_id='second',
+ data_schema=vol.Schema([str])
+ )
+
+ form = await manager.async_init('test')
+
+ with pytest.raises(vol.Invalid):
+ form = await manager.async_configure(
+ form['flow_id'], 'INCORRECT-DATA')
+
+ form = await manager.async_configure(
+ form['flow_id'], ['INIT-DATA'])
+ form = await manager.async_configure(
+ form['flow_id'], ['SECOND-DATA'])
+ assert form['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert len(manager.async_progress()) == 0
+ assert len(manager.mock_created_entries) == 1
+ result = manager.mock_created_entries[0]
+ assert result['handler'] == 'test'
+ assert result['data'] == ['INIT-DATA', 'SECOND-DATA']
+
+
+async def test_show_form(manager):
+ """Test that abort removes the flow from progress."""
+ schema = vol.Schema({
+ vol.Required('username'): str,
+ vol.Required('password'): str
+ })
+
+ @manager.mock_reg_handler('test')
+ class TestFlow(data_entry_flow.FlowHandler):
+ async def async_step_init(self, user_input=None):
+ return self.async_show_form(
+ step_id='init',
+ data_schema=schema,
+ errors={
+ 'username': 'Should be unique.'
+ }
+ )
+
+ form = await manager.async_init('test')
+ assert form['type'] == 'form'
+ assert form['data_schema'] is schema
+ assert form['errors'] == {
+ 'username': 'Should be unique.'
+ }
+
+
+async def test_abort_removes_instance(manager):
+ """Test that abort removes the flow from progress."""
+ @manager.mock_reg_handler('test')
+ class TestFlow(data_entry_flow.FlowHandler):
+ is_new = True
+
+ async def async_step_init(self, user_input=None):
+ old = self.is_new
+ self.is_new = False
+ return self.async_abort(reason=str(old))
+
+ form = await manager.async_init('test')
+ assert form['reason'] == 'True'
+ assert len(manager.async_progress()) == 0
+ assert len(manager.mock_created_entries) == 0
+ form = await manager.async_init('test')
+ assert form['reason'] == 'True'
+ assert len(manager.async_progress()) == 0
+ assert len(manager.mock_created_entries) == 0
+
+
+async def test_create_saves_data(manager):
+ """Test creating a config entry."""
+ @manager.mock_reg_handler('test')
+ class TestFlow(data_entry_flow.FlowHandler):
+ VERSION = 5
+
+ async def async_step_init(self, user_input=None):
+ return self.async_create_entry(
+ title='Test Title',
+ data='Test Data'
+ )
+
+ await manager.async_init('test')
+ assert len(manager.async_progress()) == 0
+ assert len(manager.mock_created_entries) == 1
+
+ entry = manager.mock_created_entries[0]
+ assert entry['version'] == 5
+ assert entry['handler'] == 'test'
+ assert entry['title'] == 'Test Title'
+ assert entry['data'] == 'Test Data'
+ assert entry['source'] == data_entry_flow.SOURCE_USER
+
+
+async def test_discovery_init_flow(manager):
+ """Test a flow initialized by discovery."""
+ @manager.mock_reg_handler('test')
+ class TestFlow(data_entry_flow.FlowHandler):
+ VERSION = 5
+
+ async def async_step_discovery(self, info):
+ return self.async_create_entry(title=info['id'], data=info)
+
+ data = {
+ 'id': 'hello',
+ 'token': 'secret'
+ }
+
+ await manager.async_init(
+ 'test', source=data_entry_flow.SOURCE_DISCOVERY, data=data)
+ assert len(manager.async_progress()) == 0
+ assert len(manager.mock_created_entries) == 1
+
+ entry = manager.mock_created_entries[0]
+ assert entry['version'] == 5
+ assert entry['handler'] == 'test'
+ assert entry['title'] == 'hello'
+ assert entry['data'] == data
+ assert entry['source'] == data_entry_flow.SOURCE_DISCOVERY