diff --git a/homeassistant/components/calendar/google.py b/homeassistant/components/calendar/google.py index a8763e8ca9ea06..6c26c65ebe77fd 100644 --- a/homeassistant/components/calendar/google.py +++ b/homeassistant/components/calendar/google.py @@ -11,6 +11,7 @@ from homeassistant.components.calendar import CalendarEventDevice from homeassistant.components.google import ( CONF_CAL_ID, CONF_ENTITIES, CONF_TRACK, TOKEN_FILE, + CONF_IGNORE_AVAILABILITY, CONF_SEARCH, GoogleCalendarService) from homeassistant.util import Throttle, dt @@ -18,7 +19,7 @@ DEFAULT_GOOGLE_SEARCH_PARAMS = { 'orderBy': 'startTime', - 'maxResults': 1, + 'maxResults': 5, 'singleEvents': True, } @@ -45,18 +46,22 @@ class GoogleCalendarEventDevice(CalendarEventDevice): def __init__(self, hass, calendar_service, calendar, data): """Create the Calendar event device.""" self.data = GoogleCalendarData(calendar_service, calendar, - data.get('search', None)) + data.get(CONF_SEARCH), + data.get(CONF_IGNORE_AVAILABILITY)) + super().__init__(hass, data) class GoogleCalendarData(object): """Class to utilize calendar service object to get next event.""" - def __init__(self, calendar_service, calendar_id, search=None): + def __init__(self, calendar_service, calendar_id, search, + ignore_availability): """Set up how we are going to search the google calendar.""" self.calendar_service = calendar_service self.calendar_id = calendar_id self.search = search + self.ignore_availability = ignore_availability self.event = None @Throttle(MIN_TIME_BETWEEN_UPDATES) @@ -80,5 +85,17 @@ def update(self): result = events.list(**params).execute() items = result.get('items', []) - self.event = items[0] if len(items) == 1 else None + + new_event = None + for item in items: + if (not self.ignore_availability + and 'transparency' in item.keys()): + if item['transparency'] == 'opaque': + new_event = item + break + else: + new_event = item + break + + self.event = new_event return True diff --git a/homeassistant/components/calendar/services.yaml b/homeassistant/components/calendar/services.yaml index 61ff4345fbec30..ebf0c7b1591ab0 100644 --- a/homeassistant/components/calendar/services.yaml +++ b/homeassistant/components/calendar/services.yaml @@ -1,21 +1,26 @@ # Describes the format for available calendar services -todoist: - new_task: - description: Create a new task and add it to a project. - fields: - content: - description: The name of the task (Required). - example: Pick up the mail - project: - description: The name of the project this task should belong to. Defaults to Inbox (Optional). - example: Errands - labels: - description: Any labels that you want to apply to this task, separated by a comma (Optional). - example: Chores,Deliveries - priority: - description: The priority of this task, from 1 (normal) to 4 (urgent) (Optional). - example: 2 - due_date: - description: The day this task is due, in format YYYY-MM-DD (Optional). - example: "2018-04-01" +todoist_new_task: + description: Create a new task and add it to a project. + fields: + content: + description: The name of the task. + example: Pick up the mail + project: + description: The name of the project this task should belong to. Defaults to Inbox. + example: Errands + labels: + description: Any labels that you want to apply to this task, separated by a comma. + example: Chores,Deliveries + priority: + description: The priority of this task, from 1 (normal) to 4 (urgent). + example: 2 + due_date_string: + description: The day this task is due, in natural language. + example: "tomorrow" + due_date_lang: + description: The language of due_date_string. + example: "en" + due_date: + description: The day this task is due, in format YYYY-MM-DD. + example: "2018-04-01" diff --git a/homeassistant/components/calendar/todoist.py b/homeassistant/components/calendar/todoist.py index 02840c7d0ee803..b70e44456db822 100644 --- a/homeassistant/components/calendar/todoist.py +++ b/homeassistant/components/calendar/todoist.py @@ -41,6 +41,14 @@ DESCRIPTION = 'description' # Calendar Platform: Used in the '_get_date()' method DATETIME = 'dateTime' +# Service Call: When is this task due (in natural language)? +DUE_DATE_STRING = 'due_date_string' +# Service Call: The language of DUE_DATE_STRING +DUE_DATE_LANG = 'due_date_lang' +# Service Call: The available options of DUE_DATE_LANG +DUE_DATE_VALID_LANGS = ['en', 'da', 'pl', 'zh', 'ko', 'de', + 'pt', 'ja', 'it', 'fr', 'sv', 'ru', + 'es', 'nl'] # Attribute: When is this task due? # Service Call: When is this task due? DUE_DATE = 'due_date' @@ -83,7 +91,11 @@ vol.Optional(PROJECT_NAME, default='inbox'): vol.All(cv.string, vol.Lower), vol.Optional(LABELS): cv.ensure_list_csv, vol.Optional(PRIORITY): vol.All(vol.Coerce(int), vol.Range(min=1, max=4)), - vol.Optional(DUE_DATE): cv.string, + + vol.Exclusive(DUE_DATE_STRING, 'due_date'): cv.string, + vol.Optional(DUE_DATE_LANG): + vol.All(cv.string, vol.In(DUE_DATE_VALID_LANGS)), + vol.Exclusive(DUE_DATE, 'due_date'): cv.string, }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -186,6 +198,12 @@ def handle_new_task(call): if PRIORITY in call.data: item.update(priority=call.data[PRIORITY]) + if DUE_DATE_STRING in call.data: + item.update(date_string=call.data[DUE_DATE_STRING]) + + if DUE_DATE_LANG in call.data: + item.update(date_lang=call.data[DUE_DATE_LANG]) + if DUE_DATE in call.data: due_date = dt.parse_datetime(call.data[DUE_DATE]) if due_date is None: diff --git a/homeassistant/components/cover/gogogate2.py b/homeassistant/components/cover/gogogate2.py index c2bdc9c5472fd6..99da248b094562 100644 --- a/homeassistant/components/cover/gogogate2.py +++ b/homeassistant/components/cover/gogogate2.py @@ -11,7 +11,7 @@ from homeassistant.components.cover import ( CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE) from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, STATE_CLOSED, STATE_UNKNOWN, + CONF_USERNAME, CONF_PASSWORD, STATE_CLOSED, CONF_IP_ADDRESS, CONF_NAME) import homeassistant.helpers.config_validation as cv @@ -50,7 +50,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(MyGogogate2Device( mygogogate2, door, name) for door in devices) - return except (TypeError, KeyError, NameError, ValueError) as ex: _LOGGER.error("%s", ex) @@ -60,7 +59,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ''.format(ex), title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID) - return class MyGogogate2Device(CoverDevice): @@ -72,7 +70,7 @@ def __init__(self, mygogogate2, device, name): self.device_id = device['door'] self._name = name or device['name'] self._status = device['status'] - self.available = None + self._available = None @property def name(self): @@ -97,24 +95,22 @@ def supported_features(self): @property def available(self): """Could the device be accessed during the last update call.""" - return self.available + return self._available def close_cover(self, **kwargs): """Issue close command to cover.""" self.mygogogate2.close_device(self.device_id) - self.schedule_update_ha_state(True) def open_cover(self, **kwargs): """Issue open command to cover.""" self.mygogogate2.open_device(self.device_id) - self.schedule_update_ha_state(True) def update(self): """Update status of cover.""" try: self._status = self.mygogogate2.get_status(self.device_id) - self.available = True + self._available = True except (TypeError, KeyError, NameError, ValueError) as ex: _LOGGER.error("%s", ex) - self._status = STATE_UNKNOWN - self.available = False + self._status = None + self._available = False diff --git a/homeassistant/components/cover/opengarage.py b/homeassistant/components/cover/opengarage.py index d68021d7db3882..028a7a0c9fc8ad 100644 --- a/homeassistant/components/cover/opengarage.py +++ b/homeassistant/components/cover/opengarage.py @@ -18,30 +18,31 @@ _LOGGER = logging.getLogger(__name__) -ATTR_DISTANCE_SENSOR = "distance_sensor" -ATTR_DOOR_STATE = "door_state" -ATTR_SIGNAL_STRENGTH = "wifi_signal" +ATTR_DISTANCE_SENSOR = 'distance_sensor' +ATTR_DOOR_STATE = 'door_state' +ATTR_SIGNAL_STRENGTH = 'wifi_signal' -CONF_DEVICEKEY = "device_key" +CONF_DEVICE_ID = 'device_id' +CONF_DEVICE_KEY = 'device_key' DEFAULT_NAME = 'OpenGarage' DEFAULT_PORT = 80 -STATE_CLOSING = "closing" -STATE_OFFLINE = "offline" -STATE_OPENING = "opening" -STATE_STOPPED = "stopped" +STATE_CLOSING = 'closing' +STATE_OFFLINE = 'offline' +STATE_OPENING = 'opening' +STATE_STOPPED = 'stopped' STATES_MAP = { 0: STATE_CLOSED, - 1: STATE_OPEN + 1: STATE_OPEN, } COVER_SCHEMA = vol.Schema({ - vol.Required(CONF_DEVICEKEY): cv.string, + vol.Required(CONF_DEVICE_KEY): cv.string, vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_NAME): cv.string }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -50,7 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up OpenGarage covers.""" + """Set up the OpenGarage covers.""" covers = [] devices = config.get(CONF_COVERS) @@ -59,8 +60,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): CONF_NAME: device_config.get(CONF_NAME), CONF_HOST: device_config.get(CONF_HOST), CONF_PORT: device_config.get(CONF_PORT), - "device_id": device_config.get(CONF_DEVICE, device_id), - CONF_DEVICEKEY: device_config.get(CONF_DEVICEKEY) + CONF_DEVICE_ID: device_config.get(CONF_DEVICE, device_id), + CONF_DEVICE_KEY: device_config.get(CONF_DEVICE_KEY) } covers.append(OpenGarageCover(hass, args)) @@ -79,8 +80,8 @@ def __init__(self, hass, args): self.hass = hass self._name = args[CONF_NAME] self.device_id = args['device_id'] - self._devicekey = args[CONF_DEVICEKEY] - self._state = STATE_UNKNOWN + self._device_key = args[CONF_DEVICE_KEY] + self._state = None self._state_before_move = None self.dist = None self.signal = None @@ -138,8 +139,8 @@ def update(self): try: status = self._get_status() if self._name is None: - if status["name"] is not None: - self._name = status["name"] + if status['name'] is not None: + self._name = status['name'] state = STATES_MAP.get(status.get('door'), STATE_UNKNOWN) if self._state_before_move is not None: if self._state_before_move != state: @@ -152,7 +153,7 @@ def update(self): self.signal = status.get('rssi') self.dist = status.get('dist') self._available = True - except (requests.exceptions.RequestException) as ex: + except requests.exceptions.RequestException as ex: _LOGGER.error("Unable to connect to OpenGarage device: %(reason)s", dict(reason=ex)) self._state = STATE_OFFLINE @@ -166,15 +167,15 @@ def _get_status(self): def _push_button(self): """Send commands to API.""" url = '{}/cc?dkey={}&click=1'.format( - self.opengarage_url, self._devicekey) + self.opengarage_url, self._device_key) try: response = requests.get(url, timeout=10).json() - if response["result"] == 2: - _LOGGER.error("Unable to control %s: device_key is incorrect.", + if response['result'] == 2: + _LOGGER.error("Unable to control %s: Device key is incorrect", self._name) self._state = self._state_before_move self._state_before_move = None - except (requests.exceptions.RequestException) as ex: + except requests.exceptions.RequestException as ex: _LOGGER.error("Unable to connect to OpenGarage device: %(reason)s", dict(reason=ex)) self._state = self._state_before_move diff --git a/homeassistant/components/cover/tahoma.py b/homeassistant/components/cover/tahoma.py index 6fb8e92e051927..c99076de851c2b 100644 --- a/homeassistant/components/cover/tahoma.py +++ b/homeassistant/components/cover/tahoma.py @@ -16,7 +16,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up Tahoma covers.""" + """Set up the Tahoma covers.""" controller = hass.data[TAHOMA_DOMAIN]['controller'] devices = [] for device in hass.data[TAHOMA_DOMAIN]['devices']['cover']: diff --git a/homeassistant/components/google.py b/homeassistant/components/google.py index 30151ee1a56b5d..b41d4ea33a20b1 100644 --- a/homeassistant/components/google.py +++ b/homeassistant/components/google.py @@ -44,6 +44,7 @@ CONF_TRACK = 'track' CONF_SEARCH = 'search' CONF_OFFSET = 'offset' +CONF_IGNORE_AVAILABILITY = 'ignore_availability' DEFAULT_CONF_TRACK_NEW = True DEFAULT_CONF_OFFSET = '!!' @@ -74,8 +75,9 @@ vol.Required(CONF_NAME): cv.string, vol.Required(CONF_DEVICE_ID): cv.string, vol.Optional(CONF_TRACK): cv.boolean, - vol.Optional(CONF_SEARCH): vol.Any(cv.string, None), + vol.Optional(CONF_SEARCH): cv.string, vol.Optional(CONF_OFFSET): cv.string, + vol.Optional(CONF_IGNORE_AVAILABILITY, default=True): cv.boolean, }) DEVICE_SCHEMA = vol.Schema({ diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py index 8ab91b08a3dcdc..b5ac37b1451b2a 100644 --- a/homeassistant/components/history.py +++ b/homeassistant/components/history.py @@ -118,6 +118,30 @@ def state_changes_during_period(hass, start_time, end_time=None, return states_to_json(hass, states, start_time, entity_ids) +def get_last_state_changes(hass, number_of_states, entity_id): + """Return the last number_of_states.""" + from homeassistant.components.recorder.models import States + + start_time = dt_util.utcnow() + + with session_scope(hass=hass) as session: + query = session.query(States).filter( + (States.last_changed == States.last_updated)) + + if entity_id is not None: + query = query.filter_by(entity_id=entity_id.lower()) + + entity_ids = [entity_id] if entity_id is not None else None + + states = execute( + query.order_by(States.last_updated.desc()).limit(number_of_states)) + + return states_to_json(hass, reversed(states), + start_time, + entity_ids, + include_start_time_state=False) + + def get_states(hass, utc_point_in_time, entity_ids=None, run=None, filters=None): """Return the states at a specific point in time.""" diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py old mode 100644 new mode 100755 index 948e26be291766..06258bcc97a04c --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -28,7 +28,7 @@ TYPES = Registry() _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['HAP-python==1.1.8'] +REQUIREMENTS = ['HAP-python==1.1.9'] CONFIG_SCHEMA = vol.Schema({ @@ -92,6 +92,11 @@ def get_accessory(hass, state, aid, config): return TYPES['HumiditySensor'](hass, state.entity_id, state.name, aid=aid) + elif state.domain == 'binary_sensor' or state.domain == 'device_tracker': + _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'BinarySensor') + return TYPES['BinarySensor'](hass, state.entity_id, + state.name, aid=aid) + elif state.domain == 'cover': # Only add covers that support set_cover_position features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index da45bee9e903f3..ec2c49f5e43991 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -1,21 +1,64 @@ """Extend the basic Accessory and Bridge functions.""" +from datetime import timedelta +from functools import wraps +from inspect import getmodule import logging from pyhap.accessory import Accessory, Bridge, Category from pyhap.accessory_driver import AccessoryDriver -from homeassistant.helpers.event import async_track_state_change +from homeassistant.core import callback +from homeassistant.helpers.event import ( + async_track_state_change, track_point_in_utc_time) +from homeassistant.util import dt as dt_util from .const import ( - ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME, - MANUFACTURER, SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, CHAR_MODEL, - CHAR_NAME, CHAR_SERIAL_NUMBER) + DEBOUNCE_TIMEOUT, ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, + BRIDGE_NAME, MANUFACTURER, SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, + CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) from .util import ( show_setup_message, dismiss_setup_message) _LOGGER = logging.getLogger(__name__) +def debounce(func): + """Decorator function. Debounce callbacks form HomeKit.""" + @callback + def call_later_listener(*args): + """Callback listener called from call_later.""" + # pylint: disable=unsubscriptable-object + nonlocal lastargs, remove_listener + hass = lastargs['hass'] + hass.async_add_job(func, *lastargs['args']) + lastargs = remove_listener = None + + @wraps(func) + def wrapper(*args): + """Wrapper starts async timer. + + The accessory must have 'self.hass' and 'self.entity_id' as attributes. + """ + # pylint: disable=not-callable + hass = args[0].hass + nonlocal lastargs, remove_listener + if remove_listener: + remove_listener() + lastargs = remove_listener = None + lastargs = {'hass': hass, 'args': [*args]} + remove_listener = track_point_in_utc_time( + hass, call_later_listener, + dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT)) + logger.debug('%s: Start %s timeout', args[0].entity_id, + func.__name__.replace('set_', '')) + + remove_listener = None + lastargs = None + name = getmodule(func).__name__ + logger = logging.getLogger(name) + return wrapper + + def add_preload_service(acc, service, chars=None): """Define and return a service to be available for the accessory.""" from pyhap.loader import get_serv_loader, get_char_loader diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py old mode 100644 new mode 100755 index d1c3d84b5177b1..55230d32e935de --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -1,5 +1,6 @@ """Constants used be the HomeKit component.""" # #### MISC #### +DEBOUNCE_TIMEOUT = 0.5 DOMAIN = 'homekit' HOMEKIT_FILE = '.homekit.state' HOMEKIT_NOTIFY_ID = 4663548 @@ -34,35 +35,48 @@ # #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' +SERV_CARBON_DIOXIDE_SENSOR = 'CarbonDioxideSensor' +SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor' +SERV_CONTACT_SENSOR = 'ContactSensor' SERV_HUMIDITY_SENSOR = 'HumiditySensor' # CurrentRelativeHumidity | StatusActive, StatusFault, StatusTampered, # StatusLowBattery, Name +SERV_LEAK_SENSOR = 'LeakSensor' SERV_LIGHTBULB = 'Lightbulb' # On | Brightness, Hue, Saturation, Name +SERV_MOTION_SENSOR = 'MotionSensor' +SERV_OCCUPANCY_SENSOR = 'OccupancySensor' SERV_SECURITY_SYSTEM = 'SecuritySystem' +SERV_SMOKE_SENSOR = 'SmokeSensor' SERV_SWITCH = 'Switch' SERV_TEMPERATURE_SENSOR = 'TemperatureSensor' SERV_THERMOSTAT = 'Thermostat' SERV_WINDOW_COVERING = 'WindowCovering' - # #### Characteristics #### CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100] +CHAR_CARBON_DIOXIDE_DETECTED = 'CarbonDioxideDetected' +CHAR_CARBON_MONOXIDE_DETECTED = 'CarbonMonoxideDetected' CHAR_COLOR_TEMPERATURE = 'ColorTemperature' +CHAR_CONTACT_SENSOR_STATE = 'ContactSensorState' CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState' -CHAR_CURRENT_POSITION = 'CurrentPosition' CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent +CHAR_CURRENT_POSITION = 'CurrentPosition' CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState' CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' CHAR_HUE = 'Hue' # arcdegress | [0, 360] +CHAR_LEAK_DETECTED = 'LeakDetected' CHAR_MANUFACTURER = 'Manufacturer' CHAR_MODEL = 'Model' +CHAR_MOTION_DETECTED = 'MotionDetected' CHAR_NAME = 'Name' +CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected' CHAR_ON = 'On' # boolean CHAR_POSITION_STATE = 'PositionState' CHAR_SATURATION = 'Saturation' # percent CHAR_SERIAL_NUMBER = 'SerialNumber' +CHAR_SMOKE_DETECTED = 'SmokeDetected' CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState' CHAR_TARGET_POSITION = 'TargetPosition' CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState' @@ -71,3 +85,12 @@ # #### Properties #### PROP_CELSIUS = {'minValue': -273, 'maxValue': 999} + +# #### Device Class #### +DEVICE_CLASS_CO2 = 'co2' +DEVICE_CLASS_GAS = 'gas' +DEVICE_CLASS_MOISTURE = 'moisture' +DEVICE_CLASS_MOTION = 'motion' +DEVICE_CLASS_OCCUPANCY = 'occupancy' +DEVICE_CLASS_OPENING = 'opening' +DEVICE_CLASS_SMOKE = 'smoke' diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 3650a948f5dc60..781f52941fcaca 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -46,7 +46,6 @@ def __init__(self, hass, entity_id, display_name, **kwargs): def move_cover(self, value): """Move cover to value if call came from HomeKit.""" - self.char_target_position.set_value(value, should_callback=False) if value != self.current_position: _LOGGER.debug('%s: Set position to %d', self.entity_id, value) self.homekit_target = value diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 018d3cd2e74a5a..4fbfb995859563 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -7,7 +7,7 @@ from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF from . import TYPES -from .accessories import HomeAccessory, add_preload_service +from .accessories import HomeAccessory, add_preload_service, debounce from .const import ( CATEGORY_LIGHT, SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE, CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION) @@ -87,18 +87,17 @@ def set_state(self, value): _LOGGER.debug('%s: Set state to %d', self.entity_id, value) self._flag[CHAR_ON] = True - self.char_on.set_value(value, should_callback=False) if value == 1: self.hass.components.light.turn_on(self.entity_id) elif value == 0: self.hass.components.light.turn_off(self.entity_id) + @debounce def set_brightness(self, value): """Set brightness if call came from HomeKit.""" _LOGGER.debug('%s: Set brightness to %d', self.entity_id, value) self._flag[CHAR_BRIGHTNESS] = True - self.char_brightness.set_value(value, should_callback=False) if value != 0: self.hass.components.light.turn_on( self.entity_id, brightness_pct=value) @@ -109,14 +108,12 @@ def set_color_temperature(self, value): """Set color temperature if call came from HomeKit.""" _LOGGER.debug('%s: Set color temp to %s', self.entity_id, value) self._flag[CHAR_COLOR_TEMPERATURE] = True - self.char_color_temperature.set_value(value, should_callback=False) self.hass.components.light.turn_on(self.entity_id, color_temp=value) def set_saturation(self, value): """Set saturation if call came from HomeKit.""" _LOGGER.debug('%s: Set saturation to %d', self.entity_id, value) self._flag[CHAR_SATURATION] = True - self.char_saturation.set_value(value, should_callback=False) self._saturation = value self.set_color() @@ -124,7 +121,6 @@ def set_hue(self, value): """Set hue if call came from HomeKit.""" _LOGGER.debug('%s: Set hue to %d', self.entity_id, value) self._flag[CHAR_HUE] = True - self.char_hue.set_value(value, should_callback=False) self._hue = value self.set_color() @@ -150,7 +146,7 @@ def update_state(self, entity_id=None, old_state=None, new_state=None): if state in (STATE_ON, STATE_OFF): self._state = 1 if state == STATE_ON else 0 if not self._flag[CHAR_ON] and self.char_on.value != self._state: - self.char_on.set_value(self._state, should_callback=False) + self.char_on.set_value(self._state) self._flag[CHAR_ON] = False # Handle Brightness @@ -159,8 +155,7 @@ def update_state(self, entity_id=None, old_state=None, new_state=None): if not self._flag[CHAR_BRIGHTNESS] and isinstance(brightness, int): brightness = round(brightness / 255 * 100, 0) if self.char_brightness.value != brightness: - self.char_brightness.set_value(brightness, - should_callback=False) + self.char_brightness.set_value(brightness) self._flag[CHAR_BRIGHTNESS] = False # Handle color temperature @@ -168,8 +163,7 @@ def update_state(self, entity_id=None, old_state=None, new_state=None): color_temperature = new_state.attributes.get(ATTR_COLOR_TEMP) if not self._flag[CHAR_COLOR_TEMPERATURE] \ and isinstance(color_temperature, int): - self.char_color_temperature.set_value(color_temperature, - should_callback=False) + self.char_color_temperature.set_value(color_temperature) self._flag[CHAR_COLOR_TEMPERATURE] = False # Handle Color @@ -180,8 +174,7 @@ def update_state(self, entity_id=None, old_state=None, new_state=None): hue != self._hue or saturation != self._saturation) and \ isinstance(hue, (int, float)) and \ isinstance(saturation, (int, float)): - self.char_hue.set_value(hue, should_callback=False) - self.char_saturation.set_value(saturation, - should_callback=False) + self.char_hue.set_value(hue) + self.char_saturation.set_value(saturation) self._hue, self._saturation = (hue, saturation) self._flag[RGB_COLOR] = False diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 2cce6653db3945..235a8b22e7c3bb 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -53,7 +53,6 @@ def set_security_state(self, value): _LOGGER.debug('%s: Set security state to %d', self.entity_id, value) self.flag_target_state = True - self.char_target_state.set_value(value, should_callback=False) hass_value = HOMEKIT_TO_HASS[value] service = STATE_TO_SERVICE[hass_value] @@ -72,13 +71,11 @@ def update_state(self, entity_id=None, old_state=None, new_state=None): return current_security_state = HASS_TO_HOMEKIT[hass_state] - self.char_current_state.set_value(current_security_state, - should_callback=False) + self.char_current_state.set_value(current_security_state) _LOGGER.debug('%s: Updated current state to %s (%d)', self.entity_id, hass_state, current_security_state) if not self.flag_target_state: - self.char_target_state.set_value(current_security_state, - should_callback=False) + self.char_target_state.set_value(current_security_state) if self.char_target_state.value == self.char_current_state.value: self.flag_target_state = False diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py old mode 100644 new mode 100755 index 80521df5991152..b25eb784d6bde4 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -2,19 +2,40 @@ import logging from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) + ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, + ATTR_DEVICE_CLASS, STATE_ON, STATE_HOME) from . import TYPES from .accessories import HomeAccessory, add_preload_service from .const import ( CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, - CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS) + CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS, + DEVICE_CLASS_CO2, SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED, + DEVICE_CLASS_GAS, SERV_CARBON_MONOXIDE_SENSOR, + CHAR_CARBON_MONOXIDE_DETECTED, + DEVICE_CLASS_MOISTURE, SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED, + DEVICE_CLASS_MOTION, SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED, + DEVICE_CLASS_OCCUPANCY, SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED, + DEVICE_CLASS_OPENING, SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, + DEVICE_CLASS_SMOKE, SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED) from .util import convert_to_float, temperature_to_homekit _LOGGER = logging.getLogger(__name__) +BINARY_SENSOR_SERVICE_MAP = { + DEVICE_CLASS_CO2: (SERV_CARBON_DIOXIDE_SENSOR, + CHAR_CARBON_DIOXIDE_DETECTED), + DEVICE_CLASS_GAS: (SERV_CARBON_MONOXIDE_SENSOR, + CHAR_CARBON_MONOXIDE_DETECTED), + DEVICE_CLASS_MOISTURE: (SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED), + DEVICE_CLASS_MOTION: (SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED), + DEVICE_CLASS_OCCUPANCY: (SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED), + DEVICE_CLASS_OPENING: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE), + DEVICE_CLASS_SMOKE: (SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED)} + + @TYPES.register('TemperatureSensor') class TemperatureSensor(HomeAccessory): """Generate a TemperatureSensor accessory for a temperature sensor. @@ -44,7 +65,7 @@ def update_state(self, entity_id=None, old_state=None, new_state=None): temperature = convert_to_float(new_state.state) if temperature: temperature = temperature_to_homekit(temperature, unit) - self.char_temp.set_value(temperature, should_callback=False) + self.char_temp.set_value(temperature) _LOGGER.debug('%s: Current temperature set to %d°C', self.entity_id, temperature) @@ -72,6 +93,38 @@ def update_state(self, entity_id=None, old_state=None, new_state=None): humidity = convert_to_float(new_state.state) if humidity: - self.char_humidity.set_value(humidity, should_callback=False) + self.char_humidity.set_value(humidity) _LOGGER.debug('%s: Percent set to %d%%', self.entity_id, humidity) + + +@TYPES.register('BinarySensor') +class BinarySensor(HomeAccessory): + """Generate a BinarySensor accessory as binary sensor.""" + + def __init__(self, hass, entity_id, name, **kwargs): + """Initialize a BinarySensor accessory object.""" + super().__init__(name, entity_id, CATEGORY_SENSOR, **kwargs) + + self.hass = hass + self.entity_id = entity_id + + device_class = hass.states.get(entity_id).attributes \ + .get(ATTR_DEVICE_CLASS) + service_char = BINARY_SENSOR_SERVICE_MAP[device_class] \ + if device_class in BINARY_SENSOR_SERVICE_MAP \ + else BINARY_SENSOR_SERVICE_MAP[DEVICE_CLASS_OCCUPANCY] + + service = add_preload_service(self, service_char[0]) + self.char_detected = service.get_characteristic(service_char[1]) + self.char_detected.value = 0 + + def update_state(self, entity_id=None, old_state=None, new_state=None): + """Update accessory after state change.""" + if new_state is None: + return + + state = new_state.state + detected = (state == STATE_ON) or (state == STATE_HOME) + self.char_detected.set_value(detected) + _LOGGER.debug('%s: Set to %d', self.entity_id, detected) diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 689edde6f37f1b..854cb49d1819c6 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -36,7 +36,6 @@ def set_state(self, value): _LOGGER.debug('%s: Set switch state to %s', self.entity_id, value) self.flag_target_state = True - self.char_on.set_value(value, should_callback=False) service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF self.hass.services.call(self._domain, service, {ATTR_ENTITY_ID: self.entity_id}) @@ -50,6 +49,6 @@ def update_state(self, entity_id=None, old_state=None, new_state=None): if not self.flag_target_state: _LOGGER.debug('%s: Set current state to %s', self.entity_id, current_state) - self.char_on.set_value(current_state, should_callback=False) + self.char_on.set_value(current_state) self.flag_target_state = False diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 69b61062791716..daf81c51c4d937 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -10,7 +10,7 @@ ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) from . import TYPES -from .accessories import HomeAccessory, add_preload_service +from .accessories import HomeAccessory, add_preload_service, debounce from .const import ( CATEGORY_THERMOSTAT, SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING, CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE, @@ -97,7 +97,6 @@ def __init__(self, hass, entity_id, display_name, support_auto, **kwargs): def set_heat_cool(self, value): """Move operation mode to value if call came from HomeKit.""" - self.char_target_heat_cool.set_value(value, should_callback=False) if value in HC_HOMEKIT_TO_HASS: _LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value) self.heat_cool_flag_target_state = True @@ -105,12 +104,12 @@ def set_heat_cool(self, value): self.hass.components.climate.set_operation_mode( operation_mode=hass_value, entity_id=self.entity_id) + @debounce def set_cooling_threshold(self, value): """Set cooling threshold temp to value if call came from HomeKit.""" _LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C', self.entity_id, value) self.coolingthresh_flag_target_state = True - self.char_cooling_thresh_temp.set_value(value, should_callback=False) low = self.char_heating_thresh_temp.value low = temperature_to_states(low, self._unit) value = temperature_to_states(value, self._unit) @@ -118,12 +117,12 @@ def set_cooling_threshold(self, value): entity_id=self.entity_id, target_temp_high=value, target_temp_low=low) + @debounce def set_heating_threshold(self, value): """Set heating threshold temp to value if call came from HomeKit.""" _LOGGER.debug('%s: Set heating threshold temperature to %.2f°C', self.entity_id, value) self.heatingthresh_flag_target_state = True - self.char_heating_thresh_temp.set_value(value, should_callback=False) # Home assistant always wants to set low and high at the same time high = self.char_cooling_thresh_temp.value high = temperature_to_states(high, self._unit) @@ -132,12 +131,12 @@ def set_heating_threshold(self, value): entity_id=self.entity_id, target_temp_high=high, target_temp_low=value) + @debounce def set_target_temperature(self, value): """Set target temperature to value if call came from HomeKit.""" _LOGGER.debug('%s: Set target temperature to %.2f°C', self.entity_id, value) self.temperature_flag_target_state = True - self.char_target_temp.set_value(value, should_callback=False) value = temperature_to_states(value, self._unit) self.hass.components.climate.set_temperature( temperature=value, entity_id=self.entity_id) @@ -161,8 +160,7 @@ def update_state(self, entity_id=None, old_state=None, new_state=None): if isinstance(target_temp, (int, float)): target_temp = temperature_to_homekit(target_temp, self._unit) if not self.temperature_flag_target_state: - self.char_target_temp.set_value(target_temp, - should_callback=False) + self.char_target_temp.set_value(target_temp) self.temperature_flag_target_state = False # Update cooling threshold temperature if characteristic exists @@ -172,8 +170,7 @@ def update_state(self, entity_id=None, old_state=None, new_state=None): cooling_thresh = temperature_to_homekit(cooling_thresh, self._unit) if not self.coolingthresh_flag_target_state: - self.char_cooling_thresh_temp.set_value( - cooling_thresh, should_callback=False) + self.char_cooling_thresh_temp.set_value(cooling_thresh) self.coolingthresh_flag_target_state = False # Update heating threshold temperature if characteristic exists @@ -183,8 +180,7 @@ def update_state(self, entity_id=None, old_state=None, new_state=None): heating_thresh = temperature_to_homekit(heating_thresh, self._unit) if not self.heatingthresh_flag_target_state: - self.char_heating_thresh_temp.set_value( - heating_thresh, should_callback=False) + self.char_heating_thresh_temp.set_value(heating_thresh) self.heatingthresh_flag_target_state = False # Update display units @@ -197,7 +193,7 @@ def update_state(self, entity_id=None, old_state=None, new_state=None): and operation_mode in HC_HASS_TO_HOMEKIT: if not self.heat_cool_flag_target_state: self.char_target_heat_cool.set_value( - HC_HASS_TO_HOMEKIT[operation_mode], should_callback=False) + HC_HASS_TO_HOMEKIT[operation_mode]) self.heat_cool_flag_target_state = False # Set current operation mode based on temperatures and target mode diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py index 031fa263e5a40c..0c0100bc9f595e 100644 --- a/homeassistant/components/ihc/__init__.py +++ b/homeassistant/components/ihc/__init__.py @@ -1,4 +1,5 @@ -"""IHC component. +""" +Support for IHC devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/ihc/ @@ -6,18 +7,18 @@ import logging import os.path import xml.etree.ElementTree + import voluptuous as vol from homeassistant.components.ihc.const import ( - ATTR_IHC_ID, ATTR_VALUE, CONF_INFO, CONF_AUTOSETUP, - CONF_BINARY_SENSOR, CONF_LIGHT, CONF_SENSOR, CONF_SWITCH, - CONF_XPATH, CONF_NODE, CONF_DIMMABLE, CONF_INVERTING, - SERVICE_SET_RUNTIME_VALUE_BOOL, SERVICE_SET_RUNTIME_VALUE_INT, - SERVICE_SET_RUNTIME_VALUE_FLOAT) + ATTR_IHC_ID, ATTR_VALUE, CONF_AUTOSETUP, CONF_BINARY_SENSOR, CONF_DIMMABLE, + CONF_INFO, CONF_INVERTING, CONF_LIGHT, CONF_NODE, CONF_SENSOR, CONF_SWITCH, + CONF_XPATH, SERVICE_SET_RUNTIME_VALUE_BOOL, + SERVICE_SET_RUNTIME_VALUE_FLOAT, SERVICE_SET_RUNTIME_VALUE_INT) from homeassistant.config import load_yaml_config_file from homeassistant.const import ( - CONF_URL, CONF_USERNAME, CONF_PASSWORD, CONF_ID, CONF_NAME, - CONF_UNIT_OF_MEASUREMENT, CONF_TYPE, TEMP_CELSIUS) + CONF_ID, CONF_NAME, CONF_PASSWORD, CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, + CONF_URL, CONF_USERNAME, TEMP_CELSIUS) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType @@ -36,7 +37,7 @@ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_AUTOSETUP, default=True): cv.boolean, - vol.Optional(CONF_INFO, default=True): cv.boolean + vol.Optional(CONF_INFO, default=True): cv.boolean, }), }, extra=vol.ALLOW_EXTRA) @@ -97,7 +98,7 @@ def setup(hass, config): - """Setup the IHC component.""" + """Set up the IHC component.""" from ihcsdk.ihccontroller import IHCController conf = config[DOMAIN] url = conf[CONF_URL] @@ -106,7 +107,7 @@ def setup(hass, config): ihc_controller = IHCController(url, username, password) if not ihc_controller.authenticate(): - _LOGGER.error("Unable to authenticate on ihc controller.") + _LOGGER.error("Unable to authenticate on IHC controller") return False if (conf[CONF_AUTOSETUP] and @@ -125,7 +126,7 @@ def autosetup_ihc_products(hass: HomeAssistantType, config, ihc_controller): """Auto setup of IHC products from the ihc project file.""" project_xml = ihc_controller.get_project() if not project_xml: - _LOGGER.error("Unable to read project from ihc controller.") + _LOGGER.error("Unable to read project from ICH controller") return False project = xml.etree.ElementTree.fromstring(project_xml) @@ -150,7 +151,7 @@ def autosetup_ihc_products(hass: HomeAssistantType, config, ihc_controller): def get_discovery_info(component_setup, groups): - """Get discovery info for specified component.""" + """Get discovery info for specified IHC component.""" discovery_data = {} for group in groups: groupname = group.attrib['name'] @@ -173,7 +174,7 @@ def get_discovery_info(component_setup, groups): def setup_service_functions(hass: HomeAssistantType, ihc_controller): - """Setup the ihc service functions.""" + """Setup the IHC service functions.""" def set_runtime_value_bool(call): """Set a IHC runtime bool value service function.""" ihc_id = call.data[ATTR_IHC_ID] diff --git a/homeassistant/components/ihc/ihcdevice.py b/homeassistant/components/ihc/ihcdevice.py index 59f4d95f0a1f24..de6db875def005 100644 --- a/homeassistant/components/ihc/ihcdevice.py +++ b/homeassistant/components/ihc/ihcdevice.py @@ -1,4 +1,4 @@ -"""Implements a base class for all IHC devices.""" +"""Implementation of a base class for all IHC devices.""" import asyncio from xml.etree.ElementTree import Element @@ -6,7 +6,7 @@ class IHCDevice(Entity): - """Base class for all ihc devices. + """Base class for all IHC devices. All IHC devices have an associated IHC resource. IHCDevice handled the registration of the IHC controller callback when the IHC resource changes. @@ -31,13 +31,13 @@ def __init__(self, ihc_controller, name, ihc_id: int, info: bool, @asyncio.coroutine def async_added_to_hass(self): - """Add callback for ihc changes.""" + """Add callback for IHC changes.""" self.ihc_controller.add_notify_event( self.ihc_id, self.on_ihc_change, True) @property def should_poll(self) -> bool: - """No polling needed for ihc devices.""" + """No polling needed for IHC devices.""" return False @property @@ -58,7 +58,7 @@ def device_state_attributes(self): } def on_ihc_change(self, ihc_id, value): - """Callback when ihc resource changes. + """Callback when IHC resource changes. Derived classes must overwrite this to do device specific stuff. """ diff --git a/homeassistant/components/light/aurora.py b/homeassistant/components/light/aurora.py index 2a9066bd55fde6..99c07166037e49 100644 --- a/homeassistant/components/light/aurora.py +++ b/homeassistant/components/light/aurora.py @@ -1,11 +1,6 @@ """ Support for Nanoleaf Aurora platform. -Based in large parts upon Software-2's ha-aurora and fully -reliant on Software-2's nanoleaf-aurora Python Library, see -https://github.com/software-2/ha-aurora as well as -https://github.com/software-2/nanoleaf - For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.nanoleaf_aurora/ """ @@ -15,9 +10,9 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, - SUPPORT_EFFECT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, - SUPPORT_COLOR, PLATFORM_SCHEMA, Light) -from homeassistant.const import CONF_HOST, CONF_TOKEN, CONF_NAME + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, + SUPPORT_EFFECT, Light) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN import homeassistant.helpers.config_validation as cv from homeassistant.util import color as color_util from homeassistant.util.color import \ @@ -25,20 +20,24 @@ REQUIREMENTS = ['nanoleaf==0.4.1'] +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'Aurora' + +ICON = 'mdi:triangle-outline' + SUPPORT_AURORA = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT | SUPPORT_COLOR) -_LOGGER = logging.getLogger(__name__) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_TOKEN): cv.string, - vol.Optional(CONF_NAME, default='Aurora'): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup Nanoleaf Aurora device.""" + """Set up the Nanoleaf Aurora device.""" import nanoleaf host = config.get(CONF_HOST) name = config.get(CONF_NAME) @@ -47,8 +46,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): aurora_light.hass_name = name if aurora_light.on is None: - _LOGGER.error("Could not connect to \ - Nanoleaf Aurora: %s on %s", name, host) + _LOGGER.error( + "Could not connect to Nanoleaf Aurora: %s on %s", name, host) + return + add_devices([AuroraLight(aurora_light)], True) @@ -56,7 +57,7 @@ class AuroraLight(Light): """Representation of a Nanoleaf Aurora.""" def __init__(self, light): - """Initialize an Aurora.""" + """Initialize an Aurora light.""" self._brightness = None self._color_temp = None self._effect = None @@ -99,7 +100,7 @@ def name(self): @property def icon(self): """Return the icon to use in the frontend, if any.""" - return "mdi:triangle-outline" + return ICON @property def is_on(self): @@ -141,10 +142,7 @@ def turn_off(self, **kwargs): self._light.on = False def update(self): - """Fetch new state data for this light. - - This is the only method that should fetch new data for Home Assistant. - """ + """Fetch new state data for this light.""" self._brightness = self._light.brightness self._color_temp = self._light.color_temperature self._effect = self._light.effect diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index 3faf51a5f47eeb..27730a8f63e4e4 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -8,6 +8,9 @@ import statistics from collections import deque, Counter from numbers import Number +from functools import partial +from copy import copy +from datetime import timedelta import voluptuous as vol @@ -20,6 +23,7 @@ from homeassistant.util.decorator import Registry from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change +import homeassistant.components.history as history import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -40,6 +44,9 @@ TIME_SMA_LAST = 'last' +WINDOW_SIZE_UNIT_NUMBER_EVENTS = 1 +WINDOW_SIZE_UNIT_TIME = 2 + DEFAULT_WINDOW_SIZE = 1 DEFAULT_PRECISION = 2 DEFAULT_FILTER_RADIUS = 2.0 @@ -123,21 +130,22 @@ def __init__(self, name, entity_id, filters): async def async_added_to_hass(self): """Register callbacks.""" @callback - def filter_sensor_state_listener(entity, old_state, new_state): + def filter_sensor_state_listener(entity, old_state, new_state, + update_ha=True): """Handle device state changes.""" if new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]: return - temp_state = new_state.state + temp_state = new_state try: for filt in self._filters: - filtered_state = filt.filter_state(temp_state) + filtered_state = filt.filter_state(copy(temp_state)) _LOGGER.debug("%s(%s=%s) -> %s", filt.name, self._entity, - temp_state, + temp_state.state, "skip" if filt.skip_processing else - filtered_state) + filtered_state.state) if filt.skip_processing: return temp_state = filtered_state @@ -146,7 +154,7 @@ def filter_sensor_state_listener(entity, old_state, new_state): self._state) return - self._state = temp_state + self._state = temp_state.state if self._icon is None: self._icon = new_state.attributes.get( @@ -156,7 +164,50 @@ def filter_sensor_state_listener(entity, old_state, new_state): self._unit_of_measurement = new_state.attributes.get( ATTR_UNIT_OF_MEASUREMENT) - self.async_schedule_update_ha_state() + if update_ha: + self.async_schedule_update_ha_state() + + if 'recorder' in self.hass.config.components: + history_list = [] + largest_window_items = 0 + largest_window_time = timedelta(0) + + # Determine the largest window_size by type + for filt in self._filters: + if filt.window_unit == WINDOW_SIZE_UNIT_NUMBER_EVENTS\ + and largest_window_items < filt.window_size: + largest_window_items = filt.window_size + elif filt.window_unit == WINDOW_SIZE_UNIT_TIME\ + and largest_window_time < filt.window_size: + largest_window_time = filt.window_size + + # Retrieve the largest window_size of each type + if largest_window_items > 0: + filter_history = await self.hass.async_add_job(partial( + history.get_last_state_changes, self.hass, + largest_window_items, entity_id=self._entity)) + history_list.extend( + [state for state in filter_history[self._entity]]) + if largest_window_time > timedelta(seconds=0): + start = dt_util.utcnow() - largest_window_time + filter_history = await self.hass.async_add_job(partial( + history.state_changes_during_period, self.hass, + start, entity_id=self._entity)) + history_list.extend( + [state for state in filter_history[self._entity] + if state not in history_list]) + + # Sort the window states + history_list = sorted(history_list, key=lambda s: s.last_updated) + _LOGGER.debug("Loading from history: %s", + [(s.state, s.last_updated) for s in history_list]) + + # Replay history through the filter chain + prev_state = None + for state in history_list: + filter_sensor_state_listener( + self._entity, prev_state, state, False) + prev_state = state async_track_state_change( self.hass, self._entity, filter_sensor_state_listener) @@ -195,6 +246,31 @@ def device_state_attributes(self): return state_attr +class FilterState(object): + """State abstraction for filter usage.""" + + def __init__(self, state): + """Initialize with HA State object.""" + self.timestamp = state.last_updated + try: + self.state = float(state.state) + except ValueError: + self.state = state.state + + def set_precision(self, precision): + """Set precision of Number based states.""" + if isinstance(self.state, Number): + self.state = round(float(self.state), precision) + + def __str__(self): + """Return state as the string representation of FilterState.""" + return str(self.state) + + def __repr__(self): + """Return timestamp and state as the representation of FilterState.""" + return "{} : {}".format(self.timestamp, self.state) + + class Filter(object): """Filter skeleton. @@ -207,11 +283,22 @@ class Filter(object): def __init__(self, name, window_size=1, precision=None, entity=None): """Initialize common attributes.""" - self.states = deque(maxlen=window_size) + if isinstance(window_size, int): + self.states = deque(maxlen=window_size) + self.window_unit = WINDOW_SIZE_UNIT_NUMBER_EVENTS + else: + self.states = deque(maxlen=0) + self.window_unit = WINDOW_SIZE_UNIT_TIME self.precision = precision self._name = name self._entity = entity self._skip_processing = False + self._window_size = window_size + + @property + def window_size(self): + """Return window size.""" + return self._window_size @property def name(self): @@ -229,11 +316,11 @@ def _filter_state(self, new_state): def filter_state(self, new_state): """Implement a common interface for filters.""" - filtered = self._filter_state(new_state) - if isinstance(filtered, Number): - filtered = round(float(filtered), self.precision) - self.states.append(filtered) - return filtered + filtered = self._filter_state(FilterState(new_state)) + filtered.set_precision(self.precision) + self.states.append(copy(filtered)) + new_state.state = filtered.state + return new_state @FILTERS.register(FILTER_NAME_OUTLIER) @@ -254,11 +341,10 @@ def __init__(self, window_size, precision, entity, radius): def _filter_state(self, new_state): """Implement the outlier filter.""" - new_state = float(new_state) - if (self.states and - abs(new_state - statistics.median(self.states)) - > self._radius): + abs(new_state.state - + statistics.median([s.state for s in self.states])) > + self._radius): self._stats_internal['erasures'] += 1 @@ -284,16 +370,15 @@ def __init__(self, window_size, precision, entity, time_constant): def _filter_state(self, new_state): """Implement the low pass filter.""" - new_state = float(new_state) - if not self.states: return new_state new_weight = 1.0 / self._time_constant prev_weight = 1.0 - new_weight - filtered = prev_weight * self.states[-1] + new_weight * new_state + new_state.state = prev_weight * self.states[-1].state +\ + new_weight * new_state.state - return filtered + return new_state @FILTERS.register(FILTER_NAME_TIME_SMA) @@ -308,35 +393,36 @@ class TimeSMAFilter(Filter): def __init__(self, window_size, precision, entity, type): """Initialize Filter.""" - super().__init__(FILTER_NAME_TIME_SMA, 0, precision, entity) - self._time_window = int(window_size.total_seconds()) + super().__init__(FILTER_NAME_TIME_SMA, window_size, precision, entity) + self._time_window = window_size self.last_leak = None self.queue = deque() - def _leak(self, now): + def _leak(self, left_boundary): """Remove timeouted elements.""" while self.queue: - timestamp, _ = self.queue[0] - if timestamp + self._time_window <= now: + if self.queue[0].timestamp + self._time_window <= left_boundary: self.last_leak = self.queue.popleft() else: return def _filter_state(self, new_state): - now = int(dt_util.utcnow().timestamp()) + """Implement the Simple Moving Average filter.""" + self._leak(new_state.timestamp) + self.queue.append(copy(new_state)) - self._leak(now) - self.queue.append((now, float(new_state))) moving_sum = 0 - start = now - self._time_window - _, prev_val = self.last_leak or (0, float(new_state)) + start = new_state.timestamp - self._time_window + prev_state = self.last_leak or self.queue[0] + for state in self.queue: + moving_sum += (state.timestamp-start).total_seconds()\ + * prev_state.state + start = state.timestamp + prev_state = state - for timestamp, val in self.queue: - moving_sum += (timestamp-start)*prev_val - start, prev_val = timestamp, val - moving_sum += (now-start)*prev_val + new_state.state = moving_sum / self._time_window.total_seconds() - return moving_sum/self._time_window + return new_state @FILTERS.register(FILTER_NAME_THROTTLE) diff --git a/homeassistant/components/sensor/smappee.py b/homeassistant/components/sensor/smappee.py index c59798d16d7242..5b84962144d5b0 100644 --- a/homeassistant/components/sensor/smappee.py +++ b/homeassistant/components/sensor/smappee.py @@ -31,7 +31,19 @@ 'solar_today': ['Solar Today', 'mdi:white-balance-sunny', 'remote', 'kWh', 'solar'], 'power_today': - ['Power Today', 'mdi:power-plug', 'remote', 'kWh', 'consumption'] + ['Power Today', 'mdi:power-plug', 'remote', 'kWh', 'consumption'], + 'water_sensor_1': + ['Water Sensor 1', 'mdi:water', 'water', 'm3', 'value1'], + 'water_sensor_2': + ['Water Sensor 2', 'mdi:water', 'water', 'm3', 'value2'], + 'water_sensor_temperature': + ['Water Sensor Temperature', 'mdi:temperature-celsius', + 'water', '°', 'temperature'], + 'water_sensor_humidity': + ['Water Sensor Humidity', 'mdi:water-percent', 'water', + '%', 'humidity'], + 'water_sensor_battery': + ['Water Sensor Battery', 'mdi:battery', 'water', '%', 'battery'], } SCAN_INTERVAL = timedelta(seconds=30) @@ -43,36 +55,50 @@ def setup_platform(hass, config, add_devices, discovery_info=None): dev = [] if smappee.is_remote_active: - for sensor in SENSOR_TYPES: - if 'remote' in SENSOR_TYPES[sensor]: - for location_id in smappee.locations.keys(): - dev.append(SmappeeSensor(smappee, location_id, sensor)) + for location_id in smappee.locations.keys(): + for sensor in SENSOR_TYPES: + if 'remote' in SENSOR_TYPES[sensor]: + dev.append(SmappeeSensor(smappee, location_id, + sensor, + SENSOR_TYPES[sensor])) + elif 'water' in SENSOR_TYPES[sensor]: + for items in smappee.info[location_id].get('sensors'): + dev.append(SmappeeSensor( + smappee, + location_id, + '{}:{}'.format(sensor, items.get('id')), + SENSOR_TYPES[sensor])) if smappee.is_local_active: - for sensor in SENSOR_TYPES: - if 'local' in SENSOR_TYPES[sensor]: - if smappee.is_remote_active: - for location_id in smappee.locations.keys(): - dev.append(SmappeeSensor(smappee, location_id, sensor)) - else: - dev.append(SmappeeSensor(smappee, None, sensor)) + for location_id in smappee.locations.keys(): + for sensor in SENSOR_TYPES: + if 'local' in SENSOR_TYPES[sensor]: + if smappee.is_remote_active: + dev.append(SmappeeSensor(smappee, location_id, sensor, + SENSOR_TYPES[sensor])) + else: + dev.append(SmappeeSensor(smappee, None, sensor, + SENSOR_TYPES[sensor])) + add_devices(dev, True) class SmappeeSensor(Entity): """Implementation of a Smappee sensor.""" - def __init__(self, smappee, location_id, sensor): - """Initialize the sensor.""" + def __init__(self, smappee, location_id, sensor, attributes): + """Initialize the Smappee sensor.""" self._smappee = smappee self._location_id = location_id + self._attributes = attributes self._sensor = sensor self.data = None self._state = None - self._name = SENSOR_TYPES[self._sensor][0] - self._icon = SENSOR_TYPES[self._sensor][1] - self._unit_of_measurement = SENSOR_TYPES[self._sensor][3] - self._smappe_name = SENSOR_TYPES[self._sensor][4] + self._name = self._attributes[0] + self._icon = self._attributes[1] + self._type = self._attributes[2] + self._unit_of_measurement = self._attributes[3] + self._smappe_name = self._attributes[4] @property def name(self): @@ -82,9 +108,7 @@ def name(self): else: location_name = 'Local' - return "{} {} {}".format(SENSOR_PREFIX, - location_name, - self._name) + return "{} {} {}".format(SENSOR_PREFIX, location_name, self._name) @property def icon(self): @@ -160,3 +184,13 @@ def update(self): if i['key'].endswith('phase5ActivePower')] power = sum(value1 + value2 + value3) / 1000 self._state = round(power, 2) + elif self._type == 'water': + sensor_name, sensor_id = self._sensor.split(":") + data = self._smappee.sensor_consumption[self._location_id]\ + .get(int(sensor_id)) + if data: + consumption = data.get('records')[-1] + _LOGGER.debug("%s (%s) %s", + sensor_name, sensor_id, consumption) + value = consumption.get(self._smappe_name) + self._state = value diff --git a/homeassistant/components/sensor/trafikverket_weatherstation.py b/homeassistant/components/sensor/trafikverket_weatherstation.py index fba16c27c7e98e..77a2b0e7338e9e 100644 --- a/homeassistant/components/sensor/trafikverket_weatherstation.py +++ b/homeassistant/components/sensor/trafikverket_weatherstation.py @@ -4,17 +4,17 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.trafikverket_weatherstation/ """ +from datetime import timedelta import json import logging -from datetime import timedelta import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, ATTR_ATTRIBUTION, TEMP_CELSIUS, CONF_API_KEY, CONF_TYPE) + ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME, CONF_TYPE, TEMP_CELSIUS) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -25,6 +25,7 @@ CONF_STATION = 'station' MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + SCAN_INTERVAL = timedelta(seconds=300) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -36,7 +37,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the sensor platform.""" + """Set up the Trafikverket sensor platform.""" sensor_name = config.get(CONF_NAME) sensor_api = config.get(CONF_API_KEY) sensor_station = config.get(CONF_STATION) @@ -47,10 +48,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class TrafikverketWeatherStation(Entity): - """Representation of a Sensor.""" + """Representation of a Trafikverket sensor.""" def __init__(self, sensor_name, sensor_api, sensor_station, sensor_type): - """Initialize the sensor.""" + """Initialize the Trafikverket sensor.""" self._name = sensor_name self._api = sensor_api self._station = sensor_station @@ -82,10 +83,7 @@ def device_state_attributes(self): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): - """Fetch new state data for the sensor. - - This is the only method that should fetch new data for Home Assistant. - """ + """Fetch new state data for the sensor.""" url = 'http://api.trafikinfo.trafikverket.se/v1.3/data.json' if self._type == 'road': @@ -117,7 +115,7 @@ def update(self): result = data["RESPONSE"]["RESULT"][0] final = result["WeatherStation"][0]["Measurement"] except KeyError: - _LOGGER.error("Incorrect weather station or API key.") + _LOGGER.error("Incorrect weather station or API key") return # air_vs_road contains "Air" or "Road" depending on user input. diff --git a/homeassistant/components/smappee.py b/homeassistant/components/smappee.py index 1241679770b3d0..b35cd8cf5a8ce1 100644 --- a/homeassistant/components/smappee.py +++ b/homeassistant/components/smappee.py @@ -110,6 +110,7 @@ def __init__(self, client_id, client_secret, username, self.locations = {} self.info = {} self.consumption = {} + self.sensor_consumption = {} self.instantaneous = {} if self._remote_active or self._local_active: @@ -124,11 +125,22 @@ def update(self): for location in service_locations: location_id = location.get('serviceLocationId') if location_id is not None: + self.sensor_consumption[location_id] = {} self.locations[location_id] = location.get('name') self.info[location_id] = self._smappy \ .get_service_location_info(location_id) _LOGGER.debug("Remote info %s %s", - self.locations, self.info) + self.locations, self.info[location_id]) + + for sensors in self.info[location_id].get('sensors'): + sensor_id = sensors.get('id') + self.sensor_consumption[location_id]\ + .update({sensor_id: self.get_sensor_consumption( + location_id, sensor_id, + aggregation=3, delta=1440)}) + _LOGGER.debug("Remote sensors %s %s", + self.locations, + self.sensor_consumption[location_id]) self.consumption[location_id] = self.get_consumption( location_id, aggregation=3, delta=1440) @@ -190,7 +202,8 @@ def get_consumption(self, location_id, aggregation, delta): "Error getting comsumption from Smappee cloud. (%s)", error) - def get_sensor_consumption(self, location_id, sensor_id): + def get_sensor_consumption(self, location_id, sensor_id, + aggregation, delta): """Update data from Smappee.""" # Start & End accept epoch (in milliseconds), # datetime and pandas timestamps @@ -203,13 +216,13 @@ def get_sensor_consumption(self, location_id, sensor_id): if not self.is_remote_active: return - start = datetime.utcnow() - timedelta(minutes=30) end = datetime.utcnow() + start = end - timedelta(minutes=delta) try: return self._smappy.get_sensor_consumption(location_id, sensor_id, start, - end, 1) + end, aggregation) except RequestException as error: _LOGGER.error( "Error getting comsumption from Smappee cloud. (%s)", diff --git a/requirements_all.txt b/requirements_all.txt index da2373443cb26d..7af7bdb95ec118 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -22,7 +22,7 @@ attrs==17.4.0 DoorBirdPy==0.1.3 # homeassistant.components.homekit -HAP-python==1.1.8 +HAP-python==1.1.9 # homeassistant.components.notify.mastodon Mastodon.py==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7c5467f76087b7..645b56b9e62dff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ requests_mock==1.4 # homeassistant.components.homekit -HAP-python==1.1.8 +HAP-python==1.1.9 # homeassistant.components.notify.html5 PyJWT==1.6.0 diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index a2facd826e46d0..b7bf625a2d6452 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -2,21 +2,67 @@ This includes tests for all mock object types. """ +from datetime import datetime, timedelta import unittest from unittest.mock import call, patch, Mock from homeassistant.components.homekit.accessories import ( add_preload_service, set_accessory_info, - HomeAccessory, HomeBridge, HomeDriver) + debounce, HomeAccessory, HomeBridge, HomeDriver) from homeassistant.components.homekit.const import ( ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME, SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) +from homeassistant.const import ATTR_NOW, EVENT_TIME_CHANGED +import homeassistant.util.dt as dt_util + +from tests.common import get_test_home_assistant + + +def patch_debounce(): + """Return patch for debounce method.""" + return patch('homeassistant.components.homekit.accessories.debounce', + lambda f: lambda *args, **kwargs: f(*args, **kwargs)) class TestAccessories(unittest.TestCase): """Test pyhap adapter methods.""" + def test_debounce(self): + """Test add_timeout decorator function.""" + def demo_func(*args): + nonlocal arguments, counter + counter += 1 + arguments = args + + arguments = None + counter = 0 + hass = get_test_home_assistant() + mock = Mock(hass=hass) + + debounce_demo = debounce(demo_func) + self.assertEqual(debounce_demo.__name__, 'demo_func') + now = datetime(2018, 1, 1, 20, 0, 0, tzinfo=dt_util.UTC) + + with patch('homeassistant.util.dt.utcnow', return_value=now): + debounce_demo(mock, 'value') + hass.bus.fire( + EVENT_TIME_CHANGED, {ATTR_NOW: now + timedelta(seconds=3)}) + hass.block_till_done() + assert counter == 1 + assert len(arguments) == 2 + + with patch('homeassistant.util.dt.utcnow', return_value=now): + debounce_demo(mock, 'value') + debounce_demo(mock, 'value') + + hass.bus.fire( + EVENT_TIME_CHANGED, {ATTR_NOW: now + timedelta(seconds=3)}) + hass.block_till_done() + assert counter == 2 + + hass.stop() + def test_add_preload_service(self): """Test add_preload_service without additional characteristics.""" acc = Mock() diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index c6d79545487356..51a965b5817145 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -14,6 +14,7 @@ CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from tests.common import get_test_home_assistant +from tests.components.homekit.test_accessories import patch_debounce IP_ADDRESS = '127.0.0.1' PATH_HOMEKIT = 'homeassistant.components.homekit' @@ -22,6 +23,17 @@ class TestHomeKit(unittest.TestCase): """Test setup of HomeKit component and HomeKit class.""" + @classmethod + def setUpClass(cls): + """Setup debounce patcher.""" + cls.patcher = patch_debounce() + cls.patcher.start() + + @classmethod + def tearDownClass(cls): + """Stop debounce patcher.""" + cls.patcher.stop() + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 45631a76c98acd..1fa1ef1728e04a 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -62,7 +62,7 @@ def test_window_set_cover_position(self): self.assertEqual(acc.char_position_state.value, 2) # Set from HomeKit - acc.char_target_position.set_value(25) + acc.char_target_position.client_update_value(25) self.hass.block_till_done() self.assertEqual( self.events[0].data[ATTR_SERVICE], 'set_cover_position') @@ -74,7 +74,7 @@ def test_window_set_cover_position(self): self.assertEqual(acc.char_position_state.value, 0) # Set from HomeKit - acc.char_target_position.set_value(75) + acc.char_target_position.client_update_value(75) self.hass.block_till_done() self.assertEqual( self.events[0].data[ATTR_SERVICE], 'set_cover_position') diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 1cfb926c4ceb1c..af8676dfd742b0 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -2,7 +2,6 @@ import unittest from homeassistant.core import callback -from homeassistant.components.homekit.type_lights import Light from homeassistant.components.light import ( DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR) @@ -12,11 +11,26 @@ SERVICE_TURN_OFF, STATE_ON, STATE_OFF, STATE_UNKNOWN) from tests.common import get_test_home_assistant +from tests.components.homekit.test_accessories import patch_debounce class TestHomekitLights(unittest.TestCase): """Test class for all accessory types regarding lights.""" + @classmethod + def setUpClass(cls): + """Setup Light class import and debounce patcher.""" + cls.patcher = patch_debounce() + cls.patcher.start() + _import = __import__('homeassistant.components.homekit.type_lights', + fromlist=['Light']) + cls.light_cls = _import.Light + + @classmethod + def tearDownClass(cls): + """Stop debounce patcher.""" + cls.patcher.stop() + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() @@ -38,7 +52,7 @@ def test_light_basic(self): entity_id = 'light.demo' self.hass.states.set(entity_id, STATE_ON, {ATTR_SUPPORTED_FEATURES: 0}) - acc = Light(self.hass, entity_id, 'Light', aid=2) + acc = self.light_cls(self.hass, entity_id, 'Light', aid=2) self.assertEqual(acc.aid, 2) self.assertEqual(acc.category, 5) # Lightbulb self.assertEqual(acc.char_on.value, 0) @@ -57,7 +71,7 @@ def test_light_basic(self): self.assertEqual(acc.char_on.value, 0) # Set from HomeKit - acc.char_on.set_value(1) + acc.char_on.client_update_value(1) self.hass.block_till_done() self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) @@ -65,7 +79,7 @@ def test_light_basic(self): self.hass.states.set(entity_id, STATE_ON) self.hass.block_till_done() - acc.char_on.set_value(0) + acc.char_on.client_update_value(0) self.hass.block_till_done() self.assertEqual(self.events[1].data[ATTR_DOMAIN], DOMAIN) self.assertEqual(self.events[1].data[ATTR_SERVICE], SERVICE_TURN_OFF) @@ -82,7 +96,7 @@ def test_light_brightness(self): entity_id = 'light.demo' self.hass.states.set(entity_id, STATE_ON, { ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255}) - acc = Light(self.hass, entity_id, 'Light', aid=2) + acc = self.light_cls(self.hass, entity_id, 'Light', aid=2) self.assertEqual(acc.char_brightness.value, 0) acc.run() @@ -94,8 +108,8 @@ def test_light_brightness(self): self.assertEqual(acc.char_brightness.value, 40) # Set from HomeKit - acc.char_brightness.set_value(20) - acc.char_on.set_value(1) + acc.char_brightness.client_update_value(20) + acc.char_on.client_update_value(1) self.hass.block_till_done() self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) @@ -103,8 +117,8 @@ def test_light_brightness(self): self.events[0].data[ATTR_SERVICE_DATA], { ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 20}) - acc.char_on.set_value(1) - acc.char_brightness.set_value(40) + acc.char_on.client_update_value(1) + acc.char_brightness.client_update_value(40) self.hass.block_till_done() self.assertEqual(self.events[1].data[ATTR_DOMAIN], DOMAIN) self.assertEqual(self.events[1].data[ATTR_SERVICE], SERVICE_TURN_ON) @@ -112,8 +126,8 @@ def test_light_brightness(self): self.events[1].data[ATTR_SERVICE_DATA], { ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 40}) - acc.char_on.set_value(1) - acc.char_brightness.set_value(0) + acc.char_on.client_update_value(1) + acc.char_brightness.client_update_value(0) self.hass.block_till_done() self.assertEqual(self.events[2].data[ATTR_DOMAIN], DOMAIN) self.assertEqual(self.events[2].data[ATTR_SERVICE], SERVICE_TURN_OFF) @@ -124,7 +138,7 @@ def test_light_color_temperature(self): self.hass.states.set(entity_id, STATE_ON, { ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP, ATTR_COLOR_TEMP: 190}) - acc = Light(self.hass, entity_id, 'Light', aid=2) + acc = self.light_cls(self.hass, entity_id, 'Light', aid=2) self.assertEqual(acc.char_color_temperature.value, 153) acc.run() @@ -132,7 +146,7 @@ def test_light_color_temperature(self): self.assertEqual(acc.char_color_temperature.value, 190) # Set from HomeKit - acc.char_color_temperature.set_value(250) + acc.char_color_temperature.client_update_value(250) self.hass.block_till_done() self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) @@ -146,7 +160,7 @@ def test_light_rgb_color(self): self.hass.states.set(entity_id, STATE_ON, { ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR, ATTR_HS_COLOR: (260, 90)}) - acc = Light(self.hass, entity_id, 'Light', aid=2) + acc = self.light_cls(self.hass, entity_id, 'Light', aid=2) self.assertEqual(acc.char_hue.value, 0) self.assertEqual(acc.char_saturation.value, 75) @@ -156,8 +170,8 @@ def test_light_rgb_color(self): self.assertEqual(acc.char_saturation.value, 90) # Set from HomeKit - acc.char_hue.set_value(145) - acc.char_saturation.set_value(75) + acc.char_hue.client_update_value(145) + acc.char_saturation.client_update_value(75) self.hass.block_till_done() self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index c689a73bac229a..46f886c4d35b94 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -71,7 +71,7 @@ def test_switch_set_state(self): self.assertEqual(acc.char_current_state.value, 3) # Set from HomeKit - acc.char_target_state.set_value(0) + acc.char_target_state.client_update_value(0) self.hass.block_till_done() self.assertEqual( self.events[0].data[ATTR_SERVICE], 'alarm_arm_home') @@ -79,7 +79,7 @@ def test_switch_set_state(self): self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') self.assertEqual(acc.char_target_state.value, 0) - acc.char_target_state.set_value(1) + acc.char_target_state.client_update_value(1) self.hass.block_till_done() self.assertEqual( self.events[1].data[ATTR_SERVICE], 'alarm_arm_away') @@ -87,7 +87,7 @@ def test_switch_set_state(self): self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') self.assertEqual(acc.char_target_state.value, 1) - acc.char_target_state.set_value(2) + acc.char_target_state.client_update_value(2) self.hass.block_till_done() self.assertEqual( self.events[2].data[ATTR_SERVICE], 'alarm_arm_night') @@ -95,7 +95,7 @@ def test_switch_set_state(self): self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') self.assertEqual(acc.char_target_state.value, 2) - acc.char_target_state.set_value(3) + acc.char_target_state.client_update_value(3) self.hass.block_till_done() self.assertEqual( self.events[3].data[ATTR_SERVICE], 'alarm_disarm') @@ -112,7 +112,7 @@ def test_no_alarm_code(self): acc.run() # Set from HomeKit - acc.char_target_state.set_value(0) + acc.char_target_state.client_update_value(0) self.hass.block_till_done() self.assertEqual( self.events[0].data[ATTR_SERVICE], 'alarm_arm_home') diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 21d7583152e8e3..7f30e457308692 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -51,14 +51,14 @@ def test_switch_set_state(self): self.assertEqual(acc.char_on.value, False) # Set from HomeKit - acc.char_on.set_value(True) + acc.char_on.client_update_value(True) self.hass.block_till_done() self.assertEqual( self.events[0].data[ATTR_DOMAIN], domain) self.assertEqual( self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - acc.char_on.set_value(False) + acc.char_on.client_update_value(False) self.hass.block_till_done() self.assertEqual( self.events[1].data[ATTR_DOMAIN], domain) @@ -76,7 +76,7 @@ def test_remote_set_state(self): self.assertEqual(acc.char_on.value, False) # Set from HomeKit - acc.char_on.set_value(True) + acc.char_on.client_update_value(True) self.hass.block_till_done() self.assertEqual( self.events[0].data[ATTR_DOMAIN], domain) @@ -95,7 +95,7 @@ def test_input_boolean_set_state(self): self.assertEqual(acc.char_on.value, False) # Set from HomeKit - acc.char_on.set_value(True) + acc.char_on.client_update_value(True) self.hass.block_till_done() self.assertEqual( self.events[0].data[ATTR_DOMAIN], domain) diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index e1511163f2fdf2..feea5c0d01a88d 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -6,17 +6,32 @@ ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, STATE_COOL, STATE_HEAT, STATE_AUTO) -from homeassistant.components.homekit.type_thermostats import Thermostat from homeassistant.const import ( ATTR_SERVICE, EVENT_CALL_SERVICE, ATTR_SERVICE_DATA, ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import get_test_home_assistant +from tests.components.homekit.test_accessories import patch_debounce class TestHomekitThermostats(unittest.TestCase): """Test class for all accessory types regarding thermostats.""" + @classmethod + def setUpClass(cls): + """Setup Thermostat class import and debounce patcher.""" + cls.patcher = patch_debounce() + cls.patcher.start() + _import = __import__( + 'homeassistant.components.homekit.type_thermostats', + fromlist=['Thermostat']) + cls.thermostat_cls = _import.Thermostat + + @classmethod + def tearDownClass(cls): + """Stop debounce patcher.""" + cls.patcher.stop() + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() @@ -37,7 +52,7 @@ def test_default_thermostat(self): """Test if accessory and HA are updated accordingly.""" climate = 'climate.test' - acc = Thermostat(self.hass, climate, 'Climate', False, aid=2) + acc = self.thermostat_cls(self.hass, climate, 'Climate', False, aid=2) acc.run() self.assertEqual(acc.aid, 2) @@ -151,7 +166,7 @@ def test_default_thermostat(self): self.assertEqual(acc.char_display_units.value, 0) # Set from HomeKit - acc.char_target_temp.set_value(19.0) + acc.char_target_temp.client_update_value(19.0) self.hass.block_till_done() self.assertEqual( self.events[0].data[ATTR_SERVICE], 'set_temperature') @@ -159,7 +174,7 @@ def test_default_thermostat(self): self.events[0].data[ATTR_SERVICE_DATA][ATTR_TEMPERATURE], 19.0) self.assertEqual(acc.char_target_temp.value, 19.0) - acc.char_target_heat_cool.set_value(1) + acc.char_target_heat_cool.client_update_value(1) self.hass.block_till_done() self.assertEqual( self.events[1].data[ATTR_SERVICE], 'set_operation_mode') @@ -172,7 +187,7 @@ def test_auto_thermostat(self): """Test if accessory and HA are updated accordingly.""" climate = 'climate.test' - acc = Thermostat(self.hass, climate, 'Climate', True) + acc = self.thermostat_cls(self.hass, climate, 'Climate', True) acc.run() self.assertEqual(acc.char_cooling_thresh_temp.value, 23.0) @@ -221,7 +236,7 @@ def test_auto_thermostat(self): self.assertEqual(acc.char_display_units.value, 0) # Set from HomeKit - acc.char_heating_thresh_temp.set_value(20.0) + acc.char_heating_thresh_temp.client_update_value(20.0) self.hass.block_till_done() self.assertEqual( self.events[0].data[ATTR_SERVICE], 'set_temperature') @@ -229,7 +244,7 @@ def test_auto_thermostat(self): self.events[0].data[ATTR_SERVICE_DATA][ATTR_TARGET_TEMP_LOW], 20.0) self.assertEqual(acc.char_heating_thresh_temp.value, 20.0) - acc.char_cooling_thresh_temp.set_value(25.0) + acc.char_cooling_thresh_temp.client_update_value(25.0) self.hass.block_till_done() self.assertEqual( self.events[1].data[ATTR_SERVICE], 'set_temperature') @@ -242,7 +257,7 @@ def test_thermostat_fahrenheit(self): """Test if accessory and HA are updated accordingly.""" climate = 'climate.test' - acc = Thermostat(self.hass, climate, 'Climate', True) + acc = self.thermostat_cls(self.hass, climate, 'Climate', True) acc.run() self.hass.states.set(climate, STATE_AUTO, @@ -260,19 +275,19 @@ def test_thermostat_fahrenheit(self): self.assertEqual(acc.char_display_units.value, 1) # Set from HomeKit - acc.char_cooling_thresh_temp.set_value(23) + acc.char_cooling_thresh_temp.client_update_value(23) self.hass.block_till_done() service_data = self.events[-1].data[ATTR_SERVICE_DATA] self.assertEqual(service_data[ATTR_TARGET_TEMP_HIGH], 73.4) self.assertEqual(service_data[ATTR_TARGET_TEMP_LOW], 68) - acc.char_heating_thresh_temp.set_value(22) + acc.char_heating_thresh_temp.client_update_value(22) self.hass.block_till_done() service_data = self.events[-1].data[ATTR_SERVICE_DATA] self.assertEqual(service_data[ATTR_TARGET_TEMP_HIGH], 73.4) self.assertEqual(service_data[ATTR_TARGET_TEMP_LOW], 71.6) - acc.char_target_temp.set_value(24.0) + acc.char_target_temp.client_update_value(24.0) self.hass.block_till_done() service_data = self.events[-1].data[ATTR_SERVICE_DATA] self.assertEqual(service_data[ATTR_TEMPERATURE], 75.2) diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py index 0d4082731ab38f..8b8e7607b0776e 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -7,7 +7,9 @@ LowPassFilter, OutlierFilter, ThrottleFilter, TimeSMAFilter) import homeassistant.util.dt as dt_util from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component +import homeassistant.core as ha +from tests.common import (get_test_home_assistant, assert_setup_component, + init_recorder_component) class TestFilterSensor(unittest.TestCase): @@ -16,12 +18,24 @@ class TestFilterSensor(unittest.TestCase): def setup_method(self, method): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - self.values = [20, 19, 18, 21, 22, 0] + raw_values = [20, 19, 18, 21, 22, 0] + self.values = [] + + timestamp = dt_util.utcnow() + for val in raw_values: + self.values.append(ha.State('sensor.test_monitored', + val, last_updated=timestamp)) + timestamp += timedelta(minutes=1) def teardown_method(self, method): """Stop everything that was started.""" self.hass.stop() + def init_recorder(self): + """Initialize the recorder.""" + init_recorder_component(self.hass) + self.hass.start() + def test_setup_fail(self): """Test if filter doesn't exist.""" config = { @@ -36,31 +50,52 @@ def test_setup_fail(self): def test_chain(self): """Test if filter chaining works.""" + self.init_recorder() config = { + 'history': { + }, 'sensor': { 'platform': 'filter', 'name': 'test', 'entity_id': 'sensor.test_monitored', + 'history_period': '00:05', 'filters': [{ 'filter': 'outlier', + 'window_size': 10, 'radius': 4.0 }, { 'filter': 'lowpass', - 'window_size': 4, 'time_constant': 10, 'precision': 2 }] } } - with assert_setup_component(1): - assert setup_component(self.hass, 'sensor', config) + t_0 = dt_util.utcnow() - timedelta(minutes=1) + t_1 = dt_util.utcnow() - timedelta(minutes=2) + t_2 = dt_util.utcnow() - timedelta(minutes=3) - for value in self.values: - self.hass.states.set(config['sensor']['entity_id'], value) - self.hass.block_till_done() + fake_states = { + 'sensor.test_monitored': [ + ha.State('sensor.test_monitored', 18.0, last_changed=t_0), + ha.State('sensor.test_monitored', 19.0, last_changed=t_1), + ha.State('sensor.test_monitored', 18.2, last_changed=t_2), + ] + } - state = self.hass.states.get('sensor.test') - self.assertEqual('20.25', state.state) + with patch('homeassistant.components.history.' + 'state_changes_during_period', return_value=fake_states): + with patch('homeassistant.components.history.' + 'get_last_state_changes', return_value=fake_states): + with assert_setup_component(1, 'sensor'): + assert setup_component(self.hass, 'sensor', config) + + for value in self.values: + self.hass.states.set( + config['sensor']['entity_id'], value.state) + self.hass.block_till_done() + + state = self.hass.states.get('sensor.test') + self.assertEqual('19.25', state.state) def test_outlier(self): """Test if outlier filter works.""" @@ -70,7 +105,7 @@ def test_outlier(self): radius=4.0) for state in self.values: filtered = filt.filter_state(state) - self.assertEqual(22, filtered) + self.assertEqual(22, filtered.state) def test_lowpass(self): """Test if lowpass filter works.""" @@ -80,7 +115,7 @@ def test_lowpass(self): time_constant=10) for state in self.values: filtered = filt.filter_state(state) - self.assertEqual(18.05, filtered) + self.assertEqual(18.05, filtered.state) def test_throttle(self): """Test if lowpass filter works.""" @@ -92,7 +127,7 @@ def test_throttle(self): new_state = filt.filter_state(state) if not filt.skip_processing: filtered.append(new_state) - self.assertEqual([20, 21], filtered) + self.assertEqual([20, 21], [f.state for f in filtered]) def test_time_sma(self): """Test if time_sma filter works.""" @@ -100,9 +135,6 @@ def test_time_sma(self): precision=2, entity=None, type='last') - past = dt_util.utcnow() - timedelta(minutes=5) for state in self.values: - with patch('homeassistant.util.dt.utcnow', return_value=past): - filtered = filt.filter_state(state) - past += timedelta(minutes=1) - self.assertEqual(21.5, filtered) + filtered = filt.filter_state(state) + self.assertEqual(21.5, filtered.state) diff --git a/tests/components/test_google.py b/tests/components/test_google.py index fd45cfc59a9e35..0ee066fcfeecaa 100644 --- a/tests/components/test_google.py +++ b/tests/components/test_google.py @@ -58,6 +58,7 @@ def test_get_calendar_info(self): 'device_id': 'we_are_we_are_a_test_calendar', 'name': 'We are, we are, a... Test Calendar', 'track': True, + 'ignore_availability': True, }] }) diff --git a/tests/components/test_history.py b/tests/components/test_history.py index bea2af396cbc5e..5d909492380c12 100644 --- a/tests/components/test_history.py +++ b/tests/components/test_history.py @@ -131,6 +131,39 @@ def set_state(state): self.assertEqual(states, hist[entity_id]) + def test_get_last_state_changes(self): + """Test number of state changes.""" + self.init_recorder() + entity_id = 'sensor.test' + + def set_state(state): + """Set the state.""" + self.hass.states.set(entity_id, state) + self.wait_recording_done() + return self.hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=2) + point = start + timedelta(minutes=1) + point2 = point + timedelta(minutes=1) + + with patch('homeassistant.components.recorder.dt_util.utcnow', + return_value=start): + set_state('1') + + states = [] + with patch('homeassistant.components.recorder.dt_util.utcnow', + return_value=point): + states.append(set_state('2')) + + with patch('homeassistant.components.recorder.dt_util.utcnow', + return_value=point2): + states.append(set_state('3')) + + hist = history.get_last_state_changes( + self.hass, 2, entity_id) + + self.assertEqual(states, hist[entity_id]) + def test_get_significant_states(self): """Test that only significant states are returned.