diff --git a/.coveragerc b/.coveragerc index 9656508982a..4ba57e0f750 100644 --- a/.coveragerc +++ b/.coveragerc @@ -20,6 +20,9 @@ omit = homeassistant/components/android_ip_webcam.py homeassistant/components/*/android_ip_webcam.py + homeassistant/components/axis.py + homeassistant/components/*/axis.py + homeassistant/components/bbb_gpio.py homeassistant/components/*/bbb_gpio.py @@ -59,6 +62,9 @@ omit = homeassistant/components/isy994.py homeassistant/components/*/isy994.py + homeassistant/components/kira.py + homeassistant/components/*/kira.py + homeassistant/components/lutron.py homeassistant/components/*/lutron.py @@ -83,12 +89,18 @@ omit = homeassistant/components/qwikswitch.py homeassistant/components/*/qwikswitch.py + homeassistant/components/raspihats.py + homeassistant/components/*/raspihats.py + homeassistant/components/rfxtrx.py homeassistant/components/*/rfxtrx.py homeassistant/components/rpi_gpio.py homeassistant/components/*/rpi_gpio.py + homeassistant/components/rpi_pfio.py + homeassistant/components/*/rpi_pfio.py + homeassistant/components/scsgate.py homeassistant/components/*/scsgate.py @@ -175,6 +187,7 @@ omit = homeassistant/components/binary_sensor/flic.py homeassistant/components/binary_sensor/hikvision.py homeassistant/components/binary_sensor/iss.py + homeassistant/components/binary_sensor/mystrom.py homeassistant/components/binary_sensor/pilight.py homeassistant/components/binary_sensor/ping.py homeassistant/components/binary_sensor/rest.py @@ -239,6 +252,7 @@ omit = homeassistant/components/ifttt.py homeassistant/components/image_processing/dlib_face_detect.py homeassistant/components/image_processing/dlib_face_identify.py + homeassistant/components/image_processing/seven_segments.py homeassistant/components/joaoapps_join.py homeassistant/components/keyboard.py homeassistant/components/keyboard_remote.py diff --git a/.travis.yml b/.travis.yml index 864699a2fbd..0bdde06bb35 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,15 @@ sudo: false +addons: + apt: + packages: + - libudev-dev matrix: fast_finish: true include: - - python: "3.4.2" - env: TOXENV=py34 - - python: "3.4.2" - env: TOXENV=requirements - python: "3.4.2" env: TOXENV=lint + - python: "3.4.2" + env: TOXENV=py34 # - python: "3.5" # env: TOXENV=typing - python: "3.5" @@ -16,6 +18,8 @@ matrix: env: TOXENV=py36 - python: "3.6-dev" env: TOXENV=py36 + - python: "3.4.2" + env: TOXENV=requirements # allow_failures: # - python: "3.5" # env: TOXENV=typing @@ -25,5 +29,5 @@ cache: - $HOME/.cache/pip install: pip install -U tox coveralls language: python -script: tox +script: travis_wait tox after_success: coveralls diff --git a/Dockerfile b/Dockerfile index f0ceb982982..54f993b01a9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.5 +FROM python:3.6 MAINTAINER Paulus Schoutsen # Uncomment any of the following lines to disable the installation. diff --git a/homeassistant/components/alarm_control_panel/wink.py b/homeassistant/components/alarm_control_panel/wink.py index 12dca97dd81..a8cad115883 100644 --- a/homeassistant/components/alarm_control_panel/wink.py +++ b/homeassistant/components/alarm_control_panel/wink.py @@ -4,6 +4,7 @@ Interfaces with Wink Cameras. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.wink/ """ +import asyncio import logging import homeassistant.components.alarm_control_panel as alarm @@ -42,6 +43,11 @@ class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel): """Initialize the Wink alarm.""" super().__init__(wink, hass) + @asyncio.coroutine + def async_added_to_hass(self): + """Callback when entity is added to hass.""" + self.hass.data[DOMAIN]['entities']['alarm_control_panel'].append(self) + @property def state(self): """Return the state of the device.""" diff --git a/homeassistant/components/alexa.py b/homeassistant/components/alexa.py index 59d2ec8852a..1a3708fb746 100644 --- a/homeassistant/components/alexa.py +++ b/homeassistant/components/alexa.py @@ -17,7 +17,6 @@ from homeassistant.core import callback from homeassistant.const import HTTP_BAD_REQUEST from homeassistant.helpers import template, script, config_validation as cv from homeassistant.components.http import HomeAssistantView -import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -36,7 +35,6 @@ CONF_TEXT = 'text' CONF_FLASH_BRIEFINGS = 'flash_briefings' CONF_UID = 'uid' -CONF_DATE = 'date' CONF_TITLE = 'title' CONF_AUDIO = 'audio' CONF_TEXT = 'text' @@ -88,7 +86,6 @@ CONFIG_SCHEMA = vol.Schema({ CONF_FLASH_BRIEFINGS: { cv.string: vol.All(cv.ensure_list, [{ vol.Required(CONF_UID, default=str(uuid.uuid4())): cv.string, - vol.Optional(CONF_DATE, default=datetime.utcnow()): cv.string, vol.Required(CONF_TITLE): cv.template, vol.Optional(CONF_AUDIO): cv.template, vol.Required(CONF_TEXT, default=""): cv.template, @@ -331,10 +328,7 @@ class AlexaFlashBriefingView(HomeAssistantView): else: output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL) - if isinstance(item[CONF_DATE], str): - item[CONF_DATE] = dt_util.parse_datetime(item[CONF_DATE]) - - output[ATTR_UPDATE_DATE] = item[CONF_DATE].strftime(DATE_FORMAT) + output[ATTR_UPDATE_DATE] = datetime.now().strftime(DATE_FORMAT) briefing.append(output) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 5f59f760d0b..9227222d479 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -16,7 +16,7 @@ from homeassistant.core import CoreState from homeassistant import config as conf_util from homeassistant.const import ( ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, - SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START) + SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START, CONF_ID) from homeassistant.components import logbook from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import extract_domain_configs, script, condition @@ -26,6 +26,7 @@ from homeassistant.helpers.restore_state import async_get_last_state from homeassistant.loader import get_platform from homeassistant.util.dt import utcnow import homeassistant.helpers.config_validation as cv +from homeassistant.components.frontend import register_built_in_panel DOMAIN = 'automation' ENTITY_ID_FORMAT = DOMAIN + '.{}' @@ -81,6 +82,8 @@ _TRIGGER_SCHEMA = vol.All( _CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA]) PLATFORM_SCHEMA = vol.Schema({ + # str on purpose + CONF_ID: str, CONF_ALIAS: cv.string, vol.Optional(CONF_INITIAL_STATE): cv.boolean, vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean, @@ -139,16 +142,21 @@ def reload(hass): hass.services.call(DOMAIN, SERVICE_RELOAD) +def async_reload(hass): + """Reload the automation from config. + + Returns a coroutine object. + """ + return hass.services.async_call(DOMAIN, SERVICE_RELOAD) + + @asyncio.coroutine def async_setup(hass, config): """Set up the automation.""" component = EntityComponent(_LOGGER, DOMAIN, hass, group_name=GROUP_NAME_ALL_AUTOMATIONS) - success = yield from _async_process_config(hass, config, component) - - if not success: - return False + yield from _async_process_config(hass, config, component) descriptions = yield from hass.loop.run_in_executor( None, conf_util.load_yaml_config_file, os.path.join( @@ -215,15 +223,20 @@ def async_setup(hass, config): DOMAIN, service, turn_onoff_service_handler, descriptions.get(service), schema=SERVICE_SCHEMA) + if 'frontend' in hass.config.components: + register_built_in_panel(hass, 'automation', 'Automations', + 'mdi:playlist-play') + return True class AutomationEntity(ToggleEntity): """Entity to show status of entity.""" - def __init__(self, name, async_attach_triggers, cond_func, async_action, - hidden, initial_state): + def __init__(self, automation_id, name, async_attach_triggers, cond_func, + async_action, hidden, initial_state): """Initialize an automation entity.""" + self._id = automation_id self._name = name self._async_attach_triggers = async_attach_triggers self._async_detach_triggers = None @@ -346,6 +359,16 @@ class AutomationEntity(ToggleEntity): self.async_trigger) yield from self.async_update_ha_state() + @property + def device_state_attributes(self): + """Return automation attributes.""" + if self._id is None: + return None + + return { + CONF_ID: self._id + } + @asyncio.coroutine def _async_process_config(hass, config, component): @@ -359,6 +382,7 @@ def _async_process_config(hass, config, component): conf = config[config_key] for list_no, config_block in enumerate(conf): + automation_id = config_block.get(CONF_ID) name = config_block.get(CONF_ALIAS) or "{} {}".format(config_key, list_no) @@ -383,16 +407,14 @@ def _async_process_config(hass, config, component): config_block.get(CONF_TRIGGER, []), name ) entity = AutomationEntity( - name, async_attach_triggers, cond_func, action, hidden, - initial_state) + automation_id, name, async_attach_triggers, cond_func, action, + hidden, initial_state) entities.append(entity) if entities: yield from component.async_add_entities(entities) - return len(entities) > 0 - def _async_get_action(hass, config, name): """Return an action based on a configuration.""" diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index ba8e67e9213..32d2d245bef 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -9,8 +9,8 @@ import logging import voluptuous as vol -from homeassistant.core import callback, CoreState -from homeassistant.const import CONF_PLATFORM, EVENT_HOMEASSISTANT_START +from homeassistant.core import callback +from homeassistant.const import CONF_PLATFORM from homeassistant.helpers import config_validation as cv CONF_EVENT_TYPE = 'event_type' @@ -31,19 +31,6 @@ def async_trigger(hass, config, action): event_type = config.get(CONF_EVENT_TYPE) event_data = config.get(CONF_EVENT_DATA) - if (event_type == EVENT_HOMEASSISTANT_START and - hass.state == CoreState.starting): - _LOGGER.warning('Deprecation: Automations should not listen to event ' - "'homeassistant_start'. Use platform 'homeassistant' " - 'instead. Feature will be removed in 0.45') - hass.async_run_job(action, { - 'trigger': { - 'platform': 'event', - 'event': None, - }, - }) - return lambda: None - @callback def handle_event(event): """Listen for events and calls the action when data matches.""" diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 576e9e60186..9c12a37f9b8 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -79,6 +79,10 @@ def async_trigger(hass, config, action): call_action() return + # If only state attributes changed, ignore this event + if from_s.last_changed == to_s.last_changed: + return + @callback def state_for_listener(now): """Fire on state changes after a delay and calls action.""" diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 3ce84d60a91..dfed411745f 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -16,8 +16,6 @@ from homeassistant.const import ( from homeassistant.helpers.event import async_track_sunrise, async_track_sunset import homeassistant.helpers.config_validation as cv -DEPENDENCIES = ['sun'] - _LOGGER = logging.getLogger(__name__) TRIGGER_SCHEMA = vol.Schema({ diff --git a/homeassistant/components/axis.py b/homeassistant/components/axis.py new file mode 100644 index 00000000000..593eee2356e --- /dev/null +++ b/homeassistant/components/axis.py @@ -0,0 +1,314 @@ +""" +Support for Axis devices. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/axis/ +""" + +import json +import logging +import os + +import voluptuous as vol + +from homeassistant.const import (ATTR_LOCATION, ATTR_TRIPPED, + CONF_HOST, CONF_INCLUDE, CONF_NAME, + CONF_PASSWORD, CONF_TRIGGER_TIME, + CONF_USERNAME, EVENT_HOMEASSISTANT_STOP) +from homeassistant.components.discovery import SERVICE_AXIS +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import discovery +from homeassistant.helpers.entity import Entity +from homeassistant.loader import get_component + + +REQUIREMENTS = ['axis==7'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'axis' +CONFIG_FILE = 'axis.conf' + +AXIS_DEVICES = {} + +EVENT_TYPES = ['motion', 'vmd3', 'pir', 'sound', + 'daynight', 'tampering', 'input'] + +PLATFORMS = ['camera'] + +AXIS_INCLUDE = EVENT_TYPES + PLATFORMS + +AXIS_DEFAULT_HOST = '192.168.0.90' +AXIS_DEFAULT_USERNAME = 'root' +AXIS_DEFAULT_PASSWORD = 'pass' + +DEVICE_SCHEMA = vol.Schema({ + vol.Required(CONF_INCLUDE): + vol.All(cv.ensure_list, [vol.In(AXIS_INCLUDE)]), + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_HOST, default=AXIS_DEFAULT_HOST): cv.string, + vol.Optional(CONF_USERNAME, default=AXIS_DEFAULT_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD, default=AXIS_DEFAULT_PASSWORD): cv.string, + vol.Optional(CONF_TRIGGER_TIME, default=0): cv.positive_int, + vol.Optional(ATTR_LOCATION, default=''): cv.string, +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + cv.slug: DEVICE_SCHEMA, + }), +}, extra=vol.ALLOW_EXTRA) + + +def request_configuration(hass, name, host, serialnumber): + """Request configuration steps from the user.""" + configurator = get_component('configurator') + + def configuration_callback(callback_data): + """Called when config is submitted.""" + if CONF_INCLUDE not in callback_data: + configurator.notify_errors(request_id, + "Functionality mandatory.") + return False + callback_data[CONF_INCLUDE] = callback_data[CONF_INCLUDE].split() + callback_data[CONF_HOST] = host + if CONF_NAME not in callback_data: + callback_data[CONF_NAME] = name + try: + config = DEVICE_SCHEMA(callback_data) + except vol.Invalid: + configurator.notify_errors(request_id, + "Bad input, please check spelling.") + return False + + if setup_device(hass, config): + config_file = _read_config(hass) + config_file[serialnumber] = dict(config) + del config_file[serialnumber]['hass'] + _write_config(hass, config_file) + configurator.request_done(request_id) + else: + configurator.notify_errors(request_id, + "Failed to register, please try again.") + return False + + title = '{} ({})'.format(name, host) + request_id = configurator.request_config( + hass, title, configuration_callback, + description='Functionality: ' + str(AXIS_INCLUDE), + entity_picture="/static/images/logo_axis.png", + link_name='Axis platform documentation', + link_url='https://home-assistant.io/components/axis/', + submit_caption="Confirm", + fields=[ + {'id': CONF_NAME, + 'name': "Device name", + 'type': 'text'}, + {'id': CONF_USERNAME, + 'name': "User name", + 'type': 'text'}, + {'id': CONF_PASSWORD, + 'name': 'Password', + 'type': 'password'}, + {'id': CONF_INCLUDE, + 'name': "Device functionality (space separated list)", + 'type': 'text'}, + {'id': ATTR_LOCATION, + 'name': "Physical location of device (optional)", + 'type': 'text'}, + {'id': CONF_TRIGGER_TIME, + 'name': "Sensor update interval (optional)", + 'type': 'number'}, + ] + ) + + +def setup(hass, base_config): + """Common setup for Axis devices.""" + def _shutdown(call): # pylint: disable=unused-argument + """Stop the metadatastream on shutdown.""" + for serialnumber, device in AXIS_DEVICES.items(): + _LOGGER.info("Stopping metadatastream for %s.", serialnumber) + device.stop_metadatastream() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown) + + def axis_device_discovered(service, discovery_info): + """Called when axis devices has been found.""" + host = discovery_info['host'] + name = discovery_info['hostname'] + serialnumber = discovery_info['properties']['macaddress'] + + if serialnumber not in AXIS_DEVICES: + config_file = _read_config(hass) + if serialnumber in config_file: + try: + config = DEVICE_SCHEMA(config_file[serialnumber]) + except vol.Invalid as err: + _LOGGER.error("Bad data from %s. %s", CONFIG_FILE, err) + return False + if not setup_device(hass, config): + _LOGGER.error("Couldn\'t set up %s", config['name']) + else: + request_configuration(hass, name, host, serialnumber) + + discovery.listen(hass, SERVICE_AXIS, axis_device_discovered) + + if DOMAIN in base_config: + for device in base_config[DOMAIN]: + config = base_config[DOMAIN][device] + if CONF_NAME not in config: + config[CONF_NAME] = device + if not setup_device(hass, config): + _LOGGER.error("Couldn\'t set up %s", config['name']) + + return True + + +def setup_device(hass, config): + """Set up device.""" + from axis import AxisDevice + + config['hass'] = hass + device = AxisDevice(config) # Initialize device + enable_metadatastream = False + + if device.serial_number is None: + # If there is no serial number a connection could not be made + _LOGGER.error("Couldn\'t connect to %s", config[CONF_HOST]) + return False + + for component in config[CONF_INCLUDE]: + if component in EVENT_TYPES: + # Sensors are created by device calling event_initialized + # when receiving initialize messages on metadatastream + device.add_event_topic(convert(component, 'type', 'subscribe')) + if not enable_metadatastream: + enable_metadatastream = True + else: + discovery.load_platform(hass, component, DOMAIN, config) + + if enable_metadatastream: + device.initialize_new_event = event_initialized + device.initiate_metadatastream() + AXIS_DEVICES[device.serial_number] = device + return True + + +def _read_config(hass): + """Read Axis config.""" + path = hass.config.path(CONFIG_FILE) + + if not os.path.isfile(path): + return {} + + with open(path) as f_handle: + # Guard against empty file + return json.loads(f_handle.read() or '{}') + + +def _write_config(hass, config): + """Write Axis config.""" + data = json.dumps(config) + with open(hass.config.path(CONFIG_FILE), 'w', encoding='utf-8') as outfile: + outfile.write(data) + + +def event_initialized(event): + """Register event initialized on metadatastream here.""" + hass = event.device_config('hass') + discovery.load_platform(hass, + convert(event.topic, 'topic', 'platform'), + DOMAIN, {'axis_event': event}) + + +class AxisDeviceEvent(Entity): + """Representation of a Axis device event.""" + + def __init__(self, axis_event): + """Initialize the event.""" + self.axis_event = axis_event + self._event_class = convert(self.axis_event.topic, 'topic', 'class') + self._name = '{}_{}_{}'.format(self.axis_event.device_name, + convert(self.axis_event.topic, + 'topic', 'type'), + self.axis_event.id) + self.axis_event.callback = self._update_callback + + def _update_callback(self): + """Update the sensor's state, if needed.""" + self.update() + self.schedule_update_ha_state() + + @property + def name(self): + """Return the name of the event.""" + return self._name + + @property + def device_class(self): + """Return the class of the event.""" + return self._event_class + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def device_state_attributes(self): + """Return the state attributes of the event.""" + attr = {} + + tripped = self.axis_event.is_tripped + attr[ATTR_TRIPPED] = 'True' if tripped else 'False' + + location = self.axis_event.device_config(ATTR_LOCATION) + if location: + attr[ATTR_LOCATION] = location + + return attr + + +def convert(item, from_key, to_key): + """Translate between Axis and HASS syntax.""" + for entry in REMAP: + if entry[from_key] == item: + return entry[to_key] + + +REMAP = [{'type': 'motion', + 'class': 'motion', + 'topic': 'tns1:VideoAnalytics/tnsaxis:MotionDetection', + 'subscribe': 'onvif:VideoAnalytics/axis:MotionDetection', + 'platform': 'binary_sensor'}, + {'type': 'vmd3', + 'class': 'motion', + 'topic': 'tns1:RuleEngine/tnsaxis:VMD3/vmd3_video_1', + 'subscribe': 'onvif:RuleEngine/axis:VMD3/vmd3_video_1', + 'platform': 'binary_sensor'}, + {'type': 'pir', + 'class': 'motion', + 'topic': 'tns1:Device/tnsaxis:Sensor/PIR', + 'subscribe': 'onvif:Device/axis:Sensor/axis:PIR', + 'platform': 'binary_sensor'}, + {'type': 'sound', + 'class': 'sound', + 'topic': 'tns1:AudioSource/tnsaxis:TriggerLevel', + 'subscribe': 'onvif:AudioSource/axis:TriggerLevel', + 'platform': 'binary_sensor'}, + {'type': 'daynight', + 'class': 'light', + 'topic': 'tns1:VideoSource/tnsaxis:DayNightVision', + 'subscribe': 'onvif:VideoSource/axis:DayNightVision', + 'platform': 'binary_sensor'}, + {'type': 'tampering', + 'class': 'safety', + 'topic': 'tns1:VideoSource/tnsaxis:Tampering', + 'subscribe': 'onvif:VideoSource/axis:Tampering', + 'platform': 'binary_sensor'}, + {'type': 'input', + 'class': 'input', + 'topic': 'tns1:Device/tnsaxis:IO/Port', + 'subscribe': 'onvif:Device/axis:IO/Port', + 'platform': 'sensor'}, ] diff --git a/homeassistant/components/binary_sensor/axis.py b/homeassistant/components/binary_sensor/axis.py new file mode 100644 index 00000000000..125e9b33bd7 --- /dev/null +++ b/homeassistant/components/binary_sensor/axis.py @@ -0,0 +1,68 @@ +""" +Support for Axis binary sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.axis/ +""" + +import logging +from datetime import timedelta + +from homeassistant.components.binary_sensor import (BinarySensorDevice) +from homeassistant.components.axis import (AxisDeviceEvent) +from homeassistant.const import (CONF_TRIGGER_TIME) +from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.util.dt import utcnow + +DEPENDENCIES = ['axis'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup Axis device event.""" + add_devices([AxisBinarySensor(discovery_info['axis_event'], hass)], True) + + +class AxisBinarySensor(AxisDeviceEvent, BinarySensorDevice): + """Representation of a binary Axis event.""" + + def __init__(self, axis_event, hass): + """Initialize the binary sensor.""" + self.hass = hass + self._state = False + self._delay = axis_event.device_config(CONF_TRIGGER_TIME) + self._timer = None + AxisDeviceEvent.__init__(self, axis_event) + + @property + def is_on(self): + """Return true if event is active.""" + return self._state + + def update(self): + """Get the latest data and update the state.""" + self._state = self.axis_event.is_tripped + + def _update_callback(self): + """Update the sensor's state, if needed.""" + self.update() + + if self._timer is not None: + self._timer() + self._timer = None + + if self._delay > 0 and not self.is_on: + # Set timer to wait until updating the state + def _delay_update(now): + """Timer callback for sensor update.""" + _LOGGER.debug("%s Called delayed (%s sec) update.", + self._name, self._delay) + self.schedule_update_ha_state() + self._timer = None + + self._timer = track_point_in_utc_time( + self.hass, _delay_update, + utcnow() + timedelta(seconds=self._delay)) + else: + self.schedule_update_ha_state() diff --git a/homeassistant/components/binary_sensor/mystrom.py b/homeassistant/components/binary_sensor/mystrom.py new file mode 100644 index 00000000000..c551a8c4efe --- /dev/null +++ b/homeassistant/components/binary_sensor/mystrom.py @@ -0,0 +1,95 @@ +""" +Support for the myStrom buttons. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.mystrom/ +""" +import asyncio +import logging + +from homeassistant.components.binary_sensor import (BinarySensorDevice, DOMAIN) +from homeassistant.components.http import HomeAssistantView +from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['http'] + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up myStrom Binary Sensor.""" + hass.http.register_view(MyStromView(async_add_devices)) + + return True + + +class MyStromView(HomeAssistantView): + """View to handle requests from myStrom buttons.""" + + url = '/api/mystrom' + name = 'api:mystrom' + + def __init__(self, add_devices): + """Initialize the myStrom URL endpoint.""" + self.buttons = {} + self.add_devices = add_devices + + @asyncio.coroutine + def get(self, request): + """The GET request received from a myStrom button.""" + res = yield from self._handle(request.app['hass'], request.GET) + return res + + @asyncio.coroutine + def _handle(self, hass, data): + """Handle requests to the myStrom endpoint.""" + button_action = list(data.keys())[0] + button_id = data[button_action] + entity_id = '{}.{}_{}'.format(DOMAIN, button_id, button_action) + + if button_action not in ['single', 'double', 'long', 'touch']: + _LOGGER.error( + "Received unidentified message from myStrom button: %s", data) + return ("Received unidentified message: {}".format(data), + HTTP_UNPROCESSABLE_ENTITY) + + if entity_id not in self.buttons: + _LOGGER.info("New myStrom button/action detected: %s/%s", + button_id, button_action) + self.buttons[entity_id] = MyStromBinarySensor( + '{}_{}'.format(button_id, button_action)) + hass.async_add_job(self.add_devices, [self.buttons[entity_id]]) + else: + new_state = True if self.buttons[entity_id].state == 'off' \ + else False + self.buttons[entity_id].async_on_update(new_state) + + +class MyStromBinarySensor(BinarySensorDevice): + """Representation of a myStrom button.""" + + def __init__(self, button_id): + """Initialize the myStrom Binary sensor.""" + self._button_id = button_id + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._button_id + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._state + + def async_on_update(self, value): + """Receive an update.""" + self._state = value + self.hass.async_add_job(self.async_update_ha_state()) diff --git a/homeassistant/components/binary_sensor/raspihats.py b/homeassistant/components/binary_sensor/raspihats.py new file mode 100644 index 00000000000..ad19fb525a1 --- /dev/null +++ b/homeassistant/components/binary_sensor/raspihats.py @@ -0,0 +1,131 @@ +""" +Configure a binary_sensor using a digital input from a raspihats board. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.raspihats/ +""" +import logging +import voluptuous as vol +from homeassistant.const import ( + CONF_NAME, CONF_DEVICE_CLASS, DEVICE_DEFAULT_NAME +) +import homeassistant.helpers.config_validation as cv +from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA, BinarySensorDevice +) +from homeassistant.components.raspihats import ( + CONF_I2C_HATS, CONF_BOARD, CONF_ADDRESS, CONF_CHANNELS, CONF_INDEX, + CONF_INVERT_LOGIC, I2C_HAT_NAMES, I2C_HATS_MANAGER, I2CHatsException +) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['raspihats'] + +DEFAULT_INVERT_LOGIC = False +DEFAULT_DEVICE_CLASS = None + +_CHANNELS_SCHEMA = vol.Schema([{ + vol.Required(CONF_INDEX): cv.positive_int, + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, + vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): cv.string, +}]) + +_I2C_HATS_SCHEMA = vol.Schema([{ + vol.Required(CONF_BOARD): vol.In(I2C_HAT_NAMES), + vol.Required(CONF_ADDRESS): vol.Coerce(int), + vol.Required(CONF_CHANNELS): _CHANNELS_SCHEMA +}]) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_I2C_HATS): _I2C_HATS_SCHEMA, +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the raspihats binary_sensor devices.""" + I2CHatBinarySensor.I2C_HATS_MANAGER = hass.data[I2C_HATS_MANAGER] + binary_sensors = [] + i2c_hat_configs = config.get(CONF_I2C_HATS) + for i2c_hat_config in i2c_hat_configs: + address = i2c_hat_config[CONF_ADDRESS] + board = i2c_hat_config[CONF_BOARD] + try: + I2CHatBinarySensor.I2C_HATS_MANAGER.register_board(board, address) + for channel_config in i2c_hat_config[CONF_CHANNELS]: + binary_sensors.append( + I2CHatBinarySensor( + address, + channel_config[CONF_INDEX], + channel_config[CONF_NAME], + channel_config[CONF_INVERT_LOGIC], + channel_config[CONF_DEVICE_CLASS] + ) + ) + except I2CHatsException as ex: + _LOGGER.error( + "Failed to register " + board + "I2CHat@" + hex(address) + " " + + str(ex) + ) + add_devices(binary_sensors) + + +class I2CHatBinarySensor(BinarySensorDevice): + """Represents a binary sensor that uses a I2C-HAT digital input.""" + + I2C_HATS_MANAGER = None + + def __init__(self, address, channel, name, invert_logic, device_class): + """Initialize sensor.""" + self._address = address + self._channel = channel + self._name = name or DEVICE_DEFAULT_NAME + self._invert_logic = invert_logic + self._device_class = device_class + self._state = self.I2C_HATS_MANAGER.read_di( + self._address, + self._channel + ) + + def online_callback(): + """Callback fired when board is online.""" + self.schedule_update_ha_state() + + self.I2C_HATS_MANAGER.register_online_callback( + self._address, + self._channel, + online_callback + ) + + def edge_callback(state): + """Read digital input state.""" + self._state = state + self.schedule_update_ha_state() + + self.I2C_HATS_MANAGER.register_di_callback( + self._address, + self._channel, + edge_callback + ) + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._device_class + + @property + def name(self): + """Return the name of this sensor.""" + return self._name + + @property + def should_poll(self): + """Polling not needed for this sensor.""" + return False + + @property + def is_on(self): + """Return the state of this sensor.""" + return self._state != self._invert_logic diff --git a/homeassistant/components/binary_sensor/rpi_pfio.py b/homeassistant/components/binary_sensor/rpi_pfio.py new file mode 100644 index 00000000000..92d02067dfc --- /dev/null +++ b/homeassistant/components/binary_sensor/rpi_pfio.py @@ -0,0 +1,93 @@ +""" +Support for binary sensor using the PiFace Digital I/O module on a RPi. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.rpi_pfio/ +""" +import logging + +import voluptuous as vol + +import homeassistant.components.rpi_pfio as rpi_pfio +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.const import DEVICE_DEFAULT_NAME +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +ATTR_NAME = 'name' +ATTR_INVERT_LOGIC = 'invert_logic' +ATTR_SETTLE_TIME = 'settle_time' +CONF_PORTS = 'ports' + +DEFAULT_INVERT_LOGIC = False +DEFAULT_SETTLE_TIME = 20 + +DEPENDENCIES = ['rpi_pfio'] + +PORT_SCHEMA = vol.Schema({ + vol.Optional(ATTR_NAME, default=None): cv.string, + vol.Optional(ATTR_SETTLE_TIME, default=DEFAULT_SETTLE_TIME): + cv.positive_int, + vol.Optional(ATTR_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_PORTS, default={}): vol.Schema({ + cv.positive_int: PORT_SCHEMA + }) +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the PiFace Digital Input devices.""" + binary_sensors = [] + ports = config.get('ports') + for port, port_entity in ports.items(): + name = port_entity[ATTR_NAME] + settle_time = port_entity[ATTR_SETTLE_TIME] / 1000 + invert_logic = port_entity[ATTR_INVERT_LOGIC] + + binary_sensors.append(RPiPFIOBinarySensor( + hass, port, name, settle_time, invert_logic)) + add_devices(binary_sensors, True) + + rpi_pfio.activate_listener(hass) + + +class RPiPFIOBinarySensor(BinarySensorDevice): + """Represent a binary sensor that a PiFace Digital Input.""" + + def __init__(self, hass, port, name, settle_time, invert_logic): + """Initialize the RPi binary sensor.""" + self._port = port + self._name = name or DEVICE_DEFAULT_NAME + self._invert_logic = invert_logic + self._state = None + + def read_pfio(port): + """Read state from PFIO.""" + self._state = rpi_pfio.read_input(self._port) + self.schedule_update_ha_state() + + rpi_pfio.edge_detect(hass, self._port, read_pfio, settle_time) + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return the state of the entity.""" + return self._state != self._invert_logic + + def update(self): + """Update the PFIO state.""" + self._state = rpi_pfio.read_input(self._port) diff --git a/homeassistant/components/binary_sensor/wink.py b/homeassistant/components/binary_sensor/wink.py index 3f77d1d6081..c16c62a5f81 100644 --- a/homeassistant/components/binary_sensor/wink.py +++ b/homeassistant/components/binary_sensor/wink.py @@ -4,6 +4,7 @@ Support for Wink binary sensors. For more details about this platform, please refer to the documentation at at https://home-assistant.io/components/binary_sensor.wink/ """ +import asyncio import logging from homeassistant.components.binary_sensor import BinarySensorDevice @@ -101,6 +102,11 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity): else: self.capability = None + @asyncio.coroutine + def async_added_to_hass(self): + """Callback when entity is added to hass.""" + self.hass.data[DOMAIN]['entities']['binary_sensor'].append(self) + @property def is_on(self): """Return true if the binary sensor is on.""" diff --git a/homeassistant/components/blink.py b/homeassistant/components/blink.py index 4ae5007d665..a44f0163787 100644 --- a/homeassistant/components/blink.py +++ b/homeassistant/components/blink.py @@ -13,7 +13,7 @@ from homeassistant.const import ( CONF_USERNAME, CONF_PASSWORD, ATTR_FRIENDLY_NAME, ATTR_ARMED) from homeassistant.helpers import discovery -REQUIREMENTS = ['blinkpy==0.5.2'] +REQUIREMENTS = ['blinkpy==0.6.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index f6238d6ae23..79f0757d006 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -269,7 +269,7 @@ class CameraImageView(CameraView): image = yield from camera.async_camera_image() if image: - return web.Response(body=image) + return web.Response(body=image, content_type='image/jpeg') return web.Response(status=500) diff --git a/homeassistant/components/camera/amcrest.py b/homeassistant/components/camera/amcrest.py index 72d3120c77a..8f8b7e5f9f5 100644 --- a/homeassistant/components/camera/amcrest.py +++ b/homeassistant/components/camera/amcrest.py @@ -12,18 +12,22 @@ import voluptuous as vol import homeassistant.loader as loader from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) +from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import ( - async_get_clientsession, async_aiohttp_proxy_web) + async_get_clientsession, async_aiohttp_proxy_web, + async_aiohttp_proxy_stream) -REQUIREMENTS = ['amcrest==1.1.9'] +REQUIREMENTS = ['amcrest==1.2.0'] +DEPENDENCIES = ['ffmpeg'] _LOGGER = logging.getLogger(__name__) CONF_RESOLUTION = 'resolution' CONF_STREAM_SOURCE = 'stream_source' +CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' DEFAULT_NAME = 'Amcrest Camera' DEFAULT_PORT = 80 @@ -40,7 +44,8 @@ RESOLUTION_LIST = { STREAM_SOURCE_LIST = { 'mjpeg': 0, - 'snapshot': 1 + 'snapshot': 1, + 'rtsp': 2, } CONTENT_TYPE_HEADER = 'Content-Type' @@ -56,6 +61,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE): vol.All(vol.In(STREAM_SOURCE_LIST)), + vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string, }) @@ -92,8 +98,9 @@ class AmcrestCam(Camera): super(AmcrestCam, self).__init__() self._camera = camera self._base_url = self._camera.get_base_url() - self._hass = hass self._name = device_info.get(CONF_NAME) + self._ffmpeg = hass.data[DATA_FFMPEG] + self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS) self._resolution = RESOLUTION_LIST[device_info.get(CONF_RESOLUTION)] self._stream_source = STREAM_SOURCE_LIST[ device_info.get(CONF_STREAM_SOURCE) @@ -117,15 +124,28 @@ class AmcrestCam(Camera): yield from super().handle_async_mjpeg_stream(request) return - # Otherwise, stream an MJPEG image stream directly from the camera - websession = async_get_clientsession(self.hass) - streaming_url = '{0}mjpg/video.cgi?channel=0&subtype={1}'.format( - self._base_url, self._resolution) + elif self._stream_source == STREAM_SOURCE_LIST['mjpeg']: + # stream an MJPEG image stream directly from the camera + websession = async_get_clientsession(self.hass) + streaming_url = self._camera.mjpeg_url(typeno=self._resolution) + stream_coro = websession.get( + streaming_url, auth=self._token, timeout=TIMEOUT) - stream_coro = websession.get( - streaming_url, auth=self._token, timeout=TIMEOUT) + yield from async_aiohttp_proxy_web(self.hass, request, stream_coro) - yield from async_aiohttp_proxy_web(self.hass, request, stream_coro) + else: + # streaming via fmpeg + from haffmpeg import CameraMjpeg + + streaming_url = self._camera.rtsp_url(typeno=self._resolution) + stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) + yield from stream.open_camera( + streaming_url, extra_cmd=self._ffmpeg_arguments) + + yield from async_aiohttp_proxy_stream( + self.hass, request, stream, + 'multipart/x-mixed-replace;boundary=ffserver') + yield from stream.close() @property def name(self): diff --git a/homeassistant/components/camera/axis.py b/homeassistant/components/camera/axis.py new file mode 100644 index 00000000000..3de1c568745 --- /dev/null +++ b/homeassistant/components/camera/axis.py @@ -0,0 +1,38 @@ +""" +Support for Axis camera streaming. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/camera.axis/ +""" +import logging + +from homeassistant.const import ( + CONF_NAME, CONF_USERNAME, CONF_PASSWORD, + CONF_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION) +from homeassistant.components.camera.mjpeg import ( + CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['axis'] +DOMAIN = 'axis' + + +def _get_image_url(host, mode): + if mode == 'mjpeg': + return 'http://{}/axis-cgi/mjpg/video.cgi'.format(host) + elif mode == 'single': + return 'http://{}/axis-cgi/jpg/image.cgi'.format(host) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup Axis camera.""" + device_info = { + CONF_NAME: discovery_info['name'], + CONF_USERNAME: discovery_info['username'], + CONF_PASSWORD: discovery_info['password'], + CONF_MJPEG_URL: _get_image_url(discovery_info['host'], 'mjpeg'), + CONF_STILL_IMAGE_URL: _get_image_url(discovery_info['host'], 'single'), + CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION, + } + add_devices([MjpegCamera(hass, device_info)]) diff --git a/homeassistant/components/camera/uvc.py b/homeassistant/components/camera/uvc.py index 3840a8a90b1..c0a8039ee64 100644 --- a/homeassistant/components/camera/uvc.py +++ b/homeassistant/components/camera/uvc.py @@ -20,12 +20,15 @@ _LOGGER = logging.getLogger(__name__) CONF_NVR = 'nvr' CONF_KEY = 'key' +CONF_PASSWORD = 'password' +DEFAULT_PASSWORD = 'ubnt' DEFAULT_PORT = 7080 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_NVR): cv.string, vol.Required(CONF_KEY): cv.string, + vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, }) @@ -34,6 +37,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Discover cameras on a Unifi NVR.""" addr = config[CONF_NVR] key = config[CONF_KEY] + password = config[CONF_PASSWORD] port = config[CONF_PORT] from uvcclient import nvr @@ -59,7 +63,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([UnifiVideoCamera(nvrconn, camera[identifier], - camera['name']) + camera['name'], + password) for camera in cameras]) return True @@ -67,12 +72,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class UnifiVideoCamera(Camera): """A Ubiquiti Unifi Video Camera.""" - def __init__(self, nvr, uuid, name): + def __init__(self, nvr, uuid, name, password): """Initialize an Unifi camera.""" super(UnifiVideoCamera, self).__init__() self._nvr = nvr self._uuid = uuid self._name = name + self._password = password self.is_streaming = False self._connect_addr = None self._camera = None @@ -102,7 +108,6 @@ class UnifiVideoCamera(Camera): def _login(self): """Login to the camera.""" from uvcclient import camera as uvc_camera - from uvcclient import store as uvc_store caminfo = self._nvr.get_camera(self._uuid) if self._connect_addr: @@ -110,13 +115,6 @@ class UnifiVideoCamera(Camera): else: addrs = [caminfo['host'], caminfo['internalHost']] - store = uvc_store.get_info_store() - password = store.get_camera_password(self._uuid) - if password is None: - _LOGGER.debug("Logging into camera %(name)s with default password", - dict(name=self._name)) - password = 'ubnt' - if self._nvr.server_version >= (3, 2, 0): client_cls = uvc_camera.UVCCameraClientV320 else: @@ -126,7 +124,7 @@ class UnifiVideoCamera(Camera): for addr in addrs: try: camera = client_cls( - addr, caminfo['username'], password) + addr, caminfo['username'], self._password) camera.login() _LOGGER.debug("Logged into UVC camera %(name)s via %(addr)s", dict(name=self._name, addr=addr)) diff --git a/homeassistant/components/camera/zoneminder.py b/homeassistant/components/camera/zoneminder.py index 5410833761b..a98e3ef066f 100644 --- a/homeassistant/components/camera/zoneminder.py +++ b/homeassistant/components/camera/zoneminder.py @@ -107,12 +107,7 @@ class ZoneMinderCamera(MjpegCamera): self._monitor_id) return - if not status_response.get("success", False): - _LOGGER.warning("Alarm status API call failed for monitor %i", - self._monitor_id) - return - - self._is_recording = status_response['status'] == ZM_STATE_ALARM + self._is_recording = status_response.get('status') == ZM_STATE_ALARM @property def is_recording(self): diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py index afb04fa3c91..79d231a69c5 100644 --- a/homeassistant/components/climate/sensibo.py +++ b/homeassistant/components/climate/sensibo.py @@ -149,22 +149,22 @@ class SensiboClimate(ClimateDevice): @property def current_fan_mode(self): """Return the fan setting.""" - return self._ac_states['fanLevel'] + return self._ac_states.get('fanLevel') @property def fan_list(self): """List of available fan modes.""" - return self._current_capabilities['fanLevels'] + return self._current_capabilities.get('fanLevels') @property def current_swing_mode(self): """Return the fan setting.""" - return self._ac_states['swing'] + return self._ac_states.get('swing') @property def swing_list(self): """List of available swing modes.""" - return self._current_capabilities['swing'] + return self._current_capabilities.get('swing') @property def name(self): diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py index 256af2d013c..1be7480a727 100644 --- a/homeassistant/components/climate/wink.py +++ b/homeassistant/components/climate/wink.py @@ -4,6 +4,8 @@ Support for Wink thermostats. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/climate.wink/ """ +import asyncio + from homeassistant.components.wink import WinkDevice, DOMAIN from homeassistant.components.climate import ( STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice, @@ -52,6 +54,11 @@ class WinkThermostat(WinkDevice, ClimateDevice): super().__init__(wink, hass) self._config_temp_unit = temp_unit + @asyncio.coroutine + def async_added_to_hass(self): + """Callback when entity is added to hass.""" + self.hass.data[DOMAIN]['entities']['climate'].append(self) + @property def temperature_unit(self): """Return the unit of measurement.""" diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 1255043b6b5..0bc44501e28 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -5,7 +5,7 @@ import os import voluptuous as vol from homeassistant.core import callback -from homeassistant.const import EVENT_COMPONENT_LOADED +from homeassistant.const import EVENT_COMPONENT_LOADED, CONF_ID from homeassistant.setup import ( async_prepare_setup_platform, ATTR_COMPONENT) from homeassistant.components.frontend import register_built_in_panel @@ -14,8 +14,8 @@ from homeassistant.util.yaml import load_yaml, dump DOMAIN = 'config' DEPENDENCIES = ['http'] -SECTIONS = ('core', 'group', 'hassbian') -ON_DEMAND = ('zwave', ) +SECTIONS = ('core', 'group', 'hassbian', 'automation') +ON_DEMAND = ('zwave') @asyncio.coroutine @@ -60,7 +60,7 @@ def async_setup(hass, config): return True -class EditKeyBasedConfigView(HomeAssistantView): +class BaseEditConfigView(HomeAssistantView): """Configure a Group endpoint.""" def __init__(self, component, config_type, path, key_schema, data_schema, @@ -73,13 +73,29 @@ class EditKeyBasedConfigView(HomeAssistantView): self.data_schema = data_schema self.post_write_hook = post_write_hook + def _empty_config(self): + """Empty config if file not found.""" + raise NotImplementedError + + def _get_value(self, data, config_key): + """Get value.""" + raise NotImplementedError + + def _write_value(self, data, config_key, new_value): + """Set value.""" + raise NotImplementedError + @asyncio.coroutine def get(self, request, config_key): """Fetch device specific config.""" hass = request.app['hass'] - current = yield from hass.loop.run_in_executor( - None, _read, hass.config.path(self.path)) - return self.json(current.get(config_key, {})) + current = yield from self.read_config(hass) + value = self._get_value(current, config_key) + + if value is None: + return self.json_message('Resource not found', 404) + + return self.json(value) @asyncio.coroutine def post(self, request, config_key): @@ -104,10 +120,10 @@ class EditKeyBasedConfigView(HomeAssistantView): hass = request.app['hass'] path = hass.config.path(self.path) - current = yield from hass.loop.run_in_executor(None, _read, path) - current.setdefault(config_key, {}).update(data) + current = yield from self.read_config(hass) + self._write_value(current, config_key, data) - yield from hass.loop.run_in_executor(None, _write, path, current) + yield from hass.async_add_job(_write, path, current) if self.post_write_hook is not None: hass.async_add_job(self.post_write_hook(hass)) @@ -116,13 +132,59 @@ class EditKeyBasedConfigView(HomeAssistantView): 'result': 'ok', }) + @asyncio.coroutine + def read_config(self, hass): + """Read the config.""" + current = yield from hass.async_add_job( + _read, hass.config.path(self.path)) + if not current: + current = self._empty_config() + return current + + +class EditKeyBasedConfigView(BaseEditConfigView): + """Configure a list of entries.""" + + def _empty_config(self): + """Return an empty config.""" + return {} + + def _get_value(self, data, config_key): + """Get value.""" + return data.get(config_key, {}) + + def _write_value(self, data, config_key, new_value): + """Set value.""" + data.setdefault(config_key, {}).update(new_value) + + +class EditIdBasedConfigView(BaseEditConfigView): + """Configure key based config entries.""" + + def _empty_config(self): + """Return an empty config.""" + return [] + + def _get_value(self, data, config_key): + """Get value.""" + return next( + (val for val in data if val.get(CONF_ID) == config_key), None) + + def _write_value(self, data, config_key, new_value): + """Set value.""" + value = self._get_value(data, config_key) + + if value is None: + value = {CONF_ID: config_key} + data.append(value) + + value.update(new_value) + def _read(path): """Read YAML helper.""" if not os.path.isfile(path): - with open(path, 'w'): - pass - return {} + return None return load_yaml(path) diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py new file mode 100644 index 00000000000..64eccfaa2b8 --- /dev/null +++ b/homeassistant/components/config/automation.py @@ -0,0 +1,20 @@ +"""Provide configuration end points for Z-Wave.""" +import asyncio + +from homeassistant.components.config import EditIdBasedConfigView +from homeassistant.components.automation import ( + PLATFORM_SCHEMA, DOMAIN, async_reload) +import homeassistant.helpers.config_validation as cv + + +CONFIG_PATH = 'automations.yaml' + + +@asyncio.coroutine +def async_setup(hass): + """Set up the Automation config API.""" + hass.http.register_view(EditIdBasedConfigView( + DOMAIN, 'config', CONFIG_PATH, cv.string, + PLATFORM_SCHEMA, post_write_hook=async_reload + )) + return True diff --git a/homeassistant/components/configurator.py b/homeassistant/components/configurator.py index c37f07956e4..e502e0a0253 100644 --- a/homeassistant/components/configurator.py +++ b/homeassistant/components/configurator.py @@ -9,9 +9,11 @@ the user has submitted configuration information. import asyncio import logging +from homeassistant.core import callback as async_callback from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \ ATTR_ENTITY_PICTURE from homeassistant.helpers.entity import generate_entity_id +from homeassistant.util.async import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) _REQUESTS = {} @@ -43,7 +45,9 @@ def request_config( Will return an ID to be used for sequent calls. """ - instance = _get_instance(hass) + instance = run_callback_threadsafe(hass.loop, + _async_get_instance, + hass).result() request_id = instance.request_config( name, callback, @@ -79,7 +83,8 @@ def async_setup(hass, config): return True -def _get_instance(hass): +@async_callback +def _async_get_instance(hass): """Get an instance per hass object.""" instance = hass.data.get(_KEY_INSTANCE) @@ -97,7 +102,7 @@ class Configurator(object): self.hass = hass self._cur_id = 0 self._requests = {} - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_CONFIGURE, self.handle_service_call) def request_config( diff --git a/homeassistant/components/cover/lutron_caseta.py b/homeassistant/components/cover/lutron_caseta.py new file mode 100644 index 00000000000..2c411c61ba4 --- /dev/null +++ b/homeassistant/components/cover/lutron_caseta.py @@ -0,0 +1,62 @@ +""" +Support for Lutron Caseta SerenaRollerShade. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/cover.lutron_caseta/ +""" +import logging + + +from homeassistant.components.cover import ( + CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE) +from homeassistant.components.lutron_caseta import ( + LUTRON_CASETA_SMARTBRIDGE, LutronCasetaDevice) + + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['lutron_caseta'] + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Lutron Caseta Serena shades as a cover device.""" + devs = [] + bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] + cover_devices = bridge.get_devices_by_types(["SerenaRollerShade"]) + for cover_device in cover_devices: + dev = LutronCasetaCover(cover_device, bridge) + devs.append(dev) + + add_devices(devs, True) + + +class LutronCasetaCover(LutronCasetaDevice, CoverDevice): + """Representation of a Lutron Serena shade.""" + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_OPEN | SUPPORT_CLOSE + + @property + def is_closed(self): + """Return if the cover is closed.""" + return self._state["current_state"] < 1 + + def close_cover(self): + """Close the cover.""" + self._smartbridge.set_value(self._device_id, 0) + + def open_cover(self): + """Open the cover.""" + self._smartbridge.set_value(self._device_id, 100) + + def set_cover_position(self, position, **kwargs): + """Move the roller shutter to a specific position.""" + self._smartbridge.set_value(self._device_id, position) + + def update(self): + """Call when forcing a refresh of the device.""" + self._state = self._smartbridge.get_device_by_id(self._device_id) + _LOGGER.debug(self._state) diff --git a/homeassistant/components/cover/wink.py b/homeassistant/components/cover/wink.py index 5472180db62..d5908c35ca2 100644 --- a/homeassistant/components/cover/wink.py +++ b/homeassistant/components/cover/wink.py @@ -4,6 +4,8 @@ Support for Wink Covers. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.wink/ """ +import asyncio + from homeassistant.components.cover import CoverDevice from homeassistant.components.wink import WinkDevice, DOMAIN @@ -31,6 +33,11 @@ class WinkCoverDevice(WinkDevice, CoverDevice): """Initialize the cover.""" super().__init__(wink, hass) + @asyncio.coroutine + def async_added_to_hass(self): + """Callback when entity is added to hass.""" + self.hass.data[DOMAIN]['entities']['cover'].append(self) + def close_cover(self): """Close the shade.""" self.wink.set_state(0) diff --git a/homeassistant/components/datadog.py b/homeassistant/components/datadog.py new file mode 100644 index 00000000000..2c8145177b7 --- /dev/null +++ b/homeassistant/components/datadog.py @@ -0,0 +1,120 @@ +""" +A component which allows you to send data to Datadog. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/datadog/ +""" +import logging +import voluptuous as vol + +from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_PREFIX, + EVENT_LOGBOOK_ENTRY, EVENT_STATE_CHANGED, + STATE_UNKNOWN) +from homeassistant.helpers import state as state_helper +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['datadog==0.15.0'] + +_LOGGER = logging.getLogger(__name__) + +CONF_RATE = 'rate' +DEFAULT_HOST = 'localhost' +DEFAULT_PORT = 8125 +DEFAULT_PREFIX = 'hass' +DEFAULT_RATE = 1 +DOMAIN = 'datadog' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_PREFIX, default=DEFAULT_PREFIX): cv.string, + vol.Optional(CONF_RATE, default=DEFAULT_RATE): + vol.All(vol.Coerce(int), vol.Range(min=1)), + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Setup the Datadog component.""" + from datadog import initialize, statsd + + conf = config[DOMAIN] + host = conf.get(CONF_HOST) + port = conf.get(CONF_PORT) + sample_rate = conf.get(CONF_RATE) + prefix = conf.get(CONF_PREFIX) + + initialize(statsd_host=host, statsd_port=port) + + def logbook_entry_listener(event): + """Listen for logbook entries and send them as events.""" + name = event.data.get('name') + message = event.data.get('message') + + statsd.event( + title="Home Assistant", + text="%%% \n **{}** {} \n %%%".format(name, message), + tags=[ + "entity:{}".format(event.data.get('entity_id')), + "domain:{}".format(event.data.get('domain')) + ] + ) + + _LOGGER.debug('Sent event %s', event.data.get('entity_id')) + + def state_changed_listener(event): + """Listen for new messages on the bus and sends them to Datadog.""" + state = event.data.get('new_state') + + if state is None or state.state == STATE_UNKNOWN: + return + + if state.attributes.get('hidden') is True: + return + + states = dict(state.attributes) + metric = "{}.{}".format(prefix, state.domain) + tags = ["entity:{}".format(state.entity_id)] + + for key, value in states.items(): + if isinstance(value, (float, int)): + attribute = "{}.{}".format(metric, key.replace(' ', '_')) + statsd.gauge( + attribute, + value, + sample_rate=sample_rate, + tags=tags + ) + + _LOGGER.debug( + 'Sent metric %s: %s (tags: %s)', + attribute, + value, + tags + ) + + try: + value = state_helper.state_as_number(state) + except ValueError: + _LOGGER.debug( + 'Error sending %s: %s (tags: %s)', + metric, + state.state, + tags + ) + return + + statsd.gauge( + metric, + value, + sample_rate=sample_rate, + tags=tags + ) + + _LOGGER.debug('Sent metric %s: %s (tags: %s)', metric, value, tags) + + hass.bus.listen(EVENT_LOGBOOK_ENTRY, logbook_entry_listener) + hass.bus.listen(EVENT_STATE_CHANGED, state_changed_listener) + + return True diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index f926d059db2..f77a5f05f62 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -157,28 +157,28 @@ def async_setup(hass, config): }}, ]})) - tasks2.append(group.Group.async_create_group(hass, 'living room', [ + tasks2.append(group.Group.async_create_group(hass, 'Living Room', [ lights[1], switches[0], 'input_select.living_room_preset', 'cover.living_room_window', media_players[1], 'scene.romantic_lights'])) - tasks2.append(group.Group.async_create_group(hass, 'bedroom', [ + tasks2.append(group.Group.async_create_group(hass, 'Bedroom', [ lights[0], switches[1], media_players[0], 'input_slider.noise_allowance'])) - tasks2.append(group.Group.async_create_group(hass, 'kitchen', [ + tasks2.append(group.Group.async_create_group(hass, 'Kitchen', [ lights[2], 'cover.kitchen_window', 'lock.kitchen_door'])) - tasks2.append(group.Group.async_create_group(hass, 'doors', [ + tasks2.append(group.Group.async_create_group(hass, 'Doors', [ 'lock.front_door', 'lock.kitchen_door', 'garage_door.right_garage_door', 'garage_door.left_garage_door'])) - tasks2.append(group.Group.async_create_group(hass, 'automations', [ + tasks2.append(group.Group.async_create_group(hass, 'Automations', [ 'input_select.who_cooks', 'input_boolean.notify', ])) - tasks2.append(group.Group.async_create_group(hass, 'people', [ + tasks2.append(group.Group.async_create_group(hass, 'People', [ 'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy', 'device_tracker.demo_paulus'])) - tasks2.append(group.Group.async_create_group(hass, 'downstairs', [ + tasks2.append(group.Group.async_create_group(hass, 'Downstairs', [ 'group.living_room', 'group.kitchen', 'scene.romantic_lights', 'cover.kitchen_window', 'cover.living_room_window', 'group.doors', - 'thermostat.ecobee', + 'climate.ecobee', ], view=True)) results = yield from asyncio.gather(*tasks2, loop=hass.loop) diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index 9119394e357..a1297c5c118 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -14,12 +14,13 @@ from homeassistant.core import callback import homeassistant.util.dt as dt_util from homeassistant.const import STATE_HOME, STATE_NOT_HOME from homeassistant.helpers.event import ( - async_track_point_in_time, async_track_state_change) + async_track_point_in_utc_time, async_track_state_change) +from homeassistant.helpers.sun import is_up, get_astral_event_next from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv DOMAIN = 'device_sun_light_trigger' -DEPENDENCIES = ['light', 'device_tracker', 'group', 'sun'] +DEPENDENCIES = ['light', 'device_tracker', 'group'] CONF_DEVICE_GROUP = 'device_group' CONF_DISABLE_TURN_OFF = 'disable_turn_off' @@ -50,7 +51,6 @@ def async_setup(hass, config): device_tracker = get_component('device_tracker') group = get_component('group') light = get_component('light') - sun = get_component('sun') conf = config[DOMAIN] disable_turn_off = conf.get(CONF_DISABLE_TURN_OFF) light_group = conf.get(CONF_LIGHT_GROUP, light.ENTITY_ID_ALL_LIGHTS) @@ -78,7 +78,7 @@ def async_setup(hass, config): Async friendly. """ - next_setting = sun.next_setting(hass) + next_setting = get_astral_event_next(hass, 'sunset') if not next_setting: return None return next_setting - LIGHT_TRANSITION_TIME * len(light_ids) @@ -103,7 +103,7 @@ def async_setup(hass, config): # Track every time sun rises so we can schedule a time-based # pre-sun set event @callback - def schedule_light_turn_on(entity, old_state, new_state): + def schedule_light_turn_on(now): """Turn on all the lights at the moment sun sets. We will schedule to have each light start after one another @@ -114,26 +114,26 @@ def async_setup(hass, config): return for index, light_id in enumerate(light_ids): - async_track_point_in_time( + async_track_point_in_utc_time( hass, async_turn_on_factory(light_id), start_point + index * LIGHT_TRANSITION_TIME) - async_track_state_change(hass, sun.ENTITY_ID, schedule_light_turn_on, - sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON) + async_track_point_in_utc_time(hass, schedule_light_turn_on, + get_astral_event_next(hass, 'sunrise')) # If the sun is already above horizon schedule the time-based pre-sun set # event. - if sun.is_on(hass): - schedule_light_turn_on(None, None, None) + if is_up(hass): + schedule_light_turn_on(None) @callback def check_light_on_dev_state_change(entity, old_state, new_state): """Handle tracked device state changes.""" lights_are_on = group.is_on(hass, light_group) - light_needed = not (lights_are_on or sun.is_on(hass)) + light_needed = not (lights_are_on or is_up(hass)) # These variables are needed for the elif check - now = dt_util.now() + now = dt_util.utcnow() start_point = calc_time_for_light_when_sunset() # Do we need lights? @@ -146,7 +146,7 @@ def async_setup(hass, config): # Check this by seeing if current time is later then the point # in time when we would start putting the lights on. elif (start_point and - start_point < now < sun.next_setting(hass)): + start_point < now < get_astral_event_next(hass, 'sunset')): # Check for every light if it would be on if someone was home # when the fading in started and turn it on if so diff --git a/homeassistant/components/device_tracker/automatic.py b/homeassistant/components/device_tracker/automatic.py index 56dccd75d6d..891f1b22775 100644 --- a/homeassistant/components/device_tracker/automatic.py +++ b/homeassistant/components/device_tracker/automatic.py @@ -21,7 +21,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval -REQUIREMENTS = ['aioautomatic==0.3.1'] +REQUIREMENTS = ['aioautomatic==0.4.0'] _LOGGER = logging.getLogger(__name__) @@ -31,7 +31,8 @@ CONF_DEVICES = 'devices' DEFAULT_TIMEOUT = 5 -SCOPE = ['location', 'vehicle:profile', 'trip'] +DEFAULT_SCOPE = ['location', 'vehicle:profile', 'trip'] +FULL_SCOPE = DEFAULT_SCOPE + ['current_location'] ATTR_FUEL_LEVEL = 'fuel_level' @@ -58,8 +59,17 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None): client_session=async_get_clientsession(hass), request_kwargs={'timeout': DEFAULT_TIMEOUT}) try: - session = yield from client.create_session_from_password( - SCOPE, config[CONF_USERNAME], config[CONF_PASSWORD]) + try: + session = yield from client.create_session_from_password( + FULL_SCOPE, config[CONF_USERNAME], config[CONF_PASSWORD]) + except aioautomatic.exceptions.ForbiddenError as exc: + if not str(exc).startswith("invalid_scope"): + raise exc + _LOGGER.info("Client not authorized for current_location scope. " + "location:updated events will not be received.") + session = yield from client.create_session_from_password( + DEFAULT_SCOPE, config[CONF_USERNAME], config[CONF_PASSWORD]) + data = AutomaticData( hass, client, session, config[CONF_DEVICES], async_see) diff --git a/homeassistant/components/device_tracker/linksys_ap.py b/homeassistant/components/device_tracker/linksys_ap.py index a337f71cec4..01f97eb6e42 100644 --- a/homeassistant/components/device_tracker/linksys_ap.py +++ b/homeassistant/components/device_tracker/linksys_ap.py @@ -22,7 +22,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) INTERFACES = 2 DEFAULT_TIMEOUT = 10 -REQUIREMENTS = ['beautifulsoup4==4.5.3'] +REQUIREMENTS = ['beautifulsoup4==4.6.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/device_tracker/tplink.py b/homeassistant/components/device_tracker/tplink.py index 65fa89d737f..b34f16ebb5a 100755 --- a/homeassistant/components/device_tracker/tplink.py +++ b/homeassistant/components/device_tracker/tplink.py @@ -163,6 +163,7 @@ class Tplink3DeviceScanner(TplinkDeviceScanner): def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" self._update_info() + self._log_out() return self.last_results.keys() # pylint: disable=no-self-use @@ -195,8 +196,9 @@ class Tplink3DeviceScanner(TplinkDeviceScanner): self.sysauth = regex_result.group(1) _LOGGER.info(self.sysauth) return True - except ValueError: - _LOGGER.error("Couldn't fetch auth tokens!") + except (ValueError, KeyError) as _: + _LOGGER.error("Couldn't fetch auth tokens! Response was: %s", + response.text) return False @Throttle(MIN_TIME_BETWEEN_SCANS) @@ -250,6 +252,21 @@ class Tplink3DeviceScanner(TplinkDeviceScanner): return False + def _log_out(self): + with self.lock: + _LOGGER.info("Logging out of router admin interface...") + + url = ('http://{}/cgi-bin/luci/;stok={}/admin/system?' + 'form=logout').format(self.host, self.stok) + referer = 'http://{}/webpages/index.html'.format(self.host) + + requests.post(url, + params={'operation': 'write'}, + headers={'referer': referer}, + cookies={'sysauth': self.sysauth}) + self.stok = '' + self.sysauth = '' + class Tplink4DeviceScanner(TplinkDeviceScanner): """This class queries an Archer C7 router with TP-Link firmware 150427.""" diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 58fc56d2cba..261d8953940 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.discovery import async_load_platform, async_discover import homeassistant.util.dt as dt_util -REQUIREMENTS = ['netdisco==1.0.0rc3'] +REQUIREMENTS = ['netdisco==1.0.0'] DOMAIN = 'discovery' @@ -31,6 +31,7 @@ SERVICE_WEMO = 'belkin_wemo' SERVICE_HASS_IOS_APP = 'hass_ios' SERVICE_IKEA_TRADFRI = 'ikea_tradfri' SERVICE_HASSIO = 'hassio' +SERVICE_AXIS = 'axis' SERVICE_HANDLERS = { SERVICE_HASS_IOS_APP: ('ios', None), @@ -38,6 +39,7 @@ SERVICE_HANDLERS = { SERVICE_WEMO: ('wemo', None), SERVICE_IKEA_TRADFRI: ('tradfri', None), SERVICE_HASSIO: ('hassio', None), + SERVICE_AXIS: ('axis', None), 'philips_hue': ('light', 'hue'), 'google_cast': ('media_player', 'cast'), 'panasonic_viera': ('media_player', 'panasonic_viera'), diff --git a/homeassistant/components/dweet.py b/homeassistant/components/dweet.py index b4e8d68e960..d5f94bb2c7b 100644 --- a/homeassistant/components/dweet.py +++ b/homeassistant/components/dweet.py @@ -15,7 +15,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers import state as state_helper from homeassistant.util import Throttle -REQUIREMENTS = ['dweepy==0.2.0'] +REQUIREMENTS = ['dweepy==0.3.0'] _LOGGER = logging.getLogger(__name__) @@ -67,4 +67,4 @@ def send_data(name, msg): try: dweepy.dweet_for(name, msg) except dweepy.DweepyError: - _LOGGER.error("Error saving data '%s' to Dweet.io", msg) + _LOGGER.error("Error saving data to Dweet.io: %s", msg) diff --git a/homeassistant/components/eight_sleep.py b/homeassistant/components/eight_sleep.py index db718aec05e..22647532d9a 100644 --- a/homeassistant/components/eight_sleep.py +++ b/homeassistant/components/eight_sleep.py @@ -24,7 +24,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -REQUIREMENTS = ['pyeight==0.0.4'] +REQUIREMENTS = ['pyeight==0.0.5'] _LOGGER = logging.getLogger(__name__) @@ -145,6 +145,9 @@ def async_setup(hass, config): sensors.append('{}_{}'.format(obj.side, sensor)) binary_sensors.append('{}_presence'.format(obj.side)) sensors.append('room_temp') + else: + # No users, cannot continue + return False hass.async_add_job(discovery.async_load_platform( hass, 'sensor', DOMAIN, { diff --git a/homeassistant/components/fan/wink.py b/homeassistant/components/fan/wink.py index e8f5d6fd17a..13f755bcdf3 100644 --- a/homeassistant/components/fan/wink.py +++ b/homeassistant/components/fan/wink.py @@ -4,6 +4,7 @@ Support for Wink fans. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/fan.wink/ """ +import asyncio import logging from homeassistant.components.fan import (FanEntity, SPEED_HIGH, @@ -12,6 +13,8 @@ from homeassistant.components.fan import (FanEntity, SPEED_HIGH, from homeassistant.helpers.entity import ToggleEntity from homeassistant.components.wink import WinkDevice, DOMAIN +DEPENDENCIES = ['wink'] + _LOGGER = logging.getLogger(__name__) SPEED_LOWEST = 'lowest' @@ -34,6 +37,11 @@ class WinkFanDevice(WinkDevice, FanEntity): """Initialize the fan.""" super().__init__(wink, hass) + @asyncio.coroutine + def async_added_to_hass(self): + """Callback when entity is added to hass.""" + self.hass.data[DOMAIN]['entities']['fan'].append(self) + def set_direction(self: ToggleEntity, direction: str) -> None: """Set the direction of the fan.""" self.wink.set_fan_direction(direction) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 0d649344862..f4af26cc376 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,22 +1,23 @@ """DO NOT MODIFY. Auto-generated by script/fingerprint_frontend.""" FINGERPRINTS = { - "compatibility.js": "83d9c77748dafa9db49ae77d7f3d8fb0", - "core.js": "5d08475f03adb5969bd31855d5ca0cfd", - "frontend.html": "5999c8fac69c503b846672cae75a12b0", + "compatibility.js": "8e4c44b5f4288cc48ec1ba94a9bec812", + "core.js": "d4a7cb8c80c62b536764e0e81385f6aa", + "frontend.html": "fbb9d6bdd3d661db26cad9475a5e22f1", "mdi.html": "f407a5a57addbe93817ee1b244d33fbe", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", + "panels/ha-panel-automation.html": "21cba0a4fee9d2b45dda47f7a1dd82d8", "panels/ha-panel-config.html": "59d9eb28758b497a4d9b2428f978b9b1", "panels/ha-panel-dev-event.html": "2db9c218065ef0f61d8d08db8093cad2", "panels/ha-panel-dev-info.html": "61610e015a411cfc84edd2c4d489e71d", "panels/ha-panel-dev-service.html": "415552027cb083badeff5f16080410ed", "panels/ha-panel-dev-state.html": "d70314913b8923d750932367b1099750", "panels/ha-panel-dev-template.html": "567fbf86735e1b891e40c2f4060fec9b", - "panels/ha-panel-hassio.html": "23d175b6744c20e2fdf475b6efdaa1d3", + "panels/ha-panel-hassio.html": "9474ba65077371622f21ed9a30cf5229", "panels/ha-panel-history.html": "89062c48c76206cad1cec14ddbb1cbb1", "panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab", "panels/ha-panel-logbook.html": "6dd6a16f52117318b202e60f98400163", "panels/ha-panel-map.html": "31c592c239636f91e07c7ac232a5ebc4", - "panels/ha-panel-zwave.html": "84fb45638d2a69bac343246a687f647c", + "panels/ha-panel-zwave.html": "19336d2c50c91dd6a122acc0606ff10d", "websocket_test.html": "575de64b431fe11c3785bf96d7813450" } diff --git a/homeassistant/components/frontend/www_static/compatibility.js b/homeassistant/components/frontend/www_static/compatibility.js index c152c50ddfa..927b37e68ce 100644 --- a/homeassistant/components/frontend/www_static/compatibility.js +++ b/homeassistant/components/frontend/www_static/compatibility.js @@ -1 +1 @@ -!(function(){"use strict";function e(e,r){var t=arguments;if(void 0===e||null===e)throw new TypeError("Cannot convert first argument to object");for(var n=Object(e),o=1;o \ No newline at end of file +}()); \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/frontend.html.gz b/homeassistant/components/frontend/www_static/frontend.html.gz index a9879e06152..edb01a40d53 100644 Binary files a/homeassistant/components/frontend/www_static/frontend.html.gz and b/homeassistant/components/frontend/www_static/frontend.html.gz differ diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 9e7dc4a921f..6858555c86f 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 9e7dc4a921f86e60cc1f14afe254e5310b63e854 +Subproject commit 6858555c86f18eb0ab176008e9aa2c3842fec7ce diff --git a/homeassistant/components/frontend/www_static/images/logo_axis.png b/homeassistant/components/frontend/www_static/images/logo_axis.png new file mode 100644 index 00000000000..5eeb9b7b2a7 Binary files /dev/null and b/homeassistant/components/frontend/www_static/images/logo_axis.png differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-automation.html b/homeassistant/components/frontend/www_static/panels/ha-panel-automation.html new file mode 100644 index 00000000000..453d631c1da --- /dev/null +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-automation.html @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-automation.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-automation.html.gz new file mode 100644 index 00000000000..f3137a76bca Binary files /dev/null and b/homeassistant/components/frontend/www_static/panels/ha-panel-automation.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html b/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html index 80d1686acf0..906a7294b1f 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html @@ -1,17 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html.gz index 5ed1205a999..4e2422b0c61 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-hassio.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-zwave.html b/homeassistant/components/frontend/www_static/panels/ha-panel-zwave.html index 6af056b0db4..70e5cc64177 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-zwave.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-zwave.html @@ -1 +1,647 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-zwave.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-zwave.html.gz index fec3a4f832b..86b8d15fd5d 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-zwave.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-zwave.html.gz differ diff --git a/homeassistant/components/frontend/www_static/service_worker.js b/homeassistant/components/frontend/www_static/service_worker.js index 445c8d0b9df..f299739d127 100644 --- a/homeassistant/components/frontend/www_static/service_worker.js +++ b/homeassistant/components/frontend/www_static/service_worker.js @@ -1 +1 @@ -"use strict";function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}function notificationEventCallback(e,t){firePushCallback({action:t.action,data:t.notification.data,tag:t.notification.tag,type:e},t.notification.data.jwt)}function firePushCallback(e,t){delete e.data.jwt,0===Object.keys(e.data).length&&e.data.constructor===Object&&delete e.data,fetch("/api/notify.html5/callback",{method:"POST",headers:new Headers({"Content-Type":"application/json",Authorization:"Bearer "+t}),body:JSON.stringify(e)})}var precacheConfig=[["/","c255cdae4bfd691ee9fa43e38e8dc462"],["/frontend/panels/dev-event-2db9c218065ef0f61d8d08db8093cad2.html","b5b751e49b1bba55f633ae0d7a92677d"],["/frontend/panels/dev-info-61610e015a411cfc84edd2c4d489e71d.html","6568377ee31cbd78fedc003b317f7faf"],["/frontend/panels/dev-service-415552027cb083badeff5f16080410ed.html","a4b1ec9bfa5bc3529af7783ae56cb55c"],["/frontend/panels/dev-state-d70314913b8923d750932367b1099750.html","c61b5b1461959aac106400e122993e9e"],["/frontend/panels/dev-template-567fbf86735e1b891e40c2f4060fec9b.html","d2853ecf45de1dbadf49fe99a7424ef3"],["/frontend/panels/map-31c592c239636f91e07c7ac232a5ebc4.html","182580419ce2c935ae6ec65502b6db96"],["/static/compatibility-83d9c77748dafa9db49ae77d7f3d8fb0.js","5f05c83be2b028d577962f9625904806"],["/static/core-5d08475f03adb5969bd31855d5ca0cfd.js","1cd99ba798bfcff9768c9d2bb2f58a7c"],["/static/frontend-5999c8fac69c503b846672cae75a12b0.html","d6ce8eb348fbea599933b2a72beb1337"],["/static/mdi-f407a5a57addbe93817ee1b244d33fbe.html","5459090f217c77747b08d06e0bf73388"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/icons/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/icons/favicon.ico","04235bda7843ec2fceb1cbe2bc696cf4"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","32b5a9b7ada86304bec6b43d3f2194f0"]],cacheName="sw-precache-v3--"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var n=new URL(e);return"/"===n.pathname.slice(-1)&&(n.pathname+=t),n.toString()},cleanResponse=function(e){return e.redirected?("body"in e?Promise.resolve(e.body):e.blob()).then(function(t){return new Response(t,{headers:e.headers,status:e.status,statusText:e.statusText})}):Promise.resolve(e)},createCacheKey=function(e,t,n,a){var c=new URL(e);return a&&c.pathname.match(a)||(c.search+=(c.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(n)),c.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var n=new URL(t).pathname;return e.some(function(e){return n.match(e)})},stripIgnoredUrlParameters=function(e,t){var n=new URL(e);return n.search=n.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(e){return t.every(function(t){return!t.test(e[0])})}).map(function(e){return e.join("=")}).join("&"),n.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],n=e[1],a=new URL(t,self.location),c=createCacheKey(a,hashParamName,n,!1);return[a.toString(),c]}));self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(e){return setOfCachedUrls(e).then(function(t){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(n){if(!t.has(n)){var a=new Request(n,{credentials:"same-origin"});return fetch(a).then(function(t){if(!t.ok)throw new Error("Request for "+n+" returned a response with status "+t.status);return cleanResponse(t).then(function(t){return e.put(n,t)})})}}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var t=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(e){return e.keys().then(function(n){return Promise.all(n.map(function(n){if(!t.has(n.url))return e.delete(n)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t,n=stripIgnoredUrlParameters(e.request.url,ignoreUrlParametersMatching);t=urlsToCacheKeys.has(n);t||(n=addDirectoryIndex(n,"index.html"),t=urlsToCacheKeys.has(n));!t&&"navigate"===e.request.mode&&isPathWhitelisted(["^((?!(static|api|local|service_worker.js|manifest.json)).)*$"],e.request.url)&&(n=new URL("/",self.location).toString(),t=urlsToCacheKeys.has(n)),t&&e.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(n)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetch(e.request)}))}}),self.addEventListener("push",function(e){var t;e.data&&(t=e.data.json(),e.waitUntil(self.registration.showNotification(t.title,t).then(function(e){firePushCallback({type:"received",tag:t.tag,data:t.data},t.data.jwt)})))}),self.addEventListener("notificationclick",function(e){var t;notificationEventCallback("clicked",e),e.notification.close(),e.notification.data&&e.notification.data.url&&(t=e.notification.data.url)&&e.waitUntil(clients.matchAll({type:"window"}).then(function(e){var n,a;for(n=0;n= self.confidence} new_plates = set(plates) - set(self.plates) - # send events + # Send events for i_plate in new_plates: self.hass.async_add_job( self.hass.bus.async_fire, EVENT_FOUND_PLATE, { @@ -138,7 +136,7 @@ class ImageProcessingAlprEntity(ImageProcessingEntity): } ) - # update entity store + # Update entity store self.plates = plates self.vehicles = vehicles @@ -192,7 +190,7 @@ class OpenAlprLocalEntity(ImageProcessingAlprEntity): stderr=asyncio.subprocess.DEVNULL ) - # send image + # Send image stdout, _ = yield from alpr.communicate(input=image) stdout = io.StringIO(str(stdout, 'utf-8')) @@ -204,12 +202,12 @@ class OpenAlprLocalEntity(ImageProcessingAlprEntity): new_plates = RE_ALPR_PLATE.search(line) new_result = RE_ALPR_RESULT.search(line) - # found new vehicle + # Found new vehicle if new_plates: vehicles += 1 continue - # found plate result + # Found plate result if new_result: try: result.update( diff --git a/homeassistant/components/image_processing/opencv.py b/homeassistant/components/image_processing/opencv.py index e48c14aeea5..e6cdd0fcef9 100644 --- a/homeassistant/components/image_processing/opencv.py +++ b/homeassistant/components/image_processing/opencv.py @@ -1,7 +1,7 @@ """ Component that performs OpenCV classification on images. -For more details about this component, please refer to the documentation at +For more details about this platform, please refer to the documentation at https://home-assistant.io/components/image_processing.opencv/ """ from datetime import timedelta @@ -9,22 +9,15 @@ import logging from homeassistant.core import split_entity_id from homeassistant.components.image_processing import ( - ImageProcessingEntity, - PLATFORM_SCHEMA, -) + ImageProcessingEntity, PLATFORM_SCHEMA) from homeassistant.components.opencv import ( - ATTR_MATCHES, - CLASSIFIER_GROUP_CONFIG, - CONF_CLASSIFIER, - CONF_ENTITY_ID, - CONF_NAME, - process_image, -) - -DEPENDENCIES = ['opencv'] + ATTR_MATCHES, CLASSIFIER_GROUP_CONFIG, CONF_CLASSIFIER, CONF_ENTITY_ID, + CONF_NAME, process_image) _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['opencv'] + DEFAULT_TIMEOUT = 10 SCAN_INTERVAL = timedelta(seconds=2) @@ -33,18 +26,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(CLASSIFIER_GROUP_CONFIG) def _create_processor_from_config(hass, camera_entity, config): - """Create an OpenCV processor from configurtaion.""" + """Create an OpenCV processor from configuration.""" classifier_config = config[CONF_CLASSIFIER] name = '{} {}'.format( - config[CONF_NAME], - split_entity_id(camera_entity)[1].replace('_', ' ')) + config[CONF_NAME], split_entity_id(camera_entity)[1].replace('_', ' ')) processor = OpenCVImageProcessor( - hass, - camera_entity, - name, - classifier_config, - ) + hass, camera_entity, name, classifier_config) return processor @@ -57,10 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices = [] for camera_entity in discovery_info[CONF_ENTITY_ID]: devices.append( - _create_processor_from_config( - hass, - camera_entity, - discovery_info)) + _create_processor_from_config(hass, camera_entity, discovery_info)) add_devices(devices) @@ -115,6 +100,5 @@ class OpenCVImageProcessor(ImageProcessingEntity): def process_image(self, image): """Process the image.""" self._last_image = image - self._matches = process_image(image, - self._classifier_configs, - False) + self._matches = process_image( + image, self._classifier_configs, False) diff --git a/homeassistant/components/image_processing/seven_segments.py b/homeassistant/components/image_processing/seven_segments.py new file mode 100644 index 00000000000..9b9c327f822 --- /dev/null +++ b/homeassistant/components/image_processing/seven_segments.py @@ -0,0 +1,114 @@ +""" +Local optical character recognition processing of seven segements displays. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/image_processing.seven_segments/ +""" +import asyncio +import logging +import io +import os + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.core import split_entity_id +from homeassistant.components.image_processing import ( + PLATFORM_SCHEMA, ImageProcessingEntity, CONF_SOURCE, CONF_ENTITY_ID, + CONF_NAME) + +_LOGGER = logging.getLogger(__name__) + +CONF_DIGITS = 'digits' +CONF_HEIGHT = 'height' +CONF_SSOCR_BIN = 'ssocr_bin' +CONF_THRESHOLD = 'threshold' +CONF_WIDTH = 'width' +CONF_X_POS = 'x_position' +CONF_Y_POS = 'y_position' + +DEFAULT_BINARY = 'ssocr' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_DIGITS, default=-1): cv.positive_int, + vol.Optional(CONF_HEIGHT, default=0): cv.positive_int, + vol.Optional(CONF_SSOCR_BIN, default=DEFAULT_BINARY): cv.string, + vol.Optional(CONF_THRESHOLD, default=0): cv.positive_int, + vol.Optional(CONF_WIDTH, default=0): cv.positive_int, + vol.Optional(CONF_X_POS, default=0): cv.string, + vol.Optional(CONF_Y_POS, default=0): cv.positive_int, +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the Seven segments OCR platform.""" + entities = [] + for camera in config[CONF_SOURCE]: + entities.append(ImageProcessingSsocr( + hass, camera[CONF_ENTITY_ID], config, camera.get(CONF_NAME) + )) + + async_add_devices(entities) + + +class ImageProcessingSsocr(ImageProcessingEntity): + """Representation of the seven segments OCR image processing entity.""" + + def __init__(self, hass, camera_entity, config, name): + """Initialize seven segments processing.""" + self.hass = hass + self._camera_entity = camera_entity + if name: + self._name = name + else: + self._name = "SevenSegement OCR {0}".format( + split_entity_id(camera_entity)[1]) + self._state = None + self.filepath = os.path.join(self.hass.config.config_dir, 'ocr.png') + self._command = [ + config[CONF_SSOCR_BIN], 'erosion', 'make_mono', 'crop', + str(config[CONF_X_POS]), str(config[CONF_Y_POS]), + str(config[CONF_WIDTH]), str(config[CONF_HEIGHT]), '-t', + str(config[CONF_THRESHOLD]), '-d', str(config[CONF_DIGITS]), + self.filepath + ] + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return 'ocr' + + @property + def camera_entity(self): + """Return camera entity id from process pictures.""" + return self._camera_entity + + @property + def name(self): + """Return the name of the image processor.""" + return self._name + + @property + def state(self): + """Return the state of the entity.""" + return self._state + + def process_image(self, image): + """Process the image.""" + from PIL import Image + import subprocess + + stream = io.BytesIO(image) + img = Image.open(stream) + img.save(self.filepath, 'png') + + ocr = subprocess.Popen( + self._command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out = ocr.communicate() + if out[0] != b'': + self._state = out[0].strip().decode('utf-8') + else: + self._state = None + _LOGGER.warning( + "Unable to detect value: %s", out[1].strip().decode('utf-8')) diff --git a/homeassistant/components/kira.py b/homeassistant/components/kira.py new file mode 100644 index 00000000000..98d1228d541 --- /dev/null +++ b/homeassistant/components/kira.py @@ -0,0 +1,142 @@ +"""KIRA interface to receive UDP packets from an IR-IP bridge.""" +# pylint: disable=import-error +import logging +import os +import yaml + +import voluptuous as vol +from voluptuous.error import Error as VoluptuousError + +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv + +from homeassistant.const import ( + CONF_DEVICE, + CONF_HOST, + CONF_NAME, + CONF_PORT, + CONF_SENSORS, + CONF_TYPE, + EVENT_HOMEASSISTANT_STOP, + STATE_UNKNOWN) + +REQUIREMENTS = ["pykira==0.1.1"] + +DOMAIN = 'kira' + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_HOST = "0.0.0.0" +DEFAULT_PORT = 65432 + +CONF_CODE = "code" +CONF_REPEAT = "repeat" +CONF_REMOTES = "remotes" +CONF_SENSOR = "sensor" +CONF_REMOTE = "remote" + +CODES_YAML = '{}_codes.yaml'.format(DOMAIN) + +CODE_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_CODE): cv.string, + vol.Optional(CONF_TYPE): cv.string, + vol.Optional(CONF_DEVICE): cv.string, + vol.Optional(CONF_REPEAT): cv.positive_int, +}) + +SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_NAME, default=DOMAIN): + vol.Exclusive(cv.string, "sensors"), + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, +}) + +REMOTE_SCHEMA = vol.Schema({ + vol.Optional(CONF_NAME, default=DOMAIN): + vol.Exclusive(cv.string, "remotes"), + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_SENSORS): [SENSOR_SCHEMA], + vol.Optional(CONF_REMOTES): [REMOTE_SCHEMA]}) +}, extra=vol.ALLOW_EXTRA) + + +def load_codes(path): + """Load Kira codes from specified file.""" + codes = [] + if os.path.exists(path): + with open(path) as code_file: + data = yaml.load(code_file) or [] + for code in data: + try: + codes.append(CODE_SCHEMA(code)) + except VoluptuousError as exception: + # keep going + _LOGGER.warning('Kira Code Invalid Data: %s', exception) + else: + with open(path, 'w') as code_file: + code_file.write('') + return codes + + +def setup(hass, config): + """Setup KIRA capability.""" + import pykira + + sensors = config.get(DOMAIN, {}).get(CONF_SENSORS, []) + remotes = config.get(DOMAIN, {}).get(CONF_REMOTES, []) + # If no sensors or remotes were specified, add a sensor + if not(sensors or remotes): + sensors.append({}) + + codes = load_codes(hass.config.path(CODES_YAML)) + + hass.data[DOMAIN] = { + CONF_SENSOR: {}, + CONF_REMOTE: {}, + } + + def load_module(platform, idx, module_conf): + """Set up Kira module and load platform.""" + # note: module_name is not the HA device name. it's just a unique name + # to ensure the component and platform can share information + module_name = ("%s_%d" % (DOMAIN, idx)) if idx else DOMAIN + device_name = module_conf.get(CONF_NAME, DOMAIN) + port = module_conf.get(CONF_PORT, DEFAULT_PORT) + host = module_conf.get(CONF_HOST, DEFAULT_HOST) + + if platform == CONF_SENSOR: + module = pykira.KiraReceiver(host, port) + module.start() + else: + module = pykira.KiraModule(host, port) + + hass.data[DOMAIN][platform][module_name] = module + for code in codes: + code_tuple = (code.get(CONF_NAME), + code.get(CONF_DEVICE, STATE_UNKNOWN)) + module.registerCode(code_tuple, code.get(CONF_CODE)) + + discovery.load_platform(hass, platform, DOMAIN, + {'name': module_name, 'device': device_name}, + config) + + for idx, module_conf in enumerate(sensors): + load_module(CONF_SENSOR, idx, module_conf) + + for idx, module_conf in enumerate(remotes): + load_module(CONF_REMOTE, idx, module_conf) + + def _stop_kira(_event): + for receiver in hass.data[DOMAIN][CONF_SENSOR].values(): + receiver.stop() + _LOGGER.info("Terminated receivers") + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _stop_kira) + + return True diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 853fe4d9fb1..92db75d1e50 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -50,13 +50,15 @@ ATTR_TRANSITION = "transition" ATTR_RGB_COLOR = "rgb_color" ATTR_XY_COLOR = "xy_color" ATTR_COLOR_TEMP = "color_temp" +ATTR_KELVIN = "kelvin" ATTR_MIN_MIREDS = "min_mireds" ATTR_MAX_MIREDS = "max_mireds" ATTR_COLOR_NAME = "color_name" ATTR_WHITE_VALUE = "white_value" -# int with value 0 .. 255 representing brightness of the light. +# Brightness of the light, 0..255 or percentage ATTR_BRIGHTNESS = "brightness" +ATTR_BRIGHTNESS_PCT = "brightness_pct" # String representing a profile (built-in ones or external defined). ATTR_PROFILE = "profile" @@ -92,18 +94,21 @@ PROP_TO_ATTR = { # Service call validation schemas VALID_TRANSITION = vol.All(vol.Coerce(float), vol.Clamp(min=0, max=6553)) VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)) +VALID_BRIGHTNESS_PCT = vol.All(vol.Coerce(float), vol.Range(min=0, max=100)) LIGHT_TURN_ON_SCHEMA = vol.Schema({ ATTR_ENTITY_ID: cv.entity_ids, ATTR_PROFILE: cv.string, ATTR_TRANSITION: VALID_TRANSITION, ATTR_BRIGHTNESS: VALID_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, ATTR_COLOR_NAME: cv.string, ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple)), ATTR_XY_COLOR: vol.All(vol.ExactSequence((cv.small_float, cv.small_float)), vol.Coerce(tuple)), ATTR_COLOR_TEMP: vol.All(vol.Coerce(int), vol.Range(min=1)), + ATTR_KELVIN: vol.All(vol.Coerce(int), vol.Range(min=0)), ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)), ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]), ATTR_EFFECT: cv.string, @@ -142,20 +147,21 @@ def is_on(hass, entity_id=None): def turn_on(hass, entity_id=None, transition=None, brightness=None, - rgb_color=None, xy_color=None, color_temp=None, white_value=None, + brightness_pct=None, rgb_color=None, xy_color=None, + color_temp=None, kelvin=None, white_value=None, profile=None, flash=None, effect=None, color_name=None): """Turn all or specified light on.""" hass.add_job( - async_turn_on, hass, entity_id, transition, brightness, - rgb_color, xy_color, color_temp, white_value, + async_turn_on, hass, entity_id, transition, brightness, brightness_pct, + rgb_color, xy_color, color_temp, kelvin, white_value, profile, flash, effect, color_name) @callback def async_turn_on(hass, entity_id=None, transition=None, brightness=None, - rgb_color=None, xy_color=None, color_temp=None, - white_value=None, profile=None, flash=None, effect=None, - color_name=None): + brightness_pct=None, rgb_color=None, xy_color=None, + color_temp=None, kelvin=None, white_value=None, + profile=None, flash=None, effect=None, color_name=None): """Turn all or specified light on.""" data = { key: value for key, value in [ @@ -163,9 +169,11 @@ def async_turn_on(hass, entity_id=None, transition=None, brightness=None, (ATTR_PROFILE, profile), (ATTR_TRANSITION, transition), (ATTR_BRIGHTNESS, brightness), + (ATTR_BRIGHTNESS_PCT, brightness_pct), (ATTR_RGB_COLOR, rgb_color), (ATTR_XY_COLOR, xy_color), (ATTR_COLOR_TEMP, color_temp), + (ATTR_KELVIN, kelvin), (ATTR_WHITE_VALUE, white_value), (ATTR_FLASH, flash), (ATTR_EFFECT, effect), @@ -207,6 +215,27 @@ def toggle(hass, entity_id=None, transition=None): hass.services.call(DOMAIN, SERVICE_TOGGLE, data) +def preprocess_turn_on_alternatives(params): + """Processing extra data for turn light on request.""" + profile = Profiles.get(params.pop(ATTR_PROFILE, None)) + if profile is not None: + params.setdefault(ATTR_XY_COLOR, profile[:2]) + params.setdefault(ATTR_BRIGHTNESS, profile[2]) + + color_name = params.pop(ATTR_COLOR_NAME, None) + if color_name is not None: + params[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name) + + kelvin = params.pop(ATTR_KELVIN, None) + if kelvin is not None: + mired = color_util.color_temperature_kelvin_to_mired(kelvin) + params[ATTR_COLOR_TEMP] = int(mired) + + brightness_pct = params.pop(ATTR_BRIGHTNESS_PCT, None) + if brightness_pct is not None: + params[ATTR_BRIGHTNESS] = int(255 * brightness_pct/100) + + @asyncio.coroutine def async_setup(hass, config): """Expose light control via statemachine and services.""" @@ -215,10 +244,8 @@ def async_setup(hass, config): yield from component.async_setup(config) # load profiles from files - profiles = yield from hass.loop.run_in_executor( - None, _load_profile_data, hass) - - if profiles is None: + profiles_valid = yield from Profiles.load_profiles(hass) + if not profiles_valid: return False @asyncio.coroutine @@ -231,17 +258,7 @@ def async_setup(hass, config): target_lights = component.async_extract_from_service(service) params.pop(ATTR_ENTITY_ID, None) - # Processing extra data for turn light on request. - profile = profiles.get(params.pop(ATTR_PROFILE, None)) - - if profile: - params.setdefault(ATTR_XY_COLOR, profile[:2]) - params.setdefault(ATTR_BRIGHTNESS, profile[2]) - - color_name = params.pop(ATTR_COLOR_NAME, None) - - if color_name is not None: - params[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name) + preprocess_turn_on_alternatives(params) for light in target_lights: if service.service == SERVICE_TURN_ON: @@ -287,31 +304,51 @@ def async_setup(hass, config): return True -def _load_profile_data(hass): - """Load built-in profiles and custom profiles.""" - profile_paths = [os.path.join(os.path.dirname(__file__), - LIGHT_PROFILES_FILE), - hass.config.path(LIGHT_PROFILES_FILE)] - profiles = {} +class Profiles: + """Representation of available color profiles.""" - for profile_path in profile_paths: - if not os.path.isfile(profile_path): - continue - with open(profile_path) as inp: - reader = csv.reader(inp) + _all = None - # Skip the header - next(reader, None) + @classmethod + @asyncio.coroutine + def load_profiles(cls, hass): + """Load and cache profiles.""" + def load_profile_data(hass): + """Load built-in profiles and custom profiles.""" + profile_paths = [os.path.join(os.path.dirname(__file__), + LIGHT_PROFILES_FILE), + hass.config.path(LIGHT_PROFILES_FILE)] + profiles = {} - try: - for rec in reader: - profile, color_x, color_y, brightness = PROFILE_SCHEMA(rec) - profiles[profile] = (color_x, color_y, brightness) - except vol.MultipleInvalid as ex: - _LOGGER.error("Error parsing light profile from %s: %s", - profile_path, ex) - return None - return profiles + for profile_path in profile_paths: + if not os.path.isfile(profile_path): + continue + with open(profile_path) as inp: + reader = csv.reader(inp) + + # Skip the header + next(reader, None) + + try: + for rec in reader: + profile, color_x, color_y, brightness = \ + PROFILE_SCHEMA(rec) + profiles[profile] = (color_x, color_y, brightness) + except vol.MultipleInvalid as ex: + _LOGGER.error( + "Error parsing light profile from %s: %s", + profile_path, ex) + return None + return profiles + + cls._all = yield from hass.loop.run_in_executor( + None, load_profile_data, hass) + return cls._all is not None + + @classmethod + def get(cls, name): + """Return a named profile.""" + return cls._all.get(name) class Light(ToggleEntity): diff --git a/homeassistant/components/light/blinksticklight.py b/homeassistant/components/light/blinksticklight.py index bbbde10ecc8..d6a6ef465a8 100644 --- a/homeassistant/components/light/blinksticklight.py +++ b/homeassistant/components/light/blinksticklight.py @@ -31,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """Add device specified by serial number.""" + """Set up Blinkstick device specified by serial number.""" from blinkstick import blinkstick name = config.get(CONF_NAME) diff --git a/homeassistant/components/light/blinkt.py b/homeassistant/components/light/blinkt.py index ffd3c102c7f..e2bef31089f 100644 --- a/homeassistant/components/light/blinkt.py +++ b/homeassistant/components/light/blinkt.py @@ -29,6 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Blinkt Light platform.""" + # pylint: disable=import-error import blinkt # ensure that the lights are off when exiting diff --git a/homeassistant/components/light/enocean.py b/homeassistant/components/light/enocean.py index ad4bc381b80..beb9094b1cb 100644 --- a/homeassistant/components/light/enocean.py +++ b/homeassistant/components/light/enocean.py @@ -20,14 +20,13 @@ _LOGGER = logging.getLogger(__name__) CONF_SENDER_ID = 'sender_id' DEFAULT_NAME = 'EnOcean Light' - DEPENDENCIES = ['enocean'] SUPPORT_ENOCEAN = SUPPORT_BRIGHTNESS PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_ID, default=[]): vol.All(cv.ensure_list, - [vol.Coerce(int)]), + vol.Optional(CONF_ID, default=[]): + vol.All(cv.ensure_list, [vol.Coerce(int)]), vol.Required(CONF_SENDER_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index 4b9bed10201..499ec8f74ab 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -35,26 +35,26 @@ SUPPORT_FLUX_LED_RGBW = (SUPPORT_WHITE_VALUE | SUPPORT_EFFECT | MODE_RGB = 'rgb' MODE_RGBW = 'rgbw' -# List of Supported Effects which aren't already declared in LIGHT -EFFECT_RED_FADE = "red_fade" -EFFECT_GREEN_FADE = "green_fade" -EFFECT_BLUE_FADE = "blue_fade" -EFFECT_YELLOW_FADE = "yellow_fade" -EFFECT_CYAN_FADE = "cyan_fade" -EFFECT_PURPLE_FADE = "purple_fade" -EFFECT_WHITE_FADE = "white_fade" -EFFECT_RED_GREEN_CROSS_FADE = "rg_cross_fade" -EFFECT_RED_BLUE_CROSS_FADE = "rb_cross_fade" -EFFECT_GREEN_BLUE_CROSS_FADE = "gb_cross_fade" -EFFECT_COLORSTROBE = "colorstrobe" -EFFECT_RED_STROBE = "red_strobe" -EFFECT_GREEN_STROBE = "green_strobe" -EFFECT_BLUE_STOBE = "blue_strobe" -EFFECT_YELLOW_STROBE = "yellow_strobe" -EFFECT_CYAN_STROBE = "cyan_strobe" -EFFECT_PURPLE_STROBE = "purple_strobe" -EFFECT_WHITE_STROBE = "white_strobe" -EFFECT_COLORJUMP = "colorjump" +# List of supported effects which aren't already declared in LIGHT +EFFECT_RED_FADE = 'red_fade' +EFFECT_GREEN_FADE = 'green_fade' +EFFECT_BLUE_FADE = 'blue_fade' +EFFECT_YELLOW_FADE = 'yellow_fade' +EFFECT_CYAN_FADE = 'cyan_fade' +EFFECT_PURPLE_FADE = 'purple_fade' +EFFECT_WHITE_FADE = 'white_fade' +EFFECT_RED_GREEN_CROSS_FADE = 'rg_cross_fade' +EFFECT_RED_BLUE_CROSS_FADE = 'rb_cross_fade' +EFFECT_GREEN_BLUE_CROSS_FADE = 'gb_cross_fade' +EFFECT_COLORSTROBE = 'colorstrobe' +EFFECT_RED_STROBE = 'red_strobe' +EFFECT_GREEN_STROBE = 'green_strobe' +EFFECT_BLUE_STOBE = 'blue_strobe' +EFFECT_YELLOW_STROBE = 'yellow_strobe' +EFFECT_CYAN_STROBE = 'cyan_strobe' +EFFECT_PURPLE_STROBE = 'purple_strobe' +EFFECT_WHITE_STROBE = 'white_strobe' +EFFECT_COLORJUMP = 'colorjump' FLUX_EFFECT_LIST = [ EFFECT_COLORLOOP, @@ -121,7 +121,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ipaddr = device['ipaddr'] if ipaddr in light_ips: continue - device['name'] = device['id'] + " " + ipaddr + device['name'] = '{} {}'.format(device['id'], ipaddr) device[ATTR_MODE] = 'rgbw' device[CONF_PROTOCOL] = None light = FluxLight(device) @@ -167,7 +167,7 @@ class FluxLight(Light): @property def unique_id(self): """Return the ID of this light.""" - return "{}.{}".format(self.__class__, self._ipaddr) + return '{}.{}'.format(self.__class__, self._ipaddr) @property def name(self): diff --git a/homeassistant/components/light/insteon_local.py b/homeassistant/components/light/insteon_local.py index f7beb0c31ac..e5b99ca1cb2 100644 --- a/homeassistant/components/light/insteon_local.py +++ b/homeassistant/components/light/insteon_local.py @@ -84,7 +84,7 @@ def setup_light(device_id, name, insteonhub, hass, add_devices_callback): request_id = _CONFIGURING.pop(device_id) configurator = get_component('configurator') configurator.request_done(request_id) - _LOGGER.info("Device configuration done!") + _LOGGER.debug("Device configuration done") conf_lights = config_from_file(hass.config.path(INSTEON_LOCAL_LIGHTS_CONF)) if device_id not in conf_lights: @@ -107,7 +107,7 @@ def config_from_file(filename, config=None): with open(filename, 'w') as fdesc: fdesc.write(json.dumps(config)) except IOError as error: - _LOGGER.error('Saving config file failed: %s', error) + _LOGGER.error("Saving config file failed: %s", error) return False return True else: diff --git a/homeassistant/components/light/insteon_plm.py b/homeassistant/components/light/insteon_plm.py index 2cd22cf6d4d..3b3dd43f496 100644 --- a/homeassistant/components/light/insteon_plm.py +++ b/homeassistant/components/light/insteon_plm.py @@ -1,5 +1,5 @@ """ -Support for INSTEON lights via PowerLinc Modem. +Support for Insteon lights via PowerLinc Modem. For more details about this component, please refer to the documentation at https://home-assistant.io/components/insteon_plm/ @@ -12,16 +12,16 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) from homeassistant.loader import get_component +_LOGGER = logging.getLogger(__name__) + DEPENDENCIES = ['insteon_plm'] MAX_BRIGHTNESS = 255 -_LOGGER = logging.getLogger(__name__) - @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the INSTEON PLM device class for the hass platform.""" + """Set up the Insteon PLM device.""" plm = hass.data['insteon_plm'] device_list = [] @@ -30,7 +30,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): address = device.get('address_hex') dimmable = bool('dimmable' in device.get('capabilities')) - _LOGGER.info('Registered %s with light platform.', name) + _LOGGER.info("Registered %s with light platform", name) device_list.append( InsteonPLMDimmerDevice(hass, plm, address, name, dimmable) @@ -72,14 +72,14 @@ class InsteonPLMDimmerDevice(Light): def brightness(self): """Return the brightness of this light between 0..255.""" onlevel = self._plm.get_device_attr(self._address, 'onlevel') - _LOGGER.debug('on level for %s is %s', self._address, onlevel) + _LOGGER.debug("on level for %s is %s", self._address, onlevel) return int(onlevel) @property def is_on(self): """Return the boolean response if the node is on.""" onlevel = self._plm.get_device_attr(self._address, 'onlevel') - _LOGGER.debug('on level for %s is %s', self._address, onlevel) + _LOGGER.debug("on level for %s is %s", self._address, onlevel) return bool(onlevel) @property @@ -101,7 +101,7 @@ class InsteonPLMDimmerDevice(Light): @callback def async_light_update(self, message): """Receive notification from transport that new data exists.""" - _LOGGER.info('Received update calback from PLM for %s', self._address) + _LOGGER.info("Received update calback from PLM for %s", self._address) self._hass.async_add_job(self.async_update_ha_state()) @asyncio.coroutine diff --git a/homeassistant/components/light/isy994.py b/homeassistant/components/light/isy994.py index 1cde50de820..10feecca518 100644 --- a/homeassistant/components/light/isy994.py +++ b/homeassistant/components/light/isy994.py @@ -24,13 +24,12 @@ def setup_platform(hass, config: ConfigType, add_devices: Callable[[list], None], discovery_info=None): """Set up the ISY994 light platform.""" if isy.ISY is None or not isy.ISY.connected: - _LOGGER.error('A connection has not been made to the ISY controller.') + _LOGGER.error("A connection has not been made to the ISY controller") return False devices = [] - for node in isy.filter_nodes(isy.NODES, units=UOM, - states=STATES): + for node in isy.filter_nodes(isy.NODES, units=UOM, states=STATES): if node.dimmable or '51' in node.uom: devices.append(ISYLightDevice(node)) @@ -57,12 +56,12 @@ class ISYLightDevice(isy.ISYDevice, Light): def turn_off(self, **kwargs) -> None: """Send the turn off command to the ISY994 light device.""" if not self._node.off(): - _LOGGER.debug('Unable to turn on light.') + _LOGGER.debug("Unable to turn on light") def turn_on(self, brightness=None, **kwargs) -> None: """Send the turn on command to the ISY994 light device.""" if not self._node.on(val=brightness): - _LOGGER.debug('Unable to turn on light.') + _LOGGER.debug("Unable to turn on light") @property def supported_features(self): diff --git a/homeassistant/components/light/lifx/__init__.py b/homeassistant/components/light/lifx/__init__.py index f13934011e9..c264fec35c5 100644 --- a/homeassistant/components/light/lifx/__init__.py +++ b/homeassistant/components/light/lifx/__init__.py @@ -9,6 +9,7 @@ import logging import asyncio import sys import math +from os import path from functools import partial from datetime import timedelta import async_timeout @@ -16,15 +17,19 @@ import async_timeout import voluptuous as vol from homeassistant.components.light import ( - Light, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, ATTR_COLOR_NAME, ATTR_RGB_COLOR, + Light, DOMAIN, PLATFORM_SCHEMA, LIGHT_TURN_ON_SCHEMA, + ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_XY_COLOR, ATTR_COLOR_TEMP, ATTR_TRANSITION, ATTR_EFFECT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, - SUPPORT_XY_COLOR, SUPPORT_TRANSITION, SUPPORT_EFFECT) + SUPPORT_XY_COLOR, SUPPORT_TRANSITION, SUPPORT_EFFECT, + preprocess_turn_on_alternatives) +from homeassistant.config import load_yaml_config_file from homeassistant.util.color import ( color_temperature_mired_to_kelvin, color_temperature_kelvin_to_mired) from homeassistant import util from homeassistant.core import callback from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.service import extract_entity_ids import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util @@ -41,7 +46,10 @@ BULB_LATENCY = 500 CONF_SERVER = 'server' +SERVICE_LIFX_SET_STATE = 'lifx_set_state' + ATTR_HSBK = 'hsbk' +ATTR_POWER = 'power' BYTE_MAX = 255 SHORT_MAX = 65535 @@ -53,6 +61,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_SERVER, default='0.0.0.0'): cv.string, }) +LIFX_SET_STATE_SCHEMA = LIGHT_TURN_ON_SCHEMA.extend({ + ATTR_POWER: cv.boolean, +}) + @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): @@ -87,6 +99,41 @@ class LIFXManager(object): self.hass = hass self.async_add_devices = async_add_devices + @asyncio.coroutine + def async_service_handle(service): + """Apply a service.""" + tasks = [] + for light in self.service_to_entities(service): + if service.service == SERVICE_LIFX_SET_STATE: + task = light.async_set_state(**service.data) + tasks.append(hass.async_add_job(task)) + if tasks: + yield from asyncio.wait(tasks, loop=hass.loop) + + descriptions = self.get_descriptions() + + hass.services.async_register( + DOMAIN, SERVICE_LIFX_SET_STATE, async_service_handle, + descriptions.get(SERVICE_LIFX_SET_STATE), + schema=LIFX_SET_STATE_SCHEMA) + + @staticmethod + def get_descriptions(): + """Load and return descriptions for our own service calls.""" + return load_yaml_config_file( + path.join(path.dirname(__file__), 'services.yaml')) + + def service_to_entities(self, service): + """Return the known devices that a service call mentions.""" + entity_ids = extract_entity_ids(self.hass, service) + if entity_ids: + entities = [entity for entity in self.entities.values() + if entity.entity_id in entity_ids] + else: + entities = list(self.entities.values()) + + return entities + @callback def register(self, device): """Handle for newly detected bulb.""" @@ -298,6 +345,18 @@ class LIFXLight(Light): @asyncio.coroutine def async_turn_on(self, **kwargs): """Turn the device on.""" + kwargs[ATTR_POWER] = True + yield from self.async_set_state(**kwargs) + + @asyncio.coroutine + def async_turn_off(self, **kwargs): + """Turn the device off.""" + kwargs[ATTR_POWER] = False + yield from self.async_set_state(**kwargs) + + @asyncio.coroutine + def async_set_state(self, **kwargs): + """Set a color on the light and turn it on/off.""" yield from self.stop_effect() if ATTR_EFFECT in kwargs: @@ -309,39 +368,41 @@ class LIFXLight(Light): else: fade = 0 + # These are both False if ATTR_POWER is not set + power_on = kwargs.get(ATTR_POWER, False) + power_off = not kwargs.get(ATTR_POWER, True) + hsbk, changed_color = self.find_hsbk(**kwargs) _LOGGER.debug("turn_on: %s (%d) %d %d %d %d %d", self.who, self._power, fade, *hsbk) if self._power == 0: + if power_off: + self.device.set_power(False, None, 0) if changed_color: self.device.set_color(hsbk, None, 0) - self.device.set_power(True, None, fade) + if power_on: + self.device.set_power(True, None, fade) else: - self.device.set_power(True, None, 0) # racing for power status + if power_on: + self.device.set_power(True, None, 0) if changed_color: self.device.set_color(hsbk, None, fade) + if power_off: + self.device.set_power(False, None, fade) - self.update_later(0) - if fade < BULB_LATENCY: - self.set_power(1) - self.set_color(*hsbk) - - @asyncio.coroutine - def async_turn_off(self, **kwargs): - """Turn the device off.""" - yield from self.stop_effect() - - if ATTR_TRANSITION in kwargs: - fade = int(kwargs[ATTR_TRANSITION] * 1000) + if power_on: + self.update_later(0) else: - fade = 0 + self.update_later(fade) - self.device.set_power(False, None, fade) - - self.update_later(fade) - if fade < BULB_LATENCY: - self.set_power(0) + if fade <= BULB_LATENCY: + if power_on: + self.set_power(1) + if power_off: + self.set_power(0) + if changed_color: + self.set_color(*hsbk) @asyncio.coroutine def async_update(self): @@ -374,9 +435,7 @@ class LIFXLight(Light): if hsbk is not None: return [hsbk, True] - color_name = kwargs.pop(ATTR_COLOR_NAME, None) - if color_name is not None: - kwargs[ATTR_RGB_COLOR] = color_util.color_name_to_rgb(color_name) + preprocess_turn_on_alternatives(kwargs) if ATTR_RGB_COLOR in kwargs: hue, saturation, brightness = \ diff --git a/homeassistant/components/light/lifx/effects.py b/homeassistant/components/light/lifx/effects.py index a15360df33e..0a8c9cbf80f 100644 --- a/homeassistant/components/light/lifx/effects.py +++ b/homeassistant/components/light/lifx/effects.py @@ -2,16 +2,14 @@ import logging import asyncio import random -from os import path import voluptuous as vol from homeassistant.components.light import ( - DOMAIN, ATTR_BRIGHTNESS, ATTR_COLOR_NAME, ATTR_RGB_COLOR, ATTR_EFFECT, - ATTR_TRANSITION) -from homeassistant.config import load_yaml_config_file + DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME, + ATTR_RGB_COLOR, ATTR_EFFECT, ATTR_TRANSITION, + VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT) from homeassistant.const import (ATTR_ENTITY_ID) -from homeassistant.helpers.service import extract_entity_ids import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -39,7 +37,8 @@ LIFX_EFFECT_SCHEMA = vol.Schema({ }) LIFX_EFFECT_BREATHE_SCHEMA = LIFX_EFFECT_SCHEMA.extend({ - ATTR_BRIGHTNESS: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)), + ATTR_BRIGHTNESS: VALID_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, ATTR_COLOR_NAME: cv.string, ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple)), @@ -52,7 +51,8 @@ LIFX_EFFECT_BREATHE_SCHEMA = LIFX_EFFECT_SCHEMA.extend({ LIFX_EFFECT_PULSE_SCHEMA = LIFX_EFFECT_BREATHE_SCHEMA LIFX_EFFECT_COLORLOOP_SCHEMA = LIFX_EFFECT_SCHEMA.extend({ - ATTR_BRIGHTNESS: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)), + ATTR_BRIGHTNESS: VALID_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, vol.Optional(ATTR_PERIOD, default=60): vol.All(vol.Coerce(float), vol.Clamp(min=0.05)), vol.Optional(ATTR_CHANGE, default=20): @@ -73,19 +73,12 @@ def setup(hass, lifx_manager): @asyncio.coroutine def async_service_handle(service): """Apply a service.""" - entity_ids = extract_entity_ids(hass, service) - if entity_ids: - devices = [entity for entity in lifx_manager.entities.values() - if entity.entity_id in entity_ids] - else: - devices = list(lifx_manager.entities.values()) - - if devices: - yield from start_effect(hass, devices, + entities = lifx_manager.service_to_entities(service) + if entities: + yield from start_effect(hass, entities, service.service, **service.data) - descriptions = load_yaml_config_file( - path.join(path.dirname(__file__), 'services.yaml')) + descriptions = lifx_manager.get_descriptions() hass.services.async_register( DOMAIN, SERVICE_EFFECT_BREATHE, async_service_handle, @@ -292,7 +285,7 @@ class LIFXEffectColorloop(LIFXEffect): direction = 1 if random.randint(0, 1) else -1 # Random start - hue = random.randint(0, 359) + hue = random.uniform(0, 360) % 360 while self.lights: hue = (hue + direction*change) % 360 @@ -312,7 +305,7 @@ class LIFXEffectColorloop(LIFXEffect): brightness = light.effect_data.color[2] hsbk = [ - int(65535/359*lhue), + int(65535/360*lhue), int(random.uniform(0.8, 1.0)*65535), brightness, NEUTRAL_WHITE, diff --git a/homeassistant/components/light/lifx/services.yaml b/homeassistant/components/light/lifx/services.yaml index d939e1432bc..a907a665753 100644 --- a/homeassistant/components/light/lifx/services.yaml +++ b/homeassistant/components/light/lifx/services.yaml @@ -1,3 +1,23 @@ +lifx_set_state: + description: Set a color/brightness and possibliy turn the light on/off + + fields: + entity_id: + description: Name(s) of entities to set a state on + example: 'light.garage' + + '...': + description: All turn_on parameters can be used to specify a color + + transition: + description: Duration in seconds it takes to get to the final state + example: 10 + + power: + description: Turn the light on (True) or off (False). Leave out to keep the power as it is. + example: True + + lifx_effect_breathe: description: Run a breathe effect by fading to a color and back. diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index 1a1fe1cffd7..4e44351b7dd 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -4,7 +4,6 @@ Support for LimitlessLED bulbs. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.limitlessled/ """ - import logging import voluptuous as vol @@ -17,7 +16,7 @@ from homeassistant.components.light import ( SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['limitlessled==1.0.5'] +REQUIREMENTS = ['limitlessled==1.0.8'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/light/osramlightify.py b/homeassistant/components/light/osramlightify.py index 143dc52cbee..bc0cfacda1a 100644 --- a/homeassistant/components/light/osramlightify.py +++ b/homeassistant/components/light/osramlightify.py @@ -195,7 +195,7 @@ class Luminary(Light): self._brightness = kwargs[ATTR_BRIGHTNESS] _LOGGER.debug("turn_on requested brightness for light: %s is: %s ", self._name, self._brightness) - self._brightness = self._luminary.set_luminance( + self._luminary.set_luminance( int(self._brightness / 2.55), transition) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 495ef9c8b39..6ccd45dda66 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -26,7 +26,11 @@ turn_on: color_temp: description: Color temperature for the light in mireds - example: '250' + example: 250 + + kelvin: + description: Color temperature for the light in Kelvin + example: 4000 white_value: description: Number between 0..255 indicating level of white @@ -36,6 +40,10 @@ turn_on: description: Number between 0..255 indicating brightness example: 120 + brightness_pct: + description: Number between 0..100 indicating percentage of full brightness + example: 47 + profile: description: Name of a light profile to use example: relax diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py index 016771a15ca..28c42c5699c 100644 --- a/homeassistant/components/light/tradfri.py +++ b/homeassistant/components/light/tradfri.py @@ -1,4 +1,9 @@ -"""Support for the IKEA Tradfri platform.""" +""" +Support for the IKEA Tradfri platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.tradfri/ +""" import logging from homeassistant.components.light import ( @@ -6,7 +11,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, Light) from homeassistant.components.light import \ PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA -from homeassistant.components.tradfri import KEY_GATEWAY +from homeassistant.components.tradfri import KEY_GATEWAY, KEY_TRADFRI_GROUPS from homeassistant.util import color as color_util _LOGGER = logging.getLogger(__name__) @@ -30,8 +35,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): lights = [dev for dev in devices if dev.has_light_control] add_devices(Tradfri(light) for light in lights) - groups = gateway.get_groups() - add_devices(TradfriGroup(group) for group in groups) + allow_tradfri_groups = hass.data[KEY_TRADFRI_GROUPS][gateway_id] + if allow_tradfri_groups: + groups = gateway.get_groups() + add_devices(TradfriGroup(group) for group in groups) class TradfriGroup(Light): diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index 82b7c9f4f8c..1f046a2ec27 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -4,6 +4,7 @@ Support for Wink lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.wink/ """ +import asyncio import colorsys from homeassistant.components.light import ( @@ -38,6 +39,11 @@ class WinkLight(WinkDevice, Light): """Initialize the Wink device.""" super().__init__(wink, hass) + @asyncio.coroutine + def async_added_to_hass(self): + """Callback when entity is added to hass.""" + self.hass.data[DOMAIN]['entities']['light'].append(self) + @property def is_on(self): """Return true if light is on.""" diff --git a/homeassistant/components/lock/services.yaml b/homeassistant/components/lock/services.yaml index 6b12d49302d..df370ca0168 100644 --- a/homeassistant/components/lock/services.yaml +++ b/homeassistant/components/lock/services.yaml @@ -55,3 +55,59 @@ unlock: code: description: An optional code to unlock the lock with example: 1234 + +wink_set_lock_vacation_mode: + description: Set vacation mode for all or specified locks. Disables all user codes. + + fields: + entity_id: + description: Name of lock to unlock + example: 'lock.front_door' + enabled: + description: enable or disable. true or false. + example: true + +wink_set_lock_alarm_mode: + description: Set alarm mode for all or specified locks. + + fields: + entity_id: + description: Name of lock to unlock + example: 'lock.front_door' + mode: + description: One of tamper, activity, or forced_entry + example: tamper + +wink_set_lock_alarm_sensitivity: + description: Set alarm sensitivity for all or specified locks. + + fields: + entity_id: + description: Name of lock to unlock + example: 'lock.front_door' + sensitivity: + description: One of low, medium_low, medium, medium_high, high + example: medium + +wink_set_lock_alarm_state: + description: Set alarm state. + + fields: + entity_id: + description: Name of lock to unlock + example: 'lock.front_door' + enabled: + description: enable or disable. true or false. + example: true + +wink_set_lock_beeper_state: + description: Set beeper state. + + fields: + entity_id: + description: Name of lock to unlock + example: 'lock.front_door' + enabled: + description: enable or disable. true or false. + example: true + diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/lock/wink.py index 9ac5c579ab5..6fbf9edf954 100644 --- a/homeassistant/components/lock/wink.py +++ b/homeassistant/components/lock/wink.py @@ -4,11 +4,55 @@ Support for Wink locks. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/lock.wink/ """ +import asyncio +import logging +from os import path + +import voluptuous as vol + from homeassistant.components.lock import LockDevice from homeassistant.components.wink import WinkDevice, DOMAIN +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.config import load_yaml_config_file DEPENDENCIES = ['wink'] +_LOGGER = logging.getLogger(__name__) + +SERVICE_SET_VACATION_MODE = 'wink_set_lock_vacation_mode' +SERVICE_SET_ALARM_MODE = 'wink_set_lock_alarm_mode' +SERVICE_SET_ALARM_SENSITIVITY = 'wink_set_lock_alarm_sensitivity' +SERVICE_SET_ALARM_STATE = 'wink_set_lock_alarm_state' +SERVICE_SET_BEEPER_STATE = 'wink_set_lock_beeper_state' + +ATTR_ENABLED = 'enabled' +ATTR_SENSITIVITY = 'sensitivity' +ATTR_MODE = 'mode' + +ALARM_SENSITIVITY_MAP = {"low": 0.2, "medium_low": 0.4, + "medium": 0.6, "medium_high": 0.8, + "high": 1.0} + +ALARM_MODES_MAP = {"tamper": "tamper", + "activity": "alert", + "forced_entry": "forced_entry"} + +SET_ENABLED_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_ENABLED): cv.string, +}) + +SET_SENSITIVITY_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_SENSITIVITY): vol.In(ALARM_SENSITIVITY_MAP) +}) + +SET_ALARM_MODES_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_MODE): vol.In(ALARM_MODES_MAP) +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Wink platform.""" @@ -19,6 +63,58 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if _id not in hass.data[DOMAIN]['unique_ids']: add_devices([WinkLockDevice(lock, hass)]) + def service_handle(service): + """Handler for services.""" + entity_ids = service.data.get('entity_id') + all_locks = hass.data[DOMAIN]['entities']['lock'] + locks_to_set = [] + if entity_ids is None: + locks_to_set = all_locks + else: + for lock in all_locks: + if lock.entity_id in entity_ids: + locks_to_set.append(lock) + + for lock in locks_to_set: + if service.service == SERVICE_SET_VACATION_MODE: + lock.set_vacation_mode(service.data.get(ATTR_ENABLED)) + elif service.service == SERVICE_SET_ALARM_STATE: + lock.set_alarm_state(service.data.get(ATTR_ENABLED)) + elif service.service == SERVICE_SET_BEEPER_STATE: + lock.set_beeper_state(service.data.get(ATTR_ENABLED)) + elif service.service == SERVICE_SET_ALARM_MODE: + lock.set_alarm_mode(service.data.get(ATTR_MODE)) + elif service.service == SERVICE_SET_ALARM_SENSITIVITY: + lock.set_alarm_sensitivity(service.data.get(ATTR_SENSITIVITY)) + + descriptions = load_yaml_config_file( + path.join(path.dirname(__file__), 'services.yaml')) + + hass.services.register(DOMAIN, SERVICE_SET_VACATION_MODE, + service_handle, + descriptions.get(SERVICE_SET_VACATION_MODE), + schema=SET_ENABLED_SCHEMA) + + hass.services.register(DOMAIN, SERVICE_SET_ALARM_STATE, + service_handle, + descriptions.get(SERVICE_SET_ALARM_STATE), + schema=SET_ENABLED_SCHEMA) + + hass.services.register(DOMAIN, SERVICE_SET_BEEPER_STATE, + service_handle, + descriptions.get(SERVICE_SET_BEEPER_STATE), + schema=SET_ENABLED_SCHEMA) + + hass.services.register(DOMAIN, SERVICE_SET_ALARM_MODE, + service_handle, + descriptions.get(SERVICE_SET_ALARM_MODE), + schema=SET_ALARM_MODES_SCHEMA) + + hass.services.register(DOMAIN, SERVICE_SET_ALARM_SENSITIVITY, + service_handle, + descriptions.get(SERVICE_SET_ALARM_SENSITIVITY), + schema=SET_SENSITIVITY_SCHEMA) + class WinkLockDevice(WinkDevice, LockDevice): """Representation of a Wink lock.""" @@ -27,6 +123,11 @@ class WinkLockDevice(WinkDevice, LockDevice): """Initialize the lock.""" super().__init__(wink, hass) + @asyncio.coroutine + def async_added_to_hass(self): + """Callback when entity is added to hass.""" + self.hass.data[DOMAIN]['entities']['lock'].append(self) + @property def is_locked(self): """Return true if device is locked.""" @@ -39,3 +140,60 @@ class WinkLockDevice(WinkDevice, LockDevice): def unlock(self, **kwargs): """Unlock the device.""" self.wink.set_state(False) + + def set_alarm_state(self, enabled): + """Set lock's alarm state.""" + self.wink.set_alarm_state(enabled) + + def set_vacation_mode(self, enabled): + """Set lock's vacation mode.""" + self.wink.set_vacation_mode(enabled) + + def set_beeper_state(self, enabled): + """Set lock's beeper mode.""" + self.wink.set_beeper_mode(enabled) + + def set_alarm_sensitivity(self, sensitivity): + """ + Set lock's alarm sensitivity. + + Valid sensitivities: + 0.2, 0.4, 0.6, 0.8, 1.0 + """ + self.wink.set_alarm_sensitivity(sensitivity) + + def set_alarm_mode(self, mode): + """ + Set lock's alarm mode. + + Valid modes: + alert - Beep when lock is locked or unlocked + tamper - 15 sec alarm when lock is disturbed when locked + forced_entry - 3 min alarm when significant force applied + to door when locked. + """ + self.wink.set_alarm_mode(mode) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + super_attrs = super().device_state_attributes + sensitivity = dict_value_to_key(ALARM_SENSITIVITY_MAP, + self.wink.alarm_sensitivity()) + super_attrs['alarm sensitivity'] = sensitivity + super_attrs['vacation mode'] = self.wink.vacation_mode_enabled() + super_attrs['beeper mode'] = self.wink.beeper_enabled() + super_attrs['auto lock'] = self.wink.auto_lock_enabled() + alarm_mode = dict_value_to_key(ALARM_MODES_MAP, + self.wink.alarm_mode()) + super_attrs['alarm mode'] = alarm_mode + super_attrs['alarm enabled'] = self.wink.alarm_enabled() + return super_attrs + + +def dict_value_to_key(dict_map, comp_value): + """Return the key that has the provided value.""" + for key, value in dict_map.items(): + if value == comp_value: + return key + return STATE_UNKNOWN diff --git a/homeassistant/components/lock/zwave.py b/homeassistant/components/lock/zwave.py index a46406e8361..7654d354a31 100644 --- a/homeassistant/components/lock/zwave.py +++ b/homeassistant/components/lock/zwave.py @@ -141,9 +141,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class_id=zwave.const.COMMAND_CLASS_USER_CODE).values(): if value.index != code_slot: continue - if len(str(usercode)) > 4: + if len(str(usercode)) < 4: _LOGGER.error("Invalid code provided: (%s) " - "usercode must %s or less digits", + "usercode must be atleast 4 and at most" + " %s digits", usercode, len(value.data)) break value.data = str(usercode) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 98a0973a807..053648e3428 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -19,7 +19,8 @@ from homeassistant.components.frontend import register_built_in_panel from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, - STATE_NOT_HOME, STATE_OFF, STATE_ON, ATTR_HIDDEN, HTTP_BAD_REQUEST) + STATE_NOT_HOME, STATE_OFF, STATE_ON, ATTR_HIDDEN, HTTP_BAD_REQUEST, + EVENT_LOGBOOK_ENTRY) from homeassistant.core import State, split_entity_id, DOMAIN as HA_DOMAIN DOMAIN = 'logbook' @@ -47,10 +48,10 @@ CONFIG_SCHEMA = vol.Schema({ }), }, extra=vol.ALLOW_EXTRA) -EVENT_LOGBOOK_ENTRY = 'logbook_entry' - GROUP_BY_MINUTES = 15 +CONTINUOUS_DOMAINS = ['proximity', 'sensor'] + ATTR_NAME = 'name' ATTR_MESSAGE = 'message' ATTR_DOMAIN = 'domain' @@ -191,7 +192,8 @@ def humanify(events): if entity_id is None: continue - if entity_id.startswith('sensor.'): + if entity_id.startswith(tuple('{}.'.format( + domain) for domain in CONTINUOUS_DOMAINS)): last_sensor_event[entity_id] = event elif event.event_type == EVENT_HOMEASSISTANT_STOP: @@ -223,12 +225,12 @@ def humanify(events): domain = to_state.domain # Skip all but the last sensor state - if domain == 'sensor' and \ + if domain in CONTINUOUS_DOMAINS and \ event != last_sensor_event[to_state.entity_id]: continue # Don't show continuous sensor value changes in the logbook - if domain == 'sensor' and \ + if domain in CONTINUOUS_DOMAINS and \ to_state.attributes.get('unit_of_measurement'): continue diff --git a/homeassistant/components/lutron_caseta.py b/homeassistant/components/lutron_caseta.py index 375e2930a4f..e2ad733fb36 100644 --- a/homeassistant/components/lutron_caseta.py +++ b/homeassistant/components/lutron_caseta.py @@ -14,9 +14,7 @@ from homeassistant.const import CONF_HOST from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['https://github.com/gurumitts/' - 'pylutron-caseta/archive/v0.2.6.zip#' - 'pylutron-caseta==v0.2.6'] +REQUIREMENTS = ['pylutron-caseta==0.2.6'] _LOGGER = logging.getLogger(__name__) @@ -46,7 +44,7 @@ def setup(hass, base_config): _LOGGER.info("Connected to Lutron smartbridge at %s", config[CONF_HOST]) - for component in ('light', 'switch'): + for component in ('light', 'switch', 'cover'): discovery.load_platform(hass, component, DOMAIN, {}, config) return True diff --git a/homeassistant/components/media_player/denonavr.py b/homeassistant/components/media_player/denonavr.py index 494a3f69e6d..0a4ec012382 100644 --- a/homeassistant/components/media_player/denonavr.py +++ b/homeassistant/components/media_player/denonavr.py @@ -19,7 +19,7 @@ from homeassistant.const import ( CONF_NAME, STATE_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['denonavr==0.4.0'] +REQUIREMENTS = ['denonavr==0.4.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 10d13002625..9861887df89 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -9,20 +9,22 @@ from functools import wraps import logging import urllib import re +import os import aiohttp import voluptuous as vol +from homeassistant.config import load_yaml_config_file from homeassistant.components.media_player import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP, - SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, MediaPlayerDevice, - PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, - MEDIA_TYPE_PLAYLIST, MEDIA_PLAYER_SCHEMA, DOMAIN) + SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, SUPPORT_SHUFFLE_SET, + MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, + MEDIA_TYPE_VIDEO, MEDIA_TYPE_PLAYLIST, MEDIA_PLAYER_SCHEMA, DOMAIN) from homeassistant.const import ( STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME, CONF_PORT, CONF_SSL, CONF_PROXY_SSL, CONF_USERNAME, CONF_PASSWORD, - EVENT_HOMEASSISTANT_STOP) + CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP) from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -32,6 +34,8 @@ REQUIREMENTS = ['jsonrpc-async==0.6', 'jsonrpc-websocket==0.5'] _LOGGER = logging.getLogger(__name__) +EVENT_KODI_CALL_METHOD_RESULT = 'kodi_call_method_result' + CONF_TCP_PORT = 'tcp_port' CONF_TURN_OFF_ACTION = 'turn_off_action' CONF_ENABLE_WEBSOCKET = 'enable_websocket' @@ -61,8 +65,9 @@ MEDIA_TYPES = { } SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ - SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \ - SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY | SUPPORT_VOLUME_STEP + SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \ + SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_SHUFFLE_SET | \ + SUPPORT_PLAY | SUPPORT_VOLUME_STEP PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -71,6 +76,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_PORT): cv.port, vol.Optional(CONF_PROXY_SSL, default=DEFAULT_PROXY_SSL): cv.boolean, vol.Optional(CONF_TURN_OFF_ACTION, default=None): vol.In(TURN_OFF_ACTION), + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, vol.Inclusive(CONF_USERNAME, 'auth'): cv.string, vol.Inclusive(CONF_PASSWORD, 'auth'): cv.string, vol.Optional(CONF_ENABLE_WEBSOCKET, default=DEFAULT_ENABLE_WEBSOCKET): @@ -78,16 +84,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) SERVICE_ADD_MEDIA = 'kodi_add_to_playlist' -SERVICE_SET_SHUFFLE = 'kodi_set_shuffle' +SERVICE_CALL_METHOD = 'kodi_call_method' + +DATA_KODI = 'kodi' ATTR_MEDIA_TYPE = 'media_type' ATTR_MEDIA_NAME = 'media_name' ATTR_MEDIA_ARTIST_NAME = 'artist_name' ATTR_MEDIA_ID = 'media_id' - -MEDIA_PLAYER_SET_SHUFFLE_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ - vol.Required('shuffle_on'): cv.boolean, -}) +ATTR_METHOD = 'method' MEDIA_PLAYER_ADD_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ vol.Required(ATTR_MEDIA_TYPE): cv.string, @@ -95,20 +100,25 @@ MEDIA_PLAYER_ADD_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ vol.Optional(ATTR_MEDIA_NAME): cv.string, vol.Optional(ATTR_MEDIA_ARTIST_NAME): cv.string, }) +MEDIA_PLAYER_CALL_METHOD_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ + vol.Required(ATTR_METHOD): cv.string, +}, extra=vol.ALLOW_EXTRA) SERVICE_TO_METHOD = { SERVICE_ADD_MEDIA: { 'method': 'async_add_media_to_playlist', 'schema': MEDIA_PLAYER_ADD_MEDIA_SCHEMA}, - SERVICE_SET_SHUFFLE: { - 'method': 'async_set_shuffle', - 'schema': MEDIA_PLAYER_SET_SHUFFLE_SCHEMA}, + SERVICE_CALL_METHOD: { + 'method': 'async_call_method', + 'schema': MEDIA_PLAYER_CALL_METHOD_SCHEMA}, } @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the Kodi platform.""" + if DATA_KODI not in hass.data: + hass.data[DATA_KODI] = [] host = config.get(CONF_HOST) port = config.get(CONF_PORT) tcp_port = config.get(CONF_TCP_PORT) @@ -128,8 +138,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): host=host, port=port, tcp_port=tcp_port, encryption=encryption, username=config.get(CONF_USERNAME), password=config.get(CONF_PASSWORD), - turn_off_action=config.get(CONF_TURN_OFF_ACTION), websocket=websocket) + turn_off_action=config.get(CONF_TURN_OFF_ACTION), + timeout=config.get(CONF_TIMEOUT), websocket=websocket) + hass.data[DATA_KODI].append(entity) async_add_devices([entity], update_before_add=True) @asyncio.coroutine @@ -141,23 +153,37 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): params = {key: value for key, value in service.data.items() if key != 'entity_id'} - - yield from getattr(entity, method['method'])(**params) + entity_ids = service.data.get('entity_id') + if entity_ids: + target_players = [player for player in hass.data[DATA_KODI] + if player.entity_id in entity_ids] + else: + target_players = hass.data[DATA_KODI] update_tasks = [] - if entity.should_poll: - update_coro = entity.async_update_ha_state(True) - update_tasks.append(update_coro) + for player in target_players: + yield from getattr(player, method['method'])(**params) + + for player in target_players: + if player.should_poll: + update_coro = player.async_update_ha_state(True) + update_tasks.append(update_coro) if update_tasks: yield from asyncio.wait(update_tasks, loop=hass.loop) + if hass.services.has_service(DOMAIN, SERVICE_ADD_MEDIA): + return + + descriptions = yield from hass.loop.run_in_executor( + None, load_yaml_config_file, os.path.join( + os.path.dirname(__file__), 'services.yaml')) + for service in SERVICE_TO_METHOD: - schema = SERVICE_TO_METHOD[service].get( - 'schema', MEDIA_PLAYER_SCHEMA) + schema = SERVICE_TO_METHOD[service]['schema'] hass.services.async_register( DOMAIN, service, async_service_handler, - description=None, schema=schema) + description=descriptions.get(service), schema=schema) def cmd(func): @@ -185,7 +211,7 @@ class KodiDevice(MediaPlayerDevice): def __init__(self, hass, name, host, port, tcp_port, encryption=False, username=None, password=None, turn_off_action=None, - websocket=True): + timeout=DEFAULT_TIMEOUT, websocket=True): """Initialize the Kodi device.""" import jsonrpc_async import jsonrpc_websocket @@ -193,7 +219,7 @@ class KodiDevice(MediaPlayerDevice): self._name = name kwargs = { - 'timeout': DEFAULT_TIMEOUT, + 'timeout': timeout, 'session': async_get_clientsession(hass), } @@ -657,16 +683,40 @@ class KodiDevice(MediaPlayerDevice): {"item": {"file": str(media_id)}}) @asyncio.coroutine - def async_set_shuffle(self, shuffle_on): + def async_set_shuffle(self, shuffle): """Set shuffle mode, for the first player.""" if len(self._players) < 1: raise RuntimeError("Error: No active player.") yield from self.server.Player.SetShuffle( - {"playerid": self._players[0]['playerid'], "shuffle": shuffle_on}) + {"playerid": self._players[0]['playerid'], "shuffle": shuffle}) + + @asyncio.coroutine + def async_call_method(self, method, **kwargs): + """Run Kodi JSONRPC API method with params.""" + import jsonrpc_base + _LOGGER.debug('Run API method "%s", kwargs=%s', method, kwargs) + result_ok = False + try: + result = yield from getattr(self.server, method)(**kwargs) + result_ok = True + except jsonrpc_base.jsonrpc.ProtocolError as exc: + result = exc.args[2]['error'] + _LOGGER.error('Run API method %s.%s(%s) error: %s', + self.entity_id, method, kwargs, result) + + if isinstance(result, dict): + event_data = {'entity_id': self.entity_id, + 'result': result, + 'result_ok': result_ok, + 'input': {'method': method, 'params': kwargs}} + _LOGGER.debug('EVENT kodi_call_method_result: %s', event_data) + self.hass.bus.async_fire(EVENT_KODI_CALL_METHOD_RESULT, + event_data=event_data) + return result @asyncio.coroutine def async_add_media_to_playlist( - self, media_type, media_id=None, media_name='', artist_name=''): + self, media_type, media_id=None, media_name='ALL', artist_name=''): """Add a media to default playlist (i.e. playlistid=0). First the media type must be selected, then @@ -675,13 +725,14 @@ class KodiDevice(MediaPlayerDevice): All the albums of an artist can be added with media_name="ALL" """ + import jsonrpc_base + params = {"playlistid": 0} if media_type == "SONG": if media_id is None: media_id = yield from self.async_find_song( media_name, artist_name) - - yield from self.server.Playlist.Add( - {"playlistid": 0, "item": {"songid": int(media_id)}}) + if media_id: + params["item"] = {"songid": int(media_id)} elif media_type == "ALBUM": if media_id is None: @@ -691,12 +742,22 @@ class KodiDevice(MediaPlayerDevice): media_id = yield from self.async_find_album( media_name, artist_name) + if media_id: + params["item"] = {"albumid": int(media_id)} - yield from self.server.Playlist.Add( - {"playlistid": 0, "item": {"albumid": int(media_id)}}) else: raise RuntimeError("Unrecognized media type.") + if media_id is not None: + try: + yield from self.server.Playlist.Add(params) + except jsonrpc_base.jsonrpc.ProtocolError as exc: + result = exc.args[2]['error'] + _LOGGER.error('Run API method %s.Playlist.Add(%s) error: %s', + self.entity_id, media_type, result) + else: + _LOGGER.warning('No media detected for Playlist.Add') + @asyncio.coroutine def async_add_all_albums(self, artist_name): """Add all albums of an artist to default playlist (i.e. playlistid=0). @@ -734,9 +795,13 @@ class KodiDevice(MediaPlayerDevice): def async_find_artist(self, artist_name): """Find artist by name.""" artists = yield from self.async_get_artists() - out = self._find( - artist_name, [a['artist'] for a in artists['artists']]) - return artists['artists'][out[0][0]]['artistid'] + try: + out = self._find( + artist_name, [a['artist'] for a in artists['artists']]) + return artists['artists'][out[0][0]]['artistid'] + except KeyError: + _LOGGER.warning('No artists were found: %s', artist_name) + return None @asyncio.coroutine def async_get_songs(self, artist_id=None): @@ -769,8 +834,14 @@ class KodiDevice(MediaPlayerDevice): artist_id = yield from self.async_find_artist(artist_name) albums = yield from self.async_get_albums(artist_id) - out = self._find(album_name, [a['label'] for a in albums['albums']]) - return albums['albums'][out[0][0]]['albumid'] + try: + out = self._find( + album_name, [a['label'] for a in albums['albums']]) + return albums['albums'][out[0][0]]['albumid'] + except KeyError: + _LOGGER.warning('No albums were found with artist: %s, album: %s', + artist_name, album_name) + return None @staticmethod def _find(key_word, words): diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py index 18690cca871..19c46b811f7 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -14,9 +14,7 @@ from homeassistant.components.media_player import ( from homeassistant.const import (STATE_OFF, STATE_ON, CONF_HOST, CONF_NAME) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['https://github.com/miracle2k/onkyo-eiscp/archive/' - '066023aec04770518d494c32fb72eea0ec5c1b7c.zip#' - 'onkyo-eiscp==1.0'] +REQUIREMENTS = ['onkyo-eiscp==1.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index 97f7ba2e716..a81f9330ab8 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -17,9 +17,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv import homeassistant.loader as loader -REQUIREMENTS = [ - 'https://github.com/bah2830/python-roku/archive/3.1.3.zip' - '#roku==3.1.3'] +REQUIREMENTS = ['python-roku==3.1.3'] KNOWN_HOSTS = [] DEFAULT_PORT = 8060 diff --git a/homeassistant/components/media_player/russound_rnet.py b/homeassistant/components/media_player/russound_rnet.py index 6615f85db65..9ce3dcfc4f4 100644 --- a/homeassistant/components/media_player/russound_rnet.py +++ b/homeassistant/components/media_player/russound_rnet.py @@ -15,9 +15,7 @@ from homeassistant.const import ( CONF_HOST, CONF_PORT, STATE_OFF, STATE_ON, CONF_NAME) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = [ - 'https://github.com/laf/russound/archive/0.1.7.zip' - '#russound==0.1.7'] +REQUIREMENTS = ['russound==0.1.7'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index ae90e141289..00ce0987fd9 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -269,3 +269,34 @@ soundtouch_remove_zone_slave: slaves: description: Name of slaves entities to remove from the existing zone example: 'media_player.soundtouch_bedroom' + +kodi_add_to_playlist: + description: Add music to the default playlist (i.e. playlistid=0). + + fields: + entity_id: + description: Name(s) of the Kodi entities where to add the media. + example: 'media_player.living_room_kodi' + media_type: + description: Media type identifier. It must be one of SONG or ALBUM. + example: ALBUM + media_id: + description: Unique Id of the media entry to add (`songid` or albumid`). If not defined, `media_name` and `artist_name` are needed to search the Kodi music library. + example: 123456 + media_name: + description: Optional media name for filtering media. Can be 'ALL' when `media_type` is 'ALBUM' and `artist_name` is specified, to add all songs from one artist. + example: 'Highway to Hell' + artist_name: + description: Optional artist name for filtering media. + example: 'AC/DC' + +kodi_call_method: + description: 'Call a Kodi JSONRPC API method with optional parameters. Results of the Kodi API call will be redirected in a Home Assistant event: `kodi_call_method_result`.' + + fields: + entity_id: + description: Name(s) of the Kodi entities where to run the API method. + example: 'media_player.living_room_kodi' + method: + description: Name of the Kodi JSONRPC API method to be called. + example: 'VideoLibrary.GetRecentlyAddedEpisodes' diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index da75b89c19d..c209fde1679 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -51,6 +51,7 @@ SERVICE_SNAPSHOT = 'sonos_snapshot' SERVICE_RESTORE = 'sonos_restore' SERVICE_SET_TIMER = 'sonos_set_sleep_timer' SERVICE_CLEAR_TIMER = 'sonos_clear_sleep_timer' +SERVICE_UPDATE_ALARM = 'sonos_update_alarm' DATA_SONOS = 'sonos' @@ -62,6 +63,11 @@ CONF_INTERFACE_ADDR = 'interface_addr' # Service call validation schemas ATTR_SLEEP_TIME = 'sleep_time' +ATTR_ALARM_ID = 'alarm_id' +ATTR_VOLUME = 'volume' +ATTR_ENABLED = 'enabled' +ATTR_INCLUDE_LINKED_ZONES = 'include_linked_zones' +ATTR_TIME = 'time' ATTR_MASTER = 'master' ATTR_WITH_GROUP = 'with_group' @@ -90,6 +96,14 @@ SONOS_SET_TIMER_SCHEMA = SONOS_SCHEMA.extend({ vol.All(vol.Coerce(int), vol.Range(min=0, max=86399)) }) +SONOS_UPDATE_ALARM_SCHEMA = SONOS_SCHEMA.extend({ + vol.Required(ATTR_ALARM_ID): cv.positive_int, + vol.Optional(ATTR_TIME): cv.time, + vol.Optional(ATTR_VOLUME): cv.small_float, + vol.Optional(ATTR_ENABLED): cv.boolean, + vol.Optional(ATTR_INCLUDE_LINKED_ZONES): cv.boolean, +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Sonos platform.""" @@ -163,9 +177,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): elif service.service == SERVICE_RESTORE: device.restore(service.data[ATTR_WITH_GROUP]) elif service.service == SERVICE_SET_TIMER: - device.set_timer(service.data[ATTR_SLEEP_TIME]) + device.set_sleep_timer(service.data[ATTR_SLEEP_TIME]) elif service.service == SERVICE_CLEAR_TIMER: - device.clear_timer() + device.clear_sleep_timer() + elif service.service == SERVICE_UPDATE_ALARM: + device.update_alarm(**service.data) device.schedule_update_ha_state(True) @@ -193,6 +209,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): DOMAIN, SERVICE_CLEAR_TIMER, service_handle, descriptions.get(SERVICE_CLEAR_TIMER), schema=SONOS_SCHEMA) + hass.services.register( + DOMAIN, SERVICE_UPDATE_ALARM, service_handle, + descriptions.get(SERVICE_UPDATE_ALARM), + schema=SONOS_UPDATE_ALARM_SCHEMA) + def _parse_timespan(timespan): """Parse a time-span into number of seconds.""" @@ -1034,6 +1055,30 @@ class SonosDevice(MediaPlayerDevice): """Clear the timer on the player.""" self._player.set_sleep_timer(None) + @soco_error + @soco_coordinator + def update_alarm(self, **data): + """Set the alarm clock on the player.""" + from soco import alarms + a = None + for alarm in alarms.get_alarms(self.soco): + # pylint: disable=protected-access + if alarm._alarm_id == str(data[ATTR_ALARM_ID]): + a = alarm + if a is None: + _LOGGER.warning("did not find alarm with id %s", + data[ATTR_ALARM_ID]) + return + if ATTR_TIME in data: + a.start_time = data[ATTR_TIME] + if ATTR_VOLUME in data: + a.volume = int(data[ATTR_VOLUME] * 100) + if ATTR_ENABLED in data: + a.enabled = data[ATTR_ENABLED] + if ATTR_INCLUDE_LINKED_ZONES in data: + a.include_linked_zones = data[ATTR_INCLUDE_LINKED_ZONES] + a.save() + @property def device_state_attributes(self): """Return device specific state attributes.""" diff --git a/homeassistant/components/media_player/spotify.py b/homeassistant/components/media_player/spotify.py index a73a4a922ca..8ceb245eb03 100644 --- a/homeassistant/components/media_player/spotify.py +++ b/homeassistant/components/media_player/spotify.py @@ -176,6 +176,7 @@ class SpotifyMediaPlayer(MediaPlayerDevice): self._state = STATE_PAUSED if current.get('is_playing'): self._state = STATE_PLAYING + self._shuffle = current.get('shuffle_state') device = current.get('device') if device is None: self._state = STATE_IDLE @@ -184,8 +185,6 @@ class SpotifyMediaPlayer(MediaPlayerDevice): self._volume = device.get('volume_percent') / 100 if device.get('name'): self._current_device = device.get('name') - if device.get('shuffle_state'): - self._shuffle = device.get('shuffle_state') def set_volume_level(self, volume): """Set the volume level.""" @@ -213,7 +212,8 @@ class SpotifyMediaPlayer(MediaPlayerDevice): def select_source(self, source): """Select playback device.""" - self._player.transfer_playback(self._devices[source]) + self._player.transfer_playback(self._devices[source], + self._state == STATE_PLAYING) def play_media(self, media_type, media_id, **kwargs): """Play media.""" diff --git a/homeassistant/components/microsoft_face.py b/homeassistant/components/microsoft_face.py index a0ff2ed99e7..a2a52b68665 100644 --- a/homeassistant/components/microsoft_face.py +++ b/homeassistant/components/microsoft_face.py @@ -28,10 +28,12 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = 'microsoft_face' DEPENDENCIES = ['camera'] -FACE_API_URL = "https://westus.api.cognitive.microsoft.com/face/v1.0/{0}" +FACE_API_URL = "api.cognitive.microsoft.com/face/v1.0/{0}" DATA_MICROSOFT_FACE = 'microsoft_face' +CONF_AZURE_REGION = 'azure_region' + SERVICE_CREATE_GROUP = 'create_group' SERVICE_DELETE_GROUP = 'delete_group' SERVICE_TRAIN_GROUP = 'train_group' @@ -49,6 +51,7 @@ DEFAULT_TIMEOUT = 10 CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_AZURE_REGION, default="westus"): cv.string, vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, }), }, extra=vol.ALLOW_EXTRA) @@ -115,6 +118,7 @@ def async_setup(hass, config): entities = {} face = MicrosoftFace( hass, + config[DOMAIN].get(CONF_AZURE_REGION), config[DOMAIN].get(CONF_API_KEY), config[DOMAIN].get(CONF_TIMEOUT), entities @@ -304,12 +308,13 @@ class MicrosoftFaceGroupEntity(Entity): class MicrosoftFace(object): """Microsoft Face api for HomeAssistant.""" - def __init__(self, hass, api_key, timeout, entities): + def __init__(self, hass, server_loc, api_key, timeout, entities): """Initialize Microsoft Face api.""" self.hass = hass self.websession = async_get_clientsession(hass) self.timeout = timeout self._api_key = api_key + self._server_url = "https://{0}.{1}".format(server_loc, FACE_API_URL) self._store = {} self._entities = entities @@ -346,7 +351,7 @@ class MicrosoftFace(object): params=None): """Make a api call.""" headers = {"Ocp-Apim-Subscription-Key": self._api_key} - url = FACE_API_URL.format(function) + url = self._server_url.format(function) payload = None if binary: diff --git a/homeassistant/components/notify/html5.py b/homeassistant/components/notify/html5.py index 3aa5db7faba..52d2deedcd4 100644 --- a/homeassistant/components/notify/html5.py +++ b/homeassistant/components/notify/html5.py @@ -25,8 +25,7 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.components.frontend import add_manifest_json_key from homeassistant.helpers import config_validation as cv -# pyelliptic is dependency of pywebpush and 1.5.8 contains a breaking change -REQUIREMENTS = ['pywebpush==0.6.1', 'PyJWT==1.4.2', 'pyelliptic==1.5.7'] +REQUIREMENTS = ['pywebpush==1.0.0', 'PyJWT==1.4.2'] DEPENDENCIES = ['frontend'] diff --git a/homeassistant/components/notify/sendgrid.py b/homeassistant/components/notify/sendgrid.py index bb4a5078013..d3ba79a059f 100644 --- a/homeassistant/components/notify/sendgrid.py +++ b/homeassistant/components/notify/sendgrid.py @@ -13,7 +13,7 @@ from homeassistant.components.notify import ( from homeassistant.const import (CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['sendgrid==4.0.0'] +REQUIREMENTS = ['sendgrid==4.1.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/notify/smtp.py b/homeassistant/components/notify/smtp.py index 1a2e1bf5b4e..d66d024e111 100644 --- a/homeassistant/components/notify/smtp.py +++ b/homeassistant/components/notify/smtp.py @@ -9,9 +9,9 @@ import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.mime.image import MIMEImage -import email.utils from email.mime.application import MIMEApplication - +import email.utils +import os import voluptuous as vol from homeassistant.components.notify import ( @@ -26,10 +26,12 @@ import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) ATTR_IMAGES = 'images' # optional embedded image file attachments +ATTR_HTML = 'html' CONF_STARTTLS = 'starttls' CONF_DEBUG = 'debug' CONF_SERVER = 'server' +CONF_SENDER_NAME = 'sender_name' DEFAULT_HOST = 'localhost' DEFAULT_PORT = 25 @@ -47,6 +49,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_STARTTLS, default=DEFAULT_STARTTLS): cv.boolean, vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SENDER_NAME): cv.string, vol.Optional(CONF_DEBUG, default=DEFAULT_DEBUG): cv.boolean, }) @@ -62,6 +65,7 @@ def get_service(hass, config, discovery_info=None): config.get(CONF_USERNAME), config.get(CONF_PASSWORD), config.get(CONF_RECIPIENT), + config.get(CONF_SENDER_NAME), config.get(CONF_DEBUG)) if mail_service.connection_is_valid(): @@ -74,7 +78,7 @@ class MailNotificationService(BaseNotificationService): """Implement the notification service for E-Mail messages.""" def __init__(self, server, port, timeout, sender, starttls, username, - password, recipients, debug): + password, recipients, sender_name, debug): """Initialize the service.""" self._server = server self._port = port @@ -84,6 +88,8 @@ class MailNotificationService(BaseNotificationService): self.username = username self.password = password self.recipients = recipients + self._sender_name = sender_name + self._timeout = timeout self.debug = debug self.tries = 2 @@ -128,19 +134,28 @@ class MailNotificationService(BaseNotificationService): Build and send a message to a user. Will send plain text normally, or will build a multipart HTML message - with inline image attachments if images config is defined. + with inline image attachments if images config is defined, or will + build a multipart HTML if html config is defined. """ subject = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) data = kwargs.get(ATTR_DATA) if data: - msg = _build_multipart_msg(message, images=data.get(ATTR_IMAGES)) + if ATTR_HTML in data: + msg = _build_html_msg(message, data[ATTR_HTML], + images=data.get(ATTR_IMAGES)) + else: + msg = _build_multipart_msg(message, + images=data.get(ATTR_IMAGES)) else: msg = _build_text_msg(message) msg['Subject'] = subject msg['To'] = ','.join(self.recipients) - msg['From'] = self._sender + if self._sender_name: + msg['From'] = '{} <{}>'.format(self._sender_name, self._sender) + else: + msg['From'] = self._sender msg['X-Mailer'] = 'HomeAssistant' msg['Date'] = email.utils.format_datetime(dt_util.now()) msg['Message-Id'] = email.utils.make_msgid() @@ -155,12 +170,16 @@ class MailNotificationService(BaseNotificationService): mail.sendmail(self._sender, self.recipients, msg.as_string()) break + except smtplib.SMTPServerDisconnected: + _LOGGER.warning( + "SMTPServerDisconnected sending mail: retrying connection") + mail.quit() + mail = self.connect() except smtplib.SMTPException: _LOGGER.warning( "SMTPException sending mail: retrying connection") mail.quit() mail = self.connect() - mail.quit() @@ -204,3 +223,25 @@ def _build_multipart_msg(message, images): body_html = MIMEText(''.join(body_text), 'html') msg_alt.attach(body_html) return msg + + +def _build_html_msg(text, html, images): + """Build Multipart message with in-line images and rich html (UTF-8).""" + _LOGGER.debug("Building html rich email") + msg = MIMEMultipart('related') + alternative = MIMEMultipart('alternative') + alternative.attach(MIMEText(text, _charset='utf-8')) + alternative.attach(MIMEText(html, ATTR_HTML, _charset='utf-8')) + msg.attach(alternative) + + for atch_num, atch_name in enumerate(images): + name = os.path.basename(atch_name) + try: + with open(atch_name, 'rb') as attachment_file: + attachment = MIMEImage(attachment_file.read(), filename=name) + msg.attach(attachment) + attachment.add_header('Content-ID', '<{}>'.format(name)) + except FileNotFoundError: + _LOGGER.warning('Attachment %s [#%s] not found. Skipping', + atch_name, atch_num) + return msg diff --git a/homeassistant/components/notify/telegram.py b/homeassistant/components/notify/telegram.py index 7ca2e1ed262..1bc2baa632e 100644 --- a/homeassistant/components/notify/telegram.py +++ b/homeassistant/components/notify/telegram.py @@ -4,186 +4,86 @@ Telegram platform for notify component. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.telegram/ """ -import io import logging -import urllib -import requests import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( - ATTR_TITLE, ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService) -from homeassistant.const import ( - CONF_API_KEY, ATTR_LOCATION, ATTR_LATITUDE, ATTR_LONGITUDE) + ATTR_MESSAGE, ATTR_TITLE, ATTR_DATA, ATTR_TARGET, + PLATFORM_SCHEMA, BaseNotificationService) +from homeassistant.const import ATTR_LOCATION _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['python-telegram-bot==5.3.1'] +DOMAIN = 'telegram_bot' +DEPENDENCIES = [DOMAIN] -ATTR_PHOTO = 'photo' ATTR_KEYBOARD = 'keyboard' +ATTR_INLINE_KEYBOARD = 'inline_keyboard' +ATTR_PHOTO = 'photo' ATTR_DOCUMENT = 'document' -ATTR_CAPTION = 'caption' -ATTR_URL = 'url' -ATTR_FILE = 'file' -ATTR_USERNAME = 'username' -ATTR_PASSWORD = 'password' CONF_CHAT_ID = 'chat_id' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_CHAT_ID): cv.string, + vol.Required(CONF_CHAT_ID): cv.positive_int, }) def get_service(hass, config, discovery_info=None): """Get the Telegram notification service.""" - import telegram - - try: - chat_id = config.get(CONF_CHAT_ID) - api_key = config.get(CONF_API_KEY) - bot = telegram.Bot(token=api_key) - username = bot.getMe()['username'] - _LOGGER.debug("Telegram bot is '%s'", username) - except urllib.error.HTTPError: - _LOGGER.error("Please check your access token") - return None - - return TelegramNotificationService(api_key, chat_id) - - -def load_data(url=None, file=None, username=None, password=None): - """Load photo/document into ByteIO/File container from a source.""" - try: - if url is not None: - # Load photo from URL - if username is not None and password is not None: - req = requests.get(url, auth=(username, password), timeout=15) - else: - req = requests.get(url, timeout=15) - return io.BytesIO(req.content) - - elif file is not None: - # Load photo from file - return open(file, "rb") - else: - _LOGGER.warning("Can't load photo no photo found in params!") - - except OSError as error: - _LOGGER.error("Can't load photo into ByteIO: %s", error) - - return None + chat_id = config.get(CONF_CHAT_ID) + return TelegramNotificationService(hass, chat_id) class TelegramNotificationService(BaseNotificationService): """Implement the notification service for Telegram.""" - def __init__(self, api_key, chat_id): + def __init__(self, hass, chat_id): """Initialize the service.""" - import telegram - - self._api_key = api_key self._chat_id = chat_id - self.bot = telegram.Bot(token=self._api_key) + self.hass = hass def send_message(self, message="", **kwargs): """Send a message to a user.""" - import telegram - - title = kwargs.get(ATTR_TITLE) + service_data = dict(target=kwargs.get(ATTR_TARGET, self._chat_id)) + if ATTR_TITLE in kwargs: + service_data.update({ATTR_TITLE: kwargs.get(ATTR_TITLE)}) + if message: + service_data.update({ATTR_MESSAGE: message}) data = kwargs.get(ATTR_DATA) - # Exists data for send a photo/location + # Get keyboard info + if data is not None and ATTR_KEYBOARD in data: + keys = data.get(ATTR_KEYBOARD) + keys = keys if isinstance(keys, list) else [keys] + service_data.update(keyboard=keys) + elif data is not None and ATTR_INLINE_KEYBOARD in data: + keys = data.get(ATTR_INLINE_KEYBOARD) + keys = keys if isinstance(keys, list) else [keys] + service_data.update(inline_keyboard=keys) + + # Send a photo, a document or a location if data is not None and ATTR_PHOTO in data: photos = data.get(ATTR_PHOTO, None) photos = photos if isinstance(photos, list) else [photos] - for photo_data in photos: - self.send_photo(photo_data) + service_data.update(photo_data) + self.hass.services.call( + DOMAIN, 'send_photo', service_data=service_data) return elif data is not None and ATTR_LOCATION in data: - return self.send_location(data.get(ATTR_LOCATION)) + service_data.update(data.get(ATTR_LOCATION)) + return self.hass.services.call( + DOMAIN, 'send_location', service_data=service_data) elif data is not None and ATTR_DOCUMENT in data: - return self.send_document(data.get(ATTR_DOCUMENT)) - elif data is not None and ATTR_KEYBOARD in data: - keys = data.get(ATTR_KEYBOARD) - keys = keys if isinstance(keys, list) else [keys] - return self.send_keyboard(message, keys) - - if title: - text = '{} {}'.format(title, message) - else: - text = message - - parse_mode = telegram.parsemode.ParseMode.MARKDOWN + service_data.update(data.get(ATTR_DOCUMENT)) + return self.hass.services.call( + DOMAIN, 'send_document', service_data=service_data) # Send message - try: - self.bot.sendMessage( - chat_id=self._chat_id, text=text, parse_mode=parse_mode) - except telegram.error.TelegramError: - _LOGGER.exception("Error sending message") - - def send_keyboard(self, message, keys): - """Display keyboard.""" - import telegram - - keyboard = telegram.ReplyKeyboardMarkup([ - [key.strip() for key in row.split(",")] for row in keys]) - try: - self.bot.sendMessage( - chat_id=self._chat_id, text=message, reply_markup=keyboard) - except telegram.error.TelegramError: - _LOGGER.exception("Error sending message") - - def send_photo(self, data): - """Send a photo.""" - import telegram - caption = data.get(ATTR_CAPTION) - - # Send photo - try: - photo = load_data( - url=data.get(ATTR_URL), - file=data.get(ATTR_FILE), - username=data.get(ATTR_USERNAME), - password=data.get(ATTR_PASSWORD), - ) - self.bot.sendPhoto( - chat_id=self._chat_id, photo=photo, caption=caption) - except telegram.error.TelegramError: - _LOGGER.exception("Error sending photo") - - def send_document(self, data): - """Send a document.""" - import telegram - caption = data.get(ATTR_CAPTION) - - # send photo - try: - document = load_data( - url=data.get(ATTR_URL), - file=data.get(ATTR_FILE), - username=data.get(ATTR_USERNAME), - password=data.get(ATTR_PASSWORD), - ) - self.bot.sendDocument( - chat_id=self._chat_id, document=document, caption=caption) - except telegram.error.TelegramError: - _LOGGER.exception("Error sending document") - - def send_location(self, gps): - """Send a location.""" - import telegram - latitude = float(gps.get(ATTR_LATITUDE, 0.0)) - longitude = float(gps.get(ATTR_LONGITUDE, 0.0)) - - # Send location - try: - self.bot.sendLocation( - chat_id=self._chat_id, latitude=latitude, longitude=longitude) - except telegram.error.TelegramError: - _LOGGER.exception("Error sending location") + _LOGGER.debug('TELEGRAM NOTIFIER calling %s.send_message with %s', + DOMAIN, service_data) + return self.hass.services.call( + DOMAIN, 'send_message', service_data=service_data) diff --git a/homeassistant/components/raspihats.py b/homeassistant/components/raspihats.py new file mode 100644 index 00000000000..3ab433f4b91 --- /dev/null +++ b/homeassistant/components/raspihats.py @@ -0,0 +1,249 @@ +""" +Support for controlling raspihats boards. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/raspihats/ +""" +import logging +import threading +import time + +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP +) + +REQUIREMENTS = ['raspihats==2.2.1'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'raspihats' + +CONF_I2C_HATS = 'i2c_hats' +CONF_BOARD = 'board' +CONF_ADDRESS = 'address' +CONF_CHANNELS = 'channels' +CONF_INDEX = 'index' +CONF_INVERT_LOGIC = 'invert_logic' +CONF_INITIAL_STATE = 'initial_state' + +I2C_HAT_NAMES = [ + 'Di16', 'Rly10', 'Di6Rly6', + 'DI16ac', 'DQ10rly', 'DQ16oc', 'DI6acDQ6rly' +] + +I2C_HATS_MANAGER = 'I2CH_MNG' + + +# pylint: disable=unused-argument +def setup(hass, config): + """Setup the raspihats component.""" + hass.data[I2C_HATS_MANAGER] = I2CHatsManager() + + def start_i2c_hats_keep_alive(event): + """Start I2C-HATs keep alive.""" + hass.data[I2C_HATS_MANAGER].start_keep_alive() + + def stop_i2c_hats_keep_alive(event): + """Stop I2C-HATs keep alive.""" + hass.data[I2C_HATS_MANAGER].stop_keep_alive() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_i2c_hats_keep_alive) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_i2c_hats_keep_alive) + return True + + +def log_message(source, *parts): + """Build log message.""" + message = source.__class__.__name__ + for part in parts: + message += ": " + str(part) + return message + + +class I2CHatsException(Exception): + """I2C-HATs exception.""" + + +class I2CHatsDIScanner(object): + """Scan Digital Inputs and fire callbacks.""" + + _DIGITAL_INPUTS = "di" + _OLD_VALUE = "old_value" + _CALLBACKS = "callbacks" + + def setup(self, i2c_hat): + """Setup I2C-HAT instance for digital inputs scanner.""" + if hasattr(i2c_hat, self._DIGITAL_INPUTS): + digital_inputs = getattr(i2c_hat, self._DIGITAL_INPUTS) + old_value = None + # add old value attribute + setattr(digital_inputs, self._OLD_VALUE, old_value) + # add callbacks dict attribute {channel: callback} + setattr(digital_inputs, self._CALLBACKS, {}) + + def register_callback(self, i2c_hat, channel, callback): + """Register edge callback.""" + if hasattr(i2c_hat, self._DIGITAL_INPUTS): + digital_inputs = getattr(i2c_hat, self._DIGITAL_INPUTS) + callbacks = getattr(digital_inputs, self._CALLBACKS) + callbacks[channel] = callback + setattr(digital_inputs, self._CALLBACKS, callbacks) + + def scan(self, i2c_hat): + """Scan I2C-HATs digital inputs and fire callbacks.""" + if hasattr(i2c_hat, self._DIGITAL_INPUTS): + digital_inputs = getattr(i2c_hat, self._DIGITAL_INPUTS) + callbacks = getattr(digital_inputs, self._CALLBACKS) + old_value = getattr(digital_inputs, self._OLD_VALUE) + value = digital_inputs.value # i2c data transfer + if old_value is not None and value != old_value: + for channel in range(0, len(digital_inputs.channels)): + state = (value >> channel) & 0x01 + old_state = (old_value >> channel) & 0x01 + if state != old_state: + callback = callbacks.get(channel, None) + if callback is not None: + callback(state) + setattr(digital_inputs, self._OLD_VALUE, value) + + +class I2CHatsManager(threading.Thread): + """Manages all I2C-HATs instances.""" + + _EXCEPTION = "exception" + _CALLBACKS = "callbacks" + + def __init__(self): + """Init I2C-HATs Manager.""" + threading.Thread.__init__(self) + self._lock = threading.Lock() + self._i2c_hats = {} + self._run = False + self._di_scanner = I2CHatsDIScanner() + + def register_board(self, board, address): + """Register I2C-HAT.""" + with self._lock: + i2c_hat = self._i2c_hats.get(address) + if i2c_hat is None: + # pylint: disable=import-error + import raspihats.i2c_hats as module + constructor = getattr(module, board) + i2c_hat = constructor(address) + setattr(i2c_hat, self._CALLBACKS, {}) + + # Setting exception attribute will trigger online callbacks + # when keep alive thread starts. + setattr(i2c_hat, self._EXCEPTION, None) + + self._di_scanner.setup(i2c_hat) + self._i2c_hats[address] = i2c_hat + status_word = i2c_hat.status # read status_word to reset bits + _LOGGER.info( + log_message(self, i2c_hat, "registered", status_word) + ) + + def run(self): + """Keep alive for I2C-HATs.""" + # pylint: disable=import-error + from raspihats.i2c_hats import ResponseException + + _LOGGER.info( + log_message(self, "starting") + ) + while self._run: + with self._lock: + for i2c_hat in list(self._i2c_hats.values()): + try: + self._di_scanner.scan(i2c_hat) + self._read_status(i2c_hat) + + if hasattr(i2c_hat, self._EXCEPTION): + if getattr(i2c_hat, self._EXCEPTION) is not None: + _LOGGER.warning( + log_message(self, i2c_hat, "online again") + ) + delattr(i2c_hat, self._EXCEPTION) + # trigger online callbacks + callbacks = getattr(i2c_hat, self._CALLBACKS) + for callback in list(callbacks.values()): + callback() + except ResponseException as ex: + if not hasattr(i2c_hat, self._EXCEPTION): + _LOGGER.error( + log_message(self, i2c_hat, ex) + ) + setattr(i2c_hat, self._EXCEPTION, ex) + time.sleep(0.05) + _LOGGER.info( + log_message(self, "exiting") + ) + + def _read_status(self, i2c_hat): + """Read I2C-HATs status.""" + status_word = i2c_hat.status + if status_word.value != 0x00: + _LOGGER.error( + log_message(self, i2c_hat, status_word) + ) + + def start_keep_alive(self): + """Start keep alive mechanism.""" + self._run = True + threading.Thread.start(self) + + def stop_keep_alive(self): + """Stop keep alive mechanism.""" + self._run = False + self.join() + + def register_di_callback(self, address, channel, callback): + """Register I2C-HAT digital input edge callback.""" + with self._lock: + i2c_hat = self._i2c_hats[address] + self._di_scanner.register_callback(i2c_hat, channel, callback) + + def register_online_callback(self, address, channel, callback): + """Register I2C-HAT online callback.""" + with self._lock: + i2c_hat = self._i2c_hats[address] + callbacks = getattr(i2c_hat, self._CALLBACKS) + callbacks[channel] = callback + setattr(i2c_hat, self._CALLBACKS, callbacks) + + def read_di(self, address, channel): + """Read a value from a I2C-HAT digital input.""" + # pylint: disable=import-error + from raspihats.i2c_hats import ResponseException + + with self._lock: + i2c_hat = self._i2c_hats[address] + try: + value = i2c_hat.di.value + return (value >> channel) & 0x01 + except ResponseException as ex: + raise I2CHatsException(str(ex)) + + def write_dq(self, address, channel, value): + """Write a value to a I2C-HAT digital output.""" + # pylint: disable=import-error + from raspihats.i2c_hats import ResponseException + + with self._lock: + i2c_hat = self._i2c_hats[address] + try: + i2c_hat.dq.channels[channel] = value + except ResponseException as ex: + raise I2CHatsException(str(ex)) + + def read_dq(self, address, channel): + """Read a value from a I2C-HAT digital output.""" + # pylint: disable=import-error + from raspihats.i2c_hats import ResponseException + + with self._lock: + i2c_hat = self._i2c_hats[address] + try: + return i2c_hat.dq.channels[channel] + except ResponseException as ex: + raise I2CHatsException(str(ex)) diff --git a/homeassistant/components/remote/kira.py b/homeassistant/components/remote/kira.py new file mode 100755 index 00000000000..3e816844a35 --- /dev/null +++ b/homeassistant/components/remote/kira.py @@ -0,0 +1,79 @@ +""" +Support for Keene Electronics IR-IP devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/remote.kira/ +""" +import logging +import functools as ft + +import homeassistant.components.remote as remote +from homeassistant.helpers.entity import Entity + +from homeassistant.const import ( + STATE_UNKNOWN, + CONF_DEVICE, + CONF_NAME) + +DOMAIN = 'kira' + +_LOGGER = logging.getLogger(__name__) + +CONF_REMOTE = "remote" + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Kira platform.""" + if discovery_info: + name = discovery_info.get(CONF_NAME) + device = discovery_info.get(CONF_DEVICE) + + kira = hass.data[DOMAIN][CONF_REMOTE][name] + add_devices([KiraRemote(device, kira)]) + return True + + +class KiraRemote(Entity): + """Remote representation used to send commands to a Kira device.""" + + def __init__(self, name, kira): + """Initialize KiraRemote class.""" + _LOGGER.debug("KiraRemote device init started for: %s", name) + self._name = name + self._state = STATE_UNKNOWN + + self._kira = kira + + @property + def name(self): + """Return the Kira device's name.""" + return self._name + + @property + def device_state_attributes(self): + """Add platform specific attributes.""" + return {} + + @property + def is_on(self): + """Return True. Power state doesn't apply to this device.""" + return True + + def update(self): + """No-op.""" + + def send_command(self, **kwargs): + """Send a command to one device.""" + code_tuple = (kwargs.get(remote.ATTR_COMMAND), + kwargs.get(remote.ATTR_DEVICE)) + _LOGGER.info("Sending Command: %s to %s", *code_tuple) + + self._kira.sendCode(code_tuple) + + def async_send_command(self, **kwargs): + """Send a command to a device. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.loop.run_in_executor( + None, ft.partial(self.send_command, **kwargs)) diff --git a/homeassistant/components/remote/services.yaml b/homeassistant/components/remote/services.yaml index 2023588fcc2..189377c503f 100644 --- a/homeassistant/components/remote/services.yaml +++ b/homeassistant/components/remote/services.yaml @@ -1,7 +1,7 @@ # Describes the format for available remote services turn_on: - description: Semds the Power On Command + description: Sends the Power On Command fields: entity_id: @@ -20,7 +20,7 @@ turn_off: example: 'remote.family_room' send_command: - description: Semds a single command to a single device + description: Sends a single command to a single device fields: entity_id: diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py index 52fd602f0de..33feb8c034b 100644 --- a/homeassistant/components/rflink.py +++ b/homeassistant/components/rflink.py @@ -20,7 +20,7 @@ from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['rflink==0.0.31'] +REQUIREMENTS = ['rflink==0.0.34'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/rpi_pfio.py b/homeassistant/components/rpi_pfio.py new file mode 100644 index 00000000000..bf8fdccfab0 --- /dev/null +++ b/homeassistant/components/rpi_pfio.py @@ -0,0 +1,63 @@ +""" +Support for controlling the PiFace Digital I/O module on a RPi. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/rpi_pfio/ +""" +import logging + +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + +REQUIREMENTS = ['pifacecommon==4.1.2', 'pifacedigitalio==3.0.5'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'rpi_pfio' + +DATA_PFIO_LISTENER = 'pfio_listener' + + +def setup(hass, config): + """Set up the Raspberry PI PFIO component.""" + import pifacedigitalio as PFIO + + pifacedigital = PFIO.PiFaceDigital() + hass.data[DATA_PFIO_LISTENER] = PFIO.InputEventListener(chip=pifacedigital) + + def cleanup_pfio(event): + """Stuff to do before stopping.""" + PFIO.deinit() + + def prepare_pfio(event): + """Stuff to do when home assistant starts.""" + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_pfio) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_pfio) + PFIO.init() + + return True + + +def write_output(port, value): + """Write a value to a PFIO.""" + import pifacedigitalio as PFIO + PFIO.digital_write(port, value) + + +def read_input(port): + """Read a value from a PFIO.""" + import pifacedigitalio as PFIO + return PFIO.digital_read(port) + + +def edge_detect(hass, port, event_callback, settle): + """Add detection for RISING and FALLING events.""" + import pifacedigitalio as PFIO + hass.data[DATA_PFIO_LISTENER].register( + port, PFIO.IODIR_BOTH, event_callback, settle_time=settle) + + +def activate_listener(hass): + """Activate the registered listener events.""" + hass.data[DATA_PFIO_LISTENER].activate() diff --git a/homeassistant/components/scene/wink.py b/homeassistant/components/scene/wink.py index 3906e7b5551..008edf6f131 100644 --- a/homeassistant/components/scene/wink.py +++ b/homeassistant/components/scene/wink.py @@ -4,6 +4,7 @@ Support for Wink scenes. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/scene.wink/ """ +import asyncio import logging from homeassistant.components.scene import Scene @@ -29,6 +30,12 @@ class WinkScene(WinkDevice, Scene): def __init__(self, wink, hass): """Initialize the Wink device.""" super().__init__(wink, hass) + hass.data[DOMAIN]['entities']['scene'].append(self) + + @asyncio.coroutine + def async_added_to_hass(self): + """Callback when entity is added to hass.""" + self.hass.data[DOMAIN]['entities']['scene'].append(self) @property def is_on(self): diff --git a/homeassistant/components/sensor/amcrest.py b/homeassistant/components/sensor/amcrest.py index 40556fbe5ad..23f7fc4dfbe 100644 --- a/homeassistant/components/sensor/amcrest.py +++ b/homeassistant/components/sensor/amcrest.py @@ -19,7 +19,7 @@ import homeassistant.loader as loader from requests.exceptions import HTTPError, ConnectTimeout -REQUIREMENTS = ['amcrest==1.1.9'] +REQUIREMENTS = ['amcrest==1.2.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/blink.py b/homeassistant/components/sensor/blink.py index e434776ffc6..44557978117 100644 --- a/homeassistant/components/sensor/blink.py +++ b/homeassistant/components/sensor/blink.py @@ -77,7 +77,7 @@ class BlinkSensor(Entity): if self._type == 'temperature': self._state = camera.temperature elif self._type == 'battery': - self._state = camera.battery + self._state = camera.battery_string elif self._type == 'notifications': self._state = camera.notifications else: diff --git a/homeassistant/components/sensor/dweet.py b/homeassistant/components/sensor/dweet.py index e5f3d00830b..c049368153c 100644 --- a/homeassistant/components/sensor/dweet.py +++ b/homeassistant/components/sensor/dweet.py @@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['dweepy==0.2.0'] +REQUIREMENTS = ['dweepy==0.3.0'] _LOGGER = logging.getLogger(__name__) @@ -44,7 +44,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): device = config.get(CONF_DEVICE) value_template = config.get(CONF_VALUE_TEMPLATE) unit = config.get(CONF_UNIT_OF_MEASUREMENT) - value_template.hass = hass + if value_template is not None: + value_template.hass = hass + try: content = json.dumps(dweepy.get_latest_dweet_for(device)[0]['content']) except dweepy.DweepyError: @@ -57,7 +59,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): dweet = DweetData(device) - add_devices([DweetSensor(hass, dweet, name, value_template, unit)]) + add_devices([DweetSensor(hass, dweet, name, value_template, unit)], True) class DweetSensor(Entity): @@ -71,7 +73,6 @@ class DweetSensor(Entity): self._value_template = value_template self._state = STATE_UNKNOWN self._unit_of_measurement = unit_of_measurement - self.update() @property def name(self): @@ -86,18 +87,19 @@ class DweetSensor(Entity): @property def state(self): """Return the state.""" - if self.dweet.data is None: - return STATE_UNKNOWN - else: - values = json.dumps(self.dweet.data[0]['content']) - value = self._value_template.render_with_possible_json_value( - values) - return value + return self._state def update(self): """Get the latest data from REST API.""" self.dweet.update() + if self.dweet.data is None: + self._state = STATE_UNKNOWN + else: + values = json.dumps(self.dweet.data[0]['content']) + self._state = self._value_template.render_with_possible_json_value( + values, STATE_UNKNOWN) + class DweetData(object): """The class for handling the data retrieval.""" @@ -115,5 +117,5 @@ class DweetData(object): try: self.data = dweepy.get_latest_dweet_for(self._device) except dweepy.DweepyError: - _LOGGER.error("Device %s could not be found", self._device) + _LOGGER.warning("Device %s doesn't contain any data", self._device) self.data = None diff --git a/homeassistant/components/sensor/envirophat.py b/homeassistant/components/sensor/envirophat.py index 48370d76c83..f2db833954f 100644 --- a/homeassistant/components/sensor/envirophat.py +++ b/homeassistant/components/sensor/envirophat.py @@ -54,7 +54,14 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Sense HAT sensor platform.""" - data = EnvirophatData(config.get(CONF_USE_LEDS)) + try: + # pylint: disable=import-error + import envirophat + except OSError: + _LOGGER.error("No Enviro pHAT was found.") + return False + + data = EnvirophatData(envirophat, config.get(CONF_USE_LEDS)) dev = [] for variable in config[CONF_DISPLAY_OPTIONS]: @@ -97,9 +104,6 @@ class EnvirophatSensor(Entity): def update(self): """Get the latest data and updates the states.""" self.data.update() - if not self.data.light: - _LOGGER.error("Didn't receive data") - return if self.type == 'light': self._state = self.data.light @@ -138,8 +142,9 @@ class EnvirophatSensor(Entity): class EnvirophatData(object): """Get the latest data and update.""" - def __init__(self, use_leds): + def __init__(self, envirophat, use_leds): """Initialize the data object.""" + self.envirophat = envirophat self.use_leds = use_leds # sensors readings self.light = None @@ -162,34 +167,32 @@ class EnvirophatData(object): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from Enviro pHAT.""" - from envirophat import analog, leds, light, motion, weather - # Light sensor reading: 16-bit integer - self.light = light.light() + self.light = self.envirophat.light.light() if self.use_leds: - # pylint: disable=no-value-for-parameter - leds.on() + self.envirophat.leds.on() # the three color values scaled agains the overall light, 0-255 - self.light_red, self.light_green, self.light_blue = light.rgb() + self.light_red, self.light_green, self.light_blue = \ + self.envirophat.light.rgb() if self.use_leds: # pylint: disable=no-value-for-parameter - leds.off() + self.envirophat.leds.off() # accelerometer readings in G self.accelerometer_x, self.accelerometer_y, self.accelerometer_z = \ - motion.accelerometer() + self.envirophat.motion.accelerometer() # raw magnetometer reading self.magnetometer_x, self.magnetometer_y, self.magnetometer_z = \ - motion.magnetometer() + self.envirophat.motion.magnetometer() # temperature resolution of BMP280 sensor: 0.01°C - self.temperature = round(weather.temperature(), 2) + self.temperature = round(self.envirophat.weather.temperature(), 2) # pressure resolution of BMP280 sensor: 0.16 Pa, rounding to 0.1 Pa # with conversion to 100 Pa = 1 hPa - self.pressure = round(weather.pressure() / 100.0, 3) + self.pressure = round(self.envirophat.weather.pressure() / 100.0, 3) # Voltage sensor, reading between 0-3.3V self.voltage_0, self.voltage_1, self.voltage_2, self.voltage_3 = \ - analog.read_all() + self.envirophat.analog.read_all() diff --git a/homeassistant/components/sensor/file.py b/homeassistant/components/sensor/file.py new file mode 100644 index 00000000000..afa305a0fb0 --- /dev/null +++ b/homeassistant/components/sensor/file.py @@ -0,0 +1,98 @@ +""" +Support for sensor value(s) stored in local files. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.file/ +""" +import os +import asyncio +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_VALUE_TEMPLATE, CONF_NAME, CONF_UNIT_OF_MEASUREMENT) +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +CONF_FILE_PATH = 'file_path' + +DEFAULT_NAME = 'File' + +ICON = 'mdi:file' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_FILE_PATH): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the file sensor.""" + file_path = config.get(CONF_FILE_PATH) + name = config.get(CONF_NAME) + unit = config.get(CONF_UNIT_OF_MEASUREMENT) + value_template = config.get(CONF_VALUE_TEMPLATE) + + if value_template is not None: + value_template.hass = hass + + async_add_devices( + [FileSensor(name, file_path, unit, value_template)], True) + + +class FileSensor(Entity): + """Implementation of a file sensor.""" + + def __init__(self, name, file_path, unit_of_measurement, value_template): + """Initialize the file sensor.""" + self._name = name + self._file_path = file_path + self._unit_of_measurement = unit_of_measurement + self._val_tpl = value_template + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit_of_measurement + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return ICON + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + """Get the latest entry from a file and updates the state.""" + try: + with open(self._file_path, 'r', encoding='utf-8') as file_data: + for line in file_data: + data = line + data = data.strip() + except (IndexError, FileNotFoundError, IsADirectoryError, + UnboundLocalError): + _LOGGER.warning("File or data not present at the moment: %s", + os.path.basename(self._file_path)) + return + + if self._val_tpl is not None: + self._state = self._val_tpl.async_render_with_possible_json_value( + data, None) + else: + self._state = data diff --git a/homeassistant/components/sensor/influxdb.py b/homeassistant/components/sensor/influxdb.py index b4688c77e1b..d1d693543be 100644 --- a/homeassistant/components/sensor/influxdb.py +++ b/homeassistant/components/sensor/influxdb.py @@ -174,7 +174,7 @@ class InfluxSensorData(object): "to UNKNOWN: %s", self.query) self.value = None else: - if points: + if len(points) > 1: _LOGGER.warning("Query returned multiple points, only first " "one shown: %s", self.query) self.value = points[0].get('value') diff --git a/homeassistant/components/sensor/kira.py b/homeassistant/components/sensor/kira.py new file mode 100644 index 00000000000..232e50b85ed --- /dev/null +++ b/homeassistant/components/sensor/kira.py @@ -0,0 +1,79 @@ +"""KIRA interface to receive UDP packets from an IR-IP bridge.""" +# pylint: disable=import-error +import logging + +from homeassistant.const import ( + CONF_DEVICE, + CONF_NAME, + STATE_UNKNOWN) + +from homeassistant.helpers.entity import Entity + +DOMAIN = 'kira' + +_LOGGER = logging.getLogger(__name__) + +ICON = 'mdi:remote' + +CONF_SENSOR = "sensor" + + +# pylint: disable=unused-argument, too-many-function-args +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """Setup Kira sensor.""" + if discovery_info is not None: + name = discovery_info.get(CONF_NAME) + device = discovery_info.get(CONF_DEVICE) + kira = hass.data[DOMAIN][CONF_SENSOR][name] + add_devices_callback([KiraReceiver(device, kira)]) + + +class KiraReceiver(Entity): + """Implementation of a Kira Receiver.""" + + def __init__(self, name, kira): + """Initialize the sensor.""" + self._name = name + self._state = STATE_UNKNOWN + self._device = STATE_UNKNOWN + + kira.registerCallback(self._update_callback) + + def _update_callback(self, code): + code_name, device = code + _LOGGER.info("Kira Code: %s", code_name) + self._state = code_name + self._device = device + self.schedule_update_ha_state() + + @property + def name(self): + """Return the name of the receiver.""" + return self._name + + @property + def icon(self): + """Return icon.""" + return ICON + + @property + def state(self): + """Return the state of the receiver.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + attr = {} + attr[CONF_DEVICE] = self._device + return attr + + @property + def should_poll(self) -> bool: + """Entity should not be polled.""" + return False + + @property + def force_update(self) -> bool: + """Kira should force updates. Repeated states have meaning.""" + return True diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py index ac8646bb3c1..063c4e8068e 100644 --- a/homeassistant/components/sensor/miflora.py +++ b/homeassistant/components/sensor/miflora.py @@ -45,7 +45,7 @@ SENSOR_TYPES = { PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_MAC): cv.string, - vol.Required(CONF_MONITORED_CONDITIONS): + vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_MEDIAN, default=DEFAULT_MEDIAN): cv.positive_int, diff --git a/homeassistant/components/sensor/modem_callerid.py b/homeassistant/components/sensor/modem_callerid.py index e12ddb445ec..0b71540f346 100644 --- a/homeassistant/components/sensor/modem_callerid.py +++ b/homeassistant/components/sensor/modem_callerid.py @@ -14,9 +14,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['https://github.com/vroomfonde1/basicmodem' - '/archive/0.7.zip' - '#basicmodem==0.7'] +REQUIREMENTS = ['basicmodem==0.7'] _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Modem CallerID' diff --git a/homeassistant/components/sensor/moon.py b/homeassistant/components/sensor/moon.py index dc890c0f3cd..ca79e5241c4 100644 --- a/homeassistant/components/sensor/moon.py +++ b/homeassistant/components/sensor/moon.py @@ -15,8 +15,6 @@ import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['astral==1.4'] - _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Moon' diff --git a/homeassistant/components/sensor/nzbget.py b/homeassistant/components/sensor/nzbget.py index 7a95e445ae0..a440074b81b 100644 --- a/homeassistant/components/sensor/nzbget.py +++ b/homeassistant/components/sensor/nzbget.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_NAME, CONF_PORT, - CONTENT_TYPE_JSON, CONF_MONITORED_VARIABLES) + CONF_SSL, CONTENT_TYPE_JSON, CONF_MONITORED_VARIABLES) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -44,6 +44,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SSL, default=False): cv.boolean, vol.Optional(CONF_USERNAME): cv.string, }) @@ -53,12 +54,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the NZBGet sensors.""" host = config.get(CONF_HOST) port = config.get(CONF_PORT) + ssl = 's' if config.get(CONF_SSL) else '' name = config.get(CONF_NAME) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) monitored_types = config.get(CONF_MONITORED_VARIABLES) - url = "http://{}:{}/jsonrpc".format(host, port) + url = "http{}://{}:{}/jsonrpc".format(ssl, host, port) try: nzbgetapi = NZBGetAPI( diff --git a/homeassistant/components/sensor/pocketcasts.py b/homeassistant/components/sensor/pocketcasts.py index 36e0bb88e0a..20b1c9885cc 100644 --- a/homeassistant/components/sensor/pocketcasts.py +++ b/homeassistant/components/sensor/pocketcasts.py @@ -17,9 +17,7 @@ from homeassistant.components.sensor import (PLATFORM_SCHEMA) _LOGGER = logging.getLogger(__name__) -COMMIT = '9f61ff00c77c7c98ffa0af9dd3540df3dce4a836' -REQUIREMENTS = ['https://github.com/molobrakos/python-pocketcasts/' - 'archive/%s.zip#python-pocketcasts==0.0.1' % COMMIT] +REQUIREMENTS = ['pocketcasts==0.1'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_USERNAME): cv.string, diff --git a/homeassistant/components/sensor/scrape.py b/homeassistant/components/sensor/scrape.py index f825628d9ae..fe50b567319 100644 --- a/homeassistant/components/sensor/scrape.py +++ b/homeassistant/components/sensor/scrape.py @@ -16,7 +16,7 @@ from homeassistant.const import ( from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['beautifulsoup4==4.5.3'] +REQUIREMENTS = ['beautifulsoup4==4.6.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/snmp.py b/homeassistant/components/sensor/snmp.py index 1342f3d2a9e..2ce08f262d7 100644 --- a/homeassistant/components/sensor/snmp.py +++ b/homeassistant/components/sensor/snmp.py @@ -22,11 +22,18 @@ _LOGGER = logging.getLogger(__name__) CONF_BASEOID = 'baseoid' CONF_COMMUNITY = 'community' +CONF_VERSION = 'version' DEFAULT_COMMUNITY = 'public' DEFAULT_HOST = 'localhost' DEFAULT_NAME = 'SNMP' DEFAULT_PORT = '161' +DEFAULT_VERSION = '1' + +SNMP_VERSIONS = { + '1': 0, + '2c': 1 +} MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) @@ -37,6 +44,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): + vol.In(SNMP_VERSIONS), }) @@ -52,10 +61,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): community = config.get(CONF_COMMUNITY) baseoid = config.get(CONF_BASEOID) unit = config.get(CONF_UNIT_OF_MEASUREMENT) + version = config.get(CONF_VERSION) errindication, _, _, _ = next( getCmd(SnmpEngine(), - CommunityData(community, mpModel=0), + CommunityData(community, mpModel=SNMP_VERSIONS[version]), UdpTransportTarget((host, port)), ContextData(), ObjectType(ObjectIdentity(baseoid)))) @@ -64,7 +74,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error("Please check the details in the configuration file") return False else: - data = SnmpData(host, port, community, baseoid) + data = SnmpData(host, port, community, baseoid, version) add_devices([SnmpSensor(data, name, unit)]) @@ -103,12 +113,13 @@ class SnmpSensor(Entity): class SnmpData(object): """Get the latest data and update the states.""" - def __init__(self, host, port, community, baseoid): + def __init__(self, host, port, community, baseoid, version): """Initialize the data object.""" self._host = host self._port = port self._community = community self._baseoid = baseoid + self._version = SNMP_VERSIONS[version] self.value = None @Throttle(MIN_TIME_BETWEEN_UPDATES) @@ -119,7 +130,7 @@ class SnmpData(object): ObjectType, ObjectIdentity) errindication, errstatus, errindex, restable = next( getCmd(SnmpEngine(), - CommunityData(self._community, mpModel=0), + CommunityData(self._community, mpModel=self._version), UdpTransportTarget((self._host, self._port)), ContextData(), ObjectType(ObjectIdentity(self._baseoid))) diff --git a/homeassistant/components/sensor/wink.py b/homeassistant/components/sensor/wink.py index 27cfbd691ad..b8c2b8a6236 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/sensor/wink.py @@ -4,6 +4,7 @@ Support for Wink sensors. For more details about this platform, please refer to the documentation at at https://home-assistant.io/components/sensor.wink/ """ +import asyncio import logging from homeassistant.const import TEMP_CELSIUS @@ -58,6 +59,11 @@ class WinkSensorDevice(WinkDevice, Entity): else: self._unit_of_measurement = self.wink.unit() + @asyncio.coroutine + def async_added_to_hass(self): + """Callback when entity is added to hass.""" + self.hass.data[DOMAIN]['entities']['sensor'].append(self) + @property def state(self): """Return the state.""" diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index b2af30d8438..9d3d82bd8fc 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -4,20 +4,18 @@ Support for functionality to keep track of the sun. For more details about this component, please refer to the documentation at https://home-assistant.io/components/sun/ """ +import asyncio import logging from datetime import timedelta -import voluptuous as vol - from homeassistant.const import CONF_ELEVATION +from homeassistant.core import callback from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import ( - track_point_in_utc_time, track_utc_time_change) + async_track_point_in_utc_time, async_track_utc_time_change) +from homeassistant.helpers.sun import ( + get_astral_location, get_astral_event_next) from homeassistant.util import dt as dt_util -import homeassistant.helpers.config_validation as cv -import homeassistant.util as util - -REQUIREMENTS = ['astral==1.4'] _LOGGER = logging.getLogger(__name__) @@ -37,223 +35,16 @@ STATE_ATTR_NEXT_NOON = 'next_noon' STATE_ATTR_NEXT_RISING = 'next_rising' STATE_ATTR_NEXT_SETTING = 'next_setting' -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_ELEVATION): cv.positive_int, - }), -}, extra=vol.ALLOW_EXTRA) - -def is_on(hass, entity_id=None): - """Test if the sun is currently up based on the statemachine.""" - entity_id = entity_id or ENTITY_ID - - return hass.states.is_state(entity_id, STATE_ABOVE_HORIZON) - - -def next_dawn(hass, entity_id=None): - """Local datetime object of the next dawn. - - Async friendly. - """ - utc_next = next_dawn_utc(hass, entity_id) - - return dt_util.as_local(utc_next) if utc_next else None - - -def next_dawn_utc(hass, entity_id=None): - """UTC datetime object of the next dawn. - - Async friendly. - """ - entity_id = entity_id or ENTITY_ID - - state = hass.states.get(ENTITY_ID) - - try: - return dt_util.parse_datetime( - state.attributes[STATE_ATTR_NEXT_DAWN]) - except (AttributeError, KeyError): - # AttributeError if state is None - # KeyError if STATE_ATTR_NEXT_DAWN does not exist - return None - - -def next_dusk(hass, entity_id=None): - """Local datetime object of the next dusk. - - Async friendly. - """ - utc_next = next_dusk_utc(hass, entity_id) - - return dt_util.as_local(utc_next) if utc_next else None - - -def next_dusk_utc(hass, entity_id=None): - """UTC datetime object of the next dusk. - - Async friendly. - """ - entity_id = entity_id or ENTITY_ID - - state = hass.states.get(ENTITY_ID) - - try: - return dt_util.parse_datetime( - state.attributes[STATE_ATTR_NEXT_DUSK]) - except (AttributeError, KeyError): - # AttributeError if state is None - # KeyError if STATE_ATTR_NEXT_DUSK does not exist - return None - - -def next_midnight(hass, entity_id=None): - """Local datetime object of the next midnight. - - Async friendly. - """ - utc_next = next_midnight_utc(hass, entity_id) - - return dt_util.as_local(utc_next) if utc_next else None - - -def next_midnight_utc(hass, entity_id=None): - """UTC datetime object of the next midnight. - - Async friendly. - """ - entity_id = entity_id or ENTITY_ID - - state = hass.states.get(ENTITY_ID) - - try: - return dt_util.parse_datetime( - state.attributes[STATE_ATTR_NEXT_MIDNIGHT]) - except (AttributeError, KeyError): - # AttributeError if state is None - # KeyError if STATE_ATTR_NEXT_MIDNIGHT does not exist - return None - - -def next_noon(hass, entity_id=None): - """Local datetime object of the next solar noon. - - Async friendly. - """ - utc_next = next_noon_utc(hass, entity_id) - - return dt_util.as_local(utc_next) if utc_next else None - - -def next_noon_utc(hass, entity_id=None): - """UTC datetime object of the next noon. - - Async friendly. - """ - entity_id = entity_id or ENTITY_ID - - state = hass.states.get(ENTITY_ID) - - try: - return dt_util.parse_datetime( - state.attributes[STATE_ATTR_NEXT_NOON]) - except (AttributeError, KeyError): - # AttributeError if state is None - # KeyError if STATE_ATTR_NEXT_NOON does not exist - return None - - -def next_setting(hass, entity_id=None): - """Local datetime object of the next sun setting. - - Async friendly. - """ - utc_next = next_setting_utc(hass, entity_id) - - return dt_util.as_local(utc_next) if utc_next else None - - -def next_setting_utc(hass, entity_id=None): - """UTC datetime object of the next sun setting. - - Async friendly. - """ - entity_id = entity_id or ENTITY_ID - - state = hass.states.get(ENTITY_ID) - - try: - return dt_util.parse_datetime( - state.attributes[STATE_ATTR_NEXT_SETTING]) - except (AttributeError, KeyError): - # AttributeError if state is None - # KeyError if STATE_ATTR_NEXT_SETTING does not exist - return None - - -def next_rising(hass, entity_id=None): - """Local datetime object of the next sun rising. - - Async friendly. - """ - utc_next = next_rising_utc(hass, entity_id) - - return dt_util.as_local(utc_next) if utc_next else None - - -def next_rising_utc(hass, entity_id=None): - """UTC datetime object of the next sun rising. - - Async friendly. - """ - entity_id = entity_id or ENTITY_ID - - state = hass.states.get(ENTITY_ID) - - try: - return dt_util.parse_datetime(state.attributes[STATE_ATTR_NEXT_RISING]) - except (AttributeError, KeyError): - # AttributeError if state is None - # KeyError if STATE_ATTR_NEXT_RISING does not exist - return None - - -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """Track the state of the sun.""" - if None in (hass.config.latitude, hass.config.longitude): - _LOGGER.error("Latitude or longitude not set in Home Assistant config") - return False + if config.get(CONF_ELEVATION) is not None: + _LOGGER.warning( + "Elevation is now configured in home assistant core. " + "See https://home-assistant.io/docs/configuration/basic/") - latitude = util.convert(hass.config.latitude, float) - longitude = util.convert(hass.config.longitude, float) - errors = [] - - if latitude is None: - errors.append('Latitude needs to be a decimal value') - elif -90 > latitude < 90: - errors.append('Latitude needs to be -90 .. 90') - - if longitude is None: - errors.append('Longitude needs to be a decimal value') - elif -180 > longitude < 180: - errors.append('Longitude needs to be -180 .. 180') - - if errors: - _LOGGER.error('Invalid configuration received: %s', ", ".join(errors)) - return False - - platform_config = config.get(DOMAIN, {}) - - elevation = platform_config.get(CONF_ELEVATION) - if elevation is None: - elevation = hass.config.elevation or 0 - - from astral import Location - - location = Location(('', '', latitude, longitude, - hass.config.time_zone.zone, elevation)) - - sun = Sun(hass, location) + sun = Sun(hass, get_astral_location(hass)) sun.point_in_time_listener(dt_util.utcnow()) return True @@ -271,9 +62,9 @@ class Sun(Entity): self._state = self.next_rising = self.next_setting = None self.next_dawn = self.next_dusk = None self.next_midnight = self.next_noon = None - self.solar_elevation = self.solar_azimuth = 0 + self.solar_elevation = self.solar_azimuth = None - track_utc_time_change(hass, self.timer_update, second=30) + async_track_utc_time_change(hass, self.timer_update, second=30) @property def name(self): @@ -308,64 +99,42 @@ class Sun(Entity): return min(self.next_dawn, self.next_dusk, self.next_midnight, self.next_noon, self.next_rising, self.next_setting) - @staticmethod - def get_next_solar_event(callable_on_astral_location, - utc_point_in_time, mod, increment): - """Calculate sun state at a point in UTC time.""" - import astral - - while True: - try: - next_dt = callable_on_astral_location( - utc_point_in_time + timedelta(days=mod), local=False) - if next_dt > utc_point_in_time: - break - except astral.AstralError: - pass - mod += increment - - return next_dt - + @callback def update_as_of(self, utc_point_in_time): """Update the attributes containing solar events.""" - self.next_dawn = Sun.get_next_solar_event( - self.location.dawn, utc_point_in_time, -1, 1) - self.next_dusk = Sun.get_next_solar_event( - self.location.dusk, utc_point_in_time, -1, 1) - self.next_midnight = Sun.get_next_solar_event( - self.location.solar_midnight, utc_point_in_time, -1, 1) - self.next_noon = Sun.get_next_solar_event( - self.location.solar_noon, utc_point_in_time, -1, 1) - self.next_rising = Sun.get_next_solar_event( - self.location.sunrise, utc_point_in_time, -1, 1) - self.next_setting = Sun.get_next_solar_event( - self.location.sunset, utc_point_in_time, -1, 1) + self.next_dawn = get_astral_event_next( + self.hass, 'dawn', utc_point_in_time) + self.next_dusk = get_astral_event_next( + self.hass, 'dusk', utc_point_in_time) + self.next_midnight = get_astral_event_next( + self.hass, 'solar_midnight', utc_point_in_time) + self.next_noon = get_astral_event_next( + self.hass, 'solar_noon', utc_point_in_time) + self.next_rising = get_astral_event_next( + self.hass, 'sunrise', utc_point_in_time) + self.next_setting = get_astral_event_next( + self.hass, 'sunset', utc_point_in_time) + @callback def update_sun_position(self, utc_point_in_time): """Calculate the position of the sun.""" - from astral import Astral - - self.solar_azimuth = Astral().solar_azimuth( - utc_point_in_time, - self.location.latitude, - self.location.longitude) - - self.solar_elevation = Astral().solar_elevation( - utc_point_in_time, - self.location.latitude, - self.location.longitude) + self.solar_azimuth = self.location.solar_azimuth(utc_point_in_time) + self.solar_elevation = self.location.solar_elevation(utc_point_in_time) + @callback def point_in_time_listener(self, now): """Run when the state of the sun has changed.""" + self.update_sun_position(now) self.update_as_of(now) - self.schedule_update_ha_state() + self.hass.async_add_job(self.async_update_ha_state()) # Schedule next update at next_change+1 second so sun state has changed - track_point_in_utc_time( + async_track_point_in_utc_time( self.hass, self.point_in_time_listener, self.next_change + timedelta(seconds=1)) + @callback def timer_update(self, time): """Needed to update solar elevation and azimuth.""" self.update_sun_position(time) - self.schedule_update_ha_state() + self.hass.async_add_job(self.async_update_ha_state()) diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index 2052ffc4c15..daa4d1f8cd1 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -11,18 +11,18 @@ import logging import voluptuous as vol from homeassistant.components.light import is_on, turn_on -from homeassistant.components.sun import next_setting, next_rising from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.const import CONF_NAME, CONF_PLATFORM from homeassistant.helpers.event import track_time_change +from homeassistant.helpers.sun import get_astral_event_date from homeassistant.util.color import ( color_temperature_to_rgb, color_RGB_to_xy, color_temperature_kelvin_to_mired) from homeassistant.util.dt import now as dt_now import homeassistant.helpers.config_validation as cv -DEPENDENCIES = ['sun', 'light'] -SUN = "sun.sun" +DEPENDENCIES = ['light'] + _LOGGER = logging.getLogger(__name__) CONF_LIGHTS = 'lights' @@ -159,8 +159,7 @@ class FluxSwitch(SwitchDevice): """Update all the lights using flux.""" if now is None: now = dt_now() - sunset = next_setting(self.hass, SUN).replace( - day=now.day, month=now.month, year=now.year) + sunset = get_astral_event_date(self.hass, 'sunset', now.date()) start_time = self.find_start_time(now) stop_time = now.replace( hour=self._stop_time.hour, minute=self._stop_time.minute, @@ -221,6 +220,5 @@ class FluxSwitch(SwitchDevice): hour=self._start_time.hour, minute=self._start_time.minute, second=0) else: - sunrise = next_rising(self.hass, SUN).replace( - day=now.day, month=now.month, year=now.year) + sunrise = get_astral_event_date(self.hass, 'sunrise', now.date()) return sunrise diff --git a/homeassistant/components/switch/rpi_pfio.py b/homeassistant/components/switch/rpi_pfio.py new file mode 100644 index 00000000000..6e50725b564 --- /dev/null +++ b/homeassistant/components/switch/rpi_pfio.py @@ -0,0 +1,87 @@ +""" +Allows to configure a switch using the PiFace Digital I/O module on a RPi. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.rpi_pfio/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.switch import PLATFORM_SCHEMA +import homeassistant.components.rpi_pfio as rpi_pfio +from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.helpers.entity import ToggleEntity +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['rpi_pfio'] + +ATTR_INVERT_LOGIC = 'invert_logic' +ATTR_NAME = 'name' +CONF_PORTS = 'ports' + +DEFAULT_INVERT_LOGIC = False + +PORT_SCHEMA = vol.Schema({ + vol.Optional(ATTR_NAME, default=None): cv.string, + vol.Optional(ATTR_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_PORTS, default={}): vol.Schema({ + cv.positive_int: PORT_SCHEMA + }) +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the PiFace Digital Output devices.""" + switches = [] + ports = config.get(CONF_PORTS) + for port, port_entity in ports.items(): + name = port_entity[ATTR_NAME] + invert_logic = port_entity[ATTR_INVERT_LOGIC] + + switches.append(RPiPFIOSwitch(port, name, invert_logic)) + add_devices(switches) + + +class RPiPFIOSwitch(ToggleEntity): + """Representation of a PiFace Digital Output.""" + + def __init__(self, port, name, invert_logic): + """Initialize the pin.""" + self._port = port + self._name = name or DEVICE_DEFAULT_NAME + self._invert_logic = invert_logic + self._state = False + rpi_pfio.write_output(self._port, 1 if self._invert_logic else 0) + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + def turn_on(self): + """Turn the device on.""" + rpi_pfio.write_output(self._port, 0 if self._invert_logic else 1) + self._state = True + self.schedule_update_ha_state() + + def turn_off(self): + """Turn the device off.""" + rpi_pfio.write_output(self._port, 1 if self._invert_logic else 0) + self._state = False + self.schedule_update_ha_state() diff --git a/homeassistant/components/switch/wink.py b/homeassistant/components/switch/wink.py index 6783f2201c1..b5feac5fc43 100644 --- a/homeassistant/components/switch/wink.py +++ b/homeassistant/components/switch/wink.py @@ -4,6 +4,7 @@ Support for Wink switches. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.wink/ """ +import asyncio from homeassistant.components.wink import WinkDevice, DOMAIN from homeassistant.helpers.entity import ToggleEntity @@ -40,6 +41,11 @@ class WinkToggleDevice(WinkDevice, ToggleEntity): """Initialize the Wink device.""" super().__init__(wink, hass) + @asyncio.coroutine + def async_added_to_hass(self): + """Callback when entity is added to hass.""" + self.hass.data[DOMAIN]['entities']['switch'].append(self) + @property def is_on(self): """Return true if device is on.""" diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 200c4227f4d..235217d1942 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -1,45 +1,186 @@ -"""Component to receive telegram messages.""" -import asyncio -import logging +""" +Component to send and receive Telegram messages. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/telegram_bot/ +""" +import asyncio +import io +from functools import partial +from ipaddress import ip_network +import logging +import os + +import requests import voluptuous as vol +from homeassistant.components.notify import ( + ATTR_MESSAGE, ATTR_TITLE, ATTR_DATA) +from homeassistant.config import load_yaml_config_file +from homeassistant.const import ( + CONF_PLATFORM, CONF_API_KEY, CONF_TIMEOUT, ATTR_LATITUDE, ATTR_LONGITUDE) import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_PLATFORM, CONF_API_KEY -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import discovery, config_per_platform from homeassistant.setup import async_prepare_setup_platform DOMAIN = 'telegram_bot' +REQUIREMENTS = ['python-telegram-bot==5.3.1'] _LOGGER = logging.getLogger(__name__) EVENT_TELEGRAM_COMMAND = 'telegram_command' EVENT_TELEGRAM_TEXT = 'telegram_text' +EVENT_TELEGRAM_CALLBACK = 'telegram_callback' +PARSER_MD = 'markdown' +PARSER_HTML = 'html' +ATTR_TEXT = 'text' ATTR_COMMAND = 'command' ATTR_USER_ID = 'user_id' ATTR_ARGS = 'args' +ATTR_MSG = 'message' +ATTR_CHAT_INSTANCE = 'chat_instance' +ATTR_CHAT_ID = 'chat_id' +ATTR_MSGID = 'id' ATTR_FROM_FIRST = 'from_first' ATTR_FROM_LAST = 'from_last' -ATTR_TEXT = 'text' - +ATTR_SHOW_ALERT = 'show_alert' +ATTR_MESSAGEID = 'message_id' +ATTR_PARSER = 'parse_mode' +ATTR_DISABLE_NOTIF = 'disable_notification' +ATTR_DISABLE_WEB_PREV = 'disable_web_page_preview' +ATTR_REPLY_TO_MSGID = 'reply_to_message_id' +ATTR_REPLYMARKUP = 'reply_markup' +ATTR_CALLBACK_QUERY = 'callback_query' +ATTR_CALLBACK_QUERY_ID = 'callback_query_id' +ATTR_TARGET = 'target' +ATTR_KEYBOARD = 'keyboard' +ATTR_KEYBOARD_INLINE = 'inline_keyboard' +ATTR_URL = 'url' +ATTR_FILE = 'file' +ATTR_CAPTION = 'caption' +ATTR_USERNAME = 'username' +ATTR_PASSWORD = 'password' CONF_ALLOWED_CHAT_IDS = 'allowed_chat_ids' +CONF_TRUSTED_NETWORKS = 'trusted_networks' +DEFAULT_TRUSTED_NETWORKS = [ + ip_network('149.154.167.197/32'), + ip_network('149.154.167.198/31'), + ip_network('149.154.167.200/29'), + ip_network('149.154.167.208/28'), + ip_network('149.154.167.224/29'), + ip_network('149.154.167.232/31') +] -PLATFORM_SCHEMA = vol.Schema({ - vol.Required(CONF_PLATFORM): cv.string, - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_ALLOWED_CHAT_IDS): - vol.All(cv.ensure_list, [cv.positive_int]) +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_PLATFORM): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_ALLOWED_CHAT_IDS): + vol.All(cv.ensure_list, [cv.positive_int]), + vol.Optional(ATTR_PARSER, default=PARSER_MD): cv.string, + vol.Optional(CONF_TRUSTED_NETWORKS, default=DEFAULT_TRUSTED_NETWORKS): + vol.All(cv.ensure_list, [ip_network]) + }) }, extra=vol.ALLOW_EXTRA) +BASE_SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_TARGET): vol.All(cv.ensure_list, [cv.positive_int]), + vol.Optional(ATTR_PARSER): cv.string, + vol.Optional(ATTR_DISABLE_NOTIF): cv.boolean, + vol.Optional(ATTR_DISABLE_WEB_PREV): cv.boolean, + vol.Optional(ATTR_KEYBOARD): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_KEYBOARD_INLINE): cv.ensure_list, +}, extra=vol.ALLOW_EXTRA) +SERVICE_SEND_MESSAGE = 'send_message' +SERVICE_SCHEMA_SEND_MESSAGE = BASE_SERVICE_SCHEMA.extend({ + vol.Required(ATTR_MESSAGE): cv.template, + vol.Optional(ATTR_TITLE): cv.template, +}) +SERVICE_SEND_PHOTO = 'send_photo' +SERVICE_SEND_DOCUMENT = 'send_document' +SERVICE_SCHEMA_SEND_FILE = BASE_SERVICE_SCHEMA.extend({ + vol.Optional(ATTR_URL): cv.string, + vol.Optional(ATTR_FILE): cv.string, + vol.Optional(ATTR_CAPTION): cv.string, + vol.Optional(ATTR_USERNAME): cv.string, + vol.Optional(ATTR_PASSWORD): cv.string, +}) +SERVICE_SEND_LOCATION = 'send_location' +SERVICE_SCHEMA_SEND_LOCATION = BASE_SERVICE_SCHEMA.extend({ + vol.Required(ATTR_LONGITUDE): float, + vol.Required(ATTR_LATITUDE): float, +}) +SERVICE_EDIT_MESSAGE = 'edit_message' +SERVICE_SCHEMA_EDIT_MESSAGE = SERVICE_SCHEMA_SEND_MESSAGE.extend({ + vol.Required(ATTR_MESSAGEID): vol.Any(cv.positive_int, cv.string), + vol.Required(ATTR_CHAT_ID): cv.positive_int, +}) +SERVICE_EDIT_CAPTION = 'edit_caption' +SERVICE_SCHEMA_EDIT_CAPTION = vol.Schema({ + vol.Required(ATTR_MESSAGEID): vol.Any(cv.positive_int, cv.string), + vol.Required(ATTR_CHAT_ID): cv.positive_int, + vol.Required(ATTR_CAPTION): cv.string, + vol.Optional(ATTR_KEYBOARD_INLINE): cv.ensure_list, +}, extra=vol.ALLOW_EXTRA) +SERVICE_EDIT_REPLYMARKUP = 'edit_replymarkup' +SERVICE_SCHEMA_EDIT_REPLYMARKUP = vol.Schema({ + vol.Required(ATTR_MESSAGEID): vol.Any(cv.positive_int, cv.string), + vol.Required(ATTR_CHAT_ID): cv.positive_int, + vol.Required(ATTR_KEYBOARD_INLINE): cv.ensure_list, +}, extra=vol.ALLOW_EXTRA) +SERVICE_ANSWER_CALLBACK_QUERY = 'answer_callback_query' +SERVICE_SCHEMA_ANSWER_CALLBACK_QUERY = vol.Schema({ + vol.Required(ATTR_MESSAGE): cv.template, + vol.Required(ATTR_CALLBACK_QUERY_ID): cv.positive_int, + vol.Optional(ATTR_SHOW_ALERT): cv.boolean, +}, extra=vol.ALLOW_EXTRA) + +SERVICE_MAP = { + SERVICE_SEND_MESSAGE: SERVICE_SCHEMA_SEND_MESSAGE, + SERVICE_SEND_PHOTO: SERVICE_SCHEMA_SEND_FILE, + SERVICE_SEND_DOCUMENT: SERVICE_SCHEMA_SEND_FILE, + SERVICE_SEND_LOCATION: SERVICE_SCHEMA_SEND_LOCATION, + SERVICE_EDIT_MESSAGE: SERVICE_SCHEMA_EDIT_MESSAGE, + SERVICE_EDIT_CAPTION: SERVICE_SCHEMA_EDIT_CAPTION, + SERVICE_EDIT_REPLYMARKUP: SERVICE_SCHEMA_EDIT_REPLYMARKUP, + SERVICE_ANSWER_CALLBACK_QUERY: SERVICE_SCHEMA_ANSWER_CALLBACK_QUERY, +} + + +def load_data(url=None, file=None, username=None, password=None): + """Load photo/document into ByteIO/File container from a source.""" + try: + if url is not None: + # Load photo from URL + if username is not None and password is not None: + req = requests.get(url, auth=(username, password), timeout=15) + else: + req = requests.get(url, timeout=15) + return io.BytesIO(req.content) + + elif file is not None: + # Load photo from file + return open(file, "rb") + else: + _LOGGER.warning("Can't load photo. No photo found in params!") + + except OSError as error: + _LOGGER.error("Can't load photo into ByteIO: %s", error) + + return None + @asyncio.coroutine def async_setup(hass, config): - """Set up the telegram bot component.""" + """Set up the Telegram bot component.""" + conf = config[DOMAIN] + descriptions = yield from hass.loop.run_in_executor( + None, load_yaml_config_file, + os.path.join(os.path.dirname(__file__), 'services.yaml')) + @asyncio.coroutine def async_setup_platform(p_type, p_config=None, discovery_info=None): - """Set up a telegram bot platform.""" + """Set up a Telegram bot platform.""" platform = yield from async_prepare_setup_platform( hass, config, DOMAIN, p_type) @@ -48,20 +189,10 @@ def async_setup(hass, config): return _LOGGER.info("Setting up %s.%s", DOMAIN, p_type) - try: - if hasattr(platform, 'async_setup_platform'): - notify_service = yield from \ - platform.async_setup_platform(hass, p_config, - discovery_info) - elif hasattr(platform, 'setup_platform'): - notify_service = yield from hass.loop.run_in_executor( - None, platform.setup_platform, hass, p_config, - discovery_info) - else: - raise HomeAssistantError("Invalid Telegram bot platform") - - if notify_service is None: + receiver_service = yield from \ + platform.async_setup_platform(hass, p_config, discovery_info) + if receiver_service is None: _LOGGER.error( "Failed to initialize Telegram bot %s", p_type) return @@ -70,22 +201,275 @@ def async_setup(hass, config): _LOGGER.exception('Error setting up platform %s', p_type) return - setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config - in config_per_platform(config, DOMAIN)] + notify_service = TelegramNotificationService( + hass, + p_config.get(CONF_API_KEY), + p_config.get(CONF_ALLOWED_CHAT_IDS), + p_config.get(ATTR_PARSER) + ) - if setup_tasks: - yield from asyncio.wait(setup_tasks, loop=hass.loop) + @asyncio.coroutine + def async_send_telegram_message(service): + """Handle sending Telegram Bot message service calls.""" + def _render_template_attr(data, attribute): + attribute_templ = data.get(attribute) + if attribute_templ: + attribute_templ.hass = hass + data[attribute] = attribute_templ.async_render() - @asyncio.coroutine - def async_platform_discovered(platform, info): - """Handle the loading of a platform.""" - yield from async_setup_platform(platform, discovery_info=info) + msgtype = service.service + kwargs = dict(service.data) + _render_template_attr(kwargs, ATTR_MESSAGE) + _render_template_attr(kwargs, ATTR_TITLE) + _LOGGER.debug('NEW telegram_message "%s": %s', msgtype, kwargs) - discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered) + if msgtype == SERVICE_SEND_MESSAGE: + yield from hass.async_add_job( + partial(notify_service.send_message, **kwargs)) + elif msgtype == SERVICE_SEND_PHOTO: + yield from hass.async_add_job( + partial(notify_service.send_file, True, **kwargs)) + elif msgtype == SERVICE_SEND_DOCUMENT: + yield from hass.async_add_job( + partial(notify_service.send_file, False, **kwargs)) + elif msgtype == SERVICE_SEND_LOCATION: + yield from hass.async_add_job( + partial(notify_service.send_location, **kwargs)) + elif msgtype == SERVICE_ANSWER_CALLBACK_QUERY: + yield from hass.async_add_job( + partial(notify_service.answer_callback_query, **kwargs)) + else: + yield from hass.async_add_job( + partial(notify_service.edit_message, msgtype, **kwargs)) + + # Register notification services + for service_notif, schema in SERVICE_MAP.items(): + hass.services.async_register( + DOMAIN, service_notif, async_send_telegram_message, + descriptions.get(service_notif), schema=schema) + + return True + + yield from async_setup_platform(conf.get(CONF_PLATFORM), conf) return True +class TelegramNotificationService: + """Implement the notification services for the Telegram Bot domain.""" + + def __init__(self, hass, api_key, allowed_chat_ids, parser): + """Initialize the service.""" + from telegram import Bot + from telegram.parsemode import ParseMode + + self.allowed_chat_ids = allowed_chat_ids + self._default_user = self.allowed_chat_ids[0] + self._last_message_id = {user: None for user in self.allowed_chat_ids} + self._parsers = {PARSER_HTML: ParseMode.HTML, + PARSER_MD: ParseMode.MARKDOWN} + self._parse_mode = self._parsers.get(parser) + self.bot = Bot(token=api_key) + self.hass = hass + + def _get_msg_ids(self, msg_data, chat_id): + """Get the message id to edit. + + This can be one of (message_id, inline_message_id) from a msg dict, + returning a tuple. + **You can use 'last' as message_id** to edit + the last sended message in the chat_id. + """ + message_id = inline_message_id = None + if ATTR_MESSAGEID in msg_data: + message_id = msg_data[ATTR_MESSAGEID] + if (isinstance(message_id, str) and (message_id == 'last') and + (self._last_message_id[chat_id] is not None)): + message_id = self._last_message_id[chat_id] + else: + inline_message_id = msg_data['inline_message_id'] + return message_id, inline_message_id + + def _get_target_chat_ids(self, target): + """Validate chat_id targets or return default target (fist defined). + + :param target: optional list of strings or ints (['12234'] or [12234]) + :return list of chat_id targets (integers) + """ + if target is not None: + if isinstance(target, int): + if target in self.allowed_chat_ids: + return [target] + _LOGGER.warning('BAD TARGET "%s", using default: %s', + target, self._default_user) + else: + try: + chat_ids = [int(t) for t in target + if int(t) in self.allowed_chat_ids] + if len(chat_ids) > 0: + return chat_ids + _LOGGER.warning('ALL BAD TARGETS: "%s"', target) + except (ValueError, TypeError): + _LOGGER.warning('BAD TARGET DATA "%s", using default: %s', + target, self._default_user) + return [self._default_user] + + def _get_msg_kwargs(self, data): + """Get parameters in message data kwargs.""" + def _make_row_of_kb(row_keyboard): + """Make a list of InlineKeyboardButtons from a list of tuples. + + :param row_keyboard: [(text_b1, data_callback_b1), + (text_b2, data_callback_b2), ...] + """ + from telegram import InlineKeyboardButton + if isinstance(row_keyboard, str): + return [InlineKeyboardButton( + key.strip()[1:].upper(), callback_data=key) + for key in row_keyboard.split(",")] + elif isinstance(row_keyboard, list): + return [InlineKeyboardButton( + text_btn, callback_data=data_btn) + for text_btn, data_btn in row_keyboard] + else: + raise ValueError(str(row_keyboard)) + + # Defaults + params = { + ATTR_PARSER: self._parse_mode, + ATTR_DISABLE_NOTIF: False, + ATTR_DISABLE_WEB_PREV: None, + ATTR_REPLY_TO_MSGID: None, + ATTR_REPLYMARKUP: None, + CONF_TIMEOUT: None + } + if data is not None: + if ATTR_PARSER in data: + params[ATTR_PARSER] = self._parsers.get( + data[ATTR_PARSER], self._parse_mode) + if CONF_TIMEOUT in data: + params[CONF_TIMEOUT] = data[CONF_TIMEOUT] + if ATTR_DISABLE_NOTIF in data: + params[ATTR_DISABLE_NOTIF] = data[ATTR_DISABLE_NOTIF] + if ATTR_DISABLE_WEB_PREV in data: + params[ATTR_DISABLE_WEB_PREV] = data[ATTR_DISABLE_WEB_PREV] + if ATTR_REPLY_TO_MSGID in data: + params[ATTR_REPLY_TO_MSGID] = data[ATTR_REPLY_TO_MSGID] + # Keyboards: + if ATTR_KEYBOARD in data: + from telegram import ReplyKeyboardMarkup + keys = data.get(ATTR_KEYBOARD) + keys = keys if isinstance(keys, list) else [keys] + params[ATTR_REPLYMARKUP] = ReplyKeyboardMarkup( + [[key.strip() for key in row.split(",")] for row in keys]) + elif ATTR_KEYBOARD_INLINE in data: + from telegram import InlineKeyboardMarkup + keys = data.get(ATTR_KEYBOARD_INLINE) + keys = keys if isinstance(keys, list) else [keys] + params[ATTR_REPLYMARKUP] = InlineKeyboardMarkup( + [_make_row_of_kb(row) for row in keys]) + return params + + def _send_msg(self, func_send, msg_error, *args_rep, **kwargs_rep): + """Send one message.""" + from telegram.error import TelegramError + try: + out = func_send(*args_rep, **kwargs_rep) + if not isinstance(out, bool) and hasattr(out, ATTR_MESSAGEID): + chat_id = out.chat_id + self._last_message_id[chat_id] = out[ATTR_MESSAGEID] + _LOGGER.debug('LAST MSG ID: %s (from chat_id %s)', + self._last_message_id, chat_id) + elif not isinstance(out, bool): + _LOGGER.warning('UPDATE LAST MSG??: out_type:%s, out=%s', + type(out), out) + return out + except TelegramError: + _LOGGER.exception(msg_error) + + def send_message(self, message="", target=None, **kwargs): + """Send a message to one or multiple pre-allowed chat_ids.""" + title = kwargs.get(ATTR_TITLE) + text = '{}\n{}'.format(title, message) if title else message + params = self._get_msg_kwargs(kwargs) + for chat_id in self._get_target_chat_ids(target): + _LOGGER.debug('send_message in chat_id %s with params: %s', + chat_id, params) + self._send_msg(self.bot.sendMessage, + "Error sending message", + chat_id, text, **params) + + def edit_message(self, type_edit, chat_id=None, **kwargs): + """Edit a previously sent message.""" + chat_id = self._get_target_chat_ids(chat_id)[0] + message_id, inline_message_id = self._get_msg_ids(kwargs, chat_id) + params = self._get_msg_kwargs(kwargs) + _LOGGER.debug('edit_message %s in chat_id %s with params: %s', + message_id or inline_message_id, chat_id, params) + if type_edit == SERVICE_EDIT_MESSAGE: + message = kwargs.get(ATTR_MESSAGE) + title = kwargs.get(ATTR_TITLE) + text = '{}\n{}'.format(title, message) if title else message + _LOGGER.debug('editing message w/id %s.', + message_id or inline_message_id) + return self._send_msg(self.bot.editMessageText, + "Error editing text message", + text, chat_id=chat_id, message_id=message_id, + inline_message_id=inline_message_id, + **params) + elif type_edit == SERVICE_EDIT_CAPTION: + func_send = self.bot.editMessageCaption + params[ATTR_CAPTION] = kwargs.get(ATTR_CAPTION) + else: + func_send = self.bot.editMessageReplyMarkup + return self._send_msg(func_send, + "Error editing message attributes", + chat_id=chat_id, message_id=message_id, + inline_message_id=inline_message_id, + **params) + + def answer_callback_query(self, message, callback_query_id, + show_alert=False, **kwargs): + """Answer a callback originated with a press in an inline keyboard.""" + params = self._get_msg_kwargs(kwargs) + _LOGGER.debug('answer_callback_query w/callback_id %s: %s, alert: %s.', + callback_query_id, message, show_alert) + self._send_msg(self.bot.answerCallbackQuery, + "Error sending answer callback query", + callback_query_id, + text=message, show_alert=show_alert, **params) + + def send_file(self, is_photo=True, target=None, **kwargs): + """Send a photo or a document.""" + file = load_data( + url=kwargs.get(ATTR_URL), + file=kwargs.get(ATTR_FILE), + username=kwargs.get(ATTR_USERNAME), + password=kwargs.get(ATTR_PASSWORD), + ) + params = self._get_msg_kwargs(kwargs) + caption = kwargs.get(ATTR_CAPTION) + func_send = self.bot.sendPhoto if is_photo else self.bot.sendDocument + for chat_id in self._get_target_chat_ids(target): + _LOGGER.debug('send file %s to chat_id %s. Caption: %s.', + file, chat_id, caption) + self._send_msg(func_send, "Error sending file", + chat_id, file, caption=caption, **params) + + def send_location(self, latitude, longitude, target=None, **kwargs): + """Send a location.""" + latitude = float(latitude) + longitude = float(longitude) + params = self._get_msg_kwargs(kwargs) + for chat_id in self._get_target_chat_ids(target): + _LOGGER.debug('send location %s/%s to chat_id %s.', + latitude, longitude, chat_id) + self._send_msg(self.bot.sendLocation, + "Error sending location", + chat_id=chat_id, + latitude=latitude, longitude=longitude, **params) + + class BaseTelegramBotEntity: """The base class for the telegram bot.""" @@ -94,32 +478,56 @@ class BaseTelegramBotEntity: self.allowed_chat_ids = allowed_chat_ids self.hass = hass + def _get_message_data(self, msg_data): + if (not msg_data or + ('text' not in msg_data and 'data' not in msg_data) or + 'from' not in msg_data or + msg_data['from'].get('id') not in self.allowed_chat_ids): + # Message is not correct. + _LOGGER.error("Incoming message does not have required data (%s)", + msg_data) + return None + + return { + ATTR_USER_ID: msg_data['from']['id'], + ATTR_FROM_FIRST: msg_data['from']['first_name'], + ATTR_FROM_LAST: msg_data['from']['last_name'] + } + def process_message(self, data): """Check for basic message rules and fire an event if message is ok.""" - data = data.get('message') + if ATTR_MSG in data: + event = EVENT_TELEGRAM_COMMAND + data = data.get(ATTR_MSG) + event_data = self._get_message_data(data) + if event_data is None: + return False - if (not data or - 'from' not in data or - 'text' not in data or - data['from'].get('id') not in self.allowed_chat_ids): - _LOGGER.error("Incoming message does not have required data") - return False + if data[ATTR_TEXT][0] == '/': + pieces = data[ATTR_TEXT].split(' ') + event_data[ATTR_COMMAND] = pieces[0] + event_data[ATTR_ARGS] = pieces[1:] + else: + event_data[ATTR_TEXT] = data[ATTR_TEXT] + event = EVENT_TELEGRAM_TEXT - event = EVENT_TELEGRAM_COMMAND - event_data = { - ATTR_USER_ID: data['from']['id'], - ATTR_FROM_FIRST: data['from'].get('first_name', 'N/A'), - ATTR_FROM_LAST: data['from'].get('last_name', 'N/A')} + self.hass.bus.async_fire(event, event_data) + return True + elif ATTR_CALLBACK_QUERY in data: + event = EVENT_TELEGRAM_CALLBACK + data = data.get(ATTR_CALLBACK_QUERY) + event_data = self._get_message_data(data) + if event_data is None: + return False - if data['text'][0] == '/': - pieces = data['text'].split(' ') - event_data[ATTR_COMMAND] = pieces[0] - event_data[ATTR_ARGS] = pieces[1:] + event_data[ATTR_DATA] = data[ATTR_DATA] + event_data[ATTR_MSG] = data[ATTR_MSG] + event_data[ATTR_CHAT_INSTANCE] = data[ATTR_CHAT_INSTANCE] + event_data[ATTR_MSGID] = data[ATTR_MSGID] + self.hass.bus.async_fire(event, event_data) + return True else: - event_data[ATTR_TEXT] = data['text'] - event = EVENT_TELEGRAM_TEXT - - self.hass.bus.async_fire(event, event_data) - - return True + # Some other thing... + _LOGGER.warning('SOME OTHER THING RECEIVED --> "%s"', data) + return False diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index 8ae0a07a480..161c4e356a2 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -11,19 +11,15 @@ import logging import async_timeout from aiohttp.client_exceptions import ClientError -from homeassistant.components.telegram_bot import CONF_ALLOWED_CHAT_IDS, \ - BaseTelegramBotEntity, PLATFORM_SCHEMA -from homeassistant.const import EVENT_HOMEASSISTANT_START, \ - EVENT_HOMEASSISTANT_STOP, CONF_API_KEY +from homeassistant.components.telegram_bot import ( + CONF_ALLOWED_CHAT_IDS, BaseTelegramBotEntity) +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_API_KEY) from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['python-telegram-bot==5.3.1'] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA - @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml new file mode 100644 index 00000000000..4ce932d5f41 --- /dev/null +++ b/homeassistant/components/telegram_bot/services.yaml @@ -0,0 +1,227 @@ +send_message: + description: Send a notification + + fields: + message: + description: Message body of the notification. + example: The garage door has been open for 10 minutes. + + title: + description: Optional title for your notification. Will be composed as '%title\n%message' + example: 'Your Garage Door Friend' + + target: + description: An array of pre-authorized chat_ids to send the notification to. If not present, first allowed chat_id is the default. + example: '[12345, 67890] or 12345' + + parse_mode: + description: "Parser for the message text: `html` or `markdown`." + example: 'html' + + disable_notification: + description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. + example: true + + disable_web_page_preview: + description: Disables link previews for links in the message. + example: true + + keyboard: + description: List of rows of commands, comma-separated, to make a custom keyboard. + example: '["/command1, /command2", "/command3"]' + + inline_keyboard: + description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data. + example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + +send_photo: + description: Send a photo + + fields: + url: + description: Remote path to an image. + example: 'http://example.org/path/to/the/image.png' + + file: + description: Local path to an image. + example: '/path/to/the/image.png' + + caption: + description: The title of the image. + example: 'My image' + + username: + description: Username for a URL which require HTTP basic authentication. + example: myuser + + password: + description: Password for a URL which require HTTP basic authentication. + example: myuser_pwd + + target: + description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. + example: '[12345, 67890] or 12345' + + disable_notification: + description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. + example: true + + keyboard: + description: List of rows of commands, comma-separated, to make a custom keyboard. + example: '["/command1, /command2", "/command3"]' + + inline_keyboard: + description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data. + example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + +send_document: + description: Send a document + + fields: + url: + description: Remote path to a document. + example: 'http://example.org/path/to/the/document.odf' + + file: + description: Local path to a document. + example: '/tmp/whatever.odf' + + caption: + description: The title of the document. + example: Document Title xy + + username: + description: Username for a URL which require HTTP basic authentication. + example: myuser + + password: + description: Password for a URL which require HTTP basic authentication. + example: myuser_pwd + + target: + description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. + example: '[12345, 67890] or 12345' + + disable_notification: + description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. + example: true + + keyboard: + description: List of rows of commands, comma-separated, to make a custom keyboard. + example: '["/command1, /command2", "/command3"]' + + inline_keyboard: + description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data. + example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + +send_location: + description: Send a location + + fields: + latitude: + description: The latitude to send. + example: -15.123 + + longitude: + description: The longitude to send. + example: 38.123 + + target: + description: An array of pre-authorized chat_ids to send the location to. If not present, first allowed chat_id is the default. + example: '[12345, 67890] or 12345' + + disable_notification: + description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. + example: true + + keyboard: + description: List of rows of commands, comma-separated, to make a custom keyboard. + example: '["/command1, /command2", "/command3"]' + + inline_keyboard: + description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data. + example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + +edit_message: + description: Edit a previusly sent message. + + fields: + message_id: + description: id of the message to edit. + example: '{{ trigger.event.data.message.message_id }}' + + chat_id: + description: The chat_id where to edit the message. + example: 12345 + + message: + description: Message body of the notification. + example: The garage door has been open for 10 minutes. + + title: + description: Optional title for your notification. Will be composed as '%title\n%message' + example: 'Your Garage Door Friend' + + parse_mode: + description: "Parser for the message text: `html` or `markdown`." + example: 'html' + + disable_web_page_preview: + description: Disables link previews for links in the message. + example: true + + inline_keyboard: + description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data. + example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + +edit_caption: + description: Edit the caption of a previusly sent message. + + fields: + message_id: + description: id of the message to edit. + example: '{{ trigger.event.data.message.message_id }}' + + chat_id: + description: The chat_id where to edit the caption. + example: 12345 + + caption: + description: Message body of the notification. + example: The garage door has been open for 10 minutes. + + inline_keyboard: + description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data. + example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + +edit_replymarkup: + description: Edit the inline keyboard of a previusly sent message. + + fields: + message_id: + description: id of the message to edit. + example: '{{ trigger.event.data.message.message_id }}' + + chat_id: + description: The chat_id where to edit the reply_markup. + example: 12345 + + inline_keyboard: + description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data. + example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + +answer_callback_query: + description: Respond to a callback query originated by clicking on an online keyboard button. The answer will be displayed to the user as a notification at the top of the chat screen or as an alert. + + fields: + message: + description: Unformatted text message body of the notification. + example: "OK, I'm listening" + + callback_query_id: + description: Unique id of the callback response. + example: '{{ trigger.event.data.id }}' + + show_alert: + description: Show a permanent notification. + example: true diff --git a/homeassistant/components/telegram_bot/webhooks.py b/homeassistant/components/telegram_bot/webhooks.py index d647fab490b..690340fc378 100644 --- a/homeassistant/components/telegram_bot/webhooks.py +++ b/homeassistant/components/telegram_bot/webhooks.py @@ -5,60 +5,52 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/telegram_bot.webhooks/ """ import asyncio +import datetime as dt import logging -from ipaddress import ip_network -import voluptuous as vol - -from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED) -import homeassistant.helpers.config_validation as cv from homeassistant.components.http import HomeAssistantView -from homeassistant.components.telegram_bot import CONF_ALLOWED_CHAT_IDS, \ - BaseTelegramBotEntity, PLATFORM_SCHEMA -from homeassistant.const import CONF_API_KEY from homeassistant.components.http.util import get_real_ip +from homeassistant.components.telegram_bot import ( + CONF_ALLOWED_CHAT_IDS, CONF_TRUSTED_NETWORKS, BaseTelegramBotEntity) +from homeassistant.const import ( + CONF_API_KEY, EVENT_HOMEASSISTANT_STOP, + HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED) DEPENDENCIES = ['http'] -REQUIREMENTS = ['python-telegram-bot==5.3.1'] _LOGGER = logging.getLogger(__name__) TELEGRAM_HANDLER_URL = '/api/telegram_webhooks' REMOVE_HANDLER_URL = '' -CONF_TRUSTED_NETWORKS = 'trusted_networks' -DEFAULT_TRUSTED_NETWORKS = [ - ip_network('149.154.167.197/32'), - ip_network('149.154.167.198/31'), - ip_network('149.154.167.200/29'), - ip_network('149.154.167.208/28'), - ip_network('149.154.167.224/29'), - ip_network('149.154.167.232/31') -] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_TRUSTED_NETWORKS, default=DEFAULT_TRUSTED_NETWORKS): - vol.All(cv.ensure_list, [ip_network]) -}) - - -def setup_platform(hass, config, async_add_devices, discovery_info=None): +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the Telegram webhooks platform.""" import telegram bot = telegram.Bot(config[CONF_API_KEY]) - current_status = bot.getWebhookInfo() - handler_url = '{0}{1}'.format( - hass.config.api.base_url, TELEGRAM_HANDLER_URL) + current_status = yield from hass.async_add_job(bot.getWebhookInfo) + + # Some logging of Bot current status: + last_error_date = getattr(current_status, 'last_error_date', None) + if (last_error_date is not None) and (isinstance(last_error_date, int)): + last_error_date = dt.datetime.fromtimestamp(last_error_date) + _LOGGER.info("telegram webhook last_error_date: %s. Status: %s", + last_error_date, current_status) + else: + _LOGGER.debug("telegram webhook Status: %s", current_status) + handler_url = '{0}{1}'.format(hass.config.api.base_url, + TELEGRAM_HANDLER_URL) if current_status and current_status['url'] != handler_url: - if bot.setWebhook(handler_url): + result = yield from hass.async_add_job(bot.setWebhook, handler_url) + if result: _LOGGER.info("Set new telegram webhook %s", handler_url) else: _LOGGER.error("Set telegram webhook failed %s", handler_url) return False - hass.bus.listen_once( + hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, lambda event: bot.setWebhook(REMOVE_HANDLER_URL)) hass.http.register_view(BotPushReceiver( diff --git a/homeassistant/components/tradfri.py b/homeassistant/components/tradfri.py index e1ef0f5fabd..cd83f81afd1 100644 --- a/homeassistant/components/tradfri.py +++ b/homeassistant/components/tradfri.py @@ -17,16 +17,22 @@ from homeassistant.const import CONF_HOST, CONF_API_KEY from homeassistant.loader import get_component from homeassistant.components.discovery import SERVICE_IKEA_TRADFRI +REQUIREMENTS = ['pytradfri==1.1'] + DOMAIN = 'tradfri' CONFIG_FILE = 'tradfri.conf' KEY_CONFIG = 'tradfri_configuring' KEY_GATEWAY = 'tradfri_gateway' -REQUIREMENTS = ['pytradfri==1.1'] +KEY_TRADFRI_GROUPS = 'tradfri_allow_tradfri_groups' +CONF_ALLOW_TRADFRI_GROUPS = 'allow_tradfri_groups' +DEFAULT_ALLOW_TRADFRI_GROUPS = True CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Inclusive(CONF_HOST, 'gateway'): cv.string, vol.Inclusive(CONF_API_KEY, 'gateway'): cv.string, + vol.Optional(CONF_ALLOW_TRADFRI_GROUPS, + default=DEFAULT_ALLOW_TRADFRI_GROUPS): cv.boolean, }) }, extra=vol.ALLOW_EXTRA) @@ -47,7 +53,8 @@ def request_configuration(hass, config, host): def configuration_callback(callback_data): """Handle the submitted configuration.""" res = yield from _setup_gateway(hass, config, host, - callback_data.get('key')) + callback_data.get('key'), + DEFAULT_ALLOW_TRADFRI_GROUPS) if not res: hass.async_add_job(configurator.notify_errors, instance, "Unable to connect.") @@ -77,6 +84,7 @@ def async_setup(hass, config): conf = config.get(DOMAIN, {}) host = conf.get(CONF_HOST) key = conf.get(CONF_API_KEY) + allow_tradfri_groups = conf.get(CONF_ALLOW_TRADFRI_GROUPS) @asyncio.coroutine def gateway_discovered(service, info): @@ -85,7 +93,8 @@ def async_setup(hass, config): host = info['host'] if host in keys: - yield from _setup_gateway(hass, config, host, keys[host]['key']) + yield from _setup_gateway(hass, config, host, keys[host]['key'], + allow_tradfri_groups) else: hass.async_add_job(request_configuration, hass, config, host) @@ -94,11 +103,12 @@ def async_setup(hass, config): if host is None: return True - return (yield from _setup_gateway(hass, config, host, key)) + return (yield from _setup_gateway(hass, config, host, key, + allow_tradfri_groups)) @asyncio.coroutine -def _setup_gateway(hass, hass_config, host, key): +def _setup_gateway(hass, hass_config, host, key, allow_tradfri_groups): """Create a gateway.""" from pytradfri import cli_api_factory, Gateway, RequestError, retry_timeout @@ -112,6 +122,10 @@ def _setup_gateway(hass, hass_config, host, key): hass.data.setdefault(KEY_GATEWAY, {}) gateways = hass.data[KEY_GATEWAY] + hass.data.setdefault(KEY_TRADFRI_GROUPS, {}) + tradfri_groups = hass.data[KEY_TRADFRI_GROUPS] + tradfri_groups[gateway_id] = allow_tradfri_groups + # Check if already set up if gateway_id in gateways: return True diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py index 8eb381a0b85..001105374d7 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera.py @@ -20,7 +20,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyvera==0.2.30'] +REQUIREMENTS = ['pyvera==0.2.31'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index 466236573c8..6566a20814b 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/developers/websocket_api/ """ import asyncio +from contextlib import suppress from functools import partial import json import logging @@ -30,6 +31,8 @@ DOMAIN = 'websocket_api' URL = '/api/websocket' DEPENDENCIES = 'http', +MAX_PENDING_MSG = 512 + ERR_ID_REUSE = 1 ERR_INVALID_FORMAT = 2 ERR_NOT_FOUND = 3 @@ -199,18 +202,20 @@ class WebsocketAPIView(HomeAssistantView): def get(self, request): """Handle an incoming websocket connection.""" # pylint: disable=no-self-use - return ActiveConnection(request.app['hass'], request).handle() + return ActiveConnection(request.app['hass']).handle(request) class ActiveConnection: """Handle an active websocket client connection.""" - def __init__(self, hass, request): + def __init__(self, hass): """Initialize an active connection.""" self.hass = hass - self.request = request self.wsock = None self.event_listeners = {} + self.to_write = asyncio.Queue(maxsize=MAX_PENDING_MSG, loop=hass.loop) + self._handle_task = None + self._writer_task = None def debug(self, message1, message2=''): """Print a debug message.""" @@ -220,61 +225,87 @@ class ActiveConnection: """Print an error message.""" _LOGGER.error("WS %s: %s %s", id(self.wsock), message1, message2) - def send_message(self, message): - """Send messages. + @asyncio.coroutine + def _writer(self): + """Write outgoing messages.""" + # Exceptions if Socket disconnected or cancelled by connection handler + with suppress(RuntimeError, asyncio.CancelledError): + while not self.wsock.closed: + message = yield from self.to_write.get() + if message is None: + break + self.debug("Sending", message) + yield from self.wsock.send_json(message, dumps=JSON_DUMP) - Returns a coroutine object. + @callback + def send_message_outside(self, message): + """Send a message to the client outside of the main task. + + Closes connection if the client is not reading the messages. + + Async friendly. """ - self.debug("Sending", message) - return self.wsock.send_json(message, dumps=JSON_DUMP) + try: + self.to_write.put_nowait(message) + except asyncio.QueueFull: + self.log_error("Client exceeded max pending messages [2]:", + MAX_PENDING_MSG) + self.cancel() + + @callback + def cancel(self): + """Cancel the connection.""" + self._handle_task.cancel() + self._writer_task.cancel() @asyncio.coroutine - def handle(self): + def handle(self, request): """Handle the websocket connection.""" wsock = self.wsock = web.WebSocketResponse() - yield from wsock.prepare(self.request) - - # Set up to cancel this connection when Home Assistant shuts down - socket_task = asyncio.Task.current_task(loop=self.hass.loop) - - @callback - def cancel_connection(event): - """Cancel this connection.""" - socket_task.cancel() - - unsub_stop = self.hass.bus.async_listen( - EVENT_HOMEASSISTANT_STOP, cancel_connection) - + yield from wsock.prepare(request) self.debug("Connected") + # Get a reference to current task so we can cancel our connection + self._handle_task = asyncio.Task.current_task(loop=self.hass.loop) + + @callback + def handle_hass_stop(event): + """Cancel this connection.""" + self.cancel() + + unsub_stop = self.hass.bus.async_listen( + EVENT_HOMEASSISTANT_STOP, handle_hass_stop) + self._writer_task = self.hass.async_add_job(self._writer()) + final_message = None msg = None authenticated = False try: - if self.request[KEY_AUTHENTICATED]: + if request[KEY_AUTHENTICATED]: authenticated = True else: - yield from self.send_message(auth_required_message()) + yield from self.wsock.send_json(auth_required_message()) msg = yield from wsock.receive_json() msg = AUTH_MESSAGE_SCHEMA(msg) - if validate_password(self.request, msg['api_password']): + if validate_password(request, msg['api_password']): authenticated = True else: self.debug("Invalid password") - yield from self.send_message( + yield from self.wsock.send_json( auth_invalid_message('Invalid password')) if not authenticated: - yield from process_wrong_login(self.request) + yield from process_wrong_login(request) return wsock - yield from self.send_message(auth_ok_message()) + yield from self.wsock.send_json(auth_ok_message()) + + # ---------- AUTH PHASE OVER ---------- msg = yield from wsock.receive_json() - last_id = 0 while msg: @@ -283,13 +314,13 @@ class ActiveConnection: cur_id = msg['id'] if cur_id <= last_id: - yield from self.send_message(error_message( + self.to_write.put_nowait(error_message( cur_id, ERR_ID_REUSE, 'Identifier values have to increase.')) else: handler_name = 'handle_{}'.format(msg['type']) - yield from getattr(self, handler_name)(msg) + getattr(self, handler_name)(msg) last_id = cur_id msg = yield from wsock.receive_json() @@ -304,7 +335,7 @@ class ActiveConnection: self.log_error(error_msg) if not authenticated: - yield from self.send_message(auth_invalid_message(error_msg)) + final_message = auth_invalid_message(error_msg) else: if isinstance(msg, dict): @@ -312,8 +343,8 @@ class ActiveConnection: else: iden = None - yield from self.send_message(error_message( - iden, ERR_INVALID_FORMAT, error_msg)) + final_message = error_message( + iden, ERR_INVALID_FORMAT, error_msg) except TypeError as err: if wsock.closed: @@ -327,10 +358,16 @@ class ActiveConnection: if value: msg += ': {}'.format(value) self.log_error(msg) + self._writer_task.cancel() except asyncio.CancelledError: self.debug("Connection cancelled by server") + except asyncio.QueueFull: + self.log_error("Client exceeded max pending messages [1]:", + MAX_PENDING_MSG) + self._writer_task.cancel() + except Exception: # pylint: disable=broad-except error = "Unexpected error inside websocket API. " if msg is not None: @@ -343,14 +380,25 @@ class ActiveConnection: for unsub in self.event_listeners.values(): unsub() + try: + if final_message is not None: + self.to_write.put_nowait(final_message) + self.to_write.put_nowait(None) + # Make sure all error messages are written before closing + yield from self._writer_task + except asyncio.QueueFull: + self._writer_task.cancel() + yield from wsock.close() self.debug("Closed connection") return wsock - @asyncio.coroutine def handle_subscribe_events(self, msg): - """Handle subscribe events command.""" + """Handle subscribe events command. + + Async friendly. + """ msg = SUBSCRIBE_EVENTS_MESSAGE_SCHEMA(msg) @asyncio.coroutine @@ -359,21 +407,17 @@ class ActiveConnection: if event.event_type == EVENT_TIME_CHANGED: return - try: - yield from self.send_message(event_message(msg['id'], event)) - except RuntimeError: - # Socket has been closed. - pass + self.send_message_outside(event_message(msg['id'], event)) self.event_listeners[msg['id']] = self.hass.bus.async_listen( msg['event_type'], forward_events) - return self.send_message(result_message(msg['id'])) + self.to_write.put_nowait(result_message(msg['id'])) def handle_unsubscribe_events(self, msg): """Handle unsubscribe events command. - Returns a coroutine object. + Async friendly. """ msg = UNSUBSCRIBE_EVENTS_MESSAGE_SCHEMA(msg) @@ -381,13 +425,12 @@ class ActiveConnection: if subscription in self.event_listeners: self.event_listeners.pop(subscription)() - return self.send_message(result_message(msg['id'])) + self.to_write.put_nowait(result_message(msg['id'])) else: - return self.send_message(error_message( + self.to_write.put_nowait(error_message( msg['id'], ERR_NOT_FOUND, 'Subscription not found.')) - @asyncio.coroutine def handle_call_service(self, msg): """Handle call service command. @@ -400,57 +443,53 @@ class ActiveConnection: """Call a service and fire complete message.""" yield from self.hass.services.async_call( msg['domain'], msg['service'], msg['service_data'], True) - try: - yield from self.send_message(result_message(msg['id'])) - except RuntimeError: - # Socket has been closed. - pass + self.send_message_outside(result_message(msg['id'])) self.hass.async_add_job(call_service_helper(msg)) def handle_get_states(self, msg): """Handle get states command. - Returns a coroutine object. + Async friendly. """ msg = GET_STATES_MESSAGE_SCHEMA(msg) - return self.send_message(result_message( + self.to_write.put_nowait(result_message( msg['id'], self.hass.states.async_all())) def handle_get_services(self, msg): """Handle get services command. - Returns a coroutine object. + Async friendly. """ msg = GET_SERVICES_MESSAGE_SCHEMA(msg) - return self.send_message(result_message( + self.to_write.put_nowait(result_message( msg['id'], self.hass.services.async_services())) def handle_get_config(self, msg): """Handle get config command. - Returns a coroutine object. + Async friendly. """ msg = GET_CONFIG_MESSAGE_SCHEMA(msg) - return self.send_message(result_message( + self.to_write.put_nowait(result_message( msg['id'], self.hass.config.as_dict())) def handle_get_panels(self, msg): """Handle get panels command. - Returns a coroutine object. + Async friendly. """ msg = GET_PANELS_MESSAGE_SCHEMA(msg) - return self.send_message(result_message( + self.to_write.put_nowait(result_message( msg['id'], self.hass.data[frontend.DATA_PANELS])) def handle_ping(self, msg): """Handle ping command. - Returns a coroutine object. + Async friendly. """ - return self.send_message(pong_message(msg['id'])) + self.to_write.put_nowait(pong_message(msg['id'])) diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index c22e32b51d4..c33e3b14502 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -75,6 +75,7 @@ def setup(hass, config): hass.data[DOMAIN] = {} hass.data[DOMAIN]['entities'] = [] hass.data[DOMAIN]['unique_ids'] = [] + hass.data[DOMAIN]['entities'] = {} user_agent = config[DOMAIN].get(CONF_USER_AGENT) @@ -154,10 +155,11 @@ def setup(hass, config): def force_update(call): """Force all devices to poll the Wink API.""" _LOGGER.info("Refreshing Wink states from API") - for entity in hass.data[DOMAIN]['entities']: + for entity_list in hass.data[DOMAIN]['entities'].values(): # Throttle the calls to Wink API - time.sleep(1) - entity.schedule_update_ha_state(True) + for entity in entity_list: + time.sleep(1) + entity.schedule_update_ha_state(True) hass.services.register(DOMAIN, SERVICE_REFRESH_STATES, force_update) def pull_new_devices(call): @@ -169,6 +171,7 @@ def setup(hass, config): # Load components for the devices in Wink that we support for component in WINK_COMPONENTS: + hass.data[DOMAIN]['entities'][component] = [] discovery.load_platform(hass, component, DOMAIN, {}, config) return True @@ -183,7 +186,6 @@ class WinkDevice(Entity): self.wink = wink hass.data[DOMAIN]['pubnub'].add_subscription( self.wink.pubnub_channel, self._pubnub_update) - hass.data[DOMAIN]['entities'].append(self) hass.data[DOMAIN]['unique_ids'].append(self.wink.object_id() + self.wink.name()) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index d6077763464..79067c0d2ef 100755 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -28,6 +28,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send) from homeassistant.components.frontend import register_built_in_panel +from . import api from . import const from .const import DOMAIN from .node_entity import ZWaveBaseEntity, ZWaveNodeEntity @@ -35,7 +36,7 @@ from . import workaround from .discovery_schemas import DISCOVERY_SCHEMAS from .util import check_node_schema, check_value_schema, node_name -REQUIREMENTS = ['pydispatcher==2.0.5'] +REQUIREMENTS = ['pydispatcher==2.0.5', 'python_openzwave==0.4.0.31'] _LOGGER = logging.getLogger(__name__) @@ -53,6 +54,7 @@ CONF_REFRESH_DELAY = 'delay' CONF_DEVICE_CONFIG = 'device_config' CONF_DEVICE_CONFIG_GLOB = 'device_config_glob' CONF_DEVICE_CONFIG_DOMAIN = 'device_config_domain' +CONF_NETWORK_KEY = 'network_key' ATTR_POWER = 'power_consumption' @@ -66,6 +68,8 @@ DEFAULT_CONF_REFRESH_VALUE = False DEFAULT_CONF_REFRESH_DELAY = 5 DATA_ZWAVE_DICT = 'zwave_devices' +OZW_LOG_FILENAME = 'OZW_Log.txt' +URL_API_OZW_LOG = '/api/zwave/ozwlog' ZWAVE_NETWORK = 'zwave_network' RENAME_NODE_SCHEMA = vol.Schema({ @@ -75,8 +79,8 @@ RENAME_NODE_SCHEMA = vol.Schema({ SET_CONFIG_PARAMETER_SCHEMA = vol.Schema({ vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Coerce(int), - vol.Required(const.ATTR_CONFIG_VALUE): vol.Coerce(int), - vol.Optional(const.ATTR_CONFIG_SIZE): vol.Coerce(int) + vol.Required(const.ATTR_CONFIG_VALUE): vol.Any(vol.Coerce(int), cv.string), + vol.Optional(const.ATTR_CONFIG_SIZE, default=2): vol.Coerce(int) }) PRINT_CONFIG_PARAMETER_SCHEMA = vol.Schema({ vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), @@ -122,6 +126,7 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_AUTOHEAL, default=DEFAULT_CONF_AUTOHEAL): cv.boolean, vol.Optional(CONF_CONFIG_PATH): cv.string, + vol.Optional(CONF_NETWORK_KEY): cv.string, vol.Optional(CONF_DEVICE_CONFIG, default={}): vol.Schema({cv.entity_id: DEVICE_CONFIG_SCHEMA_ENTRY}), vol.Optional(CONF_DEVICE_CONFIG_GLOB, default={}): @@ -221,22 +226,12 @@ def setup(hass, config): descriptions = conf_util.load_yaml_config_file( os.path.join(os.path.dirname(__file__), 'services.yaml')) - try: - import libopenzwave - except ImportError: - _LOGGER.error("You are missing required dependency Python Open " - "Z-Wave. Please follow instructions at: " - "https://home-assistant.io/components/zwave/") - return False from pydispatch import dispatcher # pylint: disable=import-error from openzwave.option import ZWaveOption from openzwave.network import ZWaveNetwork from openzwave.group import ZWaveGroup - default_zwave_config_path = os.path.join(os.path.dirname( - libopenzwave.__file__), 'config') - # Load configuration use_debug = config[DOMAIN].get(CONF_DEBUG) autoheal = config[DOMAIN].get(CONF_AUTOHEAL) @@ -249,10 +244,13 @@ def setup(hass, config): options = ZWaveOption( config[DOMAIN].get(CONF_USB_STICK_PATH), user_path=hass.config.config_dir, - config_path=config[DOMAIN].get( - CONF_CONFIG_PATH, default_zwave_config_path)) + config_path=config[DOMAIN].get(CONF_CONFIG_PATH)) options.set_console_output(use_debug) + + if CONF_NETWORK_KEY in config[DOMAIN]: + options.addOption("NetworkKey", config[DOMAIN][CONF_NETWORK_KEY]) + options.lock() network = hass.data[ZWAVE_NETWORK] = ZWaveNetwork(options, autostart=False) @@ -394,7 +392,7 @@ def setup(hass, config): def rename_node(service): """Rename a node.""" node_id = service.data.get(const.ATTR_NODE_ID) - node = hass.data[ZWAVE_NETWORK].nodes[node_id] + node = network.nodes[node_id] name = service.data.get(const.ATTR_NAME) node.name = name _LOGGER.info( @@ -418,28 +416,28 @@ def setup(hass, config): node = network.nodes[node_id] param = service.data.get(const.ATTR_CONFIG_PARAMETER) selection = service.data.get(const.ATTR_CONFIG_VALUE) - size = service.data.get(const.ATTR_CONFIG_SIZE, 2) - i = 0 + size = service.data.get(const.ATTR_CONFIG_SIZE) for value in ( node.get_values(class_id=const.COMMAND_CLASS_CONFIGURATION) .values()): - if value.index == param and value.type == const.TYPE_LIST: - _LOGGER.debug("Values for parameter %s: %s", param, - value.data_items) - i = len(value.data_items) - 1 - if i == 0: - node.set_config_param(param, selection, size) - else: - if selection > i: - _LOGGER.info("Config parameter selection does not exist! " - "Please check zwcfg_[home_id].xml in " - "your homeassistant config directory. " - "Available selections are 0 to %s", i) + if value.index != param: + continue + if value.type in [const.TYPE_LIST, const.TYPE_BOOL]: + value.data = selection + _LOGGER.info("Setting config list parameter %s on Node %s " + "with selection %s", param, node_id, + selection) return - node.set_config_param(param, selection, size) - _LOGGER.info("Setting config parameter %s on Node %s " - "with selection %s and size=%s", param, node_id, - selection, size) + else: + value.data = int(selection) + _LOGGER.info("Setting config parameter %s on Node %s " + "with selection %s", param, node_id, + selection) + return + node.set_config_param(param, selection, size) + _LOGGER.info("Setting unknown config parameter %s on Node %s " + "with selection %s", param, node_id, + selection) def print_config_parameter(service): """Print a config parameter from a node.""" @@ -512,7 +510,7 @@ def setup(hass, config): # to be ready. for i in range(const.NETWORK_READY_WAIT_SECS): _LOGGER.debug( - "network state: %d %s", hass.data[ZWAVE_NETWORK].state, + "network state: %d %s", network.state, network.state_str) if network.state >= network.STATE_AWAKED: _LOGGER.info("Z-Wave ready after %d seconds", i) @@ -618,6 +616,11 @@ def setup(hass, config): if 'frontend' in hass.config.components: register_built_in_panel(hass, 'zwave', 'Z-Wave', 'mdi:nfc') + hass.http.register_view(api.ZWaveNodeGroupView) + hass.http.register_view(api.ZWaveNodeConfigView) + hass.http.register_view(api.ZWaveUserCodeView) + hass.http.register_static_path( + URL_API_OZW_LOG, hass.config.path(OZW_LOG_FILENAME), False) return True diff --git a/homeassistant/components/zwave/api.py b/homeassistant/components/zwave/api.py new file mode 100644 index 00000000000..9e3066f91c5 --- /dev/null +++ b/homeassistant/components/zwave/api.py @@ -0,0 +1,95 @@ +"""API class to give info to the Z-Wave panel.""" + +import logging +import homeassistant.core as ha +from homeassistant.components.http import HomeAssistantView +from homeassistant.const import HTTP_NOT_FOUND +from . import const + +_LOGGER = logging.getLogger(__name__) + +ZWAVE_NETWORK = 'zwave_network' + + +class ZWaveNodeGroupView(HomeAssistantView): + """View to return the nodes group configuration.""" + + url = r"/api/zwave/groups/{node_id:\d+}" + name = "api:zwave:groups" + + @ha.callback + def get(self, request, node_id): + """Retrieve groups of node.""" + nodeid = int(node_id) + hass = request.app['hass'] + network = hass.data.get(ZWAVE_NETWORK) + node = network.nodes.get(nodeid) + if node is None: + return self.json_message('Node not found', HTTP_NOT_FOUND) + groupdata = node.groups + groups = {} + for key, value in groupdata.items(): + groups[key] = {'associations': value.associations, + 'association_instances': + value.associations_instances, + 'label': value.label, + 'max_associations': value.max_associations} + return self.json(groups) + + +class ZWaveNodeConfigView(HomeAssistantView): + """View to return the nodes configuration options.""" + + url = r"/api/zwave/config/{node_id:\d+}" + name = "api:zwave:config" + + @ha.callback + def get(self, request, node_id): + """Retrieve configurations of node.""" + nodeid = int(node_id) + hass = request.app['hass'] + network = hass.data.get(ZWAVE_NETWORK) + node = network.nodes.get(nodeid) + if node is None: + return self.json_message('Node not found', HTTP_NOT_FOUND) + config = {} + for value in ( + node.get_values(class_id=const.COMMAND_CLASS_CONFIGURATION) + .values()): + config[value.index] = {'label': value.label, + 'type': value.type, + 'help': value.help, + 'data_items': value.data_items, + 'data': value.data, + 'max': value.max, + 'min': value.min} + return self.json(config) + + +class ZWaveUserCodeView(HomeAssistantView): + """View to return the nodes usercode configuration.""" + + url = r"/api/zwave/usercodes/{node_id:\d+}" + name = "api:zwave:usercodes" + + @ha.callback + def get(self, request, node_id): + """Retrieve usercodes of node.""" + nodeid = int(node_id) + hass = request.app['hass'] + network = hass.data.get(ZWAVE_NETWORK) + node = network.nodes.get(nodeid) + if node is None: + return self.json_message('Node not found', HTTP_NOT_FOUND) + usercodes = {} + if not node.has_command_class(const.COMMAND_CLASS_USER_CODE): + return self.json(usercodes) + for value in ( + node.get_values(class_id=const.COMMAND_CLASS_USER_CODE) + .values()): + if value.genre != const.GENRE_USER: + continue + usercodes[value.index] = {'code': value.data, + 'label': value.label, + 'length': len(value.data)} + return self.json(usercodes) diff --git a/homeassistant/components/zwave/services.yaml b/homeassistant/components/zwave/services.yaml index 166bd4e6f81..feacf8229aa 100644 --- a/homeassistant/components/zwave/services.yaml +++ b/homeassistant/components/zwave/services.yaml @@ -47,9 +47,9 @@ set_config_parameter: parameter: description: Parameter number to set (integer). value: - description: Value to set on parameter. (integer). + description: Value to set for parameter. (String value for list and bool parameters, integer for others). size: - description: (Optional) The size of the value. Defaults to 2. + description: (Optional) Set the size of the parameter value. Only needed if no parameters are available. print_config_parameter: description: Prints a Z-Wave node config parameter value to log. diff --git a/homeassistant/config.py b/homeassistant/config.py index 39a6d3304ac..1d86953c0cc 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -98,6 +98,7 @@ tts: platform: google group: !include groups.yaml +automation: !include automations.yaml """ @@ -158,10 +159,13 @@ def create_default_config(config_dir, detect_location=True): """ from homeassistant.components.config.group import ( CONFIG_PATH as GROUP_CONFIG_PATH) + from homeassistant.components.config.automation import ( + CONFIG_PATH as AUTOMATION_CONFIG_PATH) config_path = os.path.join(config_dir, YAML_CONFIG_FILE) version_path = os.path.join(config_dir, VERSION_FILE) group_yaml_path = os.path.join(config_dir, GROUP_CONFIG_PATH) + automation_yaml_path = os.path.join(config_dir, AUTOMATION_CONFIG_PATH) info = {attr: default for attr, default, _, _ in DEFAULT_CORE_CONFIG} @@ -203,6 +207,9 @@ def create_default_config(config_dir, detect_location=True): with open(group_yaml_path, 'w'): pass + with open(automation_yaml_path, 'wt') as fil: + fil.write('[]') + return config_path except IOError: diff --git a/homeassistant/const.py b/homeassistant/const.py index 57d4da7bc19..65a8eb070c6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,8 +1,8 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 44 -PATCH_VERSION = '2' +MINOR_VERSION = 45 +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2) @@ -172,6 +172,7 @@ EVENT_PLATFORM_DISCOVERED = 'platform_discovered' EVENT_COMPONENT_LOADED = 'component_loaded' EVENT_SERVICE_REGISTERED = 'service_registered' EVENT_SERVICE_REMOVED = 'service_removed' +EVENT_LOGBOOK_ENTRY = 'logbook_entry' # #### STATES #### STATE_ON = 'on' diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index bbfb19f7806..a0753b0f766 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -7,8 +7,7 @@ import sys from homeassistant.helpers.typing import ConfigType from homeassistant.core import HomeAssistant -from homeassistant.components import ( - zone as zone_cmp, sun as sun_cmp) +from homeassistant.components import zone as zone_cmp from homeassistant.const import ( ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ENTITY_ID, CONF_VALUE_TEMPLATE, CONF_CONDITION, @@ -17,6 +16,7 @@ from homeassistant.const import ( CONF_BELOW, CONF_ABOVE) from homeassistant.exceptions import TemplateError, HomeAssistantError import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.sun import get_astral_event_date import homeassistant.util.dt as dt_util from homeassistant.util.async import run_callback_threadsafe @@ -234,24 +234,34 @@ def state_from_config(config, config_validation=True): def sun(hass, before=None, after=None, before_offset=None, after_offset=None): """Test if current time matches sun requirements.""" - now = dt_util.now().time() + utcnow = dt_util.utcnow() + today = dt_util.as_local(utcnow).date() before_offset = before_offset or timedelta(0) after_offset = after_offset or timedelta(0) - if before == SUN_EVENT_SUNRISE and now > (sun_cmp.next_rising(hass) + - before_offset).time(): + sunrise = get_astral_event_date(hass, 'sunrise', today) + sunset = get_astral_event_date(hass, 'sunset', today) + + if sunrise is None and (before == SUN_EVENT_SUNRISE or + after == SUN_EVENT_SUNRISE): + # There is no sunrise today return False - elif before == SUN_EVENT_SUNSET and now > (sun_cmp.next_setting(hass) + - before_offset).time(): + if sunset is None and (before == SUN_EVENT_SUNSET or + after == SUN_EVENT_SUNSET): + # There is no sunset today return False - if after == SUN_EVENT_SUNRISE and now < (sun_cmp.next_rising(hass) + - after_offset).time(): + if before == SUN_EVENT_SUNRISE and utcnow > sunrise + before_offset: return False - elif after == SUN_EVENT_SUNSET and now < (sun_cmp.next_setting(hass) + - after_offset).time(): + elif before == SUN_EVENT_SUNSET and utcnow > sunset + before_offset: + return False + + if after == SUN_EVENT_SUNRISE and utcnow < sunrise + after_offset: + return False + + elif after == SUN_EVENT_SUNSET and utcnow < sunset + after_offset: return False return True diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 0cdcca42eca..d3ad93d3646 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1,7 +1,7 @@ """Helpers for listening to events.""" import functools as ft -from datetime import timedelta +from homeassistant.helpers.sun import get_astral_event_next from ..core import HomeAssistant, callback from ..const import ( ATTR_NOW, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL) @@ -197,29 +197,20 @@ track_time_interval = threaded_listener_factory(async_track_time_interval) @callback def async_track_sunrise(hass, action, offset=None): """Add a listener that will fire a specified offset from sunrise daily.""" - from homeassistant.components import sun - offset = offset or timedelta() remove = None - def next_rise(): - """Return the next sunrise.""" - next_time = sun.next_rising_utc(hass) + offset - - while next_time < dt_util.utcnow(): - next_time = next_time + timedelta(days=1) - - return next_time - @callback def sunrise_automation_listener(now): """Handle points in time to execute actions.""" nonlocal remove remove = async_track_point_in_utc_time( - hass, sunrise_automation_listener, next_rise()) + hass, sunrise_automation_listener, get_astral_event_next( + hass, 'sunrise', offset=offset)) hass.async_run_job(action) remove = async_track_point_in_utc_time( - hass, sunrise_automation_listener, next_rise()) + hass, sunrise_automation_listener, get_astral_event_next( + hass, 'sunrise', offset=offset)) def remove_listener(): """Remove sunset listener.""" @@ -234,29 +225,20 @@ track_sunrise = threaded_listener_factory(async_track_sunrise) @callback def async_track_sunset(hass, action, offset=None): """Add a listener that will fire a specified offset from sunset daily.""" - from homeassistant.components import sun - offset = offset or timedelta() remove = None - def next_set(): - """Return next sunrise.""" - next_time = sun.next_setting_utc(hass) + offset - - while next_time < dt_util.utcnow(): - next_time = next_time + timedelta(days=1) - - return next_time - @callback def sunset_automation_listener(now): """Handle points in time to execute actions.""" nonlocal remove remove = async_track_point_in_utc_time( - hass, sunset_automation_listener, next_set()) + hass, sunset_automation_listener, get_astral_event_next( + hass, 'sunset', offset=offset)) hass.async_run_job(action) remove = async_track_point_in_utc_time( - hass, sunset_automation_listener, next_set()) + hass, sunset_automation_listener, get_astral_event_next( + hass, 'sunset', offset=offset)) def remove_listener(): """Remove sunset listener.""" diff --git a/homeassistant/helpers/sun.py b/homeassistant/helpers/sun.py new file mode 100644 index 00000000000..157225c9903 --- /dev/null +++ b/homeassistant/helpers/sun.py @@ -0,0 +1,87 @@ +"""Helpers for sun events.""" +import datetime + +from homeassistant.core import callback +from homeassistant.util import dt as dt_util + +DATA_LOCATION_CACHE = 'astral_location_cache' + + +@callback +def get_astral_location(hass): + """Get an astral location for the current hass configuration.""" + from astral import Location + + latitude = hass.config.latitude + longitude = hass.config.longitude + timezone = hass.config.time_zone.zone + elevation = hass.config.elevation + info = ('', '', latitude, longitude, timezone, elevation) + + # Cache astral locations so they aren't recreated with the same args + if DATA_LOCATION_CACHE not in hass.data: + hass.data[DATA_LOCATION_CACHE] = {} + + if info not in hass.data[DATA_LOCATION_CACHE]: + hass.data[DATA_LOCATION_CACHE][info] = Location(info) + + return hass.data[DATA_LOCATION_CACHE][info] + + +@callback +def get_astral_event_next(hass, event, utc_point_in_time=None, offset=None): + """Calculate the next specified solar event.""" + import astral + + location = get_astral_location(hass) + + if offset is None: + offset = datetime.timedelta() + + if utc_point_in_time is None: + utc_point_in_time = dt_util.utcnow() + + mod = -1 + while True: + try: + next_dt = getattr(location, event)( + dt_util.as_local(utc_point_in_time).date() + + datetime.timedelta(days=mod), + local=False) + offset + if next_dt > utc_point_in_time: + return next_dt + except astral.AstralError: + pass + mod += 1 + + +@callback +def get_astral_event_date(hass, event, date=None): + """Calculate the astral event time for the specified date.""" + import astral + + location = get_astral_location(hass) + + if date is None: + date = dt_util.now().date() + + if isinstance(date, datetime.datetime): + date = dt_util.as_local(date).date() + + try: + return getattr(location, event)(date, local=False) + except astral.AstralError: + # Event never occurs for specified date. + return None + + +@callback +def is_up(hass, utc_point_in_time=None): + """Calculate if the sun is currently up.""" + if utc_point_in_time is None: + utc_point_in_time = dt_util.utcnow() + + next_sunrise = get_astral_event_next(hass, 'sunrise', utc_point_in_time) + next_sunset = get_astral_event_next(hass, 'sunset', utc_point_in_time) + + return next_sunrise > next_sunset diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 57fedd7278d..39314f963ae 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,5 +6,6 @@ jinja2>=2.9.5 voluptuous==0.10.5 typing>=3,<4 aiohttp==2.0.7 -async_timeout==1.2.0 +async_timeout==1.2.1 chardet==3.0.2 +astral==1.4 diff --git a/pylintrc b/pylintrc index 4c0b1523078..e94cbffe9f9 100644 --- a/pylintrc +++ b/pylintrc @@ -15,24 +15,25 @@ reports=no # abstract-method - with intro of async there are always methods missing disable= - locally-disabled, - duplicate-code, - cyclic-import, abstract-class-little-used, abstract-class-not-used, - unused-argument, + abstract-method, + cyclic-import, + duplicate-code, global-statement, + locally-disabled, + not-context-manager, redefined-variable-type, + too-few-public-methods, too-many-arguments, too-many-branches, too-many-instance-attributes, + too-many-lines, too-many-locals, too-many-public-methods, too-many-return-statements, too-many-statements, - too-many-lines, - too-few-public-methods, - abstract-method + unused-argument [EXCEPTIONS] overgeneral-exceptions=Exception,HomeAssistantError diff --git a/requirements_all.txt b/requirements_all.txt index ecf96d6653e..c8f8c5aafd9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -7,8 +7,9 @@ jinja2>=2.9.5 voluptuous==0.10.5 typing>=3,<4 aiohttp==2.0.7 -async_timeout==1.2.0 +async_timeout==1.2.1 chardet==3.0.2 +astral==1.4 # homeassistant.components.nuimo_controller --only-binary=all https://github.com/getSenic/nuimo-linux-python/archive/29fc42987f74d8090d0e2382e8f248ff5990b8c9.zip#nuimo==1.0.0 @@ -38,7 +39,7 @@ SoCo==0.12 TwitterAPI==2.4.5 # homeassistant.components.device_tracker.automatic -aioautomatic==0.3.1 +aioautomatic==0.4.0 # homeassistant.components.sensor.dnsip aiodns==1.1.1 @@ -55,7 +56,7 @@ alarmdecoder==0.12.1.0 # homeassistant.components.camera.amcrest # homeassistant.components.sensor.amcrest -amcrest==1.1.9 +amcrest==1.2.0 # homeassistant.components.media_player.anthemav anthemav==1.1.8 @@ -66,13 +67,15 @@ apcaccess==0.0.4 # homeassistant.components.notify.apns apns2==0.1.1 -# homeassistant.components.sun -# homeassistant.components.sensor.moon -astral==1.4 - # homeassistant.components.light.avion # avion==0.6 +# homeassistant.components.axis +axis==7 + +# homeassistant.components.sensor.modem_callerid +basicmodem==0.7 + # homeassistant.components.sensor.linux_battery batinfo==0.4.2 @@ -81,19 +84,19 @@ batinfo==0.4.2 # homeassistant.components.device_tracker.linksys_ap # homeassistant.components.sensor.scrape -beautifulsoup4==4.5.3 +beautifulsoup4==4.6.0 # homeassistant.components.zha bellows==0.2.7 # homeassistant.components.blink -blinkpy==0.5.2 +blinkpy==0.6.0 # homeassistant.components.light.blinksticklight blinkstick==1.1.8 # homeassistant.components.light.blinkt -blinkt==0.1.0 +# blinkt==0.1.0 # homeassistant.components.sensor.bitcoin blockchain==1.3.3 @@ -124,6 +127,9 @@ concord232==0.14 # homeassistant.components.sensor.crimereports crimereports==1.0.0 +# homeassistant.components.datadog +datadog==0.15.0 + # homeassistant.components.sensor.metoffice # homeassistant.components.weather.metoffice datapoint==0.4.3 @@ -132,7 +138,7 @@ datapoint==0.4.3 # decora==0.4 # homeassistant.components.media_player.denonavr -denonavr==0.4.0 +denonavr==0.4.1 # homeassistant.components.media_player.directv directpy==0.1 @@ -157,7 +163,7 @@ dsmr_parser==0.8 # homeassistant.components.dweet # homeassistant.components.sensor.dweet -dweepy==0.2.0 +dweepy==0.3.0 # homeassistant.components.sensor.eliqonline eliqonline==1.0.13 @@ -166,7 +172,7 @@ eliqonline==1.0.13 enocean==0.31 # homeassistant.components.sensor.envirophat -envirophat==0.0.6 +# envirophat==0.0.6 # homeassistant.components.keyboard_remote # evdev==0.6.1 @@ -264,15 +270,9 @@ https://github.com/aparraga/braviarc/archive/0.3.7.zip#braviarc==0.3.7 # homeassistant.components.cover.myq https://github.com/arraylabs/pymyq/archive/v0.0.8.zip#pymyq==0.0.8 -# homeassistant.components.media_player.roku -https://github.com/bah2830/python-roku/archive/3.1.3.zip#roku==3.1.3 - # homeassistant.components.modbus https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b612661.zip#pymodbus==1.2.0 -# homeassistant.components.lutron_caseta -https://github.com/gurumitts/pylutron-caseta/archive/v0.2.6.zip#pylutron-caseta==v0.2.6 - # homeassistant.components.media_player.spotify https://github.com/happyleavesaoc/spotipy/archive/544614f4b1d508201d363e84e871f86c90aa26b2.zip#spotipy==2.4.4 @@ -288,15 +288,6 @@ https://github.com/jamespcole/home-assistant-nzb-clients/archive/616cad591540925 # homeassistant.components.media_player.nad https://github.com/joopert/nad_receiver/archive/0.0.3.zip#nad_receiver==0.0.3 -# homeassistant.components.media_player.russound_rnet -https://github.com/laf/russound/archive/0.1.7.zip#russound==0.1.7 - -# homeassistant.components.media_player.onkyo -https://github.com/miracle2k/onkyo-eiscp/archive/066023aec04770518d494c32fb72eea0ec5c1b7c.zip#onkyo-eiscp==1.0 - -# homeassistant.components.sensor.pocketcasts -https://github.com/molobrakos/python-pocketcasts/archive/9f61ff00c77c7c98ffa0af9dd3540df3dce4a836.zip#python-pocketcasts==0.0.1 - # homeassistant.components.switch.anel_pwrctrl https://github.com/mweinelt/anel-pwrctrl/archive/ed26e8830e28a2bfa4260a9002db23ce3e7e63d7.zip#anel_pwrctrl==0.0.1 @@ -318,9 +309,6 @@ https://github.com/tfriedel/python-lightify/archive/1bb1db0e7bd5b14304d7bb267e23 # homeassistant.components.lutron https://github.com/thecynic/pylutron/archive/v0.1.0.zip#pylutron==0.1.0 -# homeassistant.components.sensor.modem_callerid -https://github.com/vroomfonde1/basicmodem/archive/0.7.zip#basicmodem==0.7 - # homeassistant.components.tado https://github.com/wmalgadey/PyTado/archive/0.1.10.zip#PyTado==0.1.10 @@ -366,7 +354,7 @@ libsoundtouch==0.3.0 liffylights==0.9.4 # homeassistant.components.light.limitlessled -limitlessled==1.0.5 +limitlessled==1.0.8 # homeassistant.components.media_player.liveboxplaytv liveboxplaytv==1.4.9 @@ -397,7 +385,7 @@ mutagen==1.37.0 myusps==1.0.5 # homeassistant.components.discovery -netdisco==1.0.0rc3 +netdisco==1.0.0 # homeassistant.components.sensor.neurio_energy neurio==0.3.1 @@ -411,6 +399,9 @@ oauth2client==4.0.0 # homeassistant.components.climate.oem oemthermostat==1.1 +# homeassistant.components.media_player.onkyo +onkyo-eiscp==1.1 + # homeassistant.components.opencv # opencv-python==3.2.0.6 @@ -441,6 +432,12 @@ pexpect==4.0.1 # homeassistant.components.light.hue phue==0.9 +# homeassistant.components.rpi_pfio +pifacecommon==4.1.2 + +# homeassistant.components.rpi_pfio +pifacedigitalio==3.0.5 + # homeassistant.components.light.piglow piglow==1.2.4 @@ -455,6 +452,9 @@ plexapi==2.0.2 # homeassistant.components.sensor.serial_pm pmsensor==0.4 +# homeassistant.components.sensor.pocketcasts +pocketcasts==0.1 + # homeassistant.components.climate.proliphix proliphix==0.4.1 @@ -527,10 +527,7 @@ pydroid-ipcam==0.8 pyebox==0.1.0 # homeassistant.components.eight_sleep -pyeight==0.0.4 - -# homeassistant.components.notify.html5 -pyelliptic==1.5.7 +pyeight==0.0.5 # homeassistant.components.media_player.emby pyemby==1.2 @@ -554,7 +551,7 @@ pyharmony==1.0.12 pyhik==0.1.2 # homeassistant.components.homematic -pyhomematic==0.1.25 +pyhomematic==0.1.26 # homeassistant.components.sensor.hydroquebec pyhydroquebec==1.1.0 @@ -568,6 +565,9 @@ pyiss==1.0.1 # homeassistant.components.remote.itach pyitachip2ir==0.0.6 +# homeassistant.components.kira +pykira==0.1.1 + # homeassistant.components.sensor.kwb pykwb==0.0.8 @@ -584,6 +584,9 @@ pylitejet==0.1 # homeassistant.components.sensor.loopenergy pyloopenergy==0.0.17 +# homeassistant.components.lutron_caseta +pylutron-caseta==0.2.6 + # homeassistant.components.notify.mailgun pymailgunner==1.4 @@ -674,12 +677,13 @@ python-nmap==0.6.1 # homeassistant.components.notify.pushover python-pushover==0.2 +# homeassistant.components.media_player.roku +python-roku==3.1.3 + # homeassistant.components.sensor.synologydsm python-synology==0.1.0 -# homeassistant.components.notify.telegram -# homeassistant.components.telegram_bot.polling -# homeassistant.components.telegram_bot.webhooks +# homeassistant.components.telegram_bot python-telegram-bot==5.3.1 # homeassistant.components.sensor.twitch @@ -691,6 +695,9 @@ python-vlc==1.1.2 # homeassistant.components.wink python-wink==1.2.4 +# homeassistant.components.zwave +python_openzwave==0.4.0.31 + # homeassistant.components.device_tracker.trackr pytrackr==0.0.5 @@ -704,10 +711,10 @@ pyunifi==2.12 # pyuserinput==0.1.11 # homeassistant.components.vera -pyvera==0.2.30 +pyvera==0.2.31 # homeassistant.components.notify.html5 -pywebpush==0.6.1 +pywebpush==1.0.0 # homeassistant.components.wemo pywemo==0.4.19 @@ -721,8 +728,11 @@ qnapstats==0.2.4 # homeassistant.components.climate.radiotherm radiotherm==1.2 +# homeassistant.components.raspihats +# raspihats==2.2.1 + # homeassistant.components.rflink -rflink==0.0.31 +rflink==0.0.34 # homeassistant.components.ring ring_doorbell==0.1.4 @@ -730,6 +740,9 @@ ring_doorbell==0.1.4 # homeassistant.components.switch.rpi_rf # rpi-rf==0.9.6 +# homeassistant.components.media_player.russound_rnet +russound==0.1.7 + # homeassistant.components.media_player.yamaha rxv==0.4.0 @@ -743,7 +756,7 @@ schiene==0.18 scsgate==0.1.0 # homeassistant.components.notify.sendgrid -sendgrid==4.0.0 +sendgrid==4.1.0 # homeassistant.components.light.sensehat # homeassistant.components.sensor.sensehat @@ -765,7 +778,7 @@ sleekxmpp==1.3.2 sleepyq==0.6 # homeassistant.components.sensor.envirophat -smbus-cffi==0.5.1 +# smbus-cffi==0.5.1 # homeassistant.components.media_player.snapcast snapcast==1.2.2 diff --git a/requirements_docs.txt b/requirements_docs.txt index b147fcca7a7..8e85b302a6b 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,3 @@ -Sphinx==1.5.5 +Sphinx==1.6.1 sphinx-autodoc-typehints==1.2.0 sphinx-autodoc-annotation==1.0.post1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt new file mode 100644 index 00000000000..de96de19214 --- /dev/null +++ b/requirements_test_all.txt @@ -0,0 +1,130 @@ +# Home Assistant test +# linters such as flake8 and pylint should be pinned, as new releases +# make new things fail. Manually update these pins when pulling in a +# new version +flake8==3.3 +pylint==1.6.5 +mypy==0.501 +pydocstyle==1.1.1 +coveralls>=1.1 +pytest>=2.9.2 +pytest-aiohttp>=0.1.3 +pytest-asyncio>=0.5.0 +pytest-cov>=2.3.1 +pytest-timeout>=1.2.0 +pytest-catchlog>=1.2.2 +pytest-sugar>=0.7.1 +requests_mock>=1.0 +mock-open>=1.3.1 +flake8-docstrings==1.0.2 +asynctest>=0.8.0 +freezegun>=0.3.8 + + +# homeassistant.components.notify.html5 +PyJWT==1.4.2 + +# homeassistant.components.media_player.sonos +SoCo==0.12 + +# homeassistant.components.device_tracker.automatic +aioautomatic==0.4.0 + +# homeassistant.components.emulated_hue +# homeassistant.components.http +aiohttp_cors==0.5.3 + +# homeassistant.components.notify.apns +apns2==0.1.1 + +# homeassistant.components.sensor.dsmr +dsmr_parser==0.8 + +# homeassistant.components.climate.honeywell +evohomeclient==0.2.5 + +# homeassistant.components.conversation +fuzzywuzzy==0.15.0 + +# homeassistant.components.tts.google +gTTS-token==1.1.1 + +# homeassistant.components.ffmpeg +ha-ffmpeg==1.5 + +# homeassistant.components.mqtt.server +hbmqtt==0.8 + +# homeassistant.components.binary_sensor.workday +holidays==0.8.1 + +# homeassistant.components.influxdb +# homeassistant.components.sensor.influxdb +influxdb==3.0.0 + +# homeassistant.components.media_player.soundtouch +libsoundtouch==0.3.0 + +# homeassistant.components.sensor.mfi +# homeassistant.components.switch.mfi +mficlient==0.3.0 + +# homeassistant.components.mqtt +paho-mqtt==1.2.3 + +# homeassistant.components.device_tracker.aruba +# homeassistant.components.device_tracker.asuswrt +# homeassistant.components.device_tracker.cisco_ios +# homeassistant.components.media_player.pandora +pexpect==4.0.1 + +# homeassistant.components.pilight +pilight==0.1.1 + +# homeassistant.components.sensor.mhz19 +# homeassistant.components.sensor.serial_pm +pmsensor==0.4 + +# homeassistant.components.zwave +pydispatcher==2.0.5 + +# homeassistant.components.litejet +pylitejet==0.1 + +# homeassistant.components.alarm_control_panel.nx584 +# homeassistant.components.binary_sensor.nx584 +pynx584==0.4 + +# homeassistant.components.sensor.darksky +python-forecastio==1.3.5 + +# homeassistant.components.notify.html5 +pywebpush==1.0.0 + +# homeassistant.components.rflink +rflink==0.0.34 + +# homeassistant.components.ring +ring_doorbell==0.1.4 + +# homeassistant.components.media_player.yamaha +rxv==0.4.0 + +# homeassistant.components.sleepiq +sleepyq==0.6 + +# homeassistant.components.climate.honeywell +somecomfort==0.4.1 + +# homeassistant.components.recorder +# homeassistant.scripts.db_migrator +sqlalchemy==1.1.9 + +# homeassistant.components.statsd +statsd==3.2.1 + +# homeassistant.components.camera.uvc +uvcclient==0.10.0 + +# homeassistant.components.sensor.yahoo_finance +yahoo-finance==1.4.0 diff --git a/script/bootstrap_frontend b/script/bootstrap_frontend index e1d4ef887be..0efe2e3584d 100755 --- a/script/bootstrap_frontend +++ b/script/bootstrap_frontend @@ -1,5 +1,5 @@ #!/bin/sh -# Resolve all frontend dependencies that the application requires to run. +# Resolve all frontend dependencies that the application requires to develop. # Stop on errors set -e diff --git a/script/bootstrap_server b/script/bootstrap_server index 38684f9266c..7929a00fe55 100755 --- a/script/bootstrap_server +++ b/script/bootstrap_server @@ -1,32 +1,10 @@ #!/bin/sh -# Resolve all server dependencies that the application requires to run. +# Resolve all server dependencies that the application requires to develop. # Stop on errors set -e cd "$(dirname "$0")/.." -echo "Installing dependencies..." -# Requirements_all.txt states minimum pip version as 7.0.0 however, -# parameter --only-binary doesn't work with pip < 7.0.0. Causing -# python3 -m pip install -r requirements_all.txt to fail unless pip upgraded. - -if ! python3 -c 'import pkg_resources ; pkg_resources.require(["pip>=7.0.0"])' 2>/dev/null ; then - echo "Upgrading pip..." - python3 -m pip install -U pip -fi -python3 -m pip install -r requirements_all.txt - -REQ_STATUS=$? - -echo "Installing development dependencies..." -python3 -m pip install -r requirements_test.txt - -REQ_DEV_STATUS=$? - -if [ $REQ_DEV_STATUS -eq 0 ] -then - exit $REQ_STATUS -else - exit $REQ_DEV_STATUS -fi +echo "Installing test dependencies..." +python3 -m pip install tox diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 4d08ff349a0..614411fbde2 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -8,6 +8,7 @@ import sys COMMENT_REQUIREMENTS = ( 'RPi.GPIO', + 'raspihats', 'rpi-rf', 'Adafruit_Python_DHT', 'Adafruit_BBIO', @@ -24,7 +25,46 @@ COMMENT_REQUIREMENTS = ( 'python-eq3bt', 'avion', 'decora', - 'face_recognition' + 'face_recognition', + 'blinkt', + 'smbus-cffi', + 'envirophat' +) + +TEST_REQUIREMENTS = ( + 'pydispatch', + 'influxdb', + 'nx584', + 'uvcclient', + 'somecomfort', + 'aioautomatic', + 'SoCo', + 'libsoundtouch', + 'rxv', + 'apns2', + 'sqlalchemy', + 'forecastio', + 'aiohttp_cors', + 'pilight', + 'fuzzywuzzy', + 'rflink', + 'ring_doorbell', + 'sleepyq', + 'statsd', + 'pylitejet', + 'holidays', + 'evohomeclient', + 'pexpect', + 'hbmqtt', + 'paho', + 'dsmr_parser', + 'mficlient', + 'pmsensor', + 'yahoo-finance', + 'ha-ffmpeg', + 'gTTS-token', + 'pywebpush', + 'PyJWT', ) IGNORE_PACKAGES = ( @@ -77,11 +117,10 @@ def comment_requirement(req): def gather_modules(): - """Collect the information and construct the output.""" + """Collect the information.""" reqs = {} errors = [] - output = [] for package in sorted(explore_module('homeassistant.components', True) + explore_module('homeassistant.scripts', True)): @@ -114,10 +153,12 @@ def gather_modules(): print("Make sure you import 3rd party libraries inside methods.") return None - output.append('# Home Assistant core') - output.append('\n') - output.append('\n'.join(core_requirements())) - output.append('\n') + return reqs + + +def generate_requirements_list(reqs): + """Generate a pip file based on requirements.""" + output = [] for pkg, requirements in sorted(reqs.items(), key=lambda item: item[0]): for req in sorted(requirements, key=lambda name: (len(name.split('.')), name)): @@ -127,6 +168,32 @@ def gather_modules(): output.append('\n# {}\n'.format(pkg)) else: output.append('\n{}\n'.format(pkg)) + return ''.join(output) + + +def requirements_all_output(reqs): + """Generate output for requirements_all.""" + output = [] + output.append('# Home Assistant core') + output.append('\n') + output.append('\n'.join(core_requirements())) + output.append('\n') + output.append(generate_requirements_list(reqs)) + + return ''.join(output) + + +def requirements_test_output(reqs): + """Generate output for test_requirements.""" + output = [] + output.append('# Home Assistant test') + output.append('\n') + with open('requirements_test.txt') as fp: + output.append(fp.read()) + output.append('\n') + filtered = {key: value for key, value in reqs.items() + if any(ign in key for ign in TEST_REQUIREMENTS)} + output.append(generate_requirements_list(filtered)) return ''.join(output) @@ -142,6 +209,12 @@ def write_requirements_file(data): req_file.write(data) +def write_test_requirements_file(data): + """Write the modules to the requirements_all.txt.""" + with open('requirements_test_all.txt', 'w+', newline="\n") as req_file: + req_file.write(data) + + def write_constraints_file(data): """Write constraints to a file.""" with open(CONSTRAINT_PATH, 'w+', newline="\n") as req_file: @@ -154,6 +227,12 @@ def validate_requirements_file(data): return data == ''.join(req_file) +def validate_requirements_test_file(data): + """Validate if requirements_all.txt is up to date.""" + with open('requirements_test_all.txt', 'r') as req_file: + return data == ''.join(req_file) + + def validate_constraints_file(data): """Validate if constraints is up to date.""" with open(CONSTRAINT_PATH, 'r') as req_file: @@ -173,22 +252,31 @@ def main(): constraints = gather_constraints() + reqs_file = requirements_all_output(data) + reqs_test_file = requirements_test_output(data) + if sys.argv[-1] == 'validate': - if not validate_requirements_file(data): - print("******* ERROR") - print("requirements_all.txt is not up to date") - print("Please run script/gen_requirements_all.py") - sys.exit(1) + errors = [] + if not validate_requirements_file(reqs_file): + errors.append("requirements_all.txt is not up to date") + + if not validate_requirements_test_file(reqs_test_file): + errors.append("requirements_test_all.txt is not up to date") if not validate_constraints_file(constraints): + errors.append( + "home-assistant/package_constraints.txt is not up to date") + + if errors: print("******* ERROR") - print("home-assistant/package_constraints.txt is not up to date") + print('\n'.join(errors)) print("Please run script/gen_requirements_all.py") sys.exit(1) sys.exit(0) - write_requirements_file(data) + write_requirements_file(reqs_file) + write_test_requirements_file(reqs_test_file) write_constraints_file(constraints) diff --git a/script/test_docker b/script/test_docker index 75b7cddf970..9f3bbb4be07 100755 --- a/script/test_docker +++ b/script/test_docker @@ -1,5 +1,8 @@ #!/bin/sh # Executes the tests with tox in a docker container. +# Every argment is passed to tox to allow running only a subset of tests. +# The following example will only run media_player tests: +# ./test_docker -- tests/components/media_player/ # Stop on errors set -e @@ -10,4 +13,4 @@ docker build -t home-assistant-test -f virtualization/Docker/Dockerfile.dev . docker run --rm \ -v `pwd`/.tox/:/usr/src/app/.tox/ \ -t -i home-assistant-test \ - tox -e py35 + tox -e py36 ${@:2} diff --git a/setup.py b/setup.py index 65d02c3e8c6..2cdcad544fb 100755 --- a/setup.py +++ b/setup.py @@ -23,8 +23,9 @@ REQUIRES = [ 'voluptuous==0.10.5', 'typing>=3,<4', 'aiohttp==2.0.7', - 'async_timeout==1.2.0', - 'chardet==3.0.2' + 'async_timeout==1.2.1', + 'chardet==3.0.2', + 'astral==1.4', ] setup( diff --git a/tests/common.py b/tests/common.py index 1585cb33e23..30bd772a81f 100644 --- a/tests/common.py +++ b/tests/common.py @@ -3,7 +3,6 @@ import asyncio import functools as ft import os import sys -from datetime import timedelta from unittest.mock import patch, MagicMock, Mock from io import StringIO import logging @@ -25,7 +24,7 @@ from homeassistant.const import ( STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED, SERVER_PORT, EVENT_HOMEASSISTANT_CLOSE) -from homeassistant.components import sun, mqtt, recorder +from homeassistant.components import mqtt, recorder from homeassistant.components.http.auth import auth_middleware from homeassistant.components.http.const import ( KEY_USE_X_FORWARDED_FOR, KEY_BANS_ENABLED, KEY_TRUSTED_NETWORKS) @@ -34,7 +33,7 @@ from homeassistant.util.async import ( _TEST_INSTANCE_PORT = SERVER_PORT _LOGGER = logging.getLogger(__name__) -INST_COUNT = 0 +INSTANCES = [] def threadsafe_callback_factory(func): @@ -99,11 +98,10 @@ def get_test_home_assistant(): @asyncio.coroutine def async_test_home_assistant(loop): """Return a Home Assistant object pointing at test config dir.""" - global INST_COUNT - INST_COUNT += 1 loop._thread_ident = threading.get_ident() hass = ha.HomeAssistant(loop) + INSTANCES.append(hass) orig_async_add_job = hass.async_add_job @@ -135,8 +133,7 @@ def async_test_home_assistant(loop): @asyncio.coroutine def mock_async_start(): """Start the mocking.""" - # 1. We only mock time during tests - # 2. We want block_till_done that is called inside stop_track_tasks + # We only mock time during tests and we want to track tasks with patch('homeassistant.core._async_create_timer'), \ patch.object(hass, 'async_stop_track_tasks'): yield from orig_start() @@ -146,8 +143,7 @@ def async_test_home_assistant(loop): @ha.callback def clear_instance(event): """Clear global instance.""" - global INST_COUNT - INST_COUNT -= 1 + INSTANCES.remove(hass) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, clear_instance) @@ -213,20 +209,6 @@ def fire_service_discovered(hass, service, info): }) -def ensure_sun_risen(hass): - """Trigger sun to rise if below horizon.""" - if sun.is_on(hass): - return - fire_time_changed(hass, sun.next_rising_utc(hass) + timedelta(seconds=10)) - - -def ensure_sun_set(hass): - """Trigger sun to set if above horizon.""" - if not sun.is_on(hass): - return - fire_time_changed(hass, sun.next_setting_utc(hass) + timedelta(seconds=10)) - - def load_fixture(filename): """Load a fixture.""" path = os.path.join(os.path.dirname(__file__), 'fixtures', filename) @@ -510,3 +492,38 @@ def mock_restore_cache(hass, states): "Duplicate entity_id? {}".format(states) hass.state = ha.CoreState.starting mock_component(hass, recorder.DOMAIN) + + +class MockDependency: + """Decorator to mock install a dependency.""" + + def __init__(self, root, *args): + """Initialize decorator.""" + self.root = root + self.submodules = args + + def __call__(self, func): + """Apply decorator.""" + from unittest.mock import MagicMock, patch + + def resolve(mock, path): + """Resolve a mock.""" + if not path: + return mock + + return resolve(getattr(mock, path[0]), path[1:]) + + def run_mocked(*args, **kwargs): + """Run with mocked dependencies.""" + base = MagicMock() + to_mock = { + "{}.{}".format(self.root, tom): resolve(base, tom.split('.')) + for tom in self.submodules + } + to_mock[self.root] = base + + with patch.dict('sys.modules', to_mock): + args = list(args) + [base] + func(*args, **kwargs) + + return run_mocked diff --git a/tests/components/automation/test_event.py b/tests/components/automation/test_event.py index a056520a5c9..b4686650057 100644 --- a/tests/components/automation/test_event.py +++ b/tests/components/automation/test_event.py @@ -1,13 +1,11 @@ """The tests for the Event automation.""" -import asyncio import unittest -from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.core import callback, CoreState -from homeassistant.setup import setup_component, async_setup_component +from homeassistant.core import callback +from homeassistant.setup import setup_component import homeassistant.components.automation as automation -from tests.common import get_test_home_assistant, mock_component, mock_service +from tests.common import get_test_home_assistant, mock_component # pylint: disable=invalid-name @@ -94,30 +92,3 @@ class TestAutomationEvent(unittest.TestCase): self.hass.bus.fire('test_event', {'some_attr': 'some_other_value'}) self.hass.block_till_done() self.assertEqual(0, len(self.calls)) - - -@asyncio.coroutine -def test_if_fires_on_event_with_data(hass): - """Test the firing of events with data.""" - calls = mock_service(hass, 'test', 'automation') - hass.state = CoreState.not_running - - res = yield from async_setup_component(hass, automation.DOMAIN, { - automation.DOMAIN: { - 'alias': 'hello', - 'trigger': { - 'platform': 'event', - 'event_type': EVENT_HOMEASSISTANT_START, - }, - 'action': { - 'service': 'test.automation', - } - } - }) - assert res - assert not automation.is_on(hass, 'automation.hello') - assert len(calls) == 0 - - yield from hass.async_start() - assert automation.is_on(hass, 'automation.hello') - assert len(calls) == 1 diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 71f9fb83b65..f67c572ae75 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -33,7 +33,7 @@ class TestAutomation(unittest.TestCase): def test_service_data_not_a_dict(self): """Test service data not dict.""" with assert_setup_component(0): - assert not setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'event', diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index afddaa85b04..cf715bc5e32 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -245,7 +245,7 @@ class TestAutomationState(unittest.TestCase): def test_if_fails_setup_if_to_boolean_value(self): """Test for setup failure for boolean to.""" with assert_setup_component(0): - assert not setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'state', @@ -260,7 +260,7 @@ class TestAutomationState(unittest.TestCase): def test_if_fails_setup_if_from_boolean_value(self): """Test for setup failure for boolean from.""" with assert_setup_component(0): - assert not setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'state', @@ -275,7 +275,7 @@ class TestAutomationState(unittest.TestCase): def test_if_fails_setup_bad_for(self): """Test for setup failure for bad for.""" with assert_setup_component(0): - assert not setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'state', @@ -293,7 +293,7 @@ class TestAutomationState(unittest.TestCase): def test_if_fails_setup_for_without_to(self): """Test for setup failures for missing to.""" with assert_setup_component(0): - assert not setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'state', @@ -333,6 +333,40 @@ class TestAutomationState(unittest.TestCase): self.hass.block_till_done() self.assertEqual(0, len(self.calls)) + def test_if_fires_on_entity_change_with_for_attribute_change(self): + """Test for firing on entity change with for and attribute change.""" + assert setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'state', + 'entity_id': 'test.entity', + 'to': 'world', + 'for': { + 'seconds': 5 + }, + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + utcnow = dt_util.utcnow() + with patch('homeassistant.core.dt_util.utcnow') as mock_utcnow: + mock_utcnow.return_value = utcnow + self.hass.states.set('test.entity', 'world') + self.hass.block_till_done() + mock_utcnow.return_value += timedelta(seconds=4) + fire_time_changed(self.hass, mock_utcnow.return_value) + self.hass.states.set('test.entity', 'world', + attributes={"mock_attr": "attr_change"}) + self.hass.block_till_done() + self.assertEqual(0, len(self.calls)) + mock_utcnow.return_value += timedelta(seconds=4) + fire_time_changed(self.hass, mock_utcnow.return_value) + self.hass.block_till_done() + self.assertEqual(1, len(self.calls)) + def test_if_fires_on_entity_change_with_for(self): """Test for firing on entity change with for.""" assert setup_component(self.hass, automation.DOMAIN, { @@ -393,10 +427,55 @@ class TestAutomationState(unittest.TestCase): self.hass.block_till_done() self.assertEqual(1, len(self.calls)) + def test_if_fires_on_for_condition_attribute_change(self): + """Test for firing if contition is on with attribute change.""" + point1 = dt_util.utcnow() + point2 = point1 + timedelta(seconds=4) + point3 = point1 + timedelta(seconds=8) + with patch('homeassistant.core.dt_util.utcnow') as mock_utcnow: + mock_utcnow.return_value = point1 + self.hass.states.set('test.entity', 'on') + assert setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'condition': 'state', + 'entity_id': 'test.entity', + 'state': 'on', + 'for': { + 'seconds': 5 + }, + }, + 'action': {'service': 'test.automation'}, + } + }) + + # not enough time has passed + self.hass.bus.fire('test_event') + self.hass.block_till_done() + self.assertEqual(0, len(self.calls)) + + # Still not enough time has passed, but an attribute is changed + mock_utcnow.return_value = point2 + self.hass.states.set('test.entity', 'on', + attributes={"mock_attr": "attr_change"}) + self.hass.bus.fire('test_event') + self.hass.block_till_done() + self.assertEqual(0, len(self.calls)) + + # Enough time has now passed + mock_utcnow.return_value = point3 + self.hass.bus.fire('test_event') + self.hass.block_till_done() + self.assertEqual(1, len(self.calls)) + def test_if_fails_setup_for_without_time(self): """Test for setup failure if no time is provided.""" with assert_setup_component(0): - assert not setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'event', @@ -414,7 +493,7 @@ class TestAutomationState(unittest.TestCase): def test_if_fails_setup_for_without_entity(self): """Test for setup failure if no entity is provided.""" with assert_setup_component(0): - assert not setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': {'event_type': 'bla'}, 'condition': { diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index 2341d22d633..ac1d7bc5acf 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -22,7 +22,8 @@ class TestAutomationSun(unittest.TestCase): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() mock_component(self.hass, 'group') - mock_component(self.hass, 'sun') + setup_component(self.hass, sun.DOMAIN, { + sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) self.calls = [] @@ -39,10 +40,6 @@ class TestAutomationSun(unittest.TestCase): def test_sunset_trigger(self): """Test the sunset trigger.""" - self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { - sun.STATE_ATTR_NEXT_SETTING: '2015-09-16T02:00:00Z', - }) - now = datetime(2015, 9, 15, 23, tzinfo=dt_util.UTC) trigger_time = datetime(2015, 9, 16, 2, tzinfo=dt_util.UTC) @@ -78,10 +75,6 @@ class TestAutomationSun(unittest.TestCase): def test_sunrise_trigger(self): """Test the sunrise trigger.""" - self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { - sun.STATE_ATTR_NEXT_RISING: '2015-09-16T14:00:00Z', - }) - now = datetime(2015, 9, 13, 23, tzinfo=dt_util.UTC) trigger_time = datetime(2015, 9, 16, 14, tzinfo=dt_util.UTC) @@ -105,10 +98,6 @@ class TestAutomationSun(unittest.TestCase): def test_sunset_trigger_with_offset(self): """Test the sunset trigger with offset.""" - self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { - sun.STATE_ATTR_NEXT_SETTING: '2015-09-16T02:00:00Z', - }) - now = datetime(2015, 9, 15, 23, tzinfo=dt_util.UTC) trigger_time = datetime(2015, 9, 16, 2, 30, tzinfo=dt_util.UTC) @@ -139,10 +128,6 @@ class TestAutomationSun(unittest.TestCase): def test_sunrise_trigger_with_offset(self): """Test the runrise trigger with offset.""" - self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { - sun.STATE_ATTR_NEXT_RISING: '2015-09-16T14:00:00Z', - }) - now = datetime(2015, 9, 13, 23, tzinfo=dt_util.UTC) trigger_time = datetime(2015, 9, 16, 13, 30, tzinfo=dt_util.UTC) @@ -167,10 +152,6 @@ class TestAutomationSun(unittest.TestCase): def test_if_action_before(self): """Test if action was before.""" - self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { - sun.STATE_ATTR_NEXT_RISING: '2015-09-16T14:00:00Z', - }) - setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -188,14 +169,14 @@ class TestAutomationSun(unittest.TestCase): }) now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) - with patch('homeassistant.util.dt.now', + with patch('homeassistant.util.dt.utcnow', return_value=now): self.hass.bus.fire('test_event') self.hass.block_till_done() self.assertEqual(0, len(self.calls)) now = datetime(2015, 9, 16, 10, tzinfo=dt_util.UTC) - with patch('homeassistant.util.dt.now', + with patch('homeassistant.util.dt.utcnow', return_value=now): self.hass.bus.fire('test_event') self.hass.block_till_done() @@ -203,10 +184,6 @@ class TestAutomationSun(unittest.TestCase): def test_if_action_after(self): """Test if action was after.""" - self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { - sun.STATE_ATTR_NEXT_RISING: '2015-09-16T14:00:00Z', - }) - setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -224,14 +201,14 @@ class TestAutomationSun(unittest.TestCase): }) now = datetime(2015, 9, 16, 13, tzinfo=dt_util.UTC) - with patch('homeassistant.util.dt.now', + with patch('homeassistant.util.dt.utcnow', return_value=now): self.hass.bus.fire('test_event') self.hass.block_till_done() self.assertEqual(0, len(self.calls)) now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) - with patch('homeassistant.util.dt.now', + with patch('homeassistant.util.dt.utcnow', return_value=now): self.hass.bus.fire('test_event') self.hass.block_till_done() @@ -239,10 +216,6 @@ class TestAutomationSun(unittest.TestCase): def test_if_action_before_with_offset(self): """Test if action was before offset.""" - self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { - sun.STATE_ATTR_NEXT_RISING: '2015-09-16T14:00:00Z', - }) - setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -260,15 +233,15 @@ class TestAutomationSun(unittest.TestCase): } }) - now = datetime(2015, 9, 16, 15, 1, tzinfo=dt_util.UTC) - with patch('homeassistant.util.dt.now', + now = datetime(2015, 9, 16, 14, 32, 44, tzinfo=dt_util.UTC) + with patch('homeassistant.util.dt.utcnow', return_value=now): self.hass.bus.fire('test_event') self.hass.block_till_done() self.assertEqual(0, len(self.calls)) - now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) - with patch('homeassistant.util.dt.now', + now = datetime(2015, 9, 16, 14, 32, 43, tzinfo=dt_util.UTC) + with patch('homeassistant.util.dt.utcnow', return_value=now): self.hass.bus.fire('test_event') self.hass.block_till_done() @@ -276,10 +249,6 @@ class TestAutomationSun(unittest.TestCase): def test_if_action_after_with_offset(self): """Test if action was after offset.""" - self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { - sun.STATE_ATTR_NEXT_RISING: '2015-09-16T14:00:00Z', - }) - setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -297,15 +266,15 @@ class TestAutomationSun(unittest.TestCase): } }) - now = datetime(2015, 9, 16, 14, 59, tzinfo=dt_util.UTC) - with patch('homeassistant.util.dt.now', + now = datetime(2015, 9, 16, 14, 32, 42, tzinfo=dt_util.UTC) + with patch('homeassistant.util.dt.utcnow', return_value=now): self.hass.bus.fire('test_event') self.hass.block_till_done() self.assertEqual(0, len(self.calls)) - now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) - with patch('homeassistant.util.dt.now', + now = datetime(2015, 9, 16, 14, 32, 43, tzinfo=dt_util.UTC) + with patch('homeassistant.util.dt.utcnow', return_value=now): self.hass.bus.fire('test_event') self.hass.block_till_done() @@ -313,11 +282,6 @@ class TestAutomationSun(unittest.TestCase): def test_if_action_before_and_after_during(self): """Test if action was before and after during.""" - self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { - sun.STATE_ATTR_NEXT_RISING: '2015-09-16T10:00:00Z', - sun.STATE_ATTR_NEXT_SETTING: '2015-09-16T15:00:00Z', - }) - setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -335,62 +299,22 @@ class TestAutomationSun(unittest.TestCase): } }) - now = datetime(2015, 9, 16, 9, 59, tzinfo=dt_util.UTC) - with patch('homeassistant.util.dt.now', + now = datetime(2015, 9, 16, 13, 8, 51, tzinfo=dt_util.UTC) + with patch('homeassistant.util.dt.utcnow', return_value=now): self.hass.bus.fire('test_event') self.hass.block_till_done() self.assertEqual(0, len(self.calls)) - now = datetime(2015, 9, 16, 15, 1, tzinfo=dt_util.UTC) - with patch('homeassistant.util.dt.now', + now = datetime(2015, 9, 17, 2, 25, 18, tzinfo=dt_util.UTC) + with patch('homeassistant.util.dt.utcnow', return_value=now): self.hass.bus.fire('test_event') self.hass.block_till_done() self.assertEqual(0, len(self.calls)) - now = datetime(2015, 9, 16, 12, tzinfo=dt_util.UTC) - with patch('homeassistant.util.dt.now', - return_value=now): - self.hass.bus.fire('test_event') - self.hass.block_till_done() - self.assertEqual(1, len(self.calls)) - - def test_if_action_after_different_tz(self): - """Test if action was after in a different timezone.""" - import pytz - - self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { - sun.STATE_ATTR_NEXT_SETTING: '2015-09-16T17:30:00Z', - }) - - setup_component(self.hass, automation.DOMAIN, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'event', - 'event_type': 'test_event', - }, - 'condition': { - 'condition': 'sun', - 'after': 'sunset', - }, - 'action': { - 'service': 'test.automation' - } - } - }) - - # Before - now = datetime(2015, 9, 16, 17, tzinfo=pytz.timezone('US/Mountain')) - with patch('homeassistant.util.dt.now', - return_value=now): - self.hass.bus.fire('test_event') - self.hass.block_till_done() - self.assertEqual(0, len(self.calls)) - - # After - now = datetime(2015, 9, 16, 18, tzinfo=pytz.timezone('US/Mountain')) - with patch('homeassistant.util.dt.now', + now = datetime(2015, 9, 16, 16, tzinfo=dt_util.UTC) + with patch('homeassistant.util.dt.utcnow', return_value=now): self.hass.bus.fire('test_event') self.hass.block_till_done() diff --git a/tests/components/automation/test_template.py b/tests/components/automation/test_template.py index cf8b7a59c87..5cc47687665 100644 --- a/tests/components/automation/test_template.py +++ b/tests/components/automation/test_template.py @@ -370,7 +370,7 @@ class TestAutomationTemplate(unittest.TestCase): def test_if_fires_on_change_with_bad_template(self): """Test for firing on change with bad template.""" with assert_setup_component(0): - assert not setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'template', diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index 738c2251264..3489699d588 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -207,7 +207,7 @@ class TestAutomationTime(unittest.TestCase): def test_if_not_working_if_no_values_in_conf_provided(self): """Test for failure if no configuration.""" with assert_setup_component(0): - assert not setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'time', @@ -230,7 +230,7 @@ class TestAutomationTime(unittest.TestCase): This should break the before rule. """ with assert_setup_component(0): - assert not setup_component(self.hass, automation.DOMAIN, { + assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { 'platform': 'time', diff --git a/tests/components/camera/test_uvc.py b/tests/components/camera/test_uvc.py index f949d1e728e..ad7ee5f5bcb 100644 --- a/tests/components/camera/test_uvc.py +++ b/tests/components/camera/test_uvc.py @@ -31,6 +31,7 @@ class TestUVCSetup(unittest.TestCase): config = { 'platform': 'uvc', 'nvr': 'foo', + 'password': 'bar', 'port': 123, 'key': 'secret', } @@ -58,8 +59,8 @@ class TestUVCSetup(unittest.TestCase): mock_remote.call_args, mock.call('foo', 123, 'secret') ) mock_uvc.assert_has_calls([ - mock.call(mock_remote.return_value, 'id1', 'Front'), - mock.call(mock_remote.return_value, 'id2', 'Back'), + mock.call(mock_remote.return_value, 'id1', 'Front', 'bar'), + mock.call(mock_remote.return_value, 'id2', 'Back', 'bar'), ]) @mock.patch('uvcclient.nvr.UVCRemote') @@ -86,8 +87,8 @@ class TestUVCSetup(unittest.TestCase): mock_remote.call_args, mock.call('foo', 7080, 'secret') ) mock_uvc.assert_has_calls([ - mock.call(mock_remote.return_value, 'id1', 'Front'), - mock.call(mock_remote.return_value, 'id2', 'Back'), + mock.call(mock_remote.return_value, 'id1', 'Front', 'ubnt'), + mock.call(mock_remote.return_value, 'id2', 'Back', 'ubnt'), ]) @mock.patch('uvcclient.nvr.UVCRemote') @@ -114,8 +115,8 @@ class TestUVCSetup(unittest.TestCase): mock_remote.call_args, mock.call('foo', 7080, 'secret') ) mock_uvc.assert_has_calls([ - mock.call(mock_remote.return_value, 'one', 'Front'), - mock.call(mock_remote.return_value, 'two', 'Back'), + mock.call(mock_remote.return_value, 'one', 'Front', 'ubnt'), + mock.call(mock_remote.return_value, 'two', 'Back', 'ubnt'), ]) @mock.patch.object(uvc, 'UnifiVideoCamera') @@ -156,7 +157,9 @@ class TestUVC(unittest.TestCase): self.nvr = mock.MagicMock() self.uuid = 'uuid' self.name = 'name' - self.uvc = uvc.UnifiVideoCamera(self.nvr, self.uuid, self.name) + self.password = 'seekret' + self.uvc = uvc.UnifiVideoCamera(self.nvr, self.uuid, self.name, + self.password) self.nvr.get_camera.return_value = { 'model': 'UVC Fake', 'recordingSettings': { @@ -179,7 +182,6 @@ class TestUVC(unittest.TestCase): @mock.patch('uvcclient.camera.UVCCameraClientV320') def test_login(self, mock_camera, mock_store): """"Test the login.""" - mock_store.return_value.get_camera_password.return_value = 'seekret' self.uvc._login() self.assertEqual(mock_camera.call_count, 1) self.assertEqual( @@ -192,7 +194,6 @@ class TestUVC(unittest.TestCase): @mock.patch('uvcclient.camera.UVCCameraClient') def test_login_v31x(self, mock_camera, mock_store): """Test login with v3.1.x server.""" - mock_store.return_value.get_camera_password.return_value = 'seekret' self.nvr.server_version = (3, 1, 3) self.uvc._login() self.assertEqual(mock_camera.call_count, 1) @@ -202,19 +203,6 @@ class TestUVC(unittest.TestCase): self.assertEqual(mock_camera.return_value.login.call_count, 1) self.assertEqual(mock_camera.return_value.login.call_args, mock.call()) - @mock.patch('uvcclient.store.get_info_store') - @mock.patch('uvcclient.camera.UVCCameraClientV320') - def test_login_no_password(self, mock_camera, mock_store): - """"Test the login with no password.""" - mock_store.return_value.get_camera_password.return_value = None - self.uvc._login() - self.assertEqual(mock_camera.call_count, 1) - self.assertEqual( - mock_camera.call_args, mock.call('host-a', 'admin', 'ubnt') - ) - self.assertEqual(mock_camera.return_value.login.call_count, 1) - self.assertEqual(mock_camera.return_value.login.call_args, mock.call()) - @mock.patch('uvcclient.store.get_info_store') @mock.patch('uvcclient.camera.UVCCameraClientV320') def test_login_tries_both_addrs_and_caches(self, mock_camera, mock_store): @@ -239,7 +227,7 @@ class TestUVC(unittest.TestCase): self.uvc._login() self.assertEqual(mock_camera.call_count, 1) self.assertEqual( - mock_camera.call_args, mock.call('host-b', 'admin', 'ubnt') + mock_camera.call_args, mock.call('host-b', 'admin', 'seekret') ) self.assertEqual(mock_camera.return_value.login.call_count, 1) self.assertEqual(mock_camera.return_value.login.call_args, mock.call()) diff --git a/tests/components/device_tracker/test_unifi.py b/tests/components/device_tracker/test_unifi.py index 37f92e99e22..eea52637241 100644 --- a/tests/components/device_tracker/test_unifi.py +++ b/tests/components/device_tracker/test_unifi.py @@ -1,157 +1,155 @@ """The tests for the Unifi WAP device tracker platform.""" -import unittest from unittest import mock import urllib -from pyunifi import controller +import pytest import voluptuous as vol -from tests.common import get_test_home_assistant from homeassistant.components.device_tracker import DOMAIN, unifi as unifi from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PLATFORM, CONF_VERIFY_SSL) -class TestUnifiScanner(unittest.TestCase): - """Test the Unifiy platform.""" +@pytest.fixture +def mock_ctrl(): + """Mock pyunifi.""" + module = mock.MagicMock() + with mock.patch.dict('sys.modules', { + 'pyunifi.controller': module.controller, + }): + yield module.controller.Controller - def setUp(self): - """Initialize values for this testcase class.""" - self.hass = get_test_home_assistant() - def tearDown(self): - """Stop everything that was started.""" - self.hass.stop() +@pytest.fixture +def mock_scanner(): + """Mock UnifyScanner.""" + with mock.patch('homeassistant.components.device_tracker' + '.unifi.UnifiScanner') as scanner: + yield scanner - @mock.patch('homeassistant.components.device_tracker.unifi.UnifiScanner') - @mock.patch.object(controller, 'Controller') - def test_config_minimal(self, mock_ctrl, mock_scanner): - """Test the setup with minimal configuration.""" - config = { - DOMAIN: unifi.PLATFORM_SCHEMA({ - CONF_PLATFORM: unifi.DOMAIN, - CONF_USERNAME: 'foo', - CONF_PASSWORD: 'password', - }) + +def test_config_minimal(hass, mock_scanner, mock_ctrl): + """Test the setup with minimal configuration.""" + config = { + DOMAIN: unifi.PLATFORM_SCHEMA({ + CONF_PLATFORM: unifi.DOMAIN, + CONF_USERNAME: 'foo', + CONF_PASSWORD: 'password', + }) + } + result = unifi.get_scanner(hass, config) + assert mock_scanner.return_value == result + assert mock_ctrl.call_count == 1 + assert mock_ctrl.mock_calls[0] == \ + mock.call('localhost', 'foo', 'password', 8443, + version='v4', site_id='default', ssl_verify=True) + + assert mock_scanner.call_count == 1 + assert mock_scanner.call_args == mock.call(mock_ctrl.return_value) + + +def test_config_full(hass, mock_scanner, mock_ctrl): + """Test the setup with full configuration.""" + config = { + DOMAIN: unifi.PLATFORM_SCHEMA({ + CONF_PLATFORM: unifi.DOMAIN, + CONF_USERNAME: 'foo', + CONF_PASSWORD: 'password', + CONF_HOST: 'myhost', + CONF_VERIFY_SSL: False, + 'port': 123, + 'site_id': 'abcdef01', + }) + } + result = unifi.get_scanner(hass, config) + assert mock_scanner.return_value == result + assert mock_ctrl.call_count == 1 + assert mock_ctrl.call_args == \ + mock.call('myhost', 'foo', 'password', 123, + version='v4', site_id='abcdef01', ssl_verify=False) + + assert mock_scanner.call_count == 1 + assert mock_scanner.call_args == mock.call(mock_ctrl.return_value) + + +def test_config_error(): + """Test for configuration errors.""" + with pytest.raises(vol.Invalid): + unifi.PLATFORM_SCHEMA({ + # no username + CONF_PLATFORM: unifi.DOMAIN, + CONF_HOST: 'myhost', + 'port': 123, + }) + with pytest.raises(vol.Invalid): + unifi.PLATFORM_SCHEMA({ + CONF_PLATFORM: unifi.DOMAIN, + CONF_USERNAME: 'foo', + CONF_PASSWORD: 'password', + CONF_HOST: 'myhost', + 'port': 'foo', # bad port! + }) + + +def test_config_controller_failed(hass, mock_ctrl, mock_scanner): + """Test for controller failure.""" + config = { + 'device_tracker': { + CONF_PLATFORM: unifi.DOMAIN, + CONF_USERNAME: 'foo', + CONF_PASSWORD: 'password', } - result = unifi.get_scanner(self.hass, config) - self.assertEqual(mock_scanner.return_value, result) - self.assertEqual(mock_ctrl.call_count, 1) - self.assertEqual( - mock_ctrl.call_args, - mock.call('localhost', 'foo', 'password', 8443, - version='v4', site_id='default', ssl_verify=True) - ) - self.assertEqual(mock_scanner.call_count, 1) - self.assertEqual( - mock_scanner.call_args, - mock.call(mock_ctrl.return_value) - ) + } + mock_ctrl.side_effect = urllib.error.HTTPError( + '/', 500, 'foo', {}, None) + result = unifi.get_scanner(hass, config) + assert result is False - @mock.patch('homeassistant.components.device_tracker.unifi.UnifiScanner') - @mock.patch.object(controller, 'Controller') - def test_config_full(self, mock_ctrl, mock_scanner): - """Test the setup with full configuration.""" - config = { - DOMAIN: unifi.PLATFORM_SCHEMA({ - CONF_PLATFORM: unifi.DOMAIN, - CONF_USERNAME: 'foo', - CONF_PASSWORD: 'password', - CONF_HOST: 'myhost', - CONF_VERIFY_SSL: False, - 'port': 123, - 'site_id': 'abcdef01', - }) - } - result = unifi.get_scanner(self.hass, config) - self.assertEqual(mock_scanner.return_value, result) - self.assertEqual(mock_ctrl.call_count, 1) - self.assertEqual( - mock_ctrl.call_args, - mock.call('myhost', 'foo', 'password', 123, - version='v4', site_id='abcdef01', ssl_verify=False) - ) - self.assertEqual(mock_scanner.call_count, 1) - self.assertEqual( - mock_scanner.call_args, - mock.call(mock_ctrl.return_value) - ) - def test_config_error(self): - """Test for configuration errors.""" - with self.assertRaises(vol.Invalid): - unifi.PLATFORM_SCHEMA({ - # no username - CONF_PLATFORM: unifi.DOMAIN, - CONF_HOST: 'myhost', - 'port': 123, - }) - with self.assertRaises(vol.Invalid): - unifi.PLATFORM_SCHEMA({ - CONF_PLATFORM: unifi.DOMAIN, - CONF_USERNAME: 'foo', - CONF_PASSWORD: 'password', - CONF_HOST: 'myhost', - 'port': 'foo', # bad port! - }) +def test_scanner_update(): + """Test the scanner update.""" + ctrl = mock.MagicMock() + fake_clients = [ + {'mac': '123'}, + {'mac': '234'}, + ] + ctrl.get_clients.return_value = fake_clients + unifi.UnifiScanner(ctrl) + assert ctrl.get_clients.call_count == 1 + assert ctrl.get_clients.call_args == mock.call() - @mock.patch('homeassistant.components.device_tracker.unifi.UnifiScanner') - @mock.patch.object(controller, 'Controller') - def test_config_controller_failed(self, mock_ctrl, mock_scanner): - """Test for controller failure.""" - config = { - 'device_tracker': { - CONF_PLATFORM: unifi.DOMAIN, - CONF_USERNAME: 'foo', - CONF_PASSWORD: 'password', - } - } - mock_ctrl.side_effect = urllib.error.HTTPError( - '/', 500, 'foo', {}, None) - result = unifi.get_scanner(self.hass, config) - self.assertFalse(result) - def test_scanner_update(self): # pylint: disable=no-self-use - """Test the scanner update.""" - ctrl = mock.MagicMock() - fake_clients = [ - {'mac': '123'}, - {'mac': '234'}, - ] - ctrl.get_clients.return_value = fake_clients - unifi.UnifiScanner(ctrl) - self.assertEqual(ctrl.get_clients.call_count, 1) - self.assertEqual(ctrl.get_clients.call_args, mock.call()) +def test_scanner_update_error(): + """Test the scanner update for error.""" + ctrl = mock.MagicMock() + ctrl.get_clients.side_effect = urllib.error.HTTPError( + '/', 500, 'foo', {}, None) + unifi.UnifiScanner(ctrl) - def test_scanner_update_error(self): # pylint: disable=no-self-use - """Test the scanner update for error.""" - ctrl = mock.MagicMock() - ctrl.get_clients.side_effect = urllib.error.HTTPError( - '/', 500, 'foo', {}, None) - unifi.UnifiScanner(ctrl) - def test_scan_devices(self): - """Test the scanning for devices.""" - ctrl = mock.MagicMock() - fake_clients = [ - {'mac': '123'}, - {'mac': '234'}, - ] - ctrl.get_clients.return_value = fake_clients - scanner = unifi.UnifiScanner(ctrl) - self.assertEqual(set(['123', '234']), set(scanner.scan_devices())) +def test_scan_devices(): + """Test the scanning for devices.""" + ctrl = mock.MagicMock() + fake_clients = [ + {'mac': '123'}, + {'mac': '234'}, + ] + ctrl.get_clients.return_value = fake_clients + scanner = unifi.UnifiScanner(ctrl) + assert set(scanner.scan_devices()) == set(['123', '234']) - def test_get_device_name(self): - """Test the getting of device names.""" - ctrl = mock.MagicMock() - fake_clients = [ - {'mac': '123', 'hostname': 'foobar'}, - {'mac': '234', 'name': 'Nice Name'}, - {'mac': '456'}, - ] - ctrl.get_clients.return_value = fake_clients - scanner = unifi.UnifiScanner(ctrl) - self.assertEqual('foobar', scanner.get_device_name('123')) - self.assertEqual('Nice Name', scanner.get_device_name('234')) - self.assertEqual(None, scanner.get_device_name('456')) - self.assertEqual(None, scanner.get_device_name('unknown')) + +def test_get_device_name(): + """Test the getting of device names.""" + ctrl = mock.MagicMock() + fake_clients = [ + {'mac': '123', 'hostname': 'foobar'}, + {'mac': '234', 'name': 'Nice Name'}, + {'mac': '456'}, + ] + ctrl.get_clients.return_value = fake_clients + scanner = unifi.UnifiScanner(ctrl) + assert scanner.get_device_name('123') == 'foobar' + assert scanner.get_device_name('234') == 'Nice Name' + assert scanner.get_device_name('456') is None + assert scanner.get_device_name('unknown') is None diff --git a/tests/components/image_processing/test_microsoft_face_detect.py b/tests/components/image_processing/test_microsoft_face_detect.py index f398db991c2..b743dee9704 100644 --- a/tests/components/image_processing/test_microsoft_face_detect.py +++ b/tests/components/image_processing/test_microsoft_face_detect.py @@ -98,6 +98,8 @@ class TestMicrosoftFaceDetect(object): } } + self.endpoint_url = "https://westus.{0}".format(mf.FACE_API_URL) + def teardown_method(self): """Stop everything that was started.""" self.hass.stop() @@ -108,15 +110,15 @@ class TestMicrosoftFaceDetect(object): def test_ms_detect_process_image(self, poll_mock, aioclient_mock): """Setup and scan a picture and test plates from event.""" aioclient_mock.get( - mf.FACE_API_URL.format("persongroups"), + self.endpoint_url.format("persongroups"), text=load_fixture('microsoft_face_persongroups.json') ) aioclient_mock.get( - mf.FACE_API_URL.format("persongroups/test_group1/persons"), + self.endpoint_url.format("persongroups/test_group1/persons"), text=load_fixture('microsoft_face_persons.json') ) aioclient_mock.get( - mf.FACE_API_URL.format("persongroups/test_group2/persons"), + self.endpoint_url.format("persongroups/test_group2/persons"), text=load_fixture('microsoft_face_persons.json') ) @@ -139,7 +141,7 @@ class TestMicrosoftFaceDetect(object): aioclient_mock.get(url, content=b'image') aioclient_mock.post( - mf.FACE_API_URL.format("detect"), + self.endpoint_url.format("detect"), text=load_fixture('microsoft_face_detect.json'), params={'returnFaceAttributes': "age,gender"} ) diff --git a/tests/components/image_processing/test_microsoft_face_identify.py b/tests/components/image_processing/test_microsoft_face_identify.py index a7958b68de7..c2ab5684ed0 100644 --- a/tests/components/image_processing/test_microsoft_face_identify.py +++ b/tests/components/image_processing/test_microsoft_face_identify.py @@ -99,6 +99,8 @@ class TestMicrosoftFaceIdentify(object): } } + self.endpoint_url = "https://westus.{0}".format(mf.FACE_API_URL) + def teardown_method(self): """Stop everything that was started.""" self.hass.stop() @@ -109,15 +111,15 @@ class TestMicrosoftFaceIdentify(object): def test_ms_identify_process_image(self, poll_mock, aioclient_mock): """Setup and scan a picture and test plates from event.""" aioclient_mock.get( - mf.FACE_API_URL.format("persongroups"), + self.endpoint_url.format("persongroups"), text=load_fixture('microsoft_face_persongroups.json') ) aioclient_mock.get( - mf.FACE_API_URL.format("persongroups/test_group1/persons"), + self.endpoint_url.format("persongroups/test_group1/persons"), text=load_fixture('microsoft_face_persons.json') ) aioclient_mock.get( - mf.FACE_API_URL.format("persongroups/test_group2/persons"), + self.endpoint_url.format("persongroups/test_group2/persons"), text=load_fixture('microsoft_face_persons.json') ) @@ -140,11 +142,11 @@ class TestMicrosoftFaceIdentify(object): aioclient_mock.get(url, content=b'image') aioclient_mock.post( - mf.FACE_API_URL.format("detect"), + self.endpoint_url.format("detect"), text=load_fixture('microsoft_face_detect.json') ) aioclient_mock.post( - mf.FACE_API_URL.format("identify"), + self.endpoint_url.format("identify"), text=load_fixture('microsoft_face_identify.json') ) diff --git a/tests/components/light/test_demo.py b/tests/components/light/test_demo.py index 9e318ea9192..2d3a752fafa 100644 --- a/tests/components/light/test_demo.py +++ b/tests/components/light/test_demo.py @@ -56,6 +56,11 @@ class TestDemoLight(unittest.TestCase): self.assertEqual(154, state.attributes.get(light.ATTR_MIN_MIREDS)) self.assertEqual(500, state.attributes.get(light.ATTR_MAX_MIREDS)) self.assertEqual('none', state.attributes.get(light.ATTR_EFFECT)) + light.turn_on(self.hass, ENTITY_LIGHT, kelvin=3000, brightness_pct=50) + self.hass.block_till_done() + state = self.hass.states.get(ENTITY_LIGHT) + self.assertEqual(333, state.attributes.get(light.ATTR_COLOR_TEMP)) + self.assertEqual(127, state.attributes.get(light.ATTR_BRIGHTNESS)) def test_turn_off(self): """Test light turn off method.""" diff --git a/tests/components/lock/test_zwave.py b/tests/components/lock/test_zwave.py index 9fb634f49e2..b0e9456b8a8 100644 --- a/tests/components/lock/test_zwave.py +++ b/tests/components/lock/test_zwave.py @@ -173,8 +173,8 @@ def test_lock_set_usercode_service(hass, mock_openzwave): """Test the zwave lock set_usercode service.""" mock_network = hass.data[zwave.zwave.ZWAVE_NETWORK] = MagicMock() node = MockNode(node_id=12) - value0 = MockValue(data=None, node=node, index=0) - value1 = MockValue(data=None, node=node, index=1) + value0 = MockValue(data=' ', node=node, index=0) + value1 = MockValue(data=' ', node=node, index=1) yield from zwave.async_setup_platform( hass, {}, MagicMock()) @@ -202,7 +202,7 @@ def test_lock_set_usercode_service(hass, mock_openzwave): yield from hass.services.async_call( zwave.DOMAIN, zwave.SERVICE_SET_USERCODE, { const.ATTR_NODE_ID: node.node_id, - zwave.ATTR_USERCODE: '12345', + zwave.ATTR_USERCODE: '123', zwave.ATTR_CODE_SLOT: 1, }) yield from hass.async_block_till_done() diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py index c29d41cc590..4ac66702d06 100644 --- a/tests/components/media_player/test_cast.py +++ b/tests/components/media_player/test_cast.py @@ -1,11 +1,22 @@ """The tests for the Cast Media player platform.""" # pylint: disable=protected-access import unittest -from unittest.mock import patch +from unittest.mock import patch, MagicMock + +import pytest from homeassistant.components.media_player import cast +@pytest.fixture(autouse=True) +def cast_mock(): + """Mock pychromecast.""" + with patch.dict('sys.modules', { + 'pychromecast': MagicMock(), + }): + yield + + class FakeChromeCast(object): """A fake Chrome Cast.""" diff --git a/tests/components/media_player/test_cmus.py b/tests/components/media_player/test_cmus.py deleted file mode 100644 index 24322b5bce0..00000000000 --- a/tests/components/media_player/test_cmus.py +++ /dev/null @@ -1,31 +0,0 @@ -"""The tests for the Demo Media player platform.""" -import unittest -from unittest import mock - -from homeassistant.components.media_player import cmus -from homeassistant import const - -from tests.common import get_test_home_assistant - -entity_id = 'media_player.cmus' - - -class TestCmusMediaPlayer(unittest.TestCase): - """Test the media_player module.""" - - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - - def tearDown(self): # pylint: disable=invalid-name - """Stop everything that was started.""" - self.hass.stop() - - @mock.patch('homeassistant.components.media_player.cmus.CmusDevice') - def test_password_required_with_host(self, cmus_mock): - """Test that a password is required when specifying a remote host.""" - fake_config = { - const.CONF_HOST: 'a_real_hostname', - } - self.assertFalse( - cmus.setup_platform(self.hass, fake_config, mock.MagicMock())) diff --git a/tests/components/media_player/test_frontier_silicon.py b/tests/components/media_player/test_frontier_silicon.py deleted file mode 100644 index a2c3223cd9c..00000000000 --- a/tests/components/media_player/test_frontier_silicon.py +++ /dev/null @@ -1,42 +0,0 @@ -"""The tests for the Demo Media player platform.""" -import unittest -from unittest import mock - -import logging - -from homeassistant.components.media_player.frontier_silicon import FSAPIDevice -from homeassistant.components.media_player import frontier_silicon -from homeassistant import const - -from tests.common import get_test_home_assistant - -_LOGGER = logging.getLogger(__name__) - - -class TestFrontierSiliconMediaPlayer(unittest.TestCase): - """Test the media_player module.""" - - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - - def tearDown(self): # pylint: disable=invalid-name - """Stop everything that was started.""" - self.hass.stop() - - def test_host_required_with_host(self): - """Test that a host with a valid url is set when using a conf.""" - fake_config = { - const.CONF_HOST: 'host_ip', - } - result = frontier_silicon.setup_platform(self.hass, - fake_config, mock.MagicMock()) - - self.assertTrue(result) - - def test_invalid_host(self): - """Test that a host with a valid url is set when using a conf.""" - import requests - - fsapi = FSAPIDevice('INVALID_URL', '1234') - self.assertRaises(requests.exceptions.MissingSchema, fsapi.update) diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py index 0a111ef3b36..8c62c6c84e9 100644 --- a/tests/components/media_player/test_sonos.py +++ b/tests/components/media_player/test_sonos.py @@ -1,9 +1,11 @@ """The tests for the Demo Media player platform.""" +import datetime import socket import unittest import soco.snapshot from unittest import mock import soco +from soco import alarms from homeassistant.setup import setup_component from homeassistant.components.media_player import sonos, DOMAIN @@ -48,10 +50,6 @@ class SoCoMock(): self.is_visible = True self.avTransport = AvTransportMock() - def clear_sleep_timer(self): - """Clear the sleep timer.""" - return - def get_sonos_favorites(self): """Get favorites list from sonos.""" return {'favorites': []} @@ -311,6 +309,36 @@ class TestSonosMediaPlayer(unittest.TestCase): device.set_sleep_timer(None) set_sleep_timerMock.assert_called_once_with(None) + @mock.patch('soco.SoCo', new=SoCoMock) + @mock.patch('soco.alarms.Alarm') + @mock.patch('socket.create_connection', side_effect=socket.error()) + def test_update_alarm(self, soco_mock, alarm_mock, *args): + """Ensuring soco methods called for sonos_set_sleep_timer service.""" + sonos.setup_platform(self.hass, {}, fake_add_device, { + 'host': '192.0.2.1' + }) + device = self.hass.data[sonos.DATA_SONOS][-1] + device.hass = self.hass + alarm1 = alarms.Alarm(soco_mock) + alarm1.configure_mock(_alarm_id="1", start_time=None, enabled=False, + include_linked_zones=False, volume=100) + with mock.patch('soco.alarms.get_alarms', return_value=[alarm1]): + attrs = { + 'time': datetime.time(12, 00), + 'enabled': True, + 'include_linked_zones': True, + 'volume': 0.30, + } + device.update_alarm(alarm_id=2) + alarm1.save.assert_not_called() + device.update_alarm(alarm_id=1, **attrs) + self.assertEqual(alarm1.enabled, attrs['enabled']) + self.assertEqual(alarm1.start_time, attrs['time']) + self.assertEqual(alarm1.include_linked_zones, + attrs['include_linked_zones']) + self.assertEqual(alarm1.volume, 30) + alarm1.save.assert_called_once_with() + @mock.patch('soco.SoCo', new=SoCoMock) @mock.patch('socket.create_connection', side_effect=socket.error()) @mock.patch.object(soco.snapshot.Snapshot, 'snapshot') diff --git a/tests/components/notify/test_smtp.py b/tests/components/notify/test_smtp.py index 016c6a5d1f4..127eecae2b7 100644 --- a/tests/components/notify/test_smtp.py +++ b/tests/components/notify/test_smtp.py @@ -23,7 +23,8 @@ class TestNotifySmtp(unittest.TestCase): self.hass = get_test_home_assistant() self.mailer = MockSMTP('localhost', 25, 5, 'test@test.com', 1, 'testuser', 'testpass', - ['recip1@example.com', 'testrecip@test.com'], 0) + ['recip1@example.com', 'testrecip@test.com'], + 'HomeAssistant', 0) def tearDown(self): # pylint: disable=invalid-name """"Stop down everything that was started.""" @@ -38,7 +39,7 @@ class TestNotifySmtp(unittest.TestCase): 'Content-Transfer-Encoding: 7bit\n' 'Subject: Home Assistant\n' 'To: recip1@example.com,testrecip@test.com\n' - 'From: test@test.com\n' + 'From: HomeAssistant \n' 'X-Mailer: HomeAssistant\n' 'Date: [^\n]+\n' 'Message-Id: <[^@]+@[^>]+>\n' @@ -52,3 +53,24 @@ class TestNotifySmtp(unittest.TestCase): msg = self.mailer.send_message('Test msg', data={'images': ['test.jpg']}) self.assertTrue('Content-Type: multipart/related' in msg) + + @patch('email.utils.make_msgid', return_value='') + def test_html_email(self, mock_make_msgid): + """Test build of html email behavior.""" + html = ''' + + + + +
+

Intruder alert at apartment!!

+
+
+ test.jpg +
+ + ''' + msg = self.mailer.send_message('Test msg', + data={'html': html, + 'images': ['test.jpg']}) + self.assertTrue('Content-Type: multipart/related' in msg) diff --git a/tests/components/remote/test_kira.py b/tests/components/remote/test_kira.py new file mode 100644 index 00000000000..144504f8aa2 --- /dev/null +++ b/tests/components/remote/test_kira.py @@ -0,0 +1,57 @@ +"""The tests for Kira sensor platform.""" +import unittest +from unittest.mock import MagicMock + +from homeassistant.components.remote import kira as kira + +from tests.common import get_test_home_assistant + +SERVICE_SEND_COMMAND = 'send_command' + +TEST_CONFIG = {kira.DOMAIN: { + 'devices': [{'host': '127.0.0.1', + 'port': 17324}]}} + +DISCOVERY_INFO = { + 'name': 'kira', + 'device': 'kira' +} + + +class TestKiraSensor(unittest.TestCase): + """Tests the Kira Sensor platform.""" + + # pylint: disable=invalid-name + DEVICES = [] + + def add_devices(self, devices): + """Mock add devices.""" + for device in devices: + self.DEVICES.append(device) + + def setUp(self): + """Initialize values for this testcase class.""" + self.hass = get_test_home_assistant() + self.mock_kira = MagicMock() + self.hass.data[kira.DOMAIN] = {kira.CONF_REMOTE: {}} + self.hass.data[kira.DOMAIN][kira.CONF_REMOTE]['kira'] = self.mock_kira + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + def test_service_call(self): + """Test Kira's ability to send commands.""" + kira.setup_platform(self.hass, TEST_CONFIG, self.add_devices, + DISCOVERY_INFO) + assert len(self.DEVICES) == 1 + remote = self.DEVICES[0] + + assert remote.name == 'kira' + + command = "FAKE_COMMAND" + device = "FAKE_DEVICE" + commandTuple = (command, device) + remote.send_command(device=device, command=command) + + self.mock_kira.sendCode.assert_called_with(commandTuple) diff --git a/tests/components/sensor/test_file.py b/tests/components/sensor/test_file.py new file mode 100644 index 00000000000..00e8f2ba525 --- /dev/null +++ b/tests/components/sensor/test_file.py @@ -0,0 +1,91 @@ +"""The tests for local file sensor platform.""" +import unittest +from unittest.mock import Mock, patch + +# Using third party package because of a bug reading binary data in Python 3.4 +# https://bugs.python.org/issue23004 +from mock_open import MockOpen + +from homeassistant.setup import setup_component +from homeassistant.const import STATE_UNKNOWN + +from tests.common import get_test_home_assistant + + +class TestFileSensor(unittest.TestCase): + """Test the File sensor.""" + + def setup_method(self, method): + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + @patch('os.path.isfile', Mock(return_value=True)) + @patch('os.access', Mock(return_value=True)) + def test_file_value(self): + """Test the File sensor.""" + config = { + 'sensor': { + 'platform': 'file', + 'name': 'file1', + 'file_path': 'mock.file1', + } + } + + m_open = MockOpen(read_data='43\n45\n21') + with patch('homeassistant.components.sensor.file.open', m_open, + create=True): + assert setup_component(self.hass, 'sensor', config) + self.hass.block_till_done() + + state = self.hass.states.get('sensor.file1') + self.assertEqual(state.state, '21') + + @patch('os.path.isfile', Mock(return_value=True)) + @patch('os.access', Mock(return_value=True)) + def test_file_value_template(self): + """Test the File sensor with JSON entries.""" + config = { + 'sensor': { + 'platform': 'file', + 'name': 'file2', + 'file_path': 'mock.file2', + 'value_template': '{{ value_json.temperature }}', + } + } + + data = '{"temperature": 29, "humidity": 31}\n' \ + '{"temperature": 26, "humidity": 36}' + + m_open = MockOpen(read_data=data) + with patch('homeassistant.components.sensor.file.open', m_open, + create=True): + assert setup_component(self.hass, 'sensor', config) + self.hass.block_till_done() + + state = self.hass.states.get('sensor.file2') + self.assertEqual(state.state, '26') + + @patch('os.path.isfile', Mock(return_value=True)) + @patch('os.access', Mock(return_value=True)) + def test_file_empty(self): + """Test the File sensor with an empty file.""" + config = { + 'sensor': { + 'platform': 'file', + 'name': 'file3', + 'file_path': 'mock.file', + } + } + + m_open = MockOpen(read_data='') + with patch('homeassistant.components.sensor.file.open', m_open, + create=True): + assert setup_component(self.hass, 'sensor', config) + self.hass.block_till_done() + + state = self.hass.states.get('sensor.file3') + self.assertEqual(state.state, STATE_UNKNOWN) diff --git a/tests/components/sensor/test_kira.py b/tests/components/sensor/test_kira.py new file mode 100644 index 00000000000..093158cb25c --- /dev/null +++ b/tests/components/sensor/test_kira.py @@ -0,0 +1,59 @@ +"""The tests for Kira sensor platform.""" +import unittest +from unittest.mock import MagicMock + +from homeassistant.components.sensor import kira as kira + +from tests.common import get_test_home_assistant + +TEST_CONFIG = {kira.DOMAIN: { + 'sensors': [{'host': '127.0.0.1', + 'port': 17324}]}} + +DISCOVERY_INFO = { + 'name': 'kira', + 'device': 'kira' +} + + +class TestKiraSensor(unittest.TestCase): + """Tests the Kira Sensor platform.""" + + # pylint: disable=invalid-name + DEVICES = [] + + def add_devices(self, devices): + """Mock add devices.""" + for device in devices: + self.DEVICES.append(device) + + def setUp(self): + """Initialize values for this testcase class.""" + self.hass = get_test_home_assistant() + mock_kira = MagicMock() + self.hass.data[kira.DOMAIN] = {kira.CONF_SENSOR: {}} + self.hass.data[kira.DOMAIN][kira.CONF_SENSOR]['kira'] = mock_kira + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + # pylint: disable=protected-access + def test_kira_sensor_callback(self): + """Ensure Kira sensor properly updates its attributes from callback.""" + kira.setup_platform(self.hass, TEST_CONFIG, self.add_devices, + DISCOVERY_INFO) + assert len(self.DEVICES) == 1 + sensor = self.DEVICES[0] + + assert sensor.name == 'kira' + + sensor.hass = self.hass + + codeName = 'FAKE_CODE' + deviceName = 'FAKE_DEVICE' + codeTuple = (codeName, deviceName) + sensor._update_callback(codeTuple) + + assert sensor.state == codeName + assert sensor.device_state_attributes == {kira.CONF_DEVICE: deviceName} diff --git a/tests/components/switch/test_flux.py b/tests/components/switch/test_flux.py index b42177a5f06..2422f0ea334 100644 --- a/tests/components/switch/test_flux.py +++ b/tests/components/switch/test_flux.py @@ -1,5 +1,4 @@ """The tests for the Flux switch platform.""" -from datetime import timedelta import unittest from unittest.mock import patch @@ -86,28 +85,30 @@ class TestSwitchFlux(unittest.TestCase): self.assertIsNone(state.attributes.get('xy_color')) self.assertIsNone(state.attributes.get('brightness')) - test_time = dt_util.now().replace(hour=10, minute=30, - second=0) - sunset_time = test_time.replace(hour=17, minute=0, - second=0) - sunrise_time = test_time.replace(hour=5, minute=0, - second=0) + timedelta(days=1) + test_time = dt_util.now().replace(hour=10, minute=30, second=0) + sunset_time = test_time.replace(hour=17, minute=0, second=0) + sunrise_time = test_time.replace(hour=5, minute=0, second=0) + + def event_date(hass, event, now=None): + if event == 'sunrise': + return sunrise_time + else: + return sunset_time + with patch('homeassistant.util.dt.now', return_value=test_time): - with patch('homeassistant.components.sun.next_rising', - return_value=sunrise_time): - with patch('homeassistant.components.sun.next_setting', - return_value=sunset_time): - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: { - 'platform': 'flux', - 'name': 'flux', - 'lights': [dev1.entity_id] - } - }) - turn_on_calls = mock_service( - self.hass, light.DOMAIN, SERVICE_TURN_ON) - fire_time_changed(self.hass, test_time) - self.hass.block_till_done() + with patch('homeassistant.helpers.sun.get_astral_event_date', + side_effect=event_date): + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'flux', + 'name': 'flux', + 'lights': [dev1.entity_id] + } + }) + turn_on_calls = mock_service( + self.hass, light.DOMAIN, SERVICE_TURN_ON) + fire_time_changed(self.hass, test_time) + self.hass.block_till_done() self.assertEqual(0, len(turn_on_calls)) def test_flux_before_sunrise(self): @@ -126,30 +127,32 @@ class TestSwitchFlux(unittest.TestCase): self.assertIsNone(state.attributes.get('xy_color')) self.assertIsNone(state.attributes.get('brightness')) - test_time = dt_util.now().replace(hour=2, minute=30, - second=0) - sunset_time = test_time.replace(hour=17, minute=0, - second=0) - sunrise_time = test_time.replace(hour=5, minute=0, - second=0) + timedelta(days=1) + test_time = dt_util.now().replace(hour=2, minute=30, second=0) + sunset_time = test_time.replace(hour=17, minute=0, second=0) + sunrise_time = test_time.replace(hour=5, minute=0, second=0) + + def event_date(hass, event, now=None): + if event == 'sunrise': + return sunrise_time + else: + return sunset_time + with patch('homeassistant.util.dt.now', return_value=test_time): - with patch('homeassistant.components.sun.next_rising', - return_value=sunrise_time): - with patch('homeassistant.components.sun.next_setting', - return_value=sunset_time): - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: { - 'platform': 'flux', - 'name': 'flux', - 'lights': [dev1.entity_id] - } - }) - turn_on_calls = mock_service( - self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') - self.hass.block_till_done() - fire_time_changed(self.hass, test_time) - self.hass.block_till_done() + with patch('homeassistant.helpers.sun.get_astral_event_date', + side_effect=event_date): + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'flux', + 'name': 'flux', + 'lights': [dev1.entity_id] + } + }) + turn_on_calls = mock_service( + self.hass, light.DOMAIN, SERVICE_TURN_ON) + switch.turn_on(self.hass, 'switch.flux') + self.hass.block_till_done() + fire_time_changed(self.hass, test_time) + self.hass.block_till_done() call = turn_on_calls[-1] self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 119) self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.591, 0.395]) @@ -173,28 +176,30 @@ class TestSwitchFlux(unittest.TestCase): test_time = dt_util.now().replace(hour=8, minute=30, second=0) sunset_time = test_time.replace(hour=17, minute=0, second=0) - sunrise_time = test_time.replace(hour=5, - minute=0, - second=0) + timedelta(days=1) + sunrise_time = test_time.replace(hour=5, minute=0, second=0) + + def event_date(hass, event, now=None): + if event == 'sunrise': + return sunrise_time + else: + return sunset_time with patch('homeassistant.util.dt.now', return_value=test_time): - with patch('homeassistant.components.sun.next_rising', - return_value=sunrise_time): - with patch('homeassistant.components.sun.next_setting', - return_value=sunset_time): - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: { - 'platform': 'flux', - 'name': 'flux', - 'lights': [dev1.entity_id] - } - }) - turn_on_calls = mock_service( - self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') - self.hass.block_till_done() - fire_time_changed(self.hass, test_time) - self.hass.block_till_done() + with patch('homeassistant.helpers.sun.get_astral_event_date', + side_effect=event_date): + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'flux', + 'name': 'flux', + 'lights': [dev1.entity_id] + } + }) + turn_on_calls = mock_service( + self.hass, light.DOMAIN, SERVICE_TURN_ON) + switch.turn_on(self.hass, 'switch.flux') + self.hass.block_till_done() + fire_time_changed(self.hass, test_time) + self.hass.block_till_done() call = turn_on_calls[-1] self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 180) self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.431, 0.38]) @@ -218,28 +223,30 @@ class TestSwitchFlux(unittest.TestCase): test_time = dt_util.now().replace(hour=17, minute=30, second=0) sunset_time = test_time.replace(hour=17, minute=0, second=0) - sunrise_time = test_time.replace(hour=5, - minute=0, - second=0) + timedelta(days=1) + sunrise_time = test_time.replace(hour=5, minute=0, second=0) + + def event_date(hass, event, now=None): + if event == 'sunrise': + return sunrise_time + else: + return sunset_time with patch('homeassistant.util.dt.now', return_value=test_time): - with patch('homeassistant.components.sun.next_rising', - return_value=sunrise_time): - with patch('homeassistant.components.sun.next_setting', - return_value=sunset_time): - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: { - 'platform': 'flux', - 'name': 'flux', - 'lights': [dev1.entity_id] - } - }) - turn_on_calls = mock_service( - self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') - self.hass.block_till_done() - fire_time_changed(self.hass, test_time) - self.hass.block_till_done() + with patch('homeassistant.helpers.sun.get_astral_event_date', + side_effect=event_date): + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'flux', + 'name': 'flux', + 'lights': [dev1.entity_id] + } + }) + turn_on_calls = mock_service( + self.hass, light.DOMAIN, SERVICE_TURN_ON) + switch.turn_on(self.hass, 'switch.flux') + self.hass.block_till_done() + fire_time_changed(self.hass, test_time) + self.hass.block_till_done() call = turn_on_calls[-1] self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 153) self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.496, 0.397]) @@ -263,28 +270,30 @@ class TestSwitchFlux(unittest.TestCase): test_time = dt_util.now().replace(hour=23, minute=30, second=0) sunset_time = test_time.replace(hour=17, minute=0, second=0) - sunrise_time = test_time.replace(hour=5, - minute=0, - second=0) + timedelta(days=1) + sunrise_time = test_time.replace(hour=5, minute=0, second=0) + + def event_date(hass, event, now=None): + if event == 'sunrise': + return sunrise_time + else: + return sunset_time with patch('homeassistant.util.dt.now', return_value=test_time): - with patch('homeassistant.components.sun.next_rising', - return_value=sunrise_time): - with patch('homeassistant.components.sun.next_setting', - return_value=sunset_time): - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: { - 'platform': 'flux', - 'name': 'flux', - 'lights': [dev1.entity_id] - } - }) - turn_on_calls = mock_service( - self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') - self.hass.block_till_done() - fire_time_changed(self.hass, test_time) - self.hass.block_till_done() + with patch('homeassistant.helpers.sun.get_astral_event_date', + side_effect=event_date): + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'flux', + 'name': 'flux', + 'lights': [dev1.entity_id] + } + }) + turn_on_calls = mock_service( + self.hass, light.DOMAIN, SERVICE_TURN_ON) + switch.turn_on(self.hass, 'switch.flux') + self.hass.block_till_done() + fire_time_changed(self.hass, test_time) + self.hass.block_till_done() call = turn_on_calls[-1] self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 119) self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.591, 0.395]) @@ -308,30 +317,32 @@ class TestSwitchFlux(unittest.TestCase): test_time = dt_util.now().replace(hour=17, minute=30, second=0) sunset_time = test_time.replace(hour=17, minute=0, second=0) - sunrise_time = test_time.replace(hour=5, - minute=0, - second=0) + timedelta(days=1) + sunrise_time = test_time.replace(hour=5, minute=0, second=0) + + def event_date(hass, event, now=None): + if event == 'sunrise': + return sunrise_time + else: + return sunset_time with patch('homeassistant.util.dt.now', return_value=test_time): - with patch('homeassistant.components.sun.next_rising', - return_value=sunrise_time): - with patch('homeassistant.components.sun.next_setting', - return_value=sunset_time): - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: { - 'platform': 'flux', - 'name': 'flux', - 'lights': [dev1.entity_id], - 'start_time': '6:00', - 'stop_time': '23:30' - } - }) - turn_on_calls = mock_service( - self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') - self.hass.block_till_done() - fire_time_changed(self.hass, test_time) - self.hass.block_till_done() + with patch('homeassistant.helpers.sun.get_astral_event_date', + side_effect=event_date): + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'flux', + 'name': 'flux', + 'lights': [dev1.entity_id], + 'start_time': '6:00', + 'stop_time': '23:30' + } + }) + turn_on_calls = mock_service( + self.hass, light.DOMAIN, SERVICE_TURN_ON) + switch.turn_on(self.hass, 'switch.flux') + self.hass.block_till_done() + fire_time_changed(self.hass, test_time) + self.hass.block_till_done() call = turn_on_calls[-1] self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 154) self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.494, 0.397]) @@ -355,30 +366,32 @@ class TestSwitchFlux(unittest.TestCase): test_time = dt_util.now().replace(hour=17, minute=30, second=0) sunset_time = test_time.replace(hour=17, minute=0, second=0) - sunrise_time = test_time.replace(hour=5, - minute=0, - second=0) + timedelta(days=1) + sunrise_time = test_time.replace(hour=5, minute=0, second=0) + + def event_date(hass, event, now=None): + if event == 'sunrise': + return sunrise_time + else: + return sunset_time with patch('homeassistant.util.dt.now', return_value=test_time): - with patch('homeassistant.components.sun.next_rising', - return_value=sunrise_time): - with patch('homeassistant.components.sun.next_setting', - return_value=sunset_time): - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: { - 'platform': 'flux', - 'name': 'flux', - 'lights': [dev1.entity_id], - 'start_colortemp': '1000', - 'stop_colortemp': '6000' - } - }) - turn_on_calls = mock_service( - self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') - self.hass.block_till_done() - fire_time_changed(self.hass, test_time) - self.hass.block_till_done() + with patch('homeassistant.helpers.sun.get_astral_event_date', + side_effect=event_date): + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'flux', + 'name': 'flux', + 'lights': [dev1.entity_id], + 'start_colortemp': '1000', + 'stop_colortemp': '6000' + } + }) + turn_on_calls = mock_service( + self.hass, light.DOMAIN, SERVICE_TURN_ON) + switch.turn_on(self.hass, 'switch.flux') + self.hass.block_till_done() + fire_time_changed(self.hass, test_time) + self.hass.block_till_done() call = turn_on_calls[-1] self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 167) self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.461, 0.389]) @@ -402,29 +415,31 @@ class TestSwitchFlux(unittest.TestCase): test_time = dt_util.now().replace(hour=17, minute=30, second=0) sunset_time = test_time.replace(hour=17, minute=0, second=0) - sunrise_time = test_time.replace(hour=5, - minute=0, - second=0) + timedelta(days=1) + sunrise_time = test_time.replace(hour=5, minute=0, second=0) + + def event_date(hass, event, now=None): + if event == 'sunrise': + return sunrise_time + else: + return sunset_time with patch('homeassistant.util.dt.now', return_value=test_time): - with patch('homeassistant.components.sun.next_rising', - return_value=sunrise_time): - with patch('homeassistant.components.sun.next_setting', - return_value=sunset_time): - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: { - 'platform': 'flux', - 'name': 'flux', - 'lights': [dev1.entity_id], - 'brightness': 255 - } - }) - turn_on_calls = mock_service( - self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') - self.hass.block_till_done() - fire_time_changed(self.hass, test_time) - self.hass.block_till_done() + with patch('homeassistant.helpers.sun.get_astral_event_date', + side_effect=event_date): + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'flux', + 'name': 'flux', + 'lights': [dev1.entity_id], + 'brightness': 255 + } + }) + turn_on_calls = mock_service( + self.hass, light.DOMAIN, SERVICE_TURN_ON) + switch.turn_on(self.hass, 'switch.flux') + self.hass.block_till_done() + fire_time_changed(self.hass, test_time) + self.hass.block_till_done() call = turn_on_calls[-1] self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 255) self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.496, 0.397]) @@ -460,30 +475,34 @@ class TestSwitchFlux(unittest.TestCase): test_time = dt_util.now().replace(hour=12, minute=0, second=0) sunset_time = test_time.replace(hour=17, minute=0, second=0) - sunrise_time = test_time.replace(hour=5, - minute=0, - second=0) + timedelta(days=1) + sunrise_time = test_time.replace(hour=5, minute=0, second=0) + + def event_date(hass, event, now=None): + if event == 'sunrise': + print('sunrise {}'.format(sunrise_time)) + return sunrise_time + else: + print('sunset {}'.format(sunset_time)) + return sunset_time with patch('homeassistant.util.dt.now', return_value=test_time): - with patch('homeassistant.components.sun.next_rising', - return_value=sunrise_time): - with patch('homeassistant.components.sun.next_setting', - return_value=sunset_time): - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: { - 'platform': 'flux', - 'name': 'flux', - 'lights': [dev1.entity_id, - dev2.entity_id, - dev3.entity_id] - } - }) - turn_on_calls = mock_service( - self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') - self.hass.block_till_done() - fire_time_changed(self.hass, test_time) - self.hass.block_till_done() + with patch('homeassistant.helpers.sun.get_astral_event_date', + side_effect=event_date): + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'flux', + 'name': 'flux', + 'lights': [dev1.entity_id, + dev2.entity_id, + dev3.entity_id] + } + }) + turn_on_calls = mock_service( + self.hass, light.DOMAIN, SERVICE_TURN_ON) + switch.turn_on(self.hass, 'switch.flux') + self.hass.block_till_done() + fire_time_changed(self.hass, test_time) + self.hass.block_till_done() call = turn_on_calls[-1] self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 171) self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.452, 0.386]) @@ -511,28 +530,30 @@ class TestSwitchFlux(unittest.TestCase): test_time = dt_util.now().replace(hour=8, minute=30, second=0) sunset_time = test_time.replace(hour=17, minute=0, second=0) - sunrise_time = test_time.replace(hour=5, - minute=0, - second=0) + timedelta(days=1) + sunrise_time = test_time.replace(hour=5, minute=0, second=0) + + def event_date(hass, event, now=None): + if event == 'sunrise': + return sunrise_time + else: + return sunset_time with patch('homeassistant.util.dt.now', return_value=test_time): - with patch('homeassistant.components.sun.next_rising', - return_value=sunrise_time): - with patch('homeassistant.components.sun.next_setting', - return_value=sunset_time): - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: { - 'platform': 'flux', - 'name': 'flux', - 'lights': [dev1.entity_id], - 'mode': 'mired' - } - }) - turn_on_calls = mock_service( - self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') - self.hass.block_till_done() - fire_time_changed(self.hass, test_time) - self.hass.block_till_done() + with patch('homeassistant.helpers.sun.get_astral_event_date', + side_effect=event_date): + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'flux', + 'name': 'flux', + 'lights': [dev1.entity_id], + 'mode': 'mired' + } + }) + turn_on_calls = mock_service( + self.hass, light.DOMAIN, SERVICE_TURN_ON) + switch.turn_on(self.hass, 'switch.flux') + self.hass.block_till_done() + fire_time_changed(self.hass, test_time) + self.hass.block_till_done() call = turn_on_calls[-1] self.assertEqual(call.data[light.ATTR_COLOR_TEMP], 269) diff --git a/tests/components/switch/test_mochad.py b/tests/components/switch/test_mochad.py index fad5b424399..0851bfbc324 100644 --- a/tests/components/switch/test_mochad.py +++ b/tests/components/switch/test_mochad.py @@ -2,6 +2,8 @@ import unittest import unittest.mock as mock +import pytest + from homeassistant.setup import setup_component from homeassistant.components import switch from homeassistant.components.switch import mochad @@ -9,6 +11,15 @@ from homeassistant.components.switch import mochad from tests.common import get_test_home_assistant +@pytest.fixture(autouse=True) +def pymochad_mock(): + """Mock pymochad.""" + with mock.patch.dict('sys.modules', { + 'pymochad': mock.MagicMock(), + }): + yield + + class TestMochadSwitchSetup(unittest.TestCase): """Test the mochad switch.""" @@ -18,17 +29,14 @@ class TestMochadSwitchSetup(unittest.TestCase): def setUp(self): """Setup things to be run when tests are started.""" - super(TestMochadSwitchSetup, self).setUp() self.hass = get_test_home_assistant() def tearDown(self): """Stop everyhing that was started.""" self.hass.stop() - super(TestMochadSwitchSetup, self).tearDown() - @mock.patch('pymochad.controller.PyMochad') @mock.patch('homeassistant.components.switch.mochad.MochadSwitch') - def test_setup_adds_proper_devices(self, mock_switch, mock_client): + def test_setup_adds_proper_devices(self, mock_switch): """Test if setup adds devices.""" good_config = { 'mochad': {}, @@ -50,12 +58,8 @@ class TestMochadSwitch(unittest.TestCase): def setUp(self): """Setup things to be run when tests are started.""" - super(TestMochadSwitch, self).setUp() self.hass = get_test_home_assistant() controller_mock = mock.MagicMock() - device_patch = mock.patch('pymochad.device.Device') - device_patch.start() - self.addCleanup(device_patch.stop) dev_dict = {'address': 'a1', 'name': 'fake_switch'} self.switch = mochad.MochadSwitch(self.hass, controller_mock, dev_dict) diff --git a/tests/components/test_alexa.py b/tests/components/test_alexa.py index 66d506d40c9..47a3e086d29 100644 --- a/tests/components/test_alexa.py +++ b/tests/components/test_alexa.py @@ -19,9 +19,6 @@ calls = [] NPR_NEWS_MP3_URL = "https://pd.npr.org/anon.npr-mp3/npr/news/newscast.mp3" -# 2016-10-10T19:51:42+00:00 -STATIC_TIME = datetime.datetime.utcfromtimestamp(1476129102) - @pytest.fixture def alexa_client(loop, hass, test_client): @@ -39,17 +36,14 @@ def alexa_client(loop, hass, test_client): "flash_briefings": { "weather": [ {"title": "Weekly forecast", - "text": "This week it will be sunny.", - "date": "2016-10-09T19:51:42.0Z"}, + "text": "This week it will be sunny."}, {"title": "Current conditions", - "text": "Currently it is 80 degrees fahrenheit.", - "date": STATIC_TIME} + "text": "Currently it is 80 degrees fahrenheit."} ], "news_audio": { "title": "NPR", "audio": NPR_NEWS_MP3_URL, "display_url": "https://npr.org", - "date": STATIC_TIME, "uid": "uuid" } }, @@ -436,16 +430,8 @@ def test_flash_briefing_date_from_str(alexa_client): req = yield from _flash_briefing_req(alexa_client, "weather") assert req.status == 200 data = yield from req.json() - assert data[0].get(alexa.ATTR_UPDATE_DATE) == "2016-10-09T19:51:42.0Z" - - -@asyncio.coroutine -def test_flash_briefing_date_from_datetime(alexa_client): - """Test the response has a valid date from a datetime object.""" - req = yield from _flash_briefing_req(alexa_client, "weather") - assert req.status == 200 - data = yield from req.json() - assert data[1].get(alexa.ATTR_UPDATE_DATE) == '2016-10-10T19:51:42.0Z' + assert isinstance(datetime.datetime.strptime(data[0].get( + alexa.ATTR_UPDATE_DATE), alexa.DATE_FORMAT), datetime.datetime) @asyncio.coroutine @@ -463,4 +449,8 @@ def test_flash_briefing_valid(alexa_client): req = yield from _flash_briefing_req(alexa_client, "news_audio") assert req.status == 200 json = yield from req.json() + assert isinstance(datetime.datetime.strptime(json[0].get( + alexa.ATTR_UPDATE_DATE), alexa.DATE_FORMAT), datetime.datetime) + json[0].pop(alexa.ATTR_UPDATE_DATE) + data[0].pop(alexa.ATTR_UPDATE_DATE) assert json == data diff --git a/tests/components/test_datadog.py b/tests/components/test_datadog.py new file mode 100644 index 00000000000..f1820c4d250 --- /dev/null +++ b/tests/components/test_datadog.py @@ -0,0 +1,184 @@ +"""The tests for the Datadog component.""" +from unittest import mock +import unittest + +from homeassistant.const import ( + EVENT_LOGBOOK_ENTRY, + EVENT_STATE_CHANGED, + STATE_OFF, + STATE_ON +) +from homeassistant.setup import setup_component +import homeassistant.components.datadog as datadog +import homeassistant.core as ha + +from tests.common import (assert_setup_component, get_test_home_assistant, + MockDependency) + + +class TestDatadog(unittest.TestCase): + """Test the Datadog component.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.stop() + + def test_invalid_config(self): + """Test invalid configuration.""" + with assert_setup_component(0): + assert not setup_component(self.hass, datadog.DOMAIN, { + datadog.DOMAIN: { + 'host1': 'host1' + } + }) + + @MockDependency('datadog', 'beer') + def test_datadog_setup_full(self, mock_datadog): + """Test setup with all data.""" + self.hass.bus.listen = mock.MagicMock() + mock_connection = mock_datadog.initialize + + assert setup_component(self.hass, datadog.DOMAIN, { + datadog.DOMAIN: { + 'host': 'host', + 'port': 123, + 'rate': 1, + 'prefix': 'foo', + } + }) + + self.assertEqual(mock_connection.call_count, 1) + self.assertEqual( + mock_connection.call_args, + mock.call(statsd_host='host', statsd_port=123) + ) + + self.assertTrue(self.hass.bus.listen.called) + self.assertEqual(EVENT_LOGBOOK_ENTRY, + self.hass.bus.listen.call_args_list[0][0][0]) + self.assertEqual(EVENT_STATE_CHANGED, + self.hass.bus.listen.call_args_list[1][0][0]) + + @MockDependency('datadog') + def test_datadog_setup_defaults(self, mock_datadog): + """Test setup with defaults.""" + self.hass.bus.listen = mock.MagicMock() + mock_connection = mock_datadog.initialize + + assert setup_component(self.hass, datadog.DOMAIN, { + datadog.DOMAIN: { + 'host': 'host', + 'port': datadog.DEFAULT_PORT, + 'prefix': datadog.DEFAULT_PREFIX, + } + }) + + self.assertEqual(mock_connection.call_count, 1) + self.assertEqual( + mock_connection.call_args, + mock.call(statsd_host='host', statsd_port=8125) + ) + self.assertTrue(self.hass.bus.listen.called) + + @MockDependency('datadog') + def test_logbook_entry(self, mock_datadog): + """Test event listener.""" + self.hass.bus.listen = mock.MagicMock() + mock_client = mock_datadog.statsd + + assert setup_component(self.hass, datadog.DOMAIN, { + datadog.DOMAIN: { + 'host': 'host', + 'rate': datadog.DEFAULT_RATE, + } + }) + + self.assertTrue(self.hass.bus.listen.called) + handler_method = self.hass.bus.listen.call_args_list[0][0][1] + + event = { + 'domain': 'automation', + 'entity_id': 'sensor.foo.bar', + 'message': 'foo bar biz', + 'name': 'triggered something' + } + handler_method(mock.MagicMock(data=event)) + + self.assertEqual(mock_client.event.call_count, 1) + self.assertEqual( + mock_client.event.call_args, + mock.call( + title="Home Assistant", + text="%%% \n **{}** {} \n %%%".format( + event['name'], + event['message'] + ), + tags=["entity:sensor.foo.bar", "domain:automation"] + ) + ) + + mock_client.event.reset_mock() + + @MockDependency('datadog') + def test_state_changed(self, mock_datadog): + """Test event listener.""" + self.hass.bus.listen = mock.MagicMock() + mock_client = mock_datadog.statsd + + assert setup_component(self.hass, datadog.DOMAIN, { + datadog.DOMAIN: { + 'host': 'host', + 'prefix': 'ha', + 'rate': datadog.DEFAULT_RATE, + } + }) + + self.assertTrue(self.hass.bus.listen.called) + handler_method = self.hass.bus.listen.call_args_list[1][0][1] + + valid = { + '1': 1, + '1.0': 1.0, + STATE_ON: 1, + STATE_OFF: 0 + } + + attributes = { + 'elevation': 3.2, + 'temperature': 5.0 + } + + for in_, out in valid.items(): + state = mock.MagicMock(domain="sensor", entity_id="sensor.foo.bar", + state=in_, attributes=attributes) + handler_method(mock.MagicMock(data={'new_state': state})) + + self.assertEqual(mock_client.gauge.call_count, 3) + + for attribute, value in attributes.items(): + mock_client.gauge.assert_has_calls([ + mock.call( + "ha.sensor.{}".format(attribute), + value, + sample_rate=1, + tags=["entity:{}".format(state.entity_id)] + ) + ]) + + self.assertEqual( + mock_client.gauge.call_args, + mock.call("ha.sensor", out, sample_rate=1, tags=[ + "entity:{}".format(state.entity_id) + ]) + ) + + mock_client.gauge.reset_mock() + + for invalid in ('foo', '', object): + handler_method(mock.MagicMock(data={ + 'new_state': ha.State('domain.test', invalid, {})})) + self.assertFalse(mock_client.gauge.called) diff --git a/tests/components/test_device_sun_light_trigger.py b/tests/components/test_device_sun_light_trigger.py index 2d2f7313199..5cd85a16a7a 100644 --- a/tests/components/test_device_sun_light_trigger.py +++ b/tests/components/test_device_sun_light_trigger.py @@ -1,17 +1,19 @@ """The tests device sun light trigger component.""" # pylint: disable=protected-access +from datetime import datetime import os import unittest +from unittest.mock import patch from homeassistant.setup import setup_component import homeassistant.loader as loader from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME from homeassistant.components import ( - device_tracker, light, sun, device_sun_light_trigger) + device_tracker, light, device_sun_light_trigger) +from homeassistant.util import dt as dt_util from tests.common import ( - get_test_config_dir, get_test_home_assistant, ensure_sun_risen, - ensure_sun_set) + get_test_config_dir, get_test_home_assistant, fire_time_changed) KNOWN_DEV_YAML_PATH = os.path.join(get_test_config_dir(), @@ -61,26 +63,26 @@ class TestDeviceSunLightTrigger(unittest.TestCase): light.DOMAIN: {CONF_PLATFORM: 'test'} })) - self.assertTrue(setup_component(self.hass, sun.DOMAIN, { - sun.DOMAIN: {sun.CONF_ELEVATION: 0}})) - def tearDown(self): # pylint: disable=invalid-name """Stop everything that was started.""" self.hass.stop() def test_lights_on_when_sun_sets(self): """Test lights go on when there is someone home and the sun sets.""" - self.assertTrue(setup_component( - self.hass, device_sun_light_trigger.DOMAIN, { - device_sun_light_trigger.DOMAIN: {}})) + test_time = datetime(2017, 4, 5, 1, 2, 3, tzinfo=dt_util.UTC) + with patch('homeassistant.util.dt.utcnow', return_value=test_time): + self.assertTrue(setup_component( + self.hass, device_sun_light_trigger.DOMAIN, { + device_sun_light_trigger.DOMAIN: {}})) - ensure_sun_risen(self.hass) light.turn_off(self.hass) self.hass.block_till_done() - ensure_sun_set(self.hass) - self.hass.block_till_done() + test_time = test_time.replace(hour=3) + with patch('homeassistant.util.dt.utcnow', return_value=test_time): + fire_time_changed(self.hass, test_time) + self.hass.block_till_done() self.assertTrue(light.is_on(self.hass)) @@ -105,17 +107,17 @@ class TestDeviceSunLightTrigger(unittest.TestCase): def test_lights_turn_on_when_coming_home_after_sun_set(self): \ # pylint: disable=invalid-name """Test lights turn on when coming home after sun set.""" - light.turn_off(self.hass) - ensure_sun_set(self.hass) + test_time = datetime(2017, 4, 5, 3, 2, 3, tzinfo=dt_util.UTC) + with patch('homeassistant.util.dt.utcnow', return_value=test_time): + light.turn_off(self.hass) + self.hass.block_till_done() - self.hass.block_till_done() + self.assertTrue(setup_component( + self.hass, device_sun_light_trigger.DOMAIN, { + device_sun_light_trigger.DOMAIN: {}})) - self.assertTrue(setup_component( - self.hass, device_sun_light_trigger.DOMAIN, { - device_sun_light_trigger.DOMAIN: {}})) + self.hass.states.set( + device_tracker.ENTITY_ID_FORMAT.format('device_2'), STATE_HOME) - self.hass.states.set( - device_tracker.ENTITY_ID_FORMAT.format('device_2'), STATE_HOME) - - self.hass.block_till_done() + self.hass.block_till_done() self.assertTrue(light.is_on(self.hass)) diff --git a/tests/components/test_discovery.py b/tests/components/test_discovery.py index d5be9c483ad..580d876982d 100644 --- a/tests/components/test_discovery.py +++ b/tests/components/test_discovery.py @@ -1,8 +1,9 @@ """The tests for the discovery component.""" import asyncio import os +from unittest.mock import patch, MagicMock -from unittest.mock import patch +import pytest from homeassistant.bootstrap import async_setup_component from homeassistant.components import discovery @@ -34,6 +35,15 @@ IGNORE_CONFIG = { } +@pytest.fixture(autouse=True) +def netdisco_mock(): + """Mock netdisco.""" + with patch.dict('sys.modules', { + 'netdisco.discovery': MagicMock(), + }): + yield + + @asyncio.coroutine def mock_discovery(hass, discoveries, config=BASE_CONFIG): """Helper to mock discoveries.""" diff --git a/tests/components/test_kira.py b/tests/components/test_kira.py new file mode 100644 index 00000000000..a80d766c3fd --- /dev/null +++ b/tests/components/test_kira.py @@ -0,0 +1,85 @@ +"""The tests for Home Assistant ffmpeg.""" + +import os +import shutil +import tempfile + +import unittest +from unittest.mock import patch, MagicMock + +import homeassistant.components.kira as kira +from homeassistant.setup import setup_component + +from tests.common import get_test_home_assistant + +TEST_CONFIG = {kira.DOMAIN: { + 'sensors': [{'name': 'test_sensor', + 'host': '127.0.0.1', + 'port': 34293}, + {'name': 'second_sensor', + 'port': 29847}], + 'remotes': [{'host': '127.0.0.1', + 'port': 34293}, + {'name': 'one_more', + 'host': '127.0.0.1', + 'port': 29847}]}} + +KIRA_CODES = """ +- name: test + code: "K 00FF" +- invalid: not_a_real_code +""" + + +class TestKiraSetup(unittest.TestCase): + """Test class for kira.""" + + # pylint: disable=invalid-name + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + _base_mock = MagicMock() + pykira = _base_mock.pykira + pykira.__file__ = 'test' + self._module_patcher = patch.dict('sys.modules', { + 'pykira': pykira + }) + self._module_patcher.start() + + self.work_dir = tempfile.mkdtemp() + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + self._module_patcher.stop() + shutil.rmtree(self.work_dir, ignore_errors=True) + + def test_kira_empty_config(self): + """Kira component should load a default sensor.""" + setup_component(self.hass, kira.DOMAIN, {}) + assert len(self.hass.data[kira.DOMAIN]['sensor']) == 1 + + def test_kira_setup(self): + """Ensure platforms are loaded correctly.""" + setup_component(self.hass, kira.DOMAIN, TEST_CONFIG) + assert len(self.hass.data[kira.DOMAIN]['sensor']) == 2 + assert sorted(self.hass.data[kira.DOMAIN]['sensor'].keys()) == \ + ['kira', 'kira_1'] + assert len(self.hass.data[kira.DOMAIN]['remote']) == 2 + assert sorted(self.hass.data[kira.DOMAIN]['remote'].keys()) == \ + ['kira', 'kira_1'] + + def test_kira_creates_codes(self): + """Kira module should create codes file if missing.""" + code_path = os.path.join(self.work_dir, 'codes.yaml') + kira.load_codes(code_path) + assert os.path.exists(code_path), \ + "Kira component didn't create codes file" + + def test_load_codes(self): + """Kira should ignore invalid codes.""" + code_path = os.path.join(self.work_dir, 'codes.yaml') + with open(code_path, 'w') as code_file: + code_file.write(KIRA_CODES) + res = kira.load_codes(code_path) + assert len(res) == 1, "Expected exactly 1 valid Kira code" diff --git a/tests/components/test_microsoft_face.py b/tests/components/test_microsoft_face.py index bb95c7e51c1..7a047a73f47 100644 --- a/tests/components/test_microsoft_face.py +++ b/tests/components/test_microsoft_face.py @@ -22,6 +22,8 @@ class TestMicrosoftFaceSetup(object): } } + self.endpoint_url = "https://westus.{0}".format(mf.FACE_API_URL) + def teardown_method(self): """Stop everything that was started.""" self.hass.stop() @@ -30,7 +32,7 @@ class TestMicrosoftFaceSetup(object): 'MicrosoftFace.update_store', return_value=mock_coro()) def test_setup_component(self, mock_update): """Setup component.""" - with assert_setup_component(2, mf.DOMAIN): + with assert_setup_component(3, mf.DOMAIN): setup_component(self.hass, mf.DOMAIN, self.config) @patch('homeassistant.components.microsoft_face.' @@ -44,7 +46,7 @@ class TestMicrosoftFaceSetup(object): 'MicrosoftFace.update_store', return_value=mock_coro()) def test_setup_component_test_service(self, mock_update): """Setup component.""" - with assert_setup_component(2, mf.DOMAIN): + with assert_setup_component(3, mf.DOMAIN): setup_component(self.hass, mf.DOMAIN, self.config) assert self.hass.services.has_service(mf.DOMAIN, 'create_group') @@ -57,19 +59,19 @@ class TestMicrosoftFaceSetup(object): def test_setup_component_test_entities(self, aioclient_mock): """Setup component.""" aioclient_mock.get( - mf.FACE_API_URL.format("persongroups"), + self.endpoint_url.format("persongroups"), text=load_fixture('microsoft_face_persongroups.json') ) aioclient_mock.get( - mf.FACE_API_URL.format("persongroups/test_group1/persons"), + self.endpoint_url.format("persongroups/test_group1/persons"), text=load_fixture('microsoft_face_persons.json') ) aioclient_mock.get( - mf.FACE_API_URL.format("persongroups/test_group2/persons"), + self.endpoint_url.format("persongroups/test_group2/persons"), text=load_fixture('microsoft_face_persons.json') ) - with assert_setup_component(2, mf.DOMAIN): + with assert_setup_component(3, mf.DOMAIN): setup_component(self.hass, mf.DOMAIN, self.config) assert len(aioclient_mock.mock_calls) == 3 @@ -95,15 +97,15 @@ class TestMicrosoftFaceSetup(object): def test_service_groups(self, mock_update, aioclient_mock): """Setup component, test groups services.""" aioclient_mock.put( - mf.FACE_API_URL.format("persongroups/service_group"), + self.endpoint_url.format("persongroups/service_group"), status=200, text="{}" ) aioclient_mock.delete( - mf.FACE_API_URL.format("persongroups/service_group"), + self.endpoint_url.format("persongroups/service_group"), status=200, text="{}" ) - with assert_setup_component(2, mf.DOMAIN): + with assert_setup_component(3, mf.DOMAIN): setup_component(self.hass, mf.DOMAIN, self.config) mf.create_group(self.hass, 'Service Group') @@ -123,29 +125,29 @@ class TestMicrosoftFaceSetup(object): def test_service_person(self, aioclient_mock): """Setup component, test person services.""" aioclient_mock.get( - mf.FACE_API_URL.format("persongroups"), + self.endpoint_url.format("persongroups"), text=load_fixture('microsoft_face_persongroups.json') ) aioclient_mock.get( - mf.FACE_API_URL.format("persongroups/test_group1/persons"), + self.endpoint_url.format("persongroups/test_group1/persons"), text=load_fixture('microsoft_face_persons.json') ) aioclient_mock.get( - mf.FACE_API_URL.format("persongroups/test_group2/persons"), + self.endpoint_url.format("persongroups/test_group2/persons"), text=load_fixture('microsoft_face_persons.json') ) - with assert_setup_component(2, mf.DOMAIN): + with assert_setup_component(3, mf.DOMAIN): setup_component(self.hass, mf.DOMAIN, self.config) assert len(aioclient_mock.mock_calls) == 3 aioclient_mock.post( - mf.FACE_API_URL.format("persongroups/test_group1/persons"), + self.endpoint_url.format("persongroups/test_group1/persons"), text=load_fixture('microsoft_face_create_person.json') ) aioclient_mock.delete( - mf.FACE_API_URL.format( + self.endpoint_url.format( "persongroups/test_group1/persons/" "25985303-c537-4467-b41d-bdb45cd95ca1"), status=200, text="{}" @@ -174,11 +176,11 @@ class TestMicrosoftFaceSetup(object): 'MicrosoftFace.update_store', return_value=mock_coro()) def test_service_train(self, mock_update, aioclient_mock): """Setup component, test train groups services.""" - with assert_setup_component(2, mf.DOMAIN): + with assert_setup_component(3, mf.DOMAIN): setup_component(self.hass, mf.DOMAIN, self.config) aioclient_mock.post( - mf.FACE_API_URL.format("persongroups/service_group/train"), + self.endpoint_url.format("persongroups/service_group/train"), status=200, text="{}" ) @@ -192,26 +194,26 @@ class TestMicrosoftFaceSetup(object): def test_service_face(self, camera_mock, aioclient_mock): """Setup component, test person face services.""" aioclient_mock.get( - mf.FACE_API_URL.format("persongroups"), + self.endpoint_url.format("persongroups"), text=load_fixture('microsoft_face_persongroups.json') ) aioclient_mock.get( - mf.FACE_API_URL.format("persongroups/test_group1/persons"), + self.endpoint_url.format("persongroups/test_group1/persons"), text=load_fixture('microsoft_face_persons.json') ) aioclient_mock.get( - mf.FACE_API_URL.format("persongroups/test_group2/persons"), + self.endpoint_url.format("persongroups/test_group2/persons"), text=load_fixture('microsoft_face_persons.json') ) self.config['camera'] = {'platform': 'demo'} - with assert_setup_component(2, mf.DOMAIN): + with assert_setup_component(3, mf.DOMAIN): setup_component(self.hass, mf.DOMAIN, self.config) assert len(aioclient_mock.mock_calls) == 3 aioclient_mock.post( - mf.FACE_API_URL.format( + self.endpoint_url.format( "persongroups/test_group2/persons/" "2ae4935b-9659-44c3-977f-61fac20d0538/persistedFaces"), status=200, text="{}" @@ -229,11 +231,11 @@ class TestMicrosoftFaceSetup(object): def test_service_status_400(self, mock_update, aioclient_mock): """Setup component, test groups services with error.""" aioclient_mock.put( - mf.FACE_API_URL.format("persongroups/service_group"), + self.endpoint_url.format("persongroups/service_group"), status=400, text="{'error': {'message': 'Error'}}" ) - with assert_setup_component(2, mf.DOMAIN): + with assert_setup_component(3, mf.DOMAIN): setup_component(self.hass, mf.DOMAIN, self.config) mf.create_group(self.hass, 'Service Group') @@ -248,11 +250,11 @@ class TestMicrosoftFaceSetup(object): def test_service_status_timeout(self, mock_update, aioclient_mock): """Setup component, test groups services with timeout.""" aioclient_mock.put( - mf.FACE_API_URL.format("persongroups/service_group"), + self.endpoint_url.format("persongroups/service_group"), status=400, exc=asyncio.TimeoutError() ) - with assert_setup_component(2, mf.DOMAIN): + with assert_setup_component(3, mf.DOMAIN): setup_component(self.hass, mf.DOMAIN, self.config) mf.create_group(self.hass, 'Service Group') diff --git a/tests/components/test_sun.py b/tests/components/test_sun.py index 659e4b1a43d..d5a4ecfcb81 100644 --- a/tests/components/test_sun.py +++ b/tests/components/test_sun.py @@ -24,118 +24,111 @@ class TestSun(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - def test_is_on(self): - """Test is_on method.""" - self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON) - self.assertTrue(sun.is_on(self.hass)) - self.hass.states.set(sun.ENTITY_ID, sun.STATE_BELOW_HORIZON) - self.assertFalse(sun.is_on(self.hass)) - def test_setting_rising(self): """Test retrieving sun setting and rising.""" - setup_component(self.hass, sun.DOMAIN, { - sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) + utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC) + with patch('homeassistant.helpers.condition.dt_util.utcnow', + return_value=utc_now): + setup_component(self.hass, sun.DOMAIN, { + sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) + + self.hass.block_till_done() + state = self.hass.states.get(sun.ENTITY_ID) from astral import Astral astral = Astral() - utc_now = dt_util.utcnow() + utc_today = utc_now.date() latitude = self.hass.config.latitude longitude = self.hass.config.longitude mod = -1 while True: - next_dawn = (astral.dawn_utc(utc_now + - timedelta(days=mod), latitude, longitude)) + next_dawn = (astral.dawn_utc( + utc_today + timedelta(days=mod), latitude, longitude)) if next_dawn > utc_now: break mod += 1 mod = -1 while True: - next_dusk = (astral.dusk_utc(utc_now + - timedelta(days=mod), latitude, longitude)) + next_dusk = (astral.dusk_utc( + utc_today + timedelta(days=mod), latitude, longitude)) if next_dusk > utc_now: break mod += 1 mod = -1 while True: - next_midnight = (astral.solar_midnight_utc(utc_now + - timedelta(days=mod), longitude)) + next_midnight = (astral.solar_midnight_utc( + utc_today + timedelta(days=mod), longitude)) if next_midnight > utc_now: break mod += 1 mod = -1 while True: - next_noon = (astral.solar_noon_utc(utc_now + - timedelta(days=mod), longitude)) + next_noon = (astral.solar_noon_utc( + utc_today + timedelta(days=mod), longitude)) if next_noon > utc_now: break mod += 1 mod = -1 while True: - next_rising = (astral.sunrise_utc(utc_now + - timedelta(days=mod), latitude, longitude)) + next_rising = (astral.sunrise_utc( + utc_today + timedelta(days=mod), latitude, longitude)) if next_rising > utc_now: break mod += 1 mod = -1 while True: - next_setting = (astral.sunset_utc(utc_now + - timedelta(days=mod), latitude, longitude)) + next_setting = (astral.sunset_utc( + utc_today + timedelta(days=mod), latitude, longitude)) if next_setting > utc_now: break mod += 1 - self.assertEqual(next_dawn, sun.next_dawn_utc(self.hass)) - self.assertEqual(next_dusk, sun.next_dusk_utc(self.hass)) - self.assertEqual(next_midnight, sun.next_midnight_utc(self.hass)) - self.assertEqual(next_noon, sun.next_noon_utc(self.hass)) - self.assertEqual(next_rising, sun.next_rising_utc(self.hass)) - self.assertEqual(next_setting, sun.next_setting_utc(self.hass)) - - # Point it at a state without the proper attributes - self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON) - self.assertIsNone(sun.next_dawn(self.hass)) - self.assertIsNone(sun.next_dusk(self.hass)) - self.assertIsNone(sun.next_midnight(self.hass)) - self.assertIsNone(sun.next_noon(self.hass)) - self.assertIsNone(sun.next_rising(self.hass)) - self.assertIsNone(sun.next_setting(self.hass)) - - # Point it at a non-existing state - self.assertIsNone(sun.next_dawn(self.hass, 'non.existing')) - self.assertIsNone(sun.next_dusk(self.hass, 'non.existing')) - self.assertIsNone(sun.next_midnight(self.hass, 'non.existing')) - self.assertIsNone(sun.next_noon(self.hass, 'non.existing')) - self.assertIsNone(sun.next_rising(self.hass, 'non.existing')) - self.assertIsNone(sun.next_setting(self.hass, 'non.existing')) + self.assertEqual(next_dawn, dt_util.parse_datetime( + state.attributes[sun.STATE_ATTR_NEXT_DAWN])) + self.assertEqual(next_dusk, dt_util.parse_datetime( + state.attributes[sun.STATE_ATTR_NEXT_DUSK])) + self.assertEqual(next_midnight, dt_util.parse_datetime( + state.attributes[sun.STATE_ATTR_NEXT_MIDNIGHT])) + self.assertEqual(next_noon, dt_util.parse_datetime( + state.attributes[sun.STATE_ATTR_NEXT_NOON])) + self.assertEqual(next_rising, dt_util.parse_datetime( + state.attributes[sun.STATE_ATTR_NEXT_RISING])) + self.assertEqual(next_setting, dt_util.parse_datetime( + state.attributes[sun.STATE_ATTR_NEXT_SETTING])) def test_state_change(self): """Test if the state changes at next setting/rising.""" - setup_component(self.hass, sun.DOMAIN, { - sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) + now = datetime(2016, 6, 1, 8, 0, 0, tzinfo=dt_util.UTC) + with patch('homeassistant.helpers.condition.dt_util.utcnow', + return_value=now): + setup_component(self.hass, sun.DOMAIN, { + sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) - if sun.is_on(self.hass): - test_state = sun.STATE_BELOW_HORIZON - test_time = sun.next_setting(self.hass) - else: - test_state = sun.STATE_ABOVE_HORIZON - test_time = sun.next_rising(self.hass) + self.hass.block_till_done() + test_time = dt_util.parse_datetime( + self.hass.states.get(sun.ENTITY_ID) + .attributes[sun.STATE_ATTR_NEXT_RISING]) self.assertIsNotNone(test_time) + self.assertEqual(sun.STATE_BELOW_HORIZON, + self.hass.states.get(sun.ENTITY_ID).state) + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: test_time + timedelta(seconds=5)}) self.hass.block_till_done() - self.assertEqual(test_state, self.hass.states.get(sun.ENTITY_ID).state) + self.assertEqual(sun.STATE_ABOVE_HORIZON, + self.hass.states.get(sun.ENTITY_ID).state) def test_norway_in_june(self): """Test location in Norway where the sun doesn't set in summer.""" @@ -150,9 +143,11 @@ class TestSun(unittest.TestCase): sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) state = self.hass.states.get(sun.ENTITY_ID) - assert state is not None - assert sun.next_rising_utc(self.hass) == \ + + assert dt_util.parse_datetime( + state.attributes[sun.STATE_ATTR_NEXT_RISING]) == \ datetime(2016, 7, 25, 23, 23, 39, tzinfo=dt_util.UTC) - assert sun.next_setting_utc(self.hass) == \ + assert dt_util.parse_datetime( + state.attributes[sun.STATE_ATTR_NEXT_SETTING]) == \ datetime(2016, 7, 26, 22, 19, 1, tzinfo=dt_util.UTC) diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py index 658a5e0be53..9ca429f6f52 100644 --- a/tests/components/test_websocket_api.py +++ b/tests/components/test_websocket_api.py @@ -50,6 +50,13 @@ def no_auth_websocket_client(hass, loop, test_client): loop.run_until_complete(ws.close()) +@pytest.fixture +def mock_low_queue(): + """Mock a low queue.""" + with patch.object(wapi, 'MAX_PENDING_MSG', 5): + yield + + @asyncio.coroutine def test_auth_via_msg(no_auth_websocket_client): """Test authenticating.""" @@ -304,3 +311,15 @@ def test_ping(websocket_client): msg = yield from websocket_client.receive_json() assert msg['id'] == 5 assert msg['type'] == wapi.TYPE_PONG + + +@asyncio.coroutine +def test_pending_msg_overflow(hass, mock_low_queue, websocket_client): + """Test get_panels command.""" + for idx in range(10): + websocket_client.send_json({ + 'id': idx + 1, + 'type': wapi.TYPE_PING, + }) + msg = yield from websocket_client.receive() + assert msg.type == WSMsgType.close diff --git a/tests/components/tts/test_google.py b/tests/components/tts/test_google.py index 9f7cc9e9d50..a68aeef80e3 100644 --- a/tests/components/tts/test_google.py +++ b/tests/components/tts/test_google.py @@ -12,6 +12,8 @@ from homeassistant.setup import setup_component from tests.common import ( get_test_home_assistant, assert_setup_component, mock_service) +from .test_init import mutagen_mock # noqa + class TestTTSGooglePlatform(object): """Test the Google speech component.""" diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index d43dcda8baf..7a15ed28f97 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -4,6 +4,7 @@ import os import shutil from unittest.mock import patch, PropertyMock +import pytest import requests import homeassistant.components.http as http @@ -19,6 +20,14 @@ from tests.common import ( mock_service) +@pytest.fixture(autouse=True) +def mutagen_mock(): + """Mock writing tags.""" + with patch('homeassistant.components.tts.SpeechManager.write_tags', + side_effect=lambda *args: args[1]): + yield + + class TestTTS(object): """Test the Google speech component.""" diff --git a/tests/components/tts/test_marytts.py b/tests/components/tts/test_marytts.py index 29e1a635462..b55236c5e8e 100644 --- a/tests/components/tts/test_marytts.py +++ b/tests/components/tts/test_marytts.py @@ -11,6 +11,8 @@ from homeassistant.components.media_player import ( from tests.common import ( get_test_home_assistant, assert_setup_component, mock_service) +from .test_init import mutagen_mock # noqa + class TestTTSMaryTTSPlatform(object): """Test the speech component.""" diff --git a/tests/components/tts/test_voicerss.py b/tests/components/tts/test_voicerss.py index 79629df6d82..2abdc0e69ff 100644 --- a/tests/components/tts/test_voicerss.py +++ b/tests/components/tts/test_voicerss.py @@ -11,6 +11,8 @@ from homeassistant.setup import setup_component from tests.common import ( get_test_home_assistant, assert_setup_component, mock_service) +from .test_init import mutagen_mock # noqa + class TestTTSVoiceRSSPlatform(object): """Test the voicerss speech component.""" diff --git a/tests/components/tts/test_yandextts.py b/tests/components/tts/test_yandextts.py index b7724d7d913..1ed92f34ebe 100644 --- a/tests/components/tts/test_yandextts.py +++ b/tests/components/tts/test_yandextts.py @@ -10,6 +10,8 @@ from homeassistant.components.media_player import ( from tests.common import ( get_test_home_assistant, assert_setup_component, mock_service) +from .test_init import mutagen_mock # noqa + class TestTTSYandexPlatform(object): """Test the speech component.""" diff --git a/tests/components/zwave/test_api.py b/tests/components/zwave/test_api.py new file mode 100644 index 00000000000..aabfd39024c --- /dev/null +++ b/tests/components/zwave/test_api.py @@ -0,0 +1,260 @@ +"""Test Z-Wave config panel.""" +import asyncio +from unittest.mock import MagicMock +from homeassistant.components.zwave import ZWAVE_NETWORK, const +from homeassistant.components.zwave.api import ( + ZWaveNodeGroupView, ZWaveNodeConfigView, ZWaveUserCodeView) +from tests.common import mock_http_component_app +from tests.mock.zwave import MockNode, MockValue + + +@asyncio.coroutine +def test_get_groups(hass, test_client): + """Test getting groupdata on node.""" + app = mock_http_component_app(hass) + ZWaveNodeGroupView().register(app.router) + + network = hass.data[ZWAVE_NETWORK] = MagicMock() + node = MockNode(node_id=2) + node.groups.associations = 'assoc' + node.groups.associations_instances = 'inst' + node.groups.label = 'the label' + node.groups.max_associations = 'max' + node.groups = {1: node.groups} + network.nodes = {2: node} + + client = yield from test_client(app) + + resp = yield from client.get('/api/zwave/groups/2') + + assert resp.status == 200 + result = yield from resp.json() + + assert result == { + '1': { + 'association_instances': 'inst', + 'associations': 'assoc', + 'label': 'the label', + 'max_associations': 'max' + } + } + + +@asyncio.coroutine +def test_get_groups_nogroups(hass, test_client): + """Test getting groupdata on node with no groups.""" + app = mock_http_component_app(hass) + ZWaveNodeGroupView().register(app.router) + + network = hass.data[ZWAVE_NETWORK] = MagicMock() + node = MockNode(node_id=2) + + network.nodes = {2: node} + + client = yield from test_client(app) + + resp = yield from client.get('/api/zwave/groups/2') + + assert resp.status == 200 + result = yield from resp.json() + + assert result == {} + + +@asyncio.coroutine +def test_get_groups_nonode(hass, test_client): + """Test getting groupdata on nonexisting node.""" + app = mock_http_component_app(hass) + ZWaveNodeGroupView().register(app.router) + + network = hass.data[ZWAVE_NETWORK] = MagicMock() + network.nodes = {1: 1, 5: 5} + + client = yield from test_client(app) + + resp = yield from client.get('/api/zwave/groups/2') + + assert resp.status == 404 + result = yield from resp.json() + + assert result == {'message': 'Node not found'} + + +@asyncio.coroutine +def test_get_config(hass, test_client): + """Test getting config on node.""" + app = mock_http_component_app(hass) + ZWaveNodeConfigView().register(app.router) + + network = hass.data[ZWAVE_NETWORK] = MagicMock() + node = MockNode(node_id=2) + value = MockValue( + index=12, + command_class=const.COMMAND_CLASS_CONFIGURATION) + value.label = 'label' + value.help = 'help' + value.type = 'type' + value.data = 'data' + value.data_items = ['item1', 'item2'] + value.max = 'max' + value.min = 'min' + node.values = {12: value} + network.nodes = {2: node} + node.get_values.return_value = node.values + + client = yield from test_client(app) + + resp = yield from client.get('/api/zwave/config/2') + + assert resp.status == 200 + result = yield from resp.json() + + assert result == {'12': {'data': 'data', + 'data_items': ['item1', 'item2'], + 'help': 'help', + 'label': 'label', + 'max': 'max', + 'min': 'min', + 'type': 'type'}} + + +@asyncio.coroutine +def test_get_config_noconfig_node(hass, test_client): + """Test getting config on node without config.""" + app = mock_http_component_app(hass) + ZWaveNodeConfigView().register(app.router) + + network = hass.data[ZWAVE_NETWORK] = MagicMock() + node = MockNode(node_id=2) + + network.nodes = {2: node} + node.get_values.return_value = node.values + + client = yield from test_client(app) + + resp = yield from client.get('/api/zwave/config/2') + + assert resp.status == 200 + result = yield from resp.json() + + assert result == {} + + +@asyncio.coroutine +def test_get_config_nonode(hass, test_client): + """Test getting config on nonexisting node.""" + app = mock_http_component_app(hass) + ZWaveNodeConfigView().register(app.router) + + network = hass.data[ZWAVE_NETWORK] = MagicMock() + network.nodes = {1: 1, 5: 5} + + client = yield from test_client(app) + + resp = yield from client.get('/api/zwave/config/2') + + assert resp.status == 404 + result = yield from resp.json() + + assert result == {'message': 'Node not found'} + + +@asyncio.coroutine +def test_get_usercodes_nonode(hass, test_client): + """Test getting usercodes on nonexisting node.""" + app = mock_http_component_app(hass) + ZWaveUserCodeView().register(app.router) + + network = hass.data[ZWAVE_NETWORK] = MagicMock() + network.nodes = {1: 1, 5: 5} + + client = yield from test_client(app) + + resp = yield from client.get('/api/zwave/usercodes/2') + + assert resp.status == 404 + result = yield from resp.json() + + assert result == {'message': 'Node not found'} + + +@asyncio.coroutine +def test_get_usercodes(hass, test_client): + """Test getting usercodes on node.""" + app = mock_http_component_app(hass) + ZWaveUserCodeView().register(app.router) + + network = hass.data[ZWAVE_NETWORK] = MagicMock() + node = MockNode(node_id=18, + command_classes=[const.COMMAND_CLASS_USER_CODE]) + value = MockValue( + index=0, + command_class=const.COMMAND_CLASS_USER_CODE) + value.genre = const.GENRE_USER + value.label = 'label' + value.data = '1234' + node.values = {0: value} + network.nodes = {18: node} + node.get_values.return_value = node.values + + client = yield from test_client(app) + + resp = yield from client.get('/api/zwave/usercodes/18') + + assert resp.status == 200 + result = yield from resp.json() + + assert result == {'0': {'code': '1234', + 'label': 'label', + 'length': 4}} + + +@asyncio.coroutine +def test_get_usercode_nousercode_node(hass, test_client): + """Test getting usercodes on node without usercodes.""" + app = mock_http_component_app(hass) + ZWaveUserCodeView().register(app.router) + + network = hass.data[ZWAVE_NETWORK] = MagicMock() + node = MockNode(node_id=18) + + network.nodes = {18: node} + node.get_values.return_value = node.values + + client = yield from test_client(app) + + resp = yield from client.get('/api/zwave/usercodes/18') + + assert resp.status == 200 + result = yield from resp.json() + + assert result == {} + + +@asyncio.coroutine +def test_get_usercodes_no_genreuser(hass, test_client): + """Test getting usercodes on node missing genre user.""" + app = mock_http_component_app(hass) + ZWaveUserCodeView().register(app.router) + + network = hass.data[ZWAVE_NETWORK] = MagicMock() + node = MockNode(node_id=18, + command_classes=[const.COMMAND_CLASS_USER_CODE]) + value = MockValue( + index=0, + command_class=const.COMMAND_CLASS_USER_CODE) + value.genre = const.GENRE_SYSTEM + value.label = 'label' + value.data = '1234' + node.values = {0: value} + network.nodes = {18: node} + node.get_values.return_value = node.values + + client = yield from test_client(app) + + resp = yield from client.get('/api/zwave/usercodes/18') + + assert resp.status == 200 + result = yield from resp.json() + + assert result == {} diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 91902f9c4a8..17fac86c748 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -1,6 +1,7 @@ """Tests for the Z-Wave init.""" import asyncio from collections import OrderedDict +from datetime import datetime from homeassistant.bootstrap import async_setup_component from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START @@ -14,7 +15,8 @@ import pytest import unittest from unittest.mock import patch, MagicMock -from tests.common import get_test_home_assistant +from tests.common import ( + get_test_home_assistant, async_fire_time_changed, mock_http_component) from tests.mock.zwave import MockNetwork, MockNode, MockValue, MockEntityValues @@ -69,6 +71,70 @@ def test_config_access_error(): assert result is None +@asyncio.coroutine +def test_network_options(hass, mock_openzwave): + """Test network options.""" + result = yield from async_setup_component(hass, 'zwave', { + 'zwave': { + 'usb_path': 'mock_usb_path', + 'config_path': 'mock_config_path', + }}) + + assert result + + network = hass.data[zwave.ZWAVE_NETWORK] + assert network.options.device == 'mock_usb_path' + assert network.options.config_path == 'mock_config_path' + + +@asyncio.coroutine +def test_auto_heal_midnight(hass, mock_openzwave): + """Test network auto-heal at midnight.""" + assert (yield from async_setup_component(hass, 'zwave', { + 'zwave': { + 'autoheal': True, + }})) + network = hass.data[zwave.ZWAVE_NETWORK] + assert not network.heal.called + + time = datetime(2017, 5, 6, 0, 0, 0) + async_fire_time_changed(hass, time) + yield from hass.async_block_till_done() + assert network.heal.called + assert len(network.heal.mock_calls) == 1 + + +@asyncio.coroutine +def test_auto_heal_disabled(hass, mock_openzwave): + """Test network auto-heal disabled.""" + assert (yield from async_setup_component(hass, 'zwave', { + 'zwave': { + 'autoheal': False, + }})) + network = hass.data[zwave.ZWAVE_NETWORK] + assert not network.heal.called + + time = datetime(2017, 5, 6, 0, 0, 0) + async_fire_time_changed(hass, time) + yield from hass.async_block_till_done() + assert not network.heal.called + + +@asyncio.coroutine +def test_frontend_panel_register(hass, mock_openzwave): + """Test network auto-heal disabled.""" + mock_http_component(hass) + hass.config.components |= set(['frontend']) + with patch('homeassistant.components.zwave.' + 'register_built_in_panel') as mock_register: + assert (yield from async_setup_component(hass, 'zwave', { + 'zwave': { + 'autoheal': False, + }})) + assert mock_register.called + assert len(mock_register.mock_calls) == 1 + + @asyncio.coroutine def test_setup_platform(hass, mock_openzwave): """Test invalid device config.""" @@ -138,6 +204,239 @@ def test_device_entity(hass, mock_openzwave): assert device.device_state_attributes[zwave.ATTR_POWER] == 50.123 +@asyncio.coroutine +def test_node_discovery(hass, mock_openzwave): + """Test discovery of a node.""" + mock_receivers = [] + + def mock_connect(receiver, signal, *args, **kwargs): + if signal == MockNetwork.SIGNAL_NODE_ADDED: + mock_receivers.append(receiver) + + with patch('pydispatch.dispatcher.connect', new=mock_connect): + yield from async_setup_component(hass, 'zwave', {'zwave': {}}) + + assert len(mock_receivers) == 1 + + node = MockNode(node_id=14) + hass.async_add_job(mock_receivers[0], node) + yield from hass.async_block_till_done() + + assert hass.states.get('zwave.mock_node_14').state is 'unknown' + + +@asyncio.coroutine +def test_node_ignored(hass, mock_openzwave): + """Test discovery of a node.""" + mock_receivers = [] + + def mock_connect(receiver, signal, *args, **kwargs): + if signal == MockNetwork.SIGNAL_NODE_ADDED: + mock_receivers.append(receiver) + + with patch('pydispatch.dispatcher.connect', new=mock_connect): + yield from async_setup_component(hass, 'zwave', {'zwave': { + 'device_config': { + 'zwave.mock_node_14': { + 'ignored': True, + }}}}) + + assert len(mock_receivers) == 1 + + node = MockNode(node_id=14) + hass.async_add_job(mock_receivers[0], node) + yield from hass.async_block_till_done() + + assert hass.states.get('zwave.mock_node_14') is None + + +@asyncio.coroutine +def test_value_discovery(hass, mock_openzwave): + """Test discovery of a node.""" + mock_receivers = [] + + def mock_connect(receiver, signal, *args, **kwargs): + if signal == MockNetwork.SIGNAL_VALUE_ADDED: + mock_receivers.append(receiver) + + with patch('pydispatch.dispatcher.connect', new=mock_connect): + yield from async_setup_component(hass, 'zwave', {'zwave': {}}) + + assert len(mock_receivers) == 1 + + node = MockNode(node_id=11, generic=const.GENERIC_TYPE_SENSOR_BINARY) + value = MockValue(data=False, node=node, index=12, instance=13, + command_class=const.COMMAND_CLASS_SENSOR_BINARY, + type=const.TYPE_BOOL, genre=const.GENRE_USER) + hass.async_add_job(mock_receivers[0], node, value) + yield from hass.async_block_till_done() + + assert hass.states.get( + 'binary_sensor.mock_node_mock_value_11_12_13').state is 'off' + + +@asyncio.coroutine +def test_value_discovery_existing_entity(hass, mock_openzwave): + """Test discovery of a node.""" + mock_receivers = [] + + def mock_connect(receiver, signal, *args, **kwargs): + if signal == MockNetwork.SIGNAL_VALUE_ADDED: + mock_receivers.append(receiver) + + with patch('pydispatch.dispatcher.connect', new=mock_connect): + yield from async_setup_component(hass, 'zwave', {'zwave': {}}) + + assert len(mock_receivers) == 1 + + node = MockNode(node_id=11, generic=const.GENERIC_TYPE_THERMOSTAT) + setpoint = MockValue( + data=22.0, node=node, index=12, instance=13, + command_class=const.COMMAND_CLASS_THERMOSTAT_SETPOINT, + genre=const.GENRE_USER, units='C') + hass.async_add_job(mock_receivers[0], node, setpoint) + yield from hass.async_block_till_done() + + assert hass.states.get('climate.mock_node_mock_value_11_12_13').attributes[ + 'temperature'] == 22.0 + assert hass.states.get('climate.mock_node_mock_value_11_12_13').attributes[ + 'current_temperature'] is None + + def mock_update(self): + self.hass.async_add_job(self.async_update_ha_state) + + with patch.object(zwave.node_entity.ZWaveBaseEntity, + 'maybe_schedule_update', new=mock_update): + temperature = MockValue( + data=23.5, node=node, index=12, instance=13, + command_class=const.COMMAND_CLASS_SENSOR_MULTILEVEL, + label='Temperature', genre=const.GENRE_USER, units='C') + hass.async_add_job(mock_receivers[0], node, temperature) + yield from hass.async_block_till_done() + + assert hass.states.get('climate.mock_node_mock_value_11_12_13').attributes[ + 'temperature'] == 22.0 + assert hass.states.get('climate.mock_node_mock_value_11_12_13').attributes[ + 'current_temperature'] == 23.5 + + +@asyncio.coroutine +def test_scene_activated(hass, mock_openzwave): + """Test scene activated event.""" + mock_receivers = [] + + def mock_connect(receiver, signal, *args, **kwargs): + if signal == MockNetwork.SIGNAL_SCENE_EVENT: + mock_receivers.append(receiver) + + with patch('pydispatch.dispatcher.connect', new=mock_connect): + yield from async_setup_component(hass, 'zwave', {'zwave': {}}) + + assert len(mock_receivers) == 1 + + events = [] + + def listener(event): + events.append(event) + + hass.bus.async_listen(const.EVENT_SCENE_ACTIVATED, listener) + + node = MockNode(node_id=11) + scene_id = 123 + hass.async_add_job(mock_receivers[0], node, scene_id) + yield from hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].data[ATTR_ENTITY_ID] == "mock_node_11" + assert events[0].data[const.ATTR_OBJECT_ID] == "mock_node_11" + assert events[0].data[const.ATTR_SCENE_ID] == scene_id + + +@asyncio.coroutine +def test_node_event_activated(hass, mock_openzwave): + """Test Node event activated event.""" + mock_receivers = [] + + def mock_connect(receiver, signal, *args, **kwargs): + if signal == MockNetwork.SIGNAL_NODE_EVENT: + mock_receivers.append(receiver) + + with patch('pydispatch.dispatcher.connect', new=mock_connect): + yield from async_setup_component(hass, 'zwave', {'zwave': {}}) + + assert len(mock_receivers) == 1 + + events = [] + + def listener(event): + events.append(event) + + hass.bus.async_listen(const.EVENT_NODE_EVENT, listener) + + node = MockNode(node_id=11) + value = 234 + hass.async_add_job(mock_receivers[0], node, value) + yield from hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].data[const.ATTR_OBJECT_ID] == "mock_node_11" + assert events[0].data[const.ATTR_BASIC_LEVEL] == value + + +@asyncio.coroutine +def test_network_ready(hass, mock_openzwave): + """Test Node network ready event.""" + mock_receivers = [] + + def mock_connect(receiver, signal, *args, **kwargs): + if signal == MockNetwork.SIGNAL_ALL_NODES_QUERIED: + mock_receivers.append(receiver) + + with patch('pydispatch.dispatcher.connect', new=mock_connect): + yield from async_setup_component(hass, 'zwave', {'zwave': {}}) + + assert len(mock_receivers) == 1 + + events = [] + + def listener(event): + events.append(event) + + hass.bus.async_listen(const.EVENT_NETWORK_COMPLETE, listener) + + hass.async_add_job(mock_receivers[0]) + yield from hass.async_block_till_done() + + assert len(events) == 1 + + +@asyncio.coroutine +def test_network_complete(hass, mock_openzwave): + """Test Node network complete event.""" + mock_receivers = [] + + def mock_connect(receiver, signal, *args, **kwargs): + if signal == MockNetwork.SIGNAL_AWAKE_NODES_QUERIED: + mock_receivers.append(receiver) + + with patch('pydispatch.dispatcher.connect', new=mock_connect): + yield from async_setup_component(hass, 'zwave', {'zwave': {}}) + + assert len(mock_receivers) == 1 + + events = [] + + def listener(event): + events.append(event) + + hass.bus.async_listen(const.EVENT_NETWORK_READY, listener) + + hass.async_add_job(mock_receivers[0]) + yield from hass.async_block_till_done() + + assert len(events) == 1 + + class TestZWaveDeviceEntityValues(unittest.TestCase): """Tests for the ZWaveDeviceEntityValues helper.""" @@ -598,6 +897,7 @@ class TestZWaveServices(unittest.TestCase): value = MockValue( index=12, command_class=const.COMMAND_CLASS_CONFIGURATION, + type=const.TYPE_BYTE, ) value_list = MockValue( index=13, @@ -612,38 +912,32 @@ class TestZWaveServices(unittest.TestCase): self.hass.services.call('zwave', 'set_config_parameter', { const.ATTR_NODE_ID: 14, const.ATTR_CONFIG_PARAMETER: 13, - const.ATTR_CONFIG_VALUE: 1, + const.ATTR_CONFIG_VALUE: 'item3', }) self.hass.block_till_done() - assert node.set_config_param.called - assert len(node.set_config_param.mock_calls) == 1 - assert node.set_config_param.mock_calls[0][1][0] == 13 - assert node.set_config_param.mock_calls[0][1][1] == 1 - assert node.set_config_param.mock_calls[0][1][2] == 2 - node.set_config_param.reset_mock() - - self.hass.services.call('zwave', 'set_config_parameter', { - const.ATTR_NODE_ID: 14, - const.ATTR_CONFIG_PARAMETER: 13, - const.ATTR_CONFIG_VALUE: 7, - }) - self.hass.block_till_done() - - assert not node.set_config_param.called - node.set_config_param.reset_mock() + assert value_list.data == 'item3' self.hass.services.call('zwave', 'set_config_parameter', { const.ATTR_NODE_ID: 14, const.ATTR_CONFIG_PARAMETER: 12, + const.ATTR_CONFIG_VALUE: 7, + }) + self.hass.block_till_done() + + assert value.data == 7 + + self.hass.services.call('zwave', 'set_config_parameter', { + const.ATTR_NODE_ID: 14, + const.ATTR_CONFIG_PARAMETER: 19, const.ATTR_CONFIG_VALUE: 0x01020304, - const.ATTR_CONFIG_SIZE: 4, + const.ATTR_CONFIG_SIZE: 4 }) self.hass.block_till_done() assert node.set_config_param.called assert len(node.set_config_param.mock_calls) == 1 - assert node.set_config_param.mock_calls[0][1][0] == 12 + assert node.set_config_param.mock_calls[0][1][0] == 19 assert node.set_config_param.mock_calls[0][1][1] == 0x01020304 assert node.set_config_param.mock_calls[0][1][2] == 4 node.set_config_param.reset_mock() diff --git a/tests/conftest.py b/tests/conftest.py index b6c9795f127..f1947a61ad0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,9 +12,9 @@ from homeassistant import util, setup from homeassistant.util import location from homeassistant.components import mqtt -from tests.common import async_test_home_assistant, mock_coro +from tests.common import async_test_home_assistant, mock_coro, INSTANCES from tests.test_util.aiohttp import mock_aiohttp_client -from tests.mock.zwave import MockNetwork +from tests.mock.zwave import MockNetwork, MockOption if os.environ.get('UVLOOP') == '1': import uvloop @@ -50,8 +50,12 @@ def verify_cleanup(): """Verify that the test has cleaned up resources correctly.""" yield - from tests import common - assert common.INST_COUNT < 2 + if len(INSTANCES) >= 2: + count = len(INSTANCES) + for inst in INSTANCES: + inst.stop() + pytest.exit("Detected non stopped instances " + "({}), aborting test run".format(count)) @pytest.fixture @@ -101,6 +105,7 @@ def mock_openzwave(): libopenzwave = base_mock.libopenzwave libopenzwave.__file__ = 'test' base_mock.network.ZWaveNetwork = MockNetwork + base_mock.option.ZWaveOption = MockOption with patch.dict('sys.modules', { 'libopenzwave': libopenzwave, diff --git a/tests/helpers/test_deprecation.py b/tests/helpers/test_deprecation.py new file mode 100644 index 00000000000..8064c2ea5d6 --- /dev/null +++ b/tests/helpers/test_deprecation.py @@ -0,0 +1,85 @@ +"""Test deprecation helpers.""" +from homeassistant.helpers.deprecation import ( + deprecated_substitute, get_deprecated) + +from unittest.mock import patch, MagicMock + + +class MockBaseClass(): + """Mock base class for deprecated testing.""" + + @property + @deprecated_substitute('old_property') + def new_property(self): + """Test property to fetch.""" + raise NotImplementedError() + + +class MockDeprecatedClass(MockBaseClass): + """Mock deprecated class object.""" + + @property + def old_property(self): + """Test property to fetch.""" + return True + + +class MockUpdatedClass(MockBaseClass): + """Mock updated class object.""" + + @property + def new_property(self): + """Test property to fetch.""" + return True + + +@patch('logging.getLogger') +def test_deprecated_substitute_old_class(mock_get_logger): + """Test deprecated class object.""" + mock_logger = MagicMock() + mock_get_logger.return_value = mock_logger + + mock_object = MockDeprecatedClass() + assert mock_object.new_property is True + assert mock_object.new_property is True + assert mock_logger.warning.called + assert len(mock_logger.warning.mock_calls) == 1 + + +@patch('logging.getLogger') +def test_deprecated_substitute_new_class(mock_get_logger): + """Test deprecated class object.""" + mock_logger = MagicMock() + mock_get_logger.return_value = mock_logger + + mock_object = MockUpdatedClass() + assert mock_object.new_property is True + assert mock_object.new_property is True + assert not mock_logger.warning.called + + +@patch('logging.getLogger') +def test_config_get_deprecated_old(mock_get_logger): + """Test deprecated class object.""" + mock_logger = MagicMock() + mock_get_logger.return_value = mock_logger + + config = { + 'old_name': True, + } + assert get_deprecated(config, 'new_name', 'old_name') is True + assert mock_logger.warning.called + assert len(mock_logger.warning.mock_calls) == 1 + + +@patch('logging.getLogger') +def test_config_get_deprecated_new(mock_get_logger): + """Test deprecated class object.""" + mock_logger = MagicMock() + mock_get_logger.return_value = mock_logger + + config = { + 'new_name': True, + } + assert get_deprecated(config, 'new_name', 'old_name') is True + assert not mock_logger.warning.called diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index ac60aae3fab..37ff8ba297e 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -25,6 +25,7 @@ from homeassistant.components import sun import homeassistant.util.dt as dt_util from tests.common import get_test_home_assistant +from unittest.mock import patch class TestEventHelpers(unittest.TestCase): @@ -302,24 +303,27 @@ class TestEventHelpers(unittest.TestCase): # Get next sunrise/sunset astral = Astral() - utc_now = dt_util.utcnow() + utc_now = datetime(2014, 5, 24, 12, 0, 0, tzinfo=dt_util.UTC) + utc_today = utc_now.date() mod = -1 while True: - next_rising = (astral.sunrise_utc(utc_now + - timedelta(days=mod), latitude, longitude)) + next_rising = (astral.sunrise_utc( + utc_today + timedelta(days=mod), latitude, longitude)) if next_rising > utc_now: break mod += 1 # Track sunrise runs = [] - unsub = track_sunrise(self.hass, lambda: runs.append(1)) + with patch('homeassistant.util.dt.utcnow', return_value=utc_now): + unsub = track_sunrise(self.hass, lambda: runs.append(1)) offset_runs = [] offset = timedelta(minutes=30) - unsub2 = track_sunrise(self.hass, lambda: offset_runs.append(1), - offset) + with patch('homeassistant.util.dt.utcnow', return_value=utc_now): + unsub2 = track_sunrise(self.hass, lambda: offset_runs.append(1), + offset) # run tests self._send_time_changed(next_rising - offset) @@ -334,7 +338,7 @@ class TestEventHelpers(unittest.TestCase): self._send_time_changed(next_rising + offset) self.hass.block_till_done() - self.assertEqual(2, len(runs)) + self.assertEqual(1, len(runs)) self.assertEqual(1, len(offset_runs)) unsub() @@ -342,7 +346,7 @@ class TestEventHelpers(unittest.TestCase): self._send_time_changed(next_rising + offset) self.hass.block_till_done() - self.assertEqual(2, len(runs)) + self.assertEqual(1, len(runs)) self.assertEqual(1, len(offset_runs)) def test_track_sunset(self): @@ -358,23 +362,27 @@ class TestEventHelpers(unittest.TestCase): # Get next sunrise/sunset astral = Astral() - utc_now = dt_util.utcnow() + utc_now = datetime(2014, 5, 24, 12, 0, 0, tzinfo=dt_util.UTC) + utc_today = utc_now.date() mod = -1 while True: - next_setting = (astral.sunset_utc(utc_now + - timedelta(days=mod), latitude, longitude)) + next_setting = (astral.sunset_utc( + utc_today + timedelta(days=mod), latitude, longitude)) if next_setting > utc_now: break mod += 1 # Track sunset runs = [] - unsub = track_sunset(self.hass, lambda: runs.append(1)) + with patch('homeassistant.util.dt.utcnow', return_value=utc_now): + unsub = track_sunset(self.hass, lambda: runs.append(1)) offset_runs = [] offset = timedelta(minutes=30) - unsub2 = track_sunset(self.hass, lambda: offset_runs.append(1), offset) + with patch('homeassistant.util.dt.utcnow', return_value=utc_now): + unsub2 = track_sunset( + self.hass, lambda: offset_runs.append(1), offset) # Run tests self._send_time_changed(next_setting - offset) @@ -389,7 +397,7 @@ class TestEventHelpers(unittest.TestCase): self._send_time_changed(next_setting + offset) self.hass.block_till_done() - self.assertEqual(2, len(runs)) + self.assertEqual(1, len(runs)) self.assertEqual(1, len(offset_runs)) unsub() @@ -397,7 +405,7 @@ class TestEventHelpers(unittest.TestCase): self._send_time_changed(next_setting + offset) self.hass.block_till_done() - self.assertEqual(2, len(runs)) + self.assertEqual(1, len(runs)) self.assertEqual(1, len(offset_runs)) def _send_time_changed(self, now): diff --git a/tests/helpers/test_sun.py b/tests/helpers/test_sun.py new file mode 100644 index 00000000000..2cfe28e5178 --- /dev/null +++ b/tests/helpers/test_sun.py @@ -0,0 +1,227 @@ +"""The tests for the Sun helpers.""" +# pylint: disable=protected-access +import unittest +from unittest.mock import patch +from datetime import timedelta, datetime + +import homeassistant.util.dt as dt_util +import homeassistant.helpers.sun as sun + +from tests.common import get_test_home_assistant + + +# pylint: disable=invalid-name +class TestSun(unittest.TestCase): + """Test the sun helpers.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + def test_next_events(self): + """Test retrieving next sun events.""" + utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC) + from astral import Astral + + astral = Astral() + utc_today = utc_now.date() + + latitude = self.hass.config.latitude + longitude = self.hass.config.longitude + + mod = -1 + while True: + next_dawn = (astral.dawn_utc( + utc_today + timedelta(days=mod), latitude, longitude)) + if next_dawn > utc_now: + break + mod += 1 + + mod = -1 + while True: + next_dusk = (astral.dusk_utc( + utc_today + timedelta(days=mod), latitude, longitude)) + if next_dusk > utc_now: + break + mod += 1 + + mod = -1 + while True: + next_midnight = (astral.solar_midnight_utc( + utc_today + timedelta(days=mod), longitude)) + if next_midnight > utc_now: + break + mod += 1 + + mod = -1 + while True: + next_noon = (astral.solar_noon_utc( + utc_today + timedelta(days=mod), longitude)) + if next_noon > utc_now: + break + mod += 1 + + mod = -1 + while True: + next_rising = (astral.sunrise_utc( + utc_today + timedelta(days=mod), latitude, longitude)) + if next_rising > utc_now: + break + mod += 1 + + mod = -1 + while True: + next_setting = (astral.sunset_utc( + utc_today + timedelta(days=mod), latitude, longitude)) + if next_setting > utc_now: + break + mod += 1 + + with patch('homeassistant.helpers.condition.dt_util.utcnow', + return_value=utc_now): + self.assertEqual(next_dawn, sun.get_astral_event_next( + self.hass, 'dawn')) + self.assertEqual(next_dusk, sun.get_astral_event_next( + self.hass, 'dusk')) + self.assertEqual(next_midnight, sun.get_astral_event_next( + self.hass, 'solar_midnight')) + self.assertEqual(next_noon, sun.get_astral_event_next( + self.hass, 'solar_noon')) + self.assertEqual(next_rising, sun.get_astral_event_next( + self.hass, 'sunrise')) + self.assertEqual(next_setting, sun.get_astral_event_next( + self.hass, 'sunset')) + + def test_date_events(self): + """Test retrieving next sun events.""" + utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC) + from astral import Astral + + astral = Astral() + utc_today = utc_now.date() + + latitude = self.hass.config.latitude + longitude = self.hass.config.longitude + + dawn = astral.dawn_utc(utc_today, latitude, longitude) + dusk = astral.dusk_utc(utc_today, latitude, longitude) + midnight = astral.solar_midnight_utc(utc_today, longitude) + noon = astral.solar_noon_utc(utc_today, longitude) + sunrise = astral.sunrise_utc(utc_today, latitude, longitude) + sunset = astral.sunset_utc(utc_today, latitude, longitude) + + self.assertEqual(dawn, sun.get_astral_event_date( + self.hass, 'dawn', utc_today)) + self.assertEqual(dusk, sun.get_astral_event_date( + self.hass, 'dusk', utc_today)) + self.assertEqual(midnight, sun.get_astral_event_date( + self.hass, 'solar_midnight', utc_today)) + self.assertEqual(noon, sun.get_astral_event_date( + self.hass, 'solar_noon', utc_today)) + self.assertEqual(sunrise, sun.get_astral_event_date( + self.hass, 'sunrise', utc_today)) + self.assertEqual(sunset, sun.get_astral_event_date( + self.hass, 'sunset', utc_today)) + + def test_date_events_default_date(self): + """Test retrieving next sun events.""" + utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC) + from astral import Astral + + astral = Astral() + utc_today = utc_now.date() + + latitude = self.hass.config.latitude + longitude = self.hass.config.longitude + + dawn = astral.dawn_utc(utc_today, latitude, longitude) + dusk = astral.dusk_utc(utc_today, latitude, longitude) + midnight = astral.solar_midnight_utc(utc_today, longitude) + noon = astral.solar_noon_utc(utc_today, longitude) + sunrise = astral.sunrise_utc(utc_today, latitude, longitude) + sunset = astral.sunset_utc(utc_today, latitude, longitude) + + with patch('homeassistant.util.dt.now', return_value=utc_now): + self.assertEqual(dawn, sun.get_astral_event_date( + self.hass, 'dawn', utc_today)) + self.assertEqual(dusk, sun.get_astral_event_date( + self.hass, 'dusk', utc_today)) + self.assertEqual(midnight, sun.get_astral_event_date( + self.hass, 'solar_midnight', utc_today)) + self.assertEqual(noon, sun.get_astral_event_date( + self.hass, 'solar_noon', utc_today)) + self.assertEqual(sunrise, sun.get_astral_event_date( + self.hass, 'sunrise', utc_today)) + self.assertEqual(sunset, sun.get_astral_event_date( + self.hass, 'sunset', utc_today)) + + def test_date_events_accepts_datetime(self): + """Test retrieving next sun events.""" + utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC) + from astral import Astral + + astral = Astral() + utc_today = utc_now.date() + + latitude = self.hass.config.latitude + longitude = self.hass.config.longitude + + dawn = astral.dawn_utc(utc_today, latitude, longitude) + dusk = astral.dusk_utc(utc_today, latitude, longitude) + midnight = astral.solar_midnight_utc(utc_today, longitude) + noon = astral.solar_noon_utc(utc_today, longitude) + sunrise = astral.sunrise_utc(utc_today, latitude, longitude) + sunset = astral.sunset_utc(utc_today, latitude, longitude) + + self.assertEqual(dawn, sun.get_astral_event_date( + self.hass, 'dawn', utc_now)) + self.assertEqual(dusk, sun.get_astral_event_date( + self.hass, 'dusk', utc_now)) + self.assertEqual(midnight, sun.get_astral_event_date( + self.hass, 'solar_midnight', utc_now)) + self.assertEqual(noon, sun.get_astral_event_date( + self.hass, 'solar_noon', utc_now)) + self.assertEqual(sunrise, sun.get_astral_event_date( + self.hass, 'sunrise', utc_now)) + self.assertEqual(sunset, sun.get_astral_event_date( + self.hass, 'sunset', utc_now)) + + def test_is_up(self): + """Test retrieving next sun events.""" + utc_now = datetime(2016, 11, 1, 12, 0, 0, tzinfo=dt_util.UTC) + with patch('homeassistant.helpers.condition.dt_util.utcnow', + return_value=utc_now): + self.assertFalse(sun.is_up(self.hass)) + + utc_now = datetime(2016, 11, 1, 18, 0, 0, tzinfo=dt_util.UTC) + with patch('homeassistant.helpers.condition.dt_util.utcnow', + return_value=utc_now): + self.assertTrue(sun.is_up(self.hass)) + + def test_norway_in_june(self): + """Test location in Norway where the sun doesn't set in summer.""" + self.hass.config.latitude = 69.6 + self.hass.config.longitude = 18.8 + + june = datetime(2016, 6, 1, tzinfo=dt_util.UTC) + + print(sun.get_astral_event_date(self.hass, 'sunrise', + datetime(2017, 7, 25))) + print(sun.get_astral_event_date(self.hass, 'sunset', + datetime(2017, 7, 25))) + + print(sun.get_astral_event_date(self.hass, 'sunrise', + datetime(2017, 7, 26))) + print(sun.get_astral_event_date(self.hass, 'sunset', + datetime(2017, 7, 26))) + + assert sun.get_astral_event_next(self.hass, 'sunrise', june) == \ + datetime(2016, 7, 25, 23, 23, 39, tzinfo=dt_util.UTC) + assert sun.get_astral_event_next(self.hass, 'sunset', june) == \ + datetime(2016, 7, 26, 22, 19, 1, tzinfo=dt_util.UTC) + assert sun.get_astral_event_date(self.hass, 'sunrise', june) is None + assert sun.get_astral_event_date(self.hass, 'sunset', june) is None diff --git a/tests/mock/zwave.py b/tests/mock/zwave.py index 513c606aab2..672cc884904 100644 --- a/tests/mock/zwave.py +++ b/tests/mock/zwave.py @@ -32,6 +32,23 @@ def notification(node_id, network=None): ) +class MockOption(MagicMock): + """Mock Z-Wave options.""" + + def __init__(self, device=None, config_path=None, user_path=None, + cmd_line=None): + """Initialize a Z-Wave mock options.""" + super().__init__() + self.device = device + self.config_path = config_path + self.user_path = user_path + self.cmd_line = cmd_line + + def _get_child_mock(self, **kw): + """Create child mocks with right MagicMock class.""" + return MagicMock(**kw) + + class MockNetwork(MagicMock): """Mock Z-Wave network.""" @@ -84,9 +101,10 @@ class MockNetwork(MagicMock): STATE_AWAKED = 7 STATE_READY = 10 - def __init__(self, *args, **kwargs): + def __init__(self, options=None, *args, **kwargs): """Initialize a Z-Wave mock network.""" super().__init__() + self.options = options self.state = MockNetwork.STATE_STOPPED diff --git a/tests/testing_config/kira_codes.yaml b/tests/testing_config/kira_codes.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tox.ini b/tox.ini index a1a04bd2ea7..aede0246209 100644 --- a/tox.ini +++ b/tox.ini @@ -14,12 +14,14 @@ install_command = /usr/bin/env LANG=C.UTF-8 pip install {opts} {packages} commands = py.test --timeout=30 --duration=10 --cov --cov-report= {posargs} deps = - -r{toxinidir}/requirements_all.txt - -r{toxinidir}/requirements_test.txt + -r{toxinidir}/requirements_test_all.txt [testenv:lint] basepython = python3 ignore_errors = True +deps = + -r{toxinidir}/requirements_all.txt + -r{toxinidir}/requirements_test.txt commands = flake8 pylint homeassistant diff --git a/virtualization/Docker/Dockerfile.dev b/virtualization/Docker/Dockerfile.dev index 5d16e9400ef..2f40ea5f409 100644 --- a/virtualization/Docker/Dockerfile.dev +++ b/virtualization/Docker/Dockerfile.dev @@ -2,14 +2,13 @@ # Based on the production Dockerfile, but with development additions. # Keep this file as close as possible to the production Dockerfile, so the environments match. -FROM python:3.5 +FROM python:3.6 MAINTAINER Paulus Schoutsen # Uncomment any of the following lines to disable the installation. #ENV INSTALL_TELLSTICK no #ENV INSTALL_OPENALPR no #ENV INSTALL_FFMPEG no -#ENV INSTALL_OPENZWAVE no #ENV INSTALL_LIBCEC no #ENV INSTALL_PHANTOMJS no #ENV INSTALL_COAP_CLIENT no @@ -38,11 +37,11 @@ RUN curl -sL https://deb.nodesource.com/setup_7.x | bash - && \ RUN pip3 install --no-cache-dir tox # Copy over everything required to run tox -COPY requirements_test.txt setup.cfg setup.py tox.ini ./ +COPY requirements_test_all.txt setup.cfg setup.py tox.ini ./ COPY homeassistant/const.py homeassistant/const.py # Prefetch dependencies for tox -RUN tox -e py35 --notest +RUN tox -e py36 --notest # END: Development additions diff --git a/virtualization/Docker/scripts/python_openzwave b/virtualization/Docker/scripts/python_openzwave deleted file mode 100755 index 85a41890186..00000000000 --- a/virtualization/Docker/scripts/python_openzwave +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh -# Sets up python-openzwave. -# Dependencies that need to be installed: -# apt-get install cython3 libudev-dev python3-sphinx python3-setuptools - -# Stop on errors -set -e - -cd "$(dirname "$0")/.." - -if [ ! -d build ]; then - mkdir build -fi - -cd build - -if [ -d python-openzwave ]; then - cd python-openzwave - git checkout v0.3.3 -else - git clone --branch v0.3.3 --recursive --depth 1 https://github.com/OpenZWave/python-openzwave.git - cd python-openzwave -fi - -pip3 install --upgrade cython==0.24.1 -PYTHON_EXEC=`which python3` make build -PYTHON_EXEC=`which python3` make install - -mkdir -p /usr/local/share/python-openzwave -cp -R openzwave/config /usr/local/share/python-openzwave/config diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs index 69f76e927e2..a6bf716312d 100755 --- a/virtualization/Docker/setup_docker_prereqs +++ b/virtualization/Docker/setup_docker_prereqs @@ -7,7 +7,6 @@ set -e INSTALL_TELLSTICK="${INSTALL_TELLSTICK:-yes}" INSTALL_OPENALPR="${INSTALL_OPENALPR:-yes}" INSTALL_FFMPEG="${INSTALL_FFMPEG:-yes}" -INSTALL_OPENZWAVE="${INSTALL_OPENZWAVE:-yes}" INSTALL_LIBCEC="${INSTALL_LIBCEC:-yes}" INSTALL_PHANTOMJS="${INSTALL_PHANTOMJS:-yes}" INSTALL_COAP_CLIENT="${INSTALL_COAP_CLIENT:-yes}" @@ -24,13 +23,13 @@ PACKAGES=( bluetooth libglib2.0-dev libbluetooth-dev # homeassistant.components.device_tracker.owntracks libsodium13 + # homeassistant.components.zwave + libudev-dev ) # Required debian packages for building dependencies PACKAGES_DEV=( cmake git - # python-openzwave - cython3 libudev-dev # libcec swig ) @@ -51,10 +50,6 @@ if [ "$INSTALL_FFMPEG" == "yes" ]; then virtualization/Docker/scripts/ffmpeg fi -if [ "$INSTALL_OPENZWAVE" == "yes" ]; then - virtualization/Docker/scripts/python_openzwave -fi - if [ "$INSTALL_LIBCEC" == "yes" ]; then virtualization/Docker/scripts/libcec fi