Merge pull request #3060 from home-assistant/dev

0.27.1
This commit is contained in:
Robbie Trencheny 2016-08-30 14:22:01 -07:00 committed by GitHub
commit dfc38b76a4
32 changed files with 446 additions and 286 deletions

View File

@ -7,35 +7,43 @@ https://home-assistant.io/components/apcupsd/
import logging
from datetime import timedelta
import voluptuous as vol
from homeassistant.const import (CONF_HOST, CONF_PORT)
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
DOMAIN = "apcupsd"
REQUIREMENTS = ("apcaccess==0.0.4",)
REQUIREMENTS = ['apcaccess==0.0.4']
CONF_HOST = "host"
CONF_PORT = "port"
CONF_TYPE = "type"
_LOGGER = logging.getLogger(__name__)
DEFAULT_HOST = "localhost"
CONF_TYPE = 'type'
DATA = None
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 3551
DOMAIN = 'apcupsd'
KEY_STATUS = "STATUS"
VALUE_ONLINE = "ONLINE"
KEY_STATUS = 'STATUS'
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
DATA = None
VALUE_ONLINE = 'ONLINE'
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Use config values to set up a function enabling status retrieval."""
global DATA
host = config[DOMAIN].get(CONF_HOST, DEFAULT_HOST)
port = config[DOMAIN].get(CONF_PORT, DEFAULT_PORT)
conf = config[DOMAIN]
host = conf.get(CONF_HOST)
port = conf.get(CONF_PORT)
DATA = APCUPSdData(host, port)

View File

@ -4,23 +4,32 @@ Support for tracking the online status of a UPS.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.apcupsd/
"""
from homeassistant.components import apcupsd
from homeassistant.components.binary_sensor import BinarySensorDevice
import voluptuous as vol
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv
from homeassistant.components import apcupsd
DEFAULT_NAME = 'UPS Online Status'
DEPENDENCIES = [apcupsd.DOMAIN]
DEFAULT_NAME = "UPS Online Status"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Instantiate an OnlineStatus binary sensor entity."""
"""Setup an Online Status binary sensor."""
add_entities((OnlineStatus(config, apcupsd.DATA),))
class OnlineStatus(BinarySensorDevice):
"""Represent UPS online status."""
"""Representation of an UPS online status."""
def __init__(self, config, data):
"""Initialize the APCUPSd device."""
"""Initialize the APCUPSd binary device."""
self._config = config
self._data = data
self._state = None
@ -29,7 +38,7 @@ class OnlineStatus(BinarySensorDevice):
@property
def name(self):
"""Return the name of the UPS online status sensor."""
return self._config.get("name", DEFAULT_NAME)
return self._config.get(CONF_NAME)
@property
def is_on(self):

View File

@ -16,7 +16,7 @@ from homeassistant.config import load_yaml_config_file
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, CONF_NAME,
ATTR_ENTITY_ID)
REQUIREMENTS = ["ha-ffmpeg==0.8"]
REQUIREMENTS = ["ha-ffmpeg==0.9"]
SERVICE_RESTART = 'ffmpeg_restart'

View File

@ -14,7 +14,7 @@ from homeassistant.components.camera.mjpeg import extract_image_from_mjpeg
import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_NAME
REQUIREMENTS = ['ha-ffmpeg==0.8']
REQUIREMENTS = ['ha-ffmpeg==0.9']
_LOGGER = logging.getLogger(__name__)

View File

@ -351,7 +351,7 @@ class ClimateDevice(Entity):
@property
def state(self):
"""Return the current state."""
return self.current_operation or STATE_UNKNOWN
return self.target_temperature or STATE_UNKNOWN
@property
def state_attributes(self):

View File

@ -80,6 +80,8 @@ class Thermostat(ClimateDevice):
self.thermostat_index)
self._name = self.thermostat['name']
self.hold_temp = hold_temp
self._operation_list = ['auto', 'auxHeatOnly', 'cool',
'heat', 'off']
def update(self):
"""Get the latest state from the thermostat."""
@ -124,11 +126,6 @@ class Thermostat(ClimateDevice):
"""Return the upper bound temperature we try to reach."""
return int(self.thermostat['runtime']['desiredCool'] / 10)
@property
def current_humidity(self):
"""Return the current humidity."""
return self.thermostat['runtime']['actualHumidity']
@property
def desired_fan_mode(self):
"""Return the desired fan mode of operation."""
@ -142,31 +139,26 @@ class Thermostat(ClimateDevice):
else:
return STATE_OFF
@property
def current_operation(self):
"""Return current operation."""
return self.operation_mode
@property
def operation_list(self):
"""Return the operation modes list."""
return self._operation_list
@property
def operation_mode(self):
"""Return current operation ie. heat, cool, idle."""
status = self.thermostat['equipmentStatus']
if status == '':
return STATE_IDLE
elif 'Cool' in status:
return STATE_COOL
elif 'auxHeat' in status:
return STATE_HEAT
elif 'heatPump' in status:
return STATE_HEAT
else:
return status
return self.thermostat['settings']['hvacMode']
@property
def mode(self):
"""Return current mode ie. home, away, sleep."""
return self.thermostat['program']['currentClimateRef']
@property
def current_operation(self):
"""Return current hvac mode ie. auto, auxHeatOnly, cool, heat, off."""
return self.thermostat['settings']['hvacMode']
@property
def fan_min_on_time(self):
"""Return current fan minimum on time."""
@ -176,11 +168,23 @@ class Thermostat(ClimateDevice):
def device_state_attributes(self):
"""Return device specific state attributes."""
# Move these to Thermostat Device and make them global
status = self.thermostat['equipmentStatus']
operation = None
if status == '':
operation = STATE_IDLE
elif 'Cool' in status:
operation = STATE_COOL
elif 'auxHeat' in status:
operation = STATE_HEAT
elif 'heatPump' in status:
operation = STATE_HEAT
else:
operation = status
return {
"humidity": self.current_humidity,
"humidity": self.thermostat['runtime']['actualHumidity'],
"fan": self.fan,
"mode": self.mode,
"operation_mode": self.current_operation,
"operation": operation,
"fan_min_on_time": self.fan_min_on_time
}

View File

@ -35,11 +35,21 @@ DEVICE_MAPPINGS = {
REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120
}
ZXT_120_SET_TEMP = {
SET_TEMP_TO_INDEX = {
'Heat': 1,
'Cool': 2,
'Auto': 3,
'Aux Heat': 4,
'Resume': 5,
'Fan Only': 6,
'Furnace': 7,
'Dry Air': 8,
'Auto Changeover': 10
'Moist Air': 9,
'Auto Changeover': 10,
'Heat Econ': 11,
'Cool Econ': 12,
'Away': 13,
'Unknown': 14
}
@ -78,7 +88,6 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
self._current_swing_mode = None
self._swing_list = None
self._unit = None
self._index = None
self._zxt_120 = None
self.update_properties()
# register listener
@ -107,15 +116,17 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
def update_properties(self):
"""Callback on data change for the registered node/value pair."""
# Set point
temps = []
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
self._unit = value.units
temps.append(int(value.data))
if value.index == self._index:
self._target_temperature = int(value.data)
self._target_temperature_high = max(temps)
self._target_temperature_low = min(temps)
if self.current_operation is not None:
if SET_TEMP_TO_INDEX.get(self._current_operation) \
!= value.index:
continue
if self._zxt_120:
continue
self._target_temperature = int(value.data)
# Operation Mode
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_MODE).values():
@ -209,23 +220,25 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
"""Set new target temperature."""
for value in self._node.get_values(
class_id=COMMAND_CLASS_THERMOSTAT_SETPOINT).values():
if value.command_class != 67 and value.index != self._index:
continue
if self._zxt_120:
# ZXT-120 does not support get setpoint
self._target_temperature = temperature
if ZXT_120_SET_TEMP.get(self._current_operation) \
!= value.index:
if self.current_operation is not None:
if SET_TEMP_TO_INDEX.get(self._current_operation) \
!= value.index:
continue
_LOGGER.debug("ZXT_120_SET_TEMP=%s and"
_LOGGER.debug("SET_TEMP_TO_INDEX=%s and"
" self._current_operation=%s",
ZXT_120_SET_TEMP.get(self._current_operation),
SET_TEMP_TO_INDEX.get(self._current_operation),
self._current_operation)
# ZXT-120 responds only to whole int
value.data = int(round(temperature, 0))
if self._zxt_120:
# ZXT-120 does not support get setpoint
self._target_temperature = temperature
# ZXT-120 responds only to whole int
value.data = int(round(temperature, 0))
else:
value.data = int(temperature)
break
else:
value.data = int(temperature)
break
break
def set_fan_mode(self, fan):
"""Set new target fan mode."""

View File

@ -10,14 +10,19 @@ from datetime import timedelta
import logging
import os
import threading
from typing import Any, Sequence, Callable
from homeassistant.bootstrap import prepare_setup_platform
import voluptuous as vol
from homeassistant.bootstrap import (
prepare_setup_platform, log_exception)
from homeassistant.components import group, zone
from homeassistant.components.discovery import SERVICE_NETGEAR
from homeassistant.config import load_yaml_config_file
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform, discovery
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
import homeassistant.helpers.config_validation as cv
import homeassistant.util as util
import homeassistant.util.dt as dt_util
@ -27,8 +32,7 @@ from homeassistant.const import (
ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE,
DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME)
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
DOMAIN = "device_tracker"
DOMAIN = 'device_tracker'
DEPENDENCIES = ['zone']
GROUP_NAME_ALL_DEVICES = 'all devices'
@ -38,21 +42,18 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
YAML_DEVICES = 'known_devices.yaml'
CONF_TRACK_NEW = "track_new_devices"
DEFAULT_CONF_TRACK_NEW = True
CONF_TRACK_NEW = 'track_new_devices'
DEFAULT_TRACK_NEW = True
CONF_CONSIDER_HOME = 'consider_home'
DEFAULT_CONSIDER_HOME = 180 # seconds
CONF_SCAN_INTERVAL = "interval_seconds"
CONF_SCAN_INTERVAL = 'interval_seconds'
DEFAULT_SCAN_INTERVAL = 12
CONF_AWAY_HIDE = 'hide_if_away'
DEFAULT_AWAY_HIDE = False
CONF_HOME_RANGE = 'home_range'
DEFAULT_HOME_RANGE = 100
SERVICE_SEE = 'see'
ATTR_MAC = 'mac'
@ -62,23 +63,33 @@ ATTR_LOCATION_NAME = 'location_name'
ATTR_GPS = 'gps'
ATTR_BATTERY = 'battery'
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SCAN_INTERVAL): cv.positive_int, # seconds
}, extra=vol.ALLOW_EXTRA)
_CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.All(cv.ensure_list, [
vol.Schema({
vol.Optional(CONF_TRACK_NEW): cv.boolean,
vol.Optional(CONF_CONSIDER_HOME): cv.positive_int # seconds
}, extra=vol.ALLOW_EXTRA)])}, extra=vol.ALLOW_EXTRA)
DISCOVERY_PLATFORMS = {
SERVICE_NETGEAR: 'netgear',
}
_LOGGER = logging.getLogger(__name__)
# pylint: disable=too-many-arguments
def is_on(hass, entity_id=None):
def is_on(hass: HomeAssistantType, entity_id: str=None):
"""Return the state if any or a specified device is home."""
entity = entity_id or ENTITY_ID_ALL_DEVICES
return hass.states.is_state(entity, STATE_HOME)
def see(hass, mac=None, dev_id=None, host_name=None, location_name=None,
gps=None, gps_accuracy=None, battery=None):
def see(hass: HomeAssistantType, mac: str=None, dev_id: str=None,
host_name: str=None, location_name: str=None,
gps: GPSType=None, gps_accuracy=None,
battery=None): # pylint: disable=too-many-arguments
"""Call service to notify you see device."""
data = {key: value for key, value in
((ATTR_MAC, mac),
@ -91,27 +102,24 @@ def see(hass, mac=None, dev_id=None, host_name=None, location_name=None,
hass.services.call(DOMAIN, SERVICE_SEE, data)
def setup(hass, config):
def setup(hass: HomeAssistantType, config: ConfigType):
"""Setup device tracker."""
yaml_path = hass.config.path(YAML_DEVICES)
conf = config.get(DOMAIN, {})
# Config can be an empty list. In that case, substitute a dict
if isinstance(conf, list):
try:
conf = _CONFIG_SCHEMA(config).get(DOMAIN, [])
except vol.Invalid as ex:
log_exception(ex, DOMAIN, config)
return False
else:
conf = conf[0] if len(conf) > 0 else {}
consider_home = timedelta(
seconds=conf.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME))
track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW)
consider_home = timedelta(
seconds=util.convert(conf.get(CONF_CONSIDER_HOME), int,
DEFAULT_CONSIDER_HOME))
track_new = util.convert(conf.get(CONF_TRACK_NEW), bool,
DEFAULT_CONF_TRACK_NEW)
home_range = util.convert(conf.get(CONF_HOME_RANGE), int,
DEFAULT_HOME_RANGE)
devices = load_config(yaml_path, hass, consider_home)
devices = load_config(yaml_path, hass, consider_home, home_range)
tracker = DeviceTracker(hass, consider_home, track_new, home_range,
devices)
tracker = DeviceTracker(hass, consider_home, track_new, devices)
def setup_platform(p_type, p_config, disc_info=None):
"""Setup a device tracker platform."""
@ -170,30 +178,37 @@ def setup(hass, config):
class DeviceTracker(object):
"""Representation of a device tracker."""
def __init__(self, hass, consider_home, track_new, home_range, devices):
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
track_new: bool, devices: Sequence) -> None:
"""Initialize a device tracker."""
self.hass = hass
self.devices = {dev.dev_id: dev for dev in devices}
self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac}
for dev in devices:
if self.devices[dev.dev_id] is not dev:
_LOGGER.warning('Duplicate device IDs detected %s', dev.dev_id)
if dev.mac and self.mac_to_dev[dev.mac] is not dev:
_LOGGER.warning('Duplicate device MAC addresses detected %s',
dev.mac)
self.consider_home = consider_home
self.track_new = track_new
self.home_range = home_range
self.lock = threading.Lock()
for device in devices:
if device.track:
device.update_ha_state()
self.group = None
self.group = None # type: group.Group
def see(self, mac=None, dev_id=None, host_name=None, location_name=None,
gps=None, gps_accuracy=None, battery=None):
def see(self, mac: str=None, dev_id: str=None, host_name: str=None,
location_name: str=None, gps: GPSType=None, gps_accuracy=None,
battery: str=None):
"""Notify the device tracker that you see a device."""
with self.lock:
if mac is None and dev_id is None:
raise HomeAssistantError('Neither mac or device id passed in')
elif mac is not None:
mac = mac.upper()
mac = str(mac).upper()
device = self.mac_to_dev.get(mac)
if not device:
dev_id = util.slugify(host_name or '') or util.slugify(mac)
@ -211,7 +226,7 @@ class DeviceTracker(object):
# If no device can be found, create it
dev_id = util.ensure_unique_string(dev_id, self.devices.keys())
device = Device(
self.hass, self.consider_home, self.home_range, self.track_new,
self.hass, self.consider_home, self.track_new,
dev_id, mac, (host_name or dev_id).replace('_', ' '))
self.devices[dev_id] = device
if mac is not None:
@ -234,7 +249,7 @@ class DeviceTracker(object):
self.group = group.Group(
self.hass, GROUP_NAME_ALL_DEVICES, entity_ids, False)
def update_stale(self, now):
def update_stale(self, now: dt_util.dt.datetime):
"""Update stale devices."""
with self.lock:
for device in self.devices.values():
@ -246,19 +261,21 @@ class DeviceTracker(object):
class Device(Entity):
"""Represent a tracked device."""
host_name = None
location_name = None
gps = None
host_name = None # type: str
location_name = None # type: str
gps = None # type: GPSType
gps_accuracy = 0
last_seen = None
battery = None
last_seen = None # type: dt_util.dt.datetime
battery = None # type: str
# Track if the last update of this device was HOME.
last_update_home = False
_state = STATE_NOT_HOME
def __init__(self, hass, consider_home, home_range, track, dev_id, mac,
name=None, picture=None, gravatar=None, away_hide=False):
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
track: bool, dev_id: str, mac: str, name: str=None,
picture: str=None, gravatar: str=None,
away_hide: bool=False) -> None:
"""Initialize a device."""
self.hass = hass
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
@ -267,8 +284,6 @@ class Device(Entity):
# detected anymore.
self.consider_home = consider_home
# Distance in meters
self.home_range = home_range
# Device ID
self.dev_id = dev_id
self.mac = mac
@ -287,13 +302,6 @@ class Device(Entity):
self.away_hide = away_hide
@property
def gps_home(self):
"""Return if device is within range of home."""
distance = max(
0, self.hass.config.distance(*self.gps) - self.gps_accuracy)
return self.gps is not None and distance <= self.home_range
@property
def name(self):
"""Return the name of the entity."""
@ -329,26 +337,24 @@ class Device(Entity):
"""If device should be hidden."""
return self.away_hide and self.state != STATE_HOME
def seen(self, host_name=None, location_name=None, gps=None,
gps_accuracy=0, battery=None):
def seen(self, host_name: str=None, location_name: str=None,
gps: GPSType=None, gps_accuracy=0, battery: str=None):
"""Mark the device as seen."""
self.last_seen = dt_util.utcnow()
self.host_name = host_name
self.location_name = location_name
self.gps_accuracy = gps_accuracy or 0
self.battery = battery
if gps is None:
self.gps = None
else:
self.gps = None
if gps is not None:
try:
self.gps = tuple(float(val) for val in gps)
except ValueError:
self.gps = float(gps[0]), float(gps[1])
except (ValueError, TypeError, IndexError):
_LOGGER.warning('Could not parse gps value for %s: %s',
self.dev_id, gps)
self.gps = None
self.update()
def stale(self, now=None):
def stale(self, now: dt_util.dt.datetime=None):
"""Return if device state is stale."""
return self.last_seen and \
(now or dt_util.utcnow()) - self.last_seen > self.consider_home
@ -377,32 +383,30 @@ class Device(Entity):
self.last_update_home = True
def load_config(path, hass, consider_home, home_range):
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
"""Load devices from YAML configuration file."""
if not os.path.isfile(path):
return []
try:
return [
Device(hass, consider_home, home_range, device.get('track', False),
Device(hass, consider_home, device.get('track', False),
str(dev_id).lower(), str(device.get('mac')).upper(),
device.get('name'), device.get('picture'),
device.get('gravatar'),
device.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE))
for dev_id, device in load_yaml_config_file(path).items()]
except HomeAssistantError:
except (HomeAssistantError, FileNotFoundError):
# When YAML file could not be loaded/did not contain a dict
return []
def setup_scanner_platform(hass, config, scanner, see_device):
def setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
scanner: Any, see_device: Callable):
"""Helper method to connect scanner-based platform to device tracker."""
interval = util.convert(config.get(CONF_SCAN_INTERVAL), int,
DEFAULT_SCAN_INTERVAL)
interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
# Initial scan of each mac we also tell about host name for config
seen = set()
seen = set() # type: Any
def device_tracker_scan(now):
def device_tracker_scan(now: dt_util.dt.datetime):
"""Called when interval matches."""
for mac in scanner.scan_devices():
if mac in seen:
@ -418,7 +422,7 @@ def setup_scanner_platform(hass, config, scanner, see_device):
device_tracker_scan(None)
def update_config(path, dev_id, device):
def update_config(path: str, dev_id: str, device: Device):
"""Add device to YAML configuration file."""
with open(path, 'a') as out:
out.write('\n')
@ -432,8 +436,8 @@ def update_config(path, dev_id, device):
out.write(' {}: {}\n'.format(key, '' if value is None else value))
def get_gravatar_for_email(email):
def get_gravatar_for_email(email: str):
"""Return an 80px Gravatar for the given email address."""
import hashlib
url = "https://www.gravatar.com/avatar/{}.jpg?s=80&d=wavatar"
url = 'https://www.gravatar.com/avatar/{}.jpg?s=80&d=wavatar'
return url.format(hashlib.md5(email.encode('utf-8').lower()).hexdigest())

View File

@ -34,9 +34,9 @@ PLATFORM_SCHEMA = vol.All(
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_PROTOCOL, default='ssh'):
vol.Schema(['ssh', 'telnet']),
vol.In(['ssh', 'telnet']),
vol.Optional(CONF_MODE, default='router'):
vol.Schema(['router', 'ap']),
vol.In(['router', 'ap']),
vol.Optional(CONF_SSH_KEY): cv.isfile,
vol.Optional(CONF_PUB_KEY): cv.isfile
}))

View File

@ -63,7 +63,7 @@ def setup_scanner(hass, config, see):
# Load all known devices.
# We just need the devices so set consider_home and home range
# to 0
for device in load_config(yaml_path, hass, 0, 0):
for device in load_config(yaml_path, hass, 0):
# check if device is a valid bluetooth device
if device.mac and device.mac[:3].upper() == BLE_PREFIX:
if device.track:

View File

@ -45,7 +45,7 @@ def setup_scanner(hass, config, see):
# Load all known devices.
# We just need the devices so set consider_home and home range
# to 0
for device in load_config(yaml_path, hass, 0, 0):
for device in load_config(yaml_path, hass, 0):
# check if device is a valid bluetooth device
if device.mac and device.mac[:3].upper() == BT_PREFIX:
if device.track:

View File

@ -129,7 +129,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.In(CONF_RESOLVENAMES_OPTIONS),
vol.Optional(CONF_USERNAME, default="Admin"): cv.string,
vol.Optional(CONF_PASSWORD, default=""): cv.string,
vol.Optional(CONF_DELAY, default=0.5): cv.string,
vol.Optional(CONF_DELAY, default=0.5): vol.Coerce(float),
}),
}, extra=vol.ALLOW_EXTRA)
@ -282,7 +282,7 @@ def _system_callback_handler(hass, config, src, *args):
for component_name, discovery_type in (
('switch', DISCOVER_SWITCHES),
('light', DISCOVER_LIGHTS),
('rollershutter', DISCOVER_COVER),
('cover', DISCOVER_COVER),
('binary_sensor', DISCOVER_BINARY_SENSORS),
('sensor', DISCOVER_SENSORS),
('climate', DISCOVER_CLIMATE)):

View File

@ -7,9 +7,11 @@ https://home-assistant.io/components/light.flux_led/
import logging
import socket
import random
import voluptuous as vol
from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_RGB_COLOR,
ATTR_EFFECT, EFFECT_RANDOM,
SUPPORT_BRIGHTNESS,
SUPPORT_RGB_COLOR, Light)
import homeassistant.helpers.config_validation as cv
@ -125,10 +127,15 @@ class FluxLight(Light):
rgb = kwargs.get(ATTR_RGB_COLOR)
brightness = kwargs.get(ATTR_BRIGHTNESS)
effect = kwargs.get(ATTR_EFFECT)
if rgb:
self._bulb.setRgb(*tuple(rgb))
elif brightness:
self._bulb.setWarmWhite255(brightness)
elif effect == EFFECT_RANDOM:
self._bulb.setRgb(random.randrange(0, 255),
random.randrange(0, 255),
random.randrange(0, 255))
def turn_off(self, **kwargs):
"""Turn the specified or all lights off."""

View File

@ -97,7 +97,6 @@ SERVICE_TO_METHOD = {
SERVICE_MEDIA_STOP: 'media_stop',
SERVICE_MEDIA_NEXT_TRACK: 'media_next_track',
SERVICE_MEDIA_PREVIOUS_TRACK: 'media_previous_track',
SERVICE_SELECT_SOURCE: 'select_source',
SERVICE_CLEAR_PLAYLIST: 'clear_playlist'
}

View File

@ -40,25 +40,25 @@ volume_down:
description: Name(s) of entities to turn volume down on
example: 'media_player.living_room_sonos'
mute_volume:
volume_mute:
description: Mute a media player's volume
fields:
entity_id:
description: Name(s) of entities to mute
example: 'media_player.living_room_sonos'
mute:
is_volume_muted:
description: True/false for mute/unmute
example: true
set_volume_level:
volume_set:
description: Set a media player's volume level
fields:
entity_id:
description: Name(s) of entities to set volume level on
example: 'media_player.living_room_sonos'
volume:
volume_level:
description: Volume level to set
example: 60
@ -117,7 +117,7 @@ media_seek:
entity_id:
description: Name(s) of entities to seek media on
example: 'media_player.living_room_chromecast'
position:
seek_position:
description: Position to seek to. The format is platform dependent.
example: 100

View File

@ -10,7 +10,7 @@ from homeassistant.components.notify import (
ATTR_TITLE, DOMAIN, BaseNotificationService)
from homeassistant.helpers import validate_config
REQUIREMENTS = ['sendgrid==3.1.10']
REQUIREMENTS = ['sendgrid==3.2.10']
_LOGGER = logging.getLogger(__name__)

View File

@ -10,31 +10,46 @@ from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.notify import (
ATTR_TITLE, ATTR_DATA, DOMAIN, BaseNotificationService)
from homeassistant.helpers import validate_config
ATTR_TITLE, ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService)
from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
_LOGGER = logging.getLogger(__name__)
ATTR_IMAGES = 'images' # optional embedded image file attachments
CONF_STARTTLS = 'starttls'
CONF_SENDER = 'sender'
CONF_RECIPIENT = 'recipient'
CONF_DEBUG = 'debug'
CONF_SERVER = 'server'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_RECIPIENT): cv.string,
vol.Optional(CONF_SERVER, default='localhost'): cv.string,
vol.Optional(CONF_PORT, default=25): cv.port,
vol.Optional(CONF_SENDER): cv.string,
vol.Optional(CONF_STARTTLS, default=False): cv.boolean,
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_DEBUG, default=False): cv.boolean,
})
def get_service(hass, config):
"""Get the mail notification service."""
if not validate_config({DOMAIN: config},
{DOMAIN: ['recipient']},
_LOGGER):
return None
mail_service = MailNotificationService(
config.get('server', 'localhost'),
int(config.get('port', '25')),
config.get('sender', None),
int(config.get('starttls', 0)),
config.get('username', None),
config.get('password', None),
config.get('recipient', None),
config.get('debug', 0))
config.get(CONF_SERVER),
config.get(CONF_PORT),
config.get(CONF_SENDER),
config.get(CONF_STARTTLS),
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD),
config.get(CONF_RECIPIENT),
config.get(CONF_DEBUG))
if mail_service.connection_is_valid():
return mail_service
@ -65,7 +80,7 @@ class MailNotificationService(BaseNotificationService):
mail = smtplib.SMTP(self._server, self._port, timeout=5)
mail.set_debuglevel(self.debug)
mail.ehlo_or_helo_if_needed()
if self.starttls == 1:
if self.starttls:
mail.starttls()
mail.ehlo()
if self.username and self.password:

View File

@ -11,7 +11,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers import validate_config
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['TwitterAPI==2.4.1']
REQUIREMENTS = ['TwitterAPI==2.4.2']
CONF_CONSUMER_KEY = "consumer_key"
CONF_CONSUMER_SECRET = "consumer_secret"

View File

@ -6,10 +6,16 @@ https://home-assistant.io/components/sensor.apcupsd/
"""
import logging
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
import homeassistant.helpers.config_validation as cv
from homeassistant.components import apcupsd
from homeassistant.const import TEMP_CELSIUS
from homeassistant.const import (TEMP_CELSIUS, CONF_RESOURCES)
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = [apcupsd.DOMAIN]
SENSOR_PREFIX = 'UPS '
@ -92,14 +98,17 @@ INFERRED_UNITS = {
' C': TEMP_CELSIUS,
}
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_RESOURCES, default=[]):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
})
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Setup the APCUPSd sensors."""
entities = []
for resource in config['resources']:
for resource in config[CONF_RESOURCES]:
sensor_type = resource.lower()
if sensor_type not in SENSOR_TYPES:
@ -109,7 +118,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if sensor_type.upper() not in apcupsd.DATA.status:
_LOGGER.warning(
'Sensor type: "%s" does not appear in the APCUPSd status '
'output.', sensor_type)
'output', sensor_type)
entities.append(APCUPSdSensor(apcupsd.DATA, sensor_type))

View File

@ -29,6 +29,7 @@ CONF_SECOND = 'second'
CONF_MINUTE = 'minute'
CONF_HOUR = 'hour'
CONF_DAY = 'day'
CONF_SERVER_ID = 'server_id'
SENSOR_TYPES = {
'ping': ['Ping', 'ms'],
'download': ['Download', 'Mbit/s'],
@ -38,6 +39,7 @@ SENSOR_TYPES = {
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_MONITORED_CONDITIONS):
vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES.keys()))]),
vol.Optional(CONF_SERVER_ID): cv.positive_int,
vol.Optional(CONF_SECOND, default=[0]):
vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 59))]),
vol.Optional(CONF_MINUTE, default=[0]):
@ -131,6 +133,7 @@ class SpeedtestData(object):
def __init__(self, hass, config):
"""Initialize the data object."""
self.data = None
self._server_id = config.get(CONF_SERVER_ID)
track_time_change(hass, self.update,
second=config.get(CONF_SECOND),
minute=config.get(CONF_MINUTE),
@ -143,9 +146,12 @@ class SpeedtestData(object):
_LOGGER.info('Executing speedtest')
try:
args = [sys.executable, speedtest_cli.__file__, '--simple']
if self._server_id:
args = args + ['--server', str(self._server_id)]
re_output = _SPEEDTEST_REGEX.split(
check_output([sys.executable, speedtest_cli.__file__,
'--simple']).decode("utf-8"))
check_output(args).decode("utf-8"))
except CalledProcessError as process_error:
_LOGGER.error('Error executing speedtest: %s', process_error)
return

View File

@ -101,7 +101,10 @@ class WUndergroundSensor(Entity):
def state(self):
"""Return the state of the sensor."""
if self.rest.data and self._condition in self.rest.data:
return self.rest.data[self._condition]
if self._condition == 'relative_humidity':
return int(self.rest.data[self._condition][:-1])
else:
return self.rest.data[self._condition]
else:
return STATE_UNKNOWN

View File

@ -81,7 +81,7 @@ def setup(hass, config):
# Add static devices from the config file.
devices.extend((address, None)
for address in config.get(DOMAIN, {}).get(CONF_STATIC))
for address in config.get(DOMAIN, {}).get(CONF_STATIC, []))
for address, device in devices:
port = pywemo.ouimeaux_device.probe_wemo(address)

View File

@ -1,7 +1,7 @@
# coding: utf-8
"""Constants used by Home Assistant components."""
__version__ = '0.27.0'
__version__ = '0.27.1'
REQUIRED_PYTHON_VER = (3, 4)
PLATFORM_FORMAT = '{}.{}'

View File

@ -1,24 +1,13 @@
"""Typing Helpers for Home-Assistant."""
from typing import Dict, Any
from typing import Dict, Any, Tuple
# NOTE: NewType added to typing in 3.5.2 in June, 2016; Since 3.5.2 includes
# security fixes everyone on 3.5 should upgrade "soon"
try:
from typing import NewType
except ImportError:
NewType = None
import homeassistant.core
# pylint: disable=invalid-name
if NewType:
ConfigType = NewType('ConfigType', Dict[str, Any])
# Custom type for recorder Queries
QueryType = NewType('QueryType', Any)
GPSType = Tuple[float, float]
ConfigType = Dict[str, Any]
HomeAssistantType = homeassistant.core.HomeAssistant
# Duplicates for 3.5.1
# pylint: disable=invalid-name
else:
ConfigType = Dict[str, Any] # type: ignore
# Custom type for recorder Queries
QueryType = Any # type: ignore
# Custom type for recorder Queries
QueryType = Any

View File

@ -12,7 +12,7 @@ import string
from functools import wraps
from types import MappingProxyType
from typing import Any, Optional, TypeVar, Callable, Sequence
from typing import Any, Optional, TypeVar, Callable, Sequence, KeysView, Union
from .dt import as_local, utcnow
@ -63,8 +63,8 @@ def convert(value: T, to_type: Callable[[T], U],
return default
def ensure_unique_string(preferred_string: str,
current_strings: Sequence[str]) -> str:
def ensure_unique_string(preferred_string: str, current_strings:
Union[Sequence[str], KeysView[str]]) -> str:
"""Return a string that is not present in current_strings.
If preferred string exists will append _2, _3, ..

View File

@ -23,7 +23,7 @@ PyMata==2.12
SoCo==0.11.1
# homeassistant.components.notify.twitter
TwitterAPI==2.4.1
TwitterAPI==2.4.2
# homeassistant.components.http
Werkzeug==0.11.10
@ -104,7 +104,7 @@ gps3==0.33.2
# homeassistant.components.binary_sensor.ffmpeg
# homeassistant.components.camera.ffmpeg
ha-ffmpeg==0.8
ha-ffmpeg==0.9
# homeassistant.components.mqtt.server
hbmqtt==0.7.1
@ -422,7 +422,7 @@ schiene==0.17
scsgate==0.1.0
# homeassistant.components.notify.sendgrid
sendgrid==3.1.10
sendgrid==3.2.10
# homeassistant.components.notify.slack
slacker==0.9.24

View File

@ -7,7 +7,7 @@ from io import StringIO
import logging
from homeassistant import core as ha, loader
from homeassistant.bootstrap import _setup_component
from homeassistant.bootstrap import setup_component
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.util.unit_system import METRIC_SYSTEM
import homeassistant.util.dt as date_util
@ -137,15 +137,15 @@ def mock_http_component(hass):
hass.config.components.append('http')
@mock.patch('homeassistant.components.mqtt.MQTT')
def mock_mqtt_component(hass, mock_mqtt):
def mock_mqtt_component(hass):
"""Mock the MQTT component."""
_setup_component(hass, mqtt.DOMAIN, {
mqtt.DOMAIN: {
mqtt.CONF_BROKER: 'mock-broker',
}
})
return mock_mqtt
with mock.patch('homeassistant.components.mqtt.MQTT') as mock_mqtt:
setup_component(hass, mqtt.DOMAIN, {
mqtt.DOMAIN: {
mqtt.CONF_BROKER: 'mock-broker',
}
})
return mock_mqtt
class MockModule(object):

View File

@ -110,16 +110,19 @@ class TestDemoClimate(unittest.TestCase):
def test_set_operation_bad_attr(self):
"""Test setting operation mode without required attribute."""
self.assertEqual("Cool", self.hass.states.get(ENTITY_CLIMATE).state)
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual("Cool", state.attributes.get('operation_mode'))
climate.set_operation_mode(self.hass, None, ENTITY_CLIMATE)
self.hass.pool.block_till_done()
self.assertEqual("Cool", self.hass.states.get(ENTITY_CLIMATE).state)
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual("Cool", state.attributes.get('operation_mode'))
def test_set_operation(self):
"""Test setting of new operation mode."""
climate.set_operation_mode(self.hass, "Heat", ENTITY_CLIMATE)
self.hass.pool.block_till_done()
self.assertEqual("Heat", self.hass.states.get(ENTITY_CLIMATE).state)
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual("Heat", state.attributes.get('operation_mode'))
def test_set_away_mode_bad_attr(self):
"""Test setting the away mode without required attribute."""

View File

@ -1,10 +1,10 @@
"""The tests for the device tracker component."""
# pylint: disable=protected-access,too-many-public-methods
import logging
import unittest
from unittest.mock import patch
from datetime import datetime, timedelta
import os
import tempfile
from homeassistant.loader import get_component
import homeassistant.util.dt as dt_util
@ -12,14 +12,23 @@ from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN,
STATE_HOME, STATE_NOT_HOME, CONF_PLATFORM)
import homeassistant.components.device_tracker as device_tracker
from homeassistant.exceptions import HomeAssistantError
from tests.common import (
get_test_home_assistant, fire_time_changed, fire_service_discovered)
get_test_home_assistant, fire_time_changed, fire_service_discovered,
patch_yaml_files)
TEST_PLATFORM = {device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}}
_LOGGER = logging.getLogger(__name__)
class TestComponentsDeviceTracker(unittest.TestCase):
"""Test the Device tracker."""
hass = None # HomeAssistant
yaml_devices = None # type: str
def setUp(self): # pylint: disable=invalid-name
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
@ -48,27 +57,28 @@ class TestComponentsDeviceTracker(unittest.TestCase):
def test_reading_broken_yaml_config(self): # pylint: disable=no-self-use
"""Test when known devices contains invalid data."""
with tempfile.NamedTemporaryFile() as fpt:
# file is empty
assert device_tracker.load_config(fpt.name, None, False, 0) == []
fpt.write('100'.encode('utf-8'))
fpt.flush()
# file contains a non-dict format
assert device_tracker.load_config(fpt.name, None, False, 0) == []
files = {'empty.yaml': '',
'bad.yaml': '100',
'ok.yaml': 'my_device:\n name: Device'}
with patch_yaml_files(files):
# File is empty
assert device_tracker.load_config('empty.yaml', None, False) == []
# File contains a non-dict format
assert device_tracker.load_config('bad.yaml', None, False) == []
# A file that works fine
assert len(device_tracker.load_config('ok.yaml', None, False)) == 1
def test_reading_yaml_config(self):
"""Test the rendering of the YAML configuration."""
dev_id = 'test'
device = device_tracker.Device(
self.hass, timedelta(seconds=180), 0, True, dev_id,
self.hass, timedelta(seconds=180), True, dev_id,
'AB:CD:EF:GH:IJ', 'Test name', picture='http://test.picture',
away_hide=True)
device_tracker.update_config(self.yaml_devices, dev_id, device)
self.assertTrue(device_tracker.setup(self.hass, {}))
self.assertTrue(device_tracker.setup(self.hass, TEST_PLATFORM))
config = device_tracker.load_config(self.yaml_devices, self.hass,
device.consider_home, 0)[0]
device.consider_home)[0]
self.assertEqual(device.dev_id, config.dev_id)
self.assertEqual(device.track, config.track)
self.assertEqual(device.mac, config.mac)
@ -76,12 +86,44 @@ class TestComponentsDeviceTracker(unittest.TestCase):
self.assertEqual(device.away_hide, config.away_hide)
self.assertEqual(device.consider_home, config.consider_home)
@patch('homeassistant.components.device_tracker._LOGGER.warning')
def test_track_with_duplicate_mac_dev_id(self, mock_warning): \
# pylint: disable=invalid-name
"""Test adding duplicate MACs or device IDs to DeviceTracker."""
devices = [
device_tracker.Device(self.hass, True, True, 'my_device', 'AB:01',
'My device', None, None, False),
device_tracker.Device(self.hass, True, True, 'your_device',
'AB:01', 'Your device', None, None, False)]
device_tracker.DeviceTracker(self.hass, False, True, devices)
_LOGGER.debug(mock_warning.call_args_list)
assert mock_warning.call_count == 1, \
"The only warning call should be duplicates (check DEBUG)"
args, _ = mock_warning.call_args
assert 'Duplicate device MAC' in args[0], \
'Duplicate MAC warning expected'
mock_warning.reset_mock()
devices = [
device_tracker.Device(self.hass, True, True, 'my_device',
'AB:01', 'My device', None, None, False),
device_tracker.Device(self.hass, True, True, 'my_device',
None, 'Your device', None, None, False)]
device_tracker.DeviceTracker(self.hass, False, True, devices)
_LOGGER.debug(mock_warning.call_args_list)
assert mock_warning.call_count == 1, \
"The only warning call should be duplicates (check DEBUG)"
args, _ = mock_warning.call_args
assert 'Duplicate device IDs' in args[0], \
'Duplicate device IDs warning expected'
def test_setup_without_yaml_file(self):
"""Test with no YAML file."""
self.assertTrue(device_tracker.setup(self.hass, {}))
self.assertTrue(device_tracker.setup(self.hass, TEST_PLATFORM))
# pylint: disable=invalid-name
def test_adding_unknown_device_to_config(self):
def test_adding_unknown_device_to_config(self): \
# pylint: disable=invalid-name
"""Test the adding of unknown devices to configuration file."""
scanner = get_component('device_tracker.test').SCANNER
scanner.reset()
@ -90,7 +132,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
self.assertTrue(device_tracker.setup(self.hass, {
device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}}))
config = device_tracker.load_config(self.yaml_devices, self.hass,
timedelta(seconds=0), 0)
timedelta(seconds=0))
assert len(config) == 1
assert config[0].dev_id == 'dev1'
assert config[0].track
@ -99,7 +141,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
"""Test the Gravatar generation."""
dev_id = 'test'
device = device_tracker.Device(
self.hass, timedelta(seconds=180), 0, True, dev_id,
self.hass, timedelta(seconds=180), True, dev_id,
'AB:CD:EF:GH:IJ', 'Test name', gravatar='test@example.com')
gravatar_url = ("https://www.gravatar.com/avatar/"
"55502f40dc8b7c769880b10874abc9d0.jpg?s=80&d=wavatar")
@ -109,7 +151,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
"""Test that Gravatar overrides picture."""
dev_id = 'test'
device = device_tracker.Device(
self.hass, timedelta(seconds=180), 0, True, dev_id,
self.hass, timedelta(seconds=180), True, dev_id,
'AB:CD:EF:GH:IJ', 'Test name', picture='http://test.picture',
gravatar='test@example.com')
gravatar_url = ("https://www.gravatar.com/avatar/"
@ -122,8 +164,8 @@ class TestComponentsDeviceTracker(unittest.TestCase):
with patch.dict(device_tracker.DISCOVERY_PLATFORMS, {'test': 'test'}):
with patch.object(scanner, 'scan_devices') as mock_scan:
self.assertTrue(device_tracker.setup(self.hass, {
device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}}))
self.assertTrue(device_tracker.setup(self.hass,
TEST_PLATFORM))
fire_service_discovered(self.hass, 'test', {})
self.assertTrue(mock_scan.called)
@ -139,9 +181,9 @@ class TestComponentsDeviceTracker(unittest.TestCase):
with patch('homeassistant.components.device_tracker.dt_util.utcnow',
return_value=register_time):
self.assertTrue(device_tracker.setup(self.hass, {
'device_tracker': {
'platform': 'test',
'consider_home': 59,
device_tracker.DOMAIN: {
CONF_PLATFORM: 'test',
device_tracker.CONF_CONSIDER_HOME: 59,
}}))
self.assertEqual(STATE_HOME,
@ -165,11 +207,11 @@ class TestComponentsDeviceTracker(unittest.TestCase):
picture = 'http://placehold.it/200x200'
device = device_tracker.Device(
self.hass, timedelta(seconds=180), 0, True, dev_id, None,
self.hass, timedelta(seconds=180), True, dev_id, None,
friendly_name, picture, away_hide=True)
device_tracker.update_config(self.yaml_devices, dev_id, device)
self.assertTrue(device_tracker.setup(self.hass, {}))
self.assertTrue(device_tracker.setup(self.hass, TEST_PLATFORM))
attrs = self.hass.states.get(entity_id).attributes
@ -181,15 +223,14 @@ class TestComponentsDeviceTracker(unittest.TestCase):
dev_id = 'test_entity'
entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id)
device = device_tracker.Device(
self.hass, timedelta(seconds=180), 0, True, dev_id, None,
self.hass, timedelta(seconds=180), True, dev_id, None,
away_hide=True)
device_tracker.update_config(self.yaml_devices, dev_id, device)
scanner = get_component('device_tracker.test').SCANNER
scanner.reset()
self.assertTrue(device_tracker.setup(self.hass, {
device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}}))
self.assertTrue(device_tracker.setup(self.hass, TEST_PLATFORM))
self.assertTrue(self.hass.states.get(entity_id)
.attributes.get(ATTR_HIDDEN))
@ -199,15 +240,14 @@ class TestComponentsDeviceTracker(unittest.TestCase):
dev_id = 'test_entity'
entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id)
device = device_tracker.Device(
self.hass, timedelta(seconds=180), 0, True, dev_id, None,
self.hass, timedelta(seconds=180), True, dev_id, None,
away_hide=True)
device_tracker.update_config(self.yaml_devices, dev_id, device)
scanner = get_component('device_tracker.test').SCANNER
scanner.reset()
self.assertTrue(device_tracker.setup(self.hass, {
device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}}))
self.assertTrue(device_tracker.setup(self.hass, TEST_PLATFORM))
state = self.hass.states.get(device_tracker.ENTITY_ID_ALL_DEVICES)
self.assertIsNotNone(state)
@ -217,40 +257,31 @@ class TestComponentsDeviceTracker(unittest.TestCase):
@patch('homeassistant.components.device_tracker.DeviceTracker.see')
def test_see_service(self, mock_see):
"""Test the see service."""
self.assertTrue(device_tracker.setup(self.hass, {}))
mac = 'AB:CD:EF:GH'
dev_id = 'some_device'
host_name = 'example.com'
location_name = 'Work'
gps = [.3, .8]
device_tracker.see(self.hass, mac, dev_id, host_name, location_name,
gps)
self.hass.pool.block_till_done()
mock_see.assert_called_once_with(
mac=mac, dev_id=dev_id, host_name=host_name,
location_name=location_name, gps=gps)
@patch('homeassistant.components.device_tracker.DeviceTracker.see')
def test_see_service_unicode_dev_id(self, mock_see):
"""Test the see service with a unicode dev_id and NO MAC."""
self.assertTrue(device_tracker.setup(self.hass, {}))
self.assertTrue(device_tracker.setup(self.hass, TEST_PLATFORM))
params = {
'dev_id': chr(233), # e' acute accent from icloud
'dev_id': 'some_device',
'host_name': 'example.com',
'location_name': 'Work',
'gps': [.3, .8]
}
device_tracker.see(self.hass, **params)
self.hass.pool.block_till_done()
assert mock_see.call_count == 1
mock_see.assert_called_once_with(**params)
def test_not_write_duplicate_yaml_keys(self):
mock_see.reset_mock()
params['dev_id'] += chr(233) # e' acute accent from icloud
device_tracker.see(self.hass, **params)
self.hass.pool.block_till_done()
assert mock_see.call_count == 1
mock_see.assert_called_once_with(**params)
def test_not_write_duplicate_yaml_keys(self): \
# pylint: disable=invalid-name
"""Test that the device tracker will not generate invalid YAML."""
self.assertTrue(device_tracker.setup(self.hass, {}))
self.assertTrue(device_tracker.setup(self.hass, TEST_PLATFORM))
device_tracker.see(self.hass, 'mac_1', host_name='hello')
device_tracker.see(self.hass, 'mac_2', host_name='hello')
@ -258,15 +289,46 @@ class TestComponentsDeviceTracker(unittest.TestCase):
self.hass.pool.block_till_done()
config = device_tracker.load_config(self.yaml_devices, self.hass,
timedelta(seconds=0), 0)
timedelta(seconds=0))
assert len(config) == 2
def test_not_allow_invalid_dev_id(self):
def test_not_allow_invalid_dev_id(self): # pylint: disable=invalid-name
"""Test that the device tracker will not allow invalid dev ids."""
self.assertTrue(device_tracker.setup(self.hass, {}))
self.assertTrue(device_tracker.setup(self.hass, TEST_PLATFORM))
device_tracker.see(self.hass, dev_id='hello-world')
config = device_tracker.load_config(self.yaml_devices, self.hass,
timedelta(seconds=0), 0)
timedelta(seconds=0))
assert len(config) == 0
@patch('homeassistant.components.device_tracker._LOGGER.warning')
def test_see_failures(self, mock_warning):
"""Test that the device tracker see failures."""
tracker = device_tracker.DeviceTracker(
self.hass, timedelta(seconds=60), 0, [])
# MAC is not a string (but added)
tracker.see(mac=567, host_name="Number MAC")
# No device id or MAC(not added)
with self.assertRaises(HomeAssistantError):
tracker.see()
assert mock_warning.call_count == 0
# Ignore gps on invalid GPS (both added & warnings)
tracker.see(mac='mac_1_bad_gps', gps=1)
tracker.see(mac='mac_2_bad_gps', gps=[1])
tracker.see(mac='mac_3_bad_gps', gps='gps')
config = device_tracker.load_config(self.yaml_devices, self.hass,
timedelta(seconds=0))
assert mock_warning.call_count == 3
assert len(config) == 4
@patch('homeassistant.components.device_tracker.log_exception')
def test_config_failure(self, mock_ex):
"""Test that the device tracker see failures."""
device_tracker.setup(self.hass, {device_tracker.DOMAIN: {
device_tracker.CONF_CONSIDER_HOME: -1}})
assert mock_ex.call_count == 1

View File

@ -8,17 +8,19 @@ import requests
from homeassistant import bootstrap, const
import homeassistant.components.device_tracker as device_tracker
import homeassistant.components.http as http
from homeassistant.const import CONF_PLATFORM
from tests.common import get_test_home_assistant, get_test_instance_port
SERVER_PORT = get_test_instance_port()
HTTP_BASE_URL = "http://127.0.0.1:{}".format(SERVER_PORT)
hass = None
hass = None # pylint: disable=invalid-name
def _url(data={}):
def _url(data=None):
"""Helper method to generate URLs."""
data = data or {}
data = "&".join(["{}={}".format(name, value) for
name, value in data.items()])
return "{}{}locative?{}".format(HTTP_BASE_URL, const.URL_API, data)
@ -26,7 +28,7 @@ def _url(data={}):
def setUpModule(): # pylint: disable=invalid-name
"""Initalize a Home Assistant server."""
global hass
global hass # pylint: disable=invalid-name
hass = get_test_home_assistant()
bootstrap.setup_component(hass, http.DOMAIN, {
@ -38,7 +40,7 @@ def setUpModule(): # pylint: disable=invalid-name
# Set up device tracker
bootstrap.setup_component(hass, device_tracker.DOMAIN, {
device_tracker.DOMAIN: {
'platform': 'locative'
CONF_PLATFORM: 'locative'
}
})

View File

@ -1,5 +1,7 @@
"""The tests for the MQTT device tracker platform."""
import unittest
from unittest.mock import patch
import logging
import os
from homeassistant.bootstrap import _setup_component
@ -9,6 +11,8 @@ from homeassistant.const import CONF_PLATFORM
from tests.common import (
get_test_home_assistant, mock_mqtt_component, fire_mqtt_message)
_LOGGER = logging.getLogger(__name__)
class TestComponentsDeviceTrackerMQTT(unittest.TestCase):
"""Test MQTT device tracker platform."""
@ -25,6 +29,27 @@ class TestComponentsDeviceTrackerMQTT(unittest.TestCase):
except FileNotFoundError:
pass
def test_ensure_device_tracker_platform_validation(self): \
# pylint: disable=invalid-name
"""Test if platform validation was done."""
def mock_setup_scanner(hass, config, see):
"""Check that Qos was added by validation."""
self.assertTrue('qos' in config)
with patch('homeassistant.components.device_tracker.mqtt.'
'setup_scanner', side_effect=mock_setup_scanner) as mock_sp:
dev_id = 'paulus'
topic = '/location/paulus'
self.hass.config.components = ['mqtt', 'zone']
assert _setup_component(self.hass, device_tracker.DOMAIN, {
device_tracker.DOMAIN: {
CONF_PLATFORM: 'mqtt',
'devices': {dev_id: topic}
}
})
assert mock_sp.call_count == 1
def test_new_message(self):
"""Test new message."""
dev_id = 'paulus'

View File

@ -22,12 +22,12 @@ def setUpModule(): # pylint: disable=invalid-name
"""Write a device tracker known devices file to be used."""
device_tracker.update_config(
KNOWN_DEV_YAML_PATH, 'device_1', device_tracker.Device(
None, None, None, True, 'device_1', 'DEV1',
None, None, True, 'device_1', 'DEV1',
picture='http://example.com/dev1.jpg'))
device_tracker.update_config(
KNOWN_DEV_YAML_PATH, 'device_2', device_tracker.Device(
None, None, None, True, 'device_2', 'DEV2',
None, None, True, 'device_2', 'DEV2',
picture='http://example.com/dev2.jpg'))
@ -83,7 +83,8 @@ class TestDeviceSunLightTrigger(unittest.TestCase):
self.assertTrue(light.is_on(self.hass))
def test_lights_turn_off_when_everyone_leaves(self):
def test_lights_turn_off_when_everyone_leaves(self): \
# pylint: disable=invalid-name
"""Test lights turn off when everyone leaves the house."""
light.turn_on(self.hass)
@ -99,7 +100,8 @@ class TestDeviceSunLightTrigger(unittest.TestCase):
self.assertFalse(light.is_on(self.hass))
def test_lights_turn_on_when_coming_home_after_sun_set(self):
def test_lights_turn_on_when_coming_home_after_sun_set(self): \
# pylint: disable=invalid-name
"""Test lights turn on when coming home after sun set."""
light.turn_off(self.hass)
ensure_sun_set(self.hass)