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 import logging
from datetime import timedelta 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 from homeassistant.util import Throttle
DOMAIN = "apcupsd" REQUIREMENTS = ['apcaccess==0.0.4']
REQUIREMENTS = ("apcaccess==0.0.4",)
CONF_HOST = "host" _LOGGER = logging.getLogger(__name__)
CONF_PORT = "port"
CONF_TYPE = "type"
DEFAULT_HOST = "localhost" CONF_TYPE = 'type'
DATA = None
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 3551 DEFAULT_PORT = 3551
DOMAIN = 'apcupsd'
KEY_STATUS = "STATUS" KEY_STATUS = 'STATUS'
VALUE_ONLINE = "ONLINE"
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) 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): def setup(hass, config):
"""Use config values to set up a function enabling status retrieval.""" """Use config values to set up a function enabling status retrieval."""
global DATA global DATA
conf = config[DOMAIN]
host = config[DOMAIN].get(CONF_HOST, DEFAULT_HOST) host = conf.get(CONF_HOST)
port = config[DOMAIN].get(CONF_PORT, DEFAULT_PORT) port = conf.get(CONF_PORT)
DATA = APCUPSdData(host, 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 For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.apcupsd/ https://home-assistant.io/components/binary_sensor.apcupsd/
""" """
from homeassistant.components import apcupsd import voluptuous as vol
from homeassistant.components.binary_sensor import BinarySensorDevice
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] 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): 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),)) add_entities((OnlineStatus(config, apcupsd.DATA),))
class OnlineStatus(BinarySensorDevice): class OnlineStatus(BinarySensorDevice):
"""Represent UPS online status.""" """Representation of an UPS online status."""
def __init__(self, config, data): def __init__(self, config, data):
"""Initialize the APCUPSd device.""" """Initialize the APCUPSd binary device."""
self._config = config self._config = config
self._data = data self._data = data
self._state = None self._state = None
@ -29,7 +38,7 @@ class OnlineStatus(BinarySensorDevice):
@property @property
def name(self): def name(self):
"""Return the name of the UPS online status sensor.""" """Return the name of the UPS online status sensor."""
return self._config.get("name", DEFAULT_NAME) return self._config.get(CONF_NAME)
@property @property
def is_on(self): 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, from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, CONF_NAME,
ATTR_ENTITY_ID) ATTR_ENTITY_ID)
REQUIREMENTS = ["ha-ffmpeg==0.8"] REQUIREMENTS = ["ha-ffmpeg==0.9"]
SERVICE_RESTART = 'ffmpeg_restart' 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 import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
REQUIREMENTS = ['ha-ffmpeg==0.8'] REQUIREMENTS = ['ha-ffmpeg==0.9']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@ from homeassistant.components.notify import (
ATTR_TITLE, DOMAIN, BaseNotificationService) ATTR_TITLE, DOMAIN, BaseNotificationService)
from homeassistant.helpers import validate_config from homeassistant.helpers import validate_config
REQUIREMENTS = ['sendgrid==3.1.10'] REQUIREMENTS = ['sendgrid==3.2.10']
_LOGGER = logging.getLogger(__name__) _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.text import MIMEText
from email.mime.image import MIMEImage from email.mime.image import MIMEImage
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.notify import ( from homeassistant.components.notify import (
ATTR_TITLE, ATTR_DATA, DOMAIN, BaseNotificationService) ATTR_TITLE, ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService)
from homeassistant.helpers import validate_config from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_IMAGES = 'images' # optional embedded image file attachments 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): def get_service(hass, config):
"""Get the mail notification service.""" """Get the mail notification service."""
if not validate_config({DOMAIN: config},
{DOMAIN: ['recipient']},
_LOGGER):
return None
mail_service = MailNotificationService( mail_service = MailNotificationService(
config.get('server', 'localhost'), config.get(CONF_SERVER),
int(config.get('port', '25')), config.get(CONF_PORT),
config.get('sender', None), config.get(CONF_SENDER),
int(config.get('starttls', 0)), config.get(CONF_STARTTLS),
config.get('username', None), config.get(CONF_USERNAME),
config.get('password', None), config.get(CONF_PASSWORD),
config.get('recipient', None), config.get(CONF_RECIPIENT),
config.get('debug', 0)) config.get(CONF_DEBUG))
if mail_service.connection_is_valid(): if mail_service.connection_is_valid():
return mail_service return mail_service
@ -65,7 +80,7 @@ class MailNotificationService(BaseNotificationService):
mail = smtplib.SMTP(self._server, self._port, timeout=5) mail = smtplib.SMTP(self._server, self._port, timeout=5)
mail.set_debuglevel(self.debug) mail.set_debuglevel(self.debug)
mail.ehlo_or_helo_if_needed() mail.ehlo_or_helo_if_needed()
if self.starttls == 1: if self.starttls:
mail.starttls() mail.starttls()
mail.ehlo() mail.ehlo()
if self.username and self.password: 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 from homeassistant.helpers import validate_config
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['TwitterAPI==2.4.1'] REQUIREMENTS = ['TwitterAPI==2.4.2']
CONF_CONSUMER_KEY = "consumer_key" CONF_CONSUMER_KEY = "consumer_key"
CONF_CONSUMER_SECRET = "consumer_secret" CONF_CONSUMER_SECRET = "consumer_secret"

View File

@ -6,10 +6,16 @@ https://home-assistant.io/components/sensor.apcupsd/
""" """
import logging 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.components import apcupsd
from homeassistant.const import TEMP_CELSIUS from homeassistant.const import (TEMP_CELSIUS, CONF_RESOURCES)
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = [apcupsd.DOMAIN] DEPENDENCIES = [apcupsd.DOMAIN]
SENSOR_PREFIX = 'UPS ' SENSOR_PREFIX = 'UPS '
@ -92,14 +98,17 @@ INFERRED_UNITS = {
' C': TEMP_CELSIUS, ' 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): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Setup the APCUPSd sensors.""" """Setup the APCUPSd sensors."""
entities = [] entities = []
for resource in config['resources']: for resource in config[CONF_RESOURCES]:
sensor_type = resource.lower() sensor_type = resource.lower()
if sensor_type not in SENSOR_TYPES: 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: if sensor_type.upper() not in apcupsd.DATA.status:
_LOGGER.warning( _LOGGER.warning(
'Sensor type: "%s" does not appear in the APCUPSd status ' 'Sensor type: "%s" does not appear in the APCUPSd status '
'output.', sensor_type) 'output', sensor_type)
entities.append(APCUPSdSensor(apcupsd.DATA, sensor_type)) entities.append(APCUPSdSensor(apcupsd.DATA, sensor_type))

View File

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

View File

@ -101,7 +101,10 @@ class WUndergroundSensor(Entity):
def state(self): def state(self):
"""Return the state of the sensor.""" """Return the state of the sensor."""
if self.rest.data and self._condition in self.rest.data: 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: else:
return STATE_UNKNOWN return STATE_UNKNOWN

View File

@ -81,7 +81,7 @@ def setup(hass, config):
# Add static devices from the config file. # Add static devices from the config file.
devices.extend((address, None) 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: for address, device in devices:
port = pywemo.ouimeaux_device.probe_wemo(address) port = pywemo.ouimeaux_device.probe_wemo(address)

View File

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

View File

@ -1,24 +1,13 @@
"""Typing Helpers for Home-Assistant.""" """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 import homeassistant.core
# security fixes everyone on 3.5 should upgrade "soon"
try:
from typing import NewType
except ImportError:
NewType = None
# pylint: disable=invalid-name # pylint: disable=invalid-name
if NewType:
ConfigType = NewType('ConfigType', Dict[str, Any])
# Custom type for recorder Queries GPSType = Tuple[float, float]
QueryType = NewType('QueryType', Any) ConfigType = Dict[str, Any]
HomeAssistantType = homeassistant.core.HomeAssistant
# Duplicates for 3.5.1 # Custom type for recorder Queries
# pylint: disable=invalid-name QueryType = Any
else:
ConfigType = Dict[str, Any] # type: ignore
# Custom type for recorder Queries
QueryType = Any # type: ignore

View File

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

View File

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

View File

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

View File

@ -110,16 +110,19 @@ class TestDemoClimate(unittest.TestCase):
def test_set_operation_bad_attr(self): def test_set_operation_bad_attr(self):
"""Test setting operation mode without required attribute.""" """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) climate.set_operation_mode(self.hass, None, ENTITY_CLIMATE)
self.hass.pool.block_till_done() 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): def test_set_operation(self):
"""Test setting of new operation mode.""" """Test setting of new operation mode."""
climate.set_operation_mode(self.hass, "Heat", ENTITY_CLIMATE) climate.set_operation_mode(self.hass, "Heat", ENTITY_CLIMATE)
self.hass.pool.block_till_done() 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): def test_set_away_mode_bad_attr(self):
"""Test setting the away mode without required attribute.""" """Test setting the away mode without required attribute."""

View File

@ -1,10 +1,10 @@
"""The tests for the device tracker component.""" """The tests for the device tracker component."""
# pylint: disable=protected-access,too-many-public-methods # pylint: disable=protected-access,too-many-public-methods
import logging
import unittest import unittest
from unittest.mock import patch from unittest.mock import patch
from datetime import datetime, timedelta from datetime import datetime, timedelta
import os import os
import tempfile
from homeassistant.loader import get_component from homeassistant.loader import get_component
import homeassistant.util.dt as dt_util 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, ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN,
STATE_HOME, STATE_NOT_HOME, CONF_PLATFORM) STATE_HOME, STATE_NOT_HOME, CONF_PLATFORM)
import homeassistant.components.device_tracker as device_tracker import homeassistant.components.device_tracker as device_tracker
from homeassistant.exceptions import HomeAssistantError
from tests.common import ( 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): class TestComponentsDeviceTracker(unittest.TestCase):
"""Test the Device tracker.""" """Test the Device tracker."""
hass = None # HomeAssistant
yaml_devices = None # type: str
def setUp(self): # pylint: disable=invalid-name def setUp(self): # pylint: disable=invalid-name
"""Setup things to be run when tests are started.""" """Setup things to be run when tests are started."""
self.hass = get_test_home_assistant() 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 def test_reading_broken_yaml_config(self): # pylint: disable=no-self-use
"""Test when known devices contains invalid data.""" """Test when known devices contains invalid data."""
with tempfile.NamedTemporaryFile() as fpt: files = {'empty.yaml': '',
# file is empty 'bad.yaml': '100',
assert device_tracker.load_config(fpt.name, None, False, 0) == [] 'ok.yaml': 'my_device:\n name: Device'}
with patch_yaml_files(files):
fpt.write('100'.encode('utf-8')) # File is empty
fpt.flush() assert device_tracker.load_config('empty.yaml', None, False) == []
# File contains a non-dict format
# file contains a non-dict format assert device_tracker.load_config('bad.yaml', None, False) == []
assert device_tracker.load_config(fpt.name, None, False, 0) == [] # A file that works fine
assert len(device_tracker.load_config('ok.yaml', None, False)) == 1
def test_reading_yaml_config(self): def test_reading_yaml_config(self):
"""Test the rendering of the YAML configuration.""" """Test the rendering of the YAML configuration."""
dev_id = 'test' dev_id = 'test'
device = device_tracker.Device( 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', 'AB:CD:EF:GH:IJ', 'Test name', picture='http://test.picture',
away_hide=True) away_hide=True)
device_tracker.update_config(self.yaml_devices, dev_id, device) 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, 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.dev_id, config.dev_id)
self.assertEqual(device.track, config.track) self.assertEqual(device.track, config.track)
self.assertEqual(device.mac, config.mac) 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.away_hide, config.away_hide)
self.assertEqual(device.consider_home, config.consider_home) 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): def test_setup_without_yaml_file(self):
"""Test with no YAML file.""" """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.""" """Test the adding of unknown devices to configuration file."""
scanner = get_component('device_tracker.test').SCANNER scanner = get_component('device_tracker.test').SCANNER
scanner.reset() scanner.reset()
@ -90,7 +132,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
self.assertTrue(device_tracker.setup(self.hass, { self.assertTrue(device_tracker.setup(self.hass, {
device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}})) device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}}))
config = device_tracker.load_config(self.yaml_devices, self.hass, config = device_tracker.load_config(self.yaml_devices, self.hass,
timedelta(seconds=0), 0) timedelta(seconds=0))
assert len(config) == 1 assert len(config) == 1
assert config[0].dev_id == 'dev1' assert config[0].dev_id == 'dev1'
assert config[0].track assert config[0].track
@ -99,7 +141,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
"""Test the Gravatar generation.""" """Test the Gravatar generation."""
dev_id = 'test' dev_id = 'test'
device = device_tracker.Device( 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') 'AB:CD:EF:GH:IJ', 'Test name', gravatar='test@example.com')
gravatar_url = ("https://www.gravatar.com/avatar/" gravatar_url = ("https://www.gravatar.com/avatar/"
"55502f40dc8b7c769880b10874abc9d0.jpg?s=80&d=wavatar") "55502f40dc8b7c769880b10874abc9d0.jpg?s=80&d=wavatar")
@ -109,7 +151,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
"""Test that Gravatar overrides picture.""" """Test that Gravatar overrides picture."""
dev_id = 'test' dev_id = 'test'
device = device_tracker.Device( 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', 'AB:CD:EF:GH:IJ', 'Test name', picture='http://test.picture',
gravatar='test@example.com') gravatar='test@example.com')
gravatar_url = ("https://www.gravatar.com/avatar/" 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.dict(device_tracker.DISCOVERY_PLATFORMS, {'test': 'test'}):
with patch.object(scanner, 'scan_devices') as mock_scan: with patch.object(scanner, 'scan_devices') as mock_scan:
self.assertTrue(device_tracker.setup(self.hass, { self.assertTrue(device_tracker.setup(self.hass,
device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}})) TEST_PLATFORM))
fire_service_discovered(self.hass, 'test', {}) fire_service_discovered(self.hass, 'test', {})
self.assertTrue(mock_scan.called) self.assertTrue(mock_scan.called)
@ -139,9 +181,9 @@ class TestComponentsDeviceTracker(unittest.TestCase):
with patch('homeassistant.components.device_tracker.dt_util.utcnow', with patch('homeassistant.components.device_tracker.dt_util.utcnow',
return_value=register_time): return_value=register_time):
self.assertTrue(device_tracker.setup(self.hass, { self.assertTrue(device_tracker.setup(self.hass, {
'device_tracker': { device_tracker.DOMAIN: {
'platform': 'test', CONF_PLATFORM: 'test',
'consider_home': 59, device_tracker.CONF_CONSIDER_HOME: 59,
}})) }}))
self.assertEqual(STATE_HOME, self.assertEqual(STATE_HOME,
@ -165,11 +207,11 @@ class TestComponentsDeviceTracker(unittest.TestCase):
picture = 'http://placehold.it/200x200' picture = 'http://placehold.it/200x200'
device = device_tracker.Device( 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) friendly_name, picture, away_hide=True)
device_tracker.update_config(self.yaml_devices, dev_id, device) 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 attrs = self.hass.states.get(entity_id).attributes
@ -181,15 +223,14 @@ class TestComponentsDeviceTracker(unittest.TestCase):
dev_id = 'test_entity' dev_id = 'test_entity'
entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id)
device = device_tracker.Device( 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) away_hide=True)
device_tracker.update_config(self.yaml_devices, dev_id, device) device_tracker.update_config(self.yaml_devices, dev_id, device)
scanner = get_component('device_tracker.test').SCANNER scanner = get_component('device_tracker.test').SCANNER
scanner.reset() scanner.reset()
self.assertTrue(device_tracker.setup(self.hass, { self.assertTrue(device_tracker.setup(self.hass, TEST_PLATFORM))
device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}}))
self.assertTrue(self.hass.states.get(entity_id) self.assertTrue(self.hass.states.get(entity_id)
.attributes.get(ATTR_HIDDEN)) .attributes.get(ATTR_HIDDEN))
@ -199,15 +240,14 @@ class TestComponentsDeviceTracker(unittest.TestCase):
dev_id = 'test_entity' dev_id = 'test_entity'
entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id) entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id)
device = device_tracker.Device( 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) away_hide=True)
device_tracker.update_config(self.yaml_devices, dev_id, device) device_tracker.update_config(self.yaml_devices, dev_id, device)
scanner = get_component('device_tracker.test').SCANNER scanner = get_component('device_tracker.test').SCANNER
scanner.reset() scanner.reset()
self.assertTrue(device_tracker.setup(self.hass, { self.assertTrue(device_tracker.setup(self.hass, TEST_PLATFORM))
device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}}))
state = self.hass.states.get(device_tracker.ENTITY_ID_ALL_DEVICES) state = self.hass.states.get(device_tracker.ENTITY_ID_ALL_DEVICES)
self.assertIsNotNone(state) self.assertIsNotNone(state)
@ -217,40 +257,31 @@ class TestComponentsDeviceTracker(unittest.TestCase):
@patch('homeassistant.components.device_tracker.DeviceTracker.see') @patch('homeassistant.components.device_tracker.DeviceTracker.see')
def test_see_service(self, mock_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.""" """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 = { params = {
'dev_id': chr(233), # e' acute accent from icloud 'dev_id': 'some_device',
'host_name': 'example.com', 'host_name': 'example.com',
'location_name': 'Work', 'location_name': 'Work',
'gps': [.3, .8] 'gps': [.3, .8]
} }
device_tracker.see(self.hass, **params) device_tracker.see(self.hass, **params)
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
assert mock_see.call_count == 1
mock_see.assert_called_once_with(**params) 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.""" """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_1', host_name='hello')
device_tracker.see(self.hass, 'mac_2', 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() self.hass.pool.block_till_done()
config = device_tracker.load_config(self.yaml_devices, self.hass, config = device_tracker.load_config(self.yaml_devices, self.hass,
timedelta(seconds=0), 0) timedelta(seconds=0))
assert len(config) == 2 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.""" """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') device_tracker.see(self.hass, dev_id='hello-world')
config = device_tracker.load_config(self.yaml_devices, self.hass, config = device_tracker.load_config(self.yaml_devices, self.hass,
timedelta(seconds=0), 0) timedelta(seconds=0))
assert len(config) == 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 from homeassistant import bootstrap, const
import homeassistant.components.device_tracker as device_tracker import homeassistant.components.device_tracker as device_tracker
import homeassistant.components.http as http import homeassistant.components.http as http
from homeassistant.const import CONF_PLATFORM
from tests.common import get_test_home_assistant, get_test_instance_port from tests.common import get_test_home_assistant, get_test_instance_port
SERVER_PORT = get_test_instance_port() SERVER_PORT = get_test_instance_port()
HTTP_BASE_URL = "http://127.0.0.1:{}".format(SERVER_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.""" """Helper method to generate URLs."""
data = data or {}
data = "&".join(["{}={}".format(name, value) for data = "&".join(["{}={}".format(name, value) for
name, value in data.items()]) name, value in data.items()])
return "{}{}locative?{}".format(HTTP_BASE_URL, const.URL_API, data) return "{}{}locative?{}".format(HTTP_BASE_URL, const.URL_API, data)
@ -26,7 +28,7 @@ def _url(data={}):
def setUpModule(): # pylint: disable=invalid-name def setUpModule(): # pylint: disable=invalid-name
"""Initalize a Home Assistant server.""" """Initalize a Home Assistant server."""
global hass global hass # pylint: disable=invalid-name
hass = get_test_home_assistant() hass = get_test_home_assistant()
bootstrap.setup_component(hass, http.DOMAIN, { bootstrap.setup_component(hass, http.DOMAIN, {
@ -38,7 +40,7 @@ def setUpModule(): # pylint: disable=invalid-name
# Set up device tracker # Set up device tracker
bootstrap.setup_component(hass, device_tracker.DOMAIN, { bootstrap.setup_component(hass, device_tracker.DOMAIN, {
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.""" """The tests for the MQTT device tracker platform."""
import unittest import unittest
from unittest.mock import patch
import logging
import os import os
from homeassistant.bootstrap import _setup_component from homeassistant.bootstrap import _setup_component
@ -9,6 +11,8 @@ from homeassistant.const import CONF_PLATFORM
from tests.common import ( from tests.common import (
get_test_home_assistant, mock_mqtt_component, fire_mqtt_message) get_test_home_assistant, mock_mqtt_component, fire_mqtt_message)
_LOGGER = logging.getLogger(__name__)
class TestComponentsDeviceTrackerMQTT(unittest.TestCase): class TestComponentsDeviceTrackerMQTT(unittest.TestCase):
"""Test MQTT device tracker platform.""" """Test MQTT device tracker platform."""
@ -25,6 +29,27 @@ class TestComponentsDeviceTrackerMQTT(unittest.TestCase):
except FileNotFoundError: except FileNotFoundError:
pass 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): def test_new_message(self):
"""Test new message.""" """Test new message."""
dev_id = 'paulus' 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.""" """Write a device tracker known devices file to be used."""
device_tracker.update_config( device_tracker.update_config(
KNOWN_DEV_YAML_PATH, 'device_1', device_tracker.Device( 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')) picture='http://example.com/dev1.jpg'))
device_tracker.update_config( device_tracker.update_config(
KNOWN_DEV_YAML_PATH, 'device_2', device_tracker.Device( 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')) picture='http://example.com/dev2.jpg'))
@ -83,7 +83,8 @@ class TestDeviceSunLightTrigger(unittest.TestCase):
self.assertTrue(light.is_on(self.hass)) 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.""" """Test lights turn off when everyone leaves the house."""
light.turn_on(self.hass) light.turn_on(self.hass)
@ -99,7 +100,8 @@ class TestDeviceSunLightTrigger(unittest.TestCase):
self.assertFalse(light.is_on(self.hass)) 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.""" """Test lights turn on when coming home after sun set."""
light.turn_off(self.hass) light.turn_off(self.hass)
ensure_sun_set(self.hass) ensure_sun_set(self.hass)