Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| db3bfad0b5 | |||
| 2b1416c514 | |||
| b9154158e8 | |||
| b43bf62347 | |||
| c6b6ab1b79 | |||
| 07148fc580 | |||
| bc600b8f32 | |||
| dd4611064f | |||
| 71aa1a2f3c | |||
| 0cfa5e5f67 | |||
| f0ec51711c | |||
| d6ca930427 | |||
| 7bce8bc33f | |||
| 36785296ce | |||
| 1b46ed5045 |
@@ -26,7 +26,7 @@ DEFAULT_NAME = 'SimpliSafe'
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_CODE): cv.positive_int,
|
||||
vol.Optional(CONF_CODE): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
@@ -116,9 +116,6 @@ class Thermostat(ClimateDevice):
|
||||
return self.target_temperature_low
|
||||
elif self.operation_mode == 'cool':
|
||||
return self.target_temperature_high
|
||||
else:
|
||||
return (self.target_temperature_low +
|
||||
self.target_temperature_high) / 2
|
||||
|
||||
@property
|
||||
def target_temperature_low(self):
|
||||
@@ -223,19 +220,27 @@ class Thermostat(ClimateDevice):
|
||||
"""Set new target temperature."""
|
||||
if kwargs.get(ATTR_TEMPERATURE) is not None:
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
low_temp = temperature - 1
|
||||
high_temp = temperature + 1
|
||||
low_temp = int(temperature)
|
||||
high_temp = int(temperature)
|
||||
if kwargs.get(ATTR_TARGET_TEMP_LOW) is not None and \
|
||||
kwargs.get(ATTR_TARGET_TEMP_HIGH) is not None:
|
||||
low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW)
|
||||
high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH)
|
||||
high_temp = int(kwargs.get(ATTR_TARGET_TEMP_LOW))
|
||||
low_temp = int(kwargs.get(ATTR_TARGET_TEMP_HIGH))
|
||||
|
||||
if self.hold_temp:
|
||||
self.data.ecobee.set_hold_temp(self.thermostat_index, low_temp,
|
||||
high_temp, "indefinite")
|
||||
_LOGGER.debug("Setting ecobee hold_temp to: low=%s, is=%s, "
|
||||
"high=%s, is=%s", low_temp, isinstance(
|
||||
low_temp, (int, float)), high_temp,
|
||||
isinstance(high_temp, (int, float)))
|
||||
else:
|
||||
self.data.ecobee.set_hold_temp(self.thermostat_index, low_temp,
|
||||
high_temp)
|
||||
_LOGGER.debug("Setting ecobee temp to: low=%s, is=%s, "
|
||||
"high=%s, is=%s", low_temp, isinstance(
|
||||
low_temp, (int, float)), high_temp,
|
||||
isinstance(high_temp, (int, float)))
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set HVAC mode (auto, auxHeatOnly, cool, heat, off)."""
|
||||
|
||||
@@ -70,8 +70,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
node = zwave.NETWORK.nodes[discovery_info[ATTR_NODE_ID]]
|
||||
value = node.values[discovery_info[ATTR_VALUE_ID]]
|
||||
value.set_change_verified(False)
|
||||
if value.index != 1: # Only add 1 device
|
||||
return
|
||||
add_devices([ZWaveClimate(value, temp_unit)])
|
||||
_LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s",
|
||||
discovery_info, zwave.NETWORK)
|
||||
|
||||
@@ -28,8 +28,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
pywink.set_bearer_token(token)
|
||||
|
||||
add_devices(WinkCoverDevice(shade) for shade, door in
|
||||
add_devices(WinkCoverDevice(shade) for shade in
|
||||
pywink.get_shades())
|
||||
add_devices(WinkCoverDevice(door) for door in
|
||||
pywink.get_garage_doors())
|
||||
|
||||
|
||||
class WinkCoverDevice(WinkDevice, CoverDevice):
|
||||
|
||||
@@ -338,7 +338,7 @@ class Device(Entity):
|
||||
attr[ATTR_BATTERY] = self.battery
|
||||
|
||||
if self.attributes:
|
||||
for key, value in self.attributes:
|
||||
for key, value in self.attributes.items():
|
||||
attr[key] = value
|
||||
|
||||
return attr
|
||||
|
||||
@@ -15,12 +15,11 @@ from homeassistant.components.device_tracker import (PLATFORM_SCHEMA,
|
||||
ATTR_ATTRIBUTES)
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle, datetime as dt_util
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.util import datetime as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30)
|
||||
|
||||
CONF_CLIENT_ID = 'client_id'
|
||||
CONF_SECRET = 'secret'
|
||||
CONF_DEVICES = 'devices'
|
||||
@@ -53,7 +52,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
def setup_scanner(hass, config: dict, see):
|
||||
"""Validate the configuration and return an Automatic scanner."""
|
||||
try:
|
||||
AutomaticDeviceScanner(config, see)
|
||||
AutomaticDeviceScanner(hass, config, see)
|
||||
except requests.HTTPError as err:
|
||||
_LOGGER.error(str(err))
|
||||
return False
|
||||
@@ -61,11 +60,14 @@ def setup_scanner(hass, config: dict, see):
|
||||
return True
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
# pylint: disable=too-few-public-methods
|
||||
class AutomaticDeviceScanner(object):
|
||||
"""A class representing an Automatic device."""
|
||||
|
||||
def __init__(self, config: dict, see) -> None:
|
||||
def __init__(self, hass, config: dict, see) -> None:
|
||||
"""Initialize the automatic device scanner."""
|
||||
self.hass = hass
|
||||
self._devices = config.get(CONF_DEVICES, None)
|
||||
self._access_token_payload = {
|
||||
'username': config.get(CONF_USERNAME),
|
||||
@@ -81,20 +83,10 @@ class AutomaticDeviceScanner(object):
|
||||
self.last_trips = {}
|
||||
self.see = see
|
||||
|
||||
self.scan_devices()
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
|
||||
return [item['id'] for item in self.last_results]
|
||||
|
||||
def get_device_name(self, device):
|
||||
"""Get the device name from id."""
|
||||
vehicle = [item['display_name'] for item in self.last_results
|
||||
if item['id'] == device]
|
||||
|
||||
return vehicle[0]
|
||||
track_utc_time_change(self.hass, self._update_info,
|
||||
second=range(0, 60, 30))
|
||||
|
||||
def _update_headers(self):
|
||||
"""Get the access token from automatic."""
|
||||
@@ -114,10 +106,9 @@ class AutomaticDeviceScanner(object):
|
||||
'Authorization': 'Bearer {}'.format(access_token)
|
||||
}
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS)
|
||||
def _update_info(self) -> None:
|
||||
def _update_info(self, now=None) -> None:
|
||||
"""Update the device info."""
|
||||
_LOGGER.info('Updating devices')
|
||||
_LOGGER.debug('Updating devices %s', now)
|
||||
self._update_headers()
|
||||
|
||||
response = requests.get(URL_VEHICLES, headers=self._headers)
|
||||
@@ -142,6 +133,7 @@ class AutomaticDeviceScanner(object):
|
||||
|
||||
for vehicle in self.last_results:
|
||||
dev_id = vehicle.get('id')
|
||||
host_name = vehicle.get('display_name')
|
||||
|
||||
attrs = {
|
||||
'fuel_level': vehicle.get('fuel_level_percent')
|
||||
@@ -149,6 +141,7 @@ class AutomaticDeviceScanner(object):
|
||||
|
||||
kwargs = {
|
||||
'dev_id': dev_id,
|
||||
'host_name': host_name,
|
||||
'mac': dev_id,
|
||||
ATTR_ATTRIBUTES: attrs
|
||||
}
|
||||
|
||||
@@ -46,12 +46,12 @@ def _conf_preprocess(value):
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: {cv.match_all: vol.Schema(vol.All(_conf_preprocess, {
|
||||
DOMAIN: cv.ordered_dict(vol.All(_conf_preprocess, {
|
||||
vol.Optional(CONF_ENTITIES): vol.Any(cv.entity_ids, None),
|
||||
CONF_VIEW: cv.boolean,
|
||||
CONF_NAME: cv.string,
|
||||
CONF_ICON: cv.icon,
|
||||
}))}
|
||||
}, cv.match_all))
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
# List of ON/OFF state tuples for groupable states
|
||||
|
||||
@@ -10,7 +10,6 @@ import socket
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.config_validation import ensure_list
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT,
|
||||
CONF_WHITELIST)
|
||||
@@ -29,7 +28,10 @@ EVENT = 'pilight_received'
|
||||
|
||||
# The pilight code schema depends on the protocol
|
||||
# Thus only require to have the protocol information
|
||||
RF_CODE_SCHEMA = vol.Schema({vol.Required(ATTR_PROTOCOL): cv.string},
|
||||
# Ensure that protocol is in a list otherwise segfault in pilight-daemon
|
||||
# https://github.com/pilight/pilight/issues/296
|
||||
RF_CODE_SCHEMA = vol.Schema({vol.Required(ATTR_PROTOCOL):
|
||||
vol.All(cv.ensure_list, [cv.string])},
|
||||
extra=vol.ALLOW_EXTRA)
|
||||
SERVICE_NAME = 'send'
|
||||
|
||||
@@ -71,12 +73,9 @@ def setup(hass, config):
|
||||
|
||||
def send_code(call):
|
||||
"""Send RF code to the pilight-daemon."""
|
||||
message_data = call.data
|
||||
|
||||
# Patch data because of bug:
|
||||
# https://github.com/pilight/pilight/issues/296
|
||||
# Protocol has to be in a list otherwise segfault in pilight-daemon
|
||||
message_data['protocol'] = ensure_list(message_data['protocol'])
|
||||
# Change type to dict from mappingproxy
|
||||
# since data has to be JSON serializable
|
||||
message_data = dict(call.data)
|
||||
|
||||
try:
|
||||
pilight_client.send_code(message_data)
|
||||
|
||||
@@ -20,6 +20,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
|
||||
EVENT_TIME_CHANGED, MATCH_ALL)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant.helpers.typing import ConfigType, QueryType
|
||||
import homeassistant.util.dt as dt_util
|
||||
@@ -40,10 +41,9 @@ QUERY_RETRY_WAIT = 0.1
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_PURGE_DAYS): vol.All(vol.Coerce(int),
|
||||
vol.Range(min=1)),
|
||||
# pylint: disable=no-value-for-parameter
|
||||
vol.Optional(CONF_DB_URL): vol.Url(),
|
||||
vol.Optional(CONF_PURGE_DAYS):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.Optional(CONF_DB_URL): cv.string,
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# coding: utf-8
|
||||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 29
|
||||
PATCH_VERSION = '0.dev0'
|
||||
MINOR_VERSION = 28
|
||||
PATCH_VERSION = '2'
|
||||
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
|
||||
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
|
||||
REQUIRED_PYTHON_VER = (3, 4)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Helpers for config validation using voluptuous."""
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
@@ -290,6 +291,27 @@ def url(value: Any) -> str:
|
||||
raise vol.Invalid('invalid url')
|
||||
|
||||
|
||||
def ordered_dict(value_validator, key_validator=match_all):
|
||||
"""Validate an ordered dict validator that maintains ordering.
|
||||
|
||||
value_validator will be applied to each value of the dictionary.
|
||||
key_validator (optional) will be applied to each key of the dictionary.
|
||||
"""
|
||||
item_validator = vol.Schema({key_validator: value_validator})
|
||||
|
||||
def validator(value):
|
||||
"""Validate ordered dict."""
|
||||
config = OrderedDict()
|
||||
|
||||
for key, val in value.items():
|
||||
v_res = item_validator({key: val})
|
||||
config.update(v_res)
|
||||
|
||||
return config
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
# Validator helpers
|
||||
|
||||
def key_dependency(key, dependency):
|
||||
|
||||
@@ -29,10 +29,10 @@ from homeassistant.const import (
|
||||
SERVICE_CLOSE, SERVICE_LOCK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY,
|
||||
SERVICE_MEDIA_SEEK, SERVICE_MOVE_DOWN, SERVICE_MOVE_UP, SERVICE_OPEN,
|
||||
SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_UNLOCK, SERVICE_VOLUME_MUTE,
|
||||
SERVICE_VOLUME_SET, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, STATE_CLOSED, STATE_LOCKED,
|
||||
STATE_OFF, STATE_ON, STATE_OPEN, STATE_PAUSED, STATE_PLAYING,
|
||||
STATE_UNKNOWN, STATE_UNLOCKED)
|
||||
SERVICE_VOLUME_SET, SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_TRIGGERED, STATE_CLOSED, STATE_LOCKED, STATE_OFF, STATE_ON,
|
||||
STATE_OPEN, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN, STATE_UNLOCKED)
|
||||
from homeassistant.core import State
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -77,6 +77,8 @@ SERVICE_TO_STATE = {
|
||||
SERVICE_OPEN: STATE_OPEN,
|
||||
SERVICE_MOVE_UP: STATE_OPEN,
|
||||
SERVICE_MOVE_DOWN: STATE_CLOSED,
|
||||
SERVICE_OPEN_COVER: STATE_OPEN,
|
||||
SERVICE_CLOSE_COVER: STATE_CLOSED
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,8 +6,9 @@ import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.device_tracker.automatic import (
|
||||
URL_AUTHORIZE, URL_VEHICLES, URL_TRIPS, setup_scanner,
|
||||
AutomaticDeviceScanner)
|
||||
URL_AUTHORIZE, URL_VEHICLES, URL_TRIPS, setup_scanner)
|
||||
|
||||
from tests.common import get_test_home_assistant
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -205,6 +206,7 @@ class TestAutomatic(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data."""
|
||||
self.hass = get_test_home_assistant()
|
||||
|
||||
def tearDown(self):
|
||||
"""Tear down test data."""
|
||||
@@ -221,7 +223,7 @@ class TestAutomatic(unittest.TestCase):
|
||||
'secret': CLIENT_SECRET
|
||||
}
|
||||
|
||||
self.assertFalse(setup_scanner(None, config, self.see_mock))
|
||||
self.assertFalse(setup_scanner(self.hass, config, self.see_mock))
|
||||
|
||||
@patch('requests.get', side_effect=mocked_requests)
|
||||
@patch('requests.post', side_effect=mocked_requests)
|
||||
@@ -235,20 +237,4 @@ class TestAutomatic(unittest.TestCase):
|
||||
'secret': CLIENT_SECRET
|
||||
}
|
||||
|
||||
self.assertTrue(setup_scanner(None, config, self.see_mock))
|
||||
|
||||
@patch('requests.get', side_effect=mocked_requests)
|
||||
@patch('requests.post', side_effect=mocked_requests)
|
||||
def test_device_attributes(self, mock_get, mock_post):
|
||||
"""Test device attributes are set on load."""
|
||||
config = {
|
||||
'platform': 'automatic',
|
||||
'username': VALID_USERNAME,
|
||||
'password': PASSWORD,
|
||||
'client_id': CLIENT_ID,
|
||||
'secret': CLIENT_SECRET
|
||||
}
|
||||
|
||||
scanner = AutomaticDeviceScanner(config, self.see_mock)
|
||||
|
||||
self.assertEqual(DISPLAY_NAME, scanner.get_device_name('vid'))
|
||||
self.assertTrue(setup_scanner(self.hass, config, self.see_mock))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""The tests for the Group components."""
|
||||
# pylint: disable=protected-access,too-many-public-methods
|
||||
from collections import OrderedDict
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -220,16 +221,16 @@ class TestComponentsGroup(unittest.TestCase):
|
||||
test_group = group.Group(
|
||||
self.hass, 'init_group', ['light.Bowl', 'light.Ceiling'], False)
|
||||
|
||||
_setup_component(self.hass, 'group', {'group': {
|
||||
'second_group': {
|
||||
group_conf = OrderedDict()
|
||||
group_conf['second_group'] = {
|
||||
'entities': 'light.Bowl, ' + test_group.entity_id,
|
||||
'icon': 'mdi:work',
|
||||
'view': True,
|
||||
},
|
||||
'test_group': 'hello.world,sensor.happy',
|
||||
'empty_group': {'name': 'Empty Group', 'entities': None},
|
||||
}
|
||||
})
|
||||
}
|
||||
group_conf['test_group'] = 'hello.world,sensor.happy'
|
||||
group_conf['empty_group'] = {'name': 'Empty Group', 'entities': None}
|
||||
|
||||
_setup_component(self.hass, 'group', {'group': group_conf})
|
||||
|
||||
group_state = self.hass.states.get(
|
||||
group.ENTITY_ID_FORMAT.format('second_group'))
|
||||
@@ -241,6 +242,7 @@ class TestComponentsGroup(unittest.TestCase):
|
||||
group_state.attributes.get(ATTR_ICON))
|
||||
self.assertTrue(group_state.attributes.get(group.ATTR_VIEW))
|
||||
self.assertTrue(group_state.attributes.get(ATTR_HIDDEN))
|
||||
self.assertEqual(1, group_state.attributes.get(group.ATTR_ORDER))
|
||||
|
||||
group_state = self.hass.states.get(
|
||||
group.ENTITY_ID_FORMAT.format('test_group'))
|
||||
@@ -251,6 +253,7 @@ class TestComponentsGroup(unittest.TestCase):
|
||||
self.assertIsNone(group_state.attributes.get(ATTR_ICON))
|
||||
self.assertIsNone(group_state.attributes.get(group.ATTR_VIEW))
|
||||
self.assertIsNone(group_state.attributes.get(ATTR_HIDDEN))
|
||||
self.assertEqual(2, group_state.attributes.get(group.ATTR_ORDER))
|
||||
|
||||
def test_groups_get_unique_names(self):
|
||||
"""Two groups with same name should both have a unique entity id."""
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Test config validators."""
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
import os
|
||||
import tempfile
|
||||
@@ -367,3 +369,51 @@ def test_has_at_least_one_key():
|
||||
|
||||
for value in ({'beer': None}, {'soda': None}):
|
||||
schema(value)
|
||||
|
||||
|
||||
def test_ordered_dict_order():
|
||||
"""Test ordered_dict validator."""
|
||||
schema = vol.Schema(cv.ordered_dict(int, cv.string))
|
||||
|
||||
val = OrderedDict()
|
||||
val['first'] = 1
|
||||
val['second'] = 2
|
||||
|
||||
validated = schema(val)
|
||||
|
||||
assert isinstance(validated, OrderedDict)
|
||||
assert ['first', 'second'] == list(validated.keys())
|
||||
|
||||
|
||||
def test_ordered_dict_key_validator():
|
||||
"""Test ordered_dict key validator."""
|
||||
schema = vol.Schema(cv.ordered_dict(cv.match_all, cv.string))
|
||||
|
||||
with pytest.raises(vol.Invalid):
|
||||
schema({None: 1})
|
||||
|
||||
schema({'hello': 'world'})
|
||||
|
||||
schema = vol.Schema(cv.ordered_dict(cv.match_all, int))
|
||||
|
||||
with pytest.raises(vol.Invalid):
|
||||
schema({'hello': 1})
|
||||
|
||||
schema({1: 'works'})
|
||||
|
||||
|
||||
def test_ordered_dict_value_validator():
|
||||
"""Test ordered_dict validator."""
|
||||
schema = vol.Schema(cv.ordered_dict(cv.string))
|
||||
|
||||
with pytest.raises(vol.Invalid):
|
||||
schema({'hello': None})
|
||||
|
||||
schema({'hello': 'world'})
|
||||
|
||||
schema = vol.Schema(cv.ordered_dict(int))
|
||||
|
||||
with pytest.raises(vol.Invalid):
|
||||
schema({'hello': 'world'})
|
||||
|
||||
schema({'hello': 5})
|
||||
|
||||
Reference in New Issue
Block a user