diff --git a/.coveragerc b/.coveragerc
index d0967918a60..4aae4cbc242 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -122,6 +122,7 @@ omit =
homeassistant/components/alarm_control_panel/simplisafe.py
homeassistant/components/binary_sensor/arest.py
homeassistant/components/binary_sensor/concord232.py
+ homeassistant/components/binary_sensor/flic.py
homeassistant/components/binary_sensor/rest.py
homeassistant/components/browser.py
homeassistant/components/camera/amcrest.py
@@ -183,6 +184,7 @@ omit =
homeassistant/components/light/x10.py
homeassistant/components/light/yeelight.py
homeassistant/components/lirc.py
+ homeassistant/components/media_player/aquostv.py
homeassistant/components/media_player/braviatv.py
homeassistant/components/media_player/cast.py
homeassistant/components/media_player/cmus.py
@@ -210,6 +212,7 @@ omit =
homeassistant/components/media_player/snapcast.py
homeassistant/components/media_player/sonos.py
homeassistant/components/media_player/squeezebox.py
+ homeassistant/components/media_player/vlc.py
homeassistant/components/media_player/yamaha.py
homeassistant/components/notify/aws_lambda.py
homeassistant/components/notify/aws_sns.py
@@ -280,6 +283,7 @@ omit =
homeassistant/components/sensor/mhz19.py
homeassistant/components/sensor/miflora.py
homeassistant/components/sensor/mqtt_room.py
+ homeassistant/components/sensor/netdata.py
homeassistant/components/sensor/neurio_energy.py
homeassistant/components/sensor/nut.py
homeassistant/components/sensor/nzbget.py
@@ -292,6 +296,7 @@ omit =
homeassistant/components/sensor/pvoutput.py
homeassistant/components/sensor/sabnzbd.py
homeassistant/components/sensor/scrape.py
+ homeassistant/components/sensor/sensehat.py
homeassistant/components/sensor/serial_pm.py
homeassistant/components/sensor/snmp.py
homeassistant/components/sensor/sonarr.py
@@ -313,10 +318,11 @@ omit =
homeassistant/components/sensor/waqi.py
homeassistant/components/sensor/xbox_live.py
homeassistant/components/sensor/yweather.py
- homeassistant/components/sensor/waqi.py
+ homeassistant/components/sensor/zamg.py
homeassistant/components/switch/acer_projector.py
homeassistant/components/switch/anel_pwrctrl.py
homeassistant/components/switch/arest.py
+ homeassistant/components/switch/digitalloggers.py
homeassistant/components/switch/dlink.py
homeassistant/components/switch/edimax.py
homeassistant/components/switch/hikvisioncam.py
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8621851ffb6..a63c1400723 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,13 +1,14 @@
# Contributing to Home Assistant
-Everybody is invited and welcome to contribute to Home Assistant. There is a lot to do...if you are not a developer perhaps you would like to help with the documentation on [home-assistant.io](https://home-assistant.io/)? If you are a developer and have devices in your home which aren't working with Home Assistant yet, why not spent a couple of hours and help to integrate them?
+Everybody is invited and welcome to contribute to Home Assistant. There is a lot to do...if you are not a developer perhaps you would like to help with the documentation on [home-assistant.io](https://home-assistant.io/)? If you are a developer and have devices in your home which aren't working with Home Assistant yet, why not spent a couple of hours and help to integrate them?
The process is straight-forward.
+ - Read [How to get faster PR reviews](https://github.com/kubernetes/kubernetes/blob/master/docs/devel/faster_reviews.md) by Kubernetes (but skip step 0)
- Fork the Home Assistant [git repository](https://github.com/home-assistant/home-assistant).
- Write the code for your device, notification service, sensor, or IoT thing.
- Ensure tests work.
- Create a Pull Request against the [**dev**](https://github.com/home-assistant/home-assistant/tree/dev) branch of Home Assistant.
-Still interested? Then you should take a peak at the [developer documentation](https://home-assistant.io/developers/) to get more details.
+Still interested? Then you should take a peak at the [developer documentation](https://home-assistant.io/developers/) to get more details.
diff --git a/Dockerfile b/Dockerfile
index b42d7edcc89..02e3db616ae 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,9 +8,12 @@ WORKDIR /usr/src/app
RUN pip3 install --no-cache-dir colorlog cython
-# For the nmap tracker, bluetooth tracker, Z-Wave
-RUN apt-get update && \
- apt-get install -y --no-install-recommends nmap net-tools cython3 libudev-dev sudo libglib2.0-dev bluetooth libbluetooth-dev && \
+# For the nmap tracker, bluetooth tracker, Z-Wave, tellstick
+RUN echo "deb http://download.telldus.com/debian/ stable main" >> /etc/apt/sources.list.d/telldus.list && \
+ wget -qO - http://download.telldus.se/debian/telldus-public.key | apt-key add - && \
+ apt-get update && \
+ apt-get install -y --no-install-recommends nmap net-tools cython3 libudev-dev sudo libglib2.0-dev bluetooth libbluetooth-dev \
+ libtelldus-core2 libtelldus-core-dev && \
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
COPY script/build_python_openzwave script/build_python_openzwave
diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py
index 49decfc62fe..1b64431c7a1 100644
--- a/homeassistant/components/alarm_control_panel/__init__.py
+++ b/homeassistant/components/alarm_control_panel/__init__.py
@@ -4,6 +4,7 @@ Component to interface with an alarm control panel.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel/
"""
+import asyncio
import logging
import os
@@ -42,40 +43,6 @@ ALARM_SERVICE_SCHEMA = vol.Schema({
})
-def setup(hass, config):
- """Track states and offer events for sensors."""
- component = EntityComponent(
- logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
-
- component.setup(config)
-
- def alarm_service_handler(service):
- """Map services to methods on Alarm."""
- target_alarms = component.extract_from_service(service)
-
- code = service.data.get(ATTR_CODE)
-
- method = SERVICE_TO_METHOD[service.service]
-
- for alarm in target_alarms:
- getattr(alarm, method)(code)
-
- for alarm in target_alarms:
- if not alarm.should_poll:
- continue
-
- alarm.update_ha_state(True)
-
- descriptions = load_yaml_config_file(
- os.path.join(os.path.dirname(__file__), 'services.yaml'))
-
- for service in SERVICE_TO_METHOD:
- hass.services.register(DOMAIN, service, alarm_service_handler,
- descriptions.get(service),
- schema=ALARM_SERVICE_SCHEMA)
- return True
-
-
def alarm_disarm(hass, code=None, entity_id=None):
"""Send the alarm the command for disarm."""
data = {}
@@ -120,6 +87,53 @@ def alarm_trigger(hass, code=None, entity_id=None):
hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data)
+@asyncio.coroutine
+def async_setup(hass, config):
+ """Track states and offer events for sensors."""
+ component = EntityComponent(
+ logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
+
+ yield from component.async_setup(config)
+
+ @asyncio.coroutine
+ def async_alarm_service_handler(service):
+ """Map services to methods on Alarm."""
+ target_alarms = component.async_extract_from_service(service)
+
+ code = service.data.get(ATTR_CODE)
+
+ method = "async_{}".format(SERVICE_TO_METHOD[service.service])
+
+ for alarm in target_alarms:
+ yield from getattr(alarm, method)(code)
+
+ update_tasks = []
+ for alarm in target_alarms:
+ if not alarm.should_poll:
+ continue
+
+ update_coro = hass.loop.create_task(
+ alarm.async_update_ha_state(True))
+ if hasattr(alarm, 'async_update'):
+ update_tasks.append(hass.loop.create_task(update_coro))
+ else:
+ yield from update_coro
+
+ if update_tasks:
+ yield from asyncio.wait(update_tasks, loop=hass.loop)
+
+ descriptions = yield from hass.loop.run_in_executor(
+ None, load_yaml_config_file, os.path.join(
+ os.path.dirname(__file__), 'services.yaml'))
+
+ for service in SERVICE_TO_METHOD:
+ hass.services.async_register(
+ DOMAIN, service, async_alarm_service_handler,
+ descriptions.get(service), schema=ALARM_SERVICE_SCHEMA)
+
+ return True
+
+
# pylint: disable=no-self-use
class AlarmControlPanel(Entity):
"""An abstract class for alarm control devices."""
@@ -138,18 +152,42 @@ class AlarmControlPanel(Entity):
"""Send disarm command."""
raise NotImplementedError()
+ @asyncio.coroutine
+ def async_alarm_disarm(self, code=None):
+ """Send disarm command."""
+ yield from self.hass.loop.run_in_executor(
+ None, self.alarm_disarm, code)
+
def alarm_arm_home(self, code=None):
"""Send arm home command."""
raise NotImplementedError()
+ @asyncio.coroutine
+ def async_alarm_arm_home(self, code=None):
+ """Send arm home command."""
+ yield from self.hass.loop.run_in_executor(
+ None, self.alarm_arm_home, code)
+
def alarm_arm_away(self, code=None):
"""Send arm away command."""
raise NotImplementedError()
+ @asyncio.coroutine
+ def async_alarm_arm_away(self, code=None):
+ """Send arm away command."""
+ yield from self.hass.loop.run_in_executor(
+ None, self.alarm_arm_away, code)
+
def alarm_trigger(self, code=None):
"""Send alarm trigger command."""
raise NotImplementedError()
+ @asyncio.coroutine
+ def async_alarm_trigger(self, code=None):
+ """Send alarm trigger command."""
+ yield from self.hass.loop.run_in_executor(
+ None, self.alarm_trigger, code)
+
@property
def state_attributes(self):
"""Return the state attributes."""
diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py
index cd37fc6a828..07f90cf4476 100644
--- a/homeassistant/components/alarm_control_panel/alarmdotcom.py
+++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py
@@ -56,11 +56,6 @@ class AlarmDotCom(alarm.AlarmControlPanel):
self._password = password
self._state = STATE_UNKNOWN
- @property
- def should_poll(self):
- """No polling needed."""
- return True
-
def update(self):
"""Fetch the latest state."""
self._state = self._alarm.state
diff --git a/homeassistant/components/alarm_control_panel/concord232.py b/homeassistant/components/alarm_control_panel/concord232.py
index 0bdcf274c08..de153a9e0a5 100755
--- a/homeassistant/components/alarm_control_panel/concord232.py
+++ b/homeassistant/components/alarm_control_panel/concord232.py
@@ -71,11 +71,6 @@ class Concord232Alarm(alarm.AlarmControlPanel):
self._alarm.last_partition_update = datetime.datetime.now()
self.update()
- @property
- def should_poll(self):
- """Polling needed."""
- return True
-
@property
def name(self):
"""Return the name of the device."""
@@ -126,7 +121,3 @@ class Concord232Alarm(alarm.AlarmControlPanel):
def alarm_arm_away(self, code=None):
"""Send arm away command."""
self._alarm.arm('auto')
-
- def alarm_trigger(self, code=None):
- """Alarm trigger command."""
- raise NotImplementedError()
diff --git a/homeassistant/components/alarm_control_panel/envisalink.py b/homeassistant/components/alarm_control_panel/envisalink.py
index e84320738a2..96b0fc83ea7 100644
--- a/homeassistant/components/alarm_control_panel/envisalink.py
+++ b/homeassistant/components/alarm_control_panel/envisalink.py
@@ -97,7 +97,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
def _update_callback(self, partition):
"""Update HA state, if needed."""
if partition is None or int(partition) == self._partition_number:
- self.hass.async_add_job(self.update_ha_state)
+ self.hass.async_add_job(self.async_update_ha_state())
@property
def code_format(self):
diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py
index 073d55508ed..cc67795d713 100644
--- a/homeassistant/components/alarm_control_panel/manual.py
+++ b/homeassistant/components/alarm_control_panel/manual.py
@@ -116,7 +116,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
self._state = STATE_ALARM_DISARMED
self._state_ts = dt_util.utcnow()
- self.update_ha_state()
+ self.schedule_update_ha_state()
def alarm_arm_home(self, code=None):
"""Send arm home command."""
@@ -125,7 +125,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
self._state = STATE_ALARM_ARMED_HOME
self._state_ts = dt_util.utcnow()
- self.update_ha_state()
+ self.schedule_update_ha_state()
if self._pending_time:
track_point_in_time(
@@ -139,7 +139,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
self._state = STATE_ALARM_ARMED_AWAY
self._state_ts = dt_util.utcnow()
- self.update_ha_state()
+ self.schedule_update_ha_state()
if self._pending_time:
track_point_in_time(
@@ -151,7 +151,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
self._pre_trigger_state = self._state
self._state = STATE_ALARM_TRIGGERED
self._state_ts = dt_util.utcnow()
- self.update_ha_state()
+ self.schedule_update_ha_state()
if self._trigger_time:
track_point_in_time(
diff --git a/homeassistant/components/alarm_control_panel/nx584.py b/homeassistant/components/alarm_control_panel/nx584.py
index cb32fc924e6..58ec8d915ab 100644
--- a/homeassistant/components/alarm_control_panel/nx584.py
+++ b/homeassistant/components/alarm_control_panel/nx584.py
@@ -62,11 +62,6 @@ class NX584Alarm(alarm.AlarmControlPanel):
self._alarm.list_zones()
self._state = STATE_UNKNOWN
- @property
- def should_poll(self):
- """Polling needed."""
- return True
-
@property
def name(self):
"""Return the name of the device."""
@@ -122,7 +117,3 @@ class NX584Alarm(alarm.AlarmControlPanel):
def alarm_arm_away(self, code=None):
"""Send arm away command."""
self._alarm.arm('exit')
-
- def alarm_trigger(self, code=None):
- """Alarm trigger command."""
- raise NotImplementedError()
diff --git a/homeassistant/components/alarm_control_panel/simplisafe.py b/homeassistant/components/alarm_control_panel/simplisafe.py
index 40ebfb2f39f..7a8f8409c59 100644
--- a/homeassistant/components/alarm_control_panel/simplisafe.py
+++ b/homeassistant/components/alarm_control_panel/simplisafe.py
@@ -61,11 +61,6 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
else:
self._state = STATE_UNKNOWN
- @property
- def should_poll(self):
- """Poll the SimpliSafe API."""
- return True
-
@property
def name(self):
"""Return the name of the device."""
@@ -104,7 +99,6 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
return
self.simplisafe.set_state('off')
_LOGGER.info('SimpliSafe alarm disarming')
- self.update()
def alarm_arm_home(self, code=None):
"""Send arm home command."""
@@ -112,7 +106,6 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
return
self.simplisafe.set_state('home')
_LOGGER.info('SimpliSafe alarm arming home')
- self.update()
def alarm_arm_away(self, code=None):
"""Send arm away command."""
@@ -120,7 +113,6 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
return
self.simplisafe.set_state('away')
_LOGGER.info('SimpliSafe alarm arming away')
- self.update()
def _validate_code(self, code, state):
"""Validate given code."""
diff --git a/homeassistant/components/alarm_control_panel/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py
index 4ef07c68f59..c1a394fe462 100644
--- a/homeassistant/components/alarm_control_panel/verisure.py
+++ b/homeassistant/components/alarm_control_panel/verisure.py
@@ -84,18 +84,15 @@ class VerisureAlarm(alarm.AlarmControlPanel):
hub.my_pages.alarm.set(code, 'DISARMED')
_LOGGER.info('verisure alarm disarming')
hub.my_pages.alarm.wait_while_pending()
- self.update()
def alarm_arm_home(self, code=None):
"""Send arm home command."""
hub.my_pages.alarm.set(code, 'ARMED_HOME')
_LOGGER.info('verisure alarm arming home')
hub.my_pages.alarm.wait_while_pending()
- self.update()
def alarm_arm_away(self, code=None):
"""Send arm away command."""
hub.my_pages.alarm.set(code, 'ARMED_AWAY')
_LOGGER.info('verisure alarm arming away')
hub.my_pages.alarm.wait_while_pending()
- self.update()
diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py
index 39deae3d66e..4818c02d9ff 100644
--- a/homeassistant/components/automation/mqtt.py
+++ b/homeassistant/components/automation/mqtt.py
@@ -4,6 +4,8 @@ Offer MQTT listening automation rules.
For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#mqtt-trigger
"""
+import json
+
import voluptuous as vol
from homeassistant.core import callback
@@ -31,13 +33,20 @@ def async_trigger(hass, config, action):
def mqtt_automation_listener(msg_topic, msg_payload, qos):
"""Listen for MQTT messages."""
if payload is None or payload == msg_payload:
+ data = {
+ 'platform': 'mqtt',
+ 'topic': msg_topic,
+ 'payload': msg_payload,
+ 'qos': qos,
+ }
+
+ try:
+ data['payload_json'] = json.loads(msg_payload)
+ except ValueError:
+ pass
+
hass.async_run_job(action, {
- 'trigger': {
- 'platform': 'mqtt',
- 'topic': msg_topic,
- 'payload': msg_payload,
- 'qos': qos,
- }
+ 'trigger': data
})
return mqtt.async_subscribe(hass, topic, mqtt_automation_listener)
diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py
index fb146991602..65cca462ed9 100644
--- a/homeassistant/components/automation/state.py
+++ b/homeassistant/components/automation/state.py
@@ -64,10 +64,19 @@ def async_trigger(hass, config, action):
call_action()
return
+ @callback
+ def clear_listener():
+ """Clear all unsub listener."""
+ nonlocal async_remove_state_for_cancel
+ nonlocal async_remove_state_for_listener
+ async_remove_state_for_listener = None
+ async_remove_state_for_cancel = None
+
@callback
def state_for_listener(now):
"""Fire on state changes after a delay and calls action."""
async_remove_state_for_cancel()
+ clear_listener()
call_action()
@callback
@@ -77,6 +86,7 @@ def async_trigger(hass, config, action):
return
async_remove_state_for_listener()
async_remove_state_for_cancel()
+ clear_listener()
async_remove_state_for_listener = async_track_point_in_utc_time(
hass, state_for_listener, dt_util.utcnow() + time_delta)
diff --git a/homeassistant/components/binary_sensor/flic.py b/homeassistant/components/binary_sensor/flic.py
new file mode 100644
index 00000000000..980af069f38
--- /dev/null
+++ b/homeassistant/components/binary_sensor/flic.py
@@ -0,0 +1,257 @@
+"""Contains functionality to use flic buttons as a binary sensor."""
+import asyncio
+import logging
+
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.const import (
+ CONF_HOST, CONF_PORT, CONF_DISCOVERY, CONF_TIMEOUT,
+ EVENT_HOMEASSISTANT_STOP)
+from homeassistant.components.binary_sensor import (
+ BinarySensorDevice, PLATFORM_SCHEMA)
+from homeassistant.util.async import run_callback_threadsafe
+
+
+REQUIREMENTS = ['https://github.com/soldag/pyflic/archive/0.4.zip#pyflic==0.4']
+
+_LOGGER = logging.getLogger(__name__)
+
+DEFAULT_TIMEOUT = 3
+
+CLICK_TYPE_SINGLE = "single"
+CLICK_TYPE_DOUBLE = "double"
+CLICK_TYPE_HOLD = "hold"
+CLICK_TYPES = [CLICK_TYPE_SINGLE, CLICK_TYPE_DOUBLE, CLICK_TYPE_HOLD]
+
+CONF_IGNORED_CLICK_TYPES = "ignored_click_types"
+
+EVENT_NAME = "flic_click"
+EVENT_DATA_NAME = "button_name"
+EVENT_DATA_ADDRESS = "button_address"
+EVENT_DATA_TYPE = "click_type"
+EVENT_DATA_QUEUED_TIME = "queued_time"
+
+# Validation of the user's configuration
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Optional(CONF_HOST, default='localhost'): cv.string,
+ vol.Optional(CONF_PORT, default=5551): cv.port,
+ vol.Optional(CONF_DISCOVERY, default=True): cv.boolean,
+ vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
+ vol.Optional(CONF_IGNORED_CLICK_TYPES): vol.All(cv.ensure_list,
+ [vol.In(CLICK_TYPES)])
+})
+
+
+@asyncio.coroutine
+def async_setup_platform(hass, config, async_add_entities,
+ discovery_info=None):
+ """Setup the flic platform."""
+ import pyflic
+
+ # Initialize flic client responsible for
+ # connecting to buttons and retrieving events
+ host = config.get(CONF_HOST)
+ port = config.get(CONF_PORT)
+ discovery = config.get(CONF_DISCOVERY)
+
+ try:
+ client = pyflic.FlicClient(host, port)
+ except ConnectionRefusedError:
+ _LOGGER.error("Failed to connect to flic server.")
+ return
+
+ def new_button_callback(address):
+ """Setup newly verified button as device in home assistant."""
+ hass.add_job(async_setup_button(hass, config, async_add_entities,
+ client, address))
+
+ client.on_new_verified_button = new_button_callback
+ if discovery:
+ start_scanning(hass, config, async_add_entities, client)
+
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
+ lambda event: client.close())
+ hass.loop.run_in_executor(None, client.handle_events)
+
+ # Get addresses of already verified buttons
+ addresses = yield from async_get_verified_addresses(client)
+ if addresses:
+ for address in addresses:
+ yield from async_setup_button(hass, config, async_add_entities,
+ client, address)
+
+
+def start_scanning(hass, config, async_add_entities, client):
+ """Start a new flic client for scanning & connceting to new buttons."""
+ import pyflic
+
+ scan_wizard = pyflic.ScanWizard()
+
+ def scan_completed_callback(scan_wizard, result, address, name):
+ """Restart scan wizard to constantly check for new buttons."""
+ if result == pyflic.ScanWizardResult.WizardSuccess:
+ _LOGGER.info("Found new button (%s)", address)
+ elif result != pyflic.ScanWizardResult.WizardFailedTimeout:
+ _LOGGER.warning("Failed to connect to button (%s). Reason: %s",
+ address, result)
+
+ # Restart scan wizard
+ start_scanning(hass, config, async_add_entities, client)
+
+ scan_wizard.on_completed = scan_completed_callback
+ client.add_scan_wizard(scan_wizard)
+
+
+@asyncio.coroutine
+def async_setup_button(hass, config, async_add_entities, client, address):
+ """Setup single button device."""
+ timeout = config.get(CONF_TIMEOUT)
+ ignored_click_types = config.get(CONF_IGNORED_CLICK_TYPES)
+ button = FlicButton(hass, client, address, timeout, ignored_click_types)
+ _LOGGER.info("Connected to button (%s)", address)
+
+ yield from async_add_entities([button])
+
+
+@asyncio.coroutine
+def async_get_verified_addresses(client):
+ """Retrieve addresses of verified buttons."""
+ future = asyncio.Future()
+ loop = asyncio.get_event_loop()
+
+ def get_info_callback(items):
+ """Set the addressed of connected buttons as result of the future."""
+ addresses = items["bd_addr_of_verified_buttons"]
+ run_callback_threadsafe(loop, future.set_result, addresses)
+ client.get_info(get_info_callback)
+
+ return future
+
+
+class FlicButton(BinarySensorDevice):
+ """Representation of a flic button."""
+
+ def __init__(self, hass, client, address, timeout, ignored_click_types):
+ """Initialize the flic button."""
+ import pyflic
+
+ self._hass = hass
+ self._address = address
+ self._timeout = timeout
+ self._is_down = False
+ self._ignored_click_types = ignored_click_types or []
+ self._hass_click_types = {
+ pyflic.ClickType.ButtonClick: CLICK_TYPE_SINGLE,
+ pyflic.ClickType.ButtonSingleClick: CLICK_TYPE_SINGLE,
+ pyflic.ClickType.ButtonDoubleClick: CLICK_TYPE_DOUBLE,
+ pyflic.ClickType.ButtonHold: CLICK_TYPE_HOLD,
+ }
+
+ self._channel = self._create_channel()
+ client.add_connection_channel(self._channel)
+
+ def _create_channel(self):
+ """Create a new connection channel to the button."""
+ import pyflic
+
+ channel = pyflic.ButtonConnectionChannel(self._address)
+ channel.on_button_up_or_down = self._on_up_down
+
+ # If all types of clicks should be ignored, skip registering callbacks
+ if set(self._ignored_click_types) == set(CLICK_TYPES):
+ return channel
+
+ if CLICK_TYPE_DOUBLE in self._ignored_click_types:
+ # Listen to all but double click type events
+ channel.on_button_click_or_hold = self._on_click
+ elif CLICK_TYPE_HOLD in self._ignored_click_types:
+ # Listen to all but hold click type events
+ channel.on_button_single_or_double_click = self._on_click
+ else:
+ # Listen to all click type events
+ channel.on_button_single_or_double_click_or_hold = self._on_click
+
+ return channel
+
+ @property
+ def name(self):
+ """Return the name of the device."""
+ return "flic_%s" % self.address.replace(":", "")
+
+ @property
+ def address(self):
+ """Return the bluetooth address of the device."""
+ return self._address
+
+ @property
+ def is_on(self):
+ """Return true if sensor is on."""
+ return self._is_down
+
+ @property
+ def should_poll(self):
+ """No polling needed."""
+ return False
+
+ @property
+ def state_attributes(self):
+ """Return device specific state attributes."""
+ attr = super(FlicButton, self).state_attributes
+ attr["address"] = self.address
+
+ return attr
+
+ def _queued_event_check(self, click_type, time_diff):
+ """Generate a log message and returns true if timeout exceeded."""
+ time_string = "{:d} {}".format(
+ time_diff, "second" if time_diff == 1 else "seconds")
+
+ if time_diff > self._timeout:
+ _LOGGER.warning(
+ "Queued %s dropped for %s. Time in queue was %s.",
+ click_type, self.address, time_string)
+ return True
+ else:
+ _LOGGER.info(
+ "Queued %s allowed for %s. Time in queue was %s.",
+ click_type, self.address, time_string)
+ return False
+
+ def _on_up_down(self, channel, click_type, was_queued, time_diff):
+ """Update device state, if event was not queued."""
+ import pyflic
+
+ if was_queued and self._queued_event_check(click_type, time_diff):
+ return
+
+ self._is_down = click_type == pyflic.ClickType.ButtonDown
+ self.schedule_update_ha_state()
+
+ def _on_click(self, channel, click_type, was_queued, time_diff):
+ """Fire click event, if event was not queued."""
+ # Return if click event was queued beyond allowed timeout
+ if was_queued and self._queued_event_check(click_type, time_diff):
+ return
+
+ # Return if click event is in ignored click types
+ hass_click_type = self._hass_click_types[click_type]
+ if hass_click_type in self._ignored_click_types:
+ return
+
+ self._hass.bus.fire(EVENT_NAME, {
+ EVENT_DATA_NAME: self.name,
+ EVENT_DATA_ADDRESS: self.address,
+ EVENT_DATA_QUEUED_TIME: time_diff,
+ EVENT_DATA_TYPE: hass_click_type
+ })
+
+ def _connection_status_changed(self, channel,
+ connection_status, disconnect_reason):
+ """Remove device, if button disconnects."""
+ import pyflic
+
+ if connection_status == pyflic.ConnectionStatus.Disconnected:
+ _LOGGER.info("Button (%s) disconnected. Reason: %s",
+ self.address, disconnect_reason)
+ self.remove()
diff --git a/homeassistant/components/binary_sensor/netatmo.py b/homeassistant/components/binary_sensor/netatmo.py
index 93b3bb5817c..94ef0faaad0 100644
--- a/homeassistant/components/binary_sensor/netatmo.py
+++ b/homeassistant/components/binary_sensor/netatmo.py
@@ -23,9 +23,11 @@ _LOGGER = logging.getLogger(__name__)
# These are the available sensors mapped to binary_sensor class
SENSOR_TYPES = {
- "Someone known": "motion",
- "Someone unknown": "motion",
- "Motion": "motion",
+ "Someone known": 'occupancy',
+ "Someone unknown": 'motion',
+ "Motion": 'motion',
+ "Tag Vibration": 'vibration',
+ "Tag Open": 'opening',
}
CONF_HOME = 'home'
@@ -48,6 +50,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
home = config.get(CONF_HOME, None)
timeout = config.get(CONF_TIMEOUT, 15)
+ module_name = None
+
import lnetatmo
try:
data = WelcomeData(netatmo.NETATMO_AUTH, home)
@@ -64,23 +68,35 @@ 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, timeout,
- variable)])
+ if variable in ('Tag Vibration', 'Tag Open'):
+ continue
+ add_devices([WelcomeBinarySensor(data, camera_name, module_name,
+ home, timeout, variable)])
+
+ for module_name in data.get_module_names(camera_name):
+ for variable in sensors:
+ if variable in ('Tag Vibration', 'Tag Open'):
+ add_devices([WelcomeBinarySensor(data, camera_name,
+ module_name, home,
+ timeout, variable)])
class WelcomeBinarySensor(BinarySensorDevice):
"""Represent a single binary sensor in a Netatmo Welcome device."""
- def __init__(self, data, camera_name, home, timeout, sensor):
+ def __init__(self, data, camera_name, module_name, home, timeout, sensor):
"""Setup for access to the Netatmo camera events."""
self._data = data
self._camera_name = camera_name
+ self._module_name = module_name
self._home = home
self._timeout = timeout
if home:
self._name = home + ' / ' + camera_name
else:
self._name = camera_name
+ if module_name:
+ self._name += ' / ' + module_name
self._sensor_name = sensor
self._name += ' ' + sensor
camera_id = data.welcomedata.cameraByName(camera=camera_name,
@@ -112,7 +128,7 @@ class WelcomeBinarySensor(BinarySensorDevice):
def update(self):
"""Request an update from the Netatmo API."""
self._data.update()
- self._data.welcomedata.updateEvent(home=self._data.home)
+ self._data.update_event()
if self._sensor_name == "Someone known":
self._state =\
@@ -129,5 +145,16 @@ class WelcomeBinarySensor(BinarySensorDevice):
self._data.welcomedata.motionDetected(self._home,
self._camera_name,
self._timeout*60)
+ elif self._sensor_name == "Tag Vibration":
+ self._state =\
+ self._data.welcomedata.moduleMotionDetected(self._home,
+ self._module_name,
+ self._camera_name,
+ self._timeout*60)
+ elif self._sensor_name == "Tag Open":
+ self._state =\
+ self._data.welcomedata.moduleOpened(self._home,
+ self._module_name,
+ self._camera_name)
else:
return None
diff --git a/homeassistant/components/binary_sensor/wink.py b/homeassistant/components/binary_sensor/wink.py
index 2d0e3f7226f..b129b5f24d4 100644
--- a/homeassistant/components/binary_sensor/wink.py
+++ b/homeassistant/components/binary_sensor/wink.py
@@ -40,6 +40,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for sensor in pywink.get_smoke_and_co_detectors():
add_devices([WinkBinarySensorDevice(sensor, hass)])
+ for hub in pywink.get_hubs():
+ add_devices([WinkHub(hub, hass)])
+
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
"""Representation of a Wink binary sensor."""
@@ -79,3 +82,24 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
def sensor_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES."""
return SENSOR_TYPES.get(self.capability)
+
+
+class WinkHub(WinkDevice, BinarySensorDevice, Entity):
+ """Representation of a Wink Hub."""
+
+ def __init(self, wink, hass):
+ """Initialize the hub sensor."""
+ WinkDevice.__init__(self, wink, hass)
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ return {
+ 'update needed': self.wink.update_needed(),
+ 'firmware version': self.wink.firmware_version()
+ }
+
+ @property
+ def is_on(self):
+ """Return true if the binary sensor is on."""
+ return self.wink.state()
diff --git a/homeassistant/components/camera/amcrest.py b/homeassistant/components/camera/amcrest.py
index 97ed45f21e4..c6568677583 100644
--- a/homeassistant/components/camera/amcrest.py
+++ b/homeassistant/components/camera/amcrest.py
@@ -18,16 +18,26 @@ REQUIREMENTS = ['amcrest==1.0.0']
_LOGGER = logging.getLogger(__name__)
-DEFAULT_PORT = 80
+CONF_RESOLUTION = 'resolution'
+
DEFAULT_NAME = 'Amcrest Camera'
+DEFAULT_PORT = 80
+DEFAULT_RESOLUTION = 'high'
NOTIFICATION_ID = 'amcrest_notification'
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
+RESOLUTION_LIST = {
+ 'high': 0,
+ 'low': 1,
+}
+
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
+ vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
+ vol.All(vol.In(RESOLUTION_LIST)),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
})
@@ -64,13 +74,14 @@ class AmcrestCam(Camera):
def __init__(self, device_info, data):
"""Initialize an Amcrest camera."""
super(AmcrestCam, self).__init__()
- self._name = device_info.get(CONF_NAME)
self._data = data
+ self._name = device_info.get(CONF_NAME)
+ self._resolution = RESOLUTION_LIST[device_info.get(CONF_RESOLUTION)]
def camera_image(self):
"""Return a still image reponse from the camera."""
# Send the request to snap a picture and return raw jpg data
- response = self._data.camera.snapshot()
+ response = self._data.camera.snapshot(channel=self._resolution)
return response.data
@property
diff --git a/homeassistant/components/climate/ecobee.py b/homeassistant/components/climate/ecobee.py
index c98ac6d0106..84d8c32f9ff 100644
--- a/homeassistant/components/climate/ecobee.py
+++ b/homeassistant/components/climate/ecobee.py
@@ -195,8 +195,9 @@ class Thermostat(ClimateDevice):
mode = self.mode
events = self.thermostat['events']
for event in events:
- if event['running']:
- mode = event['holdClimateRef']
+ if event['holdClimateRef'] == 'away' or \
+ event['type'] == 'autoAway':
+ mode = "away"
break
return 'away' in mode
diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py
index 1b3d20d8b59..a40795c37c5 100644
--- a/homeassistant/components/climate/generic_thermostat.py
+++ b/homeassistant/components/climate/generic_thermostat.py
@@ -198,24 +198,30 @@ class GenericThermostat(ClimateDevice):
return
if self.ac_mode:
- too_hot = self._cur_temp - self._target_temp > self._tolerance
is_cooling = self._is_device_active
- if too_hot and not is_cooling:
- _LOGGER.info('Turning on AC %s', self.heater_entity_id)
- switch.turn_on(self.hass, self.heater_entity_id)
- elif not too_hot and is_cooling:
- _LOGGER.info('Turning off AC %s', self.heater_entity_id)
- switch.turn_off(self.hass, self.heater_entity_id)
+ if is_cooling:
+ too_cold = self._target_temp - self._cur_temp > self._tolerance
+ if too_cold:
+ _LOGGER.info('Turning off AC %s', self.heater_entity_id)
+ switch.turn_off(self.hass, self.heater_entity_id)
+ else:
+ too_hot = self._cur_temp - self._target_temp > self._tolerance
+ if too_hot:
+ _LOGGER.info('Turning on AC %s', self.heater_entity_id)
+ switch.turn_on(self.hass, self.heater_entity_id)
else:
- too_cold = self._target_temp - self._cur_temp > self._tolerance
is_heating = self._is_device_active
-
- if too_cold and not is_heating:
- _LOGGER.info('Turning on heater %s', self.heater_entity_id)
- switch.turn_on(self.hass, self.heater_entity_id)
- elif not too_cold and is_heating:
- _LOGGER.info('Turning off heater %s', self.heater_entity_id)
- switch.turn_off(self.hass, self.heater_entity_id)
+ if is_heating:
+ too_hot = self._cur_temp - self._target_temp > self._tolerance
+ if too_hot:
+ _LOGGER.info('Turning off heater %s',
+ self.heater_entity_id)
+ switch.turn_off(self.hass, self.heater_entity_id)
+ else:
+ too_cold = self._target_temp - self._cur_temp > self._tolerance
+ if too_cold:
+ _LOGGER.info('Turning on heater %s', self.heater_entity_id)
+ switch.turn_on(self.hass, self.heater_entity_id)
@property
def _is_device_active(self):
diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py
index ab41c10e6d1..e098c3c3709 100644
--- a/homeassistant/components/climate/nest.py
+++ b/homeassistant/components/climate/nest.py
@@ -155,8 +155,8 @@ class NestThermostat(ClimateDevice):
"""Set new target temperature."""
target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW)
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
- if target_temp_low is not None and target_temp_high is not None:
- if self._mode == STATE_HEAT_COOL:
+ if self._mode == STATE_HEAT_COOL:
+ if target_temp_low is not None and target_temp_high is not None:
temp = (target_temp_low, target_temp_high)
else:
temp = kwargs.get(ATTR_TEMPERATURE)
diff --git a/homeassistant/components/climate/radiotherm.py b/homeassistant/components/climate/radiotherm.py
index d06e148cfdd..9a0e5666036 100644
--- a/homeassistant/components/climate/radiotherm.py
+++ b/homeassistant/components/climate/radiotherm.py
@@ -23,10 +23,19 @@ ATTR_FAN = 'fan'
ATTR_MODE = 'mode'
CONF_HOLD_TEMP = 'hold_temp'
+CONF_AWAY_TEMPERATURE_HEAT = 'away_temperature_heat'
+CONF_AWAY_TEMPERATURE_COOL = 'away_temperature_cool'
+
+DEFAULT_AWAY_TEMPERATURE_HEAT = 60
+DEFAULT_AWAY_TEMPERATURE_COOL = 85
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_HOLD_TEMP, default=False): cv.boolean,
+ vol.Optional(CONF_AWAY_TEMPERATURE_HEAT,
+ default=DEFAULT_AWAY_TEMPERATURE_HEAT): vol.Coerce(float),
+ vol.Optional(CONF_AWAY_TEMPERATURE_COOL,
+ default=DEFAULT_AWAY_TEMPERATURE_COOL): vol.Coerce(float),
})
@@ -45,12 +54,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return False
hold_temp = config.get(CONF_HOLD_TEMP)
+ away_temps = [
+ config.get(CONF_AWAY_TEMPERATURE_HEAT),
+ config.get(CONF_AWAY_TEMPERATURE_COOL)
+ ]
tstats = []
for host in hosts:
try:
tstat = radiotherm.get_thermostat(host)
- tstats.append(RadioThermostat(tstat, hold_temp))
+ tstats.append(RadioThermostat(tstat, hold_temp, away_temps))
except OSError:
_LOGGER.exception("Unable to connect to Radio Thermostat: %s",
host)
@@ -61,7 +74,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class RadioThermostat(ClimateDevice):
"""Representation of a Radio Thermostat."""
- def __init__(self, device, hold_temp):
+ def __init__(self, device, hold_temp, away_temps):
"""Initialize the thermostat."""
self.device = device
self.set_time()
@@ -71,7 +84,10 @@ class RadioThermostat(ClimateDevice):
self._name = None
self._fmode = None
self._tmode = None
- self.hold_temp = hold_temp
+ self._hold_temp = hold_temp
+ self._away = False
+ self._away_temps = away_temps
+ self._prev_temp = None
self.update()
self._operation_list = [STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_OFF]
@@ -113,6 +129,11 @@ class RadioThermostat(ClimateDevice):
"""Return the temperature we try to reach."""
return self._target_temperature
+ @property
+ def is_away_mode_on(self):
+ """Return true if away mode is on."""
+ return self._away
+
def update(self):
"""Update the data from the thermostat."""
self._current_temperature = self.device.temp['raw']
@@ -138,7 +159,7 @@ class RadioThermostat(ClimateDevice):
self.device.t_cool = round(temperature * 2.0) / 2.0
elif self._current_operation == STATE_HEAT:
self.device.t_heat = round(temperature * 2.0) / 2.0
- if self.hold_temp:
+ if self._hold_temp or self._away:
self.device.hold = 1
else:
self.device.hold = 0
@@ -162,3 +183,23 @@ class RadioThermostat(ClimateDevice):
self.device.t_cool = round(self._target_temperature * 2.0) / 2.0
elif operation_mode == STATE_HEAT:
self.device.t_heat = round(self._target_temperature * 2.0) / 2.0
+
+ def turn_away_mode_on(self):
+ """Turn away on.
+
+ The RTCOA app simulates away mode by using a hold.
+ """
+ away_temp = None
+ if not self._away:
+ self._prev_temp = self._target_temperature
+ if self._current_operation == STATE_HEAT:
+ away_temp = self._away_temps[0]
+ elif self._current_operation == STATE_COOL:
+ away_temp = self._away_temps[1]
+ self._away = True
+ self.set_temperature(temperature=away_temp)
+
+ def turn_away_mode_off(self):
+ """Turn away off."""
+ self._away = False
+ self.set_temperature(temperature=self._prev_temp)
diff --git a/homeassistant/components/cover/tellduslive.py b/homeassistant/components/cover/tellduslive.py
new file mode 100644
index 00000000000..c48a14e9133
--- /dev/null
+++ b/homeassistant/components/cover/tellduslive.py
@@ -0,0 +1,46 @@
+"""
+Support for Tellstick covers using Tellstick Net.
+
+This platform uses the Telldus Live online service.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/cover.tellduslive/
+
+"""
+import logging
+
+from homeassistant.components.cover import CoverDevice
+from homeassistant.components.tellduslive import TelldusLiveEntity
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Setup covers."""
+ if discovery_info is None:
+ return
+ add_devices(TelldusLiveCover(hass, cover) for cover in discovery_info)
+
+
+class TelldusLiveCover(TelldusLiveEntity, CoverDevice):
+ """Representation of a cover."""
+
+ @property
+ def is_closed(self):
+ """Return the current position of the cover."""
+ return self.device.is_down
+
+ def close_cover(self, **kwargs):
+ """Close the cover."""
+ self.device.down()
+ self.changed()
+
+ def open_cover(self, **kwargs):
+ """Open the cover."""
+ self.device.up()
+ self.changed()
+
+ def stop_cover(self, **kwargs):
+ """Stop the cover."""
+ self.device.stop()
+ self.changed()
diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py
index 91f0720e927..d497ea4c314 100644
--- a/homeassistant/components/device_tracker/__init__.py
+++ b/homeassistant/components/device_tracker/__init__.py
@@ -282,7 +282,7 @@ class DeviceTracker(object):
list(self.group.tracking) + [device.entity_id])
# lookup mac vendor string to be stored in config
- device.set_vendor_for_mac()
+ yield from device.set_vendor_for_mac()
# update known_devices.yaml
self.hass.async_add_job(
@@ -370,6 +370,7 @@ class Device(Entity):
self.away_hide = hide_if_away
self.vendor = vendor
+ self._attributes = {}
@property
def name(self):
@@ -399,12 +400,13 @@ class Device(Entity):
if self.battery:
attr[ATTR_BATTERY] = self.battery
- if self.attributes:
- for key, value in self.attributes.items():
- attr[key] = value
-
return attr
+ @property
+ def device_state_attributes(self):
+ """Return device state attributes."""
+ return self._attributes
+
@property
def hidden(self):
"""If device should be hidden."""
@@ -419,8 +421,11 @@ class Device(Entity):
self.host_name = host_name
self.location_name = location_name
self.gps_accuracy = gps_accuracy or 0
- self.battery = battery
- self.attributes = attributes
+ if battery:
+ self.battery = battery
+ if attributes:
+ self._attributes.update(attributes)
+
self.gps = None
if gps is not None:
diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py
index 2eced1b4dd4..4e860846f8e 100644
--- a/homeassistant/components/device_tracker/asuswrt.py
+++ b/homeassistant/components/device_tracker/asuswrt.py
@@ -286,8 +286,10 @@ class AsusWrtDeviceScanner(object):
# match mac addresses to IP addresses in ARP table
for arp in result.arp:
- if match.group('mac').lower() in arp.decode('utf-8'):
- arp_match = _ARP_REGEX.search(arp.decode('utf-8'))
+ if match.group('mac').lower() in \
+ arp.decode('utf-8').lower():
+ arp_match = _ARP_REGEX.search(
+ arp.decode('utf-8').lower())
if not arp_match:
_LOGGER.warning('Could not parse arp row: %s', arp)
continue
diff --git a/homeassistant/components/device_tracker/gpslogger.py b/homeassistant/components/device_tracker/gpslogger.py
index 2e897ccb10c..22099630bd1 100644
--- a/homeassistant/components/device_tracker/gpslogger.py
+++ b/homeassistant/components/device_tracker/gpslogger.py
@@ -64,9 +64,22 @@ class GPSLoggerView(HomeAssistantView):
if 'battery' in data:
battery = float(data['battery'])
+ attrs = {}
+ if 'speed' in data:
+ attrs['speed'] = float(data['speed'])
+ if 'direction' in data:
+ attrs['direction'] = float(data['direction'])
+ if 'altitude' in data:
+ attrs['altitude'] = float(data['altitude'])
+ if 'provider' in data:
+ attrs['provider'] = data['provider']
+ if 'activity' in data:
+ attrs['activity'] = data['activity']
+
yield from hass.loop.run_in_executor(
None, partial(self.see, dev_id=device,
gps=gps_location, battery=battery,
- gps_accuracy=accuracy))
+ gps_accuracy=accuracy,
+ attributes=attrs))
return 'Setting location for {}'.format(device)
diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py
index 2e8bbc5d2a1..404492e35cf 100644
--- a/homeassistant/components/device_tracker/nmap_tracker.py
+++ b/homeassistant/components/device_tracker/nmap_tracker.py
@@ -25,6 +25,8 @@ _LOGGER = logging.getLogger(__name__)
CONF_EXCLUDE = 'exclude'
# Interval in minutes to exclude devices from a scan while they are home
CONF_HOME_INTERVAL = 'home_interval'
+CONF_OPTIONS = 'scan_options'
+DEFAULT_OPTIONS = '-F --host-timeout 5s'
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
@@ -33,7 +35,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOSTS): cv.ensure_list,
vol.Required(CONF_HOME_INTERVAL, default=0): cv.positive_int,
vol.Optional(CONF_EXCLUDE, default=[]):
- vol.All(cv.ensure_list, vol.Length(min=1))
+ vol.All(cv.ensure_list, vol.Length(min=1)),
+ vol.Optional(CONF_OPTIONS, default=DEFAULT_OPTIONS):
+ cv.string
})
@@ -69,8 +73,9 @@ class NmapDeviceScanner(object):
self.last_results = []
self.hosts = config[CONF_HOSTS]
- self.exclude = config.get(CONF_EXCLUDE, [])
+ self.exclude = config[CONF_EXCLUDE]
minutes = config[CONF_HOME_INTERVAL]
+ self._options = config[CONF_OPTIONS]
self.home_interval = timedelta(minutes=minutes)
self.success_init = self._update_info()
@@ -103,7 +108,7 @@ class NmapDeviceScanner(object):
from nmap import PortScanner, PortScannerError
scanner = PortScanner()
- options = '-F --host-timeout 5s '
+ options = self._options
if self.home_interval:
boundary = dt_util.now() - self.home_interval
diff --git a/homeassistant/components/device_tracker/unifi.py b/homeassistant/components/device_tracker/unifi.py
index d654c3e3eef..ab84eb22e04 100644
--- a/homeassistant/components/device_tracker/unifi.py
+++ b/homeassistant/components/device_tracker/unifi.py
@@ -9,16 +9,20 @@ import urllib
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
+import homeassistant.loader as loader
from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
# Unifi package doesn't list urllib3 as a requirement
-REQUIREMENTS = ['urllib3', 'unifi==1.2.5']
+REQUIREMENTS = ['urllib3', 'pyunifi==1.3']
_LOGGER = logging.getLogger(__name__)
CONF_PORT = 'port'
CONF_SITE_ID = 'site_id'
+NOTIFICATION_ID = 'unifi_notification'
+NOTIFICATION_TITLE = 'Unifi Device Tracker Setup'
+
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default='localhost'): cv.string,
vol.Optional(CONF_SITE_ID, default='default'): cv.string,
@@ -30,7 +34,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def get_scanner(hass, config):
"""Setup Unifi device_tracker."""
- from unifi.controller import Controller
+ from pyunifi.controller import Controller
host = config[DOMAIN].get(CONF_HOST)
username = config[DOMAIN].get(CONF_USERNAME)
@@ -38,10 +42,18 @@ def get_scanner(hass, config):
site_id = config[DOMAIN].get(CONF_SITE_ID)
port = config[DOMAIN].get(CONF_PORT)
+ persistent_notification = loader.get_component('persistent_notification')
try:
ctrl = Controller(host, username, password, port, 'v4', site_id)
except urllib.error.HTTPError as ex:
- _LOGGER.error('Failed to connect to unifi: %s', ex)
+ _LOGGER.error('Failed to connect to Unifi: %s', ex)
+ persistent_notification.create(
+ hass, 'Failed to connect to Unifi. '
+ 'Error: {}
'
+ 'You will need to restart hass after fixing.'
+ ''.format(ex),
+ title=NOTIFICATION_TITLE,
+ notification_id=NOTIFICATION_ID)
return False
return UnifiScanner(ctrl)
diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py
index 142764ea522..3c4dff6eac5 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.7']
+REQUIREMENTS = ['netdisco==0.8.0']
DOMAIN = 'discovery'
diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py
index ed06da9495b..32fb4af071c 100644
--- a/homeassistant/components/emulated_hue/hue_api.py
+++ b/homeassistant/components/emulated_hue/hue_api.py
@@ -6,8 +6,8 @@ from aiohttp import web
from homeassistant import core
from homeassistant.const import (
- ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON,
- STATE_ON, STATE_OFF, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
+ ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON,
+ STATE_OFF, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
)
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS
@@ -251,8 +251,7 @@ def entity_to_json(entity, is_on=None, brightness=None):
if brightness is None:
brightness = 255 if is_on else 0
- name = entity.attributes.get(
- ATTR_EMULATED_HUE_NAME, entity.attributes[ATTR_FRIENDLY_NAME])
+ name = entity.attributes.get(ATTR_EMULATED_HUE_NAME, entity.name)
return {
'state':
diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py
index 837fdd0e1fe..e6211c145e2 100644
--- a/homeassistant/components/frontend/version.py
+++ b/homeassistant/components/frontend/version.py
@@ -2,17 +2,17 @@
FINGERPRINTS = {
"core.js": "5dfb2d3e567fad37af0321d4b29265ed",
- "frontend.html": "6a89b74ab2b76c7d28fad2aea9444ec2",
+ "frontend.html": "ac15b11435132aab3da592f9e7b05400",
"mdi.html": "46a76f877ac9848899b8ed382427c16f",
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
"panels/ha-panel-dev-event.html": "c2d5ec676be98d4474d19f94d0262c1e",
"panels/ha-panel-dev-info.html": "a9c07bf281fe9791fb15827ec1286825",
- "panels/ha-panel-dev-service.html": "b3fe49532c5c03198fafb0c6ed58b76a",
+ "panels/ha-panel-dev-service.html": "20420e2387fd93db53c8d778097e3d59",
"panels/ha-panel-dev-state.html": "65e5f791cc467561719bf591f1386054",
"panels/ha-panel-dev-template.html": "7d744ab7f7c08b6d6ad42069989de400",
"panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295",
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
"panels/ha-panel-logbook.html": "4bc5c8370a85a4215413fbae8f85addb",
- "panels/ha-panel-map.html": "1bf6965b24d76db71a1871865cd4a3a2",
+ "panels/ha-panel-map.html": "3b0ca63286cbe80f27bd36dbc2434e89",
"websocket_test.html": "575de64b431fe11c3785bf96d7813450"
}
diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html
index d0a3e75d8db..dd0eba180ec 100644
--- a/homeassistant/components/frontend/www_static/frontend.html
+++ b/homeassistant/components/frontend/www_static/frontend.html
@@ -1,5 +1,5 @@
[[_text]]
![[[stateObj.entityDisplay]]]([[cameraFeedSrc]])
[[stateObj.entityDisplay]](Error loading image)
[[stateObj.entityDisplay]]
[[stateObj.attributes.operation_mode]] [[computeTargetTemperature(stateObj)]]
Currently: [[stateObj.attributes.current_temperature]] [[stateObj.attributes.unit_of_measurement]]

[[stateObj.stateDisplay]]
[[_charCounterStr]][[errorMessage]]
[[item]][[computePrimaryText(stateObj, isPlaying)]]
[[secondaryText]]
[[stateObj.entityDisplay]][[computeTitle(states, groupEntity)]]
To install Home Assistant, run:
pip3 install homeassistant
hass --open-ui
Here are some resources to get started.
To remove this card, edit your config in configuration.yaml
and disable the introduction
component. [[stateObj.entityDisplay]]
[[playerObj.primaryText]]
[[playerObj.secondaryText]]
DISMISS[[computeTitle(views, locationName)]]
[[locationName]][[item.entityDisplay]] {{text}}
No state history found.
Last triggered:
TRIGGER[[formatAttribute(attribute)]]
[[getAttributeValue(stateObj, attribute)]]
[[itemCaption(item)]]
[[itemValue(item)]]
Elevation
[[stateObj.attributes.elevation]]
Last Action
[[stateObj.attributes.last_action]]
[[caption]]
![[[stateObj.entityDisplay]]]([[computeCameraImageUrl(hass, stateObj)]])
{{finalTranscript}} [[interimTranscript]] …
\ No newline at end of file
+},customStyle:null,getComputedStyleValue:function(e){return!i&&this._styleProperties&&this._styleProperties[e]||getComputedStyle(this).getPropertyValue(e)},_setupStyleProperties:function(){this.customStyle={},this._styleCache=null,this._styleProperties=null,this._scopeSelector=null,this._ownStyleProperties=null,this._customStyle=null},_needsStyleProperties:function(){return Boolean(!i&&this._ownStylePropertyNames&&this._ownStylePropertyNames.length)},_validateApplyShim:function(){if(this.__applyShimInvalid){Polymer.ApplyShim.transform(this._styles,this.__proto__);var e=n.elementStyles(this);if(s){var t=this._template.content.querySelector("style");t&&(t.textContent=e)}else{var r=this._scopeStyle&&this._scopeStyle.nextSibling;r&&(r.textContent=e)}}},_beforeAttached:function(){this._scopeSelector&&!this.__stylePropertiesInvalid||!this._needsStyleProperties()||(this.__stylePropertiesInvalid=!1,this._updateStyleProperties())},_findStyleHost:function(){for(var e,t=this;e=Polymer.dom(t).getOwnerRoot();){if(Polymer.isInstance(e.host))return e.host;t=e.host}return r},_updateStyleProperties:function(){var e,n=this._findStyleHost();n._styleProperties||n._computeStyleProperties(),n._styleCache||(n._styleCache=new Polymer.StyleCache);var r=t.propertyDataFromStyles(n._styles,this),i=!this.__notStyleScopeCacheable;i&&(r.key.customStyle=this.customStyle,e=n._styleCache.retrieve(this.is,r.key,this._styles));var a=Boolean(e);a?this._styleProperties=e._styleProperties:this._computeStyleProperties(r.properties),this._computeOwnStyleProperties(),a||(e=o.retrieve(this.is,this._ownStyleProperties,this._styles));var l=Boolean(e)&&!a,c=this._applyStyleProperties(e);a||(c=c&&s?c.cloneNode(!0):c,e={style:c,_scopeSelector:this._scopeSelector,_styleProperties:this._styleProperties},i&&(r.key.customStyle={},this.mixin(r.key.customStyle,this.customStyle),n._styleCache.store(this.is,e,r.key,this._styles)),l||o.store(this.is,Object.create(e),this._ownStyleProperties,this._styles))},_computeStyleProperties:function(e){var n=this._findStyleHost();n._styleProperties||n._computeStyleProperties();var r=Object.create(n._styleProperties),s=t.hostAndRootPropertiesForScope(this);this.mixin(r,s.hostProps),e=e||t.propertyDataFromStyles(n._styles,this).properties,this.mixin(r,e),this.mixin(r,s.rootProps),t.mixinCustomStyle(r,this.customStyle),t.reify(r),this._styleProperties=r},_computeOwnStyleProperties:function(){for(var e,t={},n=0;n0&&l.push(t);return[{removed:a,added:l}]}},Polymer.Collection.get=function(e){return Polymer._collections.get(e)||new Polymer.Collection(e)},Polymer.Collection.applySplices=function(e,t){var n=Polymer._collections.get(e);return n?n._applySplices(t):null},Polymer({is:"dom-repeat",extends:"template",_template:null,properties:{items:{type:Array},as:{type:String,value:"item"},indexAs:{type:String,value:"index"},sort:{type:Function,observer:"_sortChanged"},filter:{type:Function,observer:"_filterChanged"},observe:{type:String,observer:"_observeChanged"},delay:Number,renderedItemCount:{type:Number,notify:!0,readOnly:!0},initialCount:{type:Number,observer:"_initializeChunking"},targetFramerate:{type:Number,value:20},_targetFrameTime:{type:Number,computed:"_computeFrameTime(targetFramerate)"}},behaviors:[Polymer.Templatizer],observers:["_itemsChanged(items.*)"],created:function(){this._instances=[],this._pool=[],this._limit=1/0;var e=this;this._boundRenderChunk=function(){e._renderChunk()}},detached:function(){this.__isDetached=!0;for(var e=0;e=0;t--){var n=this._instances[t];n.isPlaceholder&&t=this._limit&&(n=this._downgradeInstance(t,n.__key__)),e[n.__key__]=t,n.isPlaceholder||n.__setProperty(this.indexAs,t,!0)}this._pool.length=0,this._setRenderedItemCount(this._instances.length),this.fire("dom-change"),this._tryRenderChunk()},_applyFullRefresh:function(){var e,t=this.collection;if(this._sortFn)e=t?t.getKeys():[];else{e=[];var n=this.items;if(n)for(var r=0;r=r;a--)this._detachAndRemoveInstance(a)},_numericSort:function(e,t){return e-t},_applySplicesUserSort:function(e){for(var t,n,r=this.collection,s={},i=0;i=0;i--){var c=a[i];void 0!==c&&this._detachAndRemoveInstance(c)}var h=this;if(l.length){this._filterFn&&(l=l.filter(function(e){return h._filterFn(r.getItem(e))})),l.sort(function(e,t){return h._sortFn(r.getItem(e),r.getItem(t))});var u=0;for(i=0;i>1,a=this._instances[o].__key__,l=this._sortFn(n.getItem(a),r);if(l<0)e=o+1;else{if(!(l>0)){i=o;break}s=o-1}}return i<0&&(i=s+1),this._insertPlaceholder(i,t),i},_applySplicesArrayOrder:function(e){for(var t,n=0;n=0?(e=this.as+"."+e.substring(n+1),i._notifyPath(e,t,!0)):i.__setProperty(this.as,t,!0))}},itemForElement:function(e){var t=this.modelForElement(e);return t&&t[this.as]},keyForElement:function(e){var t=this.modelForElement(e);return t&&t.__key__},indexForElement:function(e){var t=this.modelForElement(e);return t&&t[this.indexAs]}}),Polymer({is:"array-selector",_template:null,properties:{items:{type:Array,observer:"clearSelection"},multi:{type:Boolean,value:!1,observer:"clearSelection"},selected:{type:Object,notify:!0},selectedItem:{type:Object,notify:!0},toggle:{type:Boolean,value:!1}},clearSelection:function(){if(Array.isArray(this.selected))for(var e=0;e
[[_text]]
![[[stateObj.entityDisplay]]]([[cameraFeedSrc]])
[[stateObj.entityDisplay]](Error loading image)
[[stateObj.entityDisplay]]
[[stateObj.attributes.operation_mode]] [[computeTargetTemperature(stateObj)]]
Currently: [[stateObj.attributes.current_temperature]] [[stateObj.attributes.unit_of_measurement]]

[[stateObj.stateDisplay]]
[[_charCounterStr]][[errorMessage]]
[[item]][[computePrimaryText(stateObj, isPlaying)]]
[[secondaryText]]
[[stateObj.entityDisplay]][[computeTitle(states, groupEntity)]]
To install Home Assistant, run:
pip3 install homeassistant
hass --open-ui
Here are some resources to get started.
To remove this card, edit your config in configuration.yaml
and disable the introduction
component. [[stateObj.entityDisplay]]
[[playerObj.primaryText]]
[[playerObj.secondaryText]]
DISMISS[[computeTitle(views, locationName)]]
[[locationName]][[item.entityDisplay]] {{text}}
No state history found.
Last triggered:
TRIGGER[[formatAttribute(attribute)]]
[[getAttributeValue(stateObj, attribute)]]
[[itemCaption(item)]]
[[itemValue(item)]]
Elevation
[[stateObj.attributes.elevation]]
Last Action
[[stateObj.attributes.last_action]]
[[caption]]
![[[stateObj.entityDisplay]]]([[computeCameraImageUrl(hass, stateObj)]])
{{finalTranscript}} [[interimTranscript]] …