diff --git a/.coveragerc b/.coveragerc index cd86d001e37..d572fe7afc0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -28,6 +28,9 @@ omit = homeassistant/components/envisalink.py homeassistant/components/*/envisalink.py + homeassistant/components/google.py + homeassistant/components/*/google.py + homeassistant/components/insteon_hub.py homeassistant/components/*/insteon_hub.py @@ -98,6 +101,9 @@ omit = homeassistant/components/netatmo.py homeassistant/components/*/netatmo.py + homeassistant/components/neato.py + homeassistant/components/*/neato.py + homeassistant/components/homematic.py homeassistant/components/*/homematic.py @@ -132,7 +138,7 @@ omit = homeassistant/components/climate/knx.py homeassistant/components/climate/proliphix.py homeassistant/components/climate/radiotherm.py - homeassistant/components/cover/garadget.py + homeassistant/components/cover/garadget.py homeassistant/components/cover/homematic.py homeassistant/components/cover/rpi_gpio.py homeassistant/components/cover/scsgate.py @@ -144,12 +150,14 @@ omit = homeassistant/components/device_tracker/bluetooth_le_tracker.py homeassistant/components/device_tracker/bluetooth_tracker.py homeassistant/components/device_tracker/bt_home_hub_5.py + homeassistant/components/device_tracker/cisco_ios.py homeassistant/components/device_tracker/fritz.py homeassistant/components/device_tracker/icloud.py homeassistant/components/device_tracker/luci.py homeassistant/components/device_tracker/netgear.py homeassistant/components/device_tracker/nmap_tracker.py homeassistant/components/device_tracker/snmp.py + homeassistant/components/device_tracker/swisscom.py homeassistant/components/device_tracker/thomson.py homeassistant/components/device_tracker/tomato.py homeassistant/components/device_tracker/tplink.py @@ -278,6 +286,7 @@ omit = homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/pi_hole.py homeassistant/components/sensor/plex.py + homeassistant/components/sensor/pvoutput.py homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/scrape.py homeassistant/components/sensor/serial_pm.py @@ -306,7 +315,6 @@ omit = homeassistant/components/switch/edimax.py homeassistant/components/switch/hikvisioncam.py homeassistant/components/switch/mystrom.py - homeassistant/components/switch/neato.py homeassistant/components/switch/netio.py homeassistant/components/switch/orvibo.py homeassistant/components/switch/pilight.py diff --git a/.hound.yml b/.hound.yml new file mode 100644 index 00000000000..c5ab91614dc --- /dev/null +++ b/.hound.yml @@ -0,0 +1,2 @@ +python: + enabled: true diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 31e404ad87a..59061f40754 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -4,7 +4,7 @@ import logging import logging.handlers import os import sys -from collections import defaultdict +from collections import OrderedDict from types import ModuleType from typing import Any, Optional, Dict @@ -57,7 +57,7 @@ def async_setup_component(hass: core.HomeAssistant, domain: str, yield from hass.loop.run_in_executor(None, loader.prepare, hass) if config is None: - config = defaultdict(dict) + config = {} components = loader.load_order_component(domain) @@ -142,6 +142,7 @@ def _async_setup_component(hass: core.HomeAssistant, async_comp = hasattr(component, 'async_setup') try: + _LOGGER.info("Setting up %s", domain) if async_comp: result = yield from component.async_setup(hass, config) else: @@ -165,15 +166,6 @@ def _async_setup_component(hass: core.HomeAssistant, hass.config.components.append(component.DOMAIN) - # Assumption: if a component does not depend on groups - # it communicates with devices - if (not async_comp and - 'group' not in getattr(component, 'DEPENDENCIES', [])): - if hass.pool is None: - hass.async_init_pool() - if hass.pool.worker_count <= 10: - hass.pool.add_worker() - hass.bus.async_fire( EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN} ) @@ -353,7 +345,7 @@ def from_config_dict(config: Dict[str, Any], # run task future = asyncio.Future(loop=hass.loop) - hass.loop.create_task(_async_init_from_config_dict(future)) + hass.async_add_job(_async_init_from_config_dict(future)) hass.loop.run_until_complete(future) return future.result() @@ -373,6 +365,12 @@ def async_from_config_dict(config: Dict[str, Any], Dynamically loads required components and its dependencies. This method is a coroutine. """ + setup_lock = hass.data.get('setup_lock') + if setup_lock is None: + setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop) + + yield from setup_lock.acquire() + core_config = config.get(core.DOMAIN, {}) try: @@ -396,10 +394,12 @@ def async_from_config_dict(config: Dict[str, Any], yield from hass.loop.run_in_executor(None, loader.prepare, hass) # Make a copy because we are mutating it. - # Convert it to defaultdict so components can always have config dict + # Use OrderedDict in case original one was one. # Convert values to dictionaries if they are None - config = defaultdict( - dict, {key: value or {} for key, value in config.items()}) + new_config = OrderedDict() + for key, value in config.items(): + new_config[key] = value or {} + config = new_config # Filter out the repeating and common config section [homeassistant] components = set(key.split(' ')[0] for key in config.keys() @@ -425,6 +425,8 @@ def async_from_config_dict(config: Dict[str, Any], for domain in loader.load_order_components(components): yield from _async_setup_component(hass, domain, config) + setup_lock.release() + return hass diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 81450c726f1..a4f18250d17 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -119,7 +119,7 @@ def async_setup(hass, config): tasks.append(hass.services.async_call( domain, service.service, data, blocking)) - yield from asyncio.gather(*tasks, loop=hass.loop) + yield from asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service) diff --git a/homeassistant/components/alarm_control_panel/envisalink.py b/homeassistant/components/alarm_control_panel/envisalink.py index fa013cd3ffe..e84320738a2 100644 --- a/homeassistant/components/alarm_control_panel/envisalink.py +++ b/homeassistant/components/alarm_control_panel/envisalink.py @@ -4,20 +4,45 @@ Support for Envisalink-based alarm control panels (Honeywell/DSC). For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.envisalink/ """ +from os import path import logging +import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm +import homeassistant.helpers.config_validation as cv +from homeassistant.config import load_yaml_config_file from homeassistant.components.envisalink import ( EVL_CONTROLLER, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC, CONF_PARTITIONNAME, SIGNAL_PARTITION_UPDATE, SIGNAL_KEYPAD_UPDATE) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, - STATE_UNKNOWN, STATE_ALARM_TRIGGERED) + STATE_UNKNOWN, STATE_ALARM_TRIGGERED, STATE_ALARM_PENDING, ATTR_ENTITY_ID) _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['envisalink'] +DEVICES = [] + +SERVICE_ALARM_KEYPRESS = 'envisalink_alarm_keypress' +ATTR_KEYPRESS = 'keypress' +ALARM_KEYPRESS_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_KEYPRESS): cv.string +}) + + +def alarm_keypress_handler(service): + """Map services to methods on Alarm.""" + entity_ids = service.data.get(ATTR_ENTITY_ID) + keypress = service.data.get(ATTR_KEYPRESS) + + _target_devices = [device for device in DEVICES + if device.entity_id in entity_ids] + + for device in _target_devices: + EnvisalinkAlarm.alarm_keypress(device, keypress) + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): @@ -35,8 +60,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _panic_type, EVL_CONTROLLER.alarm_state['partition'][part_num], EVL_CONTROLLER) - add_devices([_device]) + DEVICES.append(_device) + add_devices(DEVICES) + + # Register Envisalink specific services + descriptions = load_yaml_config_file( + path.join(path.dirname(__file__), 'services.yaml')) + + hass.services.register(alarm.DOMAIN, SERVICE_ALARM_KEYPRESS, + alarm_keypress_handler, + descriptions.get(SERVICE_ALARM_KEYPRESS), + schema=ALARM_KEYPRESS_SCHEMA) return True @@ -66,42 +101,64 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): @property def code_format(self): - """The characters if code is defined.""" - return self._code + """Regex for code format or None if no code is required.""" + if self._code: + return None + else: + return '^\\d{4,6}$' @property def state(self): """Return the state of the device.""" + state = STATE_UNKNOWN + if self._info['status']['alarm']: - return STATE_ALARM_TRIGGERED + state = STATE_ALARM_TRIGGERED elif self._info['status']['armed_away']: - return STATE_ALARM_ARMED_AWAY + state = STATE_ALARM_ARMED_AWAY elif self._info['status']['armed_stay']: - return STATE_ALARM_ARMED_HOME + state = STATE_ALARM_ARMED_HOME + elif self._info['status']['exit_delay']: + state = STATE_ALARM_PENDING + elif self._info['status']['entry_delay']: + state = STATE_ALARM_PENDING elif self._info['status']['alpha']: - return STATE_ALARM_DISARMED - else: - return STATE_UNKNOWN + state = STATE_ALARM_DISARMED + return state def alarm_disarm(self, code=None): """Send disarm command.""" - if self._code: - EVL_CONTROLLER.disarm_partition( - str(code), self._partition_number) + if code: + EVL_CONTROLLER.disarm_partition(str(code), + self._partition_number) + else: + EVL_CONTROLLER.disarm_partition(str(self._code), + self._partition_number) def alarm_arm_home(self, code=None): """Send arm home command.""" - if self._code: - EVL_CONTROLLER.arm_stay_partition( - str(code), self._partition_number) + if code: + EVL_CONTROLLER.arm_stay_partition(str(code), + self._partition_number) + else: + EVL_CONTROLLER.arm_stay_partition(str(self._code), + self._partition_number) def alarm_arm_away(self, code=None): """Send arm away command.""" - if self._code: - EVL_CONTROLLER.arm_away_partition( - str(code), self._partition_number) + if code: + EVL_CONTROLLER.arm_away_partition(str(code), + self._partition_number) + else: + EVL_CONTROLLER.arm_away_partition(str(self._code), + self._partition_number) def alarm_trigger(self, code=None): """Alarm trigger command. Will be used to trigger a panic alarm.""" - if self._code: - EVL_CONTROLLER.panic_alarm(self._panic_type) + EVL_CONTROLLER.panic_alarm(self._panic_type) + + def alarm_keypress(self, keypress=None): + """Send custom keypress.""" + if keypress: + EVL_CONTROLLER.keypresses_to_partition(self._partition_number, + keypress) diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index 9a7efbeaf5a..073d55508ed 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -129,7 +129,7 @@ class ManualAlarm(alarm.AlarmControlPanel): if self._pending_time: track_point_in_time( - self._hass, self.update_ha_state, + self._hass, self.async_update_ha_state, self._state_ts + self._pending_time) def alarm_arm_away(self, code=None): @@ -143,7 +143,7 @@ class ManualAlarm(alarm.AlarmControlPanel): if self._pending_time: track_point_in_time( - self._hass, self.update_ha_state, + self._hass, self.async_update_ha_state, self._state_ts + self._pending_time) def alarm_trigger(self, code=None): @@ -155,11 +155,11 @@ class ManualAlarm(alarm.AlarmControlPanel): if self._trigger_time: track_point_in_time( - self._hass, self.update_ha_state, + self._hass, self.async_update_ha_state, self._state_ts + self._pending_time) track_point_in_time( - self._hass, self.update_ha_state, + self._hass, self.async_update_ha_state, self._state_ts + self._pending_time + self._trigger_time) def _validate_code(self, code, state): diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml index 40188e32d99..6cc3946ca66 100644 --- a/homeassistant/components/alarm_control_panel/services.yaml +++ b/homeassistant/components/alarm_control_panel/services.yaml @@ -41,3 +41,14 @@ alarm_trigger: code: description: An optional code to trigger the alarm control panel with example: 1234 + +envisalink_alarm_keypress: + description: Send custom keypresses to the alarm + + fields: + entity_id: + description: Name of the alarm control panel to trigger + example: 'alarm_control_panel.downstairs' + keypress: + description: 'String to send to the alarm panel (1-6 characters)' + example: '*71' diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index e88caab6824..9ff2fbd878a 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -66,6 +66,7 @@ def _platform_validator(config): return getattr(platform, 'TRIGGER_SCHEMA')(config) + _TRIGGER_SCHEMA = vol.All( cv.ensure_list, [ @@ -165,7 +166,7 @@ def async_setup(hass, config): for entity in component.async_extract_from_service(service_call): tasks.append(entity.async_trigger( service_call.data.get(ATTR_VARIABLES), True)) - yield from asyncio.gather(*tasks, loop=hass.loop) + yield from asyncio.wait(tasks, loop=hass.loop) @asyncio.coroutine def turn_onoff_service_handler(service_call): @@ -174,7 +175,7 @@ def async_setup(hass, config): method = 'async_{}'.format(service_call.service) for entity in component.async_extract_from_service(service_call): tasks.append(getattr(entity, method)()) - yield from asyncio.gather(*tasks, loop=hass.loop) + yield from asyncio.wait(tasks, loop=hass.loop) @asyncio.coroutine def toggle_service_handler(service_call): @@ -185,7 +186,7 @@ def async_setup(hass, config): tasks.append(entity.async_turn_off()) else: tasks.append(entity.async_turn_on()) - yield from asyncio.gather(*tasks, loop=hass.loop) + yield from asyncio.wait(tasks, loop=hass.loop) @asyncio.coroutine def reload_service_handler(service_call): @@ -348,8 +349,10 @@ def _async_process_config(hass, config, component): tasks.append(entity.async_enable()) entities.append(entity) - yield from asyncio.gather(*tasks, loop=hass.loop) - yield from component.async_add_entities(entities) + if tasks: + yield from asyncio.wait(tasks, loop=hass.loop) + if entities: + yield from component.async_add_entities(entities) return len(entities) > 0 diff --git a/homeassistant/components/binary_sensor/ffmpeg.py b/homeassistant/components/binary_sensor/ffmpeg.py index 72140936e18..818a6b5b387 100644 --- a/homeassistant/components/binary_sensor/ffmpeg.py +++ b/homeassistant/components/binary_sensor/ffmpeg.py @@ -138,7 +138,7 @@ class FFmpegBinarySensor(BinarySensorDevice): def _callback(self, state): """HA-FFmpeg callback for noise detection.""" self._state = state - self.update_ha_state() + self.schedule_update_ha_state() def _start_ffmpeg(self, config): """Start a FFmpeg instance.""" diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index 53c8e9a60b6..28d9566b2ab 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -8,6 +8,7 @@ import logging import voluptuous as vol +from homeassistant.core import callback import homeassistant.components.mqtt as mqtt from homeassistant.components.binary_sensor import ( BinarySensorDevice, SENSOR_CLASSES) @@ -66,17 +67,18 @@ class MqttBinarySensor(BinarySensorDevice): self._payload_off = payload_off self._qos = qos + @callback def message_received(topic, payload, qos): """A new MQTT message has been received.""" if value_template is not None: - payload = value_template.render_with_possible_json_value( + payload = value_template.async_render_with_possible_json_value( payload) if payload == self._payload_on: self._state = True - self.update_ha_state() + hass.async_add_job(self.async_update_ha_state()) elif payload == self._payload_off: self._state = False - self.update_ha_state() + hass.async_add_job(self.async_update_ha_state()) mqtt.subscribe(hass, self._state_topic, message_received, self._qos) diff --git a/homeassistant/components/binary_sensor/mysensors.py b/homeassistant/components/binary_sensor/mysensors.py index 789e188537e..e938f946457 100644 --- a/homeassistant/components/binary_sensor/mysensors.py +++ b/homeassistant/components/binary_sensor/mysensors.py @@ -22,7 +22,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info is None: return - for gateway in mysensors.GATEWAYS.values(): + gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) + if not gateways: + return + + for gateway in gateways: # Define the S_TYPES and V_TYPES that the platform should handle as # states. Map them in a dict of lists. pres = gateway.const.Presentation diff --git a/homeassistant/components/binary_sensor/netatmo.py b/homeassistant/components/binary_sensor/netatmo.py index e5004db0a4b..93b3bb5817c 100644 --- a/homeassistant/components/binary_sensor/netatmo.py +++ b/homeassistant/components/binary_sensor/netatmo.py @@ -13,7 +13,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorDevice, PLATFORM_SCHEMA) from homeassistant.components.netatmo import WelcomeData from homeassistant.loader import get_component -from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_TIMEOUT from homeassistant.helpers import config_validation as cv DEPENDENCIES = ["netatmo"] @@ -33,6 +33,7 @@ CONF_CAMERAS = 'cameras' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOME): cv.string, + vol.Optional(CONF_TIMEOUT): cv.positive_int, vol.Optional(CONF_CAMERAS, default=[]): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES.keys()): @@ -45,6 +46,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup access to Netatmo binary sensor.""" netatmo = get_component('netatmo') home = config.get(CONF_HOME, None) + timeout = config.get(CONF_TIMEOUT, 15) import lnetatmo try: @@ -62,18 +64,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None): camera_name not in config[CONF_CAMERAS]: continue for variable in sensors: - add_devices([WelcomeBinarySensor(data, camera_name, home, + add_devices([WelcomeBinarySensor(data, camera_name, home, timeout, variable)]) class WelcomeBinarySensor(BinarySensorDevice): """Represent a single binary sensor in a Netatmo Welcome device.""" - def __init__(self, data, camera_name, home, sensor): + def __init__(self, data, camera_name, home, timeout, sensor): """Setup for access to the Netatmo camera events.""" self._data = data self._camera_name = camera_name self._home = home + self._timeout = timeout if home: self._name = home + ' / ' + camera_name else: @@ -114,14 +117,17 @@ class WelcomeBinarySensor(BinarySensorDevice): if self._sensor_name == "Someone known": self._state =\ self._data.welcomedata.someoneKnownSeen(self._home, - self._camera_name) + self._camera_name, + self._timeout*60) elif self._sensor_name == "Someone unknown": self._state =\ self._data.welcomedata.someoneUnknownSeen(self._home, - self._camera_name) + self._camera_name, + self._timeout*60) elif self._sensor_name == "Motion": self._state =\ self._data.welcomedata.motionDetected(self._home, - self._camera_name) + self._camera_name, + self._timeout*60) else: return None diff --git a/homeassistant/components/binary_sensor/nx584.py b/homeassistant/components/binary_sensor/nx584.py index e158da02f2b..b21e40dc5dd 100644 --- a/homeassistant/components/binary_sensor/nx584.py +++ b/homeassistant/components/binary_sensor/nx584.py @@ -123,7 +123,7 @@ class NX584Watcher(threading.Thread): if not zone_sensor: return zone_sensor._zone['state'] = event['zone_state'] - zone_sensor.update_ha_state() + zone_sensor.schedule_update_ha_state() def _process_events(self, events): for event in events: diff --git a/homeassistant/components/binary_sensor/rpi_gpio.py b/homeassistant/components/binary_sensor/rpi_gpio.py index 13dd7d0b860..03978ac625b 100644 --- a/homeassistant/components/binary_sensor/rpi_gpio.py +++ b/homeassistant/components/binary_sensor/rpi_gpio.py @@ -72,7 +72,7 @@ class RPiGPIOBinarySensor(BinarySensorDevice): def read_gpio(port): """Read state from GPIO.""" self._state = rpi_gpio.read_input(self._port) - self.update_ha_state() + self.schedule_update_ha_state() rpi_gpio.edge_detect(self._port, read_gpio, self._bouncetime) diff --git a/homeassistant/components/binary_sensor/template.py b/homeassistant/components/binary_sensor/template.py index 6d470f335e3..e097c7c0ea4 100644 --- a/homeassistant/components/binary_sensor/template.py +++ b/homeassistant/components/binary_sensor/template.py @@ -63,7 +63,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): _LOGGER.error('No sensors added') return False - hass.loop.create_task(async_add_devices(sensors, True)) + yield from async_add_devices(sensors, True) return True @@ -84,7 +84,7 @@ class BinarySensorTemplate(BinarySensorDevice): @callback def template_bsensor_state_listener(entity, old_state, new_state): """Called when the target device changes state.""" - hass.loop.create_task(self.async_update_ha_state(True)) + hass.async_add_job(self.async_update_ha_state, True) async_track_state_change( hass, entity_ids, template_bsensor_state_listener) diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py index 2ef9c487d82..7c38d4505ae 100644 --- a/homeassistant/components/binary_sensor/trend.py +++ b/homeassistant/components/binary_sensor/trend.py @@ -4,8 +4,11 @@ A sensor that monitors trands in other components. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.trend/ """ +import asyncio import logging import voluptuous as vol + +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.components.binary_sensor import ( @@ -87,13 +90,12 @@ class SensorTrend(BinarySensorDevice): self.from_state = None self.to_state = None - self.update() - + @callback def trend_sensor_state_listener(entity, old_state, new_state): """Called when the target device changes state.""" self.from_state = old_state self.to_state = new_state - self.update_ha_state(True) + hass.async_add_job(self.async_update_ha_state(True)) track_state_change(hass, target_entity, trend_sensor_state_listener) @@ -118,7 +120,8 @@ class SensorTrend(BinarySensorDevice): """No polling needed.""" return False - def update(self): + @asyncio.coroutine + def async_update(self): """Get the latest data and update the states.""" if self.from_state is None or self.to_state is None: return diff --git a/homeassistant/components/binary_sensor/wemo.py b/homeassistant/components/binary_sensor/wemo.py index 0e3259a3a96..07deea02f6e 100644 --- a/homeassistant/components/binary_sensor/wemo.py +++ b/homeassistant/components/binary_sensor/wemo.py @@ -45,10 +45,10 @@ class WemoBinarySensor(BinarySensorDevice): _LOGGER.info( 'Subscription update for %s', _device) + self.update() if not hasattr(self, 'hass'): - self.update() return - self.update_ha_state(True) + self.schedule_update_ha_state() @property def should_poll(self): diff --git a/homeassistant/components/binary_sensor/wink.py b/homeassistant/components/binary_sensor/wink.py index 9813ca213e6..e4448d96e36 100644 --- a/homeassistant/components/binary_sensor/wink.py +++ b/homeassistant/components/binary_sensor/wink.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at at https://home-assistant.io/components/binary_sensor.wink/ """ import json +import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.sensor.wink import WinkDevice @@ -53,12 +54,17 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity): self.capability = self.wink.capability() def _pubnub_update(self, message, channel): - if 'data' in message: - json_data = json.dumps(message.get('data')) - else: - json_data = message - self.wink.pubnub_update(json.loads(json_data)) - self.update_ha_state() + try: + if 'data' in message: + json_data = json.dumps(message.get('data')) + else: + json_data = message + self.wink.pubnub_update(json.loads(json_data)) + self.update_ha_state() + except (AttributeError, KeyError): + error = "Pubnub returned invalid json for " + self.name + logging.getLogger(__name__).error(error) + self.update_ha_state(True) @property def is_on(self): diff --git a/homeassistant/components/binary_sensor/zwave.py b/homeassistant/components/binary_sensor/zwave.py index 69688c7e4f6..e99a2625ea2 100644 --- a/homeassistant/components/binary_sensor/zwave.py +++ b/homeassistant/components/binary_sensor/zwave.py @@ -96,7 +96,7 @@ class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity, Entity): """Called when a value has changed on the network.""" if self._value.value_id == value.value_id or \ self._value.node == value.node: - self.update_ha_state() + self.schedule_update_ha_state() class ZWaveTriggerSensor(ZWaveBinarySensor, Entity): @@ -112,19 +112,19 @@ class ZWaveTriggerSensor(ZWaveBinarySensor, Entity): # If it's active make sure that we set the timeout tracker if sensor_value.data: track_point_in_time( - self._hass, self.update_ha_state, + self._hass, self.async_update_ha_state, self.invalidate_after) def value_changed(self, value): """Called when a value has changed on the network.""" if self._value.value_id == value.value_id: - self.update_ha_state() + self.schedule_update_ha_state() if value.data: # only allow this value to be true for re_arm secs self.invalidate_after = dt_util.utcnow() + datetime.timedelta( seconds=self.re_arm_sec) track_point_in_time( - self._hass, self.update_ha_state, + self._hass, self.async_update_ha_state, self.invalidate_after) @property diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py new file mode 100644 index 00000000000..503b97a2b13 --- /dev/null +++ b/homeassistant/components/calendar/__init__.py @@ -0,0 +1,183 @@ +""" +Support for Google Calendar event device sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/calendar/ + +""" +import logging +import re + +from homeassistant.components.google import (CONF_OFFSET, + CONF_DEVICE_ID, + CONF_NAME) +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.helpers.config_validation import time_period_str +from homeassistant.helpers.entity import Entity, generate_entity_id +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.template import DATE_STR_FORMAT +from homeassistant.util import dt + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'calendar' +ENTITY_ID_FORMAT = DOMAIN + '.{}' + + +def setup(hass, config): + """Track states and offer events for calendars.""" + component = EntityComponent( + logging.getLogger(__name__), DOMAIN, hass, 60, DOMAIN) + + component.setup(config) + + return True + + +DEFAULT_CONF_TRACK_NEW = True +DEFAULT_CONF_OFFSET = '!!' + + +# pylint: disable=too-many-instance-attributes +class CalendarEventDevice(Entity): + """A calendar event device.""" + + # Classes overloading this must set data to an object + # with an update() method + data = None + + # pylint: disable=too-many-arguments + def __init__(self, hass, data): + """Create the Calendar Event Device.""" + self._name = data.get(CONF_NAME) + self.dev_id = data.get(CONF_DEVICE_ID) + self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET) + self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, + self.dev_id, + hass=hass) + + self._cal_data = { + 'all_day': False, + 'offset_time': dt.dt.timedelta(), + 'message': '', + 'start': None, + 'end': None, + 'location': '', + 'description': '', + } + + self.update() + + def offset_reached(self): + """Have we reached the offset time specified in the event title.""" + if self._cal_data['start'] is None or \ + self._cal_data['offset_time'] == dt.dt.timedelta(): + return False + + return self._cal_data['start'] + self._cal_data['offset_time'] <= \ + dt.now(self._cal_data['start'].tzinfo) + + @property + def name(self): + """Return the name of the entity.""" + return self._name + + @property + def device_state_attributes(self): + """State Attributes for HA.""" + start = self._cal_data.get('start', None) + end = self._cal_data.get('end', None) + start = start.strftime(DATE_STR_FORMAT) if start is not None else None + end = end.strftime(DATE_STR_FORMAT) if end is not None else None + + return { + 'message': self._cal_data.get('message', ''), + 'all_day': self._cal_data.get('all_day', False), + 'offset_reached': self.offset_reached(), + 'start_time': start, + 'end_time': end, + 'location': self._cal_data.get('location', None), + 'description': self._cal_data.get('description', None), + } + + @property + def state(self): + """Return the state of the calendar event.""" + start = self._cal_data.get('start', None) + end = self._cal_data.get('end', None) + if start is None or end is None: + return STATE_OFF + + now = dt.now() + + if start <= now and end > now: + return STATE_ON + + if now >= end: + self.cleanup() + + return STATE_OFF + + def cleanup(self): + """Cleanup any start/end listeners that were setup.""" + self._cal_data = { + 'all_day': False, + 'offset_time': 0, + 'message': '', + 'start': None, + 'end': None, + 'location': None, + 'description': None + } + + def update(self): + """Search for the next event.""" + if not self.data or not self.data.update(): + # update cached, don't do anything + return + + if not self.data.event: + # we have no event to work on, make sure we're clean + self.cleanup() + return + + def _get_date(date): + """Get the dateTime from date or dateTime as a local.""" + if 'date' in date: + return dt.as_utc(dt.dt.datetime.combine( + dt.parse_date(date['date']), dt.dt.time())) + else: + return dt.parse_datetime(date['dateTime']) + + start = _get_date(self.data.event['start']) + end = _get_date(self.data.event['end']) + + summary = self.data.event['summary'] + + # check if we have an offset tag in the message + # time is HH:MM or MM + reg = '{}([+-]?[0-9]{{0,2}}(:[0-9]{{0,2}})?)'.format(self._offset) + search = re.search(reg, summary) + if search and search.group(1): + time = search.group(1) + if ':' not in time: + if time[0] == '+' or time[0] == '-': + time = '{}0:{}'.format(time[0], time[1:]) + else: + time = '0:{}'.format(time) + + offset_time = time_period_str(time) + summary = (summary[:search.start()] + summary[search.end():]) \ + .strip() + else: + offset_time = dt.dt.timedelta() # default it + + # cleanup the string so we don't have a bunch of double+ spaces + self._cal_data['message'] = re.sub(' +', '', summary).strip() + + self._cal_data['offset_time'] = offset_time + self._cal_data['location'] = self.data.event.get('location', '') + self._cal_data['description'] = self.data.event.get('description', '') + self._cal_data['start'] = start + self._cal_data['end'] = end + self._cal_data['all_day'] = 'date' in self.data.event['start'] diff --git a/homeassistant/components/calendar/demo.py b/homeassistant/components/calendar/demo.py new file mode 100755 index 00000000000..279119a1ff5 --- /dev/null +++ b/homeassistant/components/calendar/demo.py @@ -0,0 +1,82 @@ +""" +Demo platform that has two fake binary sensors. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/demo/ +""" +import homeassistant.util.dt as dt_util +from homeassistant.components.calendar import CalendarEventDevice +from homeassistant.components.google import CONF_DEVICE_ID, CONF_NAME + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Demo binary sensor platform.""" + calendar_data_future = DemoGoogleCalendarDataFuture() + calendar_data_current = DemoGoogleCalendarDataCurrent() + add_devices([ + DemoGoogleCalendar(hass, calendar_data_future, { + CONF_NAME: 'Future Event', + CONF_DEVICE_ID: 'future_event', + }), + + DemoGoogleCalendar(hass, calendar_data_current, { + CONF_NAME: 'Current Event', + CONF_DEVICE_ID: 'current_event', + }), + ]) + + +class DemoGoogleCalendarData(object): + """Setup base class for data.""" + + # pylint: disable=no-self-use + def update(self): + """Return true so entity knows we have new data.""" + return True + + +class DemoGoogleCalendarDataFuture(DemoGoogleCalendarData): + """Setup future data event.""" + + def __init__(self): + """Set the event to a future event.""" + one_hour_from_now = dt_util.now() \ + + dt_util.dt.timedelta(minutes=30) + self.event = { + 'start': { + 'dateTime': one_hour_from_now.isoformat() + }, + 'end': { + 'dateTime': (one_hour_from_now + dt_util.dt. + timedelta(minutes=60)).isoformat() + }, + 'summary': 'Future Event', + } + + +class DemoGoogleCalendarDataCurrent(DemoGoogleCalendarData): + """Create a current event we're in the middle of.""" + + def __init__(self): + """Set the event data.""" + middle_of_event = dt_util.now() \ + - dt_util.dt.timedelta(minutes=30) + self.event = { + 'start': { + 'dateTime': middle_of_event.isoformat() + }, + 'end': { + 'dateTime': (middle_of_event + dt_util.dt. + timedelta(minutes=60)).isoformat() + }, + 'summary': 'Current Event', + } + + +class DemoGoogleCalendar(CalendarEventDevice): + """A Demo binary sensor.""" + + def __init__(self, hass, calendar_data, data): + """The same as a google calendar but without the api calls.""" + self.data = calendar_data + super().__init__(hass, data) diff --git a/homeassistant/components/calendar/google.py b/homeassistant/components/calendar/google.py new file mode 100644 index 00000000000..741b3238b49 --- /dev/null +++ b/homeassistant/components/calendar/google.py @@ -0,0 +1,79 @@ +""" +Support for Google Calendar Search binary sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.google_calendar/ +""" +# pylint: disable=import-error +import logging +from datetime import timedelta + +from homeassistant.components.calendar import CalendarEventDevice +from homeassistant.components.google import (CONF_CAL_ID, CONF_ENTITIES, + CONF_TRACK, TOKEN_FILE, + GoogleCalendarService) +from homeassistant.util import Throttle, dt + +DEFAULT_GOOGLE_SEARCH_PARAMS = { + 'orderBy': 'startTime', + 'maxResults': 1, + 'singleEvents': True, +} + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, disc_info=None): + """Setup the calendar platform for event devices.""" + if disc_info is None: + return + + if not any([data[CONF_TRACK] for data in disc_info[CONF_ENTITIES]]): + return + + calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE)) + add_devices([GoogleCalendarEventDevice(hass, calendar_service, + disc_info[CONF_CAL_ID], data) + for data in disc_info[CONF_ENTITIES] if data[CONF_TRACK]]) + + +# pylint: disable=too-many-instance-attributes +class GoogleCalendarEventDevice(CalendarEventDevice): + """A calendar event device.""" + + def __init__(self, hass, calendar_service, calendar, data): + """Create the Calendar event device.""" + self.data = GoogleCalendarData(calendar_service, calendar, + data.get('search', None)) + super().__init__(hass, data) + + +class GoogleCalendarData(object): + """Class to utilize calendar service object to get next event.""" + + def __init__(self, calendar_service, calendar_id, search=None): + """Setup how we are going to search the google calendar.""" + self.calendar_service = calendar_service + self.calendar_id = calendar_id + self.search = search + self.event = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data.""" + service = self.calendar_service.get() + params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS) + params['timeMin'] = dt.utcnow().isoformat('T') + params['calendarId'] = self.calendar_id + if self.search: + params['q'] = self.search + + events = service.events() # pylint: disable=no-member + result = events.list(**params).execute() + + items = result.get('items', []) + self.event = items[0] if len(items) == 1 else None + return True diff --git a/homeassistant/components/camera/ffmpeg.py b/homeassistant/components/camera/ffmpeg.py index c3f0ffbfe0b..bb7c7ac6cdc 100644 --- a/homeassistant/components/camera/ffmpeg.py +++ b/homeassistant/components/camera/ffmpeg.py @@ -35,7 +35,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a FFmpeg Camera.""" if not async_run_test(hass, config.get(CONF_INPUT)): return - hass.loop.create_task(async_add_devices([FFmpegCamera(hass, config)])) + yield from async_add_devices([FFmpegCamera(hass, config)]) class FFmpegCamera(Camera): @@ -85,7 +85,7 @@ class FFmpegCamera(Camera): break response.write(data) finally: - self.hass.loop.create_task(stream.close()) + self.hass.async_add_job(stream.close()) yield from response.write_eof() @property diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index e4d97987dbf..ec85e6306d4 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a generic IP Camera.""" - hass.loop.create_task(async_add_devices([GenericCamera(hass, config)])) + yield from async_add_devices([GenericCamera(hass, config)]) class GenericCamera(Camera): diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index e8799d1be34..a2c35410c55 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a MJPEG IP Camera.""" - hass.loop.create_task(async_add_devices([MjpegCamera(hass, config)])) + yield from async_add_devices([MjpegCamera(hass, config)]) def extract_image_from_mjpeg(stream): @@ -122,7 +122,7 @@ class MjpegCamera(Camera): break response.write(data) finally: - self.hass.loop.create_task(stream.release()) + self.hass.async_add_job(stream.release()) yield from response.write_eof() @property diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index bbca25fd6b6..9292e839b53 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -14,6 +14,7 @@ from aiohttp import web from aiohttp.web_exceptions import HTTPGatewayTimeout import async_timeout +from homeassistant.core import callback from homeassistant.const import ( CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP) @@ -60,8 +61,16 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a Synology IP Camera.""" if not config.get(CONF_VERIFY_SSL): connector = aiohttp.TCPConnector(verify_ssl=False) + + @asyncio.coroutine + def _async_close_connector(event): + """Close websession on shutdown.""" + yield from connector.close() + + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, _async_close_connector) else: - connector = None + connector = hass.websession.connector websession_init = aiohttp.ClientSession( loop=hass.loop, @@ -115,10 +124,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): websession = aiohttp.ClientSession( loop=hass.loop, connector=connector, cookies={'id': session_id}) - @asyncio.coroutine + @callback def _async_close_websession(event): - """Close webssesion on shutdown.""" - yield from websession.close() + """Close websession on shutdown.""" + websession.detach() hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, _async_close_websession) diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index 3030ea9090e..1a0b20dc11e 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -145,7 +145,7 @@ class GenericThermostat(ClimateDevice): def max_temp(self): """Return the maximum temperature.""" # pylint: disable=no-member - if self._min_temp: + if self._max_temp: return self._max_temp else: # Get default temp from super class @@ -158,7 +158,7 @@ class GenericThermostat(ClimateDevice): self._update_temp(new_state) self._control_heating() - self.update_ha_state() + self.schedule_update_ha_state() def _update_temp(self, state): """Update thermostat with latest state from sensor.""" diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py index 2a815625434..13a062a335e 100755 --- a/homeassistant/components/climate/mysensors.py +++ b/homeassistant/components/climate/mysensors.py @@ -24,7 +24,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the mysensors climate.""" if discovery_info is None: return - for gateway in mysensors.GATEWAYS.values(): + + gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) + if not gateways: + return + + for gateway in gateways: if float(gateway.protocol_version) < 1.5: continue pres = gateway.const.Presentation diff --git a/homeassistant/components/climate/proliphix.py b/homeassistant/components/climate/proliphix.py index 515c43f7eba..5b3708db72e 100644 --- a/homeassistant/components/climate/proliphix.py +++ b/homeassistant/components/climate/proliphix.py @@ -12,7 +12,7 @@ from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['proliphix==0.4.0'] +REQUIREMENTS = ['proliphix==0.4.1'] ATTR_FAN = 'fan' diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py new file mode 100644 index 00000000000..a0094a7c290 --- /dev/null +++ b/homeassistant/components/climate/wink.py @@ -0,0 +1,331 @@ +""" +Support for Wink thermostats. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/climate.wink/ +""" +from homeassistant.components.wink import WinkDevice +from homeassistant.components.climate import ( + STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice, + ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + ATTR_TEMPERATURE, + ATTR_CURRENT_HUMIDITY) +from homeassistant.const import ( + TEMP_CELSIUS, STATE_ON, + STATE_OFF, STATE_UNKNOWN) +from homeassistant.loader import get_component + +DEPENDENCIES = ['wink'] + +STATE_AUX = 'aux' +STATE_ECO = 'eco' + +ATTR_EXTERNAL_TEMPERATURE = "external_temperature" +ATTR_SMART_TEMPERATURE = "smart_temperature" +ATTR_ECO_TARGET = "eco_target" +ATTR_OCCUPIED = "occupied" + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Wink thermostat.""" + import pywink + temp_unit = hass.config.units.temperature_unit + add_devices(WinkThermostat(thermostat, temp_unit) + for thermostat in pywink.get_thermostats()) + + +# pylint: disable=abstract-method,too-many-public-methods, too-many-branches +class WinkThermostat(WinkDevice, ClimateDevice): + """Representation of a Wink thermostat.""" + + def __init__(self, wink, temp_unit): + """Initialize the Wink device.""" + super().__init__(wink) + wink = get_component('wink') + self._config_temp_unit = temp_unit + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + # The Wink API always returns temp in Celsius + return TEMP_CELSIUS + + @property + def device_state_attributes(self): + """Return the optional state attributes.""" + data = {} + target_temp_high = self.target_temperature_high + target_temp_low = self.target_temperature_low + if target_temp_high is not None: + data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display( + self.target_temperature_high) + if target_temp_low is not None: + data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display( + self.target_temperature_low) + + if self.external_temperature: + data[ATTR_EXTERNAL_TEMPERATURE] = self._convert_for_display( + self.external_temperature) + + if self.smart_temperature: + data[ATTR_SMART_TEMPERATURE] = self.smart_temperature + + if self.occupied: + data[ATTR_OCCUPIED] = self.occupied + + if self.eco_target: + data[ATTR_ECO_TARGET] = self.eco_target + + current_humidity = self.current_humidity + if current_humidity is not None: + data[ATTR_CURRENT_HUMIDITY] = current_humidity + + return data + + @property + def current_temperature(self): + """Return the current temperature.""" + return self.wink.current_temperature() + + @property + def current_humidity(self): + """Return the current humidity.""" + if self.wink.current_humidity() is not None: + # The API states humidity will be a float 0-1 + # the only example API response with humidity listed show an int + # This will address both possibilities + if self.wink.current_humidity() < 1: + return self.wink.current_humidity() * 100 + else: + return self.wink.current_humidity() + + @property + def external_temperature(self): + """Return the current external temperature.""" + return self.wink.current_external_temperature() + + @property + def smart_temperature(self): + """Return the current average temp of all remote sensor.""" + return self.wink.current_smart_temperature() + + @property + def eco_target(self): + """Return status of eco target (Is the termostat in eco mode).""" + return self.wink.eco_target() + + @property + def occupied(self): + """Return status of if the thermostat has detected occupancy.""" + return self.wink.occupied() + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + if not self.wink.is_on(): + current_op = STATE_OFF + elif self.wink.current_hvac_mode() == 'cool_only': + current_op = STATE_COOL + elif self.wink.current_hvac_mode() == 'heat_only': + current_op = STATE_HEAT + elif self.wink.current_hvac_mode() == 'aux': + current_op = STATE_HEAT + elif self.wink.current_hvac_mode() == 'auto': + current_op = STATE_AUTO + elif self.wink.current_hvac_mode() == 'eco': + current_op = STATE_ECO + else: + current_op = STATE_UNKNOWN + return current_op + + @property + def target_humidity(self): + """Return the humidity we try to reach.""" + target_hum = None + if self.wink.current_humidifier_mode() == 'on': + if self.wink.current_humidifier_set_point() is not None: + target_hum = self.wink.current_humidifier_set_point() * 100 + elif self.wink.current_dehumidifier_mode() == 'on': + if self.wink.current_dehumidifier_set_point() is not None: + target_hum = self.wink.current_dehumidifier_set_point() * 100 + else: + target_hum = None + return target_hum + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + if self.current_operation != STATE_AUTO and not self.is_away_mode_on: + if self.current_operation == STATE_COOL: + return self.wink.current_max_set_point() + elif self.current_operation == STATE_HEAT: + return self.wink.current_min_set_point() + else: + return None + else: + return None + + @property + def target_temperature_low(self): + """Return the lower bound temperature we try to reach.""" + if self.current_operation == STATE_AUTO: + return self.wink.current_min_set_point() + return None + + @property + def target_temperature_high(self): + """Return the higher bound temperature we try to reach.""" + if self.current_operation == STATE_AUTO: + return self.wink.current_max_set_point() + return None + + @property + def is_away_mode_on(self): + """Return if away mode is on.""" + return self.wink.away() + + @property + def is_aux_heat_on(self): + """Return true if aux heater.""" + if self.wink.current_hvac_mode() == 'aux' and self.wink.is_on(): + return True + elif self.wink.current_hvac_mode() == 'aux' and not self.wink.is_on(): + return False + else: + return None + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + target_temp = kwargs.get(ATTR_TEMPERATURE) + target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) + target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) + if target_temp is not None: + if self.current_operation == STATE_COOL: + target_temp_high = target_temp + if self.current_operation == STATE_HEAT: + target_temp_low = target_temp + if target_temp_low is not None: + target_temp_low = target_temp_low + if target_temp_high is not None: + target_temp_high = target_temp_high + self.wink.set_temperature(target_temp_low, target_temp_high) + + def set_operation_mode(self, operation_mode): + """Set operation mode.""" + if operation_mode == STATE_HEAT: + self.wink.set_operation_mode('heat_only') + elif operation_mode == STATE_COOL: + self.wink.set_operation_mode('cool_only') + elif operation_mode == STATE_AUTO: + self.wink.set_operation_mode('auto') + elif operation_mode == STATE_OFF: + self.wink.set_operation_mode('off') + elif operation_mode == STATE_AUX: + self.wink.set_operation_mode('aux') + elif operation_mode == STATE_ECO: + self.wink.set_operation_mode('eco') + + @property + def operation_list(self): + """List of available operation modes.""" + op_list = ['off'] + modes = self.wink.hvac_modes() + if 'cool_only' in modes: + op_list.append(STATE_COOL) + if 'heat_only' in modes or 'aux' in modes: + op_list.append(STATE_HEAT) + if 'auto' in modes: + op_list.append(STATE_AUTO) + if 'eco' in modes: + op_list.append(STATE_ECO) + return op_list + + def turn_away_mode_on(self): + """Turn away on.""" + self.wink.set_away_mode() + + def turn_away_mode_off(self): + """Turn away off.""" + self.wink.set_away_mode(False) + + @property + def current_fan_mode(self): + """Return whether the fan is on.""" + if self.wink.current_fan_mode() == 'on': + return STATE_ON + elif self.wink.current_fan_mode() == 'auto': + return STATE_AUTO + else: + # No Fan available so disable slider + return None + + @property + def fan_list(self): + """List of available fan modes.""" + if self.wink.has_fan(): + return self.wink.fan_modes() + return None + + def set_fan_mode(self, fan): + """Turn fan on/off.""" + self.wink.set_fan_mode(fan.lower()) + + def turn_aux_heat_on(self): + """Turn auxillary heater on.""" + self.set_operation_mode(STATE_AUX) + + def turn_aux_heat_off(self): + """Turn auxillary heater off.""" + self.set_operation_mode(STATE_AUTO) + + @property + def min_temp(self): + """Return the minimum temperature.""" + minimum = 7 # Default minimum + min_min = self.wink.min_min_set_point() + min_max = self.wink.min_max_set_point() + return_value = minimum + if self.current_operation == STATE_HEAT: + if min_min: + return_value = min_min + else: + return_value = minimum + elif self.current_operation == STATE_COOL: + if min_max: + return_value = min_max + else: + return_value = minimum + elif self.current_operation == STATE_AUTO: + if min_min and min_max: + return_value = min(min_min, min_max) + else: + return_value = minimum + else: + return_value = minimum + return return_value + + @property + def max_temp(self): + """Return the maximum temperature.""" + maximum = 35 # Default maximum + max_min = self.wink.max_min_set_point() + max_max = self.wink.max_max_set_point() + return_value = maximum + if self.current_operation == STATE_HEAT: + if max_min: + return_value = max_min + else: + return_value = maximum + elif self.current_operation == STATE_COOL: + if max_max: + return_value = max_max + else: + return_value = maximum + elif self.current_operation == STATE_AUTO: + if max_min and max_max: + return_value = min(max_min, max_max) + else: + return_value = maximum + else: + return_value = maximum + return return_value diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py index d94c4f1b94b..eb3aca7be5b 100755 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/climate/zwave.py @@ -1,5 +1,5 @@ """ -Support for ZWave climate devices. +Support for Z-Wave climate devices. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/climate.zwave/ @@ -8,8 +8,7 @@ https://home-assistant.io/components/climate.zwave/ # pylint: disable=import-error import logging from homeassistant.components.climate import DOMAIN -from homeassistant.components.climate import ( - ClimateDevice, ATTR_OPERATION_MODE) +from homeassistant.components.climate import ClimateDevice from homeassistant.components.zwave import ZWaveDeviceEntity from homeassistant.components import zwave from homeassistant.const import ( @@ -18,44 +17,23 @@ from homeassistant.const import ( _LOGGER = logging.getLogger(__name__) CONF_NAME = 'name' -DEFAULT_NAME = 'ZWave Climate' +DEFAULT_NAME = 'Z-Wave Climate' REMOTEC = 0x5254 REMOTEC_ZXT_120 = 0x8377 REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120) - -HORSTMANN = 0x0059 -HORSTMANN_HRT4_ZW = 0x3 -HORSTMANN_HRT4_ZW_THERMOSTAT = (HORSTMANN, HORSTMANN_HRT4_ZW) +ATTR_OPERATING_STATE = 'operating_state' +ATTR_FAN_STATE = 'fan_state' WORKAROUND_ZXT_120 = 'zxt_120' -WORKAROUND_HRT4_ZW = 'hrt4_zw' DEVICE_MAPPINGS = { - REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120, - HORSTMANN_HRT4_ZW_THERMOSTAT: WORKAROUND_HRT4_ZW -} - -SET_TEMP_TO_INDEX = { - 'Heat': 1, - 'Cool': 2, - 'Auto': 3, - 'Aux Heat': 4, - 'Resume': 5, - 'Fan Only': 6, - 'Furnace': 7, - 'Dry Air': 8, - 'Moist Air': 9, - 'Auto Changeover': 10, - 'Heat Econ': 11, - 'Cool Econ': 12, - 'Away': 13, - 'Unknown': 14 + REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120 } def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the ZWave Climate devices.""" + """Set up the Z-Wave Climate devices.""" if discovery_info is None or zwave.NETWORK is None: _LOGGER.debug("No discovery_info=%s or no NETWORK=%s", discovery_info, zwave.NETWORK) @@ -70,13 +48,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): - """Represents a ZWave Climate device.""" + """Representation of a Z-Wave Climate device.""" def __init__(self, value, temp_unit): - """Initialize the zwave climate device.""" + """Initialize the Z-Wave climate device.""" from openzwave.network import ZWaveNetwork from pydispatch import dispatcher ZWaveDeviceEntity.__init__(self, value, DOMAIN) + self._index = value.index self._node = value.node self._target_temperature = None self._current_temperature = None @@ -85,13 +64,12 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): self._operating_state = None self._current_fan_mode = None self._fan_list = None + self._fan_state = None self._current_swing_mode = None self._swing_list = None self._unit = temp_unit - self._index_operation = None _LOGGER.debug("temp_unit is %s", self._unit) self._zxt_120 = None - self._hrt4_zw = None self.update_properties() # register listener dispatcher.connect( @@ -106,17 +84,13 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): _LOGGER.debug("Remotec ZXT-120 Zwave Thermostat" " workaround") self._zxt_120 = 1 - if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_HRT4_ZW: - _LOGGER.debug("Horstmann HRT4-ZW Zwave Thermostat" - " workaround") - self._hrt4_zw = 1 def value_changed(self, value): """Called when a value has changed on the network.""" if self._value.value_id == value.value_id or \ self._value.node == value.node: self.update_properties() - self.update_ha_state() + self.schedule_update_ha_state() _LOGGER.debug("Value changed on network %s", value) def update_properties(self): @@ -125,23 +99,23 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): for value in self._node.get_values( class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_MODE).values(): self._current_operation = value.data - self._index_operation = SET_TEMP_TO_INDEX.get( - self._current_operation) self._operation_list = list(value.data_items) _LOGGER.debug("self._operation_list=%s", self._operation_list) _LOGGER.debug("self._current_operation=%s", self._current_operation) # Current Temp - for value in (self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL) - .values()): + for value in ( + self._node.get_values( + class_id=zwave.const.COMMAND_CLASS_SENSOR_MULTILEVEL) + .values()): if value.label == 'Temperature': - self._current_temperature = int(value.data) + self._current_temperature = round((float(value.data)), 1) self._unit = value.units # Fan Mode - for value in (self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE) - .values()): + for value in ( + self._node.get_values( + class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_MODE) + .values()): self._current_fan_mode = value.data self._fan_list = list(value.data_items) _LOGGER.debug("self._fan_list=%s", self._fan_list) @@ -149,9 +123,10 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): self._current_fan_mode) # Swing mode if self._zxt_120 == 1: - for value in (self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_CONFIGURATION) - .values()): + for value in ( + self._node.get_values( + class_id=zwave.const.COMMAND_CLASS_CONFIGURATION) + .values()): if value.command_class == \ zwave.const.COMMAND_CLASS_CONFIGURATION and \ value.index == 33: @@ -161,35 +136,39 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): _LOGGER.debug("self._current_swing_mode=%s", self._current_swing_mode) # Set point - for value in (self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT) - .values()): - if value.data == 0: - _LOGGER.debug("Setpoint is 0, setting default to " - "current_temperature=%s", - self._current_temperature) - self._target_temperature = int(self._current_temperature) - break - if self.current_operation is not None and \ - self.current_operation != 'Off': - if self._index_operation != value.index: - continue - if self._zxt_120: + temps = [] + for value in ( + self._node.get_values( + class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT) + .values()): + temps.append((round(float(value.data)), 1)) + if value.index == self._index: + if value.data == 0: + _LOGGER.debug("Setpoint is 0, setting default to " + "current_temperature=%s", + self._current_temperature) + self._target_temperature = ( + round((float(self._current_temperature)), 1)) break - self._target_temperature = int(value.data) - break - _LOGGER.debug("Device can't set setpoint based on operation mode." - " Defaulting to index=1") - self._target_temperature = int(value.data) + else: + self._target_temperature = round((float(value.data)), 1) # Operating state - for value in (self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE) - .values()): + for value in ( + self._node.get_values( + class_id=zwave.const + .COMMAND_CLASS_THERMOSTAT_OPERATING_STATE).values()): self._operating_state = value.data + # Fan operating state + for value in ( + self._node.get_values( + class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_FAN_STATE) + .values()): + self._fan_state = value.data + @property def should_poll(self): - """No polling on ZWave.""" + """No polling on Z-Wave.""" return False @property @@ -248,53 +227,19 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): temperature = kwargs.get(ATTR_TEMPERATURE) else: return - operation_mode = kwargs.get(ATTR_OPERATION_MODE) - _LOGGER.debug("set_temperature operation_mode=%s", operation_mode) for value in (self._node.get_values( class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT) .values()): - if operation_mode is not None: - setpoint_mode = SET_TEMP_TO_INDEX.get(operation_mode) - if value.index != setpoint_mode: - continue - _LOGGER.debug("setpoint_mode=%s", setpoint_mode) - value.data = temperature - break - - if self.current_operation is not None: - if self._hrt4_zw and self.current_operation == 'Off': - # HRT4-ZW can change setpoint when off. - value.data = int(temperature) - if self._index_operation != value.index: - continue - _LOGGER.debug("self._index_operation=%s and" - " self._current_operation=%s", - self._index_operation, - self._current_operation) + if value.index == self._index: if self._zxt_120: - _LOGGER.debug("zxt_120: Setting new setpoint for %s, " - " operation=%s, temp=%s", - self._index_operation, - self._current_operation, temperature) - # ZXT-120 does not support get setpoint - self._target_temperature = temperature # ZXT-120 responds only to whole int value.data = round(temperature, 0) + self._target_temperature = temperature self.update_ha_state() - break else: - _LOGGER.debug("Setting new setpoint for %s, " - "operation=%s, temp=%s", - self._index_operation, - self._current_operation, temperature) value.data = temperature - break - else: - _LOGGER.debug("Setting new setpoint for no known " - "operation mode. Index=1 and " - "temperature=%s", temperature) - value.data = temperature + self.update_ha_state() break def set_fan_mode(self, fan): @@ -331,9 +276,9 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): @property def device_state_attributes(self): """Return the device specific state attributes.""" + data = super().device_state_attributes if self._operating_state: - return { - "operating_state": self._operating_state, - } - else: - return {} + data[ATTR_OPERATING_STATE] = self._operating_state, + if self._fan_state: + data[ATTR_FAN_STATE] = self._fan_state + return data diff --git a/homeassistant/components/conversation.py b/homeassistant/components/conversation.py index b688e3d7082..e589ff9155c 100644 --- a/homeassistant/components/conversation.py +++ b/homeassistant/components/conversation.py @@ -15,7 +15,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['fuzzywuzzy==0.12.0'] +REQUIREMENTS = ['fuzzywuzzy==0.14.0'] ATTR_TEXT = 'text' diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 27b30e5e013..44b59133d21 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -8,6 +8,7 @@ import logging import voluptuous as vol +from homeassistant.core import callback import homeassistant.components.mqtt as mqtt from homeassistant.components.cover import CoverDevice from homeassistant.const import ( @@ -89,29 +90,30 @@ class MqttCover(CoverDevice): self._retain = retain self._optimistic = optimistic or state_topic is None + @callback def message_received(topic, payload, qos): """A new MQTT message has been received.""" if value_template is not None: - payload = value_template.render_with_possible_json_value( + payload = value_template.async_render_with_possible_json_value( payload) if payload == self._state_open: self._state = False - _LOGGER.warning("state=%s", int(self._state)) - self.update_ha_state() + hass.async_add_job(self.async_update_ha_state()) elif payload == self._state_closed: self._state = True - self.update_ha_state() + hass.async_add_job(self.async_update_ha_state()) elif payload.isnumeric() and 0 <= int(payload) <= 100: if int(payload) > 0: self._state = False else: self._state = True self._position = int(payload) - self.update_ha_state() + hass.async_add_job(self.async_update_ha_state()) else: _LOGGER.warning( "Payload is not True, False, or integer (0-100): %s", payload) + if self._state_topic is None: # Force into optimistic mode. self._optimistic = True diff --git a/homeassistant/components/cover/mysensors.py b/homeassistant/components/cover/mysensors.py index aa3d866bcd6..7dd63a8c745 100644 --- a/homeassistant/components/cover/mysensors.py +++ b/homeassistant/components/cover/mysensors.py @@ -18,7 +18,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the mysensors platform for covers.""" if discovery_info is None: return - for gateway in mysensors.GATEWAYS.values(): + + gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) + if not gateways: + return + + for gateway in gateways: pres = gateway.const.Presentation set_req = gateway.const.SetReq map_sv_types = { diff --git a/homeassistant/components/cover/zwave.py b/homeassistant/components/cover/zwave.py index 2794995abb1..a190e69bf53 100644 --- a/homeassistant/components/cover/zwave.py +++ b/homeassistant/components/cover/zwave.py @@ -36,15 +36,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): and value.index == 0): value.set_change_verified(False) add_devices([ZwaveRollershutter(value)]) - elif value.node.specific == zwave.const.GENERIC_TYPE_ENTRY_CONTROL: - if (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY or - value.command_class == - zwave.const.COMMAND_CLASS_BARRIER_OPERATOR): - if (value.type != zwave.const.TYPE_BOOL and - value.genre != zwave.const.GENRE_USER): - return - value.set_change_verified(False) - add_devices([ZwaveGarageDoor(value)]) + elif (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY or + value.command_class == zwave.const.COMMAND_CLASS_BARRIER_OPERATOR): + if (value.type != zwave.const.TYPE_BOOL and + value.genre != zwave.const.GENRE_USER): + return + value.set_change_verified(False) + add_devices([ZwaveGarageDoor(value)]) else: return diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index 9f3042320c9..3f3454e0f02 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -17,6 +17,7 @@ DOMAIN = 'demo' COMPONENTS_WITH_DEMO_PLATFORM = [ 'alarm_control_panel', 'binary_sensor', + 'calendar', 'camera', 'climate', 'cover', diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index ed31e624b91..9da4348362d 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -9,6 +9,7 @@ from datetime import timedelta import voluptuous as vol +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 track_point_in_time @@ -79,21 +80,22 @@ def setup(hass, config): return None return next_setting - LIGHT_TRANSITION_TIME * len(light_ids) - def turn_light_on_before_sunset(light_id): + def async_turn_on_before_sunset(light_id): """Helper function to turn on lights. Speed is slow if there are devices home and the light is not on yet. """ if not device_tracker.is_on(hass) or light.is_on(hass, light_id): return - light.turn_on(hass, light_id, - transition=LIGHT_TRANSITION_TIME.seconds, - profile=light_profile) + light.async_turn_on(hass, light_id, + transition=LIGHT_TRANSITION_TIME.seconds, + profile=light_profile) # Track every time sun rises so we can schedule a time-based # pre-sun set event @track_state_change(sun.ENTITY_ID, sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON) + @callback def schedule_lights_at_sun_set(hass, entity, old_state, new_state): """The moment sun sets we want to have all the lights on. @@ -104,16 +106,21 @@ def setup(hass, config): if not start_point: return - def turn_on(light_id): + def async_turn_on_factory(light_id): """Lambda can keep track of function parameters. No local parameters. If we put the lambda directly in the below statement only the last light will be turned on. """ - return lambda now: turn_light_on_before_sunset(light_id) + @callback + def async_turn_on_light(now): + """Turn on specific light.""" + async_turn_on_before_sunset(light_id) + + return async_turn_on_light for index, light_id in enumerate(light_ids): - track_point_in_time(hass, turn_on(light_id), + track_point_in_time(hass, async_turn_on_factory(light_id), start_point + index * LIGHT_TRANSITION_TIME) # If the sun is already above horizon schedule the time-based pre-sun set @@ -122,6 +129,7 @@ def setup(hass, config): schedule_lights_at_sun_set(hass, None, None, None) @track_state_change(device_entity_ids, STATE_NOT_HOME, STATE_HOME) + @callback def check_light_on_dev_state_change(hass, entity, old_state, new_state): """Handle tracked device state changes.""" # pylint: disable=unused-variable @@ -136,7 +144,7 @@ def setup(hass, config): # Do we need lights? if light_needed: logger.info("Home coming event for %s. Turning lights on", entity) - light.turn_on(hass, light_ids, profile=light_profile) + light.async_turn_on(hass, light_ids, profile=light_profile) # Are we in the time span were we would turn on the lights # if someone would be home? @@ -149,7 +157,7 @@ def setup(hass, config): # when the fading in started and turn it on if so for index, light_id in enumerate(light_ids): if now > start_point + index * LIGHT_TRANSITION_TIME: - light.turn_on(hass, light_id) + light.async_turn_on(hass, light_id) else: # If this light didn't happen to be turned on yet so @@ -158,6 +166,7 @@ def setup(hass, config): if not disable_turn_off: @track_state_change(device_group, STATE_HOME, STATE_NOT_HOME) + @callback def turn_off_lights_when_all_leave(hass, entity, old_state, new_state): """Handle device group state change.""" # pylint: disable=unused-variable @@ -166,6 +175,6 @@ def setup(hass, config): logger.info( "Everyone has left but there are lights on. Turning them off") - light.turn_off(hass, light_ids) + light.async_turn_off(hass, light_ids) return True diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 13194f88894..7390357f4d7 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -8,13 +8,13 @@ import asyncio from datetime import timedelta import logging import os -import threading from typing import Any, Sequence, Callable import voluptuous as vol from homeassistant.bootstrap import ( - prepare_setup_platform, log_exception) + async_prepare_setup_platform, async_log_exception) +from homeassistant.core import callback from homeassistant.components import group, zone from homeassistant.components.discovery import SERVICE_NETGEAR from homeassistant.config import load_yaml_config_file @@ -28,7 +28,7 @@ from homeassistant.util.async import run_coroutine_threadsafe import homeassistant.util.dt as dt_util from homeassistant.util.yaml import dump -from homeassistant.helpers.event import track_utc_time_change +from homeassistant.helpers.event import async_track_utc_time_change from homeassistant.const import ( ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID) @@ -106,14 +106,15 @@ def see(hass: HomeAssistantType, mac: str=None, dev_id: str=None, hass.services.call(DOMAIN, SERVICE_SEE, data) -def setup(hass: HomeAssistantType, config: ConfigType): +@asyncio.coroutine +def async_setup(hass: HomeAssistantType, config: ConfigType): """Setup device tracker.""" yaml_path = hass.config.path(YAML_DEVICES) try: conf = config.get(DOMAIN, []) except vol.Invalid as ex: - log_exception(ex, DOMAIN, config, hass) + async_log_exception(ex, DOMAIN, config, hass) return False else: conf = conf[0] if len(conf) > 0 else {} @@ -121,60 +122,77 @@ def setup(hass: HomeAssistantType, config: ConfigType): timedelta(seconds=DEFAULT_CONSIDER_HOME)) track_new = conf.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) - devices = load_config(yaml_path, hass, consider_home) - + devices = yield from async_load_config(yaml_path, hass, consider_home) tracker = DeviceTracker(hass, consider_home, track_new, devices) - def setup_platform(p_type, p_config, disc_info=None): + # update tracked devices + update_tasks = [device.async_update_ha_state() for device in devices + if device.track] + if update_tasks: + yield from asyncio.wait(update_tasks, loop=hass.loop) + + @asyncio.coroutine + def async_setup_platform(p_type, p_config, disc_info=None): """Setup a device tracker platform.""" - platform = prepare_setup_platform(hass, config, DOMAIN, p_type) + platform = yield from async_prepare_setup_platform( + hass, config, DOMAIN, p_type) if platform is None: return try: if hasattr(platform, 'get_scanner'): - scanner = platform.get_scanner(hass, {DOMAIN: p_config}) + scanner = yield from hass.loop.run_in_executor( + None, platform.get_scanner, hass, {DOMAIN: p_config}) if scanner is None: _LOGGER.error('Error setting up platform %s', p_type) return - setup_scanner_platform(hass, p_config, scanner, tracker.see) + yield from async_setup_scanner_platform( + hass, p_config, scanner, tracker.async_see) return - if not platform.setup_scanner(hass, p_config, tracker.see): + ret = yield from hass.loop.run_in_executor( + None, platform.setup_scanner, hass, p_config, tracker.see) + if not ret: _LOGGER.error('Error setting up platform %s', p_type) except Exception: # pylint: disable=broad-except _LOGGER.exception('Error setting up platform %s', p_type) - for p_type, p_config in config_per_platform(config, DOMAIN): - setup_platform(p_type, p_config) + setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config + in config_per_platform(config, DOMAIN)] + if setup_tasks: + yield from asyncio.wait(setup_tasks, loop=hass.loop) - def device_tracker_discovered(service, info): + yield from tracker.async_setup_group() + + @callback + def async_device_tracker_discovered(service, info): """Called when a device tracker platform is discovered.""" - setup_platform(DISCOVERY_PLATFORMS[service], {}, info) + hass.async_add_job( + async_setup_platform(DISCOVERY_PLATFORMS[service], {}, info)) - discovery.listen(hass, DISCOVERY_PLATFORMS.keys(), - device_tracker_discovered) + discovery.async_listen( + hass, DISCOVERY_PLATFORMS.keys(), async_device_tracker_discovered) - def update_stale(now): - """Clean up stale devices.""" - tracker.update_stale(now) - track_utc_time_change(hass, update_stale, second=range(0, 60, 5)) + # Clean up stale devices + async_track_utc_time_change( + hass, tracker.async_update_stale, second=range(0, 60, 5)) - tracker.setup_group() - - def see_service(call): + @asyncio.coroutine + def async_see_service(call): """Service to see a device.""" args = {key: value for key, value in call.data.items() if key in (ATTR_MAC, ATTR_DEV_ID, ATTR_HOST_NAME, ATTR_LOCATION_NAME, ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_BATTERY, ATTR_ATTRIBUTES)} - tracker.see(**args) + yield from tracker.async_see(**args) - descriptions = load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) - hass.services.register(DOMAIN, SERVICE_SEE, see_service, - descriptions.get(SERVICE_SEE)) + descriptions = yield from hass.loop.run_in_executor( + None, load_yaml_config_file, + os.path.join(os.path.dirname(__file__), 'services.yaml') + ) + hass.services.async_register( + DOMAIN, SERVICE_SEE, async_see_service, descriptions.get(SERVICE_SEE)) return True @@ -188,94 +206,116 @@ class DeviceTracker(object): self.hass = hass self.devices = {dev.dev_id: dev for dev in devices} self.mac_to_dev = {dev.mac: dev for dev in devices if dev.mac} + self.consider_home = consider_home + self.track_new = track_new + self.group = None # type: group.Group + self._is_updating = asyncio.Lock(loop=hass.loop) + for dev in devices: if self.devices[dev.dev_id] is not dev: _LOGGER.warning('Duplicate device IDs detected %s', dev.dev_id) if dev.mac and self.mac_to_dev[dev.mac] is not dev: _LOGGER.warning('Duplicate device MAC addresses detected %s', dev.mac) - self.consider_home = consider_home - self.track_new = track_new - self.lock = threading.Lock() - - for device in devices: - if device.track: - device.update_ha_state() - - self.group = None # type: group.Group def see(self, mac: str=None, dev_id: str=None, host_name: str=None, location_name: str=None, gps: GPSType=None, gps_accuracy=None, battery: str=None, attributes: dict=None): """Notify the device tracker that you see a device.""" - with self.lock: - if mac is None and dev_id is None: - raise HomeAssistantError('Neither mac or device id passed in') - elif mac is not None: - mac = str(mac).upper() - device = self.mac_to_dev.get(mac) - if not device: - dev_id = util.slugify(host_name or '') or util.slugify(mac) - else: - dev_id = cv.slug(str(dev_id).lower()) - device = self.devices.get(dev_id) + self.hass.add_job( + self.async_see(mac, dev_id, host_name, location_name, gps, + gps_accuracy, battery, attributes) + ) - if device: - device.seen(host_name, location_name, gps, gps_accuracy, - battery, attributes) - if device.track: - device.update_ha_state() - return + @asyncio.coroutine + def async_see(self, mac: str=None, dev_id: str=None, host_name: str=None, + location_name: str=None, gps: GPSType=None, + gps_accuracy=None, battery: str=None, attributes: dict=None): + """Notify the device tracker that you see a device. - # If no device can be found, create it - dev_id = util.ensure_unique_string(dev_id, self.devices.keys()) - device = Device( - self.hass, self.consider_home, self.track_new, - dev_id, mac, (host_name or dev_id).replace('_', ' ')) - self.devices[dev_id] = device - if mac is not None: - self.mac_to_dev[mac] = device - - device.seen(host_name, location_name, gps, gps_accuracy, battery, - attributes) + This method is a coroutine. + """ + if mac is None and dev_id is None: + raise HomeAssistantError('Neither mac or device id passed in') + elif mac is not None: + mac = str(mac).upper() + device = self.mac_to_dev.get(mac) + if not device: + dev_id = util.slugify(host_name or '') or util.slugify(mac) + else: + dev_id = cv.slug(str(dev_id).lower()) + device = self.devices.get(dev_id) + if device: + yield from device.async_seen(host_name, location_name, gps, + gps_accuracy, battery, attributes) if device.track: - device.update_ha_state() + yield from device.async_update_ha_state() + return - self.hass.bus.fire(EVENT_NEW_DEVICE, { - ATTR_ENTITY_ID: device.entity_id, - ATTR_HOST_NAME: device.host_name, - }) + # If no device can be found, create it + dev_id = util.ensure_unique_string(dev_id, self.devices.keys()) + device = Device( + self.hass, self.consider_home, self.track_new, + dev_id, mac, (host_name or dev_id).replace('_', ' ')) + self.devices[dev_id] = device + if mac is not None: + self.mac_to_dev[mac] = device - # During init, we ignore the group - if self.group is not None: - self.group.update_tracked_entity_ids( - list(self.group.tracking) + [device.entity_id]) - update_config(self.hass.config.path(YAML_DEVICES), dev_id, device) + yield from device.async_seen(host_name, location_name, gps, + gps_accuracy, battery, attributes) - def setup_group(self): - """Initialize group for all tracked devices.""" - run_coroutine_threadsafe( - self.async_setup_group(), self.hass.loop).result() + if device.track: + yield from device.async_update_ha_state() + + self.hass.bus.async_fire(EVENT_NEW_DEVICE, { + ATTR_ENTITY_ID: device.entity_id, + ATTR_HOST_NAME: device.host_name, + }) + + # During init, we ignore the group + if self.group is not None: + yield from self.group.async_update_tracked_entity_ids( + list(self.group.tracking) + [device.entity_id]) + + # update known_devices.yaml + self.hass.async_add_job( + self.async_update_config(self.hass.config.path(YAML_DEVICES), + dev_id, device) + ) + + @asyncio.coroutine + def async_update_config(self, path, dev_id, device): + """Add device to YAML configuration file. + + This method is a coroutine. + """ + with (yield from self._is_updating): + self.hass.loop.run_in_executor( + None, update_config, self.hass.config.path(YAML_DEVICES), + dev_id, device) @asyncio.coroutine def async_setup_group(self): """Initialize group for all tracked devices. - This method must be run in the event loop. + This method is a coroutine. """ entity_ids = (dev.entity_id for dev in self.devices.values() if dev.track) self.group = yield from group.Group.async_create_group( self.hass, GROUP_NAME_ALL_DEVICES, entity_ids, False) - def update_stale(self, now: dt_util.dt.datetime): - """Update stale devices.""" - with self.lock: - for device in self.devices.values(): - if (device.track and device.last_update_home and - device.stale(now)): - device.update_ha_state(True) + @callback + def async_update_stale(self, now: dt_util.dt.datetime): + """Update stale devices. + + This method must be run in the event loop. + """ + for device in self.devices.values(): + if (device.track and device.last_update_home) and \ + device.stale(now): + self.hass.async_add_job(device.async_update_ha_state(True)) class Device(Entity): @@ -362,9 +402,10 @@ class Device(Entity): """If device should be hidden.""" return self.away_hide and self.state != STATE_HOME - def seen(self, host_name: str=None, location_name: str=None, - gps: GPSType=None, gps_accuracy=0, battery: str=None, - attributes: dict=None): + @asyncio.coroutine + def async_seen(self, host_name: str=None, location_name: str=None, + gps: GPSType=None, gps_accuracy=0, battery: str=None, + attributes: dict=None): """Mark the device as seen.""" self.last_seen = dt_util.utcnow() self.host_name = host_name @@ -373,28 +414,38 @@ class Device(Entity): self.battery = battery self.attributes = attributes self.gps = None + if gps is not None: try: self.gps = float(gps[0]), float(gps[1]) except (ValueError, TypeError, IndexError): _LOGGER.warning('Could not parse gps value for %s: %s', self.dev_id, gps) - self.update() + + # pylint: disable=not-an-iterable + yield from self.async_update() def stale(self, now: dt_util.dt.datetime=None): - """Return if device state is stale.""" + """Return if device state is stale. + + Async friendly. + """ return self.last_seen and \ (now or dt_util.utcnow()) - self.last_seen > self.consider_home - def update(self): - """Update state of entity.""" + @asyncio.coroutine + def async_update(self): + """Update state of entity. + + This method is a coroutine. + """ if not self.last_seen: return elif self.location_name: self._state = self.location_name elif self.gps is not None: - zone_state = zone.active_zone(self.hass, self.gps[0], self.gps[1], - self.gps_accuracy) + zone_state = zone.async_active_zone( + self.hass, self.gps[0], self.gps[1], self.gps_accuracy) if zone_state is None: self._state = STATE_NOT_HOME elif zone_state.entity_id == zone.ENTITY_ID_HOME: @@ -412,6 +463,17 @@ class Device(Entity): def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta): """Load devices from YAML configuration file.""" + return run_coroutine_threadsafe( + async_load_config(path, hass, consider_home), hass.loop).result() + + +@asyncio.coroutine +def async_load_config(path: str, hass: HomeAssistantType, + consider_home: timedelta): + """Load devices from YAML configuration file. + + This method is a coroutine. + """ dev_schema = vol.Schema({ vol.Required('name'): cv.string, vol.Optional('track', default=False): cv.boolean, @@ -426,7 +488,8 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta): try: result = [] try: - devices = load_yaml_config_file(path) + devices = yield from hass.loop.run_in_executor( + None, load_yaml_config_file, path) except HomeAssistantError as err: _LOGGER.error('Unable to load %s: %s', path, str(err)) return [] @@ -436,7 +499,7 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta): device = dev_schema(device) device['dev_id'] = cv.slugify(dev_id) except vol.Invalid as exp: - log_exception(exp, dev_id, devices, hass) + async_log_exception(exp, dev_id, devices, hass) else: result.append(Device(hass, **device)) return result @@ -445,9 +508,13 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta): return [] -def setup_scanner_platform(hass: HomeAssistantType, config: ConfigType, - scanner: Any, see_device: Callable): - """Helper method to connect scanner-based platform to device tracker.""" +@asyncio.coroutine +def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType, + scanner: Any, async_see_device: Callable): + """Helper method to connect scanner-based platform to device tracker. + + This method is a coroutine. + """ interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) # Initial scan of each mac we also tell about host name for config @@ -455,18 +522,20 @@ def setup_scanner_platform(hass: HomeAssistantType, config: ConfigType, def device_tracker_scan(now: dt_util.dt.datetime): """Called when interval matches.""" - for mac in scanner.scan_devices(): + found_devices = scanner.scan_devices() + + for mac in found_devices: if mac in seen: host_name = None else: host_name = scanner.get_device_name(mac) seen.add(mac) - see_device(mac=mac, host_name=host_name) + hass.async_add_job(async_see_device(mac=mac, host_name=host_name)) - track_utc_time_change(hass, device_tracker_scan, second=range(0, 60, - interval)) + async_track_utc_time_change( + hass, device_tracker_scan, second=range(0, 60, interval)) - device_tracker_scan(None) + hass.async_add_job(device_tracker_scan, None) def update_config(path: str, dev_id: str, device: Device): @@ -484,7 +553,10 @@ def update_config(path: str, dev_id: str, device: Device): def get_gravatar_for_email(email: str): - """Return an 80px Gravatar for the given email address.""" + """Return an 80px Gravatar for the given email address. + + Async friendly. + """ import hashlib url = 'https://www.gravatar.com/avatar/{}.jpg?s=80&d=wavatar' return url.format(hashlib.md5(email.encode('utf-8').lower()).hexdigest()) diff --git a/homeassistant/components/device_tracker/actiontec.py b/homeassistant/components/device_tracker/actiontec.py index a4804848f4a..9583237a912 100644 --- a/homeassistant/components/device_tracker/actiontec.py +++ b/homeassistant/components/device_tracker/actiontec.py @@ -42,6 +42,7 @@ def get_scanner(hass, config): scanner = ActiontecDeviceScanner(config[DOMAIN]) return scanner if scanner.success_init else None + Device = namedtuple("Device", ["mac", "ip", "last_update"]) diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index 50411591cb7..2eced1b4dd4 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -76,6 +76,15 @@ _IP_NEIGH_REGEX = re.compile( r'(\w+\s(?P(([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))))?\s' + r'(?P(\w+))') +_NVRAM_CMD = 'nvram get client_info_tmp' +_NVRAM_REGEX = re.compile( + r'.*>.*>' + + r'(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})' + + r'>' + + r'(?P(([0-9a-fA-F]{2}[:-]){5}([0-9a-fA-F]{2})))' + + r'>' + + r'.*') + # pylint: disable=unused-argument def get_scanner(hass, config): @@ -84,7 +93,8 @@ def get_scanner(hass, config): return scanner if scanner.success_init else None -AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp') + +AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp nvram') class AsusWrtDeviceScanner(object): @@ -155,7 +165,8 @@ class AsusWrtDeviceScanner(object): active_clients = [client for client in data.values() if client['status'] == 'REACHABLE' or client['status'] == 'DELAY' or - client['status'] == 'STALE'] + client['status'] == 'STALE' or + client['status'] == 'IN_NVRAM'] self.last_results = active_clients return True @@ -184,13 +195,18 @@ class AsusWrtDeviceScanner(object): ssh.sendline(_WL_CMD) ssh.prompt() leases_result = ssh.before.split(b'\n')[1:-1] + ssh.sendline(_NVRAM_CMD) + ssh.prompt() + nvram_result = ssh.before.split(b'\n')[1].split(b'<')[1:] else: arp_result = [''] + nvram_result = [''] ssh.sendline(_LEASES_CMD) ssh.prompt() leases_result = ssh.before.split(b'\n')[1:-1] ssh.logout() - return AsusWrtResult(neighbors, leases_result, arp_result) + return AsusWrtResult(neighbors, leases_result, arp_result, + nvram_result) except pxssh.ExceptionPxssh as exc: _LOGGER.error('Unexpected response from router: %s', exc) return None @@ -213,13 +229,18 @@ class AsusWrtDeviceScanner(object): telnet.write('{}\n'.format(_WL_CMD).encode('ascii')) leases_result = (telnet.read_until(prompt_string). split(b'\n')[1:-1]) + telnet.write('{}\n'.format(_NVRAM_CMD).encode('ascii')) + nvram_result = (telnet.read_until(prompt_string). + split(b'\n')[1].split(b'<')[1:]) else: arp_result = [''] + nvram_result = [''] telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii')) leases_result = (telnet.read_until(prompt_string). split(b'\n')[1:-1]) telnet.write('exit\n'.encode('ascii')) - return AsusWrtResult(neighbors, leases_result, arp_result) + return AsusWrtResult(neighbors, leases_result, arp_result, + nvram_result) except EOFError: _LOGGER.error('Unexpected response from router') return None @@ -277,6 +298,26 @@ class AsusWrtDeviceScanner(object): 'ip': arp_match.group('ip'), 'mac': match.group('mac').upper(), } + + # match mac addresses to IP addresses in NVRAM table + for nvr in result.nvram: + if match.group('mac').upper() in nvr.decode('utf-8'): + nvram_match = _NVRAM_REGEX.search(nvr.decode('utf-8')) + if not nvram_match: + _LOGGER.warning('Could not parse nvr row: %s', nvr) + continue + + # skip current check if already in ARP table + if nvram_match.group('ip') in devices.keys(): + continue + + devices[nvram_match.group('ip')] = { + 'host': host, + 'status': 'IN_NVRAM', + 'ip': nvram_match.group('ip'), + 'mac': match.group('mac').upper(), + } + else: for lease in result.leases: match = _LEASES_REGEX.search(lease.decode('utf-8')) diff --git a/homeassistant/components/device_tracker/cisco_ios.py b/homeassistant/components/device_tracker/cisco_ios.py new file mode 100644 index 00000000000..0d42282b17c --- /dev/null +++ b/homeassistant/components/device_tracker/cisco_ios.py @@ -0,0 +1,162 @@ +""" +Support for Cisco IOS Routers. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.cisco_ios/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, \ + CONF_PORT +from homeassistant.util import Throttle + +# Return cached results if last scan was less then this time ago. +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['pexpect==4.0.1'] + +PLATFORM_SCHEMA = vol.All( + PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD, default=''): cv.string, + vol.Optional(CONF_PORT): cv.port, + }) +) + + +def get_scanner(hass, config): + """Validate the configuration and return a Cisco scanner.""" + scanner = CiscoDeviceScanner(config[DOMAIN]) + + return scanner if scanner.success_init else None + + +class CiscoDeviceScanner(object): + """This class queries a wireless router running Cisco IOS firmware.""" + + def __init__(self, config): + """Initialize the scanner.""" + self.host = config[CONF_HOST] + self.username = config[CONF_USERNAME] + self.port = config.get(CONF_PORT) + self.password = config.get(CONF_PASSWORD) + + self.last_results = {} + + self.success_init = self._update_info() + _LOGGER.info('cisco_ios scanner initialized') + + # pylint: disable=no-self-use + def get_device_name(self, device): + """The firmware doesn't save the name of the wireless device.""" + return None + + def scan_devices(self): + """Scan for new devices and return a list with found device IDs.""" + self._update_info() + + return self.last_results + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self): + """ + Ensure the information from the Cisco router is up to date. + + Returns boolean if scanning successful. + """ + string_result = self._get_arp_data() + + if string_result: + self.last_results = [] + last_results = [] + + lines_result = string_result.splitlines() + + # Remove the first two lines, as they contains the arp command + # and the arp table titles e.g. + # show ip arp + # Protocol Address | Age (min) | Hardware Addr | Type | Interface + lines_result = lines_result[2:] + + for line in lines_result: + if len(line.split()) is 6: + parts = line.split() + if len(parts) != 6: + continue + + # ['Internet', '10.10.11.1', '-', '0027.d32d.0123', 'ARPA', + # 'GigabitEthernet0'] + age = parts[2] + hw_addr = parts[3] + + if age != "-": + mac = _parse_cisco_mac_address(hw_addr) + age = int(age) + if age < 1: + last_results.append(mac) + + self.last_results = last_results + return True + + return False + + def _get_arp_data(self): + """Open connection to the router and get arp entries.""" + from pexpect import pxssh + import re + + try: + cisco_ssh = pxssh.pxssh() + cisco_ssh.login(self.host, self.username, self.password, + port=self.port, auto_prompt_reset=False) + + # Find the hostname + initial_line = cisco_ssh.before.decode('utf-8').splitlines() + router_hostname = initial_line[len(initial_line) - 1] + router_hostname += "#" + # Set the discovered hostname as prompt + regex_expression = ('(?i)^%s' % router_hostname).encode() + cisco_ssh.PROMPT = re.compile(regex_expression, re.MULTILINE) + # Allow full arp table to print at once + cisco_ssh.sendline("terminal length 0") + cisco_ssh.prompt(1) + + cisco_ssh.sendline("show ip arp") + cisco_ssh.prompt(1) + + devices_result = cisco_ssh.before + + return devices_result.decode("utf-8") + except pxssh.ExceptionPxssh as px_e: + _LOGGER.error("pxssh failed on login.") + _LOGGER.error(px_e) + + return None + + +def _parse_cisco_mac_address(cisco_hardware_addr): + """ + Parse a Cisco formatted HW address to normal MAC. + + e.g. convert + 001d.ec02.07ab + + to: + 00:1D:EC:02:07:AB + + Takes in cisco_hwaddr: HWAddr String from Cisco ARP table + Returns a regular standard MAC address + """ + cisco_hardware_addr = cisco_hardware_addr.replace('.', '') + blocks = [cisco_hardware_addr[x:x + 2] + for x in range(0, len(cisco_hardware_addr), 2)] + + return ':'.join(blocks).upper() diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index f6419ae2490..e6bd74e57c9 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -8,7 +8,9 @@ import asyncio from functools import partial import logging -from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME +from homeassistant.const import (ATTR_LATITUDE, ATTR_LONGITUDE, + STATE_NOT_HOME, + HTTP_UNPROCESSABLE_ENTITY) from homeassistant.components.http import HomeAssistantView # pylint: disable=unused-import from homeassistant.components.device_tracker import ( # NOQA @@ -76,11 +78,13 @@ class LocativeView(HomeAssistantView): device = data['device'].replace('-', '') location_name = data['id'].lower() direction = data['trigger'] + gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]) if direction == 'enter': yield from self.hass.loop.run_in_executor( None, partial(self.see, dev_id=device, - location_name=location_name)) + location_name=location_name, + gps=gps_location)) return 'Setting location to {}'.format(location_name) elif direction == 'exit': @@ -88,9 +92,11 @@ class LocativeView(HomeAssistantView): '{}.{}'.format(DOMAIN, device)) if current_state is None or current_state.state == location_name: + location_name = STATE_NOT_HOME yield from self.hass.loop.run_in_executor( None, partial(self.see, dev_id=device, - location_name=STATE_NOT_HOME)) + location_name=location_name, + gps=gps_location)) return 'Setting location to not home' else: # Ignore the message if it is telling us to exit a zone that we diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py index e8a6f2b7371..2e8bbc5d2a1 100644 --- a/homeassistant/components/device_tracker/nmap_tracker.py +++ b/homeassistant/components/device_tracker/nmap_tracker.py @@ -2,7 +2,7 @@ Support for scanning a network with nmap. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/device_tracker.nmap_scanner/ +https://home-assistant.io/components/device_tracker.nmap_tracker/ """ import logging import re @@ -43,6 +43,7 @@ def get_scanner(hass, config): return scanner if scanner.success_init else None + Device = namedtuple('Device', ['mac', 'name', 'ip', 'last_update']) diff --git a/homeassistant/components/device_tracker/swisscom.py b/homeassistant/components/device_tracker/swisscom.py new file mode 100644 index 00000000000..7966fe3ea6a --- /dev/null +++ b/homeassistant/components/device_tracker/swisscom.py @@ -0,0 +1,108 @@ +""" +Support for Swisscom routers (Internet-Box). + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.swisscom/ +""" +import logging +import threading +from datetime import timedelta + +import requests +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA +from homeassistant.const import CONF_HOST +from homeassistant.util import Throttle + +# Return cached results if last scan was less then this time ago. +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_IP = '192.168.1.1' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOST, default=DEFAULT_IP): cv.string +}) + + +def get_scanner(hass, config): + """Return the Swisscom device scanner.""" + scanner = SwisscomDeviceScanner(config[DOMAIN]) + + return scanner if scanner.success_init else None + + +class SwisscomDeviceScanner(object): + """This class queries a router running Swisscom Internet-Box firmware.""" + + def __init__(self, config): + """Initialize the scanner.""" + self.host = config[CONF_HOST] + + self.lock = threading.Lock() + + self.last_results = {} + + # Test the router is accessible. + data = self.get_swisscom_data() + self.success_init = data is not None + + def scan_devices(self): + """Scan for new devices and return a list with found device IDs.""" + self._update_info() + return [client['mac'] for client in self.last_results] + + def get_device_name(self, device): + """Return the name of the given device or None if we don't know.""" + if not self.last_results: + return None + for client in self.last_results: + if client['mac'] == device: + return client['host'] + return None + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self): + """Ensure the information from the Swisscom router is up to date. + + Return boolean if scanning successful. + """ + if not self.success_init: + return False + + with self.lock: + _LOGGER.info("Loading data from Swisscom Internet Box") + data = self.get_swisscom_data() + if not data: + return False + + active_clients = [client for client in data.values() if + client['status']] + self.last_results = active_clients + return True + + def get_swisscom_data(self): + """Retrieve data from Swisscom and return parsed result.""" + url = 'http://{}/ws'.format(self.host) + headers = {'Content-Type': 'application/x-sah-ws-4-call+json'} + data = """ + {"service":"Devices", "method":"get", + "parameters":{"expression":"lan and not self"}}""" + + request = requests.post(url, headers=headers, data=data, timeout=10) + + devices = {} + for device in request.json()['status']: + try: + devices[device['Key']] = { + 'ip': device['IPAddress'], + 'mac': device['PhysAddress'], + 'host': device['Name'], + 'status': device['Active'] + } + except (KeyError, requests.exceptions.RequestException): + pass + return devices diff --git a/homeassistant/components/device_tracker/volvooncall.py b/homeassistant/components/device_tracker/volvooncall.py index 0fea3eadd65..36e0d96cdf1 100644 --- a/homeassistant/components/device_tracker/volvooncall.py +++ b/homeassistant/components/device_tracker/volvooncall.py @@ -55,25 +55,30 @@ def setup_scanner(hass, config, see): """True if any door/window is opened.""" return any([door[key] for key in door if "Open" in key]) + attributes = dict( + unlocked=not vehicle["carLocked"], + tank_volume=vehicle["fuelTankVolume"], + average_fuel_consumption=round( + vehicle["averageFuelConsumption"] / 10, 1), # l/100km + washer_fluid_low=vehicle["washerFluidLevel"] != "Normal", + brake_fluid_low=vehicle["brakeFluid"] != "Normal", + service_warning=vehicle["serviceWarningStatus"] != "Normal", + bulb_failures=len(vehicle["bulbFailures"]) > 0, + doors_open=any_opened(vehicle["doors"]), + windows_open=any_opened(vehicle["windows"]), + fuel=vehicle["fuelAmount"], + odometer=round(vehicle["odometer"] / 1000), # km + range=vehicle["distanceToEmpty"]) + + if "heater" in vehicle and \ + "status" in vehicle["heater"]: + attributes.update(heater_on=vehicle["heater"]["status"] != "off") + see(dev_id=dev_id, host_name=host_name, gps=(position["latitude"], position["longitude"]), - attributes=dict( - unlocked=not vehicle["carLocked"], - tank_volume=vehicle["fuelTankVolume"], - average_fuel_consumption=round( - vehicle["averageFuelConsumption"] / 10, 1), # l/100km - washer_fluid_low=vehicle["washerFluidLevel"] != "Normal", - brake_fluid_low=vehicle["brakeFluid"] != "Normal", - service_warning=vehicle["serviceWarningStatus"] != "Normal", - bulb_failures=len(vehicle["bulbFailures"]) > 0, - doors_open=any_opened(vehicle["doors"]), - windows_open=any_opened(vehicle["windows"]), - heater_on=vehicle["heater"]["status"] != "off", - fuel=vehicle["fuelAmount"], - odometer=round(vehicle["odometer"] / 1000), # km - range=vehicle["distanceToEmpty"])) + attributes=attributes) def update(now): """Update status from the online service.""" diff --git a/homeassistant/components/digital_ocean.py b/homeassistant/components/digital_ocean.py index f976c17ae9d..dd8c7de99d4 100644 --- a/homeassistant/components/digital_ocean.py +++ b/homeassistant/components/digital_ocean.py @@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-digitalocean==1.10.0'] +REQUIREMENTS = ['python-digitalocean==1.10.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 65a5af79bfb..780f2ab75d5 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.helpers.discovery import load_platform, discover -REQUIREMENTS = ['netdisco==0.7.5'] +REQUIREMENTS = ['netdisco==0.7.6'] DOMAIN = 'discovery' diff --git a/homeassistant/components/emulated_hue.py b/homeassistant/components/emulated_hue.py index 0f06ed631ca..ad89e001df0 100644 --- a/homeassistant/components/emulated_hue.py +++ b/homeassistant/components/emulated_hue.py @@ -18,7 +18,7 @@ from homeassistant import util, core from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - STATE_ON, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, + STATE_ON, STATE_OFF, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, ) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS @@ -318,7 +318,16 @@ class HueLightsView(HomeAssistantView): # Construct what we need to send to the service data = {ATTR_ENTITY_ID: entity_id} - if brightness is not None: + # If the requested entity is a script add some variables + if entity.domain.lower() == "script": + data['variables'] = { + 'requested_state': STATE_ON if result else STATE_OFF + } + + if brightness is not None: + data['variables']['requested_level'] = brightness + + elif brightness is not None: data[ATTR_BRIGHTNESS] = brightness if entity.domain.lower() in config.off_maps_to_on_domains: @@ -402,6 +411,13 @@ def parse_hue_api_put_light_body(request_json, entity): report_brightness = True result = (brightness > 0) + elif entity.domain.lower() == "script": + # Convert 0-255 to 0-100 + level = int(request_json[HUE_API_STATE_BRI]) / 255 * 100 + + brightness = round(level) + report_brightness = True + result = True return (result, brightness) if report_brightness else (result, None) diff --git a/homeassistant/components/envisalink.py b/homeassistant/components/envisalink.py index 21bc081224b..29ce08b2f0a 100644 --- a/homeassistant/components/envisalink.py +++ b/homeassistant/components/envisalink.py @@ -12,7 +12,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers.entity import Entity from homeassistant.components.discovery import load_platform -REQUIREMENTS = ['pyenvisalink==1.7', 'pydispatcher==2.0.5'] +REQUIREMENTS = ['pyenvisalink==1.9', 'pydispatcher==2.0.5'] _LOGGER = logging.getLogger(__name__) DOMAIN = 'envisalink' diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 896e0427675..6071315b167 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 896e0427675bb99348de6f1453bd6f8cf48b5c6f +Subproject commit 6071315b1675dfef1090b4683c9639ef0f56cfc0 diff --git a/homeassistant/components/google.py b/homeassistant/components/google.py new file mode 100644 index 00000000000..3dbc2c1a1ec --- /dev/null +++ b/homeassistant/components/google.py @@ -0,0 +1,292 @@ +""" +Support for Google - Calendar Event Devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/google/ + +NOTE TO OTHER DEVELOPERS: IF YOU ADD MORE SCOPES TO THE OAUTH THAN JUST +CALENDAR THEN USERS WILL NEED TO DELETE THEIR TOKEN_FILE. THEY WILL LOSE THEIR +REFRESH_TOKEN PIECE WHEN RE-AUTHENTICATING TO ADD MORE API ACCESS +IT'S BEST TO JUST HAVE SEPARATE OAUTH FOR DIFFERENT PIECES OF GOOGLE +""" +import logging +import os +import yaml + +import voluptuous as vol +from voluptuous.error import Error as VoluptuousError + +import homeassistant.helpers.config_validation as cv +import homeassistant.loader as loader +from homeassistant import bootstrap +from homeassistant.helpers import discovery +from homeassistant.helpers.entity import generate_entity_id +from homeassistant.helpers.event import track_time_change +from homeassistant.util import convert, dt + +REQUIREMENTS = [ + 'google-api-python-client==1.5.5', + 'oauth2client==3.0.0', +] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'google' +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +CONF_CLIENT_ID = 'client_id' +CONF_CLIENT_SECRET = 'client_secret' +CONF_TRACK_NEW = 'track_new_calendar' + +CONF_CAL_ID = 'cal_id' +CONF_DEVICE_ID = 'device_id' +CONF_NAME = 'name' +CONF_ENTITIES = 'entities' +CONF_TRACK = 'track' +CONF_SEARCH = 'search' +CONF_OFFSET = 'offset' + +DEFAULT_CONF_TRACK_NEW = True +DEFAULT_CONF_OFFSET = '!!' + +NOTIFICATION_ID = 'google_calendar_notification' +NOTIFICATION_TITLE = 'Google Calendar Setup' +GROUP_NAME_ALL_CALENDARS = "Google Calendar Sensors" + +SERVICE_SCAN_CALENDARS = 'scan_for_calendars' +SERVICE_FOUND_CALENDARS = 'found_calendar' + +DATA_INDEX = 'google_calendars' + +YAML_DEVICES = '{}_calendars.yaml'.format(DOMAIN) +SCOPES = 'https://www.googleapis.com/auth/calendar.readonly' + +TOKEN_FILE = '.{}.token'.format(DOMAIN) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + vol.Optional(CONF_TRACK_NEW): cv.boolean, + }) +}, extra=vol.ALLOW_EXTRA) + +_SINGLE_CALSEARCH_CONFIG = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Optional(CONF_TRACK): cv.boolean, + vol.Optional(CONF_SEARCH): vol.Any(cv.string, None), + vol.Optional(CONF_OFFSET): cv.string, +}) + +DEVICE_SCHEMA = vol.Schema({ + vol.Required(CONF_CAL_ID): cv.string, + vol.Required(CONF_ENTITIES, None): + vol.All(cv.ensure_list, [_SINGLE_CALSEARCH_CONFIG]), +}, extra=vol.ALLOW_EXTRA) + + +def do_authentication(hass, config): + """Notify user of actions and authenticate. + + Notify user of user_code and verification_url then poll + until we have an access token. + """ + from oauth2client.client import ( + OAuth2WebServerFlow, + OAuth2DeviceCodeError, + FlowExchangeError + ) + from oauth2client.file import Storage + + oauth = OAuth2WebServerFlow( + config[CONF_CLIENT_ID], + config[CONF_CLIENT_SECRET], + 'https://www.googleapis.com/auth/calendar.readonly', + 'Home-Assistant.io', + ) + + persistent_notification = loader.get_component('persistent_notification') + try: + dev_flow = oauth.step1_get_device_and_user_codes() + except OAuth2DeviceCodeError as err: + persistent_notification.create( + hass, 'Error: {}
You will need to restart hass after fixing.' + ''.format(err), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + + persistent_notification.create( + hass, 'In order to authorize Home-Assistant to view your calendars' + 'You must visit: {} and enter' + 'code: {}'.format(dev_flow.verification_url, + dev_flow.verification_url, + dev_flow.user_code), + title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID + ) + + def step2_exchange(now): + """Keep trying to validate the user_code until it expires.""" + if now >= dt.as_local(dev_flow.user_code_expiry): + persistent_notification.create( + hass, 'Authenication code expired, please restart ' + 'Home-Assistant and try again', + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + listener() + + try: + credentials = oauth.step2_exchange(device_flow_info=dev_flow) + except FlowExchangeError: + # not ready yet, call again + return + + storage = Storage(hass.config.path(TOKEN_FILE)) + storage.put(credentials) + do_setup(hass, config) + listener() + persistent_notification.create( + hass, 'We are all setup now. Check {} for calendars that have ' + 'been found'.format(YAML_DEVICES), + title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID) + + listener = track_time_change(hass, step2_exchange, + second=range(0, 60, dev_flow.interval)) + + return True + + +def setup(hass, config): + """Setup the platform.""" + if DATA_INDEX not in hass.data: + hass.data[DATA_INDEX] = {} + + conf = config.get(DOMAIN, {}) + + token_file = hass.config.path(TOKEN_FILE) + if not os.path.isfile(token_file): + do_authentication(hass, conf) + else: + do_setup(hass, conf) + + return True + + +def setup_services(hass, track_new_found_calendars, calendar_service): + """Setup service listeners.""" + def _found_calendar(call): + """Check if we know about a calendar and generate PLATFORM_DISCOVER.""" + calendar = get_calendar_info(hass, call.data) + if hass.data[DATA_INDEX].get(calendar[CONF_CAL_ID], None) is not None: + return + + hass.data[DATA_INDEX].update({calendar[CONF_CAL_ID]: calendar}) + + update_config( + hass.config.path(YAML_DEVICES), + hass.data[DATA_INDEX][calendar[CONF_CAL_ID]] + ) + + discovery.load_platform(hass, 'calendar', DOMAIN, + hass.data[DATA_INDEX][calendar[CONF_CAL_ID]]) + + hass.services.register( + DOMAIN, SERVICE_FOUND_CALENDARS, _found_calendar, + None, schema=None) + + def _scan_for_calendars(service): + """Scan for new calendars.""" + service = calendar_service.get() + cal_list = service.calendarList() # pylint: disable=no-member + calendars = cal_list.list().execute()['items'] + for calendar in calendars: + calendar['track'] = track_new_found_calendars + hass.services.call(DOMAIN, SERVICE_FOUND_CALENDARS, + calendar) + + hass.services.register( + DOMAIN, SERVICE_SCAN_CALENDARS, + _scan_for_calendars, + None, schema=None) + return True + + +def do_setup(hass, config): + """Run the setup after we have everything configured.""" + # load calendars the user has configured + hass.data[DATA_INDEX] = load_config(hass.config.path(YAML_DEVICES)) + + calendar_service = GoogleCalendarService(hass.config.path(TOKEN_FILE)) + track_new_found_calendars = convert(config.get(CONF_TRACK_NEW), + bool, DEFAULT_CONF_TRACK_NEW) + setup_services(hass, track_new_found_calendars, calendar_service) + + # Ensure component is loaded + bootstrap.setup_component(hass, 'calendar', config) + + for calendar in hass.data[DATA_INDEX].values(): + discovery.load_platform(hass, 'calendar', DOMAIN, calendar) + + # look for any new calendars + hass.services.call(DOMAIN, SERVICE_SCAN_CALENDARS, None) + return True + + +class GoogleCalendarService(object): + """Calendar service interface to google.""" + + def __init__(self, token_file): + """We just need the token_file.""" + self.token_file = token_file + + def get(self): + """Get the calendar service from the storage file token.""" + import httplib2 + from oauth2client.file import Storage + from googleapiclient import discovery as google_discovery + credentials = Storage(self.token_file).get() + http = credentials.authorize(httplib2.Http()) + service = google_discovery.build('calendar', 'v3', http=http) + return service + + +def get_calendar_info(hass, calendar): + """Convert data from Google into DEVICE_SCHEMA.""" + calendar_info = DEVICE_SCHEMA({ + CONF_CAL_ID: calendar['id'], + CONF_ENTITIES: [{ + CONF_TRACK: calendar['track'], + CONF_NAME: calendar['summary'], + CONF_DEVICE_ID: generate_entity_id('{}', calendar['summary'], + hass=hass), + }] + }) + return calendar_info + + +def load_config(path): + """Load the google_calendar_devices.yaml.""" + calendars = {} + try: + with open(path) as file: + data = yaml.load(file) + for calendar in data: + try: + calendars.update({calendar[CONF_CAL_ID]: + DEVICE_SCHEMA(calendar)}) + except VoluptuousError as exception: + # keep going + _LOGGER.warning('Calendar Invalid Data: %s', exception) + except FileNotFoundError: + # When YAML file could not be loaded/did not contain a dict + return {} + + return calendars + + +def update_config(path, calendar): + """Write the google_calendar_devices.yaml.""" + with open(path, 'a') as out: + out.write('\n') + yaml.dump([calendar], out, default_flow_style=False) diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index f57c56f17db..cbdfef85942 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -184,7 +184,7 @@ def async_setup(hass, config): tasks = [group.async_set_visible(visible) for group in component.async_extract_from_service(service, expand_group=False)] - yield from asyncio.gather(*tasks, loop=hass.loop) + yield from asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SET_VISIBILITY, visibility_service_handler, @@ -207,13 +207,14 @@ def _async_process_config(hass, config, component): icon = conf.get(CONF_ICON) view = conf.get(CONF_VIEW) - # This order is important as groups get a number based on creation - # order. + # Don't create tasks and await them all. The order is important as + # groups get a number based on creation order. group = yield from Group.async_create_group( hass, name, entity_ids, icon=icon, view=view, object_id=object_id) groups.append(group) - yield from component.async_add_entities(groups) + if groups: + yield from component.async_add_entities(groups) class Group(Entity): @@ -394,7 +395,7 @@ class Group(Entity): This method must be run in the event loop. """ self._async_update_group_state(new_state) - self.hass.loop.create_task(self.async_update_ha_state()) + self.hass.async_add_job(self.async_update_ha_state()) @property def _tracking_states(self): diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index fabff7add53..a008a3f4db6 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -222,6 +222,7 @@ class GzipFileSender(FileSender): return resp + _GZIP_FILE_SENDER = GzipFileSender() @@ -461,6 +462,9 @@ def request_handler_factory(view, handler): @asyncio.coroutine def handle(request): """Handle incoming request.""" + if not view.hass.is_running: + return web.Response(status=503) + remote_addr = view.hass.http.get_real_ip(request) # Auth code verbose on purpose diff --git a/homeassistant/components/input_boolean.py b/homeassistant/components/input_boolean.py index 1a510fbf6ec..579e6bade3e 100644 --- a/homeassistant/components/input_boolean.py +++ b/homeassistant/components/input_boolean.py @@ -23,17 +23,23 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' _LOGGER = logging.getLogger(__name__) CONF_INITIAL = 'initial' +DEFAULT_INITIAL = False SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, }) -CONFIG_SCHEMA = vol.Schema({DOMAIN: { - cv.slug: vol.Any({ - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_INITIAL, default=False): cv.boolean, - vol.Optional(CONF_ICON): cv.icon, - }, None)}}, extra=vol.ALLOW_EXTRA) +DEFAULT_CONFIG = {CONF_INITIAL: DEFAULT_INITIAL} + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + cv.slug: vol.Any({ + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL): cv.boolean, + vol.Optional(CONF_ICON): cv.icon, + }, None) + }) +}, extra=vol.ALLOW_EXTRA) def is_on(hass, entity_id): @@ -65,10 +71,10 @@ def async_setup(hass, config): for object_id, cfg in config[DOMAIN].items(): if not cfg: - cfg = {} + cfg = DEFAULT_CONFIG name = cfg.get(CONF_NAME) - state = cfg.get(CONF_INITIAL, False) + state = cfg.get(CONF_INITIAL) icon = cfg.get(CONF_ICON) entities.append(InputBoolean(object_id, name, state, icon)) @@ -89,7 +95,7 @@ def async_setup(hass, config): attr = 'async_toggle' tasks = [getattr(input_b, attr)() for input_b in target_inputs] - yield from asyncio.gather(*tasks, loop=hass.loop) + yield from asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_TURN_OFF, async_handler_service, schema=SERVICE_SCHEMA) diff --git a/homeassistant/components/input_select.py b/homeassistant/components/input_select.py index 61385c46cd6..9b563d271f5 100644 --- a/homeassistant/components/input_select.py +++ b/homeassistant/components/input_select.py @@ -55,14 +55,16 @@ def _cv_input_select(cfg): return cfg -CONFIG_SCHEMA = vol.Schema({DOMAIN: { - cv.slug: vol.All({ - vol.Optional(CONF_NAME): cv.string, - vol.Required(CONF_OPTIONS): vol.All(cv.ensure_list, vol.Length(min=1), - [cv.string]), - vol.Optional(CONF_INITIAL): cv.string, - vol.Optional(CONF_ICON): cv.icon, - }, _cv_input_select)}}, required=True, extra=vol.ALLOW_EXTRA) +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + cv.slug: vol.All({ + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_OPTIONS): + vol.All(cv.ensure_list, vol.Length(min=1), [cv.string]), + vol.Optional(CONF_INITIAL): cv.string, + vol.Optional(CONF_ICON): cv.icon, + }, _cv_input_select)}) +}, required=True, extra=vol.ALLOW_EXTRA) def select_option(hass, entity_id, option): @@ -111,7 +113,7 @@ def async_setup(hass, config): tasks = [input_select.async_select_option(call.data[ATTR_OPTION]) for input_select in target_inputs] - yield from asyncio.gather(*tasks, loop=hass.loop) + yield from asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SELECT_OPTION, async_select_option_service, @@ -124,7 +126,7 @@ def async_setup(hass, config): tasks = [input_select.async_offset_index(1) for input_select in target_inputs] - yield from asyncio.gather(*tasks, loop=hass.loop) + yield from asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SELECT_NEXT, async_select_next_service, @@ -137,7 +139,7 @@ def async_setup(hass, config): tasks = [input_select.async_offset_index(-1) for input_select in target_inputs] - yield from asyncio.gather(*tasks, loop=hass.loop) + yield from asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SELECT_PREVIOUS, async_select_previous_service, diff --git a/homeassistant/components/input_slider.py b/homeassistant/components/input_slider.py index 2a942829517..eccffb5ae29 100644 --- a/homeassistant/components/input_slider.py +++ b/homeassistant/components/input_slider.py @@ -51,17 +51,21 @@ def _cv_input_slider(cfg): cfg[CONF_INITIAL] = state return cfg -CONFIG_SCHEMA = vol.Schema({DOMAIN: { - cv.slug: vol.All({ - vol.Optional(CONF_NAME): cv.string, - vol.Required(CONF_MIN): vol.Coerce(float), - vol.Required(CONF_MAX): vol.Coerce(float), - vol.Optional(CONF_INITIAL): vol.Coerce(float), - vol.Optional(CONF_STEP, default=1): vol.All(vol.Coerce(float), - vol.Range(min=1e-3)), - vol.Optional(CONF_ICON): cv.icon, - vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string - }, _cv_input_slider)}}, required=True, extra=vol.ALLOW_EXTRA) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + cv.slug: vol.All({ + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_MIN): vol.Coerce(float), + vol.Required(CONF_MAX): vol.Coerce(float), + vol.Optional(CONF_INITIAL): vol.Coerce(float), + vol.Optional(CONF_STEP, default=1): vol.All(vol.Coerce(float), + vol.Range(min=1e-3)), + vol.Optional(CONF_ICON): cv.icon, + vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string + }, _cv_input_slider) + }) +}, required=True, extra=vol.ALLOW_EXTRA) def select_value(hass, entity_id, value): @@ -101,7 +105,7 @@ def async_setup(hass, config): tasks = [input_slider.async_select_value(call.data[ATTR_VALUE]) for input_slider in target_inputs] - yield from asyncio.gather(*tasks, loop=hass.loop) + yield from asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SELECT_VALUE, async_select_value_service, diff --git a/homeassistant/components/ios.py b/homeassistant/components/ios.py index f67ad966ead..f9b17b552de 100644 --- a/homeassistant/components/ios.py +++ b/homeassistant/components/ios.py @@ -2,7 +2,7 @@ Native Home Assistant iOS app component. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/ios/ +https://home-assistant.io/ecosystem/ios/ """ import asyncio import os diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index e3437d89e72..b4708164fe2 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -10,6 +10,7 @@ import csv import voluptuous as vol +from homeassistant.core import callback from homeassistant.components import group from homeassistant.config import load_yaml_config_file from homeassistant.const import ( @@ -20,6 +21,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util +from homeassistant.util.async import run_callback_threadsafe DOMAIN = "light" @@ -128,6 +130,18 @@ def 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): """Turn all or specified light on.""" + run_callback_threadsafe( + hass.loop, async_turn_on, hass, entity_id, transition, brightness, + rgb_color, xy_color, color_temp, white_value, + profile, flash, effect, color_name).result() + + +@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): + """Turn all or specified light on.""" data = { key: value for key, value in [ (ATTR_ENTITY_ID, entity_id), @@ -144,10 +158,17 @@ def turn_on(hass, entity_id=None, transition=None, brightness=None, ] if value is not None } - hass.services.call(DOMAIN, SERVICE_TURN_ON, data) + hass.async_add_job(hass.services.async_call, DOMAIN, SERVICE_TURN_ON, data) def turn_off(hass, entity_id=None, transition=None): + """Turn all or specified light off.""" + run_callback_threadsafe( + hass.loop, async_turn_off, hass, entity_id, transition).result() + + +@callback +def async_turn_off(hass, entity_id=None, transition=None): """Turn all or specified light off.""" data = { key: value for key, value in [ @@ -156,7 +177,8 @@ def turn_off(hass, entity_id=None, transition=None): ] if value is not None } - hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) + hass.async_add_job(hass.services.async_call, DOMAIN, SERVICE_TURN_OFF, + data) def toggle(hass, entity_id=None, transition=None): diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index 9de4aa6b0fc..1e1d4136142 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -17,8 +17,8 @@ from homeassistant.components.light import ( PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.8.zip' - '#flux_led==0.8'] +REQUIREMENTS = ['https://github.com/Danielhiversen/flux_led/archive/0.9.zip' + '#flux_led==0.9'] _LOGGER = logging.getLogger(__name__) @@ -135,9 +135,11 @@ class FluxLight(Light): rgb = kwargs.get(ATTR_RGB_COLOR) brightness = kwargs.get(ATTR_BRIGHTNESS) effect = kwargs.get(ATTR_EFFECT) - if rgb: + if rgb is not None and brightness is not None: + self._bulb.setRgb(*tuple(rgb), brightness=brightness) + elif rgb is not None: self._bulb.setRgb(*tuple(rgb)) - elif brightness: + elif brightness is not None: if self._mode == 'rgbw': self._bulb.setWarmWhite255(brightness) elif self._mode == 'rgb': diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 249cc30498f..8fd8a6ef097 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -22,11 +22,12 @@ from homeassistant.components.light import ( FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, SUPPORT_XY_COLOR, Light, PLATFORM_SCHEMA) +from homeassistant.config import load_yaml_config_file from homeassistant.const import (CONF_FILENAME, CONF_HOST, DEVICE_DEFAULT_NAME) from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['phue==0.8'] +REQUIREMENTS = ['phue==0.9'] # Track previously setup bridges _CONFIGURED_BRIDGES = {} @@ -37,6 +38,8 @@ _LOGGER = logging.getLogger(__name__) CONF_ALLOW_UNREACHABLE = 'allow_unreachable' DEFAULT_ALLOW_UNREACHABLE = False +DOMAIN = "light" +SERVICE_HUE_SCENE = "hue_activate_scene" MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) @@ -53,6 +56,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_FILENAME): cv.string, }) +ATTR_GROUP_NAME = "group_name" +ATTR_SCENE_NAME = "scene_name" +SCENE_SCHEMA = vol.Schema({ + vol.Required(ATTR_GROUP_NAME): cv.string, + vol.Required(ATTR_SCENE_NAME): cv.string, +}) + def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE): """Attempt to detect host based on existing configuration.""" @@ -166,6 +176,21 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable): add_devices(new_lights) _CONFIGURED_BRIDGES[socket.gethostbyname(host)] = True + + # create a service for calling run_scene directly on the bridge, + # used to simplify automation rules. + def hue_activate_scene(call): + """Service to call directly directly into bridge to set scenes.""" + group_name = call.data[ATTR_GROUP_NAME] + scene_name = call.data[ATTR_SCENE_NAME] + bridge.run_scene(group_name, scene_name) + + descriptions = load_yaml_config_file( + os.path.join(os.path.dirname(__file__), 'services.yaml')) + hass.services.register(DOMAIN, SERVICE_HUE_SCENE, hue_activate_scene, + descriptions.get(SERVICE_HUE_SCENE), + schema=SCENE_SCHEMA) + update_lights() diff --git a/homeassistant/components/light/litejet.py b/homeassistant/components/light/litejet.py index 3ff8067ec8c..33185dad07b 100644 --- a/homeassistant/components/light/litejet.py +++ b/homeassistant/components/light/litejet.py @@ -47,7 +47,7 @@ class LiteJetLight(Light): def _on_load_changed(self): """Called on a LiteJet thread when a load's state changes.""" _LOGGER.debug("Updating due to notification for %s", self._name) - self._hass.loop.create_task(self.async_update_ha_state(True)) + self._hass.async_add_job(self.async_update_ha_state(True)) @property def name(self): diff --git a/homeassistant/components/light/mqtt_template.py b/homeassistant/components/light/mqtt_template.py new file mode 100755 index 00000000000..4566a383645 --- /dev/null +++ b/homeassistant/components/light/mqtt_template.py @@ -0,0 +1,252 @@ +""" +Support for MQTT Template lights. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.mqtt_template/ +""" + +import logging +import voluptuous as vol + +import homeassistant.components.mqtt as mqtt +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_TRANSITION, PLATFORM_SCHEMA, + ATTR_FLASH, SUPPORT_BRIGHTNESS, SUPPORT_FLASH, + SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light) +from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, STATE_ON, STATE_OFF +from homeassistant.components.mqtt import ( + CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'mqtt_template' + +DEPENDENCIES = ['mqtt'] + +DEFAULT_NAME = 'MQTT Template Light' +DEFAULT_OPTIMISTIC = False + +CONF_COMMAND_ON_TEMPLATE = 'command_on_template' +CONF_COMMAND_OFF_TEMPLATE = 'command_off_template' +CONF_STATE_TEMPLATE = 'state_template' +CONF_BRIGHTNESS_TEMPLATE = 'brightness_template' +CONF_RED_TEMPLATE = 'red_template' +CONF_GREEN_TEMPLATE = 'green_template' +CONF_BLUE_TEMPLATE = 'blue_template' + +SUPPORT_MQTT_TEMPLATE = (SUPPORT_BRIGHTNESS | SUPPORT_FLASH | + SUPPORT_RGB_COLOR | SUPPORT_TRANSITION) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Required(CONF_COMMAND_ON_TEMPLATE): cv.template, + vol.Required(CONF_COMMAND_OFF_TEMPLATE): cv.template, + vol.Optional(CONF_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_BRIGHTNESS_TEMPLATE): cv.template, + vol.Optional(CONF_RED_TEMPLATE): cv.template, + vol.Optional(CONF_GREEN_TEMPLATE): cv.template, + vol.Optional(CONF_BLUE_TEMPLATE): cv.template, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS): + vol.All(vol.Coerce(int), vol.In([0, 1, 2])), + vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup a MQTT Template light.""" + add_devices([MqttTemplate( + hass, + config.get(CONF_NAME), + { + key: config.get(key) for key in ( + CONF_STATE_TOPIC, + CONF_COMMAND_TOPIC + ) + }, + { + key: config.get(key) for key in ( + CONF_COMMAND_ON_TEMPLATE, + CONF_COMMAND_OFF_TEMPLATE, + CONF_STATE_TEMPLATE, + CONF_BRIGHTNESS_TEMPLATE, + CONF_RED_TEMPLATE, + CONF_GREEN_TEMPLATE, + CONF_BLUE_TEMPLATE + ) + }, + config.get(CONF_OPTIMISTIC), + config.get(CONF_QOS), + config.get(CONF_RETAIN) + )]) + + +class MqttTemplate(Light): + """Representation of a MQTT Template light.""" + + def __init__(self, hass, name, topics, templates, optimistic, qos, retain): + """Initialize MQTT Template light.""" + self._hass = hass + self._name = name + self._topics = topics + self._templates = templates + for tpl in self._templates.values(): + if tpl is not None: + tpl.hass = hass + self._optimistic = optimistic or topics[CONF_STATE_TOPIC] is None \ + or templates[CONF_STATE_TEMPLATE] is None + self._qos = qos + self._retain = retain + + # features + self._state = False + if self._templates[CONF_BRIGHTNESS_TEMPLATE] is not None: + self._brightness = 255 + else: + self._brightness = None + + if (self._templates[CONF_RED_TEMPLATE] is not None and + self._templates[CONF_GREEN_TEMPLATE] is not None and + self._templates[CONF_BLUE_TEMPLATE] is not None): + self._rgb = [0, 0, 0] + else: + self._rgb = None + + def state_received(topic, payload, qos): + """A new MQTT message has been received.""" + # read state + state = self._templates[CONF_STATE_TEMPLATE].\ + render_with_possible_json_value(payload) + if state == STATE_ON: + self._state = True + elif state == STATE_OFF: + self._state = False + else: + _LOGGER.warning('Invalid state value received') + + # read brightness + if self._brightness is not None: + try: + self._brightness = int( + self._templates[CONF_BRIGHTNESS_TEMPLATE]. + render_with_possible_json_value(payload) + ) + except ValueError: + _LOGGER.warning('Invalid brightness value received') + + # read color + if self._rgb is not None: + try: + self._rgb[0] = int( + self._templates[CONF_RED_TEMPLATE]. + render_with_possible_json_value(payload)) + self._rgb[1] = int( + self._templates[CONF_GREEN_TEMPLATE]. + render_with_possible_json_value(payload)) + self._rgb[2] = int( + self._templates[CONF_BLUE_TEMPLATE]. + render_with_possible_json_value(payload)) + except ValueError: + _LOGGER.warning('Invalid color value received') + + self.update_ha_state() + + if self._topics[CONF_STATE_TOPIC] is not None: + mqtt.subscribe(self._hass, self._topics[CONF_STATE_TOPIC], + state_received, self._qos) + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return self._brightness + + @property + def rgb_color(self): + """Return the RGB color value [int, int, int].""" + return self._rgb + + @property + def should_poll(self): + """Return True if entity has to be polled for state. + + False if entity pushes its state to HA. + """ + return False + + @property + def name(self): + """Return the name of the entity.""" + return self._name + + @property + def is_on(self): + """Return True if entity is on.""" + return self._state + + @property + def assumed_state(self): + """Return True if unable to access real state of the entity.""" + return self._optimistic + + def turn_on(self, **kwargs): + """Turn the entity on.""" + # state + values = {'state': True} + if self._optimistic: + self._state = True + + # brightness + if ATTR_BRIGHTNESS in kwargs: + values['brightness'] = int(kwargs[ATTR_BRIGHTNESS]) + + if self._optimistic: + self._brightness = kwargs[ATTR_BRIGHTNESS] + + # color + if ATTR_RGB_COLOR in kwargs: + values['red'] = kwargs[ATTR_RGB_COLOR][0] + values['green'] = kwargs[ATTR_RGB_COLOR][1] + values['blue'] = kwargs[ATTR_RGB_COLOR][2] + + if self._optimistic: + self._rgb = kwargs[ATTR_RGB_COLOR] + + # flash + if ATTR_FLASH in kwargs: + values['flash'] = kwargs.get(ATTR_FLASH) + + # transition + if ATTR_TRANSITION in kwargs: + values['transition'] = kwargs[ATTR_TRANSITION] + + mqtt.publish( + self._hass, self._topics[CONF_COMMAND_TOPIC], + self._templates[CONF_COMMAND_ON_TEMPLATE].render(**values), + self._qos, self._retain + ) + + if self._optimistic: + self.update_ha_state() + + def turn_off(self, **kwargs): + """Turn the entity off.""" + # state + values = {'state': False} + if self._optimistic: + self._state = False + + # transition + if ATTR_TRANSITION in kwargs: + values['transition'] = kwargs[ATTR_TRANSITION] + + mqtt.publish( + self._hass, self._topics[CONF_COMMAND_TOPIC], + self._templates[CONF_COMMAND_OFF_TEMPLATE].render(**values), + self._qos, self._retain + ) + + if self._optimistic: + self.update_ha_state() diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index 3bd53ff9064..20da91682ad 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -31,7 +31,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info is None: return - for gateway in mysensors.GATEWAYS.values(): + gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS) + if not gateways: + return + + for gateway in gateways: # Define the S_TYPES and V_TYPES that the platform should handle as # states. Map them in a dict of lists. pres = gateway.const.Presentation diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index afcc54d717f..8931a46bb73 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -81,3 +81,15 @@ toggle: transition: description: Duration in seconds it takes to get to next state example: 60 + +hue_activate_scene: + description: Activate a hue scene stored in the hue hub + + fields: + group_name: + description: Name of hue group/room from the hue app + example: "Living Room" + + scene_name: + description: Name of hue scene from the hue app + example: "Energize" diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index d117b66df79..1d292a53419 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -41,7 +41,10 @@ class WinkLight(WinkDevice, Light): @property def brightness(self): """Return the brightness of the light.""" - return int(self.wink.brightness() * 255) + if self.wink.brightness() is not None: + return int(self.wink.brightness() * 255) + else: + return None @property def rgb_color(self): @@ -52,6 +55,8 @@ class WinkLight(WinkDevice, Light): hue = self.wink.color_hue() saturation = self.wink.color_saturation() value = int(self.wink.brightness() * 255) + if hue is None or saturation is None or value is None: + return None rgb = colorsys.hsv_to_rgb(hue, saturation, value) r_value = int(round(rgb[0])) g_value = int(round(rgb[1])) diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index fe965efd107..d4e94b00e66 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -24,26 +24,6 @@ AEOTEC = 0x86 AEOTEC_ZW098_LED_BULB = 0x62 AEOTEC_ZW098_LED_BULB_LIGHT = (AEOTEC, AEOTEC_ZW098_LED_BULB) -LINEAR = 0x14f -LINEAR_WD500Z_DIMMER = 0x3034 -LINEAR_WD500Z_DIMMER_LIGHT = (LINEAR, LINEAR_WD500Z_DIMMER) - -GE = 0x63 -GE_12724_DIMMER = 0x3031 -GE_12724_DIMMER_LIGHT = (GE, GE_12724_DIMMER) - -DRAGONTECH = 0x184 -DRAGONTECH_PD100_DIMMER = 0x3032 -DRAGONTECH_PD100_DIMMER_LIGHT = (DRAGONTECH, DRAGONTECH_PD100_DIMMER) - -ACT = 0x01 -ACT_ZDP100_DIMMER = 0x3030 -ACT_ZDP100_DIMMER_LIGHT = (ACT, ACT_ZDP100_DIMMER) - -HOMESEER = 0x0c -HOMESEER_WD100_DIMMER = 0x3034 -HOMESEER_WD100_DIMMER_LIGHT = (HOMESEER, HOMESEER_WD100_DIMMER) - COLOR_CHANNEL_WARM_WHITE = 0x01 COLOR_CHANNEL_COLD_WHITE = 0x02 COLOR_CHANNEL_RED = 0x04 @@ -51,15 +31,9 @@ COLOR_CHANNEL_GREEN = 0x08 COLOR_CHANNEL_BLUE = 0x10 WORKAROUND_ZW098 = 'zw098' -WORKAROUND_DELAY = 'alt_delay' DEVICE_MAPPINGS = { - AEOTEC_ZW098_LED_BULB_LIGHT: WORKAROUND_ZW098, - LINEAR_WD500Z_DIMMER_LIGHT: WORKAROUND_DELAY, - GE_12724_DIMMER_LIGHT: WORKAROUND_DELAY, - DRAGONTECH_PD100_DIMMER_LIGHT: WORKAROUND_DELAY, - ACT_ZDP100_DIMMER_LIGHT: WORKAROUND_DELAY, - HOMESEER_WD100_DIMMER_LIGHT: WORKAROUND_DELAY, + AEOTEC_ZW098_LED_BULB_LIGHT: WORKAROUND_ZW098 } # Generate midpoint color temperatures for bulbs that have limited @@ -75,10 +49,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Find and add Z-Wave lights.""" if discovery_info is None or zwave.NETWORK is None: return - node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]] value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]] - + customize = hass.data['zwave_customize'] + name = super().entity_id + node_config = customize.get(name, {}) + refresh = node_config.get(zwave.CONF_REFRESH_VALUE) + delay = node_config.get(zwave.CONF_REFRESH_DELAY) + _LOGGER.debug('customize=%s name=%s node_config=%s CONF_REFRESH_VALUE=%s' + ' CONF_REFRESH_DELAY=%s', customize, name, node_config, + refresh, delay) if value.command_class != zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL: return if value.type != zwave.const.TYPE_BYTE: @@ -89,9 +69,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): value.set_change_verified(False) if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_COLOR): - add_devices([ZwaveColorLight(value)]) + add_devices([ZwaveColorLight(value, refresh, delay)]) else: - add_devices([ZwaveDimmer(value)]) + add_devices([ZwaveDimmer(value, refresh, delay)]) def brightness_state(value): @@ -105,7 +85,7 @@ def brightness_state(value): class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light): """Representation of a Z-Wave dimmer.""" - def __init__(self, value): + def __init__(self, value, refresh, delay): """Initialize the light.""" from openzwave.network import ZWaveNetwork from pydispatch import dispatcher @@ -113,7 +93,8 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light): zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN) self._brightness = None self._state = None - self._alt_delay = None + self._delay = delay + self._refresh_value = refresh self._zw098 = None # Enable appropriate workaround flags for our device @@ -126,17 +107,14 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light): if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZW098: _LOGGER.debug("AEOTEC ZW098 workaround enabled") self._zw098 = 1 - elif DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_DELAY: - _LOGGER.debug("Dimmer delay workaround enabled for node:" - " %s", value.parent_id) - self._alt_delay = 1 self.update_properties() # Used for value change event handling self._refreshing = False self._timer = None - + _LOGGER.debug('self._refreshing=%s self.delay=%s', + self._refresh_value, self._delay) dispatcher.connect( self._value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED) @@ -149,26 +127,25 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light): """Called when a value has changed on the network.""" if self._value.value_id == value.value_id or \ self._value.node == value.node: - - if self._refreshing: - self._refreshing = False - self.update_properties() - else: - def _refresh_value(): - """Used timer callback for delayed value refresh.""" - self._refreshing = True - self._value.refresh() - - if self._timer is not None and self._timer.isAlive(): - self._timer.cancel() - - if self._alt_delay: - self._timer = Timer(5, _refresh_value) + if self._refresh_value: + if self._refreshing: + self._refreshing = False + self.update_properties() else: - self._timer = Timer(2, _refresh_value) - self._timer.start() + def _refresh_value(): + """Used timer callback for delayed value refresh.""" + self._refreshing = True + self._value.refresh() - self.update_ha_state() + if self._timer is not None and self._timer.isAlive(): + self._timer.cancel() + + self._timer = Timer(self._delay, _refresh_value) + self._timer.start() + self.update_ha_state() + else: + self.update_properties() + self.update_ha_state() @property def brightness(self): @@ -213,7 +190,7 @@ def ct_to_rgb(temp): class ZwaveColorLight(ZwaveDimmer): """Representation of a Z-Wave color changing light.""" - def __init__(self, value): + def __init__(self, value, refresh, delay): """Initialize the light.""" from openzwave.network import ZWaveNetwork from pydispatch import dispatcher diff --git a/homeassistant/components/media_player/braviatv.py b/homeassistant/components/media_player/braviatv.py index d1c60bf2ec1..dbcecd6097a 100644 --- a/homeassistant/components/media_player/braviatv.py +++ b/homeassistant/components/media_player/braviatv.py @@ -21,8 +21,8 @@ from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv REQUIREMENTS = [ - 'https://github.com/aparraga/braviarc/archive/0.3.5.zip' - '#braviarc==0.3.5'] + 'https://github.com/aparraga/braviarc/archive/0.3.6.zip' + '#braviarc==0.3.6'] BRAVIA_CONFIG_FILE = 'bravia.conf' diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 5d6289587be..1ec61cc621a 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -280,9 +280,9 @@ class CastDevice(MediaPlayerDevice): def new_cast_status(self, status): """Called when a new cast status is received.""" self.cast_status = status - self.update_ha_state() + self.schedule_update_ha_state() def new_media_status(self, status): """Called when a new media status is received.""" self.media_status = status - self.update_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 1b2bc4f7fc7..ae9f8c8d721 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -104,7 +104,7 @@ class KodiDevice(MediaPlayerDevice): if len(self._players) == 0: return STATE_IDLE - if self._properties['speed'] == 0: + if self._properties['speed'] == 0 and not self._properties['live']: return STATE_PAUSED else: return STATE_PLAYING @@ -120,7 +120,7 @@ class KodiDevice(MediaPlayerDevice): self._properties = self._server.Player.GetProperties( player_id, - ['time', 'totaltime', 'speed'] + ['time', 'totaltime', 'speed', 'live'] ) self._item = self._server.Player.GetItem( @@ -163,7 +163,7 @@ class KodiDevice(MediaPlayerDevice): @property def media_duration(self): """Duration of current playing media in seconds.""" - if self._properties is not None: + if self._properties is not None and not self._properties['live']: total_time = self._properties['totaltime'] return ( diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index e384fd4bd3f..a43e8b551a3 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -128,6 +128,8 @@ class SamsungTVDevice(MediaPlayerDevice): def turn_off(self): """Turn off media player.""" self.send_key('KEY_POWEROFF') + # Force closing of remote session to provide instant UI feedback + self.get_remote().close() def volume_up(self): """Volume up the media player.""" diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index ebc8d58874a..5b070e1f5bd 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -73,7 +73,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): player = soco.SoCo(discovery_info) # if device allready exists by config - if player.uid in DEVICES: + if player.uid in [x.unique_id for x in DEVICES]: return True if player.is_visible: @@ -350,9 +350,6 @@ class SonosDevice(MediaPlayerDevice): if is_available: - self._is_playing_tv = self._player.is_playing_tv - self._is_playing_line_in = self._player.is_playing_line_in - track_info = None if self._last_avtransport_event: variables = self._last_avtransport_event.variables @@ -394,6 +391,10 @@ class SonosDevice(MediaPlayerDevice): self._coordinator = None if not self._coordinator: + + is_playing_tv = self._player.is_playing_tv + is_playing_line_in = self._player.is_playing_line_in + media_info = self._player.avTransport.GetMediaInfo( [('InstanceID', 0)] ) @@ -407,7 +408,23 @@ class SonosDevice(MediaPlayerDevice): current_media_uri.startswith('x-sonosapi-stream:') or \ current_media_uri.startswith('x-rincon-mp3radio:') - if is_radio_stream: + if is_playing_tv or is_playing_line_in: + # playing from line-in/tv. + + support_previous_track = False + support_next_track = False + support_pause = False + + if is_playing_tv: + media_artist = SUPPORT_SOURCE_TV + else: + media_artist = SUPPORT_SOURCE_LINEIN + + media_album_name = None + media_title = None + media_image_url = None + + elif is_radio_stream: is_radio_stream = True media_image_url = self._format_media_image_url( current_media_uri @@ -506,6 +523,8 @@ class SonosDevice(MediaPlayerDevice): self._support_previous_track = support_previous_track self._support_next_track = support_next_track self._support_pause = support_pause + self._is_playing_tv = is_playing_tv + self._is_playing_line_in = is_playing_line_in # update state of the whole group # pylint: disable=protected-access @@ -513,7 +532,7 @@ class SonosDevice(MediaPlayerDevice): if device.entity_id is not self.entity_id: self.hass.add_job(device.async_update_ha_state) - if self._queue is None: + if self._queue is None and self.entity_id is not None: self._subscribe_to_player_events() else: self._player_volume = None @@ -714,10 +733,13 @@ class SonosDevice(MediaPlayerDevice): @property def source(self): """Name of the current input source.""" - if self._is_playing_line_in: - return SUPPORT_SOURCE_LINEIN - if self._is_playing_tv: - return SUPPORT_SOURCE_TV + if self._coordinator: + return self._coordinator.source + else: + if self._is_playing_line_in: + return SUPPORT_SOURCE_LINEIN + elif self._is_playing_tv: + return SUPPORT_SOURCE_TV return None diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index ee21e67bf49..18081e9eebb 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -111,9 +111,11 @@ class LogitechMediaServer(object): def query(self, *parameters): """Send request and await response from server.""" - response = urllib.parse.unquote(self.get(' '.join(parameters))) + response = self.get(' '.join(parameters)) + response = response.split(' ')[-1].strip() + response = urllib.parse.unquote(response) - return response.split(' ')[-1].strip() + return response def get_player_status(self, player): """Get the status of a player.""" diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index 0e265199fce..05f60cf06d8 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -18,17 +18,12 @@ from homeassistant.const import (CONF_NAME, CONF_HOST, STATE_OFF, STATE_ON, STATE_PLAYING, STATE_IDLE) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['rxv==0.3.1'] +REQUIREMENTS = ['rxv==0.4.0'] _LOGGER = logging.getLogger(__name__) SUPPORT_YAMAHA = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ - SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE | \ - SUPPORT_PLAY_MEDIA - -# Only supported by some sources -SUPPORT_PLAYBACK = SUPPORT_PLAY_MEDIA | SUPPORT_PAUSE | SUPPORT_STOP | \ - SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK + SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE CONF_SOURCE_NAMES = 'source_names' CONF_SOURCE_IGNORE = 'source_ignore' @@ -187,8 +182,16 @@ class YamahaDevice(MediaPlayerDevice): def supported_media_commands(self): """Flag of media commands that are supported.""" supported_commands = SUPPORT_YAMAHA - if self._is_playback_supported: - supported_commands |= SUPPORT_PLAYBACK + + supports = self._receiver.get_playback_support() + mapping = {'play': SUPPORT_PLAY_MEDIA, + 'pause': SUPPORT_PAUSE, + 'stop': SUPPORT_STOP, + 'skip_f': SUPPORT_NEXT_TRACK, + 'skip_r': SUPPORT_PREVIOUS_TRACK} + for attr, feature in mapping.items(): + if getattr(supports, attr, False): + supported_commands |= feature return supported_commands def turn_off(self): diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 85d99e5f7ee..87c1a783e6e 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -18,11 +18,12 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import template, config_validation as cv from homeassistant.helpers.event import threaded_listener_factory from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_VALUE_TEMPLATE) + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_VALUE_TEMPLATE, + CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_PAYLOAD) _LOGGER = logging.getLogger(__name__) -DOMAIN = "mqtt" +DOMAIN = 'mqtt' MQTT_CLIENT = None @@ -33,16 +34,15 @@ REQUIREMENTS = ['paho-mqtt==1.2'] CONF_EMBEDDED = 'embedded' CONF_BROKER = 'broker' -CONF_PORT = 'port' CONF_CLIENT_ID = 'client_id' CONF_KEEPALIVE = 'keepalive' -CONF_USERNAME = 'username' -CONF_PASSWORD = 'password' CONF_CERTIFICATE = 'certificate' CONF_CLIENT_KEY = 'client_key' CONF_CLIENT_CERT = 'client_cert' CONF_TLS_INSECURE = 'tls_insecure' -CONF_PROTOCOL = 'protocol' + +CONF_BIRTH_MESSAGE = 'birth_message' +CONF_WILL_MESSAGE = 'will_message' CONF_STATE_TOPIC = 'state_topic' CONF_COMMAND_TOPIC = 'command_topic' @@ -78,20 +78,27 @@ def valid_publish_topic(value): """Validate that we can publish using this MQTT topic.""" return valid_subscribe_topic(value, invalid_chars='#+\0') + _VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2])) _HBMQTT_CONFIG_SCHEMA = vol.Schema(dict) CLIENT_KEY_AUTH_MSG = 'client_key and client_cert must both be present in ' \ 'the mqtt broker config' +MQTT_WILL_BIRTH_SCHEMA = vol.Schema({ + vol.Required(ATTR_TOPIC): valid_publish_topic, + vol.Required(ATTR_PAYLOAD, CONF_PAYLOAD): cv.string, + vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, + vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, +}, required=True) + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_CLIENT_ID): cv.string, vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All(vol.Coerce(int), vol.Range(min=15)), vol.Optional(CONF_BROKER): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): - vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)), + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_CERTIFICATE): cv.isfile, @@ -103,6 +110,8 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All(cv.string, vol.In([PROTOCOL_31, PROTOCOL_311])), vol.Optional(CONF_EMBEDDED): _HBMQTT_CONFIG_SCHEMA, + vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA }), }, extra=vol.ALLOW_EXTRA) @@ -130,10 +139,10 @@ MQTT_RW_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend({ # Service call validation schema MQTT_PUBLISH_SCHEMA = vol.Schema({ vol.Required(ATTR_TOPIC): valid_publish_topic, - vol.Exclusive(ATTR_PAYLOAD, 'payload'): object, - vol.Exclusive(ATTR_PAYLOAD_TEMPLATE, 'payload'): cv.string, - vol.Required(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, - vol.Required(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + vol.Exclusive(ATTR_PAYLOAD, CONF_PAYLOAD): object, + vol.Exclusive(ATTR_PAYLOAD_TEMPLATE, CONF_PAYLOAD): cv.string, + vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, + vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, }, required=True) @@ -196,7 +205,7 @@ def _setup_server(hass, config): server = prepare_setup_platform(hass, config, DOMAIN, 'server') if server is None: - _LOGGER.error('Unable to load embedded server.') + _LOGGER.error("Unable to load embedded server") return None success, broker_config = server.start(hass, conf.get(CONF_EMBEDDED)) @@ -221,7 +230,7 @@ def setup(hass, config): # Embedded broker doesn't have some ssl variables client_key, client_cert, tls_insecure = None, None, None elif not broker_config and not broker_in_conf: - _LOGGER.error('Unable to start broker and auto-configure MQTT.') + _LOGGER.error("Unable to start broker and auto-configure MQTT") return False if broker_in_conf: @@ -241,15 +250,18 @@ def setup(hass, config): certificate = os.path.join(os.path.dirname(__file__), 'addtrustexternalcaroot.crt') + will_message = conf.get(CONF_WILL_MESSAGE) + birth_message = conf.get(CONF_BIRTH_MESSAGE) + global MQTT_CLIENT try: MQTT_CLIENT = MQTT(hass, broker, port, client_id, keepalive, username, password, certificate, client_key, - client_cert, tls_insecure, protocol) + client_cert, tls_insecure, protocol, will_message, + birth_message) except socket.error: _LOGGER.exception("Can't connect to the broker. " - "Please check your settings and the broker " - "itself.") + "Please check your settings and the broker itself") return False def stop_mqtt(event): @@ -274,7 +286,7 @@ def setup(hass, config): except template.jinja2.TemplateError as exc: _LOGGER.error( "Unable to publish to '%s': rendering payload template of " - "'%s' failed because %s.", + "'%s' failed because %s", msg_topic, payload_template, exc) return MQTT_CLIENT.publish(msg_topic, payload, qos, retain) @@ -296,13 +308,14 @@ class MQTT(object): def __init__(self, hass, broker, port, client_id, keepalive, username, password, certificate, client_key, client_cert, - tls_insecure, protocol): + tls_insecure, protocol, will_message, birth_message): """Initialize Home Assistant MQTT client.""" import paho.mqtt.client as mqtt self.hass = hass self.topics = {} self.progress = {} + self.birth_message = birth_message if protocol == PROTOCOL_31: proto = mqtt.MQTTv31 @@ -329,7 +342,11 @@ class MQTT(object): self._mqttc.on_connect = self._mqtt_on_connect self._mqttc.on_disconnect = self._mqtt_on_disconnect self._mqttc.on_message = self._mqtt_on_message - + if will_message: + self._mqttc.will_set(will_message.get(ATTR_TOPIC), + will_message.get(ATTR_PAYLOAD), + will_message.get(ATTR_QOS), + will_message.get(ATTR_RETAIN)) self._mqttc.connect(broker, port, keepalive) def publish(self, topic, payload, qos, retain): @@ -365,7 +382,8 @@ class MQTT(object): def _mqtt_on_connect(self, _mqttc, _userdata, _flags, result_code): """On connect callback. - Resubscribe to all topics we were subscribed to. + Resubscribe to all topics we were subscribed to and publish birth + message. """ if result_code != 0: _LOGGER.error('Unable to connect to the MQTT broker: %s', { @@ -387,6 +405,11 @@ class MQTT(object): # qos is None if we were in process of subscribing if qos is not None: self.subscribe(topic, qos) + if self.birth_message: + self.publish(self.birth_message.get(ATTR_TOPIC), + self.birth_message.get(ATTR_PAYLOAD), + self.birth_message.get(ATTR_QOS), + self.birth_message.get(ATTR_RETAIN)) def _mqtt_on_subscribe(self, _mqttc, _userdata, mid, granted_qos): """Subscribe successful callback.""" @@ -404,7 +427,7 @@ class MQTT(object): "MQTT topic: %s, Payload: %s", msg.topic, msg.payload) else: - _LOGGER.debug("received message on %s: %s", + _LOGGER.debug("Received message on %s: %s", msg.topic, payload) self.hass.bus.fire(EVENT_MQTT_MESSAGE_RECEIVED, { ATTR_TOPIC: msg.topic, @@ -440,14 +463,14 @@ class MQTT(object): while True: try: if self._mqttc.reconnect() == 0: - _LOGGER.info('Successfully reconnected to the MQTT server') + _LOGGER.info("Successfully reconnected to the MQTT server") break except socket.error: pass wait_time = min(2**tries, MAX_RECONNECT_WAIT) _LOGGER.warning( - 'Disconnected from MQTT (%s). Trying to reconnect in %ss', + "Disconnected from MQTT (%s). Trying to reconnect in %s s", result_code, wait_time) # It is ok to sleep here as we are in the MQTT thread. time.sleep(wait_time) diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py index cffde56b319..cc240e41a30 100644 --- a/homeassistant/components/mqtt/server.py +++ b/homeassistant/components/mqtt/server.py @@ -4,41 +4,21 @@ Support for a local MQTT broker. For more details about this component, please refer to the documentation at https://home-assistant.io/components/mqtt/#use-the-embedded-broker """ -import asyncio import logging import tempfile -import threading +from homeassistant.core import callback from homeassistant.components.mqtt import PROTOCOL_311 from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.util.async import run_coroutine_threadsafe REQUIREMENTS = ['hbmqtt==0.7.1'] DEPENDENCIES = ['http'] -@asyncio.coroutine -def broker_coro(loop, config): - """Start broker coroutine.""" - from hbmqtt.broker import Broker - broker = Broker(config, loop) - yield from broker.start() - return broker - - -def loop_run(loop, broker, shutdown_complete): - """Run broker and clean up when done.""" - loop.run_forever() - # run_forever ends when stop is called because we're shutting down - loop.run_until_complete(broker.shutdown()) - loop.close() - shutdown_complete.set() - - def start(hass, server_config): """Initialize MQTT Server.""" - from hbmqtt.broker import BrokerException - - loop = asyncio.new_event_loop() + from hbmqtt.broker import Broker, BrokerException try: passwd = tempfile.NamedTemporaryFile() @@ -48,29 +28,20 @@ def start(hass, server_config): else: client_config = None - start_server = asyncio.gather(broker_coro(loop, server_config), - loop=loop) - loop.run_until_complete(start_server) - # Result raises exception if one was raised during startup - broker = start_server.result()[0] + broker = Broker(server_config, hass.loop) + run_coroutine_threadsafe(broker.start(), hass.loop).result() except BrokerException: logging.getLogger(__name__).exception('Error initializing MQTT server') - loop.close() return False, None finally: passwd.close() - shutdown_complete = threading.Event() + @callback + def shutdown_mqtt_server(event): + """Shut down the MQTT server.""" + hass.async_add_job(broker.shutdown()) - def shutdown(event): - """Gracefully shutdown MQTT broker.""" - loop.call_soon_threadsafe(loop.stop) - shutdown_complete.wait() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) - - threading.Thread(target=loop_run, args=(loop, broker, shutdown_complete), - name="MQTT-server").start() + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_mqtt_server) return True, client_config diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index b86bed57b82..b6778760b1a 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -38,7 +38,7 @@ DEFAULT_VERSION = 1.4 DEFAULT_BAUD_RATE = 115200 DEFAULT_TCP_PORT = 5003 DOMAIN = 'mysensors' -GATEWAYS = None +MYSENSORS_GATEWAYS = 'mysensors_gateways' MQTT_COMPONENT = 'mqtt' REQUIREMENTS = [ 'https://github.com/theolind/pymysensors/archive/' @@ -132,9 +132,15 @@ def setup(hass, config): return gateway + gateways = hass.data.get(MYSENSORS_GATEWAYS) + if gateways is not None: + _LOGGER.error( + '%s already exists in %s, will not setup %s component', + MYSENSORS_GATEWAYS, hass.data, DOMAIN) + return False + # Setup all devices from config - global GATEWAYS - GATEWAYS = {} + gateways = [] conf_gateways = config[DOMAIN][CONF_GATEWAYS] for index, gway in enumerate(conf_gateways): @@ -146,17 +152,19 @@ def setup(hass, config): tcp_port = gway.get(CONF_TCP_PORT) in_prefix = gway.get(CONF_TOPIC_IN_PREFIX) out_prefix = gway.get(CONF_TOPIC_OUT_PREFIX) - GATEWAYS[device] = setup_gateway( + ready_gateway = setup_gateway( device, persistence_file, baud_rate, tcp_port, in_prefix, out_prefix) - if GATEWAYS[device] is None: - GATEWAYS.pop(device) + if ready_gateway is not None: + gateways.append(ready_gateway) - if not GATEWAYS: + if not gateways: _LOGGER.error( 'No devices could be setup as gateways, check your configuration') return False + hass.data[MYSENSORS_GATEWAYS] = gateways + for component in ['sensor', 'switch', 'light', 'binary_sensor', 'climate', 'cover']: discovery.load_platform(hass, component, DOMAIN, {}, config) diff --git a/homeassistant/components/neato.py b/homeassistant/components/neato.py new file mode 100644 index 00000000000..0c77c3a6b5c --- /dev/null +++ b/homeassistant/components/neato.py @@ -0,0 +1,81 @@ +""" +Support for Neato botvac connected vacuum cleaners. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/neato/ +""" +import logging +from datetime import timedelta +from urllib.error import HTTPError + +import voluptuous as vol + +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers import discovery +from homeassistant.util import Throttle +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['https://github.com/jabesq/pybotvac/archive/v0.0.1.zip' + '#pybotvac==0.0.1'] + +DOMAIN = 'neato' +NEATO_ROBOTS = 'neato_robots' +NEATO_LOGIN = 'neato_login' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Setup the Verisure component.""" + from pybotvac import Account + + hass.data[NEATO_LOGIN] = NeatoHub(hass, config[DOMAIN], Account) + hub = hass.data[NEATO_LOGIN] + if not hub.login(): + _LOGGER.debug('Failed to login to Neato API') + return False + hub.update_robots() + for component in ('sensor', 'switch'): + discovery.load_platform(hass, component, DOMAIN, {}, config) + + return True + + +class NeatoHub(object): + """A My Neato hub wrapper class.""" + + def __init__(self, hass, domain_config, neato): + """Initialize the Neato hub.""" + self.config = domain_config + self._neato = neato + self._hass = hass + + self.my_neato = neato( + domain_config[CONF_USERNAME], + domain_config[CONF_PASSWORD]) + self._hass.data[NEATO_ROBOTS] = self.my_neato.robots + + def login(self): + """Login to My Neato.""" + try: + _LOGGER.debug('Trying to connect to Neato API') + self.my_neato = self._neato(self.config[CONF_USERNAME], + self.config[CONF_PASSWORD]) + return True + except HTTPError: + _LOGGER.error("Unable to connect to Neato API") + return False + + @Throttle(timedelta(seconds=1)) + def update_robots(self): + """Update the robot states.""" + _LOGGER.debug('Running HUB.update_robots %s', + self._hass.data[NEATO_ROBOTS]) + self._hass.data[NEATO_ROBOTS] = self.my_neato.robots diff --git a/homeassistant/components/netatmo.py b/homeassistant/components/netatmo.py index 77432411e1a..d6e0101e4e0 100644 --- a/homeassistant/components/netatmo.py +++ b/homeassistant/components/netatmo.py @@ -18,7 +18,7 @@ from homeassistant.util import Throttle REQUIREMENTS = [ 'https://github.com/jabesq/netatmo-api-python/archive/' - 'v0.6.0.zip#lnetatmo==0.6.0'] + 'v0.7.0.zip#lnetatmo==0.7.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/notify/html5.py b/homeassistant/components/notify/html5.py index 35f59af1135..baf887c1e6e 100644 --- a/homeassistant/components/notify/html5.py +++ b/homeassistant/components/notify/html5.py @@ -25,9 +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 -REQUIREMENTS = ['https://github.com/web-push-libs/pywebpush/archive/' - 'e743dc92558fc62178d255c0018920d74fa778ed.zip#' - 'pywebpush==0.5.0', 'PyJWT==1.4.2'] +REQUIREMENTS = ['pywebpush==0.6.1', 'PyJWT==1.4.2'] DEPENDENCIES = ['frontend'] @@ -141,11 +139,23 @@ def _load_config(filename): return None +class JSONBytesDecoder(json.JSONEncoder): + """JSONEncoder to decode bytes objects to unicode.""" + + # pylint: disable=method-hidden + def default(self, obj): + """Decode object if it's a bytes object, else defer to baseclass.""" + if isinstance(obj, bytes): + return obj.decode() + return json.JSONEncoder.default(self, obj) + + def _save_config(filename, config): """Save configuration.""" try: with open(filename, 'w') as fdesc: - fdesc.write(json.dumps(config)) + fdesc.write(json.dumps( + config, cls=JSONBytesDecoder, indent=4, sort_keys=True)) except (IOError, TypeError) as error: _LOGGER.error('Saving config file failed: %s', error) return False diff --git a/homeassistant/components/notify/ios.py b/homeassistant/components/notify/ios.py index 8dc4c7d9701..5cd18640487 100644 --- a/homeassistant/components/notify/ios.py +++ b/homeassistant/components/notify/ios.py @@ -2,7 +2,7 @@ iOS push notification platform for notify component. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.ios/ +https://home-assistant.io/ecosystem/ios/notifications/ """ import logging from datetime import datetime, timezone @@ -48,8 +48,8 @@ def get_service(hass, config): if not ios.devices_with_push(): _LOGGER.error(("The notify.ios platform was loaded but no " "devices exist! Please check the documentation at " - "https://home-assistant.io/components/notify.ios/ " - "for more information")) + "https://home-assistant.io/ecosystem/ios/notifications" + "/ for more information")) return None return iOSNotificationService() diff --git a/homeassistant/components/notify/nma.py b/homeassistant/components/notify/nma.py index a21a37bb323..7a05d08134f 100644 --- a/homeassistant/components/notify/nma.py +++ b/homeassistant/components/notify/nma.py @@ -26,8 +26,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def get_service(hass, config): """Get the NMA notification service.""" - response = requests.get(_RESOURCE + 'verify', - params={"apikey": config[CONF_API_KEY]}) + parameters = { + 'apikey': config[CONF_API_KEY], + } + response = requests.get( + '{}{}'.format(_RESOURCE, 'verify'), params=parameters, timeout=5) tree = ET.fromstring(response.content) if tree[0].tag == 'error': @@ -47,14 +50,15 @@ class NmaNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" data = { - "apikey": self._api_key, - "application": 'home-assistant', - "event": kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), - "description": message, - "priority": 0, + 'apikey': self._api_key, + 'application': 'home-assistant', + 'event': kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), + 'description': message, + 'priority': 0, } - response = requests.get(_RESOURCE + 'notify', params=data) + response = requests.get( + '{}{}'.format(_RESOURCE, 'notify'), params=data, timeout=5) tree = ET.fromstring(response.content) if tree[0].tag == 'error': diff --git a/homeassistant/components/notify/sendgrid.py b/homeassistant/components/notify/sendgrid.py index c771240f80a..35e7d10cacb 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==3.6.0'] +REQUIREMENTS = ['sendgrid==3.6.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/persistent_notification.py b/homeassistant/components/persistent_notification.py index 5e91aef4d9f..b4dde02baff 100644 --- a/homeassistant/components/persistent_notification.py +++ b/homeassistant/components/persistent_notification.py @@ -55,8 +55,7 @@ def async_create(hass, message, title=None, notification_id=None): ] if value is not None } - hass.loop.create_task( - hass.services.async_call(DOMAIN, SERVICE_CREATE, data)) + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_CREATE, data)) @asyncio.coroutine diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 858ed2c1cf3..6e8869e2d63 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -27,7 +27,7 @@ import homeassistant.util.dt as dt_util DOMAIN = 'recorder' -REQUIREMENTS = ['sqlalchemy==1.1.2'] +REQUIREMENTS = ['sqlalchemy==1.1.3'] DEFAULT_URL = 'sqlite:///{hass_config_path}' DEFAULT_DB_FILE = 'home-assistant_v2.db' diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index d026002e408..f8a4f29738f 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -94,6 +94,7 @@ def valid_sensor(value): def _valid_light_switch(value): return _valid_device(value, "light_switch") + DEVICE_SCHEMA = vol.Schema({ vol.Required(ATTR_NAME): cv.string, vol.Optional(ATTR_FIREEVENT, default=False): cv.boolean, diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index bc66e562e0a..df46fb5a03d 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -7,6 +7,7 @@ by the user or automatically based upon automation events, etc. For more details about this component, please refer to the documentation at https://home-assistant.io/components/script/ """ +import asyncio import logging import voluptuous as vol @@ -40,7 +41,7 @@ _SCRIPT_ENTRY_SCHEMA = vol.Schema({ }) CONFIG_SCHEMA = vol.Schema({ - vol.Required(DOMAIN): {cv.slug: _SCRIPT_ENTRY_SCHEMA} + vol.Required(DOMAIN): vol.Schema({cv.slug: _SCRIPT_ENTRY_SCHEMA}) }, extra=vol.ALLOW_EXTRA) SCRIPT_SERVICE_SCHEMA = vol.Schema(dict) @@ -72,11 +73,13 @@ def toggle(hass, entity_id): hass.services.call(DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}) -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """Load the scripts from the configuration.""" component = EntityComponent(_LOGGER, DOMAIN, hass, group_name=GROUP_NAME_ALL_SCRIPTS) + @asyncio.coroutine def service_handler(service): """Execute a service call to script.