diff --git a/.coveragerc b/.coveragerc
index dfbbb232efca86..d059d62b5f31a9 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -61,6 +61,9 @@ omit =
homeassistant/components/coinbase.py
homeassistant/components/sensor/coinbase.py
+ homeassistant/components/cast/*
+ homeassistant/components/*/cast.py
+
homeassistant/components/comfoconnect.py
homeassistant/components/*/comfoconnect.py
@@ -97,7 +100,7 @@ omit =
homeassistant/components/*/envisalink.py
homeassistant/components/fritzbox.py
- homeassistant/components/*/fritzbox.py
+ homeassistant/components/switch/fritzbox.py
homeassistant/components/eufy.py
homeassistant/components/*/eufy.py
@@ -195,12 +198,15 @@ omit =
homeassistant/components/neato.py
homeassistant/components/*/neato.py
- homeassistant/components/nest.py
+ homeassistant/components/nest/__init__.py
homeassistant/components/*/nest.py
homeassistant/components/netatmo.py
homeassistant/components/*/netatmo.py
+ homeassistant/components/netgear_lte.py
+ homeassistant/components/*/netgear_lte.py
+
homeassistant/components/octoprint.py
homeassistant/components/*/octoprint.py
@@ -249,6 +255,9 @@ omit =
homeassistant/components/smappee.py
homeassistant/components/*/smappee.py
+ homeassistant/components/sonos/__init__.py
+ homeassistant/components/*/sonos.py
+
homeassistant/components/tado.py
homeassistant/components/*/tado.py
@@ -311,6 +320,9 @@ omit =
homeassistant/components/wink/*
homeassistant/components/*/wink.py
+ homeassistant/components/wirelesstag.py
+ homeassistant/components/*/wirelesstag.py
+
homeassistant/components/xiaomi_aqara.py
homeassistant/components/*/xiaomi_aqara.py
@@ -348,6 +360,7 @@ omit =
homeassistant/components/binary_sensor/ping.py
homeassistant/components/binary_sensor/rest.py
homeassistant/components/binary_sensor/tapsaff.py
+ homeassistant/components/binary_sensor/uptimerobot.py
homeassistant/components/browser.py
homeassistant/components/calendar/caldav.py
homeassistant/components/calendar/todoist.py
@@ -363,6 +376,7 @@ omit =
homeassistant/components/camera/rpi_camera.py
homeassistant/components/camera/synology.py
homeassistant/components/camera/xeoma.py
+ homeassistant/components/camera/xiaomi.py
homeassistant/components/camera/yi.py
homeassistant/components/climate/econet.py
homeassistant/components/climate/ephember.py
@@ -378,6 +392,7 @@ omit =
homeassistant/components/climate/sensibo.py
homeassistant/components/climate/touchline.py
homeassistant/components/climate/venstar.py
+ homeassistant/components/climate/zhong_hong.py
homeassistant/components/cover/garadget.py
homeassistant/components/cover/gogogate2.py
homeassistant/components/cover/homematic.py
@@ -397,6 +412,7 @@ omit =
homeassistant/components/device_tracker/bt_home_hub_5.py
homeassistant/components/device_tracker/cisco_ios.py
homeassistant/components/device_tracker/ddwrt.py
+ homeassistant/components/device_tracker/freebox.py
homeassistant/components/device_tracker/fritz.py
homeassistant/components/device_tracker/google_maps.py
homeassistant/components/device_tracker/gpslogger.py
@@ -462,6 +478,7 @@ omit =
homeassistant/components/light/yeelightsunflower.py
homeassistant/components/light/zengge.py
homeassistant/components/lirc.py
+ homeassistant/components/lock/kiwi.py
homeassistant/components/lock/lockitron.py
homeassistant/components/lock/nello.py
homeassistant/components/lock/nuki.py
@@ -472,7 +489,6 @@ omit =
homeassistant/components/media_player/aquostv.py
homeassistant/components/media_player/bluesound.py
homeassistant/components/media_player/braviatv.py
- homeassistant/components/media_player/cast.py
homeassistant/components/media_player/channels.py
homeassistant/components/media_player/clementine.py
homeassistant/components/media_player/cmus.py
@@ -481,10 +497,12 @@ omit =
homeassistant/components/media_player/directv.py
homeassistant/components/media_player/dunehd.py
homeassistant/components/media_player/emby.py
+ homeassistant/components/media_player/epson.py
homeassistant/components/media_player/firetv.py
homeassistant/components/media_player/frontier_silicon.py
homeassistant/components/media_player/gpmdp.py
homeassistant/components/media_player/gstreamer.py
+ homeassistant/components/media_player/horizon.py
homeassistant/components/media_player/itunes.py
homeassistant/components/media_player/kodi.py
homeassistant/components/media_player/lg_netcast.py
@@ -506,7 +524,6 @@ omit =
homeassistant/components/media_player/russound_rnet.py
homeassistant/components/media_player/snapcast.py
homeassistant/components/media_player/songpal.py
- homeassistant/components/media_player/sonos.py
homeassistant/components/media_player/spotify.py
homeassistant/components/media_player/squeezebox.py
homeassistant/components/media_player/ue_smart_radio.py
@@ -644,6 +661,7 @@ omit =
homeassistant/components/sensor/nederlandse_spoorwegen.py
homeassistant/components/sensor/netdata.py
homeassistant/components/sensor/neurio_energy.py
+ homeassistant/components/sensor/nsw_fuel_station.py
homeassistant/components/sensor/nut.py
homeassistant/components/sensor/nzbget.py
homeassistant/components/sensor/ohmconnect.py
@@ -748,6 +766,7 @@ omit =
homeassistant/components/tts/picotts.py
homeassistant/components/vacuum/mqtt.py
homeassistant/components/vacuum/roomba.py
+ homeassistant/components/watson_iot.py
homeassistant/components/weather/bom.py
homeassistant/components/weather/buienradar.py
homeassistant/components/weather/darksky.py
diff --git a/CODEOWNERS b/CODEOWNERS
index 0da8353e5aa84d..556791b879c64e 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -70,6 +70,7 @@ homeassistant/components/sensor/filter.py @dgomes
homeassistant/components/sensor/gearbest.py @HerrHofrat
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
+homeassistant/components/sensor/nsw_fuel_station.py @nickw444
homeassistant/components/sensor/pollen.py @bachya
homeassistant/components/sensor/qnap.py @colinodell
homeassistant/components/sensor/sma.py @kellerza
diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py
index a405362d368d8b..b108ac805e9f67 100644
--- a/homeassistant/bootstrap.py
+++ b/homeassistant/bootstrap.py
@@ -1,5 +1,4 @@
"""Provide methods to bootstrap a Home Assistant instance."""
-import asyncio
import logging
import logging.handlers
import os
@@ -17,7 +16,7 @@
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
from homeassistant.setup import async_setup_component
from homeassistant.util.logging import AsyncHandler
-from homeassistant.util.package import async_get_user_site, get_user_site
+from homeassistant.util.package import async_get_user_site, is_virtual_env
from homeassistant.util.yaml import clear_secret_cache
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.signal import async_register_signal_handling
@@ -53,8 +52,9 @@ def from_config_dict(config: Dict[str, Any],
if config_dir is not None:
config_dir = os.path.abspath(config_dir)
hass.config.config_dir = config_dir
- hass.loop.run_until_complete(
- async_mount_local_lib_path(config_dir, hass.loop))
+ if not is_virtual_env():
+ hass.loop.run_until_complete(
+ async_mount_local_lib_path(config_dir))
# run task
hass = hass.loop.run_until_complete(
@@ -197,7 +197,9 @@ async def async_from_config_file(config_path: str,
# Set config dir to directory holding config file
config_dir = os.path.abspath(os.path.dirname(config_path))
hass.config.config_dir = config_dir
- await async_mount_local_lib_path(config_dir, hass.loop)
+
+ if not is_virtual_env():
+ await async_mount_local_lib_path(config_dir)
async_enable_logging(hass, verbose, log_rotate_days, log_file,
log_no_color)
@@ -211,9 +213,8 @@ async def async_from_config_file(config_path: str,
finally:
clear_secret_cache()
- hass = await async_from_config_dict(
+ return await async_from_config_dict(
config_dict, hass, enable_log=False, skip_pip=skip_pip)
- return hass
@core.callback
@@ -308,23 +309,13 @@ async def async_stop_async_handler(event):
"Unable to setup error log %s (access denied)", err_log_path)
-def mount_local_lib_path(config_dir: str) -> str:
- """Add local library to Python Path."""
- deps_dir = os.path.join(config_dir, 'deps')
- lib_dir = get_user_site(deps_dir)
- if lib_dir not in sys.path:
- sys.path.insert(0, lib_dir)
- return deps_dir
-
-
-async def async_mount_local_lib_path(config_dir: str,
- loop: asyncio.AbstractEventLoop) -> str:
+async def async_mount_local_lib_path(config_dir: str) -> str:
"""Add local library to Python Path.
This function is a coroutine.
"""
deps_dir = os.path.join(config_dir, 'deps')
- lib_dir = await async_get_user_site(deps_dir, loop=loop)
+ lib_dir = await async_get_user_site(deps_dir)
if lib_dir not in sys.path:
sys.path.insert(0, lib_dir)
return deps_dir
diff --git a/homeassistant/components/alarm_control_panel/arlo.py b/homeassistant/components/alarm_control_panel/arlo.py
index 333bde9ee36a75..20887157cb4dff 100644
--- a/homeassistant/components/alarm_control_panel/arlo.py
+++ b/homeassistant/components/alarm_control_panel/arlo.py
@@ -4,15 +4,17 @@
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.arlo/
"""
-import asyncio
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.alarm_control_panel import (
AlarmControlPanel, PLATFORM_SCHEMA)
-from homeassistant.components.arlo import (DATA_ARLO, CONF_ATTRIBUTION)
+from homeassistant.components.arlo import (
+ DATA_ARLO, CONF_ATTRIBUTION, SIGNAL_UPDATE_ARLO)
from homeassistant.const import (
ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED)
@@ -36,21 +38,20 @@
})
-@asyncio.coroutine
-def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
+def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Arlo Alarm Control Panels."""
- data = hass.data[DATA_ARLO]
+ arlo = hass.data[DATA_ARLO]
- if not data.base_stations:
+ if not arlo.base_stations:
return
home_mode_name = config.get(CONF_HOME_MODE_NAME)
away_mode_name = config.get(CONF_AWAY_MODE_NAME)
base_stations = []
- for base_station in data.base_stations:
+ for base_station in arlo.base_stations:
base_stations.append(ArloBaseStation(base_station, home_mode_name,
away_mode_name))
- async_add_devices(base_stations, True)
+ add_devices(base_stations, True)
class ArloBaseStation(AlarmControlPanel):
@@ -68,6 +69,16 @@ def icon(self):
"""Return icon."""
return ICON
+ async def async_added_to_hass(self):
+ """Register callbacks."""
+ async_dispatcher_connect(
+ self.hass, SIGNAL_UPDATE_ARLO, self._update_callback)
+
+ @callback
+ def _update_callback(self):
+ """Call update method."""
+ self.async_schedule_update_ha_state(True)
+
@property
def state(self):
"""Return the state of the device."""
@@ -75,30 +86,22 @@ def state(self):
def update(self):
"""Update the state of the device."""
- # PyArlo sometimes returns None for mode. So retry 3 times before
- # returning None.
- num_retries = 3
- i = 0
- while i < num_retries:
- mode = self._base_station.mode
- if mode:
- self._state = self._get_state_from_mode(mode)
- return
- i += 1
- self._state = None
-
- @asyncio.coroutine
- def async_alarm_disarm(self, code=None):
+ _LOGGER.debug("Updating Arlo Alarm Control Panel %s", self.name)
+ mode = self._base_station.mode
+ if mode:
+ self._state = self._get_state_from_mode(mode)
+ else:
+ self._state = None
+
+ async def async_alarm_disarm(self, code=None):
"""Send disarm command."""
self._base_station.mode = DISARMED
- @asyncio.coroutine
- def async_alarm_arm_away(self, code=None):
+ async def async_alarm_arm_away(self, code=None):
"""Send arm away command. Uses custom mode."""
self._base_station.mode = self._away_mode_name
- @asyncio.coroutine
- def async_alarm_arm_home(self, code=None):
+ async def async_alarm_arm_home(self, code=None):
"""Send arm home command. Uses custom mode."""
self._base_station.mode = self._home_mode_name
@@ -125,4 +128,4 @@ def _get_state_from_mode(self, mode):
return STATE_ALARM_ARMED_HOME
elif mode == self._away_mode_name:
return STATE_ALARM_ARMED_AWAY
- return None
+ return mode
diff --git a/homeassistant/components/amcrest.py b/homeassistant/components/amcrest.py
index d0e470e3f8ec44..820ca41ad2e73f 100644
--- a/homeassistant/components/amcrest.py
+++ b/homeassistant/components/amcrest.py
@@ -18,7 +18,7 @@
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['amcrest==1.2.2']
+REQUIREMENTS = ['amcrest==1.2.3']
DEPENDENCIES = ['ffmpeg']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo.py
index 7e51ec8c045e17..cd2c13ad292bd3 100644
--- a/homeassistant/components/arlo.py
+++ b/homeassistant/components/arlo.py
@@ -5,14 +5,18 @@
https://home-assistant.io/components/arlo/
"""
import logging
+from datetime import timedelta
import voluptuous as vol
from requests.exceptions import HTTPError, ConnectTimeout
from homeassistant.helpers import config_validation as cv
-from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
+from homeassistant.const import (
+ CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL)
+from homeassistant.helpers.event import track_time_interval
+from homeassistant.helpers.dispatcher import dispatcher_send
-REQUIREMENTS = ['pyarlo==0.1.2']
+REQUIREMENTS = ['pyarlo==0.1.7']
_LOGGER = logging.getLogger(__name__)
@@ -25,10 +29,16 @@
NOTIFICATION_ID = 'arlo_notification'
NOTIFICATION_TITLE = 'Arlo Component Setup'
+SCAN_INTERVAL = timedelta(seconds=60)
+
+SIGNAL_UPDATE_ARLO = "arlo_update"
+
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
+ vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
+ cv.time_period,
}),
}, extra=vol.ALLOW_EXTRA)
@@ -38,6 +48,7 @@ def setup(hass, config):
conf = config[DOMAIN]
username = conf.get(CONF_USERNAME)
password = conf.get(CONF_PASSWORD)
+ scan_interval = conf.get(CONF_SCAN_INTERVAL)
try:
from pyarlo import PyArlo
@@ -45,7 +56,17 @@ def setup(hass, config):
arlo = PyArlo(username, password, preload=False)
if not arlo.is_connected:
return False
+
+ # assign refresh period to base station thread
+ arlo_base_station = next((
+ station for station in arlo.base_stations), None)
+
+ if arlo_base_station is None:
+ return False
+
+ arlo_base_station.refresh_rate = scan_interval.total_seconds()
hass.data[DATA_ARLO] = arlo
+
except (ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex))
hass.components.persistent_notification.create(
@@ -55,4 +76,17 @@ def setup(hass, config):
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
+
+ def hub_refresh(event_time):
+ """Call ArloHub to refresh information."""
+ _LOGGER.info("Updating Arlo Hub component")
+ hass.data[DATA_ARLO].update(update_cameras=True,
+ update_base_station=True)
+ dispatcher_send(hass, SIGNAL_UPDATE_ARLO)
+
+ # register service
+ hass.services.register(DOMAIN, 'update', hub_refresh)
+
+ # register scan interval for ArloHub
+ track_time_interval(hass, hub_refresh, scan_interval)
return True
diff --git a/homeassistant/components/axis.py b/homeassistant/components/axis.py
index fab7d98ed98399..71894364f91440 100644
--- a/homeassistant/components/axis.py
+++ b/homeassistant/components/axis.py
@@ -145,7 +145,7 @@ def configuration_callback(callback_data):
def setup(hass, config):
"""Set up for Axis devices."""
- def _shutdown(call): # pylint: disable=unused-argument
+ def _shutdown(call):
"""Stop the event stream on shutdown."""
for serialnumber, device in AXIS_DEVICES.items():
_LOGGER.info("Stopping event stream for %s.", serialnumber)
@@ -272,8 +272,7 @@ def __init__(self, event_config):
def _update_callback(self):
"""Update the sensor's state, if needed."""
- self.update()
- self.schedule_update_ha_state()
+ self.schedule_update_ha_state(True)
@property
def name(self):
diff --git a/homeassistant/components/binary_sensor/command_line.py b/homeassistant/components/binary_sensor/command_line.py
index 2289ad5d9064ec..480786b2c2c8a7 100644
--- a/homeassistant/components/binary_sensor/command_line.py
+++ b/homeassistant/components/binary_sensor/command_line.py
@@ -35,7 +35,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Command line Binary Sensor."""
name = config.get(CONF_NAME)
diff --git a/homeassistant/components/binary_sensor/eight_sleep.py b/homeassistant/components/binary_sensor/eight_sleep.py
index a6d4476f047e20..40ca491e1f3cc0 100644
--- a/homeassistant/components/binary_sensor/eight_sleep.py
+++ b/homeassistant/components/binary_sensor/eight_sleep.py
@@ -5,7 +5,6 @@
https://home-assistant.io/components/binary_sensor.eight_sleep/
"""
import logging
-import asyncio
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.eight_sleep import (
@@ -16,8 +15,8 @@
DEPENDENCIES = ['eight_sleep']
-@asyncio.coroutine
-def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
+async def async_setup_platform(hass, config, async_add_devices,
+ discovery_info=None):
"""Set up the eight sleep binary sensor."""
if discovery_info is None:
return
@@ -63,7 +62,6 @@ def is_on(self):
"""Return true if the binary sensor is on."""
return self._state
- @asyncio.coroutine
- def async_update(self):
+ async def async_update(self):
"""Retrieve latest state."""
self._state = self._usrobj.bed_presence
diff --git a/homeassistant/components/binary_sensor/gc100.py b/homeassistant/components/binary_sensor/gc100.py
index c17e6b50911401..767be2874e6ab8 100644
--- a/homeassistant/components/binary_sensor/gc100.py
+++ b/homeassistant/components/binary_sensor/gc100.py
@@ -23,7 +23,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the GC100 devices."""
binary_sensors = []
diff --git a/homeassistant/components/binary_sensor/isy994.py b/homeassistant/components/binary_sensor/isy994.py
index 09f1739cba7804..a80e4db747d505 100644
--- a/homeassistant/components/binary_sensor/isy994.py
+++ b/homeassistant/components/binary_sensor/isy994.py
@@ -28,7 +28,6 @@
}
-# pylint: disable=unused-argument
def setup_platform(hass, config: ConfigType,
add_devices: Callable[[list], None], discovery_info=None):
"""Set up the ISY994 binary sensor platform."""
@@ -299,7 +298,6 @@ def _restart_timer(self):
# No heartbeat timer is active
pass
- # pylint: disable=unused-argument
@callback
def timer_elapsed(now) -> None:
"""Heartbeat missed; set state to indicate dead battery."""
@@ -314,7 +312,6 @@ def timer_elapsed(now) -> None:
self._heartbeat_timer = async_track_point_in_utc_time(
self.hass, timer_elapsed, point_in_time)
- # pylint: disable=unused-argument
def on_update(self, event: object) -> None:
"""Ignore node status updates.
diff --git a/homeassistant/components/binary_sensor/knx.py b/homeassistant/components/binary_sensor/knx.py
index 834186b8b185e1..e6b28047cb8f2f 100644
--- a/homeassistant/components/binary_sensor/knx.py
+++ b/homeassistant/components/binary_sensor/knx.py
@@ -115,7 +115,6 @@ def async_register_callbacks(self):
"""Register callbacks to update hass after device was changed."""
async def after_update_callback(device):
"""Call after device was updated."""
- # pylint: disable=unused-argument
await self.async_update_ha_state()
self.device.register_device_updated_cb(after_update_callback)
diff --git a/homeassistant/components/binary_sensor/linode.py b/homeassistant/components/binary_sensor/linode.py
index 8af0318373d5a0..d4fc60696cdafd 100644
--- a/homeassistant/components/binary_sensor/linode.py
+++ b/homeassistant/components/binary_sensor/linode.py
@@ -52,19 +52,18 @@ def __init__(self, li, node_id):
self._node_id = node_id
self._state = None
self.data = None
+ self._attrs = {}
+ self._name = None
@property
def name(self):
"""Return the name of the sensor."""
- if self.data is not None:
- return self.data.label
+ return self._name
@property
def is_on(self):
"""Return true if the binary sensor is on."""
- if self.data is not None:
- return self.data.status == 'running'
- return False
+ return self._state
@property
def device_class(self):
@@ -74,8 +73,18 @@ def device_class(self):
@property
def device_state_attributes(self):
"""Return the state attributes of the Linode Node."""
- if self.data:
- return {
+ return self._attrs
+
+ def update(self):
+ """Update state of sensor."""
+ self._linode.update()
+ if self._linode.data is not None:
+ for node in self._linode.data:
+ if node.id == self._node_id:
+ self.data = node
+ if self.data is not None:
+ self._state = self.data.status == 'running'
+ self._attrs = {
ATTR_CREATED: self.data.created,
ATTR_NODE_ID: self.data.id,
ATTR_NODE_NAME: self.data.label,
@@ -85,12 +94,4 @@ def device_state_attributes(self):
ATTR_REGION: self.data.region.country,
ATTR_VCPUS: self.data.specs.vcpus,
}
- return {}
-
- def update(self):
- """Update state of sensor."""
- self._linode.update()
- if self._linode.data is not None:
- for node in self._linode.data:
- if node.id == self._node_id:
- self.data = node
+ self._name = self.data.label
diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py
index e033355f655313..d2533eb8f5b478 100644
--- a/homeassistant/components/binary_sensor/mqtt.py
+++ b/homeassistant/components/binary_sensor/mqtt.py
@@ -6,6 +6,7 @@
"""
import asyncio
import logging
+from typing import Optional
import voluptuous as vol
@@ -24,7 +25,7 @@
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'MQTT Binary sensor'
-
+CONF_UNIQUE_ID = 'unique_id'
DEFAULT_PAYLOAD_OFF = 'OFF'
DEFAULT_PAYLOAD_ON = 'ON'
DEFAULT_FORCE_UPDATE = False
@@ -37,6 +38,9 @@
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
+ # Integrations shouldn't never expose unique_id through configuration
+ # this here is an exception because MQTT is a msg transport, not a protocol
+ vol.Optional(CONF_UNIQUE_ID): cv.string,
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@@ -61,7 +65,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
config.get(CONF_PAYLOAD_OFF),
config.get(CONF_PAYLOAD_AVAILABLE),
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
- value_template
+ value_template,
+ config.get(CONF_UNIQUE_ID),
)])
@@ -70,7 +75,8 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
def __init__(self, name, state_topic, availability_topic, device_class,
qos, force_update, payload_on, payload_off, payload_available,
- payload_not_available, value_template):
+ payload_not_available, value_template,
+ unique_id: Optional[str]):
"""Initialize the MQTT binary sensor."""
super().__init__(availability_topic, qos, payload_available,
payload_not_available)
@@ -83,6 +89,7 @@ def __init__(self, name, state_topic, availability_topic, device_class,
self._qos = qos
self._force_update = force_update
self._template = value_template
+ self._unique_id = unique_id
@asyncio.coroutine
def async_added_to_hass(self):
@@ -134,3 +141,8 @@ def device_class(self):
def force_update(self):
"""Force update."""
return self._force_update
+
+ @property
+ def unique_id(self):
+ """Return a unique ID."""
+ return self._unique_id
diff --git a/homeassistant/components/binary_sensor/mystrom.py b/homeassistant/components/binary_sensor/mystrom.py
index 93d56a97c4273e..5c1d9a576a6852 100644
--- a/homeassistant/components/binary_sensor/mystrom.py
+++ b/homeassistant/components/binary_sensor/mystrom.py
@@ -29,6 +29,7 @@ class MyStromView(HomeAssistantView):
url = '/api/mystrom'
name = 'api:mystrom'
+ supported_actions = ['single', 'double', 'long', 'touch']
def __init__(self, add_devices):
"""Initialize the myStrom URL endpoint."""
@@ -44,16 +45,18 @@ def get(self, request):
@asyncio.coroutine
def _handle(self, hass, data):
"""Handle requests to the myStrom endpoint."""
- button_action = list(data.keys())[0]
- button_id = data[button_action]
- entity_id = '{}.{}_{}'.format(DOMAIN, button_id, button_action)
+ button_action = next((
+ parameter for parameter in data
+ if parameter in self.supported_actions), None)
- if button_action not in ['single', 'double', 'long', 'touch']:
+ if button_action is None:
_LOGGER.error(
"Received unidentified message from myStrom button: %s", data)
return ("Received unidentified message: {}".format(data),
HTTP_UNPROCESSABLE_ENTITY)
+ button_id = data[button_action]
+ entity_id = '{}.{}_{}'.format(DOMAIN, button_id, button_action)
if entity_id not in self.buttons:
_LOGGER.info("New myStrom button/action detected: %s/%s",
button_id, button_action)
diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py
index 882ff142e8c147..9da352e1268bff 100644
--- a/homeassistant/components/binary_sensor/nest.py
+++ b/homeassistant/components/binary_sensor/nest.py
@@ -8,7 +8,8 @@
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
-from homeassistant.components.nest import DATA_NEST, NestSensorDevice
+from homeassistant.components.nest import (
+ DATA_NEST, DATA_NEST_CONFIG, CONF_BINARY_SENSORS, NestSensorDevice)
from homeassistant.const import CONF_MONITORED_CONDITIONS
DEPENDENCIES = ['nest']
@@ -56,12 +57,19 @@
def setup_platform(hass, config, add_devices, discovery_info=None):
- """Set up the Nest binary sensors."""
- if discovery_info is None:
- return
+ """Set up the Nest binary sensors.
+ No longer used.
+ """
+
+
+async def async_setup_entry(hass, entry, async_add_devices):
+ """Set up a Nest binary sensor based on a config entry."""
nest = hass.data[DATA_NEST]
+ discovery_info = \
+ hass.data.get(DATA_NEST_CONFIG, {}).get(CONF_BINARY_SENSORS, {})
+
# Add all available binary sensors if no Nest binary sensor config is set
if discovery_info == {}:
conditions = _VALID_BINARY_SENSOR_TYPES
@@ -76,32 +84,37 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"for valid options.")
_LOGGER.error(wstr)
- sensors = []
- for structure in nest.structures():
- sensors += [NestBinarySensor(structure, None, variable)
- for variable in conditions
- if variable in STRUCTURE_BINARY_TYPES]
- device_chain = chain(nest.thermostats(),
- nest.smoke_co_alarms(),
- nest.cameras())
- for structure, device in device_chain:
- sensors += [NestBinarySensor(structure, device, variable)
- for variable in conditions
- if variable in BINARY_TYPES]
- sensors += [NestBinarySensor(structure, device, variable)
- for variable in conditions
- if variable in CLIMATE_BINARY_TYPES
- and device.is_thermostat]
-
- if device.is_camera:
+ def get_binary_sensors():
+ """Get the Nest binary sensors."""
+ sensors = []
+ for structure in nest.structures():
+ sensors += [NestBinarySensor(structure, None, variable)
+ for variable in conditions
+ if variable in STRUCTURE_BINARY_TYPES]
+ device_chain = chain(nest.thermostats(),
+ nest.smoke_co_alarms(),
+ nest.cameras())
+ for structure, device in device_chain:
sensors += [NestBinarySensor(structure, device, variable)
for variable in conditions
- if variable in CAMERA_BINARY_TYPES]
- for activity_zone in device.activity_zones:
- sensors += [NestActivityZoneSensor(structure,
- device,
- activity_zone)]
- add_devices(sensors, True)
+ if variable in BINARY_TYPES]
+ sensors += [NestBinarySensor(structure, device, variable)
+ for variable in conditions
+ if variable in CLIMATE_BINARY_TYPES
+ and device.is_thermostat]
+
+ if device.is_camera:
+ sensors += [NestBinarySensor(structure, device, variable)
+ for variable in conditions
+ if variable in CAMERA_BINARY_TYPES]
+ for activity_zone in device.activity_zones:
+ sensors += [NestActivityZoneSensor(structure,
+ device,
+ activity_zone)]
+
+ return sensors
+
+ async_add_devices(await hass.async_add_job(get_binary_sensors), True)
class NestBinarySensor(NestSensorDevice, BinarySensorDevice):
diff --git a/homeassistant/components/binary_sensor/netatmo.py b/homeassistant/components/binary_sensor/netatmo.py
index fd0e30ccebc408..7c3a3e1dd306b9 100644
--- a/homeassistant/components/binary_sensor/netatmo.py
+++ b/homeassistant/components/binary_sensor/netatmo.py
@@ -57,7 +57,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the access to Netatmo binary sensor."""
netatmo = hass.components.netatmo
@@ -68,12 +67,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
module_name = None
- import lnetatmo
+ import pyatmo
try:
data = CameraData(netatmo.NETATMO_AUTH, home)
if not data.get_camera_names():
return None
- except lnetatmo.NoDevice:
+ except pyatmo.NoDevice:
return None
welcome_sensors = config.get(
diff --git a/homeassistant/components/binary_sensor/octoprint.py b/homeassistant/components/binary_sensor/octoprint.py
index 265fcec66fa9c7..1a1967b9014a0b 100644
--- a/homeassistant/components/binary_sensor/octoprint.py
+++ b/homeassistant/components/binary_sensor/octoprint.py
@@ -33,7 +33,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the available OctoPrint binary sensors."""
octoprint_api = hass.data[DOMAIN]["api"]
diff --git a/homeassistant/components/binary_sensor/pilight.py b/homeassistant/components/binary_sensor/pilight.py
index d2c46c795a8530..69dc3b834855c1 100644
--- a/homeassistant/components/binary_sensor/pilight.py
+++ b/homeassistant/components/binary_sensor/pilight.py
@@ -44,7 +44,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Pilight Binary Sensor."""
disarm = config.get(CONF_DISARM_AFTER_TRIGGER)
diff --git a/homeassistant/components/binary_sensor/rainmachine.py b/homeassistant/components/binary_sensor/rainmachine.py
index 601a73298af0de..b2f44696fbdc25 100644
--- a/homeassistant/components/binary_sensor/rainmachine.py
+++ b/homeassistant/components/binary_sensor/rainmachine.py
@@ -8,7 +8,7 @@
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.rainmachine import (
- BINARY_SENSORS, DATA_RAINMACHINE, DATA_UPDATE_TOPIC, TYPE_FREEZE,
+ BINARY_SENSORS, DATA_RAINMACHINE, SENSOR_UPDATE_TOPIC, TYPE_FREEZE,
TYPE_FREEZE_PROTECTION, TYPE_HOT_DAYS, TYPE_HOURLY, TYPE_MONTH,
TYPE_RAINDELAY, TYPE_RAINSENSOR, TYPE_WEEKDAY, RainMachineEntity)
from homeassistant.const import CONF_MONITORED_CONDITIONS
@@ -20,7 +20,8 @@
_LOGGER = logging.getLogger(__name__)
-def setup_platform(hass, config, add_devices, discovery_info=None):
+async def async_setup_platform(
+ hass, config, async_add_devices, discovery_info=None):
"""Set up the RainMachine Switch platform."""
if discovery_info is None:
return
@@ -33,7 +34,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
binary_sensors.append(
RainMachineBinarySensor(rainmachine, sensor_type, name, icon))
- add_devices(binary_sensors, True)
+ async_add_devices(binary_sensors, True)
class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice):
@@ -70,16 +71,16 @@ def unique_id(self) -> str:
self.rainmachine.device_mac.replace(':', ''), self._sensor_type)
@callback
- def update_data(self):
+ def _update_data(self):
"""Update the state."""
self.async_schedule_update_ha_state(True)
async def async_added_to_hass(self):
"""Register callbacks."""
- async_dispatcher_connect(self.hass, DATA_UPDATE_TOPIC,
- self.update_data)
+ async_dispatcher_connect(
+ self.hass, SENSOR_UPDATE_TOPIC, self._update_data)
- def update(self):
+ async def async_update(self):
"""Update the state."""
if self._sensor_type == TYPE_FREEZE:
self._state = self.rainmachine.restrictions['current']['freeze']
diff --git a/homeassistant/components/binary_sensor/raspihats.py b/homeassistant/components/binary_sensor/raspihats.py
index 9d489a59711a3f..9ab56a5a20da75 100644
--- a/homeassistant/components/binary_sensor/raspihats.py
+++ b/homeassistant/components/binary_sensor/raspihats.py
@@ -42,7 +42,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the raspihats binary_sensor devices."""
I2CHatBinarySensor.I2C_HATS_MANAGER = hass.data[I2C_HATS_MANAGER]
diff --git a/homeassistant/components/binary_sensor/rpi_gpio.py b/homeassistant/components/binary_sensor/rpi_gpio.py
index 2322b1bf49845b..e1e06ce57b9612 100644
--- a/homeassistant/components/binary_sensor/rpi_gpio.py
+++ b/homeassistant/components/binary_sensor/rpi_gpio.py
@@ -39,7 +39,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Raspberry PI GPIO devices."""
pull_mode = config.get(CONF_PULL_MODE)
diff --git a/homeassistant/components/binary_sensor/skybell.py b/homeassistant/components/binary_sensor/skybell.py
index 734f8e03375e5f..44cad11e3f0979 100644
--- a/homeassistant/components/binary_sensor/skybell.py
+++ b/homeassistant/components/binary_sensor/skybell.py
@@ -94,4 +94,4 @@ def update(self):
self._state = bool(event and event.get('id') != self._event.get('id'))
- self._event = event
+ self._event = event or {}
diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py
index 5405a6a77ba57d..dcdd312ce814f7 100644
--- a/homeassistant/components/binary_sensor/trend.py
+++ b/homeassistant/components/binary_sensor/trend.py
@@ -57,7 +57,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the trend sensors."""
sensors = []
diff --git a/homeassistant/components/binary_sensor/uptimerobot.py b/homeassistant/components/binary_sensor/uptimerobot.py
new file mode 100644
index 00000000000000..9e72d188c99552
--- /dev/null
+++ b/homeassistant/components/binary_sensor/uptimerobot.py
@@ -0,0 +1,92 @@
+"""
+A platform that to monitor Uptime Robot monitors.
+
+For more details about this platform, please refer to the documentation at
+https://www.home-assistant.io/components/binary_sensor.uptimerobot/
+"""
+import logging
+
+import voluptuous as vol
+
+from homeassistant.components.binary_sensor import (
+ PLATFORM_SCHEMA, BinarySensorDevice)
+from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY
+import homeassistant.helpers.config_validation as cv
+
+REQUIREMENTS = ['pyuptimerobot==0.0.5']
+
+_LOGGER = logging.getLogger(__name__)
+
+ATTR_TARGET = 'target'
+
+CONF_ATTRIBUTION = "Data provided by Uptime Robot"
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_API_KEY): cv.string,
+})
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the Uptime Robot binary_sensors."""
+ from pyuptimerobot import UptimeRobot
+
+ up_robot = UptimeRobot()
+ api_key = config.get(CONF_API_KEY)
+ monitors = up_robot.getMonitors(api_key)
+
+ devices = []
+ if not monitors or monitors.get('stat') != 'ok':
+ _LOGGER.error("Error connecting to Uptime Robot")
+ return
+
+ for monitor in monitors['monitors']:
+ devices.append(UptimeRobotBinarySensor(
+ api_key, up_robot, monitor['id'], monitor['friendly_name'],
+ monitor['url']))
+
+ add_devices(devices, True)
+
+
+class UptimeRobotBinarySensor(BinarySensorDevice):
+ """Representation of a Uptime Robot binary sensor."""
+
+ def __init__(self, api_key, up_robot, monitor_id, name, target):
+ """Initialize Uptime Robot the binary sensor."""
+ self._api_key = api_key
+ self._monitor_id = str(monitor_id)
+ self._name = name
+ self._target = target
+ self._up_robot = up_robot
+ self._state = None
+
+ @property
+ def name(self):
+ """Return the name of the binary sensor."""
+ return self._name
+
+ @property
+ def is_on(self):
+ """Return the state of the binary sensor."""
+ return self._state
+
+ @property
+ def device_class(self):
+ """Return the class of this device, from component DEVICE_CLASSES."""
+ return 'connectivity'
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes of the binary sensor."""
+ return {
+ ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
+ ATTR_TARGET: self._target,
+ }
+
+ def update(self):
+ """Get the latest state of the binary sensor."""
+ monitor = self._up_robot.getMonitors(self._api_key, self._monitor_id)
+ if not monitor or monitor.get('stat') != 'ok':
+ _LOGGER.warning("Failed to get new state")
+ return
+ status = monitor['monitors'][0]['status']
+ self._state = 1 if status == 2 else 0
diff --git a/homeassistant/components/binary_sensor/vera.py b/homeassistant/components/binary_sensor/vera.py
index e87886376bc307..310e2289cbc9bf 100644
--- a/homeassistant/components/binary_sensor/vera.py
+++ b/homeassistant/components/binary_sensor/vera.py
@@ -19,8 +19,8 @@
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Perform the setup for Vera controller devices."""
add_devices(
- VeraBinarySensor(device, hass.data[VERA_CONTROLLER])
- for device in hass.data[VERA_DEVICES]['binary_sensor'])
+ [VeraBinarySensor(device, hass.data[VERA_CONTROLLER])
+ for device in hass.data[VERA_DEVICES]['binary_sensor']], True)
class VeraBinarySensor(VeraDevice, BinarySensorDevice):
diff --git a/homeassistant/components/binary_sensor/verisure.py b/homeassistant/components/binary_sensor/verisure.py
index 4a1b99f4b9bc9a..7068d51f6a3632 100644
--- a/homeassistant/components/binary_sensor/verisure.py
+++ b/homeassistant/components/binary_sensor/verisure.py
@@ -54,6 +54,7 @@ def available(self):
"$.doorWindow.doorWindowDevice[?(@.deviceLabel=='%s')]",
self._device_label) is not None
+ # pylint: disable=no-self-use
def update(self):
"""Update the state of the sensor."""
hub.update_overview()
diff --git a/homeassistant/components/binary_sensor/wemo.py b/homeassistant/components/binary_sensor/wemo.py
index 30a7e291401bc0..d3c78597c70bc7 100644
--- a/homeassistant/components/binary_sensor/wemo.py
+++ b/homeassistant/components/binary_sensor/wemo.py
@@ -13,7 +13,7 @@
_LOGGER = logging.getLogger(__name__)
-# pylint: disable=unused-argument, too-many-function-args
+# pylint: disable=too-many-function-args
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Register discovered WeMo binary sensors."""
import pywemo.discovery as discovery
diff --git a/homeassistant/components/binary_sensor/wirelesstag.py b/homeassistant/components/binary_sensor/wirelesstag.py
new file mode 100644
index 00000000000000..bfc2d44fc6e8bd
--- /dev/null
+++ b/homeassistant/components/binary_sensor/wirelesstag.py
@@ -0,0 +1,214 @@
+"""
+Binary sensor support for Wireless Sensor Tags.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/binary_sensor.wirelesstag/
+"""
+import logging
+
+import voluptuous as vol
+
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.components.binary_sensor import (
+ BinarySensorDevice, PLATFORM_SCHEMA)
+from homeassistant.components.wirelesstag import (
+ DOMAIN as WIRELESSTAG_DOMAIN,
+ WIRELESSTAG_TYPE_13BIT, WIRELESSTAG_TYPE_WATER,
+ WIRELESSTAG_TYPE_ALSPRO,
+ WIRELESSTAG_TYPE_WEMO_DEVICE,
+ SIGNAL_BINARY_EVENT_UPDATE,
+ WirelessTagBaseSensor)
+from homeassistant.const import (
+ CONF_MONITORED_CONDITIONS, STATE_ON, STATE_OFF)
+import homeassistant.helpers.config_validation as cv
+
+DEPENDENCIES = ['wirelesstag']
+
+_LOGGER = logging.getLogger(__name__)
+
+# On means in range, Off means out of range
+SENSOR_PRESENCE = 'presence'
+
+# On means motion detected, Off means cear
+SENSOR_MOTION = 'motion'
+
+# On means open, Off means closed
+SENSOR_DOOR = 'door'
+
+# On means temperature become too cold, Off means normal
+SENSOR_COLD = 'cold'
+
+# On means hot, Off means normal
+SENSOR_HEAT = 'heat'
+
+# On means too dry (humidity), Off means normal
+SENSOR_DRY = 'dry'
+
+# On means too wet (humidity), Off means normal
+SENSOR_WET = 'wet'
+
+# On means light detected, Off means no light
+SENSOR_LIGHT = 'light'
+
+# On means moisture detected (wet), Off means no moisture (dry)
+SENSOR_MOISTURE = 'moisture'
+
+# On means tag battery is low, Off means normal
+SENSOR_BATTERY = 'low_battery'
+
+# Sensor types: Name, device_class, push notification type representing 'on',
+# attr to check
+SENSOR_TYPES = {
+ SENSOR_PRESENCE: ['Presence', 'presence', 'is_in_range', {
+ "on": "oor",
+ "off": "back_in_range"
+ }, 2],
+ SENSOR_MOTION: ['Motion', 'motion', 'is_moved', {
+ "on": "motion_detected",
+ }, 5],
+ SENSOR_DOOR: ['Door', 'door', 'is_door_open', {
+ "on": "door_opened",
+ "off": "door_closed"
+ }, 5],
+ SENSOR_COLD: ['Cold', 'cold', 'is_cold', {
+ "on": "temp_toolow",
+ "off": "temp_normal"
+ }, 4],
+ SENSOR_HEAT: ['Heat', 'heat', 'is_heat', {
+ "on": "temp_toohigh",
+ "off": "temp_normal"
+ }, 4],
+ SENSOR_DRY: ['Too dry', 'dry', 'is_too_dry', {
+ "on": "too_dry",
+ "off": "cap_normal"
+ }, 2],
+ SENSOR_WET: ['Too wet', 'wet', 'is_too_humid', {
+ "on": "too_humid",
+ "off": "cap_normal"
+ }, 2],
+ SENSOR_LIGHT: ['Light', 'light', 'is_light_on', {
+ "on": "too_bright",
+ "off": "light_normal"
+ }, 1],
+ SENSOR_MOISTURE: ['Leak', 'moisture', 'is_leaking', {
+ "on": "water_detected",
+ "off": "water_dried",
+ }, 1],
+ SENSOR_BATTERY: ['Low Battery', 'battery', 'is_battery_low', {
+ "on": "low_battery"
+ }, 3]
+}
+
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_MONITORED_CONDITIONS, default=[]):
+ vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
+})
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the platform for a WirelessTags."""
+ platform = hass.data.get(WIRELESSTAG_DOMAIN)
+
+ sensors = []
+ tags = platform.tags
+ for tag in tags.values():
+ allowed_sensor_types = WirelessTagBinarySensor.allowed_sensors(tag)
+ for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
+ if sensor_type in allowed_sensor_types:
+ sensors.append(WirelessTagBinarySensor(platform, tag,
+ sensor_type))
+
+ add_devices(sensors, True)
+ hass.add_job(platform.install_push_notifications, sensors)
+
+
+class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice):
+ """A binary sensor implementation for WirelessTags."""
+
+ @classmethod
+ def allowed_sensors(cls, tag):
+ """Return list of allowed sensor types for specific tag type."""
+ sensors_map = {
+ # 13-bit tag - allows everything but not light and moisture
+ WIRELESSTAG_TYPE_13BIT: [
+ SENSOR_PRESENCE, SENSOR_BATTERY,
+ SENSOR_MOTION, SENSOR_DOOR,
+ SENSOR_COLD, SENSOR_HEAT,
+ SENSOR_DRY, SENSOR_WET],
+
+ # Moister/water sensor - temperature and moisture only
+ WIRELESSTAG_TYPE_WATER: [
+ SENSOR_PRESENCE, SENSOR_BATTERY,
+ SENSOR_COLD, SENSOR_HEAT,
+ SENSOR_MOISTURE],
+
+ # ALS Pro: allows everything, but not moisture
+ WIRELESSTAG_TYPE_ALSPRO: [
+ SENSOR_PRESENCE, SENSOR_BATTERY,
+ SENSOR_MOTION, SENSOR_DOOR,
+ SENSOR_COLD, SENSOR_HEAT,
+ SENSOR_DRY, SENSOR_WET,
+ SENSOR_LIGHT],
+
+ # Wemo are power switches.
+ WIRELESSTAG_TYPE_WEMO_DEVICE: [SENSOR_PRESENCE]
+ }
+
+ # allow everything if tag type is unknown
+ # (i just dont have full catalog of them :))
+ tag_type = tag.tag_type
+ fullset = SENSOR_TYPES.keys()
+ return sensors_map[tag_type] if tag_type in sensors_map else fullset
+
+ def __init__(self, api, tag, sensor_type):
+ """Initialize a binary sensor for a Wireless Sensor Tags."""
+ super().__init__(api, tag)
+ self._sensor_type = sensor_type
+ self._name = '{0} {1}'.format(self._tag.name,
+ SENSOR_TYPES[self._sensor_type][0])
+ self._device_class = SENSOR_TYPES[self._sensor_type][1]
+ self._tag_attr = SENSOR_TYPES[self._sensor_type][2]
+ self.binary_spec = SENSOR_TYPES[self._sensor_type][3]
+ self.tag_id_index_template = SENSOR_TYPES[self._sensor_type][4]
+
+ async def async_added_to_hass(self):
+ """Register callbacks."""
+ tag_id = self.tag_id
+ event_type = self.device_class
+ async_dispatcher_connect(
+ self.hass,
+ SIGNAL_BINARY_EVENT_UPDATE.format(tag_id, event_type),
+ self._on_binary_event_callback)
+
+ @property
+ def is_on(self):
+ """Return True if the binary sensor is on."""
+ return self._state == STATE_ON
+
+ @property
+ def device_class(self):
+ """Return the class of the binary sensor."""
+ return self._device_class
+
+ @property
+ def principal_value(self):
+ """Return value of tag.
+
+ Subclasses need override based on type of sensor.
+ """
+ return (
+ STATE_ON if getattr(self._tag, self._tag_attr, False)
+ else STATE_OFF)
+
+ def updated_state_value(self):
+ """Use raw princial value."""
+ return self.principal_value
+
+ @callback
+ def _on_binary_event_callback(self, event):
+ """Update state from arrive push notification."""
+ # state should be 'on' or 'off'
+ self._state = event.data.get('state')
+ self.async_schedule_update_ha_state()
diff --git a/homeassistant/components/binary_sensor/xiaomi_aqara.py b/homeassistant/components/binary_sensor/xiaomi_aqara.py
index 72a4cfdfbaaa8b..be5d9a689d1194 100644
--- a/homeassistant/components/binary_sensor/xiaomi_aqara.py
+++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py
@@ -28,7 +28,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if model in ['motion', 'sensor_motion', 'sensor_motion.aq2']:
devices.append(XiaomiMotionSensor(device, hass, gateway))
elif model in ['magnet', 'sensor_magnet', 'sensor_magnet.aq2']:
- devices.append(XiaomiDoorSensor(device, gateway))
+ if 'proto' not in device or int(device['proto'][0:1]) == 1:
+ data_key = 'status'
+ else:
+ data_key = 'window_status'
+ devices.append(XiaomiDoorSensor(device, data_key, gateway))
elif model == 'sensor_wleak.aq1':
devices.append(XiaomiWaterLeakSensor(device, gateway))
elif model in ['smoke', 'sensor_smoke']:
@@ -43,10 +47,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
data_key = 'channel_0'
devices.append(XiaomiButton(device, 'Switch', data_key,
hass, gateway))
- elif model in ['86sw1', 'sensor_86sw1.aq1']:
+ elif model in ['86sw1', 'sensor_86sw1', 'sensor_86sw1.aq1']:
devices.append(XiaomiButton(device, 'Wall Switch', 'channel_0',
hass, gateway))
- elif model in ['86sw2', 'sensor_86sw2.aq1']:
+ elif model in ['86sw2', 'sensor_86sw2', 'sensor_86sw2.aq1']:
devices.append(XiaomiButton(device, 'Wall Switch (Left)',
'channel_0', hass, gateway))
devices.append(XiaomiButton(device, 'Wall Switch (Right)',
@@ -190,11 +194,11 @@ def parse_data(self, data, raw_data):
class XiaomiDoorSensor(XiaomiBinarySensor):
"""Representation of a XiaomiDoorSensor."""
- def __init__(self, device, xiaomi_hub):
+ def __init__(self, device, data_key, xiaomi_hub):
"""Initialize the XiaomiDoorSensor."""
self._open_since = 0
XiaomiBinarySensor.__init__(self, device, 'Door Window Sensor',
- xiaomi_hub, 'status', 'opening')
+ xiaomi_hub, data_key, 'opening')
@property
def device_state_attributes(self):
@@ -330,7 +334,7 @@ def parse_data(self, data, raw_data):
click_type = 'both'
elif value == 'shake':
click_type = 'shake'
- elif value == 'long_click':
+ elif value in ['long_click', 'long_both_click']:
return False
else:
_LOGGER.warning("Unsupported click_type detected: %s", value)
diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py
index 6931355ca0e207..224d694e0f5b63 100644
--- a/homeassistant/components/binary_sensor/zha.py
+++ b/homeassistant/components/binary_sensor/zha.py
@@ -187,8 +187,8 @@ def cluster_command(self, tsn, command_id, args):
if args[0] == 0xff:
rate = 10 # Should read default move rate
self._entity.move_level(-rate if args[0] else rate)
- elif command_id == 0x0002: # step
- # Step (technically shouldn't change on/off)
+ elif command_id in (0x0002, 0x0006): # step, -with_on_off
+ # Step (technically may change on/off)
self._entity.move_level(-args[1] if args[0] else args[1])
def attribute_update(self, attrid, value):
diff --git a/homeassistant/components/bloomsky.py b/homeassistant/components/bloomsky.py
index f04e0af7be9aba..bc9d3acf54fe60 100644
--- a/homeassistant/components/bloomsky.py
+++ b/homeassistant/components/bloomsky.py
@@ -34,7 +34,6 @@
}, extra=vol.ALLOW_EXTRA)
-# pylint: disable=unused-argument
def setup(hass, config):
"""Set up the BloomSky component."""
api_key = config[DOMAIN][CONF_API_KEY]
diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py
index 5198381b9767a3..9716e46bc032af 100644
--- a/homeassistant/components/calendar/__init__.py
+++ b/homeassistant/components/calendar/__init__.py
@@ -4,11 +4,12 @@
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/calendar/
"""
-import asyncio
import logging
from datetime import timedelta
import re
+from aiohttp import web
+
from homeassistant.components.google import (
CONF_OFFSET, CONF_DEVICE_ID, CONF_NAME)
from homeassistant.const import STATE_OFF, STATE_ON
@@ -18,23 +19,32 @@
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.template import DATE_STR_FORMAT
from homeassistant.util import dt
+from homeassistant.components import http
+
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'calendar'
+DEPENDENCIES = ['http']
+
ENTITY_ID_FORMAT = DOMAIN + '.{}'
SCAN_INTERVAL = timedelta(seconds=60)
-@asyncio.coroutine
-def async_setup(hass, config):
+async def async_setup(hass, config):
"""Track states and offer events for calendars."""
component = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, DOMAIN)
- yield from component.async_setup(config)
+ hass.http.register_view(CalendarListView(component))
+ hass.http.register_view(CalendarEventView(component))
+
+ await hass.components.frontend.async_register_built_in_panel(
+ 'calendar', 'calendar', 'hass:calendar')
+
+ await component.async_setup(config)
return True
@@ -42,7 +52,14 @@ def async_setup(hass, config):
DEFAULT_CONF_OFFSET = '!!'
-# pylint: disable=too-many-instance-attributes
+def get_date(date):
+ """Get the dateTime from date or dateTime as a local."""
+ if 'date' in date:
+ return dt.start_of_local_day(dt.dt.datetime.combine(
+ dt.parse_date(date['date']), dt.dt.time.min))
+ return dt.as_local(dt.parse_datetime(date['dateTime']))
+
+
class CalendarEventDevice(Entity):
"""A calendar event device."""
@@ -50,7 +67,6 @@ class CalendarEventDevice(Entity):
# 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)
@@ -144,15 +160,8 @@ def update(self):
self.cleanup()
return
- def _get_date(date):
- """Get the dateTime from date or dateTime as a local."""
- if 'date' in date:
- return dt.start_of_local_day(dt.dt.datetime.combine(
- dt.parse_date(date['date']), dt.dt.time.min))
- return dt.as_local(dt.parse_datetime(date['dateTime']))
-
- start = _get_date(self.data.event['start'])
- end = _get_date(self.data.event['end'])
+ start = get_date(self.data.event['start'])
+ end = get_date(self.data.event['end'])
summary = self.data.event.get('summary', '')
@@ -176,10 +185,61 @@ def _get_date(date):
# 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']
+
+
+class CalendarEventView(http.HomeAssistantView):
+ """View to retrieve calendar content."""
+
+ url = '/api/calendars/{entity_id}'
+ name = 'api:calendars:calendar'
+
+ def __init__(self, component):
+ """Initialize calendar view."""
+ self.component = component
+
+ async def get(self, request, entity_id):
+ """Return calendar events."""
+ entity = self.component.get_entity(entity_id)
+ start = request.query.get('start')
+ end = request.query.get('end')
+ if None in (start, end, entity):
+ return web.Response(status=400)
+ try:
+ start_date = dt.parse_datetime(start)
+ end_date = dt.parse_datetime(end)
+ except (ValueError, AttributeError):
+ return web.Response(status=400)
+ event_list = await entity.async_get_events(
+ request.app['hass'], start_date, end_date)
+ return self.json(event_list)
+
+
+class CalendarListView(http.HomeAssistantView):
+ """View to retrieve calendar list."""
+
+ url = '/api/calendars'
+ name = "api:calendars"
+
+ def __init__(self, component):
+ """Initialize calendar view."""
+ self.component = component
+
+ async def get(self, request):
+ """Retrieve calendar list."""
+ get_state = request.app['hass'].states.get
+ calendar_list = []
+
+ for entity in self.component.entities:
+ state = get_state(entity.entity_id)
+ calendar_list.append({
+ "name": state.name,
+ "entity_id": entity.entity_id,
+ })
+
+ return self.json(sorted(calendar_list, key=lambda x: x['name']))
diff --git a/homeassistant/components/calendar/caldav.py b/homeassistant/components/calendar/caldav.py
index 6f92891c551d76..9c30d1481f8af9 100644
--- a/homeassistant/components/calendar/caldav.py
+++ b/homeassistant/components/calendar/caldav.py
@@ -11,7 +11,7 @@
import voluptuous as vol
from homeassistant.components.calendar import (
- PLATFORM_SCHEMA, CalendarEventDevice)
+ PLATFORM_SCHEMA, CalendarEventDevice, get_date)
from homeassistant.const import (
CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME)
import homeassistant.helpers.config_validation as cv
@@ -92,7 +92,7 @@ def setup_platform(hass, config, add_devices, disc_info=None):
if not config.get(CONF_CUSTOM_CALENDARS):
device_data = {
CONF_NAME: calendar.name,
- CONF_DEVICE_ID: calendar.name
+ CONF_DEVICE_ID: calendar.name,
}
calendar_devices.append(
WebDavCalendarEventDevice(hass, device_data, calendar)
@@ -120,6 +120,10 @@ def device_state_attributes(self):
attributes = super().device_state_attributes
return attributes
+ async def async_get_events(self, hass, start_date, end_date):
+ """Get all events in a specific time frame."""
+ return await self.data.async_get_events(hass, start_date, end_date)
+
class WebDavCalendarData(object):
"""Class to utilize the calendar dav client object to get next event."""
@@ -131,6 +135,33 @@ def __init__(self, calendar, include_all_day, search):
self.search = search
self.event = None
+ async def async_get_events(self, hass, start_date, end_date):
+ """Get all events in a specific time frame."""
+ # Get event list from the current calendar
+ vevent_list = await hass.async_add_job(self.calendar.date_search,
+ start_date, end_date)
+ event_list = []
+ for event in vevent_list:
+ vevent = event.instance.vevent
+ uid = None
+ if hasattr(vevent, 'uid'):
+ uid = vevent.uid.value
+ data = {
+ "uid": uid,
+ "title": vevent.summary.value,
+ "start": self.get_hass_date(vevent.dtstart.value),
+ "end": self.get_hass_date(self.get_end_date(vevent)),
+ "location": self.get_attr_value(vevent, "location"),
+ "description": self.get_attr_value(vevent, "description"),
+ }
+
+ data['start'] = get_date(data['start']).isoformat()
+ data['end'] = get_date(data['end']).isoformat()
+
+ event_list.append(data)
+
+ return event_list
+
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data."""
diff --git a/homeassistant/components/calendar/demo.py b/homeassistant/components/calendar/demo.py
index 7823f03c85ecf4..53129d3316cf60 100644
--- a/homeassistant/components/calendar/demo.py
+++ b/homeassistant/components/calendar/demo.py
@@ -4,8 +4,10 @@
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
+import copy
+
import homeassistant.util.dt as dt_util
-from homeassistant.components.calendar import CalendarEventDevice
+from homeassistant.components.calendar import CalendarEventDevice, get_date
from homeassistant.components.google import CONF_DEVICE_ID, CONF_NAME
@@ -15,13 +17,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
calendar_data_current = DemoGoogleCalendarDataCurrent()
add_devices([
DemoGoogleCalendar(hass, calendar_data_future, {
- CONF_NAME: 'Future Event',
- CONF_DEVICE_ID: 'future_event',
+ CONF_NAME: 'Calendar 1',
+ CONF_DEVICE_ID: 'calendar_1',
}),
DemoGoogleCalendar(hass, calendar_data_current, {
- CONF_NAME: 'Current Event',
- CONF_DEVICE_ID: 'current_event',
+ CONF_NAME: 'Calendar 2',
+ CONF_DEVICE_ID: 'calendar_2',
}),
])
@@ -29,11 +31,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class DemoGoogleCalendarData(object):
"""Representation of a Demo Calendar element."""
+ event = {}
+
# pylint: disable=no-self-use
def update(self):
"""Return true so entity knows we have new data."""
return True
+ async def async_get_events(self, hass, start_date, end_date):
+ """Get all events in a specific time frame."""
+ event = copy.copy(self.event)
+ event['title'] = event['summary']
+ event['start'] = get_date(event['start']).isoformat()
+ event['end'] = get_date(event['end']).isoformat()
+ return [event]
+
class DemoGoogleCalendarDataFuture(DemoGoogleCalendarData):
"""Representation of a Demo Calendar for a future event."""
@@ -80,3 +92,7 @@ def __init__(self, hass, calendar_data, data):
"""Initialize Google Calendar but without the API calls."""
self.data = calendar_data
super().__init__(hass, data)
+
+ async def async_get_events(self, hass, start_date, end_date):
+ """Get all events in a specific time frame."""
+ return await self.data.async_get_events(hass, start_date, end_date)
diff --git a/homeassistant/components/calendar/google.py b/homeassistant/components/calendar/google.py
index 6c26c65ebe77fd..da76530a36d634 100644
--- a/homeassistant/components/calendar/google.py
+++ b/homeassistant/components/calendar/google.py
@@ -51,6 +51,10 @@ def __init__(self, hass, calendar_service, calendar, data):
super().__init__(hass, data)
+ async def async_get_events(self, hass, start_date, end_date):
+ """Get all events in a specific time frame."""
+ return await self.data.async_get_events(hass, start_date, end_date)
+
class GoogleCalendarData(object):
"""Class to utilize calendar service object to get next event."""
@@ -64,9 +68,7 @@ def __init__(self, calendar_service, calendar_id, search,
self.ignore_availability = ignore_availability
self.event = None
- @Throttle(MIN_TIME_BETWEEN_UPDATES)
- def update(self):
- """Get the latest data."""
+ def _prepare_query(self):
from httplib2 import ServerNotFoundError
try:
@@ -74,13 +76,41 @@ def update(self):
except ServerNotFoundError:
_LOGGER.warning("Unable to connect to Google, using cached data")
return False
-
params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS)
- params['timeMin'] = dt.now().isoformat('T')
params['calendarId'] = self.calendar_id
if self.search:
params['q'] = self.search
+ return service, params
+
+ async def async_get_events(self, hass, start_date, end_date):
+ """Get all events in a specific time frame."""
+ service, params = await hass.async_add_job(self._prepare_query)
+ params['timeMin'] = start_date.isoformat('T')
+ params['timeMax'] = end_date.isoformat('T')
+
+ # pylint: disable=no-member
+ events = await hass.async_add_job(service.events)
+ # pylint: enable=no-member
+ result = await hass.async_add_job(events.list(**params).execute)
+
+ items = result.get('items', [])
+ event_list = []
+ for item in items:
+ if (not self.ignore_availability
+ and 'transparency' in item.keys()):
+ if item['transparency'] == 'opaque':
+ event_list.append(item)
+ else:
+ event_list.append(item)
+ return event_list
+
+ @Throttle(MIN_TIME_BETWEEN_UPDATES)
+ def update(self):
+ """Get the latest data."""
+ service, params = self._prepare_query()
+ params['timeMin'] = dt.now().isoformat('T')
+
events = service.events() # pylint: disable=no-member
result = events.list(**params).execute()
diff --git a/homeassistant/components/calendar/todoist.py b/homeassistant/components/calendar/todoist.py
index b70e44456db822..71a6a17de107a8 100644
--- a/homeassistant/components/calendar/todoist.py
+++ b/homeassistant/components/calendar/todoist.py
@@ -257,6 +257,10 @@ def cleanup(self):
super().cleanup()
self._cal_data[ALL_TASKS] = []
+ async def async_get_events(self, hass, start_date, end_date):
+ """Get all events in a specific time frame."""
+ return await self.data.async_get_events(hass, start_date, end_date)
+
@property
def device_state_attributes(self):
"""Return the device state attributes."""
@@ -485,6 +489,31 @@ def select_best_task(project_tasks):
continue
return event
+ async def async_get_events(self, hass, start_date, end_date):
+ """Get all tasks in a specific time frame."""
+ if self._id is None:
+ project_task_data = [
+ task for task in self._api.state[TASKS]
+ if not self._project_id_whitelist or
+ task[PROJECT_ID] in self._project_id_whitelist]
+ else:
+ project_task_data = self._api.projects.get_data(self._id)[TASKS]
+
+ events = []
+ time_format = '%a %d %b %Y %H:%M:%S %z'
+ for task in project_task_data:
+ due_date = datetime.strptime(task['due_date_utc'], time_format)
+ if due_date > start_date and due_date < end_date:
+ event = {
+ 'uid': task['id'],
+ 'title': task['content'],
+ 'start': due_date.isoformat(),
+ 'end': due_date.isoformat(),
+ 'allDay': True,
+ }
+ events.append(event)
+ return events
+
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data."""
diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py
index 60f8979bb16d91..ebda09de20cd35 100644
--- a/homeassistant/components/camera/__init__.py
+++ b/homeassistant/components/camera/__init__.py
@@ -1,4 +1,3 @@
-# pylint: disable=too-many-lines
"""
Component to interface with cameras.
@@ -97,6 +96,7 @@ def disable_motion_detection(hass, entity_id=None):
@bind_hass
+@callback
def async_snapshot(hass, filename, entity_id=None):
"""Make a snapshot from a camera."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
@@ -129,8 +129,7 @@ async def async_get_image(hass, entity_id, timeout=10):
raise HomeAssistantError('Unable to get image')
-@asyncio.coroutine
-def async_setup(hass, config):
+async def async_setup(hass, config):
"""Set up the camera component."""
component = hass.data[DOMAIN] = \
EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
@@ -142,7 +141,7 @@ def async_setup(hass, config):
SCHEMA_WS_CAMERA_THUMBNAIL
)
- yield from component.async_setup(config)
+ await component.async_setup(config)
@callback
def update_tokens(time):
@@ -154,27 +153,25 @@ def update_tokens(time):
hass.helpers.event.async_track_time_interval(
update_tokens, TOKEN_CHANGE_INTERVAL)
- @asyncio.coroutine
- def async_handle_camera_service(service):
+ async def async_handle_camera_service(service):
"""Handle calls to the camera services."""
target_cameras = component.async_extract_from_service(service)
update_tasks = []
for camera in target_cameras:
if service.service == SERVICE_ENABLE_MOTION:
- yield from camera.async_enable_motion_detection()
+ await camera.async_enable_motion_detection()
elif service.service == SERVICE_DISABLE_MOTION:
- yield from camera.async_disable_motion_detection()
+ await camera.async_disable_motion_detection()
if not camera.should_poll:
continue
update_tasks.append(camera.async_update_ha_state(True))
if update_tasks:
- yield from asyncio.wait(update_tasks, loop=hass.loop)
+ await asyncio.wait(update_tasks, loop=hass.loop)
- @asyncio.coroutine
- def async_handle_snapshot_service(service):
+ async def async_handle_snapshot_service(service):
"""Handle snapshot services calls."""
target_cameras = component.async_extract_from_service(service)
filename = service.data[ATTR_FILENAME]
@@ -190,7 +187,7 @@ def async_handle_snapshot_service(service):
"Can't write %s, no access to path!", snapshot_file)
continue
- image = yield from camera.async_camera_image()
+ image = await camera.async_camera_image()
def _write_image(to_file, image_data):
"""Executor helper to write image."""
@@ -198,7 +195,7 @@ def _write_image(to_file, image_data):
img_file.write(image_data)
try:
- yield from hass.async_add_job(
+ await hass.async_add_job(
_write_image, snapshot_file, image)
except OSError as err:
_LOGGER.error("Can't write image to file: %s", err)
@@ -216,6 +213,16 @@ def _write_image(to_file, image_data):
return True
+async def async_setup_entry(hass, entry):
+ """Setup a config entry."""
+ return await hass.data[DOMAIN].async_setup_entry(entry)
+
+
+async def async_unload_entry(hass, entry):
+ """Unload a config entry."""
+ return await hass.data[DOMAIN].async_unload_entry(entry)
+
+
class Camera(Entity):
"""The base class for camera entities."""
@@ -265,6 +272,7 @@ def camera_image(self):
"""Return bytes of camera image."""
raise NotImplementedError()
+ @callback
def async_camera_image(self):
"""Return bytes of camera image.
@@ -388,8 +396,7 @@ def __init__(self, component):
"""Initialize a basic camera view."""
self.component = component
- @asyncio.coroutine
- def get(self, request, entity_id):
+ async def get(self, request, entity_id):
"""Start a GET request."""
camera = self.component.get_entity(entity_id)
@@ -403,11 +410,10 @@ def get(self, request, entity_id):
if not authenticated:
return web.Response(status=401)
- response = yield from self.handle(request, camera)
+ response = await self.handle(request, camera)
return response
- @asyncio.coroutine
- def handle(self, request, camera):
+ async def handle(self, request, camera):
"""Handle the camera request."""
raise NotImplementedError()
@@ -418,12 +424,11 @@ class CameraImageView(CameraView):
url = '/api/camera_proxy/{entity_id}'
name = 'api:camera:image'
- @asyncio.coroutine
- def handle(self, request, camera):
+ async def handle(self, request, camera):
"""Serve camera image."""
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
with async_timeout.timeout(10, loop=request.app['hass'].loop):
- image = yield from camera.async_camera_image()
+ image = await camera.async_camera_image()
if image:
return web.Response(body=image,
diff --git a/homeassistant/components/camera/arlo.py b/homeassistant/components/camera/arlo.py
index f3e70c2bdd74c9..1a98ade55183ea 100644
--- a/homeassistant/components/camera/arlo.py
+++ b/homeassistant/components/camera/arlo.py
@@ -4,23 +4,22 @@
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.arlo/
"""
-import asyncio
import logging
-from datetime import timedelta
import voluptuous as vol
+from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
-from homeassistant.components.arlo import DEFAULT_BRAND, DATA_ARLO
+from homeassistant.components.arlo import (
+ DEFAULT_BRAND, DATA_ARLO, SIGNAL_UPDATE_ARLO)
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
_LOGGER = logging.getLogger(__name__)
-SCAN_INTERVAL = timedelta(seconds=90)
-
ARLO_MODE_ARMED = 'armed'
ARLO_MODE_DISARMED = 'disarmed'
@@ -44,22 +43,19 @@
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
- vol.Optional(CONF_FFMPEG_ARGUMENTS):
- cv.string,
+ vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up an Arlo IP Camera."""
- arlo = hass.data.get(DATA_ARLO)
- if not arlo:
- return False
+ arlo = hass.data[DATA_ARLO]
cameras = []
for camera in arlo.cameras:
cameras.append(ArloCam(hass, camera, config))
- add_devices(cameras, True)
+ add_devices(cameras)
class ArloCam(Camera):
@@ -74,31 +70,41 @@ def __init__(self, hass, camera, device_info):
self._ffmpeg = hass.data[DATA_FFMPEG]
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
self._last_refresh = None
- if self._camera.base_station:
- self._camera.base_station.refresh_rate = \
- SCAN_INTERVAL.total_seconds()
self.attrs = {}
def camera_image(self):
"""Return a still image response from the camera."""
- return self._camera.last_image
+ return self._camera.last_image_from_cache
+
+ async def async_added_to_hass(self):
+ """Register callbacks."""
+ async_dispatcher_connect(
+ self.hass, SIGNAL_UPDATE_ARLO, self._update_callback)
+
+ @callback
+ def _update_callback(self):
+ """Call update method."""
+ self.async_schedule_update_ha_state()
- @asyncio.coroutine
- def handle_async_mjpeg_stream(self, request):
+ async def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera."""
from haffmpeg import CameraMjpeg
video = self._camera.last_video
if not video:
+ error_msg = \
+ 'Video not found for {0}. Is it older than {1} days?'.format(
+ self.name, self._camera.min_days_vdo_cache)
+ _LOGGER.error(error_msg)
return
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
- yield from stream.open_camera(
+ await stream.open_camera(
video.video_url, extra_cmd=self._ffmpeg_arguments)
- yield from async_aiohttp_proxy_stream(
+ await async_aiohttp_proxy_stream(
self.hass, request, stream,
'multipart/x-mixed-replace;boundary=ffserver')
- yield from stream.close()
+ await stream.close()
@property
def name(self):
@@ -132,11 +138,6 @@ def brand(self):
"""Return the camera brand."""
return DEFAULT_BRAND
- @property
- def should_poll(self):
- """Camera should poll periodically."""
- return True
-
@property
def motion_detection_enabled(self):
"""Return the camera motion detection status."""
@@ -164,7 +165,3 @@ def disable_motion_detection(self):
"""Disable the motion detection in base station (Disarm)."""
self._motion_status = False
self.set_base_station_mode(ARLO_MODE_DISARMED)
-
- def update(self):
- """Add an attribute-update task to the executor pool."""
- self._camera.update()
diff --git a/homeassistant/components/camera/bloomsky.py b/homeassistant/components/camera/bloomsky.py
index ef70692215dfbd..775289926745e6 100644
--- a/homeassistant/components/camera/bloomsky.py
+++ b/homeassistant/components/camera/bloomsky.py
@@ -13,7 +13,6 @@
DEPENDENCIES = ['bloomsky']
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up access to BloomSky cameras."""
bloomsky = hass.components.bloomsky
diff --git a/homeassistant/components/camera/demo.py b/homeassistant/components/camera/demo.py
index d009f156e9d225..3c1477d1828658 100644
--- a/homeassistant/components/camera/demo.py
+++ b/homeassistant/components/camera/demo.py
@@ -12,9 +12,10 @@
_LOGGER = logging.getLogger(__name__)
-def setup_platform(hass, config, add_devices, discovery_info=None):
+async def async_setup_platform(hass, config, async_add_devices,
+ discovery_info=None):
"""Set up the Demo camera platform."""
- add_devices([
+ async_add_devices([
DemoCamera(hass, config, 'Demo camera')
])
diff --git a/homeassistant/components/camera/doorbird.py b/homeassistant/components/camera/doorbird.py
index 034ddc2fabbe0a..6680258d95d253 100644
--- a/homeassistant/components/camera/doorbird.py
+++ b/homeassistant/components/camera/doorbird.py
@@ -17,9 +17,9 @@
DEPENDENCIES = ['doorbird']
-_CAMERA_LAST_VISITOR = "DoorBird Last Ring"
-_CAMERA_LAST_MOTION = "DoorBird Last Motion"
-_CAMERA_LIVE = "DoorBird Live"
+_CAMERA_LAST_VISITOR = "{} Last Ring"
+_CAMERA_LAST_MOTION = "{} Last Motion"
+_CAMERA_LIVE = "{} Live"
_LAST_VISITOR_INTERVAL = datetime.timedelta(minutes=1)
_LAST_MOTION_INTERVAL = datetime.timedelta(minutes=1)
_LIVE_INTERVAL = datetime.timedelta(seconds=1)
@@ -30,16 +30,22 @@
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the DoorBird camera platform."""
- device = hass.data.get(DOORBIRD_DOMAIN)
- async_add_devices([
- DoorBirdCamera(device.live_image_url, _CAMERA_LIVE, _LIVE_INTERVAL),
- DoorBirdCamera(
- device.history_image_url(1, 'doorbell'), _CAMERA_LAST_VISITOR,
- _LAST_VISITOR_INTERVAL),
- DoorBirdCamera(
- device.history_image_url(1, 'motionsensor'), _CAMERA_LAST_MOTION,
- _LAST_MOTION_INTERVAL),
- ])
+ for doorstation in hass.data[DOORBIRD_DOMAIN]:
+ device = doorstation.device
+ async_add_devices([
+ DoorBirdCamera(
+ device.live_image_url,
+ _CAMERA_LIVE.format(doorstation.name),
+ _LIVE_INTERVAL),
+ DoorBirdCamera(
+ device.history_image_url(1, 'doorbell'),
+ _CAMERA_LAST_VISITOR.format(doorstation.name),
+ _LAST_VISITOR_INTERVAL),
+ DoorBirdCamera(
+ device.history_image_url(1, 'motionsensor'),
+ _CAMERA_LAST_MOTION.format(doorstation.name),
+ _LAST_MOTION_INTERVAL),
+ ])
class DoorBirdCamera(Camera):
diff --git a/homeassistant/components/camera/foscam.py b/homeassistant/components/camera/foscam.py
index 15db83d345a93e..4ea733139a90b6 100644
--- a/homeassistant/components/camera/foscam.py
+++ b/homeassistant/components/camera/foscam.py
@@ -33,7 +33,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up a Foscam IP Camera."""
add_devices([FoscamCam(config)])
diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py
index e11bd599e45e70..911c14e72325b0 100644
--- a/homeassistant/components/camera/generic.py
+++ b/homeassistant/components/camera/generic.py
@@ -46,7 +46,6 @@
@asyncio.coroutine
-# pylint: disable=unused-argument
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up a generic IP Camera."""
async_add_devices([GenericCamera(hass, config)])
diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py
index 35d30104f6e66d..a5ed0cdc02c492 100644
--- a/homeassistant/components/camera/mjpeg.py
+++ b/homeassistant/components/camera/mjpeg.py
@@ -42,7 +42,6 @@
@asyncio.coroutine
-# pylint: disable=unused-argument
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up a MJPEG IP Camera."""
if discovery_info:
diff --git a/homeassistant/components/camera/neato.py b/homeassistant/components/camera/neato.py
index 33bd00caa6bf68..689129e1067ff9 100644
--- a/homeassistant/components/camera/neato.py
+++ b/homeassistant/components/camera/neato.py
@@ -45,7 +45,7 @@ def camera_image(self):
self.update()
return self._image
- @Throttle(timedelta(seconds=10))
+ @Throttle(timedelta(seconds=60))
def update(self):
"""Check the contents of the map list."""
self.neato.update_robots()
diff --git a/homeassistant/components/camera/nest.py b/homeassistant/components/camera/nest.py
index 6ffb7ef85619f8..ab26df5caf00da 100644
--- a/homeassistant/components/camera/nest.py
+++ b/homeassistant/components/camera/nest.py
@@ -23,14 +23,19 @@
def setup_platform(hass, config, add_devices, discovery_info=None):
- """Set up a Nest Cam."""
- if discovery_info is None:
- return
+ """Set up a Nest Cam.
- camera_devices = hass.data[nest.DATA_NEST].cameras()
+ No longer in use.
+ """
+
+
+async def async_setup_entry(hass, entry, async_add_devices):
+ """Set up a Nest sensor based on a config entry."""
+ camera_devices = \
+ await hass.async_add_job(hass.data[nest.DATA_NEST].cameras)
cameras = [NestCamera(structure, device)
for structure, device in camera_devices]
- add_devices(cameras, True)
+ async_add_devices(cameras, True)
class NestCamera(Camera):
diff --git a/homeassistant/components/camera/netatmo.py b/homeassistant/components/camera/netatmo.py
index bf2dfe39bd8b55..34a78e19f9fce1 100644
--- a/homeassistant/components/camera/netatmo.py
+++ b/homeassistant/components/camera/netatmo.py
@@ -29,13 +29,12 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up access to Netatmo cameras."""
netatmo = hass.components.netatmo
home = config.get(CONF_HOME)
verify_ssl = config.get(CONF_VERIFY_SSL, True)
- import lnetatmo
+ import pyatmo
try:
data = CameraData(netatmo.NETATMO_AUTH, home)
for camera_name in data.get_camera_names():
@@ -46,7 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
continue
add_devices([NetatmoCamera(data, camera_name, home,
camera_type, verify_ssl)])
- except lnetatmo.NoDevice:
+ except pyatmo.NoDevice:
return None
diff --git a/homeassistant/components/camera/uvc.py b/homeassistant/components/camera/uvc.py
index 20dceb8a1c5da2..e992020e2b275a 100644
--- a/homeassistant/components/camera/uvc.py
+++ b/homeassistant/components/camera/uvc.py
@@ -13,6 +13,7 @@
from homeassistant.const import CONF_PORT
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
import homeassistant.helpers.config_validation as cv
+from homeassistant.exceptions import PlatformNotReady
REQUIREMENTS = ['uvcclient==0.10.1']
@@ -41,25 +42,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
port = config[CONF_PORT]
from uvcclient import nvr
- nvrconn = nvr.UVCRemote(addr, port, key)
try:
+ # Exceptions may be raised in all method calls to the nvr library.
+ nvrconn = nvr.UVCRemote(addr, port, key)
cameras = nvrconn.index()
+
+ identifier = 'id' if nvrconn.server_version >= (3, 2, 0) else 'uuid'
+ # Filter out airCam models, which are not supported in the latest
+ # version of UnifiVideo and which are EOL by Ubiquiti
+ cameras = [
+ camera for camera in cameras
+ if 'airCam' not in nvrconn.get_camera(camera[identifier])['model']]
except nvr.NotAuthorized:
_LOGGER.error("Authorization failure while connecting to NVR")
return False
- except nvr.NvrError:
- _LOGGER.error("NVR refuses to talk to me")
- return False
+ except nvr.NvrError as ex:
+ _LOGGER.error("NVR refuses to talk to me: %s", str(ex))
+ raise PlatformNotReady
except requests.exceptions.ConnectionError as ex:
_LOGGER.error("Unable to connect to NVR: %s", str(ex))
- return False
-
- identifier = 'id' if nvrconn.server_version >= (3, 2, 0) else 'uuid'
- # Filter out airCam models, which are not supported in the latest
- # version of UnifiVideo and which are EOL by Ubiquiti
- cameras = [
- camera for camera in cameras
- if 'airCam' not in nvrconn.get_camera(camera[identifier])['model']]
+ raise PlatformNotReady
add_devices([UnifiVideoCamera(nvrconn,
camera[identifier],
diff --git a/homeassistant/components/camera/xiaomi.py b/homeassistant/components/camera/xiaomi.py
new file mode 100644
index 00000000000000..c18a3649e7bbaa
--- /dev/null
+++ b/homeassistant/components/camera/xiaomi.py
@@ -0,0 +1,166 @@
+"""
+This component provides support for Xiaomi Cameras.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/camera.xiaomi/
+"""
+import asyncio
+import logging
+
+import voluptuous as vol
+
+from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
+from homeassistant.components.ffmpeg import DATA_FFMPEG
+from homeassistant.const import (CONF_HOST, CONF_NAME, CONF_PATH,
+ CONF_PASSWORD, CONF_PORT, CONF_USERNAME)
+from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
+
+DEPENDENCIES = ['ffmpeg']
+_LOGGER = logging.getLogger(__name__)
+
+DEFAULT_BRAND = 'Xiaomi Home Camera'
+DEFAULT_PATH = '/media/mmcblk0p1/record'
+DEFAULT_PORT = 21
+DEFAULT_USERNAME = 'root'
+
+CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
+CONF_MODEL = 'model'
+
+MODEL_YI = 'yi'
+MODEL_XIAOFANG = 'xiaofang'
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_NAME): cv.string,
+ vol.Required(CONF_HOST): cv.string,
+ vol.Required(CONF_MODEL): vol.Any(MODEL_YI,
+ MODEL_XIAOFANG),
+ vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string,
+ vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string,
+ vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string
+})
+
+
+async def async_setup_platform(hass,
+ config,
+ async_add_devices,
+ discovery_info=None):
+ """Set up a Xiaomi Camera."""
+ _LOGGER.debug('Received configuration for model %s', config[CONF_MODEL])
+ async_add_devices([XiaomiCamera(hass, config)])
+
+
+class XiaomiCamera(Camera):
+ """Define an implementation of a Xiaomi Camera."""
+
+ def __init__(self, hass, config):
+ """Initialize."""
+ super().__init__()
+ self._extra_arguments = config.get(CONF_FFMPEG_ARGUMENTS)
+ self._last_image = None
+ self._last_url = None
+ self._manager = hass.data[DATA_FFMPEG]
+ self._name = config[CONF_NAME]
+ self.host = config[CONF_HOST]
+ self._model = config[CONF_MODEL]
+ self.port = config[CONF_PORT]
+ self.path = config[CONF_PATH]
+ self.user = config[CONF_USERNAME]
+ self.passwd = config[CONF_PASSWORD]
+
+ @property
+ def name(self):
+ """Return the name of this camera."""
+ return self._name
+
+ @property
+ def brand(self):
+ """Return the camera brand."""
+ return DEFAULT_BRAND
+
+ @property
+ def model(self):
+ """Return the camera model."""
+ return self._model
+
+ def get_latest_video_url(self):
+ """Retrieve the latest video file from the Xiaomi Camera FTP server."""
+ from ftplib import FTP, error_perm
+
+ ftp = FTP(self.host)
+ try:
+ ftp.login(self.user, self.passwd)
+ except error_perm as exc:
+ _LOGGER.error('Camera login failed: %s', exc)
+ return False
+
+ try:
+ ftp.cwd(self.path)
+ except error_perm as exc:
+ _LOGGER.error('Unable to find path: %s - %s', self.path, exc)
+ return False
+
+ dirs = [d for d in ftp.nlst() if '.' not in d]
+ if not dirs:
+ if self._model == MODEL_YI:
+ _LOGGER.warning("There don't appear to be any uploaded videos")
+ return False
+ elif self._model == MODEL_XIAOFANG:
+ _LOGGER.warning("There don't appear to be any folders")
+ return False
+
+ first_dir = dirs[-1]
+ try:
+ ftp.cwd(first_dir)
+ except error_perm as exc:
+ _LOGGER.error('Unable to find path: %s - %s', first_dir, exc)
+ return False
+
+ dirs = [d for d in ftp.nlst() if '.' not in d]
+ if not dirs:
+ _LOGGER.warning("There don't appear to be any uploaded videos")
+ return False
+
+ latest_dir = dirs[-1]
+ ftp.cwd(latest_dir)
+ videos = [v for v in ftp.nlst() if '.tmp' not in v]
+ if not videos:
+ _LOGGER.info('Video folder "%s" is empty; delaying', latest_dir)
+ return False
+
+ if self._model == MODEL_XIAOFANG:
+ video = videos[-2]
+ else:
+ video = videos[-1]
+
+ return 'ftp://{0}:{1}@{2}:{3}{4}/{5}'.format(
+ self.user, self.passwd, self.host, self.port, ftp.pwd(), video)
+
+ async def async_camera_image(self):
+ """Return a still image response from the camera."""
+ from haffmpeg import ImageFrame, IMAGE_JPEG
+
+ url = await self.hass.async_add_job(self.get_latest_video_url)
+ if url != self._last_url:
+ ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop)
+ self._last_image = await asyncio.shield(ffmpeg.get_image(
+ url, output_format=IMAGE_JPEG,
+ extra_cmd=self._extra_arguments), loop=self.hass.loop)
+ self._last_url = url
+
+ return self._last_image
+
+ async def handle_async_mjpeg_stream(self, request):
+ """Generate an HTTP MJPEG stream from the camera."""
+ from haffmpeg import CameraMjpeg
+
+ stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop)
+ await stream.open_camera(
+ self._last_url, extra_cmd=self._extra_arguments)
+
+ await async_aiohttp_proxy_stream(
+ self.hass, request, stream,
+ 'multipart/x-mixed-replace;boundary=ffserver')
+ await stream.close()
diff --git a/homeassistant/components/camera/yi.py b/homeassistant/components/camera/yi.py
index 41fe816c4799c6..868c5afb4473c5 100644
--- a/homeassistant/components/camera/yi.py
+++ b/homeassistant/components/camera/yi.py
@@ -11,11 +11,13 @@
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
from homeassistant.components.ffmpeg import DATA_FFMPEG
-from homeassistant.const import (CONF_HOST, CONF_NAME, CONF_PATH,
- CONF_PASSWORD, CONF_PORT, CONF_USERNAME)
+from homeassistant.const import (
+ CONF_HOST, CONF_NAME, CONF_PATH, CONF_PASSWORD, CONF_PORT, CONF_USERNAME)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
+from homeassistant.exceptions import PlatformNotReady
+REQUIREMENTS = ['aioftp==0.10.1']
DEPENDENCIES = ['ffmpeg']
_LOGGER = logging.getLogger(__name__)
@@ -38,12 +40,9 @@
})
-async def async_setup_platform(hass,
- config,
- async_add_devices,
- discovery_info=None):
+async def async_setup_platform(
+ hass, config, async_add_devices, discovery_info=None):
"""Set up a Yi Camera."""
- _LOGGER.debug('Received configuration: %s', config)
async_add_devices([YiCamera(hass, config)], True)
@@ -54,71 +53,81 @@ def __init__(self, hass, config):
"""Initialize."""
super().__init__()
self._extra_arguments = config.get(CONF_FFMPEG_ARGUMENTS)
+ self._ftp = None
self._last_image = None
self._last_url = None
self._manager = hass.data[DATA_FFMPEG]
- self._name = config.get(CONF_NAME)
- self.host = config.get(CONF_HOST)
- self.port = config.get(CONF_PORT)
- self.path = config.get(CONF_PATH)
- self.user = config.get(CONF_USERNAME)
- self.passwd = config.get(CONF_PASSWORD)
+ self._name = config[CONF_NAME]
+ self.host = config[CONF_HOST]
+ self.port = config[CONF_PORT]
+ self.path = config[CONF_PATH]
+ self.user = config[CONF_USERNAME]
+ self.passwd = config[CONF_PASSWORD]
- @property
- def name(self):
- """Return the name of this camera."""
- return self._name
+ hass.async_add_job(self._connect_to_client)
@property
def brand(self):
"""Camera brand."""
return DEFAULT_BRAND
- def get_latest_video_url(self):
- """Retrieve the latest video file from the customized Yi FTP server."""
- from ftplib import FTP, error_perm
+ @property
+ def name(self):
+ """Return the name of this camera."""
+ return self._name
- ftp = FTP(self.host)
+ async def _connect_to_client(self):
+ """Attempt to establish a connection via FTP."""
+ from aioftp import Client, StatusCodeError
+
+ ftp = Client()
try:
- ftp.login(self.user, self.passwd)
- except error_perm as exc:
- _LOGGER.error('There was an error while logging into the camera')
- _LOGGER.debug(exc)
- return False
+ await ftp.connect(self.host)
+ await ftp.login(self.user, self.passwd)
+ self._ftp = ftp
+ except StatusCodeError as err:
+ raise PlatformNotReady(err)
+
+ async def _get_latest_video_url(self):
+ """Retrieve the latest video file from the customized Yi FTP server."""
+ from aioftp import StatusCodeError
try:
- ftp.cwd(self.path)
- except error_perm as exc:
- _LOGGER.error('Unable to find path: %s', self.path)
- _LOGGER.debug(exc)
- return False
-
- dirs = [d for d in ftp.nlst() if '.' not in d]
- if not dirs:
- _LOGGER.warning("There don't appear to be any uploaded videos")
- return False
-
- latest_dir = dirs[-1]
- ftp.cwd(latest_dir)
- videos = ftp.nlst()
- if not videos:
- _LOGGER.info('Video folder "%s" is empty; delaying', latest_dir)
- return False
-
- return 'ftp://{0}:{1}@{2}:{3}{4}/{5}/{6}'.format(
- self.user, self.passwd, self.host, self.port, self.path,
- latest_dir, videos[-1])
+ await self._ftp.change_directory(self.path)
+ dirs = []
+ for path, attrs in await self._ftp.list():
+ if attrs['type'] == 'dir' and '.' not in str(path):
+ dirs.append(path)
+ latest_dir = dirs[-1]
+ await self._ftp.change_directory(latest_dir)
+
+ videos = []
+ for path, _ in await self._ftp.list():
+ videos.append(path)
+ if not videos:
+ _LOGGER.info('Video folder "%s" empty; delaying', latest_dir)
+ return None
+
+ return 'ftp://{0}:{1}@{2}:{3}{4}/{5}/{6}'.format(
+ self.user, self.passwd, self.host, self.port, self.path,
+ latest_dir, videos[-1])
+ except (ConnectionRefusedError, StatusCodeError) as err:
+ _LOGGER.error('Error while fetching video: %s', err)
+ return None
async def async_camera_image(self):
"""Return a still image response from the camera."""
from haffmpeg import ImageFrame, IMAGE_JPEG
- url = await self.hass.async_add_job(self.get_latest_video_url)
+ url = await self._get_latest_video_url()
if url != self._last_url:
ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop)
- self._last_image = await asyncio.shield(ffmpeg.get_image(
- url, output_format=IMAGE_JPEG,
- extra_cmd=self._extra_arguments), loop=self.hass.loop)
+ self._last_image = await asyncio.shield(
+ ffmpeg.get_image(
+ url,
+ output_format=IMAGE_JPEG,
+ extra_cmd=self._extra_arguments),
+ loop=self.hass.loop)
self._last_url = url
return self._last_image
diff --git a/homeassistant/components/camera/zoneminder.py b/homeassistant/components/camera/zoneminder.py
index a98e3ef066fbee..90ef08c24feba2 100644
--- a/homeassistant/components/camera/zoneminder.py
+++ b/homeassistant/components/camera/zoneminder.py
@@ -49,7 +49,6 @@ def _get_image_url(hass, monitor, mode):
@asyncio.coroutine
-# pylint: disable=unused-argument
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the ZoneMinder cameras."""
cameras = []
diff --git a/homeassistant/components/cast/.translations/ca.json b/homeassistant/components/cast/.translations/ca.json
new file mode 100644
index 00000000000000..e65e00f8624b69
--- /dev/null
+++ b/homeassistant/components/cast/.translations/ca.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "No s'han trobat dispositius de Google Cast a la xarxa.",
+ "single_instance_allowed": "Nom\u00e9s cal una \u00fanica configuraci\u00f3 de Google Cast."
+ },
+ "step": {
+ "confirm": {
+ "description": "Voleu configurar Google Cast?",
+ "title": "Google Cast"
+ }
+ },
+ "title": "Google Cast"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cast/.translations/en.json b/homeassistant/components/cast/.translations/en.json
new file mode 100644
index 00000000000000..55d79a7d560a9b
--- /dev/null
+++ b/homeassistant/components/cast/.translations/en.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "No Google Cast devices found on the network.",
+ "single_instance_allowed": "Only a single configuration of Google Cast is necessary."
+ },
+ "step": {
+ "confirm": {
+ "description": "Do you want to setup Google Cast?",
+ "title": "Google Cast"
+ }
+ },
+ "title": "Google Cast"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cast/.translations/ko.json b/homeassistant/components/cast/.translations/ko.json
new file mode 100644
index 00000000000000..2be2a69c171327
--- /dev/null
+++ b/homeassistant/components/cast/.translations/ko.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Googgle Cast \uc7a5\uce58\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.",
+ "single_instance_allowed": "Google Cast\uc758 \ub2e8\uc77c \uad6c\uc131 \ub9cc \ud544\uc694\ud569\ub2c8\ub2e4."
+ },
+ "step": {
+ "confirm": {
+ "description": "Google Cast\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "title": "Google Cast"
+ }
+ },
+ "title": "Google Cast"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cast/.translations/no.json b/homeassistant/components/cast/.translations/no.json
new file mode 100644
index 00000000000000..d36c929e7211b5
--- /dev/null
+++ b/homeassistant/components/cast/.translations/no.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Ingen Google Cast enheter funnet p\u00e5 nettverket.",
+ "single_instance_allowed": "Kun en enkelt konfigurasjon av Google Cast er n\u00f8dvendig."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u00d8nsker du \u00e5 sette opp Google Cast?",
+ "title": "Google Cast"
+ }
+ },
+ "title": "Google Cast"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cast/.translations/pl.json b/homeassistant/components/cast/.translations/pl.json
new file mode 100644
index 00000000000000..c4399f95defe81
--- /dev/null
+++ b/homeassistant/components/cast/.translations/pl.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Nie znaleziono w sieci urz\u0105dze\u0144 Google Cast.",
+ "single_instance_allowed": "Wymagana jest tylko jedna konfiguracja Google Cast."
+ },
+ "step": {
+ "confirm": {
+ "description": "Czy chcesz skonfigurowa\u0107 Google Cast?",
+ "title": "Google Cast"
+ }
+ },
+ "title": "Google Cast"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cast/.translations/ru.json b/homeassistant/components/cast/.translations/ru.json
new file mode 100644
index 00000000000000..9c9353da37e3da
--- /dev/null
+++ b/homeassistant/components/cast/.translations/ru.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Google Cast \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.",
+ "single_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f Google Cast."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Google Cast?",
+ "title": "Google Cast"
+ }
+ },
+ "title": "Google Cast"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cast/.translations/sv.json b/homeassistant/components/cast/.translations/sv.json
new file mode 100644
index 00000000000000..aea55058d108f7
--- /dev/null
+++ b/homeassistant/components/cast/.translations/sv.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Inga Google Cast-enheter hittades i n\u00e4tverket.",
+ "single_instance_allowed": "Endast en enda konfiguration av Google Cast \u00e4r n\u00f6dv\u00e4ndig."
+ },
+ "step": {
+ "confirm": {
+ "description": "Vill du konfigurera Google Cast?",
+ "title": "Google Cast"
+ }
+ },
+ "title": "Google Cast"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cast/.translations/vi.json b/homeassistant/components/cast/.translations/vi.json
new file mode 100644
index 00000000000000..2f2982293cfdac
--- /dev/null
+++ b/homeassistant/components/cast/.translations/vi.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Kh\u00f4ng t\u00ecm th\u1ea5y thi\u1ebft b\u1ecb Google Cast n\u00e0o tr\u00ean m\u1ea1ng.",
+ "single_instance_allowed": "Ch\u1ec9 c\u1ea7n m\u1ed9t c\u1ea5u h\u00ecnh duy nh\u1ea5t c\u1ee7a Google Cast l\u00e0 \u0111\u1ee7."
+ },
+ "step": {
+ "confirm": {
+ "description": "B\u1ea1n c\u00f3 mu\u1ed1n thi\u1ebft l\u1eadp Google Cast kh\u00f4ng?",
+ "title": "Google Cast"
+ }
+ },
+ "title": "Google Cast"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cast/.translations/zh-Hans.json b/homeassistant/components/cast/.translations/zh-Hans.json
new file mode 100644
index 00000000000000..4a844d3d4dd84a
--- /dev/null
+++ b/homeassistant/components/cast/.translations/zh-Hans.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u6ca1\u6709\u5728\u7f51\u7edc\u4e0a\u627e\u5230 Google Cast \u8bbe\u5907\u3002",
+ "single_instance_allowed": "\u53ea\u6709\u4e00\u6b21 Google Cast \u914d\u7f6e\u662f\u5fc5\u8981\u7684\u3002"
+ },
+ "step": {
+ "confirm": {
+ "description": "\u60a8\u60f3\u8981\u914d\u7f6e Google Cast \u5417\uff1f",
+ "title": "Google Cast"
+ }
+ },
+ "title": "Google Cast"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py
new file mode 100644
index 00000000000000..a4ee25f0915c91
--- /dev/null
+++ b/homeassistant/components/cast/__init__.py
@@ -0,0 +1,30 @@
+"""Component to embed Google Cast."""
+from homeassistant.helpers import config_entry_flow
+
+
+DOMAIN = 'cast'
+REQUIREMENTS = ['pychromecast==2.1.0']
+
+
+async def async_setup(hass, config):
+ """Set up the Cast component."""
+ hass.data[DOMAIN] = config.get(DOMAIN, {})
+ return True
+
+
+async def async_setup_entry(hass, entry):
+ """Set up Cast from a config entry."""
+ hass.async_add_job(hass.config_entries.async_forward_entry_setup(
+ entry, 'media_player'))
+ return True
+
+
+async def _async_has_devices(hass):
+ """Return if there are devices that can be discovered."""
+ from pychromecast.discovery import discover_chromecasts
+
+ return await hass.async_add_job(discover_chromecasts)
+
+
+config_entry_flow.register_discovery_flow(
+ DOMAIN, 'Google Cast', _async_has_devices)
diff --git a/homeassistant/components/cast/strings.json b/homeassistant/components/cast/strings.json
new file mode 100644
index 00000000000000..7f480de0e8bea3
--- /dev/null
+++ b/homeassistant/components/cast/strings.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "title": "Google Cast",
+ "step": {
+ "confirm": {
+ "title": "Google Cast",
+ "description": "Do you want to setup Google Cast?"
+ }
+ },
+ "abort": {
+ "single_instance_allowed": "Only a single configuration of Google Cast is necessary.",
+ "no_devices_found": "No Google Cast devices found on the network."
+ }
+ }
+}
diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py
index ebe7cbbf2c1c4a..a47edc5af42632 100644
--- a/homeassistant/components/climate/__init__.py
+++ b/homeassistant/components/climate/__init__.py
@@ -246,7 +246,8 @@ def set_swing_mode(hass, swing_mode, entity_id=None):
async def async_setup(hass, config):
"""Set up climate devices."""
- component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
+ component = hass.data[DOMAIN] = \
+ EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
await component.async_setup(config)
async def async_away_mode_set_service(service):
@@ -456,6 +457,16 @@ async def async_on_off_service(service):
return True
+async def async_setup_entry(hass, entry):
+ """Setup a config entry."""
+ return await hass.data[DOMAIN].async_setup_entry(entry)
+
+
+async def async_unload_entry(hass, entry):
+ """Unload a config entry."""
+ return await hass.data[DOMAIN].async_unload_entry(entry)
+
+
class ClimateDevice(Entity):
"""Representation of a climate device."""
diff --git a/homeassistant/components/climate/fritzbox.py b/homeassistant/components/climate/fritzbox.py
index 839da8c9d53331..fa3ca31c770725 100755
--- a/homeassistant/components/climate/fritzbox.py
+++ b/homeassistant/components/climate/fritzbox.py
@@ -13,21 +13,27 @@
ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_BATTERY_LOW, ATTR_STATE_LOCKED)
from homeassistant.components.climate import (
ATTR_OPERATION_MODE, ClimateDevice, STATE_ECO, STATE_HEAT, STATE_MANUAL,
- SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE)
+ STATE_OFF, STATE_ON, SUPPORT_OPERATION_MODE,
+ SUPPORT_TARGET_TEMPERATURE)
from homeassistant.const import (
ATTR_TEMPERATURE, PRECISION_HALVES, TEMP_CELSIUS)
-
DEPENDENCIES = ['fritzbox']
_LOGGER = logging.getLogger(__name__)
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE)
-OPERATION_LIST = [STATE_HEAT, STATE_ECO]
+OPERATION_LIST = [STATE_HEAT, STATE_ECO, STATE_OFF, STATE_ON]
MIN_TEMPERATURE = 8
MAX_TEMPERATURE = 28
+# special temperatures for on/off in Fritz!Box API (modified by pyfritzhome)
+ON_API_TEMPERATURE = 127.0
+OFF_API_TEMPERATURE = 126.5
+ON_REPORT_SET_TEMPERATURE = 30.0
+OFF_REPORT_SET_TEMPERATURE = 0.0
+
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Fritzbox smarthome thermostat platform."""
@@ -88,6 +94,9 @@ def current_temperature(self):
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
+ if self._target_temperature in (ON_API_TEMPERATURE,
+ OFF_API_TEMPERATURE):
+ return None
return self._target_temperature
def set_temperature(self, **kwargs):
@@ -102,9 +111,13 @@ def set_temperature(self, **kwargs):
@property
def current_operation(self):
"""Return the current operation mode."""
+ if self._target_temperature == ON_API_TEMPERATURE:
+ return STATE_ON
+ if self._target_temperature == OFF_API_TEMPERATURE:
+ return STATE_OFF
if self._target_temperature == self._comfort_temperature:
return STATE_HEAT
- elif self._target_temperature == self._eco_temperature:
+ if self._target_temperature == self._eco_temperature:
return STATE_ECO
return STATE_MANUAL
@@ -119,6 +132,10 @@ def set_operation_mode(self, operation_mode):
self.set_temperature(temperature=self._comfort_temperature)
elif operation_mode == STATE_ECO:
self.set_temperature(temperature=self._eco_temperature)
+ elif operation_mode == STATE_OFF:
+ self.set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE)
+ elif operation_mode == STATE_ON:
+ self.set_temperature(temperature=ON_REPORT_SET_TEMPERATURE)
@property
def min_temp(self):
diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py
index 6b7f6cb2afc91a..030a76626c6e2a 100644
--- a/homeassistant/components/climate/generic_thermostat.py
+++ b/homeassistant/components/climate/generic_thermostat.py
@@ -14,8 +14,7 @@
from homeassistant.components.climate import (
STATE_HEAT, STATE_COOL, STATE_IDLE, STATE_AUTO, ClimateDevice,
ATTR_OPERATION_MODE, ATTR_AWAY_MODE, SUPPORT_OPERATION_MODE,
- SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA,
- DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP)
+ SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA)
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE,
CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF,
@@ -268,7 +267,8 @@ def min_temp(self):
if self._min_temp:
return self._min_temp
- return DEFAULT_MIN_TEMP
+ # get default temp from super class
+ return super().min_temp
@property
def max_temp(self):
@@ -277,7 +277,8 @@ def max_temp(self):
if self._max_temp:
return self._max_temp
- return DEFAULT_MAX_TEMP
+ # Get default temp from super class
+ return super().max_temp
@asyncio.coroutine
def _async_sensor_changed(self, entity_id, old_state, new_state):
diff --git a/homeassistant/components/climate/knx.py b/homeassistant/components/climate/knx.py
index 5ce6cc2fa7af0a..f53cf2491dc5c5 100644
--- a/homeassistant/components/climate/knx.py
+++ b/homeassistant/components/climate/knx.py
@@ -136,7 +136,6 @@ def async_register_callbacks(self):
"""Register callbacks to update hass after device was changed."""
async def after_update_callback(device):
"""Call after device was updated."""
- # pylint: disable=unused-argument
await self.async_update_ha_state()
self.device.register_device_updated_cb(after_update_callback)
diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py
index 1d98a5733f7054..5397daeb784cfd 100644
--- a/homeassistant/components/climate/mqtt.py
+++ b/homeassistant/components/climate/mqtt.py
@@ -17,7 +17,7 @@
PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, STATE_AUTO,
ATTR_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
SUPPORT_SWING_MODE, SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE,
- SUPPORT_AUX_HEAT)
+ SUPPORT_AUX_HEAT, DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP)
from homeassistant.const import (
STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME, CONF_VALUE_TEMPLATE)
from homeassistant.components.mqtt import (
@@ -70,6 +70,9 @@
CONF_INITIAL = 'initial'
CONF_SEND_IF_OFF = 'send_if_off'
+CONF_MIN_TEMP = 'min_temp'
+CONF_MAX_TEMP = 'max_temp'
+
SCHEMA_BASE = CLIMATE_PLATFORM_SCHEMA.extend(MQTT_BASE_PLATFORM_SCHEMA.schema)
PLATFORM_SCHEMA = SCHEMA_BASE.extend({
vol.Optional(CONF_POWER_COMMAND_TOPIC): mqtt.valid_publish_topic,
@@ -116,6 +119,10 @@
vol.Optional(CONF_SEND_IF_OFF, default=True): cv.boolean,
vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string,
vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string,
+
+ vol.Optional(CONF_MIN_TEMP, default=DEFAULT_MIN_TEMP): vol.Coerce(float),
+ vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): vol.Coerce(float)
+
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@@ -181,19 +188,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
config.get(CONF_PAYLOAD_OFF),
config.get(CONF_AVAILABILITY_TOPIC),
config.get(CONF_PAYLOAD_AVAILABLE),
- config.get(CONF_PAYLOAD_NOT_AVAILABLE))
+ config.get(CONF_PAYLOAD_NOT_AVAILABLE),
+ config.get(CONF_MIN_TEMP),
+ config.get(CONF_MAX_TEMP))
])
class MqttClimate(MqttAvailability, ClimateDevice):
- """Representation of a demo climate device."""
+ """Representation of an MQTT climate device."""
def __init__(self, hass, name, topic, value_templates, qos, retain,
mode_list, fan_mode_list, swing_mode_list,
target_temperature, away, hold, current_fan_mode,
current_swing_mode, current_operation, aux, send_if_off,
payload_on, payload_off, availability_topic,
- payload_available, payload_not_available):
+ payload_available, payload_not_available,
+ min_temp, max_temp):
"""Initialize the climate device."""
super().__init__(availability_topic, qos, payload_available,
payload_not_available)
@@ -219,6 +229,8 @@ def __init__(self, hass, name, topic, value_templates, qos, retain,
self._send_if_off = send_if_off
self._payload_on = payload_on
self._payload_off = payload_off
+ self._min_temp = min_temp
+ self._max_temp = max_temp
@asyncio.coroutine
def async_added_to_hass(self):
@@ -619,3 +631,15 @@ def supported_features(self):
support |= SUPPORT_AUX_HEAT
return support
+
+ @property
+ def min_temp(self):
+ """Return the minimum temperature."""
+ # pylint: disable=no-member
+ return self._min_temp
+
+ @property
+ def max_temp(self):
+ """Return the maximum temperature."""
+ # pylint: disable=no-member
+ return self._max_temp
diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py
index 696f1479c08861..dc1f74613bcb46 100644
--- a/homeassistant/components/climate/nest.py
+++ b/homeassistant/components/climate/nest.py
@@ -32,16 +32,22 @@
def setup_platform(hass, config, add_devices, discovery_info=None):
- """Set up the Nest thermostat."""
- if discovery_info is None:
- return
+ """Set up the Nest thermostat.
+ No longer in use.
+ """
+
+
+async def async_setup_entry(hass, entry, async_add_devices):
+ """Set up the Nest climate device based on a config entry."""
temp_unit = hass.config.units.temperature_unit
+ thermostats = await hass.async_add_job(hass.data[DATA_NEST].thermostats)
+
all_devices = [NestThermostat(structure, device, temp_unit)
- for structure, device in hass.data[DATA_NEST].thermostats()]
+ for structure, device in thermostats]
- add_devices(all_devices, True)
+ async_add_devices(all_devices, True)
class NestThermostat(ClimateDevice):
diff --git a/homeassistant/components/climate/netatmo.py b/homeassistant/components/climate/netatmo.py
index 49452662fc43e1..a4b921037dbe4d 100644
--- a/homeassistant/components/climate/netatmo.py
+++ b/homeassistant/components/climate/netatmo.py
@@ -44,7 +44,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
netatmo = hass.components.netatmo
device = config.get(CONF_RELAY)
- import lnetatmo
+ import pyatmo
try:
data = ThermostatData(netatmo.NETATMO_AUTH, device)
for module_name in data.get_module_names():
@@ -53,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
module_name not in config[CONF_THERMOSTAT]:
continue
add_devices([NetatmoThermostat(data, module_name)], True)
- except lnetatmo.NoDevice:
+ except pyatmo.NoDevice:
return None
@@ -168,8 +168,8 @@ def get_module_names(self):
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Call the NetAtmo API to update the data."""
- import lnetatmo
- self.thermostatdata = lnetatmo.ThermostatData(self.auth)
+ import pyatmo
+ self.thermostatdata = pyatmo.ThermostatData(self.auth)
self.target_temperature = self.thermostatdata.setpoint_temp
self.setpoint_mode = self.thermostatdata.setpoint_mode
self.current_temperature = self.thermostatdata.temp
diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py
index b3fff0dd796d33..363653608e86ca 100644
--- a/homeassistant/components/climate/sensibo.py
+++ b/homeassistant/components/climate/sensibo.py
@@ -19,7 +19,7 @@
ATTR_CURRENT_HUMIDITY, ClimateDevice, DOMAIN, PLATFORM_SCHEMA,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
SUPPORT_FAN_MODE, SUPPORT_SWING_MODE,
- SUPPORT_ON_OFF, DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP)
+ SUPPORT_ON_OFF)
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -246,13 +246,13 @@ def is_on(self):
def min_temp(self):
"""Return the minimum temperature."""
return self._temperatures_list[0] \
- if self._temperatures_list else DEFAULT_MIN_TEMP
+ if self._temperatures_list else super().min_temp
@property
def max_temp(self):
"""Return the maximum temperature."""
return self._temperatures_list[-1] \
- if self._temperatures_list else DEFAULT_MAX_TEMP
+ if self._temperatures_list else super().max_temp
@property
def unique_id(self):
diff --git a/homeassistant/components/climate/tado.py b/homeassistant/components/climate/tado.py
index 59da425553a2b9..b3734e020e00e2 100644
--- a/homeassistant/components/climate/tado.py
+++ b/homeassistant/components/climate/tado.py
@@ -8,8 +8,8 @@
from homeassistant.const import (PRECISION_TENTHS, TEMP_CELSIUS)
from homeassistant.components.climate import (
- ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
- DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP)
+ ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
+from homeassistant.util.temperature import convert as convert_temperature
from homeassistant.const import ATTR_TEMPERATURE
from homeassistant.components.tado import DATA_TADO
@@ -231,18 +231,14 @@ def set_operation_mode(self, readable_operation_mode):
@property
def min_temp(self):
"""Return the minimum temperature."""
- if self._min_temp:
- return self._min_temp
-
- return DEFAULT_MIN_TEMP
+ return convert_temperature(self._min_temp, self._unit,
+ self.hass.config.units.temperature_unit)
@property
def max_temp(self):
"""Return the maximum temperature."""
- if self._max_temp:
- return self._max_temp
-
- return DEFAULT_MAX_TEMP
+ return convert_temperature(self._max_temp, self._unit,
+ self.hass.config.units.temperature_unit)
def update(self):
"""Update the state of this climate device."""
diff --git a/homeassistant/components/climate/vera.py b/homeassistant/components/climate/vera.py
index 6fb6bc0ff48410..4deb4d9ea2ea4a 100644
--- a/homeassistant/components/climate/vera.py
+++ b/homeassistant/components/climate/vera.py
@@ -32,8 +32,8 @@
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Set up of Vera thermostats."""
add_devices_callback(
- VeraThermostat(device, hass.data[VERA_CONTROLLER]) for
- device in hass.data[VERA_DEVICES]['climate'])
+ [VeraThermostat(device, hass.data[VERA_CONTROLLER]) for
+ device in hass.data[VERA_DEVICES]['climate']], True)
class VeraThermostat(VeraDevice, ClimateDevice):
@@ -101,10 +101,6 @@ def current_power_w(self):
if power:
return convert(power, float, 0.0)
- def update(self):
- """Handle state updates."""
- self._state = self.vera_device.get_hvac_mode()
-
@property
def temperature_unit(self):
"""Return the unit of measurement."""
diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py
index c67e032c14947d..12a6960f8334b7 100644
--- a/homeassistant/components/climate/wink.py
+++ b/homeassistant/components/climate/wink.py
@@ -84,7 +84,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices([WinkWaterHeater(water_heater, hass)])
-# pylint: disable=abstract-method
class WinkThermostat(WinkDevice, ClimateDevice):
"""Representation of a Wink thermostat."""
diff --git a/homeassistant/components/climate/zhong_hong.py b/homeassistant/components/climate/zhong_hong.py
new file mode 100644
index 00000000000000..7ff19871ee7bd5
--- /dev/null
+++ b/homeassistant/components/climate/zhong_hong.py
@@ -0,0 +1,217 @@
+"""
+Support for ZhongHong HVAC Controller.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/climate.zhong_hong/
+"""
+import logging
+
+import voluptuous as vol
+
+from homeassistant.components.climate import (
+ ATTR_OPERATION_MODE, PLATFORM_SCHEMA, STATE_COOL, STATE_DRY,
+ STATE_FAN_ONLY, STATE_HEAT, SUPPORT_FAN_MODE, SUPPORT_ON_OFF,
+ SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, ClimateDevice)
+from homeassistant.const import (ATTR_TEMPERATURE, CONF_HOST, CONF_PORT,
+ EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS)
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.dispatcher import (async_dispatcher_connect,
+ async_dispatcher_send)
+
+_LOGGER = logging.getLogger(__name__)
+
+CONF_GATEWAY_ADDRRESS = 'gateway_address'
+
+REQUIREMENTS = ['zhong_hong_hvac==1.0.9']
+SIGNAL_DEVICE_ADDED = 'zhong_hong_device_added'
+SIGNAL_ZHONG_HONG_HUB_START = 'zhong_hong_hub_start'
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_HOST):
+ cv.string,
+ vol.Optional(CONF_PORT, default=9999):
+ vol.Coerce(int),
+ vol.Optional(CONF_GATEWAY_ADDRRESS, default=1):
+ vol.Coerce(int),
+})
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the ZhongHong HVAC platform."""
+ from zhong_hong_hvac.hub import ZhongHongGateway
+ host = config.get(CONF_HOST)
+ port = config.get(CONF_PORT)
+ gw_addr = config.get(CONF_GATEWAY_ADDRRESS)
+ hub = ZhongHongGateway(host, port, gw_addr)
+ devices = [
+ ZhongHongClimate(hub, addr_out, addr_in)
+ for (addr_out, addr_in) in hub.discovery_ac()
+ ]
+
+ _LOGGER.debug("We got %s zhong_hong climate devices", len(devices))
+
+ hub_is_initialized = False
+
+ async def startup():
+ """Start hub socket after all climate entity is setted up."""
+ nonlocal hub_is_initialized
+ if not all([device.is_initialized for device in devices]):
+ return
+
+ if hub_is_initialized:
+ return
+
+ _LOGGER.debug("zhong_hong hub start listen event")
+ await hass.async_add_job(hub.start_listen)
+ await hass.async_add_job(hub.query_all_status)
+ hub_is_initialized = True
+
+ async_dispatcher_connect(hass, SIGNAL_DEVICE_ADDED, startup)
+
+ # add devices after SIGNAL_DEVICE_SETTED_UP event is listend
+ add_devices(devices)
+
+ def stop_listen(event):
+ """Stop ZhongHongHub socket."""
+ hub.stop_listen()
+
+ hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_listen)
+
+
+class ZhongHongClimate(ClimateDevice):
+ """Representation of a ZhongHong controller support HVAC."""
+
+ def __init__(self, hub, addr_out, addr_in):
+ """Set up the ZhongHong climate devices."""
+ from zhong_hong_hvac.hvac import HVAC
+ self._device = HVAC(hub, addr_out, addr_in)
+ self._hub = hub
+ self._current_operation = None
+ self._current_temperature = None
+ self._target_temperature = None
+ self._current_fan_mode = None
+ self._is_on = None
+ self.is_initialized = False
+
+ async def async_added_to_hass(self):
+ """Register callbacks."""
+ self._device.register_update_callback(self._after_update)
+ self.is_initialized = True
+ async_dispatcher_send(self.hass, SIGNAL_DEVICE_ADDED)
+
+ def _after_update(self, climate):
+ """Callback to update state."""
+ _LOGGER.debug("async update ha state")
+ if self._device.current_operation:
+ self._current_operation = self._device.current_operation.lower()
+ if self._device.current_temperature:
+ self._current_temperature = self._device.current_temperature
+ if self._device.current_fan_mode:
+ self._current_fan_mode = self._device.current_fan_mode
+ if self._device.target_temperature:
+ self._target_temperature = self._device.target_temperature
+ self._is_on = self._device.is_on
+ self.schedule_update_ha_state()
+
+ @property
+ def should_poll(self):
+ """Return the polling state."""
+ return False
+
+ @property
+ def name(self):
+ """Return the name of the thermostat, if any."""
+ return self.unique_id
+
+ @property
+ def unique_id(self):
+ """Return the unique ID of the HVAC."""
+ return "zhong_hong_hvac_{}_{}".format(self._device.addr_out,
+ self._device.addr_in)
+
+ @property
+ def supported_features(self):
+ """Return the list of supported features."""
+ return (SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE
+ | SUPPORT_OPERATION_MODE | SUPPORT_ON_OFF)
+
+ @property
+ def temperature_unit(self):
+ """Return the unit of measurement used by the platform."""
+ return TEMP_CELSIUS
+
+ @property
+ def current_operation(self):
+ """Return current operation ie. heat, cool, idle."""
+ return self._current_operation
+
+ @property
+ def operation_list(self):
+ """Return the list of available operation modes."""
+ return [STATE_COOL, STATE_HEAT, STATE_DRY, STATE_FAN_ONLY]
+
+ @property
+ def current_temperature(self):
+ """Return the current temperature."""
+ return self._current_temperature
+
+ @property
+ def target_temperature(self):
+ """Return the temperature we try to reach."""
+ return self._target_temperature
+
+ @property
+ def target_temperature_step(self):
+ """Return the supported step of target temperature."""
+ return 1
+
+ @property
+ def is_on(self):
+ """Return true if on."""
+ return self._device.is_on
+
+ @property
+ def current_fan_mode(self):
+ """Return the fan setting."""
+ return self._current_fan_mode
+
+ @property
+ def fan_list(self):
+ """Return the list of available fan modes."""
+ return self._device.fan_list
+
+ @property
+ def min_temp(self):
+ """Return the minimum temperature."""
+ return self._device.min_temp
+
+ @property
+ def max_temp(self):
+ """Return the maximum temperature."""
+ return self._device.max_temp
+
+ def turn_on(self):
+ """Turn on ac."""
+ return self._device.turn_on()
+
+ def turn_off(self):
+ """Turn off ac."""
+ return self._device.turn_off()
+
+ def set_temperature(self, **kwargs):
+ """Set new target temperature."""
+ temperature = kwargs.get(ATTR_TEMPERATURE)
+ if temperature is not None:
+ self._device.set_temperature(temperature)
+
+ operation_mode = kwargs.get(ATTR_OPERATION_MODE)
+ if operation_mode is not None:
+ self.set_operation_mode(operation_mode)
+
+ def set_operation_mode(self, operation_mode):
+ """Set new target operation mode."""
+ self._device.set_operation_mode(operation_mode.upper())
+
+ def set_fan_mode(self, fan_mode):
+ """Set new target fan mode."""
+ self._device.set_fan_mode(fan_mode)
diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py
index 4b9a2c89da0bab..c594bf1f99e16a 100644
--- a/homeassistant/components/config/entity_registry.py
+++ b/homeassistant/components/config/entity_registry.py
@@ -2,48 +2,85 @@
import voluptuous as vol
from homeassistant.core import callback
-from homeassistant.components.http import HomeAssistantView
-from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.helpers.entity_registry import async_get_registry
+from homeassistant.components import websocket_api
+from homeassistant.helpers import config_validation as cv
+
+DEPENDENCIES = ['websocket_api']
+
+WS_TYPE_GET = 'config/entity_registry/get'
+SCHEMA_WS_GET = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
+ vol.Required('type'): WS_TYPE_GET,
+ vol.Required('entity_id'): cv.entity_id
+})
+
+WS_TYPE_UPDATE = 'config/entity_registry/update'
+SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
+ vol.Required('type'): WS_TYPE_UPDATE,
+ vol.Required('entity_id'): cv.entity_id,
+ # If passed in, we update value. Passing None will remove old value.
+ vol.Optional('name'): vol.Any(str, None),
+})
async def async_setup(hass):
"""Enable the Entity Registry views."""
- hass.http.register_view(ConfigManagerEntityView)
+ hass.components.websocket_api.async_register_command(
+ WS_TYPE_GET, websocket_get_entity,
+ SCHEMA_WS_GET
+ )
+ hass.components.websocket_api.async_register_command(
+ WS_TYPE_UPDATE, websocket_update_entity,
+ SCHEMA_WS_UPDATE
+ )
return True
-class ConfigManagerEntityView(HomeAssistantView):
- """View to interact with an entity registry entry."""
-
- url = '/api/config/entity_registry/{entity_id}'
- name = 'api:config:entity_registry:entity'
+@callback
+def websocket_get_entity(hass, connection, msg):
+ """Handle get entity registry entry command.
- async def get(self, request, entity_id):
- """Get the entity registry settings for an entity."""
- hass = request.app['hass']
+ Async friendly.
+ """
+ async def retrieve_entity():
+ """Get entity from registry."""
registry = await async_get_registry(hass)
- entry = registry.entities.get(entity_id)
+ entry = registry.entities.get(msg['entity_id'])
if entry is None:
- return self.json_message('Entry not found', 404)
+ connection.send_message_outside(websocket_api.error_message(
+ msg['id'], websocket_api.ERR_NOT_FOUND, 'Entity not found'))
+ return
- return self.json(_entry_dict(entry))
+ connection.send_message_outside(websocket_api.result_message(
+ msg['id'], _entry_dict(entry)
+ ))
- @RequestDataValidator(vol.Schema({
- # If passed in, we update value. Passing None will remove old value.
- vol.Optional('name'): vol.Any(str, None),
- }))
- async def post(self, request, entity_id, data):
- """Update the entity registry settings for an entity."""
- hass = request.app['hass']
+ hass.async_add_job(retrieve_entity())
+
+
+@callback
+def websocket_update_entity(hass, connection, msg):
+ """Handle get camera thumbnail websocket command.
+
+ Async friendly.
+ """
+ async def update_entity():
+ """Get entity from registry."""
registry = await async_get_registry(hass)
- if entity_id not in registry.entities:
- return self.json_message('Entry not found', 404)
+ if msg['entity_id'] not in registry.entities:
+ connection.send_message_outside(websocket_api.error_message(
+ msg['id'], websocket_api.ERR_NOT_FOUND, 'Entity not found'))
+ return
+
+ entry = registry.async_update_entity(
+ msg['entity_id'], name=msg['name'])
+ connection.send_message_outside(websocket_api.result_message(
+ msg['id'], _entry_dict(entry)
+ ))
- entry = registry.async_update_entity(entity_id, **data)
- return self.json(_entry_dict(entry))
+ hass.async_add_job(update_entity())
@callback
diff --git a/homeassistant/components/cover/isy994.py b/homeassistant/components/cover/isy994.py
index 82ca60e84e6c91..743a36d41d5084 100644
--- a/homeassistant/components/cover/isy994.py
+++ b/homeassistant/components/cover/isy994.py
@@ -25,7 +25,6 @@
}
-# pylint: disable=unused-argument
def setup_platform(hass, config: ConfigType,
add_devices: Callable[[list], None], discovery_info=None):
"""Set up the ISY994 cover platform."""
diff --git a/homeassistant/components/cover/knx.py b/homeassistant/components/cover/knx.py
index 83668924268e0d..7bb20e4cf1f2cf 100644
--- a/homeassistant/components/cover/knx.py
+++ b/homeassistant/components/cover/knx.py
@@ -107,7 +107,6 @@ def async_register_callbacks(self):
"""Register callbacks to update hass after device was changed."""
async def after_update_callback(device):
"""Call after device was updated."""
- # pylint: disable=unused-argument
await self.async_update_ha_state()
self.device.register_device_updated_cb(after_update_callback)
@@ -197,7 +196,6 @@ def stop_auto_updater(self):
@callback
def auto_updater_hook(self, now):
"""Call for the autoupdater."""
- # pylint: disable=unused-argument
self.async_schedule_update_ha_state()
if self.device.position_reached():
self.stop_auto_updater()
diff --git a/homeassistant/components/cover/lutron.py b/homeassistant/components/cover/lutron.py
index 4e38681a310f3c..599bdb1cebab7f 100644
--- a/homeassistant/components/cover/lutron.py
+++ b/homeassistant/components/cover/lutron.py
@@ -17,7 +17,6 @@
DEPENDENCIES = ['lutron']
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Lutron shades."""
devs = []
diff --git a/homeassistant/components/cover/lutron_caseta.py b/homeassistant/components/cover/lutron_caseta.py
index 6ad9b093ed84ae..1ed502e0f7f8af 100644
--- a/homeassistant/components/cover/lutron_caseta.py
+++ b/homeassistant/components/cover/lutron_caseta.py
@@ -18,7 +18,6 @@
DEPENDENCIES = ['lutron_caseta']
-# pylint: disable=unused-argument
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Lutron Caseta shades as a cover device."""
diff --git a/homeassistant/components/cover/myq.py b/homeassistant/components/cover/myq.py
index 1e2ec43181cf78..a4682172feee46 100644
--- a/homeassistant/components/cover/myq.py
+++ b/homeassistant/components/cover/myq.py
@@ -13,7 +13,7 @@
CONF_USERNAME, CONF_PASSWORD, CONF_TYPE, STATE_CLOSED)
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['pymyq==0.0.8']
+REQUIREMENTS = ['pymyq==0.0.11']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/cover/rpi_gpio.py b/homeassistant/components/cover/rpi_gpio.py
index 49666139330c5c..384f96f3f52339 100644
--- a/homeassistant/components/cover/rpi_gpio.py
+++ b/homeassistant/components/cover/rpi_gpio.py
@@ -54,7 +54,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the RPi cover platform."""
relay_time = config.get(CONF_RELAY_TIME)
diff --git a/homeassistant/components/cover/vera.py b/homeassistant/components/cover/vera.py
index ff9ba6f762b861..9b2e8f3aad02b1 100644
--- a/homeassistant/components/cover/vera.py
+++ b/homeassistant/components/cover/vera.py
@@ -19,8 +19,8 @@
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Vera covers."""
add_devices(
- VeraCover(device, hass.data[VERA_CONTROLLER]) for
- device in hass.data[VERA_DEVICES]['cover'])
+ [VeraCover(device, hass.data[VERA_CONTROLLER]) for
+ device in hass.data[VERA_DEVICES]['cover']], True)
class VeraCover(VeraDevice, CoverDevice):
diff --git a/homeassistant/components/deconz/.translations/bg.json b/homeassistant/components/deconz/.translations/bg.json
index 91727cae257009..2ea6576206375d 100644
--- a/homeassistant/components/deconz/.translations/bg.json
+++ b/homeassistant/components/deconz/.translations/bg.json
@@ -1,6 +1,7 @@
{
"config": {
"abort": {
+ "already_configured": "\u041c\u043e\u0441\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d",
"no_bridges": "\u041d\u0435 \u0441\u0430 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u043c\u043e\u0441\u0442\u043e\u0432\u0435 deCONZ",
"one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u043e \u043a\u043e\u043f\u0438\u0435 \u043d\u0430 deCONZ"
},
diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json
new file mode 100644
index 00000000000000..0a9e6fdee3f68e
--- /dev/null
+++ b/homeassistant/components/deconz/.translations/ca.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "L'enlla\u00e7 ja est\u00e0 configurat",
+ "no_bridges": "No s'han descobert enlla\u00e7os amb deCONZ",
+ "one_instance_only": "El component nom\u00e9s admet una inst\u00e0ncia deCONZ"
+ },
+ "error": {
+ "no_key": "No s'ha pogut obtenir una clau API"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "host": "Amfitri\u00f3",
+ "port": "Port (predeterminat: '80')"
+ },
+ "title": "Definiu la passarel\u00b7la deCONZ"
+ },
+ "link": {
+ "description": "Desbloqueja la teva passarel\u00b7la d'enlla\u00e7 deCONZ per a registrar-te amb Home Assistant.\n\n1. V\u00e9s a la configuraci\u00f3 del sistema deCONZ\n2. Prem el bot\u00f3 \"Desbloquejar passarel\u00b7la\"",
+ "title": "Vincular amb deCONZ"
+ },
+ "options": {
+ "data": {
+ "allow_clip_sensor": "Permet la importaci\u00f3 de sensors virtuals",
+ "allow_deconz_groups": "Permet la importaci\u00f3 de grups deCONZ"
+ },
+ "title": "Opcions de configuraci\u00f3 addicionals per deCONZ"
+ }
+ },
+ "title": "deCONZ"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/.translations/cs.json b/homeassistant/components/deconz/.translations/cs.json
new file mode 100644
index 00000000000000..0721cac3321bfc
--- /dev/null
+++ b/homeassistant/components/deconz/.translations/cs.json
@@ -0,0 +1,32 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "P\u0159emost\u011bn\u00ed je ji\u017e nakonfigurov\u00e1no",
+ "no_bridges": "\u017d\u00e1dn\u00e9 deCONZ p\u0159emost\u011bn\u00ed nebyly nalezeny",
+ "one_instance_only": "Komponent podporuje pouze jednu instanci deCONZ"
+ },
+ "error": {
+ "no_key": "Nelze z\u00edskat kl\u00ed\u010d API"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "host": "Hostitel",
+ "port": "Port (v\u00fdchoz\u00ed hodnota: '80')"
+ },
+ "title": "Definujte br\u00e1nu deCONZ"
+ },
+ "link": {
+ "description": "Odemkn\u011bte br\u00e1nu deCONZ, pro registraci v Home Assistant. \n\n 1. P\u0159ejd\u011bte do nastaven\u00ed syst\u00e9mu deCONZ \n 2. Stiskn\u011bte tla\u010d\u00edtko \"Unlock Gateway\"",
+ "title": "Propojit s deCONZ"
+ },
+ "options": {
+ "data": {
+ "allow_clip_sensor": "Povolit import virtu\u00e1ln\u00edch \u010didel"
+ },
+ "title": "Dal\u0161\u00ed mo\u017enosti konfigurace pro deCONZ"
+ }
+ },
+ "title": "Br\u00e1na deCONZ Zigbee"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json
index a2f90e49e3ad36..f55f64ca43094a 100644
--- a/homeassistant/components/deconz/.translations/en.json
+++ b/homeassistant/components/deconz/.translations/en.json
@@ -21,10 +21,11 @@
"title": "Link with deCONZ"
},
"options": {
- "title": "Extra configuration options for deCONZ",
"data": {
- "allow_clip_sensor": "Allow importing virtual sensors"
- }
+ "allow_clip_sensor": "Allow importing virtual sensors",
+ "allow_deconz_groups": "Allow importing deCONZ groups"
+ },
+ "title": "Extra configuration options for deCONZ"
}
},
"title": "deCONZ Zigbee gateway"
diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json
new file mode 100644
index 00000000000000..02f174cd59f746
--- /dev/null
+++ b/homeassistant/components/deconz/.translations/fr.json
@@ -0,0 +1,32 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Ce pont est d\u00e9j\u00e0 configur\u00e9",
+ "no_bridges": "Aucun pont deCONZ n'a \u00e9t\u00e9 d\u00e9couvert",
+ "one_instance_only": "Le composant prend uniquement en charge une instance deCONZ"
+ },
+ "error": {
+ "no_key": "Impossible d'obtenir une cl\u00e9 d'API"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "host": "H\u00f4te",
+ "port": "Port (valeur par d\u00e9faut : 80)"
+ },
+ "title": "Initialiser la passerelle deCONZ"
+ },
+ "link": {
+ "description": "D\u00e9verrouillez votre passerelle deCONZ pour vous enregistrer aupr\u00e8s de Home Assistant. \n\n 1. Acc\u00e9dez aux param\u00e8tres du syst\u00e8me deCONZ \n 2. Cliquez sur \"D\u00e9verrouiller la passerelle\"",
+ "title": "Lien vers deCONZ"
+ },
+ "options": {
+ "data": {
+ "allow_clip_sensor": "Autoriser l'importation de capteurs virtuels"
+ },
+ "title": "Options de configuration suppl\u00e9mentaires pour deCONZ"
+ }
+ },
+ "title": "Passerelle deCONZ Zigbee"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/.translations/hu.json b/homeassistant/components/deconz/.translations/hu.json
index 42aab9c6d7e56f..c1fd76c5035fc2 100644
--- a/homeassistant/components/deconz/.translations/hu.json
+++ b/homeassistant/components/deconz/.translations/hu.json
@@ -1,6 +1,8 @@
{
"config": {
"abort": {
+ "already_configured": "A bridge m\u00e1r konfigur\u00e1lva van",
+ "no_bridges": "Nem tal\u00e1ltam deCONZ bridget",
"one_instance_only": "Ez a komponens csak egy deCONZ egys\u00e9get t\u00e1mogat"
},
"error": {
@@ -11,9 +13,11 @@
"data": {
"host": "H\u00e1zigazda (Host)",
"port": "Port (alap\u00e9rtelmezett \u00e9rt\u00e9k: '80')"
- }
+ },
+ "title": "deCONZ \u00e1tj\u00e1r\u00f3 megad\u00e1sa"
},
"link": {
+ "description": "Oldja fel a deCONZ \u00e1tj\u00e1r\u00f3t a Home Assistant-ban val\u00f3 regisztr\u00e1l\u00e1shoz.\n\n1. Menjen a deCONZ rendszer be\u00e1ll\u00edt\u00e1sokhoz\n2. Nyomja meg az \"\u00c1tj\u00e1r\u00f3 felold\u00e1sa\" gombot",
"title": "Kapcsol\u00f3d\u00e1s a deCONZ-hoz"
}
},
diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json
new file mode 100644
index 00000000000000..6fc7158b88269c
--- /dev/null
+++ b/homeassistant/components/deconz/.translations/it.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Il Bridge \u00e8 gi\u00e0 configurato",
+ "no_bridges": "Nessun bridge deCONZ rilevato",
+ "one_instance_only": "Il componente supporto solo un'istanza di deCONZ"
+ },
+ "error": {
+ "no_key": "Impossibile ottenere una API key"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "host": "Host",
+ "port": "Porta (valore di default: '80')"
+ },
+ "title": "Definisci il gateway deCONZ"
+ },
+ "link": {
+ "description": "Sblocca il tuo gateway deCONZ per registrarlo in Home Assistant.\n\n1. Vai nelle impostazioni di sistema di deCONZ\n2. Premi il bottone \"Unlock Gateway\"",
+ "title": "Collega con deCONZ"
+ }
+ },
+ "title": "deCONZ"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json
index d6de1028218dee..9c5ffa19257f3b 100644
--- a/homeassistant/components/deconz/.translations/ko.json
+++ b/homeassistant/components/deconz/.translations/ko.json
@@ -18,9 +18,16 @@
},
"link": {
"description": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \uc5b8\ub77d\ud558\uc5ec Home Assistant \uc5d0 \uc5f0\uacb0\ud558\uae30\n\n1. deCONZ \uc2dc\uc2a4\ud15c \uc124\uc815\uc73c\ub85c \uc774\ub3d9\ud558\uc138\uc694\n2. \"Unlock Gateway\" \ubc84\ud2bc\uc744 \ub204\ub974\uc138\uc694 ",
- "title": "deCONZ \uc640 \uc5f0\uacb0"
+ "title": "deCONZ\uc640 \uc5f0\uacb0"
+ },
+ "options": {
+ "data": {
+ "allow_clip_sensor": "\uac00\uc0c1 \uc13c\uc11c \uac00\uc838\uc624\uae30 \ud5c8\uc6a9",
+ "allow_deconz_groups": "deCONZ \ub0b4\uc6a9 \uac00\uc838\uc624\uae30 \ud5c8\uc6a9"
+ },
+ "title": "deCONZ\ub97c \uc704\ud55c \ucd94\uac00 \uad6c\uc131 \uc635\uc158"
}
},
- "title": "deCONZ"
+ "title": "deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json
index 2a9dfc5e5438dd..46190d23926b8c 100644
--- a/homeassistant/components/deconz/.translations/lb.json
+++ b/homeassistant/components/deconz/.translations/lb.json
@@ -19,6 +19,12 @@
"link": {
"description": "Entsperrt \u00e4r deCONZ gateway fir se mat Home Assistant ze registr\u00e9ieren.\n\n1. Gidd op\u00a0deCONZ System Astellungen\n2. Dr\u00e9ckt \"Unlock\" Gateway Kn\u00e4ppchen",
"title": "Link mat deCONZ"
+ },
+ "options": {
+ "data": {
+ "allow_clip_sensor": "Erlaabt den Import vun virtuellen Sensoren"
+ },
+ "title": "Extra Konfiguratiouns Optiounen fir deCONZ"
}
},
"title": "deCONZ"
diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json
index 25e3b0b7d68c40..55518b7da532ae 100644
--- a/homeassistant/components/deconz/.translations/no.json
+++ b/homeassistant/components/deconz/.translations/no.json
@@ -19,6 +19,13 @@
"link": {
"description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere deg med Home Assistant. \n\n 1. G\u00e5 til deCONZ-systeminnstillinger \n 2. Trykk p\u00e5 \"L\u00e5s opp gateway\" knappen",
"title": "Koble til deCONZ"
+ },
+ "options": {
+ "data": {
+ "allow_clip_sensor": "Tillat import av virtuelle sensorer",
+ "allow_deconz_groups": "Tillat import av deCONZ grupper"
+ },
+ "title": "Ekstra konfigurasjonsalternativer for deCONZ"
}
},
"title": "deCONZ"
diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json
index bb7488fcbec1e9..461e8b185eebeb 100644
--- a/homeassistant/components/deconz/.translations/pl.json
+++ b/homeassistant/components/deconz/.translations/pl.json
@@ -19,6 +19,12 @@
"link": {
"description": "Odblokuj bramk\u0119 deCONZ, aby zarejestrowa\u0107 j\u0105 w Home Assistant. \n\n 1. Przejd\u017a do ustawie\u0144 systemu deCONZ \n 2. Naci\u015bnij przycisk \"Odblokuj bramk\u0119\"",
"title": "Po\u0142\u0105cz z deCONZ"
+ },
+ "options": {
+ "data": {
+ "allow_clip_sensor": "Zezwalaj na importowanie wirtualnych sensor\u00f3w"
+ },
+ "title": "Dodatkowe opcje konfiguracji dla deCONZ"
}
},
"title": "deCONZ"
diff --git a/homeassistant/components/deconz/.translations/pt-BR.json b/homeassistant/components/deconz/.translations/pt-BR.json
new file mode 100644
index 00000000000000..065c51aee21cdc
--- /dev/null
+++ b/homeassistant/components/deconz/.translations/pt-BR.json
@@ -0,0 +1,32 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "A ponte j\u00e1 est\u00e1 configurada",
+ "no_bridges": "N\u00e3o h\u00e1 pontes de deCONZ descobertas",
+ "one_instance_only": "Componente suporta apenas uma inst\u00e2ncia deCONZ"
+ },
+ "error": {
+ "no_key": "N\u00e3o foi poss\u00edvel obter uma chave de API"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "host": "Hospedeiro",
+ "port": "Porta (valor padr\u00e3o: '80')"
+ },
+ "title": "Defina o gateway deCONZ"
+ },
+ "link": {
+ "description": "Desbloqueie o seu gateway deCONZ para se registar no Home Assistant. \n\n 1. V\u00e1 para as configura\u00e7\u00f5es do sistema deCONZ \n 2. Pressione o bot\u00e3o \"Desbloquear Gateway\"",
+ "title": "Linkar com deCONZ"
+ },
+ "options": {
+ "data": {
+ "allow_clip_sensor": "Permitir a importa\u00e7\u00e3o de sensores virtuais"
+ },
+ "title": "Op\u00e7\u00f5es extras de configura\u00e7\u00e3o para deCONZ"
+ }
+ },
+ "title": "Gateway deCONZ Zigbee"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/.translations/pt.json b/homeassistant/components/deconz/.translations/pt.json
index 2a00c69869140e..6ccbfe9f217d56 100644
--- a/homeassistant/components/deconz/.translations/pt.json
+++ b/homeassistant/components/deconz/.translations/pt.json
@@ -1,7 +1,32 @@
{
"config": {
"abort": {
- "already_configured": "Bridge j\u00e1 est\u00e1 configurada"
- }
+ "already_configured": "Bridge j\u00e1 est\u00e1 configurada",
+ "no_bridges": "Nenhum deCONZ descoberto",
+ "one_instance_only": "Componente suporta apenas uma conex\u00e3o deCONZ"
+ },
+ "error": {
+ "no_key": "N\u00e3o foi poss\u00edvel obter uma chave de API"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "host": "Servidor",
+ "port": "Porta (por omiss\u00e3o: '80')"
+ },
+ "title": "Defina o gateway deCONZ"
+ },
+ "link": {
+ "description": "Desbloqueie o seu gateway deCONZ para se registar no Home Assistant. \n\n 1. V\u00e1 para as configura\u00e7\u00f5es do sistema deCONZ \n 2. Pressione o bot\u00e3o \"Desbloquear Gateway\"",
+ "title": "Link com deCONZ"
+ },
+ "options": {
+ "data": {
+ "allow_clip_sensor": "Permitir a importa\u00e7\u00e3o de sensores virtuais"
+ },
+ "title": "Op\u00e7\u00f5es extra de configura\u00e7\u00e3o para deCONZ"
+ }
+ },
+ "title": "deCONZ"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json
index b0dc6a8a4a85f4..56490f67cb3dc6 100644
--- a/homeassistant/components/deconz/.translations/ru.json
+++ b/homeassistant/components/deconz/.translations/ru.json
@@ -19,6 +19,13 @@
"link": {
"description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00ab\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0448\u043b\u044e\u0437\u00bb",
"title": "\u0421\u0432\u044f\u0437\u044c \u0441 deCONZ"
+ },
+ "options": {
+ "data": {
+ "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0445 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432",
+ "allow_deconz_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442 \u0433\u0440\u0443\u043f\u043f deCONZ"
+ },
+ "title": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0434\u043b\u044f deCONZ"
}
},
"title": "deCONZ"
diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json
index b738002b273d64..59c5577c96b5a8 100644
--- a/homeassistant/components/deconz/.translations/sl.json
+++ b/homeassistant/components/deconz/.translations/sl.json
@@ -19,6 +19,12 @@
"link": {
"description": "Odklenite va\u0161 deCONZ gateway za registracijo z Home Assistant-om. \n1. Pojdite v deCONT sistemske nastavitve\n2. Pritisnite tipko \"odkleni prehod\"",
"title": "Povezava z deCONZ"
+ },
+ "options": {
+ "data": {
+ "allow_clip_sensor": "Dovoli uvoz virtualnih senzorjev"
+ },
+ "title": "Dodatne mo\u017enosti konfiguracije za deCONZ"
}
},
"title": "deCONZ"
diff --git a/homeassistant/components/deconz/.translations/sv.json b/homeassistant/components/deconz/.translations/sv.json
new file mode 100644
index 00000000000000..88cf8742acde8c
--- /dev/null
+++ b/homeassistant/components/deconz/.translations/sv.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "Bryggan \u00e4r redan konfigurerad",
+ "no_bridges": "Inga deCONZ-bryggor uppt\u00e4cktes",
+ "one_instance_only": "Komponenten st\u00f6djer endast en deCONZ-instans"
+ },
+ "error": {
+ "no_key": "Det gick inte att ta emot en API-nyckel"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "host": "V\u00e4rd",
+ "port": "Port (standardv\u00e4rde: '80')"
+ },
+ "title": "Definiera deCONZ-gatewaye"
+ },
+ "link": {
+ "description": "L\u00e5s upp din deCONZ-gateway f\u00f6r att registrera dig med Home Assistant. \n\n 1. G\u00e5 till deCONZ-systeminst\u00e4llningarna \n 2. Tryck p\u00e5 \"L\u00e5s upp gateway\"-knappen",
+ "title": "L\u00e4nka med deCONZ"
+ },
+ "options": {
+ "data": {
+ "allow_clip_sensor": "Till\u00e5t import av virtuella sensorer",
+ "allow_deconz_groups": "Till\u00e5t import av deCONZ-grupper"
+ },
+ "title": "Extra konfigurationsalternativ f\u00f6r deCONZ"
+ }
+ },
+ "title": "deCONZ"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/.translations/vi.json b/homeassistant/components/deconz/.translations/vi.json
new file mode 100644
index 00000000000000..00f1d9be57f07e
--- /dev/null
+++ b/homeassistant/components/deconz/.translations/vi.json
@@ -0,0 +1,26 @@
+{
+ "config": {
+ "abort": {
+ "already_configured": "C\u1ea7u \u0111\u00e3 \u0111\u01b0\u1ee3c c\u1ea5u h\u00ecnh",
+ "no_bridges": "Kh\u00f4ng t\u00ecm th\u1ea5y c\u1ea7u deCONZ n\u00e0o",
+ "one_instance_only": "Th\u00e0nh ph\u1ea7n ch\u1ec9 h\u1ed7 tr\u1ee3 m\u1ed9t c\u00e1 th\u1ec3 deCONZ"
+ },
+ "error": {
+ "no_key": "Kh\u00f4ng th\u1ec3 l\u1ea5y kh\u00f3a API"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "port": "C\u1ed5ng (gi\u00e1 tr\u1ecb m\u1eb7c \u0111\u1ecbnh: '80')"
+ }
+ },
+ "options": {
+ "data": {
+ "allow_clip_sensor": "Cho ph\u00e9p nh\u1eadp c\u1ea3m bi\u1ebfn \u1ea3o",
+ "allow_deconz_groups": "Cho ph\u00e9p nh\u1eadp c\u00e1c nh\u00f3m deCONZ"
+ },
+ "title": "T\u00f9y ch\u1ecdn c\u1ea5u h\u00ecnh b\u1ed5 sung cho deCONZ"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/deconz/.translations/zh-Hans.json b/homeassistant/components/deconz/.translations/zh-Hans.json
index f41b5b5111c2be..2e5a216c77ddec 100644
--- a/homeassistant/components/deconz/.translations/zh-Hans.json
+++ b/homeassistant/components/deconz/.translations/zh-Hans.json
@@ -19,6 +19,13 @@
"link": {
"description": "\u89e3\u9501\u60a8\u7684 deCONZ \u7f51\u5173\u4ee5\u6ce8\u518c\u5230 Home Assistant\u3002 \n\n 1. \u524d\u5f80 deCONZ \u7cfb\u7edf\u8bbe\u7f6e\n 2. \u70b9\u51fb\u201c\u89e3\u9501\u7f51\u5173\u201d\u6309\u94ae",
"title": "\u8fde\u63a5 deCONZ"
+ },
+ "options": {
+ "data": {
+ "allow_clip_sensor": "\u5141\u8bb8\u5bfc\u5165\u865a\u62df\u4f20\u611f\u5668",
+ "allow_deconz_groups": "\u5141\u8bb8\u5bfc\u5165 deCONZ \u7fa4\u7ec4"
+ },
+ "title": "deCONZ \u7684\u9644\u52a0\u914d\u7f6e\u9879"
}
},
"title": "deCONZ"
diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json
index 33be3846eb8290..17cbe87f1e8f9b 100644
--- a/homeassistant/components/deconz/.translations/zh-Hant.json
+++ b/homeassistant/components/deconz/.translations/zh-Hant.json
@@ -1,6 +1,7 @@
{
"config": {
"abort": {
+ "already_configured": "Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
"no_bridges": "\u672a\u641c\u5c0b\u5230 deCONZ Bridfe",
"one_instance_only": "\u7d44\u4ef6\u50c5\u652f\u63f4\u4e00\u7d44 deCONZ \u5be6\u4f8b"
},
@@ -18,6 +19,12 @@
"link": {
"description": "\u89e3\u9664 deCONZ \u7db2\u95dc\u9396\u5b9a\uff0c\u4ee5\u65bc Home Assistant \u9032\u884c\u8a3b\u518a\u3002\n\n1. \u9032\u5165 deCONZ \u7cfb\u7d71\u8a2d\u5b9a\n2. \u6309\u4e0b\u300c\u89e3\u9664\u7db2\u95dc\u9396\u5b9a\uff08Unlock Gateway\uff09\u300d\u6309\u9215",
"title": "\u9023\u7d50\u81f3 deCONZ"
+ },
+ "options": {
+ "data": {
+ "allow_clip_sensor": "\u5141\u8a31\u532f\u5165\u865b\u64ec\u611f\u61c9\u5668"
+ },
+ "title": "deCONZ \u9644\u52a0\u8a2d\u5b9a\u9078\u9805"
}
},
"title": "deCONZ"
diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py
index cb7c3aad7fdc6a..27fb6987f8c24c 100644
--- a/homeassistant/components/deconz/config_flow.py
+++ b/homeassistant/components/deconz/config_flow.py
@@ -8,7 +8,9 @@
from homeassistant.helpers import aiohttp_client
from homeassistant.util.json import load_json
-from .const import CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DOMAIN
+from .const import (
+ CONF_ALLOW_DECONZ_GROUPS, CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DOMAIN)
+
CONF_BRIDGEID = 'bridgeid'
@@ -94,12 +96,15 @@ async def async_step_options(self, user_input=None):
"""Extra options for deCONZ.
CONF_CLIP_SENSOR -- Allow user to choose if they want clip sensors.
+ CONF_DECONZ_GROUPS -- Allow user to choose if they want deCONZ groups.
"""
from pydeconz.utils import async_get_bridgeid
if user_input is not None:
self.deconz_config[CONF_ALLOW_CLIP_SENSOR] = \
user_input[CONF_ALLOW_CLIP_SENSOR]
+ self.deconz_config[CONF_ALLOW_DECONZ_GROUPS] = \
+ user_input[CONF_ALLOW_DECONZ_GROUPS]
if CONF_BRIDGEID not in self.deconz_config:
session = aiohttp_client.async_get_clientsession(self.hass)
@@ -115,6 +120,7 @@ async def async_step_options(self, user_input=None):
step_id='options',
data_schema=vol.Schema({
vol.Optional(CONF_ALLOW_CLIP_SENSOR): bool,
+ vol.Optional(CONF_ALLOW_DECONZ_GROUPS): bool,
}),
)
@@ -158,6 +164,7 @@ async def async_step_import(self, import_config):
return await self.async_step_link()
self.deconz_config[CONF_ALLOW_CLIP_SENSOR] = True
+ self.deconz_config[CONF_ALLOW_DECONZ_GROUPS] = True
return self.async_create_entry(
title='deCONZ-' + self.deconz_config[CONF_BRIDGEID],
data=self.deconz_config
diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py
index 43f3c6441da0de..f7aa4c7a43057d 100644
--- a/homeassistant/components/deconz/const.py
+++ b/homeassistant/components/deconz/const.py
@@ -10,3 +10,4 @@
DATA_DECONZ_UNSUB = 'deconz_dispatchers'
CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor'
+CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups'
diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json
index cabe58694d2a76..09549a300a0d3f 100644
--- a/homeassistant/components/deconz/strings.json
+++ b/homeassistant/components/deconz/strings.json
@@ -16,7 +16,8 @@
"options": {
"title": "Extra configuration options for deCONZ",
"data":{
- "allow_clip_sensor": "Allow importing virtual sensors"
+ "allow_clip_sensor": "Allow importing virtual sensors",
+ "allow_deconz_groups": "Allow importing deCONZ groups"
}
}
},
diff --git a/homeassistant/components/device_tracker/actiontec.py b/homeassistant/components/device_tracker/actiontec.py
index 781e486a40e83f..72d9992c60f3e2 100644
--- a/homeassistant/components/device_tracker/actiontec.py
+++ b/homeassistant/components/device_tracker/actiontec.py
@@ -31,7 +31,6 @@
})
-# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Validate the configuration and return an Actiontec scanner."""
scanner = ActiontecDeviceScanner(config[DOMAIN])
diff --git a/homeassistant/components/device_tracker/aruba.py b/homeassistant/components/device_tracker/aruba.py
index 79d8806fe22bb1..92ef78f60f3b0b 100644
--- a/homeassistant/components/device_tracker/aruba.py
+++ b/homeassistant/components/device_tracker/aruba.py
@@ -30,7 +30,6 @@
})
-# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Validate the configuration and return a Aruba scanner."""
scanner = ArubaDeviceScanner(config[DOMAIN])
diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py
index 7e9b10e9241aa1..5cb7e283c99721 100644
--- a/homeassistant/components/device_tracker/asuswrt.py
+++ b/homeassistant/components/device_tracker/asuswrt.py
@@ -78,7 +78,6 @@
r'.*')
-# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Validate the configuration and return an ASUS-WRT scanner."""
scanner = AsusWrtDeviceScanner(config[DOMAIN])
diff --git a/homeassistant/components/device_tracker/bt_home_hub_5.py b/homeassistant/components/device_tracker/bt_home_hub_5.py
index a3b5bcac77c824..707850d2215c1e 100644
--- a/homeassistant/components/device_tracker/bt_home_hub_5.py
+++ b/homeassistant/components/device_tracker/bt_home_hub_5.py
@@ -26,7 +26,6 @@
})
-# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Return a BT Home Hub 5 scanner if successful."""
scanner = BTHomeHub5DeviceScanner(config[DOMAIN])
diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py
index 3d36a1b428c024..3e17fdd332948b 100644
--- a/homeassistant/components/device_tracker/ddwrt.py
+++ b/homeassistant/components/device_tracker/ddwrt.py
@@ -27,7 +27,6 @@
})
-# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Validate the configuration and return a DD-WRT scanner."""
try:
diff --git a/homeassistant/components/device_tracker/freebox.py b/homeassistant/components/device_tracker/freebox.py
new file mode 100644
index 00000000000000..67957ca99b9f68
--- /dev/null
+++ b/homeassistant/components/device_tracker/freebox.py
@@ -0,0 +1,120 @@
+"""
+Support for device tracking through Freebox routers.
+
+This tracker keeps track of the devices connected to the configured Freebox.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/device_tracker.freebox/
+"""
+import asyncio
+import copy
+import logging
+import socket
+from collections import namedtuple
+from datetime import timedelta
+
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.event import async_track_time_interval
+from homeassistant.components.device_tracker import (
+ PLATFORM_SCHEMA, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
+from homeassistant.const import (
+ CONF_HOST, CONF_PORT)
+
+REQUIREMENTS = ['aiofreepybox==0.0.3']
+
+_LOGGER = logging.getLogger(__name__)
+
+FREEBOX_CONFIG_FILE = 'freebox.conf'
+
+PLATFORM_SCHEMA = vol.All(
+ PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_HOST): cv.string,
+ vol.Required(CONF_PORT): cv.port
+ }))
+
+MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
+
+
+async def async_setup_scanner(hass, config, async_see, discovery_info=None):
+ """Set up the Freebox device tracker and start the polling."""
+ freebox_config = copy.deepcopy(config)
+ if discovery_info is not None:
+ freebox_config[CONF_HOST] = discovery_info['properties']['api_domain']
+ freebox_config[CONF_PORT] = discovery_info['properties']['https_port']
+ _LOGGER.info("Discovered Freebox server: %s:%s",
+ freebox_config[CONF_HOST], freebox_config[CONF_PORT])
+
+ scanner = FreeboxDeviceScanner(hass, freebox_config, async_see)
+ interval = freebox_config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
+ await scanner.async_start(hass, interval)
+ return True
+
+
+Device = namedtuple('Device', ['id', 'name', 'ip'])
+
+
+def _build_device(device_dict):
+ return Device(
+ device_dict['l2ident']['id'],
+ device_dict['primary_name'],
+ device_dict['l3connectivities'][0]['addr'])
+
+
+class FreeboxDeviceScanner(object):
+ """This class scans for devices connected to the Freebox."""
+
+ def __init__(self, hass, config, async_see):
+ """Initialize the scanner."""
+ from aiofreepybox import Freepybox
+
+ self.host = config[CONF_HOST]
+ self.port = config[CONF_PORT]
+ self.token_file = hass.config.path(FREEBOX_CONFIG_FILE)
+ self.async_see = async_see
+
+ # Hardcode the app description to avoid invalidating the authentication
+ # file at each new version.
+ # The version can be changed if we want the user to re-authorize HASS
+ # on her Freebox.
+ app_desc = {
+ 'app_id': 'hass',
+ 'app_name': 'Home Assistant',
+ 'app_version': '0.65',
+ 'device_name': socket.gethostname()
+ }
+
+ api_version = 'v1' # Use the lowest working version.
+ self.fbx = Freepybox(
+ app_desc=app_desc,
+ token_file=self.token_file,
+ api_version=api_version)
+
+ async def async_start(self, hass, interval):
+ """Perform a first update and start polling at the given interval."""
+ await self.async_update_info()
+ interval = max(interval, MIN_TIME_BETWEEN_SCANS)
+ async_track_time_interval(hass, self.async_update_info, interval)
+
+ async def async_update_info(self, now=None):
+ """Check the Freebox for devices."""
+ from aiofreepybox.exceptions import HttpRequestError
+
+ _LOGGER.info('Scanning devices')
+
+ await self.fbx.open(self.host, self.port)
+ try:
+ hosts = await self.fbx.lan.get_hosts_list()
+ except HttpRequestError:
+ _LOGGER.exception('Failed to scan devices')
+ else:
+ active_devices = [_build_device(device)
+ for device in hosts
+ if device['active']]
+
+ if active_devices:
+ await asyncio.wait([self.async_see(mac=d.id, host_name=d.name)
+ for d in active_devices])
+
+ await self.fbx.close()
diff --git a/homeassistant/components/device_tracker/huawei_router.py b/homeassistant/components/device_tracker/huawei_router.py
index 775075b8a4aae9..804269e62280c5 100644
--- a/homeassistant/components/device_tracker/huawei_router.py
+++ b/homeassistant/components/device_tracker/huawei_router.py
@@ -26,7 +26,6 @@
})
-# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Validate the configuration and return a HUAWEI scanner."""
scanner = HuaweiDeviceScanner(config[DOMAIN])
diff --git a/homeassistant/components/device_tracker/sky_hub.py b/homeassistant/components/device_tracker/sky_hub.py
index c48c9bd029b94e..0c289ce9a82e55 100644
--- a/homeassistant/components/device_tracker/sky_hub.py
+++ b/homeassistant/components/device_tracker/sky_hub.py
@@ -23,7 +23,6 @@
})
-# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Return a Sky Hub scanner if successful."""
scanner = SkyHubDeviceScanner(config[DOMAIN])
diff --git a/homeassistant/components/device_tracker/snmp.py b/homeassistant/components/device_tracker/snmp.py
index c9c27fb2bfa848..3d57cb108e243c 100644
--- a/homeassistant/components/device_tracker/snmp.py
+++ b/homeassistant/components/device_tracker/snmp.py
@@ -34,7 +34,6 @@
})
-# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Validate the configuration and return an SNMP scanner."""
scanner = SnmpScanner(config[DOMAIN])
diff --git a/homeassistant/components/device_tracker/thomson.py b/homeassistant/components/device_tracker/thomson.py
index 3fa161e467dec4..8a56fcee7024b5 100644
--- a/homeassistant/components/device_tracker/thomson.py
+++ b/homeassistant/components/device_tracker/thomson.py
@@ -33,7 +33,6 @@
})
-# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Validate the configuration and return a THOMSON scanner."""
scanner = ThomsonDeviceScanner(config[DOMAIN])
diff --git a/homeassistant/components/device_tracker/unifi_direct.py b/homeassistant/components/device_tracker/unifi_direct.py
index 168ab04ec6f14a..c3c4a48bb826ff 100644
--- a/homeassistant/components/device_tracker/unifi_direct.py
+++ b/homeassistant/components/device_tracker/unifi_direct.py
@@ -33,7 +33,6 @@
})
-# pylint: disable=unused-argument
def get_scanner(hass, config):
"""Validate the configuration and return a Unifi direct scanner."""
scanner = UnifiDeviceScanner(config[DOMAIN])
diff --git a/homeassistant/components/device_tracker/xiaomi_miio.py b/homeassistant/components/device_tracker/xiaomi_miio.py
index c5769253657c61..5d6e1453124c09 100644
--- a/homeassistant/components/device_tracker/xiaomi_miio.py
+++ b/homeassistant/components/device_tracker/xiaomi_miio.py
@@ -20,7 +20,7 @@
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
})
-REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
+REQUIREMENTS = ['python-miio==0.4.0', 'construct==2.9.41']
def get_scanner(hass, config):
diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py
index 69447b81cd427e..d7041865892aa5 100644
--- a/homeassistant/components/discovery.py
+++ b/homeassistant/components/discovery.py
@@ -46,7 +46,9 @@
CONFIG_ENTRY_HANDLERS = {
SERVICE_DECONZ: 'deconz',
+ 'google_cast': 'cast',
SERVICE_HUE: 'hue',
+ 'sonos': 'sonos',
}
SERVICE_HANDLERS = {
@@ -64,11 +66,9 @@
SERVICE_SABNZBD: ('sabnzbd', None),
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
SERVICE_KONNECTED: ('konnected', None),
- 'google_cast': ('media_player', 'cast'),
'panasonic_viera': ('media_player', 'panasonic_viera'),
'plex_mediaserver': ('media_player', 'plex'),
'roku': ('media_player', 'roku'),
- 'sonos': ('media_player', 'sonos'),
'yamaha': ('media_player', 'yamaha'),
'logitech_mediaserver': ('media_player', 'squeezebox'),
'directv': ('media_player', 'directv'),
@@ -84,6 +84,7 @@
'kodi': ('media_player', 'kodi'),
'volumio': ('media_player', 'volumio'),
'nanoleaf_aurora': ('light', 'nanoleaf_aurora'),
+ 'freebox': ('device_tracker', 'freebox'),
}
OPTIONAL_SERVICE_HANDLERS = {
diff --git a/homeassistant/components/doorbird.py b/homeassistant/components/doorbird.py
index 48f229b49cad8c..6cd820816e2fef 100644
--- a/homeassistant/components/doorbird.py
+++ b/homeassistant/components/doorbird.py
@@ -4,14 +4,16 @@
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/doorbird/
"""
-import asyncio
import logging
+import asyncio
import voluptuous as vol
-from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.components.http import HomeAssistantView
+from homeassistant.const import CONF_HOST, CONF_USERNAME, \
+ CONF_PASSWORD, CONF_NAME, CONF_DEVICES, CONF_MONITORED_CONDITIONS
import homeassistant.helpers.config_validation as cv
+from homeassistant.util import slugify
REQUIREMENTS = ['DoorBirdPy==0.1.3']
@@ -24,60 +26,139 @@
CONF_DOORBELL_EVENTS = 'doorbell_events'
CONF_CUSTOM_URL = 'hass_url_override'
+DOORBELL_EVENT = 'doorbell'
+MOTION_EVENT = 'motionsensor'
+
+# Sensor types: Name, device_class, event
+SENSOR_TYPES = {
+ 'doorbell': ['Button', 'occupancy', DOORBELL_EVENT],
+ 'motion': ['Motion', 'motion', MOTION_EVENT],
+}
+
+DEVICE_SCHEMA = vol.Schema({
+ vol.Required(CONF_HOST): cv.string,
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ vol.Optional(CONF_CUSTOM_URL): cv.string,
+ vol.Optional(CONF_NAME): cv.string,
+ vol.Optional(CONF_MONITORED_CONDITIONS, default=[]):
+ vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
+})
+
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
- vol.Required(CONF_HOST): cv.string,
- vol.Required(CONF_USERNAME): cv.string,
- vol.Required(CONF_PASSWORD): cv.string,
- vol.Optional(CONF_DOORBELL_EVENTS): cv.boolean,
- vol.Optional(CONF_CUSTOM_URL): cv.string,
- })
+ vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [DEVICE_SCHEMA])
+ }),
}, extra=vol.ALLOW_EXTRA)
-SENSOR_DOORBELL = 'doorbell'
-
def setup(hass, config):
"""Set up the DoorBird component."""
from doorbirdpy import DoorBird
- device_ip = config[DOMAIN].get(CONF_HOST)
- username = config[DOMAIN].get(CONF_USERNAME)
- password = config[DOMAIN].get(CONF_PASSWORD)
+ # Provide an endpoint for the doorstations to call to trigger events
+ hass.http.register_view(DoorbirdRequestView())
+
+ doorstations = []
+
+ for index, doorstation_config in enumerate(config[DOMAIN][CONF_DEVICES]):
+ device_ip = doorstation_config.get(CONF_HOST)
+ username = doorstation_config.get(CONF_USERNAME)
+ password = doorstation_config.get(CONF_PASSWORD)
+ custom_url = doorstation_config.get(CONF_CUSTOM_URL)
+ events = doorstation_config.get(CONF_MONITORED_CONDITIONS)
+ name = (doorstation_config.get(CONF_NAME)
+ or 'DoorBird {}'.format(index + 1))
+
+ device = DoorBird(device_ip, username, password)
+ status = device.ready()
+
+ if status[0]:
+ _LOGGER.info("Connected to DoorBird at %s as %s", device_ip,
+ username)
+ doorstation = ConfiguredDoorbird(device, name, events, custom_url)
+ doorstations.append(doorstation)
+ elif status[1] == 401:
+ _LOGGER.error("Authorization rejected by DoorBird at %s",
+ device_ip)
+ return False
+ else:
+ _LOGGER.error("Could not connect to DoorBird at %s: Error %s",
+ device_ip, str(status[1]))
+ return False
+
+ # SETUP EVENT SUBSCRIBERS
+ if events is not None:
+ # This will make HA the only service that receives events.
+ doorstation.device.reset_notifications()
+
+ # Subscribe to doorbell or motion events
+ subscribe_events(hass, doorstation)
+
+ hass.data[DOMAIN] = doorstations
- device = DoorBird(device_ip, username, password)
- status = device.ready()
+ return True
- if status[0]:
- _LOGGER.info("Connected to DoorBird at %s as %s", device_ip, username)
- hass.data[DOMAIN] = device
- elif status[1] == 401:
- _LOGGER.error("Authorization rejected by DoorBird at %s", device_ip)
- return False
- else:
- _LOGGER.error("Could not connect to DoorBird at %s: Error %s",
- device_ip, str(status[1]))
- return False
- if config[DOMAIN].get(CONF_DOORBELL_EVENTS):
- # Provide an endpoint for the device to call to trigger events
- hass.http.register_view(DoorbirdRequestView())
+def subscribe_events(hass, doorstation):
+ """Initialize the subscriber."""
+ for sensor_type in doorstation.monitored_events:
+ name = '{} {}'.format(doorstation.name,
+ SENSOR_TYPES[sensor_type][0])
+ event_type = SENSOR_TYPES[sensor_type][2]
# Get the URL of this server
hass_url = hass.config.api.base_url
- # Override it if another is specified in the component configuration
- if config[DOMAIN].get(CONF_CUSTOM_URL):
- hass_url = config[DOMAIN].get(CONF_CUSTOM_URL)
- _LOGGER.info("DoorBird will connect to this instance via %s",
- hass_url)
+ # Override url if another is specified onth configuration
+ if doorstation.custom_url is not None:
+ hass_url = doorstation.custom_url
- # This will make HA the only service that gets doorbell events
- url = '{}{}/{}'.format(hass_url, API_URL, SENSOR_DOORBELL)
- device.reset_notifications()
- device.subscribe_notification(SENSOR_DOORBELL, url)
+ slug = slugify(name)
+
+ url = '{}{}/{}'.format(hass_url, API_URL, slug)
+
+ _LOGGER.info("DoorBird will connect to this instance via %s",
+ url)
+
+ _LOGGER.info("You may use the following event name for automations"
+ ": %s_%s", DOMAIN, slug)
+
+ doorstation.device.subscribe_notification(event_type, url)
- return True
+
+class ConfiguredDoorbird():
+ """Attach additional information to pass along with configured device."""
+
+ def __init__(self, device, name, events=None, custom_url=None):
+ """Initialize configured device."""
+ self._name = name
+ self._device = device
+ self._custom_url = custom_url
+ self._monitored_events = events
+
+ @property
+ def name(self):
+ """Custom device name."""
+ return self._name
+
+ @property
+ def device(self):
+ """The configured device."""
+ return self._device
+
+ @property
+ def custom_url(self):
+ """Custom url for device."""
+ return self._custom_url
+
+ @property
+ def monitored_events(self):
+ """Get monitored events."""
+ if self._monitored_events is None:
+ return []
+
+ return self._monitored_events
class DoorbirdRequestView(HomeAssistantView):
@@ -93,5 +174,7 @@ class DoorbirdRequestView(HomeAssistantView):
def get(self, request, sensor):
"""Respond to requests from the device."""
hass = request.app['hass']
+
hass.bus.async_fire('{}_{}'.format(DOMAIN, sensor))
+
return 'OK'
diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py
index 9c29cea704c325..22348dcc297abb 100644
--- a/homeassistant/components/ecobee.py
+++ b/homeassistant/components/ecobee.py
@@ -48,7 +48,6 @@ def request_configuration(network, hass, config):
return
- # pylint: disable=unused-argument
def ecobee_configuration_callback(callback_data):
"""Handle configuration callbacks."""
network.request_tokens()
@@ -106,7 +105,7 @@ def setup(hass, config):
Will automatically load thermostat and sensor components to support
devices discovered on the network.
"""
- # pylint: disable=global-statement, import-error
+ # pylint: disable=import-error
global NETWORK
if 'ecobee' in _CONFIGURING:
diff --git a/homeassistant/components/eight_sleep.py b/homeassistant/components/eight_sleep.py
index 3478d5cd08e7e4..704eab1846bb73 100644
--- a/homeassistant/components/eight_sleep.py
+++ b/homeassistant/components/eight_sleep.py
@@ -4,7 +4,6 @@
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/eight_sleep/
"""
-import asyncio
import logging
from datetime import timedelta
@@ -22,7 +21,7 @@
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util.dt import utcnow
-REQUIREMENTS = ['pyeight==0.0.8']
+REQUIREMENTS = ['pyeight==0.0.9']
_LOGGER = logging.getLogger(__name__)
@@ -86,8 +85,7 @@
}, extra=vol.ALLOW_EXTRA)
-@asyncio.coroutine
-def async_setup(hass, config):
+async def async_setup(hass, config):
"""Set up the Eight Sleep component."""
from pyeight.eight import EightSleep
@@ -107,31 +105,29 @@ def async_setup(hass, config):
hass.data[DATA_EIGHT] = eight
# Authenticate, build sensors
- success = yield from eight.start()
+ success = await eight.start()
if not success:
# Authentication failed, cannot continue
return False
- @asyncio.coroutine
- def async_update_heat_data(now):
+ async def async_update_heat_data(now):
"""Update heat data from eight in HEAT_SCAN_INTERVAL."""
- yield from eight.update_device_data()
+ await eight.update_device_data()
async_dispatcher_send(hass, SIGNAL_UPDATE_HEAT)
async_track_point_in_utc_time(
hass, async_update_heat_data, utcnow() + HEAT_SCAN_INTERVAL)
- @asyncio.coroutine
- def async_update_user_data(now):
+ async def async_update_user_data(now):
"""Update user data from eight in USER_SCAN_INTERVAL."""
- yield from eight.update_user_data()
+ await eight.update_user_data()
async_dispatcher_send(hass, SIGNAL_UPDATE_USER)
async_track_point_in_utc_time(
hass, async_update_user_data, utcnow() + USER_SCAN_INTERVAL)
- yield from async_update_heat_data(None)
- yield from async_update_user_data(None)
+ await async_update_heat_data(None)
+ await async_update_user_data(None)
# Load sub components
sensors = []
@@ -157,8 +153,7 @@ def async_update_user_data(now):
CONF_BINARY_SENSORS: binary_sensors,
}, config))
- @asyncio.coroutine
- def async_service_handler(service):
+ async def async_service_handler(service):
"""Handle eight sleep service calls."""
params = service.data.copy()
@@ -170,7 +165,7 @@ def async_service_handler(service):
side = sens.split('_')[1]
userid = eight.fetch_userid(side)
usrobj = eight.users[userid]
- yield from usrobj.set_heating_level(target, duration)
+ await usrobj.set_heating_level(target, duration)
async_dispatcher_send(hass, SIGNAL_UPDATE_HEAT)
@@ -179,10 +174,9 @@ def async_service_handler(service):
DOMAIN, SERVICE_HEAT_SET, async_service_handler,
schema=SERVICE_EIGHT_SCHEMA)
- @asyncio.coroutine
- def stop_eight(event):
+ async def stop_eight(event):
"""Handle stopping eight api session."""
- yield from eight.stop()
+ await eight.stop()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_eight)
@@ -196,8 +190,7 @@ def __init__(self, eight):
"""Initialize the data object."""
self._eight = eight
- @asyncio.coroutine
- def async_added_to_hass(self):
+ async def async_added_to_hass(self):
"""Register update dispatcher."""
@callback
def async_eight_user_update():
@@ -220,8 +213,7 @@ def __init__(self, eight):
"""Initialize the data object."""
self._eight = eight
- @asyncio.coroutine
- def async_added_to_hass(self):
+ async def async_added_to_hass(self):
"""Register update dispatcher."""
@callback
def async_eight_heat_update():
diff --git a/homeassistant/components/fan/demo.py b/homeassistant/components/fan/demo.py
index b328ebb310174b..c03c492c834a11 100644
--- a/homeassistant/components/fan/demo.py
+++ b/homeassistant/components/fan/demo.py
@@ -13,7 +13,6 @@
LIMITED_SUPPORT = SUPPORT_SET_SPEED
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Set up the demo fan platform."""
add_devices_callback([
diff --git a/homeassistant/components/fan/isy994.py b/homeassistant/components/fan/isy994.py
index 847ca3b325b2bc..97a5f9c3bd69d2 100644
--- a/homeassistant/components/fan/isy994.py
+++ b/homeassistant/components/fan/isy994.py
@@ -30,7 +30,6 @@
STATE_TO_VALUE[VALUE_TO_STATE[key]] = key
-# pylint: disable=unused-argument
def setup_platform(hass, config: ConfigType,
add_devices: Callable[[list], None], discovery_info=None):
"""Set up the ISY994 fan platform."""
diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py
index 2acc3895f3e5a4..1616d38881626d 100644
--- a/homeassistant/components/fan/xiaomi_miio.py
+++ b/homeassistant/components/fan/xiaomi_miio.py
@@ -49,7 +49,7 @@
'zhimi.humidifier.ca1']),
})
-REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
+REQUIREMENTS = ['python-miio==0.4.0', 'construct==2.9.41']
ATTR_MODEL = 'model'
@@ -314,7 +314,6 @@
}
-# pylint: disable=unused-argument
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the miio fan device from config."""
diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py
index 5892e9136d86a6..3d2231ab43b440 100644
--- a/homeassistant/components/frontend/__init__.py
+++ b/homeassistant/components/frontend/__init__.py
@@ -5,7 +5,6 @@
https://home-assistant.io/components/frontend/
"""
import asyncio
-import hashlib
import json
import logging
import os
@@ -22,16 +21,16 @@
from homeassistant.config import find_config_file, load_yaml_config_file
from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
from homeassistant.core import callback
+from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass
+from homeassistant.util.yaml import load_yaml
-REQUIREMENTS = ['home-assistant-frontend==20180608.0b0']
+REQUIREMENTS = ['home-assistant-frontend==20180622.1']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
-URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
-
CONF_THEMES = 'themes'
CONF_EXTRA_HTML_URL = 'extra_html_url'
CONF_EXTRA_HTML_URL_ES5 = 'extra_html_url_es5'
@@ -99,9 +98,22 @@
SCHEMA_GET_PANELS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_GET_PANELS,
})
+WS_TYPE_GET_THEMES = 'frontend/get_themes'
+SCHEMA_GET_THEMES = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
+ vol.Required('type'): WS_TYPE_GET_THEMES,
+})
+WS_TYPE_GET_TRANSLATIONS = 'frontend/get_translations'
+SCHEMA_GET_TRANSLATIONS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
+ vol.Required('type'): WS_TYPE_GET_TRANSLATIONS,
+ vol.Required('language'): str,
+})
+WS_TYPE_GET_LOVELACE_UI = 'frontend/lovelace_config'
+SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
+ vol.Required('type'): WS_TYPE_GET_LOVELACE_UI,
+})
-class AbstractPanel:
+class Panel:
"""Abstract class for panels."""
# Name of the webcomponent
@@ -113,30 +125,20 @@ class AbstractPanel:
# Title to show in the sidebar (optional)
sidebar_title = None
- # Url to the webcomponent (depending on JS version)
- webcomponent_url_es5 = None
- webcomponent_url_latest = None
-
# Url to show the panel in the frontend
frontend_url_path = None
# Config to pass to the webcomponent
config = None
- @asyncio.coroutine
- def async_register(self, hass):
- """Register panel with HASS."""
- panels = hass.data.get(DATA_PANELS)
- if panels is None:
- panels = hass.data[DATA_PANELS] = {}
-
- if self.frontend_url_path in panels:
- _LOGGER.warning("Overwriting component %s", self.frontend_url_path)
-
- if DATA_FINALIZE_PANEL in hass.data:
- yield from hass.data[DATA_FINALIZE_PANEL](self)
-
- panels[self.frontend_url_path] = self
+ def __init__(self, component_name, sidebar_title, sidebar_icon,
+ frontend_url_path, config):
+ """Initialize a built-in panel."""
+ self.component_name = component_name
+ self.sidebar_title = sidebar_title
+ self.sidebar_icon = sidebar_icon
+ self.frontend_url_path = frontend_url_path or component_name
+ self.config = config
@callback
def async_register_index_routes(self, router, index_view):
@@ -147,19 +149,7 @@ def async_register_index_routes(self, router, index_view):
'get', '/{}/{{extra:.+}}'.format(self.frontend_url_path),
index_view.get)
-
-class BuiltInPanel(AbstractPanel):
- """Panel that is part of hass_frontend."""
-
- def __init__(self, component_name, sidebar_title, sidebar_icon,
- frontend_url_path, config):
- """Initialize a built-in panel."""
- self.component_name = component_name
- self.sidebar_title = sidebar_title
- self.sidebar_icon = sidebar_icon
- self.frontend_url_path = frontend_url_path or component_name
- self.config = config
-
+ @callback
def to_response(self, hass, request):
"""Panel as dictionary."""
return {
@@ -171,95 +161,25 @@ def to_response(self, hass, request):
}
-class ExternalPanel(AbstractPanel):
- """Panel that is added by a custom component."""
-
- REGISTERED_COMPONENTS = set()
-
- def __init__(self, component_name, path, md5, sidebar_title, sidebar_icon,
- frontend_url_path, config):
- """Initialize an external panel."""
- self.component_name = component_name
- self.path = path
- self.md5 = md5
- self.sidebar_title = sidebar_title
- self.sidebar_icon = sidebar_icon
- self.frontend_url_path = frontend_url_path or component_name
- self.config = config
-
- @asyncio.coroutine
- def async_finalize(self, hass, frontend_repository_path):
- """Finalize this panel for usage.
-
- frontend_repository_path is set, will be prepended to path of built-in
- components.
- """
- try:
- if self.md5 is None:
- self.md5 = yield from hass.async_add_job(
- _fingerprint, self.path)
- except OSError:
- _LOGGER.error('Cannot find or access %s at %s',
- self.component_name, self.path)
- hass.data[DATA_PANELS].pop(self.frontend_url_path)
- return
-
- self.webcomponent_url_es5 = self.webcomponent_url_latest = \
- URL_PANEL_COMPONENT_FP.format(self.component_name, self.md5)
-
- if self.component_name not in self.REGISTERED_COMPONENTS:
- hass.http.register_static_path(
- self.webcomponent_url_latest, self.path,
- # if path is None, we're in prod mode, so cache static assets
- frontend_repository_path is None)
- self.REGISTERED_COMPONENTS.add(self.component_name)
-
- def to_response(self, hass, request):
- """Panel as dictionary."""
- result = {
- 'component_name': self.component_name,
- 'icon': self.sidebar_icon,
- 'title': self.sidebar_title,
- 'url_path': self.frontend_url_path,
- 'config': self.config,
- }
- if _is_latest(hass.data[DATA_JS_VERSION], request):
- result['url'] = self.webcomponent_url_latest
- else:
- result['url'] = self.webcomponent_url_es5
- return result
-
-
@bind_hass
-@asyncio.coroutine
-def async_register_built_in_panel(hass, component_name, sidebar_title=None,
- sidebar_icon=None, frontend_url_path=None,
- config=None):
+async def async_register_built_in_panel(hass, component_name,
+ sidebar_title=None, sidebar_icon=None,
+ frontend_url_path=None, config=None):
"""Register a built-in panel."""
- panel = BuiltInPanel(component_name, sidebar_title, sidebar_icon,
- frontend_url_path, config)
- yield from panel.async_register(hass)
+ panel = Panel(component_name, sidebar_title, sidebar_icon,
+ frontend_url_path, config)
+ panels = hass.data.get(DATA_PANELS)
+ if panels is None:
+ panels = hass.data[DATA_PANELS] = {}
-@bind_hass
-@asyncio.coroutine
-def async_register_panel(hass, component_name, path, md5=None,
- sidebar_title=None, sidebar_icon=None,
- frontend_url_path=None, config=None):
- """Register a panel for the frontend.
-
- component_name: name of the web component
- path: path to the HTML of the web component
- (required unless url is provided)
- md5: the md5 hash of the web component (for versioning in URL, optional)
- sidebar_title: title to show in the sidebar (optional)
- sidebar_icon: icon to show next to title in sidebar (optional)
- url_path: name to use in the URL (defaults to component_name)
- config: config to be passed into the web component
- """
- panel = ExternalPanel(component_name, path, md5, sidebar_title,
- sidebar_icon, frontend_url_path, config)
- yield from panel.async_register(hass)
+ if panel.frontend_url_path in panels:
+ _LOGGER.warning("Overwriting component %s", panel.frontend_url_path)
+
+ if DATA_FINALIZE_PANEL in hass.data:
+ hass.data[DATA_FINALIZE_PANEL](panel)
+
+ panels[panel.frontend_url_path] = panel
@bind_hass
@@ -278,11 +198,10 @@ def add_manifest_json_key(key, val):
MANIFEST_JSON[key] = val
-@asyncio.coroutine
-def async_setup(hass, config):
+async def async_setup(hass, config):
"""Set up the serving of the frontend."""
if list(hass.auth.async_auth_providers):
- client = yield from hass.auth.async_create_client(
+ client = await hass.auth.async_create_client(
'Home Assistant Frontend',
redirect_uris=['/'],
no_secret=True,
@@ -291,7 +210,15 @@ def async_setup(hass, config):
client = None
hass.components.websocket_api.async_register_command(
- WS_TYPE_GET_PANELS, websocket_handle_get_panels, SCHEMA_GET_PANELS)
+ WS_TYPE_GET_PANELS, websocket_get_panels, SCHEMA_GET_PANELS)
+ hass.components.websocket_api.async_register_command(
+ WS_TYPE_GET_THEMES, websocket_get_themes, SCHEMA_GET_THEMES)
+ hass.components.websocket_api.async_register_command(
+ WS_TYPE_GET_TRANSLATIONS, websocket_get_translations,
+ SCHEMA_GET_TRANSLATIONS)
+ hass.components.websocket_api.async_register_command(
+ WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config,
+ SCHEMA_GET_LOVELACE_UI)
hass.http.register_view(ManifestJSONView)
conf = config.get(DOMAIN, {})
@@ -331,24 +258,23 @@ def async_setup(hass, config):
index_view = IndexView(repo_path, js_version, client)
hass.http.register_view(index_view)
- async def finalize_panel(panel):
+ @callback
+ def async_finalize_panel(panel):
"""Finalize setup of a panel."""
- if hasattr(panel, 'async_finalize'):
- await panel.async_finalize(hass, repo_path)
panel.async_register_index_routes(hass.http.app.router, index_view)
- yield from asyncio.wait([
- async_register_built_in_panel(hass, panel)
- for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state',
- 'dev-template', 'dev-mqtt', 'kiosk')], loop=hass.loop)
+ await asyncio.wait(
+ [async_register_built_in_panel(hass, panel) for panel in (
+ 'dev-event', 'dev-info', 'dev-service', 'dev-state',
+ 'dev-template', 'dev-mqtt', 'kiosk', 'lovelace')],
+ loop=hass.loop)
- hass.data[DATA_FINALIZE_PANEL] = finalize_panel
+ hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel
# Finalize registration of panels that registered before frontend was setup
# This includes the built-in panels from line above.
- yield from asyncio.wait(
- [finalize_panel(panel) for panel in hass.data[DATA_PANELS].values()],
- loop=hass.loop)
+ for panel in hass.data[DATA_PANELS].values():
+ async_finalize_panel(panel)
if DATA_EXTRA_HTML_URL not in hass.data:
hass.data[DATA_EXTRA_HTML_URL] = set()
@@ -360,16 +286,14 @@ async def finalize_panel(panel):
for url in conf.get(CONF_EXTRA_HTML_URL_ES5, []):
add_extra_html_url(hass, url, True)
- async_setup_themes(hass, conf.get(CONF_THEMES))
-
- hass.http.register_view(TranslationsView)
+ _async_setup_themes(hass, conf.get(CONF_THEMES))
return True
-def async_setup_themes(hass, themes):
+@callback
+def _async_setup_themes(hass, themes):
"""Set up themes data and services."""
- hass.http.register_view(ThemesView)
hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME
if themes is None:
hass.data[DATA_THEMES] = {}
@@ -456,38 +380,23 @@ def get_template(self, latest):
return tpl
- @asyncio.coroutine
- def get(self, request, extra=None):
+ async def get(self, request, extra=None):
"""Serve the index view."""
hass = request.app['hass']
latest = self.repo_path is not None or \
_is_latest(self.js_option, request)
- if request.path == '/':
- panel = 'states'
- else:
- panel = request.path.split('/')[1]
-
- if panel == 'states':
- panel_url = ''
- elif latest:
- panel_url = hass.data[DATA_PANELS][panel].webcomponent_url_latest
- else:
- panel_url = hass.data[DATA_PANELS][panel].webcomponent_url_es5
-
no_auth = '1'
if hass.config.api.api_password and not request[KEY_AUTHENTICATED]:
# do not try to auto connect on load
no_auth = '0'
- template = yield from hass.async_add_job(self.get_template, latest)
+ template = await hass.async_add_job(self.get_template, latest)
extra_key = DATA_EXTRA_HTML_URL if latest else DATA_EXTRA_HTML_URL_ES5
template_params = dict(
no_auth=no_auth,
- panel_url=panel_url,
- panels=hass.data[DATA_PANELS],
theme_color=MANIFEST_JSON['theme_color'],
extra_urls=hass.data[extra_key],
)
@@ -506,54 +415,13 @@ class ManifestJSONView(HomeAssistantView):
url = '/manifest.json'
name = 'manifestjson'
- @asyncio.coroutine
+ @callback
def get(self, request): # pylint: disable=no-self-use
"""Return the manifest.json."""
msg = json.dumps(MANIFEST_JSON, sort_keys=True)
return web.Response(text=msg, content_type="application/manifest+json")
-class ThemesView(HomeAssistantView):
- """View to return defined themes."""
-
- requires_auth = False
- url = '/api/themes'
- name = 'api:themes'
-
- @callback
- def get(self, request):
- """Return themes."""
- hass = request.app['hass']
-
- return self.json({
- 'themes': hass.data[DATA_THEMES],
- 'default_theme': hass.data[DATA_DEFAULT_THEME],
- })
-
-
-class TranslationsView(HomeAssistantView):
- """View to return backend defined translations."""
-
- url = '/api/translations/{language}'
- name = 'api:translations'
-
- @asyncio.coroutine
- def get(self, request, language):
- """Return translations."""
- hass = request.app['hass']
-
- resources = yield from async_get_translations(hass, language)
- return self.json({
- 'resources': resources,
- })
-
-
-def _fingerprint(path):
- """Fingerprint a file."""
- with open(path) as fil:
- return hashlib.md5(fil.read().encode('utf-8')).hexdigest()
-
-
def _is_latest(js_option, request):
"""
Return whether we should serve latest untranspiled code.
@@ -587,7 +455,7 @@ def _is_latest(js_option, request):
@callback
-def websocket_handle_get_panels(hass, connection, msg):
+def websocket_get_panels(hass, connection, msg):
"""Handle get panels command.
Async friendly.
@@ -600,3 +468,58 @@ def websocket_handle_get_panels(hass, connection, msg):
connection.to_write.put_nowait(websocket_api.result_message(
msg['id'], panels))
+
+
+@callback
+def websocket_get_themes(hass, connection, msg):
+ """Handle get themes command.
+
+ Async friendly.
+ """
+ connection.to_write.put_nowait(websocket_api.result_message(msg['id'], {
+ 'themes': hass.data[DATA_THEMES],
+ 'default_theme': hass.data[DATA_DEFAULT_THEME],
+ }))
+
+
+@callback
+def websocket_get_translations(hass, connection, msg):
+ """Handle get translations command.
+
+ Async friendly.
+ """
+ async def send_translations():
+ """Send a camera still."""
+ resources = await async_get_translations(hass, msg['language'])
+ connection.send_message_outside(websocket_api.result_message(
+ msg['id'], {
+ 'resources': resources,
+ }
+ ))
+
+ hass.async_add_job(send_translations())
+
+
+def websocket_lovelace_config(hass, connection, msg):
+ """Send lovelace UI config over websocket config."""
+ async def send_exp_config():
+ """Send lovelace frontend config."""
+ error = None
+ try:
+ config = await hass.async_add_job(
+ load_yaml, hass.config.path('ui-lovelace.yaml'))
+ message = websocket_api.result_message(
+ msg['id'], config
+ )
+ except FileNotFoundError:
+ error = ('file_not_found',
+ 'Could not find ui-lovelace.yaml in your config dir.')
+ except HomeAssistantError as err:
+ error = 'load_error', str(err)
+
+ if error is not None:
+ message = websocket_api.error_message(msg['id'], *error)
+
+ connection.send_message_outside(message)
+
+ hass.async_add_job(send_exp_config())
diff --git a/homeassistant/components/frontend/www_static/images/logo_tellduslive.png b/homeassistant/components/frontend/www_static/images/logo_tellduslive.png
deleted file mode 100644
index 7ea78f8ef3aad4..00000000000000
Binary files a/homeassistant/components/frontend/www_static/images/logo_tellduslive.png and /dev/null differ
diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py
index 0fbb2a57ca95f0..6ab86435371f1b 100644
--- a/homeassistant/components/hassio/__init__.py
+++ b/homeassistant/components/hassio/__init__.py
@@ -171,14 +171,20 @@ def async_setup(hass, config):
development_repo = config.get(DOMAIN, {}).get(CONF_FRONTEND_REPO)
if development_repo is not None:
hass.http.register_static_path(
- '/api/hassio/app-es5',
- os.path.join(development_repo, 'hassio/build-es5'), False)
+ '/api/hassio/app',
+ os.path.join(development_repo, 'hassio/build'), False)
hass.http.register_view(HassIOView(host, websession))
if 'frontend' in hass.config.components:
- yield from hass.components.frontend.async_register_built_in_panel(
- 'hassio', 'Hass.io', 'hass:home-assistant')
+ yield from hass.components.panel_custom.async_register_panel(
+ frontend_url_path='hassio',
+ webcomponent_name='hassio-main',
+ sidebar_title='Hass.io',
+ sidebar_icon='hass:home-assistant',
+ js_url='/api/hassio/app/entrypoint.js',
+ embed_iframe=True,
+ )
if 'http' in config:
yield from hassio.update_hass_api(config['http'])
diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py
index bb4f8219a333bd..c51d45cc3396eb 100644
--- a/homeassistant/components/hassio/http.py
+++ b/homeassistant/components/hassio/http.py
@@ -36,7 +36,7 @@
}
NO_AUTH = {
- re.compile(r'^app-(es5|latest)/.+$'),
+ re.compile(r'^app/.*$'),
re.compile(r'^addons/[^/]*/logo$')
}
diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py
index e36e7439e09d63..0883c5a3cc85b0 100644
--- a/homeassistant/components/homekit_controller/__init__.py
+++ b/homeassistant/components/homekit_controller/__init__.py
@@ -37,6 +37,7 @@ def homekit_http_send(self, message_body=None, encode_chunked=False):
Appends an extra \r\n to the buffer.
A message_body may be specified, to be appended to the request.
"""
+ # pylint: disable=protected-access
self._buffer.extend((b"", b""))
msg = b"\r\n".join(self._buffer)
del self._buffer[:]
diff --git a/homeassistant/components/hue/.translations/ca.json b/homeassistant/components/hue/.translations/ca.json
new file mode 100644
index 00000000000000..6c41eed5467ac9
--- /dev/null
+++ b/homeassistant/components/hue/.translations/ca.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "all_configured": "Tots els enlla\u00e7os Philips Hue ja estan configurats",
+ "already_configured": "L'enlla\u00e7 ja est\u00e0 configurat",
+ "cannot_connect": "No es pot connectar amb l'enlla\u00e7",
+ "discover_timeout": "No s'han pogut descobrir enlla\u00e7os Hue",
+ "no_bridges": "No s'han trobat enlla\u00e7os Philips Hue",
+ "unknown": "S'ha produ\u00eft un error desconegut"
+ },
+ "error": {
+ "linking": "S'ha produ\u00eft un error desconegut al vincular.",
+ "register_failed": "No s'ha pogut registrar, torneu-ho a provar"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "host": "Amfitri\u00f3"
+ },
+ "title": "Tria l'enlla\u00e7 Hue"
+ },
+ "link": {
+ "description": "Premeu el bot\u00f3 de l'ella\u00e7 per registrar Philips Hue amb Home Assistant. \n\n ![Ubicaci\u00f3 del bot\u00f3 al pont](/static/images/config_philips_hue.jpg)",
+ "title": "Vincular concentrador"
+ }
+ },
+ "title": "Philips Hue"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hue/.translations/cs.json b/homeassistant/components/hue/.translations/cs.json
new file mode 100644
index 00000000000000..35c423b1a03420
--- /dev/null
+++ b/homeassistant/components/hue/.translations/cs.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "all_configured": "V\u0161echny Philips Hue p\u0159emost\u011bn\u00ed jsou ji\u017e nakonfigurov\u00e1ny",
+ "already_configured": "P\u0159emost\u011bn\u00ed je ji\u017e nakonfigurov\u00e1no",
+ "cannot_connect": "Nelze se p\u0159ipojit k p\u0159emost\u011bn\u00ed",
+ "discover_timeout": "Nelze nal\u00e9zt p\u0159emost\u011bn\u00ed Hue",
+ "no_bridges": "Nebyly nalezeny \u017e\u00e1dn\u00e9 p\u0159emost\u011bn\u00ed Philips Hue",
+ "unknown": "Do\u0161lo k nezn\u00e1m\u00e9 chyb\u011b"
+ },
+ "error": {
+ "linking": "Do\u0161lo k nezn\u00e1m\u00e9 chyb\u011b propojen\u00ed.",
+ "register_failed": "Registrace se nezda\u0159ila, zkuste to pros\u00edm znovu"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "host": "Hostitel"
+ },
+ "title": "Vybrat Hue p\u0159emost\u011bn\u00ed"
+ },
+ "link": {
+ "description": "Stiskn\u011bte tla\u010d\u00edtko na p\u0159emost\u011bn\u00ed k registraci Philips Hue v Home Assistant.\n\n! [Um\u00edst\u011bn\u00ed tla\u010d\u00edtka na p\u0159emost\u011bn\u00ed] (/ static/images/config_philips_hue.jpg)",
+ "title": "P\u0159ipojit Hub"
+ }
+ },
+ "title": "Philips Hue p\u0159emost\u011bn\u00ed"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hue/.translations/en.json b/homeassistant/components/hue/.translations/en.json
index b0459ec39163ab..cea8d8be10af34 100644
--- a/homeassistant/components/hue/.translations/en.json
+++ b/homeassistant/components/hue/.translations/en.json
@@ -24,6 +24,6 @@
"title": "Link Hub"
}
},
- "title": "Philips Hue Bridge"
+ "title": "Philips Hue"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/hue/.translations/fr.json b/homeassistant/components/hue/.translations/fr.json
new file mode 100644
index 00000000000000..73613f237dac3b
--- /dev/null
+++ b/homeassistant/components/hue/.translations/fr.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "all_configured": "Tous les ponts Philips Hue sont d\u00e9j\u00e0 configur\u00e9s",
+ "already_configured": "Ce pont est d\u00e9j\u00e0 configur\u00e9",
+ "cannot_connect": "Connexion au pont impossible",
+ "discover_timeout": "D\u00e9tection de ponts Philips Hue impossible",
+ "no_bridges": "Aucun pont Philips Hue n'a \u00e9t\u00e9 d\u00e9couvert",
+ "unknown": "Une erreur inconnue s'est produite"
+ },
+ "error": {
+ "linking": "Une erreur inconnue s'est produite lors de la liaison entre le pont et Home Assistant",
+ "register_failed": "\u00c9chec d'enregistrement. Veuillez r\u00e9essayer."
+ },
+ "step": {
+ "init": {
+ "data": {
+ "host": "H\u00f4te"
+ },
+ "title": "Choisissez le pont Philips Hue"
+ },
+ "link": {
+ "description": "Appuyez sur le bouton du pont pour lier Philips Hue avec Home Assistant. \n\n ![Emplacement du bouton sur le pont] (/static/images/config_philips_hue.jpg)",
+ "title": "Hub de liaison"
+ }
+ },
+ "title": "Pont Philips Hue"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hue/.translations/hu.json b/homeassistant/components/hue/.translations/hu.json
index a4032dcbcfc215..be6548f59a0e91 100644
--- a/homeassistant/components/hue/.translations/hu.json
+++ b/homeassistant/components/hue/.translations/hu.json
@@ -2,7 +2,7 @@
"config": {
"abort": {
"all_configured": "M\u00e1r minden Philips Hue bridge konfigur\u00e1lt",
- "already_configured": "A bridge m\u00e1r konfigur\u00e1lt",
+ "already_configured": "A bridge m\u00e1r konfigur\u00e1lva van",
"cannot_connect": "Nem siker\u00fclt csatlakozni a bridge-hez.",
"discover_timeout": "Nem tal\u00e1ltam a Hue bridget",
"no_bridges": "Nem tal\u00e1ltam Philips Hue bridget",
@@ -20,6 +20,7 @@
"title": "V\u00e1lassz Hue bridge-t"
},
"link": {
+ "description": "Nyomja meg a gombot a bridge-en a Philips Hue Home Assistant-ben val\u00f3 regisztr\u00e1l\u00e1s\u00e1hoz.\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)",
"title": "Kapcsol\u00f3d\u00e1s a hubhoz"
}
},
diff --git a/homeassistant/components/hue/.translations/it.json b/homeassistant/components/hue/.translations/it.json
index 2c7a8c1924d6db..a9f2a732127a23 100644
--- a/homeassistant/components/hue/.translations/it.json
+++ b/homeassistant/components/hue/.translations/it.json
@@ -2,8 +2,27 @@
"config": {
"abort": {
"all_configured": "Tutti i bridge Philips Hue sono gi\u00e0 configurati",
+ "already_configured": "Il Bridge \u00e8 gi\u00e0 configurato",
+ "cannot_connect": "Impossibile connettersi al bridge",
"discover_timeout": "Impossibile trovare i bridge Hue",
- "no_bridges": "Nessun bridge Hue di Philips trovato"
+ "no_bridges": "Nessun bridge Hue di Philips trovato",
+ "unknown": "Si \u00e8 verificato un errore"
+ },
+ "error": {
+ "linking": "Si \u00e8 verificato un errore sconosciuto in fase di collegamento.",
+ "register_failed": "Errore in fase di registrazione, riprova"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "host": "Host"
+ },
+ "title": "Selezione il bridge Hue"
+ },
+ "link": {
+ "description": "Premi il pulsante sul bridge per registrare Philips Hue con Home Assistant\n\n![Posizione del pulsante sul bridge](/static/images/config_philips_hue.jpg)",
+ "title": "Collega Hub"
+ }
},
"title": "Philips Hue Bridge"
}
diff --git a/homeassistant/components/hue/.translations/pt-BR.json b/homeassistant/components/hue/.translations/pt-BR.json
new file mode 100644
index 00000000000000..5c6e409245c7e9
--- /dev/null
+++ b/homeassistant/components/hue/.translations/pt-BR.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "all_configured": "Todas as pontes Philips Hue j\u00e1 est\u00e3o configuradas",
+ "already_configured": "A ponte j\u00e1 est\u00e1 configurada",
+ "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel conectar-se \u00e0 ponte",
+ "discover_timeout": "Incapaz de descobrir pontes Hue",
+ "no_bridges": "N\u00e3o h\u00e1 pontes Philips Hue descobertas",
+ "unknown": "Ocorreu um erro desconhecido"
+ },
+ "error": {
+ "linking": "Ocorreu um erro de liga\u00e7\u00e3o desconhecido.",
+ "register_failed": "Falhou ao registrar, por favor tente novamente"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "host": "Hospedeiro"
+ },
+ "title": "Escolha a ponte Hue"
+ },
+ "link": {
+ "description": "Pressione o bot\u00e3o na ponte para registrar o Philips Hue com o Home Assistant. \n\n ![Localiza\u00e7\u00e3o do bot\u00e3o na ponte] (/static/images/config_philips_hue.jpg)",
+ "title": "Hub de links"
+ }
+ },
+ "title": "Philips Hue"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hue/.translations/pt.json b/homeassistant/components/hue/.translations/pt.json
index 8c4c45f9c897c8..f7988d82d8ce3b 100644
--- a/homeassistant/components/hue/.translations/pt.json
+++ b/homeassistant/components/hue/.translations/pt.json
@@ -1,5 +1,29 @@
{
"config": {
+ "abort": {
+ "all_configured": "Todas os Philips Hue j\u00e1 est\u00e3o configuradas",
+ "already_configured": "Hue j\u00e1 est\u00e1 configurado",
+ "cannot_connect": "N\u00e3o foi poss\u00edvel se conectar",
+ "discover_timeout": "Nenhum Hue bridge descoberto",
+ "no_bridges": "Nenhum Philips Hue descoberto",
+ "unknown": "Ocorreu um erro desconhecido"
+ },
+ "error": {
+ "linking": "Ocorreu um erro de liga\u00e7\u00e3o desconhecido.",
+ "register_failed": "Falha ao registrar, por favor, tente novamente"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "host": "Servidor"
+ },
+ "title": "Hue bridge"
+ },
+ "link": {
+ "description": "Pressione o bot\u00e3o no Philips Hue para registrar com o Home Assistant. \n\n ! [Localiza\u00e7\u00e3o do bot\u00e3o] (/ static / images / config_philips_hue.jpg)",
+ "title": "Link Hub"
+ }
+ },
"title": ""
}
}
\ No newline at end of file
diff --git a/homeassistant/components/hue/.translations/sv.json b/homeassistant/components/hue/.translations/sv.json
new file mode 100644
index 00000000000000..efbcfa544f5d81
--- /dev/null
+++ b/homeassistant/components/hue/.translations/sv.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "all_configured": "Alla Philips Hue-bryggor \u00e4r redan konfigurerade",
+ "already_configured": "Bryggan \u00e4r redan konfigurerad",
+ "cannot_connect": "Det gick inte att ansluta till bryggan",
+ "discover_timeout": "Det gick inte att uppt\u00e4cka n\u00e5gra Hue-bryggor",
+ "no_bridges": "Inga Philips Hue-bryggor uppt\u00e4cktes",
+ "unknown": "Ett ok\u00e4nt fel intr\u00e4ffade"
+ },
+ "error": {
+ "linking": "Ett ok\u00e4nt l\u00e4nkningsfel intr\u00e4ffade.",
+ "register_failed": "Misslyckades med att registrera, v\u00e4nligen f\u00f6rs\u00f6k igen"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "host": "V\u00e4rd"
+ },
+ "title": "V\u00e4lj Hue-brygga"
+ },
+ "link": {
+ "description": "Tryck p\u00e5 knappen p\u00e5 bryggan f\u00f6r att registrera Philips Hue med Home Assistant. \n\n ! [Placering av knapp p\u00e5 brygga] (/ static / images / config_philips_hue.jpg)",
+ "title": "L\u00e4nka hub"
+ }
+ },
+ "title": "Philips Hue Brygga"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hue/.translations/vi.json b/homeassistant/components/hue/.translations/vi.json
new file mode 100644
index 00000000000000..5cbd0c4aebfbd0
--- /dev/null
+++ b/homeassistant/components/hue/.translations/vi.json
@@ -0,0 +1,17 @@
+{
+ "config": {
+ "abort": {
+ "all_configured": "T\u1ea5t c\u1ea3 c\u00e1c c\u1ea7u Philips Hue \u0111\u00e3 \u0111\u01b0\u1ee3c c\u1ea5u h\u00ecnh",
+ "unknown": "X\u1ea3y ra l\u1ed7i kh\u00f4ng x\u00e1c \u0111\u1ecbnh \u0111\u01b0\u1ee3c"
+ },
+ "error": {
+ "linking": "\u0110\u00e3 x\u1ea3y ra l\u1ed7i li\u00ean k\u1ebft kh\u00f4ng x\u00e1c \u0111\u1ecbnh.",
+ "register_failed": "Kh\u00f4ng th\u1ec3 \u0111\u0103ng k\u00fd, vui l\u00f2ng th\u1eed l\u1ea1i"
+ },
+ "step": {
+ "link": {
+ "title": "Li\u00ean k\u1ebft Hub"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py
index 251d8cba095bec..dbd86ef31f344d 100644
--- a/homeassistant/components/hue/__init__.py
+++ b/homeassistant/components/hue/__init__.py
@@ -9,6 +9,7 @@
import voluptuous as vol
+from homeassistant import data_entry_flow
from homeassistant.const import CONF_FILENAME, CONF_HOST
from homeassistant.helpers import aiohttp_client, config_validation as cv
@@ -107,7 +108,7 @@ async def async_setup(hass, config):
# deadlock: creating a config entry will set up the component but the
# setup would block till the entry is created!
hass.async_add_job(hass.config_entries.flow.async_init(
- DOMAIN, source='import', data={
+ DOMAIN, source=data_entry_flow.SOURCE_IMPORT, data={
'host': bridge_conf[CONF_HOST],
'path': bridge_conf[CONF_FILENAME],
}
diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json
index fc9e91c93d7523..f8873894a01bf0 100644
--- a/homeassistant/components/hue/strings.json
+++ b/homeassistant/components/hue/strings.json
@@ -1,6 +1,6 @@
{
"config": {
- "title": "Philips Hue Bridge",
+ "title": "Philips Hue",
"step": {
"init": {
"title": "Pick Hue bridge",
diff --git a/homeassistant/components/image_processing/facebox.py b/homeassistant/components/image_processing/facebox.py
index 81b43c1f8e0ce1..f556b62e935425 100644
--- a/homeassistant/components/image_processing/facebox.py
+++ b/homeassistant/components/image_processing/facebox.py
@@ -10,16 +10,22 @@
import requests
import voluptuous as vol
+from homeassistant.const import ATTR_NAME
from homeassistant.core import split_entity_id
import homeassistant.helpers.config_validation as cv
from homeassistant.components.image_processing import (
- PLATFORM_SCHEMA, ImageProcessingFaceEntity, CONF_SOURCE, CONF_ENTITY_ID,
- CONF_NAME)
+ PLATFORM_SCHEMA, ImageProcessingFaceEntity, ATTR_CONFIDENCE, CONF_SOURCE,
+ CONF_ENTITY_ID, CONF_NAME)
from homeassistant.const import (CONF_IP_ADDRESS, CONF_PORT)
_LOGGER = logging.getLogger(__name__)
+ATTR_BOUNDING_BOX = 'bounding_box'
+ATTR_IMAGE_ID = 'image_id'
+ATTR_MATCHED = 'matched'
CLASSIFIER = 'facebox'
+TIMEOUT = 9
+
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_IP_ADDRESS): cv.string,
@@ -30,7 +36,7 @@
def encode_image(image):
"""base64 encode an image stream."""
base64_img = base64.b64encode(image).decode('ascii')
- return {"base64": base64_img}
+ return base64_img
def get_matched_faces(faces):
@@ -39,6 +45,24 @@ def get_matched_faces(faces):
for face in faces if face['matched']}
+def parse_faces(api_faces):
+ """Parse the API face data into the format required."""
+ known_faces = []
+ for entry in api_faces:
+ face = {}
+ if entry['matched']: # This data is only in matched faces.
+ face[ATTR_NAME] = entry['name']
+ face[ATTR_IMAGE_ID] = entry['id']
+ else: # Lets be explicit.
+ face[ATTR_NAME] = None
+ face[ATTR_IMAGE_ID] = None
+ face[ATTR_CONFIDENCE] = round(100.0*entry['confidence'], 2)
+ face[ATTR_MATCHED] = entry['matched']
+ face[ATTR_BOUNDING_BOX] = entry['rect']
+ known_faces.append(face)
+ return known_faces
+
+
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the classifier."""
entities = []
@@ -74,18 +98,18 @@ def process_image(self, image):
try:
response = requests.post(
self._url,
- json=encode_image(image),
- timeout=9
+ json={"base64": encode_image(image)},
+ timeout=TIMEOUT
).json()
except requests.exceptions.ConnectionError:
_LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER)
response['success'] = False
if response['success']:
- faces = response['faces']
- total = response['facesCount']
- self.process_faces(faces, total)
+ total_faces = response['facesCount']
+ faces = parse_faces(response['faces'])
self._matched = get_matched_faces(faces)
+ self.process_faces(faces, total_faces)
else:
self.total_faces = None
diff --git a/homeassistant/components/insteon_plm/__init__.py b/homeassistant/components/insteon_plm/__init__.py
index b86f80cbee788d..b2f7c8b66551bd 100644
--- a/homeassistant/components/insteon_plm/__init__.py
+++ b/homeassistant/components/insteon_plm/__init__.py
@@ -17,7 +17,7 @@
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['insteonplm==0.9.2']
+REQUIREMENTS = ['insteonplm==0.10.0']
_LOGGER = logging.getLogger(__name__)
@@ -29,17 +29,31 @@
CONF_SUBCAT = 'subcat'
CONF_FIRMWARE = 'firmware'
CONF_PRODUCT_KEY = 'product_key'
+CONF_X10 = 'x10_devices'
+CONF_HOUSECODE = 'housecode'
+CONF_UNITCODE = 'unitcode'
+CONF_DIM_STEPS = 'dim_steps'
+CONF_X10_ALL_UNITS_OFF = 'x10_all_units_off'
+CONF_X10_ALL_LIGHTS_ON = 'x10_all_lights_on'
+CONF_X10_ALL_LIGHTS_OFF = 'x10_all_lights_off'
SRV_ADD_ALL_LINK = 'add_all_link'
SRV_DEL_ALL_LINK = 'delete_all_link'
SRV_LOAD_ALDB = 'load_all_link_database'
SRV_PRINT_ALDB = 'print_all_link_database'
SRV_PRINT_IM_ALDB = 'print_im_all_link_database'
+SRV_X10_ALL_UNITS_OFF = 'x10_all_units_off'
+SRV_X10_ALL_LIGHTS_OFF = 'x10_all_lights_off'
+SRV_X10_ALL_LIGHTS_ON = 'x10_all_lights_on'
SRV_ALL_LINK_GROUP = 'group'
SRV_ALL_LINK_MODE = 'mode'
SRV_LOAD_DB_RELOAD = 'reload'
SRV_CONTROLLER = 'controller'
SRV_RESPONDER = 'responder'
+SRV_HOUSECODE = 'housecode'
+
+HOUSECODES = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
+ 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p']
CONF_DEVICE_OVERRIDE_SCHEMA = vol.All(
cv.deprecated(CONF_PLATFORM), vol.Schema({
@@ -51,11 +65,24 @@
vol.Optional(CONF_PLATFORM): cv.string,
}))
+CONF_X10_SCHEMA = vol.All(
+ vol.Schema({
+ vol.Required(CONF_HOUSECODE): cv.string,
+ vol.Required(CONF_UNITCODE): vol.Range(min=1, max=16),
+ vol.Required(CONF_PLATFORM): cv.string,
+ vol.Optional(CONF_DIM_STEPS): vol.Range(min=2, max=255)
+ }))
+
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_PORT): cv.string,
vol.Optional(CONF_OVERRIDE): vol.All(
- cv.ensure_list_csv, [CONF_DEVICE_OVERRIDE_SCHEMA])
+ cv.ensure_list_csv, [CONF_DEVICE_OVERRIDE_SCHEMA]),
+ vol.Optional(CONF_X10_ALL_UNITS_OFF): vol.In(HOUSECODES),
+ vol.Optional(CONF_X10_ALL_LIGHTS_ON): vol.In(HOUSECODES),
+ vol.Optional(CONF_X10_ALL_LIGHTS_OFF): vol.In(HOUSECODES),
+ vol.Optional(CONF_X10): vol.All(
+ cv.ensure_list_csv, [CONF_X10_SCHEMA])
})
}, extra=vol.ALLOW_EXTRA)
@@ -77,6 +104,10 @@
vol.Required(CONF_ENTITY_ID): cv.entity_id,
})
+X10_HOUSECODE_SCHEMA = vol.Schema({
+ vol.Required(SRV_HOUSECODE): vol.In(HOUSECODES),
+ })
+
@asyncio.coroutine
def async_setup(hass, config):
@@ -89,6 +120,10 @@ def async_setup(hass, config):
conf = config[DOMAIN]
port = conf.get(CONF_PORT)
overrides = conf.get(CONF_OVERRIDE, [])
+ x10_devices = conf.get(CONF_X10, [])
+ x10_all_units_off_housecode = conf.get(CONF_X10_ALL_UNITS_OFF)
+ x10_all_lights_on_housecode = conf.get(CONF_X10_ALL_LIGHTS_ON)
+ x10_all_lights_off_housecode = conf.get(CONF_X10_ALL_LIGHTS_OFF)
@callback
def async_plm_new_device(device):
@@ -106,7 +141,7 @@ def async_plm_new_device(device):
hass.async_add_job(
discovery.async_load_platform(
hass, platform, DOMAIN,
- discovered={'address': device.address.hex,
+ discovered={'address': device.address.id,
'state_key': state_key},
hass_config=config))
@@ -151,6 +186,21 @@ def print_im_aldb(service):
# Furture direction is to create an INSTEON control panel.
print_aldb_to_log(plm.aldb)
+ def x10_all_units_off(service):
+ """Send the X10 All Units Off command."""
+ housecode = service.data.get(SRV_HOUSECODE)
+ plm.x10_all_units_off(housecode)
+
+ def x10_all_lights_off(service):
+ """Send the X10 All Lights Off command."""
+ housecode = service.data.get(SRV_HOUSECODE)
+ plm.x10_all_lights_off(housecode)
+
+ def x10_all_lights_on(service):
+ """Send the X10 All Lights On command."""
+ housecode = service.data.get(SRV_HOUSECODE)
+ plm.x10_all_lights_on(housecode)
+
def _register_services():
hass.services.register(DOMAIN, SRV_ADD_ALL_LINK, add_all_link,
schema=ADD_ALL_LINK_SCHEMA)
@@ -162,6 +212,15 @@ def _register_services():
schema=PRINT_ALDB_SCHEMA)
hass.services.register(DOMAIN, SRV_PRINT_IM_ALDB, print_im_aldb,
schema=None)
+ hass.services.register(DOMAIN, SRV_X10_ALL_UNITS_OFF,
+ x10_all_units_off,
+ schema=X10_HOUSECODE_SCHEMA)
+ hass.services.register(DOMAIN, SRV_X10_ALL_LIGHTS_OFF,
+ x10_all_lights_off,
+ schema=X10_HOUSECODE_SCHEMA)
+ hass.services.register(DOMAIN, SRV_X10_ALL_LIGHTS_ON,
+ x10_all_lights_on,
+ schema=X10_HOUSECODE_SCHEMA)
_LOGGER.debug("Insteon_plm Services registered")
_LOGGER.info("Looking for PLM on %s", port)
@@ -192,6 +251,36 @@ def _register_services():
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, conn.close)
plm.devices.add_device_callback(async_plm_new_device)
+
+ if x10_all_units_off_housecode:
+ device = plm.add_x10_device(x10_all_units_off_housecode,
+ 20,
+ 'allunitsoff')
+ if x10_all_lights_on_housecode:
+ device = plm.add_x10_device(x10_all_lights_on_housecode,
+ 21,
+ 'alllightson')
+ if x10_all_lights_off_housecode:
+ device = plm.add_x10_device(x10_all_lights_off_housecode,
+ 22,
+ 'alllightsoff')
+ for device in x10_devices:
+ housecode = device.get(CONF_HOUSECODE)
+ unitcode = device.get(CONF_UNITCODE)
+ x10_type = 'onoff'
+ steps = device.get(CONF_DIM_STEPS, 22)
+ if device.get(CONF_PLATFORM) == 'light':
+ x10_type = 'dimmable'
+ elif device.get(CONF_PLATFORM) == 'binary_sensor':
+ x10_type = 'sensor'
+ _LOGGER.debug("Adding X10 device to insteonplm: %s %d %s",
+ housecode, unitcode, x10_type)
+ device = plm.add_x10_device(housecode,
+ unitcode,
+ x10_type)
+ if device and hasattr(device.states[0x01], 'steps'):
+ device.states[0x01].steps = steps
+
hass.async_add_job(_register_services)
return True
@@ -219,6 +308,13 @@ def __init__(self):
IoLincSensor,
LeakSensorDryWet)
+ from insteonplm.states.x10 import (X10DimmableSwitch,
+ X10OnOffSwitch,
+ X10OnOffSensor,
+ X10AllUnitsOffSensor,
+ X10AllLightsOnSensor,
+ X10AllLightsOffSensor)
+
self.states = [State(OnOffSwitch_OutletTop, 'switch'),
State(OnOffSwitch_OutletBottom, 'switch'),
State(OpenClosedRelay, 'switch'),
@@ -231,7 +327,14 @@ def __init__(self):
State(VariableSensor, 'sensor'),
State(DimmableSwitch_Fan, 'fan'),
- State(DimmableSwitch, 'light')]
+ State(DimmableSwitch, 'light'),
+
+ State(X10DimmableSwitch, 'light'),
+ State(X10OnOffSwitch, 'switch'),
+ State(X10OnOffSensor, 'binary_sensor'),
+ State(X10AllUnitsOffSensor, 'binary_sensor'),
+ State(X10AllLightsOnSensor, 'binary_sensor'),
+ State(X10AllLightsOffSensor, 'binary_sensor')]
def __len__(self):
"""Return the number of INSTEON state types mapped to HA platforms."""
diff --git a/homeassistant/components/insteon_plm/services.yaml b/homeassistant/components/insteon_plm/services.yaml
index 9ea53c10fbf1af..4d87d7881bf666 100644
--- a/homeassistant/components/insteon_plm/services.yaml
+++ b/homeassistant/components/insteon_plm/services.yaml
@@ -30,3 +30,21 @@ print_all_link_database:
example: 'light.1a2b3c'
print_im_all_link_database:
description: Print the All-Link Database for the INSTEON Modem (IM).
+x10_all_units_off:
+ description: Send X10 All Units Off command
+ fields:
+ housecode:
+ description: X10 house code
+ example: c
+x10_all_lights_on:
+ description: Send X10 All Lights On command
+ fields:
+ housecode:
+ description: X10 house code
+ example: c
+x10_all_lights_off:
+ description: Send X10 All Lights Off command
+ fields:
+ housecode:
+ description: X10 house code
+ example: c
diff --git a/homeassistant/components/ios.py b/homeassistant/components/ios.py
index fe3c934659b927..249f147847c08b 100644
--- a/homeassistant/components/ios.py
+++ b/homeassistant/components/ios.py
@@ -203,7 +203,7 @@ def device_name_for_push_id(push_id):
def setup(hass, config):
"""Set up the iOS component."""
- # pylint: disable=global-statement, import-error
+ # pylint: disable=import-error
global CONFIG_FILE
global CONFIG_FILE_PATH
diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py
index ecabcd36a85391..90ab41cf98b7e0 100644
--- a/homeassistant/components/isy994.py
+++ b/homeassistant/components/isy994.py
@@ -425,7 +425,6 @@ def async_added_to_hass(self) -> None:
self._control_handler = self._node.controlEvents.subscribe(
self.on_control)
- # pylint: disable=unused-argument
def on_update(self, event: object) -> None:
"""Handle the update event from the ISY994 Node."""
self.schedule_update_ha_state()
diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected.py
index 70b66f84ae95f5..5b28b7b0999740 100644
--- a/homeassistant/components/konnected.py
+++ b/homeassistant/components/konnected.py
@@ -31,6 +31,7 @@
DOMAIN = 'konnected'
CONF_ACTIVATION = 'activation'
+CONF_API_HOST = 'api_host'
STATE_LOW = 'low'
STATE_HIGH = 'high'
@@ -56,10 +57,12 @@
}), cv.has_at_least_one_key(CONF_PIN, CONF_ZONE)
)
+# pylint: disable=no-value-for-parameter
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema({
vol.Required(CONF_ACCESS_TOKEN): cv.string,
+ vol.Optional(CONF_API_HOST): vol.Url(),
vol.Required(CONF_DEVICES): [{
vol.Required(CONF_ID): cv.string,
vol.Optional(CONF_BINARY_SENSORS): vol.All(
@@ -87,7 +90,10 @@ async def async_setup(hass, config):
access_token = cfg.get(CONF_ACCESS_TOKEN)
if DOMAIN not in hass.data:
- hass.data[DOMAIN] = {CONF_ACCESS_TOKEN: access_token}
+ hass.data[DOMAIN] = {
+ CONF_ACCESS_TOKEN: access_token,
+ CONF_API_HOST: cfg.get(CONF_API_HOST)
+ }
def device_discovered(service, info):
"""Call when a Konnected device has been discovered."""
@@ -254,14 +260,26 @@ def sync_device_config(self):
_LOGGER.debug('%s: current actuator config: %s', self.device_id,
current_actuator_config)
+ desired_api_host = \
+ self.hass.data[DOMAIN].get(CONF_API_HOST) or \
+ self.hass.config.api.base_url
+ desired_api_endpoint = desired_api_host + ENDPOINT_ROOT
+ current_api_endpoint = self.status.get('endpoint')
+
+ _LOGGER.debug('%s: desired api endpoint: %s', self.device_id,
+ desired_api_endpoint)
+ _LOGGER.debug('%s: current api endpoint: %s', self.device_id,
+ current_api_endpoint)
+
if (desired_sensor_configuration != current_sensor_configuration) or \
- (current_actuator_config != desired_actuator_config):
+ (current_actuator_config != desired_actuator_config) or \
+ (current_api_endpoint != desired_api_endpoint):
_LOGGER.debug('pushing settings to device %s', self.device_id)
self.client.put_settings(
desired_sensor_configuration,
desired_actuator_config,
self.hass.data[DOMAIN].get(CONF_ACCESS_TOKEN),
- self.hass.config.api.base_url + ENDPOINT_ROOT
+ desired_api_endpoint
)
diff --git a/homeassistant/components/light/blinksticklight.py b/homeassistant/components/light/blinksticklight.py
index 18a6b4ae266d99..bca587074b01c4 100644
--- a/homeassistant/components/light/blinksticklight.py
+++ b/homeassistant/components/light/blinksticklight.py
@@ -31,7 +31,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Blinkstick device specified by serial number."""
from blinkstick import blinkstick
diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py
index 916e60c00b1b9c..a4593a72617bbf 100644
--- a/homeassistant/components/light/deconz.py
+++ b/homeassistant/components/light/deconz.py
@@ -6,6 +6,7 @@
"""
from homeassistant.components.deconz import (
DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB)
+from homeassistant.components.deconz.const import CONF_ALLOW_DECONZ_GROUPS
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR,
ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT,
@@ -33,6 +34,7 @@ def async_add_light(lights):
for light in lights:
entities.append(DeconzLight(light))
async_add_devices(entities, True)
+
hass.data[DATA_DECONZ_UNSUB].append(
async_dispatcher_connect(hass, 'deconz_new_light', async_add_light))
@@ -40,10 +42,12 @@ def async_add_light(lights):
def async_add_group(groups):
"""Add group from deCONZ."""
entities = []
+ allow_group = config_entry.data.get(CONF_ALLOW_DECONZ_GROUPS, True)
for group in groups:
- if group.lights:
+ if group.lights and allow_group:
entities.append(DeconzLight(group))
async_add_devices(entities, True)
+
hass.data[DATA_DECONZ_UNSUB].append(
async_dispatcher_connect(hass, 'deconz_new_group', async_add_group))
diff --git a/homeassistant/components/light/isy994.py b/homeassistant/components/light/isy994.py
index d2ed865892e6f5..ce358d0a974e5e 100644
--- a/homeassistant/components/light/isy994.py
+++ b/homeassistant/components/light/isy994.py
@@ -15,7 +15,6 @@
_LOGGER = logging.getLogger(__name__)
-# pylint: disable=unused-argument
def setup_platform(hass, config: ConfigType,
add_devices: Callable[[list], None], discovery_info=None):
"""Set up the ISY994 light platform."""
diff --git a/homeassistant/components/light/knx.py b/homeassistant/components/light/knx.py
index 18446951735bf2..8fa2b56d1d2d1e 100644
--- a/homeassistant/components/light/knx.py
+++ b/homeassistant/components/light/knx.py
@@ -88,7 +88,6 @@ def async_register_callbacks(self):
"""Register callbacks to update hass after device was changed."""
async def after_update_callback(device):
"""Call after device was updated."""
- # pylint: disable=unused-argument
await self.async_update_ha_state()
self.device.register_device_updated_cb(after_update_callback)
diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py
index dff5ccd42acff2..421356f07bc632 100644
--- a/homeassistant/components/light/lifx.py
+++ b/homeassistant/components/light/lifx.py
@@ -30,7 +30,7 @@
_LOGGER = logging.getLogger(__name__)
-REQUIREMENTS = ['aiolifx==0.6.1', 'aiolifx_effects==0.1.2']
+REQUIREMENTS = ['aiolifx==0.6.3', 'aiolifx_effects==0.1.2']
UDP_BROADCAST_PORT = 56700
@@ -201,7 +201,7 @@ def merge_hsbk(base, change):
"""Copy change on top of base, except when None."""
if change is None:
return None
- return list(map(lambda x, y: y if y is not None else x, base, change))
+ return [b if c is None else c for b, c in zip(base, change)]
class LIFXManager(object):
@@ -256,7 +256,7 @@ async def service_handler(service):
async def start_effect(self, entities, service, **kwargs):
"""Start a light effect on entities."""
- devices = list(map(lambda l: l.device, entities))
+ devices = [light.device for light in entities]
if service == SERVICE_EFFECT_PULSE:
effect = aiolifx_effects().EffectPulse(
@@ -314,12 +314,13 @@ async def register_new_device(self, device):
# Read initial state
ack = AwaitAioLIFX().wait
- version_resp = await ack(device.get_version)
- if version_resp:
- color_resp = await ack(device.get_color)
+ color_resp = await ack(device.get_color)
+ if color_resp:
+ version_resp = await ack(device.get_version)
- if version_resp is None or color_resp is None:
+ if color_resp is None or version_resp is None:
_LOGGER.error("Failed to initialize %s", device.ip_addr)
+ device.registered = False
else:
device.timeout = MESSAGE_TIMEOUT
device.retry_count = MESSAGE_RETRIES
@@ -440,18 +441,13 @@ def supported_features(self):
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
- brightness = convert_16_to_8(self.device.color[2])
- _LOGGER.debug("brightness: %d", brightness)
- return brightness
+ return convert_16_to_8(self.device.color[2])
@property
def color_temp(self):
"""Return the color temperature."""
kelvin = self.device.color[3]
- temperature = color_util.color_temperature_kelvin_to_mired(kelvin)
-
- _LOGGER.debug("color_temp: %d", temperature)
- return temperature
+ return color_util.color_temperature_kelvin_to_mired(kelvin)
@property
def is_on(self):
@@ -564,7 +560,6 @@ async def default_effect(self, **kwargs):
async def async_update(self):
"""Update bulb status."""
- _LOGGER.debug("%s async_update", self.who)
if self.available and not self.lock.locked():
await AwaitAioLIFX().wait(self.device.get_color)
@@ -627,7 +622,7 @@ async def set_color(self, ack, hsbk, kwargs, duration=0):
zones = list(range(0, num_zones))
else:
- zones = list(filter(lambda x: x < num_zones, set(zones)))
+ zones = [x for x in set(zones) if x < num_zones]
# Zone brightness is not reported when powered off
if not self.is_on and hsbk[2] is None:
diff --git a/homeassistant/components/light/lifx_legacy.py b/homeassistant/components/light/lifx_legacy.py
index 490eeb6ecaba17..182d7536dc4544 100644
--- a/homeassistant/components/light/lifx_legacy.py
+++ b/homeassistant/components/light/lifx_legacy.py
@@ -45,7 +45,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the LIFX platform."""
server_addr = config.get(CONF_SERVER)
@@ -118,7 +117,6 @@ def on_power(self, ipaddr, power):
bulb.set_power(power)
bulb.schedule_update_ha_state()
- # pylint: disable=unused-argument
def poll(self, now):
"""Set up polling for the light."""
self.probe()
diff --git a/homeassistant/components/light/lutron.py b/homeassistant/components/light/lutron.py
index 34d6cba7cb8b09..24744110c6fd98 100644
--- a/homeassistant/components/light/lutron.py
+++ b/homeassistant/components/light/lutron.py
@@ -16,7 +16,6 @@
DEPENDENCIES = ['lutron']
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Lutron lights."""
devs = []
diff --git a/homeassistant/components/light/lutron_caseta.py b/homeassistant/components/light/lutron_caseta.py
index e4e1baf6c582d2..09f0a337cc343a 100644
--- a/homeassistant/components/light/lutron_caseta.py
+++ b/homeassistant/components/light/lutron_caseta.py
@@ -19,7 +19,6 @@
DEPENDENCIES = ['lutron_caseta']
-# pylint: disable=unused-argument
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Lutron Caseta lights."""
diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py
index 97a4cc8c137ea4..c0e363f85d6d40 100644
--- a/homeassistant/components/light/mqtt.py
+++ b/homeassistant/components/light/mqtt.py
@@ -442,8 +442,15 @@ async def async_turn_on(self, **kwargs):
self._topic[CONF_RGB_COMMAND_TOPIC] is not None:
hs_color = kwargs[ATTR_HS_COLOR]
- brightness = kwargs.get(
- ATTR_BRIGHTNESS, self._brightness if self._brightness else 255)
+
+ # If there's a brightness topic set, we don't want to scale the RGB
+ # values given using the brightness.
+ if self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None:
+ brightness = 255
+ else:
+ brightness = kwargs.get(
+ ATTR_BRIGHTNESS, self._brightness if self._brightness else
+ 255)
rgb = color_util.color_hsv_to_RGB(
hs_color[0], hs_color[1], brightness / 255 * 100)
tpl = self._templates[CONF_RGB_COMMAND_TEMPLATE]
diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py
index 14f5ee7a9b9142..705e106fdff8be 100644
--- a/homeassistant/components/light/mqtt_json.py
+++ b/homeassistant/components/light/mqtt_json.py
@@ -345,9 +345,14 @@ async def async_turn_on(self, **kwargs):
hs_color = kwargs[ATTR_HS_COLOR]
message['color'] = {}
if self._rgb:
- brightness = kwargs.get(
- ATTR_BRIGHTNESS,
- self._brightness if self._brightness else 255)
+ # If there's a brightness topic set, we don't want to scale the
+ # RGB values given using the brightness.
+ if self._brightness is not None:
+ brightness = 255
+ else:
+ brightness = kwargs.get(
+ ATTR_BRIGHTNESS,
+ self._brightness if self._brightness else 255)
rgb = color_util.color_hsv_to_RGB(
hs_color[0], hs_color[1], brightness / 255 * 100)
message['color']['r'] = rgb[0]
diff --git a/homeassistant/components/light/mqtt_template.py b/homeassistant/components/light/mqtt_template.py
index e32c13fc5b6eff..f6b3fbe8b70799 100644
--- a/homeassistant/components/light/mqtt_template.py
+++ b/homeassistant/components/light/mqtt_template.py
@@ -317,8 +317,15 @@ async def async_turn_on(self, **kwargs):
if ATTR_HS_COLOR in kwargs:
hs_color = kwargs[ATTR_HS_COLOR]
- brightness = kwargs.get(
- ATTR_BRIGHTNESS, self._brightness if self._brightness else 255)
+
+ # If there's a brightness topic set, we don't want to scale the RGB
+ # values given using the brightness.
+ if self._templates[CONF_BRIGHTNESS_TEMPLATE] is not None:
+ brightness = 255
+ else:
+ brightness = kwargs.get(
+ ATTR_BRIGHTNESS, self._brightness if self._brightness else
+ 255)
rgb = color_util.color_hsv_to_RGB(
hs_color[0], hs_color[1], brightness / 255 * 100)
values['red'] = rgb[0]
diff --git a/homeassistant/components/light/mystrom.py b/homeassistant/components/light/mystrom.py
index 8d7fb807c6dbb9..9abd96664f26ad 100644
--- a/homeassistant/components/light/mystrom.py
+++ b/homeassistant/components/light/mystrom.py
@@ -13,9 +13,9 @@
Light, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS,
SUPPORT_EFFECT, ATTR_EFFECT, SUPPORT_FLASH, SUPPORT_COLOR,
ATTR_HS_COLOR)
-from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_UNKNOWN
+from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
-REQUIREMENTS = ['python-mystrom==0.4.2']
+REQUIREMENTS = ['python-mystrom==0.4.4']
_LOGGER = logging.getLogger(__name__)
@@ -54,9 +54,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
try:
if bulb.get_status()['type'] != 'rgblamp':
_LOGGER.error("Device %s (%s) is not a myStrom bulb", host, mac)
- return False
+ return
except MyStromConnectionError:
- _LOGGER.warning("myStrom bulb not online")
+ _LOGGER.warning("No route to device: %s", host)
add_devices([MyStromLight(bulb, name)], True)
@@ -107,7 +107,7 @@ def effect_list(self):
@property
def is_on(self):
"""Return true if light is on."""
- return self._state['on'] if self._state is not None else STATE_UNKNOWN
+ return self._state['on'] if self._state is not None else None
def turn_on(self, **kwargs):
"""Turn on the light."""
@@ -136,7 +136,7 @@ def turn_on(self, **kwargs):
if effect == EFFECT_RAINBOW:
self._bulb.set_rainbow(30)
except MyStromConnectionError:
- _LOGGER.warning("myStrom bulb not online")
+ _LOGGER.warning("No route to device")
def turn_off(self, **kwargs):
"""Turn off the bulb."""
@@ -163,5 +163,5 @@ def update(self):
self._available = True
except MyStromConnectionError:
- _LOGGER.warning("myStrom bulb not online")
+ _LOGGER.warning("No route to device")
self._available = False
diff --git a/homeassistant/components/light/tellstick.py b/homeassistant/components/light/tellstick.py
index 1bf7d632af5fc1..44e5e40b3b79cc 100644
--- a/homeassistant/components/light/tellstick.py
+++ b/homeassistant/components/light/tellstick.py
@@ -15,7 +15,6 @@
SUPPORT_TELLSTICK = SUPPORT_BRIGHTNESS
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Tellstick lights."""
if (discovery_info is None or
diff --git a/homeassistant/components/light/tikteck.py b/homeassistant/components/light/tikteck.py
index 2079638f7f1046..c21da57ea96f1a 100644
--- a/homeassistant/components/light/tikteck.py
+++ b/homeassistant/components/light/tikteck.py
@@ -31,7 +31,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Tikteck platform."""
lights = []
diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/light/tplink.py
index 4101eab2150298..d7544cb6c5a2ee 100644
--- a/homeassistant/components/light/tplink.py
+++ b/homeassistant/components/light/tplink.py
@@ -19,7 +19,7 @@
from homeassistant.util.color import (
color_temperature_kelvin_to_mired as kelvin_to_mired)
-REQUIREMENTS = ['pyHS100==0.3.0']
+REQUIREMENTS = ['pyHS100==0.3.1']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py
index ab53c3669cb722..c30745239ea05c 100644
--- a/homeassistant/components/light/tradfri.py
+++ b/homeassistant/components/light/tradfri.py
@@ -19,12 +19,16 @@
_LOGGER = logging.getLogger(__name__)
+ATTR_DIMMER = 'dimmer'
+ATTR_HUE = 'hue'
+ATTR_SAT = 'saturation'
ATTR_TRANSITION_TIME = 'transition_time'
DEPENDENCIES = ['tradfri']
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA
IKEA = 'IKEA of Sweden'
TRADFRI_LIGHT_MANAGER = 'Tradfri Light Manager'
-SUPPORTED_FEATURES = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION)
+SUPPORTED_FEATURES = SUPPORT_TRANSITION
+SUPPORTED_GROUP_FEATURES = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION
async def async_setup_platform(hass, config,
@@ -79,7 +83,7 @@ def unique_id(self):
@property
def supported_features(self):
"""Flag supported features."""
- return SUPPORTED_FEATURES
+ return SUPPORTED_GROUP_FEATURES
@property
def name(self):
@@ -225,75 +229,97 @@ def hs_color(self):
"""HS color of the light."""
if self._light_control.can_set_color:
hsbxy = self._light_data.hsb_xy_color
- hue = hsbxy[0] / (65535 / 360)
- sat = hsbxy[1] / (65279 / 100)
+ hue = hsbxy[0] / (self._light_control.max_hue / 360)
+ sat = hsbxy[1] / (self._light_control.max_saturation / 100)
if hue is not None and sat is not None:
return hue, sat
async def async_turn_off(self, **kwargs):
"""Instruct the light to turn off."""
- await self._api(self._light_control.set_state(False))
+ # This allows transitioning to off, but resets the brightness
+ # to 1 for the next set_state(True) command
+ transition_time = None
+ if ATTR_TRANSITION in kwargs:
+ transition_time = int(kwargs[ATTR_TRANSITION]) * 10
+
+ dimmer_data = {ATTR_DIMMER: 0, ATTR_TRANSITION_TIME:
+ transition_time}
+ await self._api(self._light_control.set_dimmer(**dimmer_data))
+ else:
+ await self._api(self._light_control.set_state(False))
async def async_turn_on(self, **kwargs):
"""Instruct the light to turn on."""
- params = {}
transition_time = None
if ATTR_TRANSITION in kwargs:
transition_time = int(kwargs[ATTR_TRANSITION]) * 10
- brightness = kwargs.get(ATTR_BRIGHTNESS)
-
- if brightness is not None:
+ dimmer_command = None
+ if ATTR_BRIGHTNESS in kwargs:
+ brightness = kwargs[ATTR_BRIGHTNESS]
if brightness > 254:
brightness = 254
elif brightness < 0:
brightness = 0
+ dimmer_data = {ATTR_DIMMER: brightness, ATTR_TRANSITION_TIME:
+ transition_time}
+ dimmer_command = self._light_control.set_dimmer(**dimmer_data)
+ transition_time = None
+ else:
+ dimmer_command = self._light_control.set_state(True)
+ color_command = None
if ATTR_HS_COLOR in kwargs and self._light_control.can_set_color:
- params[ATTR_BRIGHTNESS] = brightness
- hue = int(kwargs[ATTR_HS_COLOR][0] * (65535 / 360))
- sat = int(kwargs[ATTR_HS_COLOR][1] * (65279 / 100))
- if brightness is None:
- params[ATTR_TRANSITION_TIME] = transition_time
- await self._api(
- self._light_control.set_hsb(hue, sat, **params))
- return
-
+ hue = int(kwargs[ATTR_HS_COLOR][0] *
+ (self._light_control.max_hue / 360))
+ sat = int(kwargs[ATTR_HS_COLOR][1] *
+ (self._light_control.max_saturation / 100))
+ color_data = {ATTR_HUE: hue, ATTR_SAT: sat, ATTR_TRANSITION_TIME:
+ transition_time}
+ color_command = self._light_control.set_hsb(**color_data)
+ transition_time = None
+
+ temp_command = None
if ATTR_COLOR_TEMP in kwargs and (self._light_control.can_set_temp or
self._light_control.can_set_color):
temp = kwargs[ATTR_COLOR_TEMP]
- if temp > self.max_mireds:
- temp = self.max_mireds
- elif temp < self.min_mireds:
- temp = self.min_mireds
-
- if brightness is None:
- params[ATTR_TRANSITION_TIME] = transition_time
# White Spectrum bulb
- if (self._light_control.can_set_temp and
- not self._light_control.can_set_color):
- await self._api(
- self._light_control.set_color_temp(temp, **params))
+ if self._light_control.can_set_temp:
+ if temp > self.max_mireds:
+ temp = self.max_mireds
+ elif temp < self.min_mireds:
+ temp = self.min_mireds
+ temp_data = {ATTR_COLOR_TEMP: temp, ATTR_TRANSITION_TIME:
+ transition_time}
+ temp_command = self._light_control.set_color_temp(**temp_data)
+ transition_time = None
# Color bulb (CWS)
# color_temp needs to be set with hue/saturation
- if self._light_control.can_set_color:
- params[ATTR_BRIGHTNESS] = brightness
+ elif self._light_control.can_set_color:
temp_k = color_util.color_temperature_mired_to_kelvin(temp)
hs_color = color_util.color_temperature_to_hs(temp_k)
- hue = int(hs_color[0] * (65535 / 360))
- sat = int(hs_color[1] * (65279 / 100))
- await self._api(
- self._light_control.set_hsb(hue, sat,
- **params))
-
- if brightness is not None:
- params[ATTR_TRANSITION_TIME] = transition_time
- await self._api(
- self._light_control.set_dimmer(brightness,
- **params))
+ hue = int(hs_color[0] * (self._light_control.max_hue / 360))
+ sat = int(hs_color[1] *
+ (self._light_control.max_saturation / 100))
+ color_data = {ATTR_HUE: hue, ATTR_SAT: sat,
+ ATTR_TRANSITION_TIME: transition_time}
+ color_command = self._light_control.set_hsb(**color_data)
+ transition_time = None
+
+ # HSB can always be set, but color temp + brightness is bulb dependant
+ command = dimmer_command
+ if command is not None:
+ command += color_command
else:
- await self._api(
- self._light_control.set_state(True))
+ command = color_command
+
+ if self._light_control.can_combine_commands:
+ await self._api(command + temp_command)
+ else:
+ if temp_command is not None:
+ await self._api(temp_command)
+ if command is not None:
+ await self._api(command)
@callback
def _async_start_observe(self, exc=None):
@@ -324,6 +350,8 @@ def _refresh(self, light):
self._name = light.name
self._features = SUPPORTED_FEATURES
+ if light.light_control.can_set_dimmer:
+ self._features |= SUPPORT_BRIGHTNESS
if light.light_control.can_set_color:
self._features |= SUPPORT_COLOR
if light.light_control.can_set_temp:
diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py
index 6b12e69341d2cc..e62ffaecdff92f 100644
--- a/homeassistant/components/light/vera.py
+++ b/homeassistant/components/light/vera.py
@@ -18,12 +18,11 @@
DEPENDENCIES = ['vera']
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Vera lights."""
add_devices(
- VeraLight(device, hass.data[VERA_CONTROLLER]) for
- device in hass.data[VERA_DEVICES]['light'])
+ [VeraLight(device, hass.data[VERA_CONTROLLER]) for
+ device in hass.data[VERA_DEVICES]['light']], True)
class VeraLight(VeraDevice, Light):
diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py
index 24eab7ebd4ad1c..fbb8dd66f013d8 100644
--- a/homeassistant/components/light/xiaomi_miio.py
+++ b/homeassistant/components/light/xiaomi_miio.py
@@ -42,7 +42,7 @@
'philips.light.candle2']),
})
-REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
+REQUIREMENTS = ['python-miio==0.4.0', 'construct==2.9.41']
# The light does not accept cct values < 1
CCT_MIN = 1
@@ -100,7 +100,6 @@
}
-# pylint: disable=unused-argument
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the light from config."""
diff --git a/homeassistant/components/light/zengge.py b/homeassistant/components/light/zengge.py
index 3c77f2d8449cac..35d2bf2388cd3b 100644
--- a/homeassistant/components/light/zengge.py
+++ b/homeassistant/components/light/zengge.py
@@ -30,7 +30,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Zengge platform."""
lights = []
diff --git a/homeassistant/components/lock/demo.py b/homeassistant/components/lock/demo.py
index d561dd333ab315..8da53a9ef11fba 100644
--- a/homeassistant/components/lock/demo.py
+++ b/homeassistant/components/lock/demo.py
@@ -8,7 +8,6 @@
from homeassistant.const import (STATE_LOCKED, STATE_UNLOCKED)
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Demo lock platform."""
add_devices([
diff --git a/homeassistant/components/lock/isy994.py b/homeassistant/components/lock/isy994.py
index 50371fdc9ae8fb..79e4308dbda114 100644
--- a/homeassistant/components/lock/isy994.py
+++ b/homeassistant/components/lock/isy994.py
@@ -21,7 +21,6 @@
}
-# pylint: disable=unused-argument
def setup_platform(hass, config: ConfigType,
add_devices: Callable[[list], None], discovery_info=None):
"""Set up the ISY994 lock platform."""
diff --git a/homeassistant/components/lock/kiwi.py b/homeassistant/components/lock/kiwi.py
new file mode 100644
index 00000000000000..78ea45525f284c
--- /dev/null
+++ b/homeassistant/components/lock/kiwi.py
@@ -0,0 +1,110 @@
+"""
+Support for the KIWI.KI lock platform.
+
+For more details about this platform, please refer to the documentation
+https://home-assistant.io/components/lock.kiwi/
+"""
+import logging
+
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.components.lock import (LockDevice, PLATFORM_SCHEMA)
+from homeassistant.const import (
+ CONF_PASSWORD, CONF_USERNAME, ATTR_ID, ATTR_LONGITUDE, ATTR_LATITUDE,
+ STATE_LOCKED, STATE_UNLOCKED)
+from homeassistant.helpers.event import async_call_later
+from homeassistant.core import callback
+
+REQUIREMENTS = ['kiwiki-client==0.1.1']
+
+_LOGGER = logging.getLogger(__name__)
+
+ATTR_TYPE = 'hardware_type'
+ATTR_PERMISSION = 'permission'
+ATTR_CAN_INVITE = 'can_invite_others'
+
+UNLOCK_MAINTAIN_TIME = 5
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string
+})
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the KIWI lock platform."""
+ from kiwiki import KiwiClient, KiwiException
+ try:
+ kiwi = KiwiClient(config[CONF_USERNAME], config[CONF_PASSWORD])
+ except KiwiException as exc:
+ _LOGGER.error(exc)
+ return
+ available_locks = kiwi.get_locks()
+ if not available_locks:
+ # No locks found; abort setup routine.
+ _LOGGER.info("No KIWI locks found in your account.")
+ return
+ add_devices([KiwiLock(lock, kiwi) for lock in available_locks], True)
+
+
+class KiwiLock(LockDevice):
+ """Representation of a Kiwi lock."""
+
+ def __init__(self, kiwi_lock, client):
+ """Initialize the lock."""
+ self._sensor = kiwi_lock
+ self._client = client
+ self.lock_id = kiwi_lock['sensor_id']
+ self._state = STATE_LOCKED
+
+ address = kiwi_lock.get('address')
+ address.update({
+ ATTR_LATITUDE: address.pop('lat', None),
+ ATTR_LONGITUDE: address.pop('lng', None)
+ })
+
+ self._device_attrs = {
+ ATTR_ID: self.lock_id,
+ ATTR_TYPE: kiwi_lock.get('hardware_type'),
+ ATTR_PERMISSION: kiwi_lock.get('highest_permission'),
+ ATTR_CAN_INVITE: kiwi_lock.get('can_invite'),
+ **address
+ }
+
+ @property
+ def name(self):
+ """Return the name of the lock."""
+ name = self._sensor.get('name')
+ specifier = self._sensor['address'].get('specifier')
+ return name or specifier
+
+ @property
+ def is_locked(self):
+ """Return true if lock is locked."""
+ return self._state == STATE_LOCKED
+
+ @property
+ def device_state_attributes(self):
+ """Return the device specific state attributes."""
+ return self._device_attrs
+
+ @callback
+ def clear_unlock_state(self, _):
+ """Clear unlock state automatically."""
+ self._state = STATE_LOCKED
+ self.async_schedule_update_ha_state()
+
+ def unlock(self, **kwargs):
+ """Unlock the device."""
+ from kiwiki import KiwiException
+ try:
+ self._client.open_door(self.lock_id)
+ except KiwiException:
+ _LOGGER.error("failed to open door")
+ else:
+ self._state = STATE_UNLOCKED
+ self.hass.add_job(
+ async_call_later, self.hass, UNLOCK_MAINTAIN_TIME,
+ self.clear_unlock_state
+ )
diff --git a/homeassistant/components/lock/lockitron.py b/homeassistant/components/lock/lockitron.py
index ea79848f60ce11..6bf445ba477529 100644
--- a/homeassistant/components/lock/lockitron.py
+++ b/homeassistant/components/lock/lockitron.py
@@ -26,7 +26,6 @@
API_ACTION_URL = BASE_URL + '/v2/locks/{}?access_token={}&state={}'
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Lockitron platform."""
access_token = config.get(CONF_ACCESS_TOKEN)
diff --git a/homeassistant/components/lock/nello.py b/homeassistant/components/lock/nello.py
index 04030c92425774..f67243415c50f8 100644
--- a/homeassistant/components/lock/nello.py
+++ b/homeassistant/components/lock/nello.py
@@ -27,7 +27,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Nello lock platform."""
from pynello import Nello
diff --git a/homeassistant/components/lock/nuki.py b/homeassistant/components/lock/nuki.py
index 4fe05279919a60..536c8f2abeb794 100644
--- a/homeassistant/components/lock/nuki.py
+++ b/homeassistant/components/lock/nuki.py
@@ -50,7 +50,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Nuki lock platform."""
from pynuki import NukiBridge
diff --git a/homeassistant/components/lock/sesame.py b/homeassistant/components/lock/sesame.py
index 5bc404354860f7..09f7266d15c6a1 100644
--- a/homeassistant/components/lock/sesame.py
+++ b/homeassistant/components/lock/sesame.py
@@ -24,7 +24,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(
hass, config: ConfigType,
add_devices: Callable[[list], None], discovery_info=None):
diff --git a/homeassistant/components/lock/vera.py b/homeassistant/components/lock/vera.py
index b3aae5e159fafc..e6e277cdee1417 100644
--- a/homeassistant/components/lock/vera.py
+++ b/homeassistant/components/lock/vera.py
@@ -19,8 +19,8 @@
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Find and return Vera locks."""
add_devices(
- VeraLock(device, hass.data[VERA_CONTROLLER]) for
- device in hass.data[VERA_DEVICES]['lock'])
+ [VeraLock(device, hass.data[VERA_CONTROLLER]) for
+ device in hass.data[VERA_DEVICES]['lock']], True)
class VeraLock(VeraDevice, LockDevice):
diff --git a/homeassistant/components/lock/volvooncall.py b/homeassistant/components/lock/volvooncall.py
index ab1d2fabefe1c5..b6e7383b138251 100644
--- a/homeassistant/components/lock/volvooncall.py
+++ b/homeassistant/components/lock/volvooncall.py
@@ -12,7 +12,6 @@
_LOGGER = logging.getLogger(__name__)
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Volvo On Call lock."""
if discovery_info is None:
diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py
index bcfae533abfc9b..e2d02acc61c019 100644
--- a/homeassistant/components/logbook.py
+++ b/homeassistant/components/logbook.py
@@ -372,7 +372,6 @@ def _exclude_events(events, config):
return filtered_events
-# pylint: disable=too-many-return-statements
def _entry_message_from_state(domain, state):
"""Convert a state to a message for the logbook."""
# We pass domain in so we don't have to split entity_id again
diff --git a/homeassistant/components/logger.py b/homeassistant/components/logger.py
index 6e8995a0444cd7..daaffd0174c7ac 100644
--- a/homeassistant/components/logger.py
+++ b/homeassistant/components/logger.py
@@ -15,6 +15,7 @@
DATA_LOGGER = 'logger'
+SERVICE_SET_DEFAULT_LEVEL = 'set_default_level'
SERVICE_SET_LEVEL = 'set_level'
LOGSEVERITY = {
@@ -31,8 +32,11 @@
LOGGER_DEFAULT = 'default'
LOGGER_LOGS = 'logs'
+ATTR_LEVEL = 'level'
+
_VALID_LOG_LEVEL = vol.All(vol.Upper, vol.In(LOGSEVERITY))
+SERVICE_SET_DEFAULT_LEVEL_SCHEMA = vol.Schema({ATTR_LEVEL: _VALID_LOG_LEVEL})
SERVICE_SET_LEVEL_SCHEMA = vol.Schema({cv.string: _VALID_LOG_LEVEL})
CONFIG_SCHEMA = vol.Schema({
@@ -76,12 +80,9 @@ async def async_setup(hass, config):
"""Set up the logger component."""
logfilter = {}
- # Set default log severity
- logfilter[LOGGER_DEFAULT] = LOGSEVERITY['DEBUG']
- if LOGGER_DEFAULT in config.get(DOMAIN):
- logfilter[LOGGER_DEFAULT] = LOGSEVERITY[
- config.get(DOMAIN)[LOGGER_DEFAULT]
- ]
+ def set_default_log_level(level):
+ """Set the default log level for components."""
+ logfilter[LOGGER_DEFAULT] = LOGSEVERITY[level]
def set_log_levels(logpoints):
"""Set the specified log levels."""
@@ -103,6 +104,12 @@ def set_log_levels(logpoints):
)
)
+ # Set default log severity
+ if LOGGER_DEFAULT in config.get(DOMAIN):
+ set_default_log_level(config.get(DOMAIN)[LOGGER_DEFAULT])
+ else:
+ set_default_log_level('DEBUG')
+
logger = logging.getLogger('')
logger.setLevel(logging.NOTSET)
@@ -116,7 +123,14 @@ def set_log_levels(logpoints):
async def async_service_handler(service):
"""Handle logger services."""
- set_log_levels(service.data)
+ if service.service == SERVICE_SET_DEFAULT_LEVEL:
+ set_default_log_level(service.data.get(ATTR_LEVEL))
+ else:
+ set_log_levels(service.data)
+
+ hass.services.async_register(
+ DOMAIN, SERVICE_SET_DEFAULT_LEVEL, async_service_handler,
+ schema=SERVICE_SET_DEFAULT_LEVEL_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_SET_LEVEL, async_service_handler,
diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py
index 20a1a473ba8bf9..d963deba7b55e7 100644
--- a/homeassistant/components/media_player/__init__.py
+++ b/homeassistant/components/media_player/__init__.py
@@ -57,6 +57,7 @@
SERVICE_PLAY_MEDIA = 'play_media'
SERVICE_SELECT_SOURCE = 'select_source'
+SERVICE_SELECT_SOUND_MODE = 'select_sound_mode'
SERVICE_CLEAR_PLAYLIST = 'clear_playlist'
ATTR_MEDIA_VOLUME_LEVEL = 'volume_level'
@@ -81,6 +82,8 @@
ATTR_APP_NAME = 'app_name'
ATTR_INPUT_SOURCE = 'source'
ATTR_INPUT_SOURCE_LIST = 'source_list'
+ATTR_SOUND_MODE = 'sound_mode'
+ATTR_SOUND_MODE_LIST = 'sound_mode_list'
ATTR_MEDIA_ENQUEUE = 'enqueue'
ATTR_MEDIA_SHUFFLE = 'shuffle'
@@ -109,6 +112,7 @@
SUPPORT_CLEAR_PLAYLIST = 8192
SUPPORT_PLAY = 16384
SUPPORT_SHUFFLE_SET = 32768
+SUPPORT_SELECT_SOUND_MODE = 65536
# Service call validation schemas
MEDIA_PLAYER_SCHEMA = vol.Schema({
@@ -132,6 +136,10 @@
vol.Required(ATTR_INPUT_SOURCE): cv.string,
})
+MEDIA_PLAYER_SELECT_SOUND_MODE_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
+ vol.Required(ATTR_SOUND_MODE): cv.string,
+})
+
MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string,
vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string,
@@ -167,6 +175,9 @@
SERVICE_SELECT_SOURCE: {
'method': 'async_select_source',
'schema': MEDIA_PLAYER_SELECT_SOURCE_SCHEMA},
+ SERVICE_SELECT_SOUND_MODE: {
+ 'method': 'async_select_sound_mode',
+ 'schema': MEDIA_PLAYER_SELECT_SOUND_MODE_SCHEMA},
SERVICE_PLAY_MEDIA: {
'method': 'async_play_media',
'schema': MEDIA_PLAYER_PLAY_MEDIA_SCHEMA},
@@ -197,6 +208,8 @@
ATTR_APP_NAME,
ATTR_INPUT_SOURCE,
ATTR_INPUT_SOURCE_LIST,
+ ATTR_SOUND_MODE,
+ ATTR_SOUND_MODE_LIST,
ATTR_MEDIA_SHUFFLE,
]
@@ -346,6 +359,17 @@ def select_source(hass, source, entity_id=None):
hass.services.call(DOMAIN, SERVICE_SELECT_SOURCE, data)
+@bind_hass
+def select_sound_mode(hass, sound_mode, entity_id=None):
+ """Send the media player the command to select sound mode."""
+ data = {ATTR_SOUND_MODE: sound_mode}
+
+ if entity_id:
+ data[ATTR_ENTITY_ID] = entity_id
+
+ hass.services.call(DOMAIN, SERVICE_SELECT_SOUND_MODE, data)
+
+
@bind_hass
def clear_playlist(hass, entity_id=None):
"""Send the media player the command for clear playlist."""
@@ -399,6 +423,8 @@ async def async_service_handler(service):
params['position'] = service.data.get(ATTR_MEDIA_SEEK_POSITION)
elif service.service == SERVICE_SELECT_SOURCE:
params['source'] = service.data.get(ATTR_INPUT_SOURCE)
+ elif service.service == SERVICE_SELECT_SOUND_MODE:
+ params['sound_mode'] = service.data.get(ATTR_SOUND_MODE)
elif service.service == SERVICE_PLAY_MEDIA:
params['media_type'] = \
service.data.get(ATTR_MEDIA_CONTENT_TYPE)
@@ -430,6 +456,16 @@ async def async_service_handler(service):
return True
+async def async_setup_entry(hass, entry):
+ """Setup a config entry."""
+ return await hass.data[DOMAIN].async_setup_entry(entry)
+
+
+async def async_unload_entry(hass, entry):
+ """Unload a config entry."""
+ return await hass.data[DOMAIN].async_unload_entry(entry)
+
+
class MediaPlayerDevice(Entity):
"""ABC for media player devices."""
@@ -580,6 +616,16 @@ def source_list(self):
"""List of available input sources."""
return None
+ @property
+ def sound_mode(self):
+ """Name of the current sound mode."""
+ return None
+
+ @property
+ def sound_mode_list(self):
+ """List of available sound modes."""
+ return None
+
@property
def shuffle(self):
"""Boolean if shuffle is enabled."""
@@ -723,6 +769,17 @@ def async_select_source(self, source):
"""
return self.hass.async_add_job(self.select_source, source)
+ def select_sound_mode(self, sound_mode):
+ """Select sound mode."""
+ raise NotImplementedError()
+
+ def async_select_sound_mode(self, sound_mode):
+ """Select sound mode.
+
+ This method must be run in the event loop and returns a coroutine.
+ """
+ return self.hass.async_add_job(self.select_sound_mode, sound_mode)
+
def clear_playlist(self):
"""Clear players playlist."""
raise NotImplementedError()
@@ -796,6 +853,11 @@ def support_select_source(self):
"""Boolean if select source command supported."""
return bool(self.supported_features & SUPPORT_SELECT_SOURCE)
+ @property
+ def support_select_sound_mode(self):
+ """Boolean if select sound mode command supported."""
+ return bool(self.supported_features & SUPPORT_SELECT_SOUND_MODE)
+
@property
def support_clear_playlist(self):
"""Boolean if clear playlist command supported."""
diff --git a/homeassistant/components/media_player/aquostv.py b/homeassistant/components/media_player/aquostv.py
index 6933286f0fe584..93daf5b2f893d6 100644
--- a/homeassistant/components/media_player/aquostv.py
+++ b/homeassistant/components/media_player/aquostv.py
@@ -59,7 +59,6 @@
8: 'PC_IN'}
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Sharp Aquos TV platform."""
import sharp_aquos_rc
@@ -104,7 +103,6 @@ def wrapper(obj, *args, **kwargs):
return wrapper
-# pylint: disable=abstract-method
class SharpAquosTVDevice(MediaPlayerDevice):
"""Representation of a Aquos TV."""
diff --git a/homeassistant/components/media_player/blackbird.py b/homeassistant/components/media_player/blackbird.py
index 1c976f5eecd32b..3d8e1fde687f74 100644
--- a/homeassistant/components/media_player/blackbird.py
+++ b/homeassistant/components/media_player/blackbird.py
@@ -61,7 +61,6 @@
}))
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Monoprice Blackbird 4k 8x8 HDBaseT Matrix platform."""
if DATA_BLACKBIRD not in hass.data:
diff --git a/homeassistant/components/media_player/braviatv.py b/homeassistant/components/media_player/braviatv.py
index f0cc93a8b0f3f2..727bda3be3f181 100644
--- a/homeassistant/components/media_player/braviatv.py
+++ b/homeassistant/components/media_player/braviatv.py
@@ -60,7 +60,6 @@ def _get_mac_address(ip_address):
return None
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Sony Bravia TV platform."""
host = config.get(CONF_HOST)
diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py
index a9bea9e4c1d137..eced0dbbe25bb2 100644
--- a/homeassistant/components/media_player/cast.py
+++ b/homeassistant/components/media_player/cast.py
@@ -17,6 +17,7 @@
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import (dispatcher_send,
async_dispatcher_connect)
+from homeassistant.components.cast import DOMAIN as CAST_DOMAIN
from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
@@ -28,7 +29,7 @@
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
-REQUIREMENTS = ['pychromecast==2.1.0']
+DEPENDENCIES = ('cast',)
_LOGGER = logging.getLogger(__name__)
@@ -186,6 +187,26 @@ def _async_create_cast_device(hass: HomeAssistantType,
async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
async_add_devices, discovery_info=None):
+ """Set up thet Cast platform.
+
+ Deprecated.
+ """
+ _LOGGER.warning(
+ 'Setting configuration for Cast via platform is deprecated. '
+ 'Configure via Cast component instead.')
+ await _async_setup_platform(
+ hass, config, async_add_devices, discovery_info)
+
+
+async def async_setup_entry(hass, config_entry, async_add_devices):
+ """Set up Cast from a config entry."""
+ await _async_setup_platform(
+ hass, hass.data[CAST_DOMAIN].get('media_player', {}),
+ async_add_devices, None)
+
+
+async def _async_setup_platform(hass: HomeAssistantType, config: ConfigType,
+ async_add_devices, discovery_info):
"""Set up the cast platform."""
import pychromecast
diff --git a/homeassistant/components/media_player/channels.py b/homeassistant/components/media_player/channels.py
index 6b41ace6ce21b5..41713e0c5bc6f6 100644
--- a/homeassistant/components/media_player/channels.py
+++ b/homeassistant/components/media_player/channels.py
@@ -105,7 +105,6 @@ def service_handler(service):
class ChannelsPlayer(MediaPlayerDevice):
"""Representation of a Channels instance."""
- # pylint: disable=too-many-public-methods
def __init__(self, name, host, port):
"""Initialize the Channels app."""
from pychannels import Channels
diff --git a/homeassistant/components/media_player/clementine.py b/homeassistant/components/media_player/clementine.py
index 6847b87e54f7c7..1ee18576ab8c85 100644
--- a/homeassistant/components/media_player/clementine.py
+++ b/homeassistant/components/media_player/clementine.py
@@ -43,7 +43,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Clementine platform."""
from clementineremote import ClementineRemote
diff --git a/homeassistant/components/media_player/demo.py b/homeassistant/components/media_player/demo.py
index 22fe1d005f711f..405c220c8770a3 100644
--- a/homeassistant/components/media_player/demo.py
+++ b/homeassistant/components/media_player/demo.py
@@ -8,13 +8,12 @@
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
- SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, SUPPORT_PLAY,
- SUPPORT_SHUFFLE_SET, MediaPlayerDevice)
+ SUPPORT_SELECT_SOURCE, SUPPORT_SELECT_SOUND_MODE, SUPPORT_CLEAR_PLAYLIST,
+ SUPPORT_PLAY, SUPPORT_SHUFFLE_SET, MediaPlayerDevice)
from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING
import homeassistant.util.dt as dt_util
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the media player demo platform."""
add_devices([
@@ -28,22 +27,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
YOUTUBE_COVER_URL_FORMAT = 'https://img.youtube.com/vi/{}/hqdefault.jpg'
+SOUND_MODE_LIST = ['Dummy Music', 'Dummy Movie']
+DEFAULT_SOUND_MODE = 'Dummy Music'
YOUTUBE_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | SUPPORT_PLAY | \
- SUPPORT_SHUFFLE_SET
+ SUPPORT_SHUFFLE_SET | SUPPORT_SELECT_SOUND_MODE
MUSIC_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_CLEAR_PLAYLIST | \
SUPPORT_PLAY | SUPPORT_SHUFFLE_SET | \
- SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
+ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
+ SUPPORT_SELECT_SOUND_MODE
NETFLIX_PLAYER_SUPPORT = \
SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \
SUPPORT_SELECT_SOURCE | SUPPORT_PLAY | SUPPORT_SHUFFLE_SET | \
- SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
+ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
+ SUPPORT_SELECT_SOUND_MODE
class AbstractDemoPlayer(MediaPlayerDevice):
@@ -58,6 +61,8 @@ def __init__(self, name):
self._volume_level = 1.0
self._volume_muted = False
self._shuffle = False
+ self._sound_mode_list = SOUND_MODE_LIST
+ self._sound_mode = DEFAULT_SOUND_MODE
@property
def should_poll(self):
@@ -89,6 +94,16 @@ def shuffle(self):
"""Boolean if shuffling is enabled."""
return self._shuffle
+ @property
+ def sound_mode(self):
+ """Return the current sound mode."""
+ return self._sound_mode
+
+ @property
+ def sound_mode_list(self):
+ """Return a list of available sound modes."""
+ return self._sound_mode_list
+
def turn_on(self):
"""Turn the media player on."""
self._player_state = STATE_PLAYING
@@ -124,6 +139,11 @@ def set_shuffle(self, shuffle):
self._shuffle = shuffle
self.schedule_update_ha_state()
+ def select_sound_mode(self, sound_mode):
+ """Select sound mode."""
+ self._sound_mode = sound_mode
+ self.schedule_update_ha_state()
+
class DemoYoutubePlayer(AbstractDemoPlayer):
"""A Demo media player that only supports YouTube."""
diff --git a/homeassistant/components/media_player/denonavr.py b/homeassistant/components/media_player/denonavr.py
index 74d3c5a0785fd3..8cd47476058f35 100644
--- a/homeassistant/components/media_player/denonavr.py
+++ b/homeassistant/components/media_player/denonavr.py
@@ -20,7 +20,7 @@
CONF_NAME, STATE_ON, CONF_ZONE, CONF_TIMEOUT)
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['denonavr==0.7.2']
+REQUIREMENTS = ['denonavr==0.7.3']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/media_player/dunehd.py b/homeassistant/components/media_player/dunehd.py
index efa5e7e607983d..ed20ac25cf90eb 100644
--- a/homeassistant/components/media_player/dunehd.py
+++ b/homeassistant/components/media_player/dunehd.py
@@ -32,7 +32,6 @@
SUPPORT_PLAY
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the DuneHD media player platform."""
from pdunehd import DuneHDPlayer
diff --git a/homeassistant/components/media_player/epson.py b/homeassistant/components/media_player/epson.py
new file mode 100644
index 00000000000000..b22234a40940fa
--- /dev/null
+++ b/homeassistant/components/media_player/epson.py
@@ -0,0 +1,211 @@
+"""
+Support for Epson projector.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/media_player.epson/
+"""
+import logging
+import voluptuous as vol
+
+from homeassistant.components.media_player import (
+ DOMAIN, MEDIA_PLAYER_SCHEMA, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK,
+ SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF,
+ SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
+ MediaPlayerDevice)
+from homeassistant.const import (
+ ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PORT, CONF_SSL, STATE_OFF,
+ STATE_ON)
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+import homeassistant.helpers.config_validation as cv
+
+REQUIREMENTS = ['epson-projector==0.1.3']
+
+DATA_EPSON = 'epson'
+DEFAULT_NAME = 'EPSON Projector'
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_HOST): cv.string,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_PORT, default=80): cv.port,
+ vol.Optional(CONF_SSL, default=False): cv.boolean
+})
+
+SERVICE_SELECT_CMODE = 'epson_select_cmode'
+ATTR_CMODE = 'cmode'
+SUPPORT_CMODE = 33001
+
+SUPPORT_EPSON = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE |\
+ SUPPORT_CMODE | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP | \
+ SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_platform(hass, config, async_add_devices,
+ discovery_info=None):
+ """Set up the Epson media player platform."""
+ if DATA_EPSON not in hass.data:
+ hass.data[DATA_EPSON] = []
+ name = config.get(CONF_NAME)
+ host = config.get(CONF_HOST)
+
+ epson = EpsonProjector(async_get_clientsession(hass, verify_ssl=False),
+ name, host,
+ config.get(CONF_PORT), config.get(CONF_SSL))
+ hass.data[DATA_EPSON].append(epson)
+ async_add_devices([epson], update_before_add=True)
+
+ async def async_service_handler(service):
+ """Handle for services."""
+ entity_ids = service.data.get(ATTR_ENTITY_ID)
+ if entity_ids:
+ devices = [device for device in hass.data[DATA_EPSON]
+ if device.entity_id in entity_ids]
+ else:
+ devices = hass.data[DATA_EPSON]
+ for device in devices:
+ if service.service == SERVICE_SELECT_CMODE:
+ cmode = service.data.get(ATTR_CMODE)
+ await device.select_cmode(cmode)
+ await device.update()
+ from epson_projector.const import (CMODE_LIST_SET)
+ epson_schema = MEDIA_PLAYER_SCHEMA.extend({
+ vol.Required(ATTR_CMODE): vol.All(cv.string, vol.Any(*CMODE_LIST_SET))
+ })
+ hass.services.async_register(
+ DOMAIN, SERVICE_SELECT_CMODE, async_service_handler,
+ schema=epson_schema)
+
+
+class EpsonProjector(MediaPlayerDevice):
+ """Representation of Epson Projector Device."""
+
+ def __init__(self, websession, name, host, port, encryption):
+ """Initialize entity to control Epson projector."""
+ self._name = name
+ import epson_projector as epson
+ from epson_projector.const import DEFAULT_SOURCES
+ self._projector = epson.Projector(
+ host,
+ websession=websession,
+ port=port)
+ self._cmode = None
+ self._source_list = list(DEFAULT_SOURCES.values())
+ self._source = None
+ self._volume = None
+ self._state = None
+
+ async def update(self):
+ """Update state of device."""
+ from epson_projector.const import (
+ EPSON_CODES, POWER,
+ CMODE, CMODE_LIST, SOURCE, VOLUME,
+ BUSY, SOURCE_LIST)
+ is_turned_on = await self._projector.get_property(POWER)
+ _LOGGER.debug("Project turn on/off status: %s", is_turned_on)
+ if is_turned_on and is_turned_on == EPSON_CODES[POWER]:
+ self._state = STATE_ON
+ cmode = await self._projector.get_property(CMODE)
+ self._cmode = CMODE_LIST.get(cmode, self._cmode)
+ source = await self._projector.get_property(SOURCE)
+ self._source = SOURCE_LIST.get(source, self._source)
+ volume = await self._projector.get_property(VOLUME)
+ if volume:
+ self._volume = volume
+ elif is_turned_on == BUSY:
+ self._state = STATE_ON
+ else:
+ self._state = STATE_OFF
+
+ @property
+ def name(self):
+ """Return the name of the device."""
+ return self._name
+
+ @property
+ def state(self):
+ """Return the state of the device."""
+ return self._state
+
+ @property
+ def supported_features(self):
+ """Flag media player features that are supported."""
+ return SUPPORT_EPSON
+
+ async def async_turn_on(self):
+ """Turn on epson."""
+ from epson_projector.const import TURN_ON
+ await self._projector.send_command(TURN_ON)
+
+ async def async_turn_off(self):
+ """Turn off epson."""
+ from epson_projector.const import TURN_OFF
+ await self._projector.send_command(TURN_OFF)
+
+ @property
+ def source_list(self):
+ """List of available input sources."""
+ return self._source_list
+
+ @property
+ def source(self):
+ """Get current input sources."""
+ return self._source
+
+ @property
+ def volume_level(self):
+ """Return the volume level of the media player (0..1)."""
+ return self._volume
+
+ async def select_cmode(self, cmode):
+ """Set color mode in Epson."""
+ from epson_projector.const import (CMODE_LIST_SET)
+ await self._projector.send_command(CMODE_LIST_SET[cmode])
+
+ async def async_select_source(self, source):
+ """Select input source."""
+ from epson_projector.const import INV_SOURCES
+ selected_source = INV_SOURCES[source]
+ await self._projector.send_command(selected_source)
+
+ async def async_mute_volume(self, mute):
+ """Mute (true) or unmute (false) sound."""
+ from epson_projector.const import MUTE
+ await self._projector.send_command(MUTE)
+
+ async def async_volume_up(self):
+ """Increase volume."""
+ from epson_projector.const import VOL_UP
+ await self._projector.send_command(VOL_UP)
+
+ async def async_volume_down(self):
+ """Decrease volume."""
+ from epson_projector.const import VOL_DOWN
+ await self._projector.send_command(VOL_DOWN)
+
+ async def async_media_play(self):
+ """Play media via Epson."""
+ from epson_projector.const import PLAY
+ await self._projector.send_command(PLAY)
+
+ async def async_media_pause(self):
+ """Pause media via Epson."""
+ from epson_projector.const import PAUSE
+ await self._projector.send_command(PAUSE)
+
+ async def async_media_next_track(self):
+ """Skip to next."""
+ from epson_projector.const import FAST
+ await self._projector.send_command(FAST)
+
+ async def async_media_previous_track(self):
+ """Skip to previous."""
+ from epson_projector.const import BACK
+ await self._projector.send_command(BACK)
+
+ @property
+ def device_state_attributes(self):
+ """Return device specific state attributes."""
+ attributes = {}
+ if self._cmode is not None:
+ attributes[ATTR_CMODE] = self._cmode
+ return attributes
diff --git a/homeassistant/components/media_player/firetv.py b/homeassistant/components/media_player/firetv.py
index 9d66ae77eeff06..157db2c44d3a35 100644
--- a/homeassistant/components/media_player/firetv.py
+++ b/homeassistant/components/media_player/firetv.py
@@ -43,7 +43,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the FireTV platform."""
name = config.get(CONF_NAME)
diff --git a/homeassistant/components/media_player/frontier_silicon.py b/homeassistant/components/media_player/frontier_silicon.py
index 6d95ea675fb812..ab594f47e14d62 100644
--- a/homeassistant/components/media_player/frontier_silicon.py
+++ b/homeassistant/components/media_player/frontier_silicon.py
@@ -41,7 +41,6 @@
})
-# pylint: disable=unused-argument
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Frontier Silicon platform."""
diff --git a/homeassistant/components/media_player/gpmdp.py b/homeassistant/components/media_player/gpmdp.py
index 2f116abebc3311..4a0ec1fa87f4aa 100644
--- a/homeassistant/components/media_player/gpmdp.py
+++ b/homeassistant/components/media_player/gpmdp.py
@@ -59,7 +59,6 @@ def request_configuration(hass, config, url, add_devices_callback):
'method': 'connect',
'arguments': ['Home Assistant']}))
- # pylint: disable=unused-argument
def gpmdp_configuration_callback(callback_data):
"""Handle configuration changes."""
while True:
diff --git a/homeassistant/components/media_player/gstreamer.py b/homeassistant/components/media_player/gstreamer.py
index 064ca68ea9561c..91cd8d19cc4a59 100644
--- a/homeassistant/components/media_player/gstreamer.py
+++ b/homeassistant/components/media_player/gstreamer.py
@@ -34,7 +34,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Gstreamer platform."""
from gsp import GstreamerPlayer
diff --git a/homeassistant/components/media_player/horizon.py b/homeassistant/components/media_player/horizon.py
new file mode 100644
index 00000000000000..4b0f9d0cf21ae5
--- /dev/null
+++ b/homeassistant/components/media_player/horizon.py
@@ -0,0 +1,187 @@
+"""
+Support for the Unitymedia Horizon HD Recorder.
+
+For more details about this platform, please refer to the documentation
+https://home-assistant.io/components/media_player.horizon/
+"""
+
+from datetime import timedelta
+import logging
+
+import voluptuous as vol
+
+from homeassistant.components.media_player import (
+ MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_CHANNEL,
+ SUPPORT_NEXT_TRACK, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PAUSE,
+ SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK)
+from homeassistant.const import (CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF,
+ STATE_PAUSED, STATE_PLAYING)
+from homeassistant.exceptions import PlatformNotReady
+import homeassistant.helpers.config_validation as cv
+import homeassistant.util as util
+
+REQUIREMENTS = ['einder==0.3.1']
+
+_LOGGER = logging.getLogger(__name__)
+
+DEFAULT_NAME = "Horizon"
+DEFAULT_PORT = 5900
+
+MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
+MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
+
+SUPPORT_HORIZON = SUPPORT_NEXT_TRACK | SUPPORT_PAUSE | SUPPORT_PLAY | \
+ SUPPORT_PLAY_MEDIA | SUPPORT_PREVIOUS_TRACK | SUPPORT_TURN_ON | \
+ SUPPORT_TURN_OFF
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_HOST): cv.string,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
+})
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the Horizon platform."""
+ from einder import Client, keys
+ from einder.exceptions import AuthenticationError
+
+ host = config[CONF_HOST]
+ name = config[CONF_NAME]
+ port = config[CONF_PORT]
+
+ try:
+ client = Client(host, port=port)
+ except AuthenticationError as msg:
+ _LOGGER.error("Authentication to %s at %s failed: %s", name, host, msg)
+ return
+ except OSError as msg:
+ # occurs if horizon box is offline
+ _LOGGER.error("Connection to %s at %s failed: %s", name, host, msg)
+ raise PlatformNotReady
+
+ _LOGGER.info("Connection to %s at %s established", name, host)
+
+ add_devices([HorizonDevice(client, name, keys)], True)
+
+
+class HorizonDevice(MediaPlayerDevice):
+ """Representation of a Horizon HD Recorder."""
+
+ def __init__(self, client, name, keys):
+ """Initialize the remote."""
+ self._client = client
+ self._name = name
+ self._state = None
+ self._keys = keys
+
+ @property
+ def name(self):
+ """Return the name of the remote."""
+ return self._name
+
+ @property
+ def state(self):
+ """Return the state of the device."""
+ return self._state
+
+ @property
+ def supported_features(self):
+ """Flag media player features that are supported."""
+ return SUPPORT_HORIZON
+
+ @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
+ def update(self):
+ """Update State using the media server running on the Horizon."""
+ if self._client.is_powered_on():
+ self._state = STATE_PLAYING
+ else:
+ self._state = STATE_OFF
+
+ def turn_on(self):
+ """Turn the device on."""
+ if self._state is STATE_OFF:
+ self._send_key(self._keys.POWER)
+
+ def turn_off(self):
+ """Turn the device off."""
+ if self._state is not STATE_OFF:
+ self._send_key(self._keys.POWER)
+
+ def media_previous_track(self):
+ """Channel down."""
+ self._send_key(self._keys.CHAN_DOWN)
+ self._state = STATE_PLAYING
+
+ def media_next_track(self):
+ """Channel up."""
+ self._send_key(self._keys.CHAN_UP)
+ self._state = STATE_PLAYING
+
+ def media_play(self):
+ """Send play command."""
+ self._send_key(self._keys.PAUSE)
+ self._state = STATE_PLAYING
+
+ def media_pause(self):
+ """Send pause command."""
+ self._send_key(self._keys.PAUSE)
+ self._state = STATE_PAUSED
+
+ def media_play_pause(self):
+ """Send play/pause command."""
+ self._send_key(self._keys.PAUSE)
+ if self._state == STATE_PAUSED:
+ self._state = STATE_PLAYING
+ else:
+ self._state = STATE_PAUSED
+
+ def play_media(self, media_type, media_id, **kwargs):
+ """Play media / switch to channel."""
+ if MEDIA_TYPE_CHANNEL == media_type:
+ try:
+ self._select_channel(int(media_id))
+ self._state = STATE_PLAYING
+ except ValueError:
+ _LOGGER.error("Invalid channel: %s", media_id)
+ else:
+ _LOGGER.error("Invalid media type %s. Supported type: %s",
+ media_type, MEDIA_TYPE_CHANNEL)
+
+ def _select_channel(self, channel):
+ """Select a channel (taken from einder library, thx)."""
+ self._send(channel=channel)
+
+ def _send_key(self, key):
+ """Send a key to the Horizon device."""
+ self._send(key=key)
+
+ def _send(self, key=None, channel=None):
+ """Send a key to the Horizon device."""
+ from einder.exceptions import AuthenticationError
+
+ try:
+ if key:
+ self._client.send_key(key)
+ elif channel:
+ self._client.select_channel(channel)
+ except OSError as msg:
+ _LOGGER.error("%s disconnected: %s. Trying to reconnect...",
+ self._name, msg)
+
+ # for reconnect, first gracefully disconnect
+ self._client.disconnect()
+
+ try:
+ self._client.connect()
+ self._client.authorize()
+ except AuthenticationError as msg:
+ _LOGGER.error("Authentication to %s failed: %s", self._name,
+ msg)
+ return
+ except OSError as msg:
+ # occurs if horizon box is offline
+ _LOGGER.error("Reconnect to %s failed: %s", self._name, msg)
+ return
+
+ self._send(key=key, channel=channel)
diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py
index 68a9da55ae4f9d..7fa8d5b3fe84f7 100644
--- a/homeassistant/components/media_player/kodi.py
+++ b/homeassistant/components/media_player/kodi.py
@@ -393,7 +393,7 @@ def state(self):
if not self._players:
return STATE_IDLE
- if self._properties['speed'] == 0 and not self._properties['live']:
+ if self._properties['speed'] == 0:
return STATE_PAUSED
return STATE_PLAYING
diff --git a/homeassistant/components/media_player/lg_netcast.py b/homeassistant/components/media_player/lg_netcast.py
index edbd6546cca9f5..8c98844cf9358a 100644
--- a/homeassistant/components/media_player/lg_netcast.py
+++ b/homeassistant/components/media_player/lg_netcast.py
@@ -43,7 +43,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the LG TV platform."""
from pylgnetcast import LgNetCastClient
diff --git a/homeassistant/components/media_player/monoprice.py b/homeassistant/components/media_player/monoprice.py
index 44d19ac6860391..a951356500f082 100644
--- a/homeassistant/components/media_player/monoprice.py
+++ b/homeassistant/components/media_player/monoprice.py
@@ -55,7 +55,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Monoprice 6-zone amplifier platform."""
port = config.get(CONF_PORT)
diff --git a/homeassistant/components/media_player/mpchc.py b/homeassistant/components/media_player/mpchc.py
index a375a585ad40db..ad8dd0bf0564f0 100644
--- a/homeassistant/components/media_player/mpchc.py
+++ b/homeassistant/components/media_player/mpchc.py
@@ -35,7 +35,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the MPC-HC platform."""
name = config.get(CONF_NAME)
diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py
index 04dd1ac5f2e1ef..73417e5f25d7f0 100644
--- a/homeassistant/components/media_player/mpd.py
+++ b/homeassistant/components/media_player/mpd.py
@@ -46,7 +46,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the MPD platform."""
host = config.get(CONF_HOST)
diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py
index 71b74868544aff..92443ca2b42d99 100644
--- a/homeassistant/components/media_player/onkyo.py
+++ b/homeassistant/components/media_player/onkyo.py
@@ -32,6 +32,9 @@
SUPPORT_VOLUME_STEP | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \
SUPPORT_SELECT_SOURCE | SUPPORT_PLAY
+SUPPORT_ONKYO_WO_VOLUME = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \
+ SUPPORT_SELECT_SOURCE | SUPPORT_PLAY
+
KNOWN_HOSTS = [] # type: List[str]
DEFAULT_SOURCES = {'tv': 'TV', 'bd': 'Bluray', 'game': 'Game', 'aux1': 'Aux1',
'video1': 'Video 1', 'video2': 'Video 2',
@@ -270,7 +273,8 @@ class OnkyoDeviceZone(OnkyoDevice):
def __init__(self, zone, receiver, sources, name=None):
"""Initialize the Zone with the zone identifier."""
self._zone = zone
- super().__init__(receiver, sources, name)
+ self._supports_volume = True
+ super(OnkyoDeviceZone, self).__init__(receiver, sources, name)
def update(self):
"""Get the latest state from the device."""
@@ -289,9 +293,18 @@ def update(self):
current_source_raw = self.command(
'zone{}.selector=query'.format(self._zone))
+ # If we received a source value, but not a volume value
+ # it's likely this zone permanently does not support volume.
+ if current_source_raw and not volume_raw:
+ self._supports_volume = False
+
if not (volume_raw and mute_raw and current_source_raw):
return
+ # It's possible for some players to have zones set to HDMI with
+ # no sound control. In this case, the string `N/A` is returned.
+ self._supports_volume = isinstance(volume_raw[1], (float, int))
+
# eiscp can return string or tuple. Make everything tuples.
if isinstance(current_source_raw[1], str):
current_source_tuples = \
@@ -307,7 +320,16 @@ def update(self):
self._current_source = '_'.join(
[i for i in current_source_tuples[1]])
self._muted = bool(mute_raw[1] == 'on')
- self._volume = volume_raw[1] / 80.0
+
+ if self._supports_volume:
+ self._volume = volume_raw[1] / 80.0
+
+ @property
+ def supported_features(self):
+ """Return media player features that are supported."""
+ if self._supports_volume:
+ return SUPPORT_ONKYO
+ return SUPPORT_ONKYO_WO_VOLUME
def turn_off(self):
"""Turn the media player off."""
diff --git a/homeassistant/components/media_player/openhome.py b/homeassistant/components/media_player/openhome.py
index 5e30f9783c7582..5d9c7bd14c578a 100644
--- a/homeassistant/components/media_player/openhome.py
+++ b/homeassistant/components/media_player/openhome.py
@@ -25,7 +25,6 @@
DEVICES = []
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Openhome platform."""
from openhomedevice.Device import Device
diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py
index db60de922d998f..549071fde8e5d4 100644
--- a/homeassistant/components/media_player/panasonic_viera.py
+++ b/homeassistant/components/media_player/panasonic_viera.py
@@ -42,7 +42,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Panasonic Viera TV platform."""
from panasonic_viera import RemoteControl
diff --git a/homeassistant/components/media_player/pandora.py b/homeassistant/components/media_player/pandora.py
index d66811eed661f1..a47db7f633c4a6 100644
--- a/homeassistant/components/media_player/pandora.py
+++ b/homeassistant/components/media_player/pandora.py
@@ -43,7 +43,6 @@
STATION_PATTERN = re.compile(r'Station\s"(.+?)"', re.MULTILINE)
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Pandora media player platform."""
if not _pianobar_exists():
diff --git a/homeassistant/components/media_player/philips_js.py b/homeassistant/components/media_player/philips_js.py
index 01d63e0b6c845d..be0c0527f1bc9f 100644
--- a/homeassistant/components/media_player/philips_js.py
+++ b/homeassistant/components/media_player/philips_js.py
@@ -48,7 +48,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Philips TV platform."""
import haphilipsjs
diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py
index 0b7fc3c078e811..15a2b41795e8c7 100644
--- a/homeassistant/components/media_player/samsungtv.py
+++ b/homeassistant/components/media_player/samsungtv.py
@@ -47,7 +47,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Samsung TV platform."""
known_devices = hass.data.get(KNOWN_DEVICES_KEY)
@@ -155,16 +154,25 @@ def send_key(self, key):
_LOGGER.info("TV is powering off, not sending command: %s", key)
return
try:
- self.get_remote().control(key)
+ # recreate connection if connection was dead
+ retry_count = 1
+ for _ in range(retry_count + 1):
+ try:
+ self.get_remote().control(key)
+ break
+ except (self._exceptions_class.ConnectionClosed,
+ BrokenPipeError):
+ # BrokenPipe can occur when the commands is sent to fast
+ self._remote = None
self._state = STATE_ON
except (self._exceptions_class.UnhandledResponse,
- self._exceptions_class.AccessDenied, BrokenPipeError):
+ self._exceptions_class.AccessDenied):
# We got a response so it's on.
- # BrokenPipe can occur when the commands is sent to fast
self._state = STATE_ON
self._remote = None
+ _LOGGER.debug("Failed sending command %s", key, exc_info=True)
return
- except (self._exceptions_class.ConnectionClosed, OSError):
+ except OSError:
self._state = STATE_OFF
self._remote = None
if self._power_off_in_progress():
@@ -207,6 +215,7 @@ def turn_off(self):
# Force closing of remote session to provide instant UI feedback
try:
self.get_remote().close()
+ self._remote = None
except OSError:
_LOGGER.debug("Could not establish connection.")
diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml
index 0a6c413a688b88..3c91f19469b2b3 100644
--- a/homeassistant/components/media_player/services.yaml
+++ b/homeassistant/components/media_player/services.yaml
@@ -144,6 +144,16 @@ select_source:
description: Name of the source to switch to. Platform dependent.
example: 'video1'
+select_sound_mode:
+ description: Send the media player the command to change sound mode.
+ fields:
+ entity_id:
+ description: Name(s) of entities to change sound mode on.
+ example: 'media_player.marantz'
+ sound_mode:
+ description: Name of the sound mode to switch to.
+ example: 'Music'
+
clear_playlist:
description: Send the media player the command to clear players playlist.
fields:
@@ -412,3 +422,13 @@ blackbird_set_all_zones:
source:
description: Name of source to switch to.
example: 'Source 1'
+
+epson_select_cmode:
+ description: Select Color mode of Epson projector
+ fields:
+ entity_id:
+ description: Name of projector
+ example: 'media_player.epson_projector'
+ cmode:
+ description: Name of Cmode
+ example: 'cinema'
diff --git a/homeassistant/components/media_player/snapcast.py b/homeassistant/components/media_player/snapcast.py
index 793800a3d2259e..a880d3c920d150 100644
--- a/homeassistant/components/media_player/snapcast.py
+++ b/homeassistant/components/media_player/snapcast.py
@@ -46,7 +46,6 @@
})
-# pylint: disable=unused-argument
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Snapcast platform."""
@@ -80,8 +79,11 @@ def _handle_service(service):
host, port)
return
- groups = [SnapcastGroupDevice(group) for group in server.groups]
- clients = [SnapcastClientDevice(client) for client in server.clients]
+ # Note: Host part is needed, when using multiple snapservers
+ hpid = '{}:{}'.format(host, port)
+
+ groups = [SnapcastGroupDevice(group, hpid) for group in server.groups]
+ clients = [SnapcastClientDevice(client, hpid) for client in server.clients]
devices = groups + clients
hass.data[DATA_KEY] = devices
async_add_devices(devices)
@@ -90,10 +92,12 @@ def _handle_service(service):
class SnapcastGroupDevice(MediaPlayerDevice):
"""Representation of a Snapcast group device."""
- def __init__(self, group):
+ def __init__(self, group, uid_part):
"""Initialize the Snapcast group device."""
group.set_callback(self.schedule_update_ha_state)
self._group = group
+ self._uid = '{}{}_{}'.format(GROUP_PREFIX, uid_part,
+ self._group.identifier)
@property
def state(self):
@@ -104,6 +108,11 @@ def state(self):
'unknown': STATE_UNKNOWN,
}.get(self._group.stream_status, STATE_UNKNOWN)
+ @property
+ def unique_id(self):
+ """Return the ID of snapcast group."""
+ return self._uid
+
@property
def name(self):
"""Return the name of the device."""
@@ -180,10 +189,21 @@ def async_restore(self):
class SnapcastClientDevice(MediaPlayerDevice):
"""Representation of a Snapcast client device."""
- def __init__(self, client):
+ def __init__(self, client, uid_part):
"""Initialize the Snapcast client device."""
client.set_callback(self.schedule_update_ha_state)
self._client = client
+ self._uid = '{}{}_{}'.format(CLIENT_PREFIX, uid_part,
+ self._client.identifier)
+
+ @property
+ def unique_id(self):
+ """
+ Return the ID of this snapcast client.
+
+ Note: Host part is needed, when using multiple snapservers
+ """
+ return self._uid
@property
def name(self):
diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py
index 0f536e1edfb5ab..da0ad24b135591 100644
--- a/homeassistant/components/media_player/sonos.py
+++ b/homeassistant/components/media_player/sonos.py
@@ -20,13 +20,14 @@
SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_STOP,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice)
+from homeassistant.components.sonos import DOMAIN as SONOS_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_TIME, CONF_HOSTS, STATE_IDLE, STATE_OFF, STATE_PAUSED,
STATE_PLAYING)
import homeassistant.helpers.config_validation as cv
from homeassistant.util.dt import utcnow
-REQUIREMENTS = ['SoCo==0.14']
+DEPENDENCIES = ('sonos',)
_LOGGER = logging.getLogger(__name__)
@@ -49,7 +50,7 @@
SERVICE_UPDATE_ALARM = 'sonos_update_alarm'
SERVICE_SET_OPTION = 'sonos_set_option'
-DATA_SONOS = 'sonos'
+DATA_SONOS = 'sonos_devices'
SOURCE_LINEIN = 'Line-in'
SOURCE_TV = 'TV'
@@ -118,6 +119,26 @@ def __init__(self):
def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the Sonos platform.
+
+ Deprecated.
+ """
+ _LOGGER.warning('Loading Sonos via platform config is deprecated.')
+ _setup_platform(hass, config, add_devices, discovery_info)
+
+
+async def async_setup_entry(hass, config_entry, async_add_devices):
+ """Set up Sonos from a config entry."""
+ def add_devices(devices, update_before_add=False):
+ """Sync version of async add devices."""
+ hass.add_job(async_add_devices, devices, update_before_add)
+
+ hass.add_job(_setup_platform, hass,
+ hass.data[SONOS_DOMAIN].get('media_player', {}),
+ add_devices, None)
+
+
+def _setup_platform(hass, config, add_devices, discovery_info):
"""Set up the Sonos platform."""
import soco
import soco.events
diff --git a/homeassistant/components/media_player/spotify.py b/homeassistant/components/media_player/spotify.py
index 963258f1861df6..73ec8a175b1f17 100644
--- a/homeassistant/components/media_player/spotify.py
+++ b/homeassistant/components/media_player/spotify.py
@@ -20,9 +20,7 @@
CONF_NAME, STATE_PLAYING, STATE_PAUSED, STATE_IDLE, STATE_UNKNOWN)
import homeassistant.helpers.config_validation as cv
-COMMIT = '544614f4b1d508201d363e84e871f86c90aa26b2'
-REQUIREMENTS = ['https://github.com/happyleavesaoc/spotipy/'
- 'archive/%s.zip#spotipy==2.4.4' % COMMIT]
+REQUIREMENTS = ['spotipy-homeassistant==2.4.4.dev1']
DEPENDENCIES = ['http']
diff --git a/homeassistant/components/media_player/vlc.py b/homeassistant/components/media_player/vlc.py
index abd8252d813c44..45e1a91c510fda 100644
--- a/homeassistant/components/media_player/vlc.py
+++ b/homeassistant/components/media_player/vlc.py
@@ -34,7 +34,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the vlc platform."""
add_devices([VlcDevice(config.get(CONF_NAME, DEFAULT_NAME),
diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py
index c3426e454048f5..42d0ae85ab3c8e 100644
--- a/homeassistant/components/media_player/webostv.py
+++ b/homeassistant/components/media_player/webostv.py
@@ -61,7 +61,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the LG WebOS TV platform."""
if discovery_info is not None:
@@ -139,7 +138,6 @@ def request_configuration(
_CONFIGURING[host], 'Failed to pair, please try again.')
return
- # pylint: disable=unused-argument
def lgtv_configuration_callback(data):
"""Handle actions when configuration callback is called."""
setup_tv(host, name, customize, config, timeout, hass,
diff --git a/homeassistant/components/media_player/xiaomi_tv.py b/homeassistant/components/media_player/xiaomi_tv.py
index be40bf7d010752..d44ac138e4171f 100644
--- a/homeassistant/components/media_player/xiaomi_tv.py
+++ b/homeassistant/components/media_player/xiaomi_tv.py
@@ -13,7 +13,7 @@
SUPPORT_TURN_ON, SUPPORT_TURN_OFF, MediaPlayerDevice, PLATFORM_SCHEMA,
SUPPORT_VOLUME_STEP)
-REQUIREMENTS = ['pymitv==1.0.0']
+REQUIREMENTS = ['pymitv==1.4.0']
DEFAULT_NAME = "Xiaomi TV"
@@ -39,7 +39,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if host is not None:
# Check if there's a valid TV at the IP address.
- if not Discover().checkIp(host):
+ if not Discover().check_ip(host):
_LOGGER.error(
"Could not find Xiaomi TV with specified IP: %s", host
)
diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py
index bb7942a2545ee2..cf36345806745e 100644
--- a/homeassistant/components/media_player/yamaha.py
+++ b/homeassistant/components/media_player/yamaha.py
@@ -6,32 +6,44 @@
"""
import logging
+import requests
import voluptuous as vol
from homeassistant.components.media_player import (
+ DOMAIN, MEDIA_PLAYER_SCHEMA, MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA,
+ SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA,
+ SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_STOP,
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
- SUPPORT_SELECT_SOURCE, SUPPORT_PLAY_MEDIA, SUPPORT_PAUSE, SUPPORT_STOP,
- SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY,
- MEDIA_TYPE_MUSIC, MEDIA_PLAYER_SCHEMA, DOMAIN,
- MediaPlayerDevice, PLATFORM_SCHEMA)
-from homeassistant.const import (CONF_NAME, CONF_HOST, STATE_OFF, STATE_ON,
- STATE_PLAYING, STATE_IDLE, ATTR_ENTITY_ID)
+ MediaPlayerDevice)
+from homeassistant.const import (
+ ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, STATE_IDLE, STATE_OFF, STATE_ON,
+ STATE_PLAYING)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['rxv==0.5.1']
_LOGGER = logging.getLogger(__name__)
-SUPPORT_YAMAHA = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
- SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE | SUPPORT_PLAY
+ATTR_ENABLED = 'enabled'
+ATTR_PORT = 'port'
-CONF_SOURCE_NAMES = 'source_names'
CONF_SOURCE_IGNORE = 'source_ignore'
-CONF_ZONE_NAMES = 'zone_names'
+CONF_SOURCE_NAMES = 'source_names'
CONF_ZONE_IGNORE = 'zone_ignore'
+CONF_ZONE_NAMES = 'zone_names'
-DEFAULT_NAME = 'Yamaha Receiver'
DATA_YAMAHA = 'yamaha_known_receivers'
+DEFAULT_NAME = "Yamaha Receiver"
+
+ENABLE_OUTPUT_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
+ vol.Required(ATTR_ENABLED): cv.boolean,
+ vol.Required(ATTR_PORT): cv.string,
+})
+
+SERVICE_ENABLE_OUTPUT = 'yamaha_enable_output'
+
+SUPPORT_YAMAHA = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
+ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE | SUPPORT_PLAY
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
@@ -44,16 +56,6 @@
vol.Optional(CONF_ZONE_NAMES, default={}): {cv.string: cv.string},
})
-SERVICE_ENABLE_OUTPUT = 'yamaha_enable_output'
-
-ATTR_PORT = 'port'
-ATTR_ENABLED = 'enabled'
-
-ENABLE_OUTPUT_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({
- vol.Required(ATTR_PORT): cv.string,
- vol.Required(ATTR_ENABLED): cv.boolean
-})
-
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Yamaha platform."""
@@ -80,7 +82,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
receivers = rxv.RXV(
ctrl_url, model_name=model, friendly_name=name,
unit_desc_url=desc_url).zone_controllers()
- _LOGGER.info("Receivers: %s", receivers)
+ _LOGGER.debug("Receivers: %s", receivers)
# when we are dynamically discovered config is empty
zone_ignore = []
elif host is None:
@@ -96,15 +98,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if receiver.zone in zone_ignore:
continue
- device = YamahaDevice(name, receiver, source_ignore,
- source_names, zone_names)
+ device = YamahaDevice(
+ name, receiver, source_ignore, source_names, zone_names)
# Only add device if it's not already added
if device.zone_id not in hass.data[DATA_YAMAHA]:
hass.data[DATA_YAMAHA][device.zone_id] = device
devices.append(device)
else:
- _LOGGER.debug('Ignoring duplicate receiver %s', name)
+ _LOGGER.debug("Ignoring duplicate receiver: %s", name)
def service_handler(service):
"""Handle for services."""
@@ -130,8 +132,8 @@ def service_handler(service):
class YamahaDevice(MediaPlayerDevice):
"""Representation of a Yamaha device."""
- def __init__(self, name, receiver, source_ignore,
- source_names, zone_names):
+ def __init__(
+ self, name, receiver, source_ignore, source_names, zone_names):
"""Initialize the Yamaha Receiver."""
self.receiver = receiver
self._muted = False
@@ -151,7 +153,12 @@ def __init__(self, name, receiver, source_ignore,
def update(self):
"""Get the latest details from the device."""
- self._play_status = self.receiver.play_status()
+ try:
+ self._play_status = self.receiver.play_status()
+ except requests.exceptions.ConnectionError:
+ _LOGGER.info("Receiver is offline: %s", self._name)
+ return
+
if self.receiver.on:
if self._play_status is None:
self._pwstate = STATE_ON
@@ -231,11 +238,13 @@ def supported_features(self):
supported_features = SUPPORT_YAMAHA
supports = self._playback_support
- mapping = {'play': (SUPPORT_PLAY | SUPPORT_PLAY_MEDIA),
- 'pause': SUPPORT_PAUSE,
- 'stop': SUPPORT_STOP,
- 'skip_f': SUPPORT_NEXT_TRACK,
- 'skip_r': SUPPORT_PREVIOUS_TRACK}
+ mapping = {
+ 'play': (SUPPORT_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_features |= feature
diff --git a/homeassistant/components/modbus.py b/homeassistant/components/modbus.py
index a928c0d3aca031..fe46c858b5119f 100644
--- a/homeassistant/components/modbus.py
+++ b/homeassistant/components/modbus.py
@@ -75,11 +75,11 @@
def setup(hass, config):
"""Set up Modbus component."""
# Modbus connection type
- # pylint: disable=global-statement, import-error
+ # pylint: disable=import-error
client_type = config[DOMAIN][CONF_TYPE]
# Connect to Modbus network
- # pylint: disable=global-statement, import-error
+ # pylint: disable=import-error
if client_type == 'serial':
from pymodbus.client.sync import ModbusSerialClient as ModbusClient
diff --git a/homeassistant/components/mqtt_eventstream.py b/homeassistant/components/mqtt_eventstream.py
index aa670578172374..ea4463f5c2347e 100644
--- a/homeassistant/components/mqtt_eventstream.py
+++ b/homeassistant/components/mqtt_eventstream.py
@@ -116,5 +116,4 @@ def _event_receiver(topic, payload, qos):
if sub_topic:
yield from mqtt.async_subscribe(sub_topic, _event_receiver)
- hass.states.async_set('{domain}.initialized'.format(domain=DOMAIN), True)
return True
diff --git a/homeassistant/components/neato.py b/homeassistant/components/neato.py
index 7402bb18843ad2..c6a3dcf9c9a605 100644
--- a/homeassistant/components/neato.py
+++ b/homeassistant/components/neato.py
@@ -17,8 +17,8 @@
_LOGGER = logging.getLogger(__name__)
-REQUIREMENTS = ['https://github.com/jabesq/pybotvac/archive/v0.0.5.zip'
- '#pybotvac==0.0.5']
+REQUIREMENTS = ['https://github.com/jabesq/pybotvac/archive/v0.0.6.zip'
+ '#pybotvac==0.0.6']
DOMAIN = 'neato'
NEATO_ROBOTS = 'neato_robots'
@@ -122,7 +122,7 @@ def login(self):
_LOGGER.error("Unable to connect to Neato API")
return False
- @Throttle(timedelta(seconds=1))
+ @Throttle(timedelta(seconds=60))
def update_robots(self):
"""Update the robot states."""
_LOGGER.debug("Running HUB.update_robots %s",
diff --git a/homeassistant/components/nest/.translations/ca.json b/homeassistant/components/nest/.translations/ca.json
new file mode 100644
index 00000000000000..2fb17916aee81b
--- /dev/null
+++ b/homeassistant/components/nest/.translations/ca.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Nom\u00e9s podeu configurar un \u00fanic compte Nest.",
+ "authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.",
+ "authorize_url_timeout": "Temps d'espera generant l'URL d'autoritzaci\u00f3 esgotat.",
+ "no_flows": "Necessiteu configurar Nest abans de poder autenticar-vos-hi. [Llegiu les instruccions](https://www.home-assistant.io/components/nest/)."
+ },
+ "error": {
+ "internal_error": "Error intern al validar el codi",
+ "invalid_code": "Codi inv\u00e0lid",
+ "timeout": "Temps d'espera de validaci\u00f3 del codi esgotat",
+ "unknown": "Error desconegut al validar el codi"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "flow_impl": "Prove\u00efdor"
+ },
+ "description": "Trieu a trav\u00e9s de quin prove\u00efdor d'autenticaci\u00f3 us voleu autenticar amb Nest.",
+ "title": "Prove\u00efdor d'autenticaci\u00f3"
+ },
+ "link": {
+ "data": {
+ "code": "Codi pin"
+ },
+ "description": "Per enlla\u00e7ar el vostre compte de Nest, [autoritzeu el vostre compte] ({url}). \n\nDespr\u00e9s de l'autoritzaci\u00f3, copieu i enganxeu el codi pin que es mostra a sota.",
+ "title": "Enlla\u00e7ar compte de Nest"
+ }
+ },
+ "title": "Nest"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nest/.translations/en.json b/homeassistant/components/nest/.translations/en.json
new file mode 100644
index 00000000000000..cf448bb35e7273
--- /dev/null
+++ b/homeassistant/components/nest/.translations/en.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "You can only configure a single Nest account.",
+ "authorize_url_fail": "Unknown error generating an authorize url.",
+ "authorize_url_timeout": "Timeout generating authorize url.",
+ "no_flows": "You need to configure Nest before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/nest/)."
+ },
+ "error": {
+ "internal_error": "Internal error validating code",
+ "invalid_code": "Invalid code",
+ "timeout": "Timeout validating code",
+ "unknown": "Unknown error validating code"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "flow_impl": "Provider"
+ },
+ "description": "Pick via which authentication provider you want to authenticate with Nest.",
+ "title": "Authentication Provider"
+ },
+ "link": {
+ "data": {
+ "code": "Pin code"
+ },
+ "description": "To link your Nest account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided pin code below.",
+ "title": "Link Nest Account"
+ }
+ },
+ "title": "Nest"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nest/.translations/ko.json b/homeassistant/components/nest/.translations/ko.json
new file mode 100644
index 00000000000000..0caa70aeff2853
--- /dev/null
+++ b/homeassistant/components/nest/.translations/ko.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "\ud558\ub098\uc758 Nest \uacc4\uc815\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
+ "authorize_url_fail": "\uc778\uc99d url \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.",
+ "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "no_flows": "Nest \ub97c \uc778\uc99d\ud558\uae30 \uc804\uc5d0 Nest \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/nest/)\ub97c \uc77d\uc5b4\ubcf4\uc138\uc694."
+ },
+ "error": {
+ "internal_error": "\ucf54\ub4dc \uc720\ud6a8\uc131 \uac80\uc0ac\uc5d0 \ub0b4\ubd80 \uc624\ub958 \ubc1c\uc0dd",
+ "invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc",
+ "timeout": "\ucf54\ub4dc \uc720\ud6a8\uc131 \uac80\uc0ac \uc2dc\uac04 \ucd08\uacfc",
+ "unknown": "\ucf54\ub4dc \uc720\ud6a8\uc131 \uac80\uc0ac\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958 \ubc1c\uc0dd"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "flow_impl": "\uacf5\uae09\uc790"
+ },
+ "description": "Nest\ub85c \uc778\uc99d\ud558\ub824\ub294 \uc778\uc99d \uacf5\uae09\uc790\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.",
+ "title": "\uc778\uc99d \uacf5\uae09\uc790"
+ },
+ "link": {
+ "data": {
+ "code": "\ud540 \ucf54\ub4dc"
+ },
+ "description": "Nest \uacc4\uc815\uc744 \uc5f0\uacb0\ud558\ub824\uba74, [\uacc4\uc815 \uc5f0\uacb0 \uc2b9\uc778]({url})\uc744 \ud574\uc8fc\uc138\uc694.\n\n\uc2b9\uc778 \ud6c4, \uc544\ub798\uc758 \ud540 \ucf54\ub4dc\ub97c \ubcf5\uc0ac\ud558\uc5ec \ubd99\uc5ec\ub123\uc73c\uc138\uc694.",
+ "title": "Nest \uacc4\uc815 \uc5f0\uacb0"
+ }
+ },
+ "title": "Nest"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nest/.translations/no.json b/homeassistant/components/nest/.translations/no.json
new file mode 100644
index 00000000000000..03cf1a82b813bf
--- /dev/null
+++ b/homeassistant/components/nest/.translations/no.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Du kan bare konfigurere en enkelt Nest konto.",
+ "authorize_url_fail": "Ukjent feil ved generering av autoriseringsadresse.",
+ "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.",
+ "no_flows": "Du m\u00e5 konfigurere Nest f\u00f8r du kan autentisere med den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/nest/)."
+ },
+ "error": {
+ "internal_error": "Intern feil ved validering av kode",
+ "invalid_code": "Ugyldig kode",
+ "timeout": "Tidsavbrudd ved validering av kode",
+ "unknown": "Ukjent feil ved validering av kode"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "flow_impl": "Tilbyder"
+ },
+ "description": "Velg via hvilken autentiseringstilbyder du vil godkjenne med Nest.",
+ "title": "Autentiseringstilbyder"
+ },
+ "link": {
+ "data": {
+ "code": "PIN kode"
+ },
+ "description": "For \u00e5 koble din Nest-konto, [autoriser kontoen din]({url}). \n\n Etter godkjenning, kopier og lim inn den oppgitte PIN koden nedenfor.",
+ "title": "Koble til Nest konto"
+ }
+ },
+ "title": "Nest"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nest/.translations/pl.json b/homeassistant/components/nest/.translations/pl.json
new file mode 100644
index 00000000000000..c03b2eff0fabd0
--- /dev/null
+++ b/homeassistant/components/nest/.translations/pl.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Nest.",
+ "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.",
+ "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.",
+ "no_flows": "Musisz skonfigurowa\u0107 Nest, zanim b\u0119dziesz m\u00f3g\u0142 wykona\u0107 uwierzytelnienie. [Przeczytaj instrukcje](https://www.home-assistant.io/components/nest/)."
+ },
+ "error": {
+ "internal_error": "Wewn\u0119trzny b\u0142\u0105d sprawdzania poprawno\u015bci kodu",
+ "invalid_code": "Nieprawid\u0142owy kod",
+ "timeout": "Min\u0105\u0142 limit czasu sprawdzania poprawno\u015bci kodu",
+ "unknown": "Nieznany b\u0142\u0105d sprawdzania poprawno\u015bci kodu"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "flow_impl": "Dostawca"
+ },
+ "description": "Wybierz, kt\u00f3rego dostawc\u0119 uwierzytelnienia chcesz u\u017cywa\u0107 z Nest.",
+ "title": "Dostawca uwierzytelnienia"
+ },
+ "link": {
+ "data": {
+ "code": "Kod PIN"
+ },
+ "description": "Aby po\u0142\u0105czy\u0107 z kontem Nest, [wykonaj autoryzacj\u0119]({url}). \n\n Po autoryzacji skopiuj i wklej podany kod PIN poni\u017cej.",
+ "title": "Po\u0142\u0105cz z kontem Nest"
+ }
+ },
+ "title": "Nest"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nest/.translations/ru.json b/homeassistant/components/nest/.translations/ru.json
new file mode 100644
index 00000000000000..0f7b9b8dd719c2
--- /dev/null
+++ b/homeassistant/components/nest/.translations/ru.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Nest.",
+ "authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.",
+ "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.",
+ "no_flows": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Nest \u043f\u0435\u0440\u0435\u0434 \u0442\u0435\u043c, \u043a\u0430\u043a \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e. [\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/nest/)."
+ },
+ "error": {
+ "internal_error": "\u0412\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u044f\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430",
+ "invalid_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434",
+ "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430.",
+ "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "flow_impl": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440"
+ },
+ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435, \u0447\u0435\u0440\u0435\u0437 \u043a\u0430\u043a\u043e\u0433\u043e \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0432\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0432\u0445\u043e\u0434 \u0432 Nest.",
+ "title": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438"
+ },
+ "link": {
+ "data": {
+ "code": "\u041f\u0438\u043d-\u043a\u043e\u0434"
+ },
+ "description": " [\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c]({url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0441\u0432\u043e\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Nest. \n\n \u041f\u043e\u0441\u043b\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u0430\u0433\u0430\u0435\u043c\u044b\u0439 \u043f\u0438\u043d-\u043a\u043e\u0434.",
+ "title": "\u041f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Nest"
+ }
+ },
+ "title": "Nest"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nest/.translations/sv.json b/homeassistant/components/nest/.translations/sv.json
new file mode 100644
index 00000000000000..721f891219daa5
--- /dev/null
+++ b/homeassistant/components/nest/.translations/sv.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "Du kan endast konfigurera ett Nest-konto.",
+ "authorize_url_fail": "Ok\u00e4nt fel vid generering av autentisieringsadress.",
+ "authorize_url_timeout": "Timeout vid generering av en autentisieringsadress.",
+ "no_flows": "Du m\u00e5ste konfigurera Nest innan du kan autentisera med det. [V\u00e4nligen l\u00e4s instruktionerna] (https://www.home-assistant.io/components/nest/)."
+ },
+ "error": {
+ "internal_error": "Internt fel vid validering av kod",
+ "invalid_code": "Ogiltig kod",
+ "timeout": "Timeout vid valididering av kod",
+ "unknown": "Ok\u00e4nt fel vid validering av kod"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "flow_impl": "Leverant\u00f6r"
+ },
+ "description": "V\u00e4lj den autentiseringsleverant\u00f6r som du vill autentisera med mot Nest.",
+ "title": "Autentiseringsleverant\u00f6r"
+ },
+ "link": {
+ "data": {
+ "code": "Pin-kod"
+ },
+ "description": "F\u00f6r att l\u00e4nka ditt Nest-konto, [autentisiera ditt konto]({url}). \n\nEfter autentisiering, klipp och klistra in den angivna pin-koden nedan.",
+ "title": "L\u00e4nka Nest-konto"
+ }
+ },
+ "title": "Nest"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nest/.translations/vi.json b/homeassistant/components/nest/.translations/vi.json
new file mode 100644
index 00000000000000..996c6c68eae9e3
--- /dev/null
+++ b/homeassistant/components/nest/.translations/vi.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "error": {
+ "internal_error": "M\u00e3 x\u00e1c th\u1ef1c l\u1ed7i n\u1ed9i b\u1ed9",
+ "invalid_code": "M\u00e3 kh\u00f4ng h\u1ee3p l\u1ec7",
+ "timeout": "M\u00e3 x\u00e1c th\u1ef1c h\u1ebft th\u1eddi gian ch\u1edd",
+ "unknown": "M\u00e3 x\u00e1c th\u1ef1c l\u1ed7i kh\u00f4ng x\u00e1c \u0111\u1ecbnh"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "flow_impl": "Nh\u00e0 cung c\u1ea5p"
+ },
+ "title": "Nh\u00e0 cung c\u1ea5p x\u00e1c th\u1ef1c"
+ },
+ "link": {
+ "title": "Li\u00ean k\u1ebft t\u00e0i kho\u1ea3n Nest"
+ }
+ },
+ "title": "Nest"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nest/.translations/zh-Hans.json b/homeassistant/components/nest/.translations/zh-Hans.json
new file mode 100644
index 00000000000000..05ba5bdf15525a
--- /dev/null
+++ b/homeassistant/components/nest/.translations/zh-Hans.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "abort": {
+ "already_setup": "\u60a8\u53ea\u80fd\u914d\u7f6e\u4e00\u4e2a Nest \u5e10\u6237\u3002",
+ "authorize_url_fail": "\u751f\u6210\u6388\u6743\u7f51\u5740\u65f6\u53d1\u751f\u672a\u77e5\u9519\u8bef\u3002",
+ "authorize_url_timeout": "\u751f\u6210\u6388\u6743\u7f51\u5740\u8d85\u65f6\u3002",
+ "no_flows": "\u60a8\u9700\u8981\u5148\u914d\u7f6e Nest\uff0c\u7136\u540e\u624d\u80fd\u5bf9\u5176\u8fdb\u884c\u6388\u6743\u3002 [\u8bf7\u9605\u8bfb\u8bf4\u660e](https://www.home-assistant.io/components/nest/)\u3002"
+ },
+ "error": {
+ "internal_error": "\u9a8c\u8bc1\u4ee3\u7801\u65f6\u53d1\u751f\u5185\u90e8\u9519\u8bef",
+ "invalid_code": "\u65e0\u6548\u4ee3\u7801",
+ "timeout": "\u4ee3\u7801\u9a8c\u8bc1\u8d85\u65f6",
+ "unknown": "\u9a8c\u8bc1\u4ee3\u7801\u65f6\u53d1\u751f\u672a\u77e5\u9519\u8bef"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "flow_impl": "\u63d0\u4f9b\u8005"
+ },
+ "description": "\u9009\u62e9\u60a8\u60f3\u901a\u8fc7\u54ea\u4e2a\u6388\u6743\u63d0\u4f9b\u8005\u4e0e Nest \u8fdb\u884c\u6388\u6743\u3002",
+ "title": "\u6388\u6743\u63d0\u4f9b\u8005"
+ },
+ "link": {
+ "data": {
+ "code": "PIN \u7801"
+ },
+ "description": "\u8981\u5173\u8054 Nest \u5e10\u6237\uff0c\u8bf7[\u6388\u6743\u5e10\u6237]({url})\u3002\n\n\u5b8c\u6210\u6388\u6743\u540e\uff0c\u5728\u4e0b\u9762\u7c98\u8d34\u83b7\u5f97\u7684 PIN \u7801\u3002",
+ "title": "\u5173\u8054 Nest \u5e10\u6237"
+ }
+ },
+ "title": "Nest"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest/__init__.py
similarity index 68%
rename from homeassistant/components/nest.py
rename to homeassistant/components/nest/__init__.py
index 16a0b80d1fd33d..bd74897371ad05 100644
--- a/homeassistant/components/nest.py
+++ b/homeassistant/components/nest/__init__.py
@@ -7,6 +7,7 @@
from concurrent.futures import ThreadPoolExecutor
import logging
import socket
+from datetime import datetime, timedelta
import voluptuous as vol
@@ -14,19 +15,22 @@
CONF_STRUCTURE, CONF_FILENAME, CONF_BINARY_SENSORS, CONF_SENSORS,
CONF_MONITORED_CONDITIONS,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
-from homeassistant.helpers import discovery, config_validation as cv
+from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send, \
async_dispatcher_connect
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['python-nest==4.0.1']
+from .const import DOMAIN
+from . import local_auth
+
+REQUIREMENTS = ['python-nest==4.0.2']
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
-DOMAIN = 'nest'
DATA_NEST = 'nest'
+DATA_NEST_CONFIG = 'nest_config'
SIGNAL_NEST_UPDATE = 'nest_update'
@@ -36,14 +40,23 @@
ATTR_HOME_MODE = 'home_mode'
ATTR_STRUCTURE = 'structure'
+ATTR_TRIP_ID = 'trip_id'
+ATTR_ETA = 'eta'
+ATTR_ETA_WINDOW = 'eta_window'
+
+HOME_MODE_AWAY = 'away'
+HOME_MODE_HOME = 'home'
SENSOR_SCHEMA = vol.Schema({
vol.Optional(CONF_MONITORED_CONDITIONS): vol.All(cv.ensure_list)
})
AWAY_SCHEMA = vol.Schema({
- vol.Required(ATTR_HOME_MODE): cv.string,
- vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, cv.string)
+ vol.Required(ATTR_HOME_MODE): vol.In([HOME_MODE_AWAY, HOME_MODE_HOME]),
+ vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, cv.string),
+ vol.Optional(ATTR_TRIP_ID): cv.string,
+ vol.Optional(ATTR_ETA): cv.time_period,
+ vol.Optional(ATTR_ETA_WINDOW): cv.time_period
})
CONFIG_SCHEMA = vol.Schema({
@@ -76,79 +89,51 @@ async def async_nest_update_event_broker(hass, nest):
return
-async def async_request_configuration(nest, hass, config):
- """Request configuration steps from the user."""
- configurator = hass.components.configurator
- if 'nest' in _CONFIGURING:
- _LOGGER.debug("configurator failed")
- configurator.async_notify_errors(
- _CONFIGURING['nest'], "Failed to configure, please try again.")
+async def async_setup(hass, config):
+ """Set up Nest components."""
+ if DOMAIN not in config:
return
- async def async_nest_config_callback(data):
- """Run when the configuration callback is called."""
- _LOGGER.debug("configurator callback")
- pin = data.get('pin')
- if await async_setup_nest(hass, nest, config, pin=pin):
- # start nest update event listener as we missed startup hook
- hass.async_add_job(async_nest_update_event_broker, hass, nest)
-
- _CONFIGURING['nest'] = configurator.async_request_config(
- "Nest", async_nest_config_callback,
- description=('To configure Nest, click Request Authorization below, '
- 'log into your Nest account, '
- 'and then enter the resulting PIN'),
- link_name='Request Authorization',
- link_url=nest.authorize_url,
- submit_caption="Confirm",
- fields=[{'id': 'pin', 'name': 'Enter the PIN', 'type': ''}]
- )
-
-
-async def async_setup_nest(hass, nest, config, pin=None):
- """Set up the Nest devices."""
- from nest.nest import AuthorizationError, APIError
- if pin is not None:
- _LOGGER.debug("pin acquired, requesting access token")
- error_message = None
- try:
- nest.request_token(pin)
- except AuthorizationError as auth_error:
- error_message = "Nest authorization failed: {}".format(auth_error)
- except APIError as api_error:
- error_message = "Failed to call Nest API: {}".format(api_error)
-
- if error_message is not None:
- _LOGGER.warning(error_message)
- hass.components.configurator.async_notify_errors(
- _CONFIGURING['nest'], error_message)
- return False
-
- if nest.access_token is None:
- _LOGGER.debug("no access_token, requesting configuration")
- await async_request_configuration(nest, hass, config)
- return False
+ conf = config[DOMAIN]
+
+ local_auth.initialize(hass, conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET])
- if 'nest' in _CONFIGURING:
- _LOGGER.debug("configuration done")
- configurator = hass.components.configurator
- configurator.async_request_done(_CONFIGURING.pop('nest'))
+ filename = config.get(CONF_FILENAME, NEST_CONFIG_FILE)
+ access_token_cache_file = hass.config.path(filename)
+
+ hass.async_add_job(hass.config_entries.flow.async_init(
+ DOMAIN, source='import', data={
+ 'nest_conf_path': access_token_cache_file,
+ }
+ ))
+
+ # Store config to be used during entry setup
+ hass.data[DATA_NEST_CONFIG] = conf
+
+ return True
+
+
+async def async_setup_entry(hass, entry):
+ """Setup Nest from a config entry."""
+ from nest import Nest
+
+ nest = Nest(access_token=entry.data['tokens']['access_token'])
_LOGGER.debug("proceeding with setup")
- conf = config[DOMAIN]
+ conf = hass.data.get(DATA_NEST_CONFIG, {})
hass.data[DATA_NEST] = NestDevice(hass, conf, nest)
+ await hass.async_add_job(hass.data[DATA_NEST].initialize)
- for component, discovered in [
- ('climate', {}),
- ('camera', {}),
- ('sensor', conf.get(CONF_SENSORS, {})),
- ('binary_sensor', conf.get(CONF_BINARY_SENSORS, {}))]:
- _LOGGER.debug("proceeding with discovery -- %s", component)
- hass.async_add_job(discovery.async_load_platform,
- hass, component, DOMAIN, discovered, config)
+ for component in 'climate', 'camera', 'sensor', 'binary_sensor':
+ hass.async_add_job(hass.config_entries.async_forward_entry_setup(
+ entry, component))
def set_mode(service):
- """Set the home/away mode for a Nest structure."""
+ """
+ Set the home/away mode for a Nest structure.
+
+ You can set optional eta information when set mode to away.
+ """
if ATTR_STRUCTURE in service.data:
structures = service.data[ATTR_STRUCTURE]
else:
@@ -158,6 +143,19 @@ def set_mode(service):
if structure.name in structures:
_LOGGER.info("Setting mode for %s", structure.name)
structure.away = service.data[ATTR_HOME_MODE]
+
+ if service.data[ATTR_HOME_MODE] == HOME_MODE_AWAY \
+ and ATTR_ETA in service.data:
+ now = datetime.utcnow()
+ eta_begin = now + service.data[ATTR_ETA]
+ eta_window = service.data.get(ATTR_ETA_WINDOW,
+ timedelta(minutes=1))
+ eta_end = eta_begin + eta_window
+ trip_id = service.data.get(
+ ATTR_TRIP_ID, "trip_{}".format(int(now.timestamp())))
+ _LOGGER.info("Setting eta for %s, eta window starts at "
+ "%s ends at %s", trip_id, eta_begin, eta_end)
+ structure.set_eta(trip_id, eta_begin, eta_end)
else:
_LOGGER.error("Invalid structure %s",
service.data[ATTR_STRUCTURE])
@@ -183,29 +181,6 @@ def shut_down(event):
return True
-async def async_setup(hass, config):
- """Set up Nest components."""
- from nest import Nest
-
- if 'nest' in _CONFIGURING:
- return
-
- conf = config[DOMAIN]
- client_id = conf[CONF_CLIENT_ID]
- client_secret = conf[CONF_CLIENT_SECRET]
- filename = config.get(CONF_FILENAME, NEST_CONFIG_FILE)
-
- access_token_cache_file = hass.config.path(filename)
-
- nest = Nest(
- access_token_cache_file=access_token_cache_file,
- client_id=client_id, client_secret=client_secret)
-
- await async_setup_nest(hass, nest, config)
-
- return True
-
-
class NestDevice(object):
"""Structure Nest functions for hass."""
@@ -213,12 +188,12 @@ def __init__(self, hass, conf, nest):
"""Init Nest Devices."""
self.hass = hass
self.nest = nest
+ self.local_structure = conf.get(CONF_STRUCTURE)
- if CONF_STRUCTURE not in conf:
- self.local_structure = [s.name for s in nest.structures]
- else:
- self.local_structure = conf[CONF_STRUCTURE]
- _LOGGER.debug("Structures to include: %s", self.local_structure)
+ def initialize(self):
+ """Initialize Nest."""
+ if self.local_structure is None:
+ self.local_structure = [s.name for s in self.nest.structures]
def structures(self):
"""Generate a list of structures."""
diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py
new file mode 100644
index 00000000000000..b5c095f34b8065
--- /dev/null
+++ b/homeassistant/components/nest/config_flow.py
@@ -0,0 +1,160 @@
+"""Config flow to configure Nest."""
+import asyncio
+from collections import OrderedDict
+import logging
+import os
+
+import async_timeout
+import voluptuous as vol
+
+from homeassistant import config_entries, data_entry_flow
+from homeassistant.core import callback
+from homeassistant.exceptions import HomeAssistantError
+from homeassistant.util.json import load_json
+
+from .const import DOMAIN
+
+
+DATA_FLOW_IMPL = 'nest_flow_implementation'
+_LOGGER = logging.getLogger(__name__)
+
+
+@callback
+def register_flow_implementation(hass, domain, name, gen_authorize_url,
+ convert_code):
+ """Register a flow implementation.
+
+ domain: Domain of the component responsible for the implementation.
+ name: Name of the component.
+ gen_authorize_url: Coroutine function to generate the authorize url.
+ convert_code: Coroutine function to convert a code to an access token.
+ """
+ if DATA_FLOW_IMPL not in hass.data:
+ hass.data[DATA_FLOW_IMPL] = OrderedDict()
+
+ hass.data[DATA_FLOW_IMPL][domain] = {
+ 'domain': domain,
+ 'name': name,
+ 'gen_authorize_url': gen_authorize_url,
+ 'convert_code': convert_code,
+ }
+
+
+class NestAuthError(HomeAssistantError):
+ """Base class for Nest auth errors."""
+
+
+class CodeInvalid(NestAuthError):
+ """Raised when invalid authorization code."""
+
+
+@config_entries.HANDLERS.register(DOMAIN)
+class NestFlowHandler(data_entry_flow.FlowHandler):
+ """Handle a Nest config flow."""
+
+ VERSION = 1
+
+ def __init__(self):
+ """Initialize the Nest config flow."""
+ self.flow_impl = None
+
+ async def async_step_init(self, user_input=None):
+ """Handle a flow start."""
+ flows = self.hass.data.get(DATA_FLOW_IMPL, {})
+
+ if self.hass.config_entries.async_entries(DOMAIN):
+ return self.async_abort(reason='already_setup')
+
+ elif not flows:
+ return self.async_abort(reason='no_flows')
+
+ elif len(flows) == 1:
+ self.flow_impl = list(flows)[0]
+ return await self.async_step_link()
+
+ elif user_input is not None:
+ self.flow_impl = user_input['flow_impl']
+ return await self.async_step_link()
+
+ return self.async_show_form(
+ step_id='init',
+ data_schema=vol.Schema({
+ vol.Required('flow_impl'): vol.In(list(flows))
+ })
+ )
+
+ async def async_step_link(self, user_input=None):
+ """Attempt to link with the Nest account.
+
+ Route the user to a website to authenticate with Nest. Depending on
+ implementation type we expect a pin or an external component to
+ deliver the authentication code.
+ """
+ flow = self.hass.data[DATA_FLOW_IMPL][self.flow_impl]
+
+ errors = {}
+
+ if user_input is not None:
+ try:
+ with async_timeout.timeout(10):
+ tokens = await flow['convert_code'](user_input['code'])
+ return self._entry_from_tokens(
+ 'Nest (via {})'.format(flow['name']), flow, tokens)
+
+ except asyncio.TimeoutError:
+ errors['code'] = 'timeout'
+ except CodeInvalid:
+ errors['code'] = 'invalid_code'
+ except NestAuthError:
+ errors['code'] = 'unknown'
+ except Exception: # pylint: disable=broad-except
+ errors['code'] = 'internal_error'
+ _LOGGER.exception("Unexpected error resolving code")
+
+ try:
+ with async_timeout.timeout(10):
+ url = await flow['gen_authorize_url'](self.flow_id)
+ except asyncio.TimeoutError:
+ return self.async_abort(reason='authorize_url_timeout')
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception("Unexpected error generating auth url")
+ return self.async_abort(reason='authorize_url_fail')
+
+ return self.async_show_form(
+ step_id='link',
+ description_placeholders={
+ 'url': url
+ },
+ data_schema=vol.Schema({
+ vol.Required('code'): str,
+ }),
+ errors=errors,
+ )
+
+ async def async_step_import(self, info):
+ """Import existing auth from Nest."""
+ if self.hass.config_entries.async_entries(DOMAIN):
+ return self.async_abort(reason='already_setup')
+
+ config_path = info['nest_conf_path']
+
+ if not await self.hass.async_add_job(os.path.isfile, config_path):
+ self.flow_impl = DOMAIN
+ return await self.async_step_link()
+
+ flow = self.hass.data[DATA_FLOW_IMPL][DOMAIN]
+ tokens = await self.hass.async_add_job(load_json, config_path)
+
+ return self._entry_from_tokens(
+ 'Nest (import from configuration.yaml)', flow, tokens)
+
+ @callback
+ def _entry_from_tokens(self, title, flow, tokens):
+ """Create an entry from tokens."""
+ return self.async_create_entry(
+ title=title,
+ data={
+ 'tokens': tokens,
+ 'impl_domain': flow['domain'],
+ },
+ )
diff --git a/homeassistant/components/nest/const.py b/homeassistant/components/nest/const.py
new file mode 100644
index 00000000000000..835918f6a048fd
--- /dev/null
+++ b/homeassistant/components/nest/const.py
@@ -0,0 +1,2 @@
+"""Constants used by the Nest component."""
+DOMAIN = 'nest'
diff --git a/homeassistant/components/nest/local_auth.py b/homeassistant/components/nest/local_auth.py
new file mode 100644
index 00000000000000..5ab10cc2a5e25c
--- /dev/null
+++ b/homeassistant/components/nest/local_auth.py
@@ -0,0 +1,45 @@
+"""Local Nest authentication."""
+import asyncio
+from functools import partial
+
+from homeassistant.core import callback
+from . import config_flow
+from .const import DOMAIN
+
+
+@callback
+def initialize(hass, client_id, client_secret):
+ """Initialize a local auth provider."""
+ config_flow.register_flow_implementation(
+ hass, DOMAIN, 'local', partial(generate_auth_url, client_id),
+ partial(resolve_auth_code, hass, client_id, client_secret)
+ )
+
+
+async def generate_auth_url(client_id, flow_id):
+ """Generate an authorize url."""
+ from nest.nest import AUTHORIZE_URL
+ return AUTHORIZE_URL.format(client_id, flow_id)
+
+
+async def resolve_auth_code(hass, client_id, client_secret, code):
+ """Resolve an authorization code."""
+ from nest.nest import NestAuth, AuthorizationError
+
+ result = asyncio.Future()
+ auth = NestAuth(
+ client_id=client_id,
+ client_secret=client_secret,
+ auth_callback=result.set_result,
+ )
+ auth.pin = code
+
+ try:
+ await hass.async_add_job(auth.login)
+ return await result
+ except AuthorizationError as err:
+ if err.response.status_code == 401:
+ raise config_flow.CodeInvalid()
+ else:
+ raise config_flow.NestAuthError('Unknown error: {} ({})'.format(
+ err, err.response.status_code))
diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json
new file mode 100644
index 00000000000000..5a70e3fd48d6c4
--- /dev/null
+++ b/homeassistant/components/nest/strings.json
@@ -0,0 +1,33 @@
+{
+ "config": {
+ "title": "Nest",
+ "step": {
+ "init": {
+ "title": "Authentication Provider",
+ "description": "Pick via which authentication provider you want to authenticate with Nest.",
+ "data": {
+ "flow_impl": "Provider"
+ }
+ },
+ "link": {
+ "title": "Link Nest Account",
+ "description": "To link your Nest account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided pin code below.",
+ "data": {
+ "code": "Pin code"
+ }
+ }
+ },
+ "error": {
+ "timeout": "Timeout validating code",
+ "invalid_code": "Invalid code",
+ "unknown": "Unknown error validating code",
+ "internal_error": "Internal error validating code"
+ },
+ "abort": {
+ "already_setup": "You can only configure a single Nest account.",
+ "no_flows": "You need to configure Nest before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/nest/).",
+ "authorize_url_timeout": "Timeout generating authorize url.",
+ "authorize_url_fail": "Unknown error generating an authorize url."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/netatmo.py b/homeassistant/components/netatmo.py
index 44a54c9551261f..a635d1820db083 100644
--- a/homeassistant/components/netatmo.py
+++ b/homeassistant/components/netatmo.py
@@ -16,9 +16,7 @@
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
-REQUIREMENTS = [
- 'https://github.com/jabesq/netatmo-api-python/archive/'
- 'v0.9.2.1.zip#lnetatmo==0.9.2.1']
+REQUIREMENTS = ['pyatmo==1.0.0']
_LOGGER = logging.getLogger(__name__)
@@ -45,11 +43,11 @@
def setup(hass, config):
"""Set up the Netatmo devices."""
- import lnetatmo
+ import pyatmo
global NETATMO_AUTH
try:
- NETATMO_AUTH = lnetatmo.ClientAuth(
+ NETATMO_AUTH = pyatmo.ClientAuth(
config[DOMAIN][CONF_API_KEY], config[DOMAIN][CONF_SECRET_KEY],
config[DOMAIN][CONF_USERNAME], config[DOMAIN][CONF_PASSWORD],
'read_station read_camera access_camera '
@@ -111,8 +109,8 @@ def get_camera_type(self, camera=None, home=None, cid=None):
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Call the Netatmo API to update the data."""
- import lnetatmo
- self.camera_data = lnetatmo.CameraData(self.auth, size=100)
+ import pyatmo
+ self.camera_data = pyatmo.CameraData(self.auth, size=100)
@Throttle(MIN_TIME_BETWEEN_EVENT_UPDATES)
def update_event(self):
diff --git a/homeassistant/components/netgear_lte.py b/homeassistant/components/netgear_lte.py
new file mode 100644
index 00000000000000..4887ea1aa67596
--- /dev/null
+++ b/homeassistant/components/netgear_lte.py
@@ -0,0 +1,86 @@
+"""
+Support for Netgear LTE modems.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/netgear_lte/
+"""
+import asyncio
+from datetime import timedelta
+
+import voluptuous as vol
+import attr
+
+from homeassistant.const import CONF_HOST, CONF_PASSWORD
+import homeassistant.helpers.config_validation as cv
+from homeassistant.util import Throttle
+
+REQUIREMENTS = ['eternalegypt==0.0.1']
+
+MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10)
+
+DOMAIN = 'netgear_lte'
+DATA_KEY = 'netgear_lte'
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
+ vol.Required(CONF_HOST): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ })])
+}, extra=vol.ALLOW_EXTRA)
+
+
+@attr.s
+class LTEData:
+ """Class for LTE state."""
+
+ eternalegypt = attr.ib()
+ unread_count = attr.ib(init=False)
+ usage = attr.ib(init=False)
+
+ @Throttle(MIN_TIME_BETWEEN_UPDATES)
+ async def async_update(self):
+ """Call the API to update the data."""
+ information = await self.eternalegypt.information()
+ self.unread_count = sum(1 for x in information.sms if x.unread)
+ self.usage = information.usage
+
+
+@attr.s
+class LTEHostData:
+ """Container for LTE states."""
+
+ hostdata = attr.ib(init=False, factory=dict)
+
+ def get(self, config):
+ """Get the requested or the only hostdata value."""
+ if CONF_HOST in config:
+ return self.hostdata.get(config[CONF_HOST])
+ elif len(self.hostdata) == 1:
+ return next(iter(self.hostdata.values()))
+
+ return None
+
+
+async def async_setup(hass, config):
+ """Set up Netgear LTE component."""
+ if DATA_KEY not in hass.data:
+ hass.data[DATA_KEY] = LTEHostData()
+
+ tasks = [_setup_lte(hass, conf) for conf in config.get(DOMAIN, [])]
+ if tasks:
+ await asyncio.wait(tasks)
+
+ return True
+
+
+async def _setup_lte(hass, lte_config):
+ """Set up a Netgear LTE modem."""
+ import eternalegypt
+
+ host = lte_config[CONF_HOST]
+ password = lte_config[CONF_PASSWORD]
+
+ eternalegypt = eternalegypt.LB2120(host, password)
+ lte_data = LTEData(eternalegypt)
+ await lte_data.async_update()
+ hass.data[DATA_KEY].hostdata[host] = lte_data
diff --git a/homeassistant/components/notify/mastodon.py b/homeassistant/components/notify/mastodon.py
index 3ba95407fec15c..e29289722e89b8 100644
--- a/homeassistant/components/notify/mastodon.py
+++ b/homeassistant/components/notify/mastodon.py
@@ -13,7 +13,7 @@
from homeassistant.const import CONF_ACCESS_TOKEN
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['Mastodon.py==1.2.2']
+REQUIREMENTS = ['Mastodon.py==1.3.0']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/notify/message_bird.py b/homeassistant/components/notify/message_bird.py
index b20abb52efc5c3..fa747ccba88dee 100644
--- a/homeassistant/components/notify/message_bird.py
+++ b/homeassistant/components/notify/message_bird.py
@@ -24,7 +24,6 @@
})
-# pylint: disable=unused-argument
def get_service(hass, config, discovery_info=None):
"""Get the MessageBird notification service."""
import messagebird
diff --git a/homeassistant/components/notify/mysensors.py b/homeassistant/components/notify/mysensors.py
index 1374779c5f0417..db568514dea25a 100644
--- a/homeassistant/components/notify/mysensors.py
+++ b/homeassistant/components/notify/mysensors.py
@@ -36,8 +36,6 @@ def __repr__(self):
class MySensorsNotificationService(BaseNotificationService):
"""Implement a MySensors notification service."""
- # pylint: disable=too-few-public-methods
-
def __init__(self, hass):
"""Initialize the service."""
self.devices = mysensors.get_mysensors_devices(hass, DOMAIN)
diff --git a/homeassistant/components/notify/netgear_lte.py b/homeassistant/components/notify/netgear_lte.py
new file mode 100644
index 00000000000000..b4ed53b828d6b3
--- /dev/null
+++ b/homeassistant/components/notify/netgear_lte.py
@@ -0,0 +1,45 @@
+"""Netgear LTE platform for notify component.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/notify.netgear_lte/
+"""
+
+import voluptuous as vol
+import attr
+
+from homeassistant.components.notify import (
+ BaseNotificationService, ATTR_TARGET, PLATFORM_SCHEMA)
+from homeassistant.const import CONF_HOST
+import homeassistant.helpers.config_validation as cv
+
+from ..netgear_lte import DATA_KEY
+
+
+DEPENDENCIES = ['netgear_lte']
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Optional(CONF_HOST): cv.string,
+ vol.Required(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]),
+})
+
+
+async def async_get_service(hass, config, discovery_info=None):
+ """Get the notification service."""
+ lte_data = hass.data[DATA_KEY].get(config)
+ phone = config.get(ATTR_TARGET)
+ return NetgearNotifyService(lte_data, phone)
+
+
+@attr.s
+class NetgearNotifyService(BaseNotificationService):
+ """Implementation of a notification service."""
+
+ lte_data = attr.ib()
+ phone = attr.ib()
+
+ async def async_send_message(self, message="", **kwargs):
+ """Send a message to a user."""
+ targets = kwargs.get(ATTR_TARGET, self.phone)
+ if targets and message:
+ for target in targets:
+ await self.lte_data.eternalegypt.sms(target, message)
diff --git a/homeassistant/components/notify/nfandroidtv.py b/homeassistant/components/notify/nfandroidtv.py
index 1fa8f1dab78b63..044a037cc2978b 100644
--- a/homeassistant/components/notify/nfandroidtv.py
+++ b/homeassistant/components/notify/nfandroidtv.py
@@ -86,7 +86,6 @@
})
-# pylint: disable=unused-argument
def get_service(hass, config, discovery_info=None):
"""Get the Notifications for Android TV notification service."""
remoteip = config.get(CONF_IP)
diff --git a/homeassistant/components/notify/pushbullet.py b/homeassistant/components/notify/pushbullet.py
index 37edb6709a74d5..a94cf4f105528d 100644
--- a/homeassistant/components/notify/pushbullet.py
+++ b/homeassistant/components/notify/pushbullet.py
@@ -29,7 +29,6 @@
})
-# pylint: disable=unused-argument
def get_service(hass, config, discovery_info=None):
"""Get the Pushbullet notification service."""
from pushbullet import PushBullet
diff --git a/homeassistant/components/notify/sendgrid.py b/homeassistant/components/notify/sendgrid.py
index 89117397a5336a..b73f3a17ee74d9 100644
--- a/homeassistant/components/notify/sendgrid.py
+++ b/homeassistant/components/notify/sendgrid.py
@@ -14,7 +14,7 @@
CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT, CONTENT_TYPE_TEXT_PLAIN)
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['sendgrid==5.3.0']
+REQUIREMENTS = ['sendgrid==5.4.0']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/nuimo_controller.py b/homeassistant/components/nuimo_controller.py
index ffd7a799413b58..25e8a230224833 100644
--- a/homeassistant/components/nuimo_controller.py
+++ b/homeassistant/components/nuimo_controller.py
@@ -97,7 +97,6 @@ def run(self):
self._nuimo.disconnect()
self._nuimo = None
- # pylint: disable=unused-argument
def stop(self, event):
"""Terminate Thread by unsetting flag."""
_LOGGER.debug('Stopping thread for Nuimo %s', self._mac)
diff --git a/homeassistant/components/panel_custom.py b/homeassistant/components/panel_custom.py
index 4659578ae27e9d..0444e7a5b5305c 100644
--- a/homeassistant/components/panel_custom.py
+++ b/homeassistant/components/panel_custom.py
@@ -9,6 +9,7 @@
import voluptuous as vol
+from homeassistant.loader import bind_hass
import homeassistant.helpers.config_validation as cv
DOMAIN = 'panel_custom'
@@ -24,6 +25,9 @@
CONF_EMBED_IFRAME = 'embed_iframe'
CONF_TRUST_EXTERNAL_SCRIPT = 'trust_external_script'
+DEFAULT_EMBED_IFRAME = False
+DEFAULT_TRUST_EXTERNAL = False
+
DEFAULT_ICON = 'mdi:bookmark'
LEGACY_URL = '/api/panel_custom/{}'
@@ -38,33 +42,99 @@
vol.Optional(CONF_CONFIG): dict,
vol.Optional(CONF_WEBCOMPONENT_PATH): cv.isfile,
vol.Optional(CONF_JS_URL): cv.string,
- vol.Optional(CONF_EMBED_IFRAME, default=False): cv.boolean,
- vol.Optional(CONF_TRUST_EXTERNAL_SCRIPT, default=False): cv.boolean,
+ vol.Optional(CONF_EMBED_IFRAME,
+ default=DEFAULT_EMBED_IFRAME): cv.boolean,
+ vol.Optional(CONF_TRUST_EXTERNAL_SCRIPT,
+ default=DEFAULT_TRUST_EXTERNAL): cv.boolean,
})])
}, extra=vol.ALLOW_EXTRA)
_LOGGER = logging.getLogger(__name__)
+@bind_hass
+async def async_register_panel(
+ hass,
+ # The url to serve the panel
+ frontend_url_path,
+ # The webcomponent name that loads your panel
+ webcomponent_name,
+ # Title/icon for sidebar
+ sidebar_title=None,
+ sidebar_icon=None,
+ # HTML source of your panel
+ html_url=None,
+ # JS source of your panel
+ js_url=None,
+ # If your panel should be run inside an iframe
+ embed_iframe=DEFAULT_EMBED_IFRAME,
+ # Should user be asked for confirmation when loading external source
+ trust_external=DEFAULT_TRUST_EXTERNAL,
+ # Configuration to be passed to the panel
+ config=None):
+ """Register a new custom panel."""
+ if js_url is None and html_url is None:
+ raise ValueError('Either js_url or html_url is required.')
+ elif js_url and html_url:
+ raise ValueError('Pass in either JS url or HTML url, not both.')
+
+ if config is not None and not isinstance(config, dict):
+ raise ValueError('Config needs to be a dictionary.')
+
+ custom_panel_config = {
+ 'name': webcomponent_name,
+ 'embed_iframe': embed_iframe,
+ 'trust_external': trust_external,
+ }
+
+ if js_url is not None:
+ custom_panel_config['js_url'] = js_url
+
+ if html_url is not None:
+ custom_panel_config['html_url'] = html_url
+
+ if config is not None:
+ # Make copy because we're mutating it
+ config = dict(config)
+ else:
+ config = {}
+
+ config['_panel_custom'] = custom_panel_config
+
+ await hass.components.frontend.async_register_built_in_panel(
+ component_name='custom',
+ sidebar_title=sidebar_title,
+ sidebar_icon=sidebar_icon,
+ frontend_url_path=frontend_url_path,
+ config=config
+ )
+
+
async def async_setup(hass, config):
"""Initialize custom panel."""
success = False
for panel in config.get(DOMAIN):
- name = panel.get(CONF_COMPONENT_NAME)
+ name = panel[CONF_COMPONENT_NAME]
+
+ kwargs = {
+ 'webcomponent_name': panel[CONF_COMPONENT_NAME],
+ 'frontend_url_path': panel.get(CONF_URL_PATH, name),
+ 'sidebar_title': panel.get(CONF_SIDEBAR_TITLE),
+ 'sidebar_icon': panel.get(CONF_SIDEBAR_ICON),
+ 'config': panel.get(CONF_CONFIG),
+ 'trust_external': panel[CONF_TRUST_EXTERNAL_SCRIPT],
+ 'embed_iframe': panel[CONF_EMBED_IFRAME],
+ }
+
panel_path = panel.get(CONF_WEBCOMPONENT_PATH)
if panel_path is None:
- panel_path = hass.config.path(PANEL_DIR, '{}.html'.format(name))
-
- custom_panel_config = {
- 'name': name,
- 'embed_iframe': panel[CONF_EMBED_IFRAME],
- 'trust_external': panel[CONF_TRUST_EXTERNAL_SCRIPT],
- }
+ panel_path = hass.config.path(
+ PANEL_DIR, '{}.html'.format(name))
if CONF_JS_URL in panel:
- custom_panel_config['js_url'] = panel[CONF_JS_URL]
+ kwargs['js_url'] = panel[CONF_JS_URL]
elif not await hass.async_add_job(os.path.isfile, panel_path):
_LOGGER.error('Unable to find webcomponent for %s: %s',
@@ -74,23 +144,9 @@ async def async_setup(hass, config):
else:
url = LEGACY_URL.format(name)
hass.http.register_static_path(url, panel_path)
- custom_panel_config['html_url'] = LEGACY_URL.format(name)
-
- if CONF_CONFIG in panel:
- # Make copy because we're mutating it
- config = dict(panel[CONF_CONFIG])
- else:
- config = {}
-
- config['_panel_custom'] = custom_panel_config
+ kwargs['html_url'] = url
- await hass.components.frontend.async_register_built_in_panel(
- component_name='custom',
- sidebar_title=panel.get(CONF_SIDEBAR_TITLE),
- sidebar_icon=panel.get(CONF_SIDEBAR_ICON),
- frontend_url_path=panel.get(CONF_URL_PATH),
- config=config
- )
+ await async_register_panel(hass, **kwargs)
success = True
diff --git a/homeassistant/components/raincloud.py b/homeassistant/components/raincloud.py
index 308a945e942e26..a04f4926b76ebb 100644
--- a/homeassistant/components/raincloud.py
+++ b/homeassistant/components/raincloud.py
@@ -19,7 +19,7 @@
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import track_time_interval
-REQUIREMENTS = ['raincloudy==0.0.4']
+REQUIREMENTS = ['raincloudy==0.0.5']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py
index 7ee6b06372008f..22fc427ccce969 100644
--- a/homeassistant/components/rainmachine/__init__.py
+++ b/homeassistant/components/rainmachine/__init__.py
@@ -11,14 +11,15 @@
from homeassistant.const import (
ATTR_ATTRIBUTION, CONF_BINARY_SENSORS, CONF_IP_ADDRESS, CONF_PASSWORD,
- CONF_PORT, CONF_SENSORS, CONF_SSL, CONF_MONITORED_CONDITIONS,
- CONF_SWITCHES)
-from homeassistant.helpers import config_validation as cv, discovery
-from homeassistant.helpers.dispatcher import dispatcher_send
+ CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS, CONF_SSL,
+ CONF_MONITORED_CONDITIONS, CONF_SWITCHES)
+from homeassistant.helpers import (
+ aiohttp_client, config_validation as cv, discovery)
+from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import Entity
-from homeassistant.helpers.event import track_time_interval
+from homeassistant.helpers.event import async_track_time_interval
-REQUIREMENTS = ['regenmaschine==0.4.2']
+REQUIREMENTS = ['regenmaschine==1.0.2']
_LOGGER = logging.getLogger(__name__)
@@ -28,8 +29,9 @@
NOTIFICATION_ID = 'rainmachine_notification'
NOTIFICATION_TITLE = 'RainMachine Component Setup'
-DATA_UPDATE_TOPIC = '{0}_data_update'.format(DOMAIN)
PROGRAM_UPDATE_TOPIC = '{0}_program_update'.format(DOMAIN)
+SENSOR_UPDATE_TOPIC = '{0}_data_update'.format(DOMAIN)
+ZONE_UPDATE_TOPIC = '{0}_zone_update'.format(DOMAIN)
CONF_PROGRAM_ID = 'program_id'
CONF_ZONE_ID = 'zone_id'
@@ -105,6 +107,8 @@
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
+ vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL):
+ cv.time_period,
vol.Optional(CONF_BINARY_SENSORS, default={}):
BINARY_SENSOR_SCHEMA,
vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA,
@@ -114,10 +118,10 @@
extra=vol.ALLOW_EXTRA)
-def setup(hass, config):
+async def async_setup(hass, config):
"""Set up the RainMachine component."""
- from regenmaschine import Authenticator, Client
- from regenmaschine.exceptions import RainMachineError
+ from regenmaschine import Client
+ from regenmaschine.errors import RequestError
conf = config[DOMAIN]
ip_address = conf[CONF_IP_ADDRESS]
@@ -126,17 +130,18 @@ def setup(hass, config):
ssl = conf[CONF_SSL]
try:
- auth = Authenticator.create_local(
- ip_address, password, port=port, https=ssl)
- rainmachine = RainMachine(hass, Client(auth))
- rainmachine.update()
+ websession = aiohttp_client.async_get_clientsession(hass)
+ client = Client(ip_address, websession, port=port, ssl=ssl)
+ await client.authenticate(password)
+ rainmachine = RainMachine(client)
+ await rainmachine.async_update()
hass.data[DATA_RAINMACHINE] = rainmachine
- except RainMachineError as exc:
- _LOGGER.error('An error occurred: %s', str(exc))
+ except RequestError as err:
+ _LOGGER.error('An error occurred: %s', str(err))
hass.components.persistent_notification.create(
'Error: {0}
'
'You will need to restart hass after fixing.'
- ''.format(exc),
+ ''.format(err),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
@@ -146,36 +151,43 @@ def setup(hass, config):
('sensor', conf[CONF_SENSORS]),
('switch', conf[CONF_SWITCHES]),
]:
- discovery.load_platform(hass, component, DOMAIN, schema, config)
+ hass.async_add_job(
+ discovery.async_load_platform(hass, component, DOMAIN, schema,
+ config))
- def refresh(event_time):
- """Refresh RainMachine data."""
- _LOGGER.debug('Updating RainMachine data')
- hass.data[DATA_RAINMACHINE].update()
- dispatcher_send(hass, DATA_UPDATE_TOPIC)
+ async def refresh_sensors(event_time):
+ """Refresh RainMachine sensor data."""
+ _LOGGER.debug('Updating RainMachine sensor data')
+ await rainmachine.async_update()
+ async_dispatcher_send(hass, SENSOR_UPDATE_TOPIC)
- track_time_interval(hass, refresh, DEFAULT_SCAN_INTERVAL)
+ async_track_time_interval(hass, refresh_sensors, conf[CONF_SCAN_INTERVAL])
- def start_program(service):
+ async def start_program(service):
"""Start a particular program."""
- rainmachine.client.programs.start(service.data[CONF_PROGRAM_ID])
+ await rainmachine.client.programs.start(service.data[CONF_PROGRAM_ID])
+ async_dispatcher_send(hass, PROGRAM_UPDATE_TOPIC)
- def start_zone(service):
+ async def start_zone(service):
"""Start a particular zone for a certain amount of time."""
- rainmachine.client.zones.start(service.data[CONF_ZONE_ID],
- service.data[CONF_ZONE_RUN_TIME])
+ await rainmachine.client.zones.start(service.data[CONF_ZONE_ID],
+ service.data[CONF_ZONE_RUN_TIME])
+ async_dispatcher_send(hass, ZONE_UPDATE_TOPIC)
- def stop_all(service):
+ async def stop_all(service):
"""Stop all watering."""
- rainmachine.client.watering.stop_all()
+ await rainmachine.client.watering.stop_all()
+ async_dispatcher_send(hass, PROGRAM_UPDATE_TOPIC)
- def stop_program(service):
+ async def stop_program(service):
"""Stop a program."""
- rainmachine.client.programs.stop(service.data[CONF_PROGRAM_ID])
+ await rainmachine.client.programs.stop(service.data[CONF_PROGRAM_ID])
+ async_dispatcher_send(hass, PROGRAM_UPDATE_TOPIC)
- def stop_zone(service):
+ async def stop_zone(service):
"""Stop a zone."""
- rainmachine.client.zones.stop(service.data[CONF_ZONE_ID])
+ await rainmachine.client.zones.stop(service.data[CONF_ZONE_ID])
+ async_dispatcher_send(hass, ZONE_UPDATE_TOPIC)
for service, method, schema in [
('start_program', start_program, SERVICE_START_PROGRAM_SCHEMA),
@@ -184,7 +196,7 @@ def stop_zone(service):
('stop_program', stop_program, SERVICE_STOP_PROGRAM_SCHEMA),
('stop_zone', stop_zone, SERVICE_STOP_ZONE_SCHEMA)
]:
- hass.services.register(DOMAIN, service, method, schema=schema)
+ hass.services.async_register(DOMAIN, service, method, schema=schema)
return True
@@ -192,17 +204,17 @@ def stop_zone(service):
class RainMachine(object):
"""Define a generic RainMachine object."""
- def __init__(self, hass, client):
+ def __init__(self, client):
"""Initialize."""
self.client = client
- self.device_mac = self.client.provision.wifi()['macAddress']
+ self.device_mac = self.client.mac
self.restrictions = {}
- def update(self):
+ async def async_update(self):
"""Update sensor/binary sensor data."""
self.restrictions.update({
- 'current': self.client.restrictions.current(),
- 'global': self.client.restrictions.universal()
+ 'current': await self.client.restrictions.current(),
+ 'global': await self.client.restrictions.universal()
})
diff --git a/homeassistant/components/raspihats.py b/homeassistant/components/raspihats.py
index 3bc45eab34ece4..41480c09a32352 100644
--- a/homeassistant/components/raspihats.py
+++ b/homeassistant/components/raspihats.py
@@ -34,7 +34,6 @@
I2C_HATS_MANAGER = 'I2CH_MNG'
-# pylint: disable=unused-argument
def setup(hass, config):
"""Set up the raspihats component."""
hass.data[I2C_HATS_MANAGER] = I2CHatsManager()
diff --git a/homeassistant/components/remote/demo.py b/homeassistant/components/remote/demo.py
index bc67c1646b27b8..d959d74574f3b9 100644
--- a/homeassistant/components/remote/demo.py
+++ b/homeassistant/components/remote/demo.py
@@ -8,7 +8,6 @@
from homeassistant.const import DEVICE_DEFAULT_NAME
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Set up the demo remotes."""
add_devices_callback([
diff --git a/homeassistant/components/remote/itach.py b/homeassistant/components/remote/itach.py
index 8b91e5356b416d..78d277ca65fd3a 100644
--- a/homeassistant/components/remote/itach.py
+++ b/homeassistant/components/remote/itach.py
@@ -44,7 +44,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the ITach connection and devices."""
import pyitachip2ir
diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py
index e731d421e69f8c..8a3e51b55b32b7 100644
--- a/homeassistant/components/remote/xiaomi_miio.py
+++ b/homeassistant/components/remote/xiaomi_miio.py
@@ -22,7 +22,7 @@
import homeassistant.helpers.config_validation as cv
from homeassistant.util.dt import utcnow
-REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
+REQUIREMENTS = ['python-miio==0.4.0', 'construct==2.9.41']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py
index 87e2a7a2331eb0..272a5b868ec8b0 100644
--- a/homeassistant/components/rflink.py
+++ b/homeassistant/components/rflink.py
@@ -20,6 +20,8 @@
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.deprecation import get_deprecated
from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.dispatcher import (
+ async_dispatcher_send, async_dispatcher_connect)
REQUIREMENTS = ['rflink==0.0.37']
@@ -65,6 +67,8 @@
SERVICE_SEND_COMMAND = 'send_command'
+SIGNAL_AVAILABILITY = 'rflink_device_available'
+
DEVICE_DEFAULTS_SCHEMA = vol.Schema({
vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean,
vol.Optional(CONF_SIGNAL_REPETITIONS,
@@ -185,6 +189,8 @@ def reconnect(exc=None):
# Reset protocol binding before starting reconnect
RflinkCommand.set_rflink_protocol(None)
+ async_dispatcher_send(hass, SIGNAL_AVAILABILITY, False)
+
# If HA is not stopping, initiate new connection
if hass.state != CoreState.stopping:
_LOGGER.warning('disconnected from Rflink, reconnecting')
@@ -219,9 +225,16 @@ def connect():
_LOGGER.exception(
"Error connecting to Rflink, reconnecting in %s",
reconnect_interval)
+ # Connection to Rflink device is lost, make entities unavailable
+ async_dispatcher_send(hass, SIGNAL_AVAILABILITY, False)
+
hass.loop.call_later(reconnect_interval, reconnect, exc)
return
+ # There is a valid connection to a Rflink device now so
+ # mark entities as available
+ async_dispatcher_send(hass, SIGNAL_AVAILABILITY, True)
+
# Bind protocol to command class to allow entities to send commands
RflinkCommand.set_rflink_protocol(
protocol, config[DOMAIN][CONF_WAIT_FOR_ACK])
@@ -244,6 +257,7 @@ class RflinkDevice(Entity):
platform = None
_state = STATE_UNKNOWN
+ _available = True
def __init__(self, device_id, hass, name=None, aliases=None, group=True,
group_aliases=None, nogroup_aliases=None, fire_event=False,
@@ -305,6 +319,23 @@ def assumed_state(self):
"""Assume device state until first device event sets state."""
return self._state is STATE_UNKNOWN
+ @property
+ def available(self):
+ """Return True if entity is available."""
+ return self._available
+
+ @callback
+ def set_availability(self, availability):
+ """Update availability state."""
+ self._available = availability
+ self.async_schedule_update_ha_state()
+
+ @asyncio.coroutine
+ def async_added_to_hass(self):
+ """Register update callback."""
+ async_dispatcher_connect(self.hass, SIGNAL_AVAILABILITY,
+ self.set_availability)
+
class RflinkCommand(RflinkDevice):
"""Singleton class to make Rflink command interface available to entities.
diff --git a/homeassistant/components/ring.py b/homeassistant/components/ring.py
index 1a15e22fca08c1..3bfa1372fabae5 100644
--- a/homeassistant/components/ring.py
+++ b/homeassistant/components/ring.py
@@ -12,7 +12,7 @@
import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
-REQUIREMENTS = ['ring_doorbell==0.1.8']
+REQUIREMENTS = ['ring_doorbell==0.2.1']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/scene/lifx_cloud.py b/homeassistant/components/scene/lifx_cloud.py
index ffbb10cba4eca1..6fe91d0acd2e25 100644
--- a/homeassistant/components/scene/lifx_cloud.py
+++ b/homeassistant/components/scene/lifx_cloud.py
@@ -29,7 +29,6 @@
})
-# pylint: disable=unused-argument
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the scenes stored in the LIFX Cloud."""
diff --git a/homeassistant/components/sensor/.translations/season.ca.json b/homeassistant/components/sensor/.translations/season.ca.json
new file mode 100644
index 00000000000000..9bce187ec65d91
--- /dev/null
+++ b/homeassistant/components/sensor/.translations/season.ca.json
@@ -0,0 +1,8 @@
+{
+ "state": {
+ "autumn": "Tardor",
+ "spring": "Primavera",
+ "summer": "Estiu",
+ "winter": "Hivern"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sensor/.translations/season.fr.json b/homeassistant/components/sensor/.translations/season.fr.json
new file mode 100644
index 00000000000000..ec9f9657428917
--- /dev/null
+++ b/homeassistant/components/sensor/.translations/season.fr.json
@@ -0,0 +1,8 @@
+{
+ "state": {
+ "autumn": "Automne",
+ "spring": "Printemps",
+ "summer": "\u00c9t\u00e9",
+ "winter": "Hiver"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sensor/.translations/season.pt-BR.json b/homeassistant/components/sensor/.translations/season.pt-BR.json
new file mode 100644
index 00000000000000..fde45ad6c8efa0
--- /dev/null
+++ b/homeassistant/components/sensor/.translations/season.pt-BR.json
@@ -0,0 +1,8 @@
+{
+ "state": {
+ "autumn": "Outono",
+ "spring": "Primavera",
+ "summer": "Ver\u00e3o",
+ "winter": "Inverno"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sensor/airvisual.py b/homeassistant/components/sensor/airvisual.py
index b4007c8d7440fb..0002274833ff20 100644
--- a/homeassistant/components/sensor/airvisual.py
+++ b/homeassistant/components/sensor/airvisual.py
@@ -9,16 +9,16 @@
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_API_KEY,
- CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, CONF_STATE,
- CONF_SHOW_ON_MAP, CONF_RADIUS)
+ CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS,
+ CONF_SCAN_INTERVAL, CONF_STATE, CONF_SHOW_ON_MAP)
+from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
-REQUIREMENTS = ['pyairvisual==1.0.0']
+REQUIREMENTS = ['pyairvisual==2.0.1']
_LOGGER = getLogger(__name__)
ATTR_CITY = 'city'
@@ -29,135 +29,173 @@
CONF_CITY = 'city'
CONF_COUNTRY = 'country'
-CONF_ATTRIBUTION = "Data provided by AirVisual"
+
+DEFAULT_ATTRIBUTION = "Data provided by AirVisual"
+DEFAULT_SCAN_INTERVAL = timedelta(minutes=10)
MASS_PARTS_PER_MILLION = 'ppm'
MASS_PARTS_PER_BILLION = 'ppb'
VOLUME_MICROGRAMS_PER_CUBIC_METER = 'µg/m3'
-MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
-
-SENSOR_TYPES = [
- ('AirPollutionLevelSensor', 'Air Pollution Level', 'mdi:scale'),
- ('AirQualityIndexSensor', 'Air Quality Index', 'mdi:format-list-numbers'),
- ('MainPollutantSensor', 'Main Pollutant', 'mdi:chemical-weapon'),
+SENSOR_TYPE_LEVEL = 'air_pollution_level'
+SENSOR_TYPE_AQI = 'air_quality_index'
+SENSOR_TYPE_POLLUTANT = 'main_pollutant'
+SENSORS = [
+ (SENSOR_TYPE_LEVEL, 'Air Pollution Level', 'mdi:scale', None),
+ (SENSOR_TYPE_AQI, 'Air Quality Index', 'mdi:format-list-numbers', 'AQI'),
+ (SENSOR_TYPE_POLLUTANT, 'Main Pollutant', 'mdi:chemical-weapon', None),
]
-POLLUTANT_LEVEL_MAPPING = [
- {'label': 'Good', 'minimum': 0, 'maximum': 50},
- {'label': 'Moderate', 'minimum': 51, 'maximum': 100},
- {'label': 'Unhealthy for sensitive group', 'minimum': 101, 'maximum': 150},
- {'label': 'Unhealthy', 'minimum': 151, 'maximum': 200},
- {'label': 'Very Unhealthy', 'minimum': 201, 'maximum': 300},
- {'label': 'Hazardous', 'minimum': 301, 'maximum': 10000}
-]
+POLLUTANT_LEVEL_MAPPING = [{
+ 'label': 'Good',
+ 'minimum': 0,
+ 'maximum': 50
+}, {
+ 'label': 'Moderate',
+ 'minimum': 51,
+ 'maximum': 100
+}, {
+ 'label': 'Unhealthy for sensitive group',
+ 'minimum': 101,
+ 'maximum': 150
+}, {
+ 'label': 'Unhealthy',
+ 'minimum': 151,
+ 'maximum': 200
+}, {
+ 'label': 'Very Unhealthy',
+ 'minimum': 201,
+ 'maximum': 300
+}, {
+ 'label': 'Hazardous',
+ 'minimum': 301,
+ 'maximum': 10000
+}]
POLLUTANT_MAPPING = {
- 'co': {'label': 'Carbon Monoxide', 'unit': MASS_PARTS_PER_MILLION},
- 'n2': {'label': 'Nitrogen Dioxide', 'unit': MASS_PARTS_PER_BILLION},
- 'o3': {'label': 'Ozone', 'unit': MASS_PARTS_PER_BILLION},
- 'p1': {'label': 'PM10', 'unit': VOLUME_MICROGRAMS_PER_CUBIC_METER},
- 'p2': {'label': 'PM2.5', 'unit': VOLUME_MICROGRAMS_PER_CUBIC_METER},
- 's2': {'label': 'Sulfur Dioxide', 'unit': MASS_PARTS_PER_BILLION},
+ 'co': {
+ 'label': 'Carbon Monoxide',
+ 'unit': MASS_PARTS_PER_MILLION
+ },
+ 'n2': {
+ 'label': 'Nitrogen Dioxide',
+ 'unit': MASS_PARTS_PER_BILLION
+ },
+ 'o3': {
+ 'label': 'Ozone',
+ 'unit': MASS_PARTS_PER_BILLION
+ },
+ 'p1': {
+ 'label': 'PM10',
+ 'unit': VOLUME_MICROGRAMS_PER_CUBIC_METER
+ },
+ 'p2': {
+ 'label': 'PM2.5',
+ 'unit': VOLUME_MICROGRAMS_PER_CUBIC_METER
+ },
+ 's2': {
+ 'label': 'Sulfur Dioxide',
+ 'unit': MASS_PARTS_PER_BILLION
+ },
}
SENSOR_LOCALES = {'cn': 'Chinese', 'us': 'U.S.'}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string,
- vol.Required(CONF_MONITORED_CONDITIONS):
+ vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_LOCALES)):
vol.All(cv.ensure_list, [vol.In(SENSOR_LOCALES)]),
- vol.Optional(CONF_CITY): cv.string,
- vol.Optional(CONF_COUNTRY): cv.string,
- vol.Optional(CONF_LATITUDE): cv.latitude,
- vol.Optional(CONF_LONGITUDE): cv.longitude,
- vol.Optional(CONF_RADIUS, default=1000): cv.positive_int,
+ vol.Inclusive(CONF_CITY, 'city'): cv.string,
+ vol.Inclusive(CONF_COUNTRY, 'city'): cv.string,
+ vol.Inclusive(CONF_LATITUDE, 'coords'): cv.latitude,
+ vol.Inclusive(CONF_LONGITUDE, 'coords'): cv.longitude,
vol.Optional(CONF_SHOW_ON_MAP, default=True): cv.boolean,
- vol.Optional(CONF_STATE): cv.string,
+ vol.Inclusive(CONF_STATE, 'city'): cv.string,
+ vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL):
+ cv.time_period
})
-def setup_platform(hass, config, add_devices, discovery_info=None):
+async def async_setup_platform(
+ hass, config, async_add_devices, discovery_info=None):
"""Configure the platform and add the sensors."""
from pyairvisual import Client
- classes = {
- 'AirPollutionLevelSensor': AirPollutionLevelSensor,
- 'AirQualityIndexSensor': AirQualityIndexSensor,
- 'MainPollutantSensor': MainPollutantSensor
- }
-
- api_key = config.get(CONF_API_KEY)
- monitored_locales = config.get(CONF_MONITORED_CONDITIONS)
- latitude = config.get(CONF_LATITUDE, hass.config.latitude)
- longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
- radius = config.get(CONF_RADIUS)
city = config.get(CONF_CITY)
state = config.get(CONF_STATE)
country = config.get(CONF_COUNTRY)
- show_on_map = config.get(CONF_SHOW_ON_MAP)
+
+ latitude = config.get(CONF_LATITUDE, hass.config.latitude)
+ longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
+
+ websession = aiohttp_client.async_get_clientsession(hass)
if city and state and country:
_LOGGER.debug(
"Using city, state, and country: %s, %s, %s", city, state, country)
location_id = ','.join((city, state, country))
data = AirVisualData(
- Client(api_key), city=city, state=state, country=country,
- show_on_map=show_on_map)
+ Client(config[CONF_API_KEY], websession),
+ city=city,
+ state=state,
+ country=country,
+ show_on_map=config[CONF_SHOW_ON_MAP],
+ scan_interval=config[CONF_SCAN_INTERVAL])
else:
_LOGGER.debug(
"Using latitude and longitude: %s, %s", latitude, longitude)
location_id = ','.join((str(latitude), str(longitude)))
data = AirVisualData(
- Client(api_key), latitude=latitude, longitude=longitude,
- radius=radius, show_on_map=show_on_map)
+ Client(config[CONF_API_KEY], websession),
+ latitude=latitude,
+ longitude=longitude,
+ show_on_map=config[CONF_SHOW_ON_MAP],
+ scan_interval=config[CONF_SCAN_INTERVAL])
- data.update()
+ await data.async_update()
sensors = []
- for locale in monitored_locales:
- for sensor_class, name, icon in SENSOR_TYPES:
- sensors.append(classes[sensor_class](
- data,
- name,
- icon,
- locale,
- location_id
- ))
-
- add_devices(sensors, True)
-
-
-class AirVisualBaseSensor(Entity):
- """Define a base class for all of our sensors."""
-
- def __init__(self, data, name, icon, locale, entity_id):
- """Initialize the sensor."""
- self.data = data
- self._attrs = {}
+ for locale in config[CONF_MONITORED_CONDITIONS]:
+ for kind, name, icon, unit in SENSORS:
+ sensors.append(
+ AirVisualSensor(
+ data, kind, name, icon, unit, locale, location_id))
+
+ async_add_devices(sensors, True)
+
+
+class AirVisualSensor(Entity):
+ """Define an AirVisual sensor."""
+
+ def __init__(self, airvisual, kind, name, icon, unit, locale, location_id):
+ """Initialize."""
+ self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
self._icon = icon
self._locale = locale
+ self._location_id = location_id
self._name = name
self._state = None
- self._entity_id = entity_id
- self._unit = None
+ self._type = kind
+ self._unit = unit
+ self.airvisual = airvisual
@property
def device_state_attributes(self):
"""Return the device state attributes."""
- self._attrs.update({
- ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
- })
-
- if self.data.show_on_map:
- self._attrs[ATTR_LATITUDE] = self.data.latitude
- self._attrs[ATTR_LONGITUDE] = self.data.longitude
+ if self.airvisual.show_on_map:
+ self._attrs[ATTR_LATITUDE] = self.airvisual.latitude
+ self._attrs[ATTR_LONGITUDE] = self.airvisual.longitude
else:
- self._attrs['lati'] = self.data.latitude
- self._attrs['long'] = self.data.longitude
+ self._attrs['lati'] = self.airvisual.latitude
+ self._attrs['long'] = self.airvisual.longitude
return self._attrs
+ @property
+ def available(self):
+ """Return True if entity is available."""
+ return bool(self.airvisual.pollution_info)
+
@property
def icon(self):
"""Return the icon."""
@@ -173,127 +211,83 @@ def state(self):
"""Return the state."""
return self._state
-
-class AirPollutionLevelSensor(AirVisualBaseSensor):
- """Define a sensor to measure air pollution level."""
-
@property
def unique_id(self):
"""Return a unique, HASS-friendly identifier for this entity."""
- return '{0}_pollution_level'.format(self._entity_id)
-
- def update(self):
- """Update the status of the sensor."""
- self.data.update()
-
- aqi = self.data.pollution_info.get('aqi{0}'.format(self._locale))
- try:
- [level] = [
- i for i in POLLUTANT_LEVEL_MAPPING
- if i['minimum'] <= aqi <= i['maximum']
- ]
- self._state = level.get('label')
- except TypeError:
- self._state = None
- except ValueError:
- self._state = None
-
-
-class AirQualityIndexSensor(AirVisualBaseSensor):
- """Define a sensor to measure AQI."""
-
- @property
- def unique_id(self):
- """Return a unique, HASS-friendly identifier for this entity."""
- return '{0}_aqi'.format(self._entity_id)
+ return '{0}_{1}_{2}'.format(
+ self._location_id, self._locale, self._type)
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
- return 'AQI'
-
- def update(self):
- """Update the status of the sensor."""
- self.data.update()
-
- self._state = self.data.pollution_info.get(
- 'aqi{0}'.format(self._locale))
-
+ return self._unit
-class MainPollutantSensor(AirVisualBaseSensor):
- """Define a sensor to the main pollutant of an area."""
+ async def async_update(self):
+ """Update the sensor."""
+ await self.airvisual.async_update()
+ data = self.airvisual.pollution_info
- def __init__(self, data, name, icon, locale, entity_id):
- """Initialize the sensor."""
- super().__init__(data, name, icon, locale, entity_id)
- self._symbol = None
- self._unit = None
+ if not data:
+ return
- @property
- def unique_id(self):
- """Return a unique, HASS-friendly identifier for this entity."""
- return '{0}_main_pollutant'.format(self._entity_id)
-
- def update(self):
- """Update the status of the sensor."""
- self.data.update()
-
- symbol = self.data.pollution_info.get('main{0}'.format(self._locale))
- pollution_info = POLLUTANT_MAPPING.get(symbol, {})
- self._state = pollution_info.get('label')
- self._unit = pollution_info.get('unit')
- self._symbol = symbol
-
- self._attrs.update({
- ATTR_POLLUTANT_SYMBOL: self._symbol,
- ATTR_POLLUTANT_UNIT: self._unit
- })
+ if self._type == SENSOR_TYPE_LEVEL:
+ aqi = data['aqi{0}'.format(self._locale)]
+ [level] = [
+ i for i in POLLUTANT_LEVEL_MAPPING
+ if i['minimum'] <= aqi <= i['maximum']
+ ]
+ self._state = level['label']
+ elif self._type == SENSOR_TYPE_AQI:
+ self._state = data['aqi{0}'.format(self._locale)]
+ elif self._type == SENSOR_TYPE_POLLUTANT:
+ symbol = data['main{0}'.format(self._locale)]
+ self._state = POLLUTANT_MAPPING[symbol]['label']
+ self._attrs.update({
+ ATTR_POLLUTANT_SYMBOL: symbol,
+ ATTR_POLLUTANT_UNIT: POLLUTANT_MAPPING[symbol]['unit']
+ })
class AirVisualData(object):
"""Define an object to hold sensor data."""
def __init__(self, client, **kwargs):
- """Initialize the AirVisual data element."""
+ """Initialize."""
self._client = client
- self.attrs = {}
- self.pollution_info = None
-
self.city = kwargs.get(CONF_CITY)
- self.state = kwargs.get(CONF_STATE)
self.country = kwargs.get(CONF_COUNTRY)
-
self.latitude = kwargs.get(CONF_LATITUDE)
self.longitude = kwargs.get(CONF_LONGITUDE)
- self._radius = kwargs.get(CONF_RADIUS)
-
+ self.pollution_info = {}
self.show_on_map = kwargs.get(CONF_SHOW_ON_MAP)
+ self.state = kwargs.get(CONF_STATE)
+
+ self.async_update = Throttle(
+ kwargs[CONF_SCAN_INTERVAL])(self._async_update)
- @Throttle(MIN_TIME_BETWEEN_UPDATES)
- def update(self):
- """Update with new AirVisual data."""
- from pyairvisual.exceptions import HTTPError
+ async def _async_update(self):
+ """Update AirVisual data."""
+ from pyairvisual.errors import AirVisualError
try:
if self.city and self.state and self.country:
- resp = self._client.city(
- self.city, self.state, self.country).get('data')
- self.longitude, self.latitude = resp.get('location').get(
- 'coordinates')
+ resp = await self._client.data.city(
+ self.city, self.state, self.country)
+ self.longitude, self.latitude = resp['location']['coordinates']
else:
- resp = self._client.nearest_city(
- self.latitude, self.longitude, self._radius).get('data')
+ resp = await self._client.data.nearest_city(
+ self.latitude, self.longitude)
+
_LOGGER.debug("New data retrieved: %s", resp)
- self.pollution_info = resp.get('current', {}).get('pollution', {})
-
- self.attrs = {
- ATTR_CITY: resp.get('city'),
- ATTR_REGION: resp.get('state'),
- ATTR_COUNTRY: resp.get('country')
- }
- except HTTPError as exc_info:
- _LOGGER.error("Unable to retrieve data on this location: %s",
- self.__dict__)
- _LOGGER.debug(exc_info)
+ self.pollution_info = resp['current']['pollution']
+ except AirVisualError as err:
+ if self.city and self.state and self.country:
+ location = (self.city, self.state, self.country)
+ else:
+ location = (self.latitude, self.longitude)
+
+ _LOGGER.error(
+ "Can't retrieve data for location: %s (%s)", location,
+ err)
self.pollution_info = {}
diff --git a/homeassistant/components/sensor/arlo.py b/homeassistant/components/sensor/arlo.py
index 97b7ac2290940d..18029691dc7bae 100644
--- a/homeassistant/components/sensor/arlo.py
+++ b/homeassistant/components/sensor/arlo.py
@@ -4,17 +4,17 @@
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.arlo/
"""
-import asyncio
import logging
-from datetime import timedelta
import voluptuous as vol
+from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from homeassistant.components.arlo import (
- CONF_ATTRIBUTION, DEFAULT_BRAND, DATA_ARLO)
+ CONF_ATTRIBUTION, DEFAULT_BRAND, DATA_ARLO, SIGNAL_UPDATE_ARLO)
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS)
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.icon import icon_for_battery_level
@@ -22,8 +22,6 @@
DEPENDENCIES = ['arlo']
-SCAN_INTERVAL = timedelta(seconds=90)
-
# sensor_type [ description, unit, icon ]
SENSOR_TYPES = {
'last_capture': ['Last', None, 'run-fast'],
@@ -39,8 +37,7 @@
})
-@asyncio.coroutine
-def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
+def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up an Arlo IP sensor."""
arlo = hass.data.get(DATA_ARLO)
if not arlo:
@@ -50,24 +47,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
if sensor_type == 'total_cameras':
sensors.append(ArloSensor(
- hass, SENSOR_TYPES[sensor_type][0], arlo, sensor_type))
+ SENSOR_TYPES[sensor_type][0], arlo, sensor_type))
else:
for camera in arlo.cameras:
name = '{0} {1}'.format(
SENSOR_TYPES[sensor_type][0], camera.name)
- sensors.append(ArloSensor(hass, name, camera, sensor_type))
+ sensors.append(ArloSensor(name, camera, sensor_type))
- async_add_devices(sensors, True)
+ add_devices(sensors, True)
class ArloSensor(Entity):
"""An implementation of a Netgear Arlo IP sensor."""
- def __init__(self, hass, name, device, sensor_type):
+ def __init__(self, name, device, sensor_type):
"""Initialize an Arlo sensor."""
- super().__init__()
self._name = name
- self._hass = hass
self._data = device
self._sensor_type = sensor_type
self._state = None
@@ -78,6 +73,16 @@ def name(self):
"""Return the name of this camera."""
return self._name
+ async def async_added_to_hass(self):
+ """Register callbacks."""
+ async_dispatcher_connect(
+ self.hass, SIGNAL_UPDATE_ARLO, self._update_callback)
+
+ @callback
+ def _update_callback(self):
+ """Call update method."""
+ self.async_schedule_update_ha_state(True)
+
@property
def state(self):
"""Return the state of the sensor."""
@@ -98,18 +103,7 @@ def unit_of_measurement(self):
def update(self):
"""Get the latest data and updates the state."""
- try:
- base_station = self._data.base_station
- except (AttributeError, IndexError):
- return
-
- if not base_station:
- return
-
- base_station.refresh_rate = SCAN_INTERVAL.total_seconds()
-
- self._data.update()
-
+ _LOGGER.debug("Updating Arlo sensor %s", self.name)
if self._sensor_type == 'total_cameras':
self._state = len(self._data.cameras)
@@ -118,9 +112,13 @@ def update(self):
elif self._sensor_type == 'last_capture':
try:
- video = self._data.videos()[0]
+ video = self._data.last_video
self._state = video.created_at_pretty("%m-%d-%Y %H:%M:%S")
except (AttributeError, IndexError):
+ error_msg = \
+ 'Video not found for {0}. Older than {1} days?'.format(
+ self.name, self._data.min_days_vdo_cache)
+ _LOGGER.debug(error_msg)
self._state = None
elif self._sensor_type == 'battery_level':
diff --git a/homeassistant/components/sensor/bloomsky.py b/homeassistant/components/sensor/bloomsky.py
index b460498c901e44..d33796d04ccc77 100644
--- a/homeassistant/components/sensor/bloomsky.py
+++ b/homeassistant/components/sensor/bloomsky.py
@@ -41,7 +41,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the available BloomSky weather sensors."""
bloomsky = hass.components.bloomsky
diff --git a/homeassistant/components/sensor/broadlink.py b/homeassistant/components/sensor/broadlink.py
index 9376687cf131fa..8806fae5974f77 100644
--- a/homeassistant/components/sensor/broadlink.py
+++ b/homeassistant/components/sensor/broadlink.py
@@ -47,7 +47,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Broadlink device sensors."""
host = config.get(CONF_HOST)
diff --git a/homeassistant/components/sensor/citybikes.py b/homeassistant/components/sensor/citybikes.py
index a8bc441b722fab..24f8ea7e6a94f6 100644
--- a/homeassistant/components/sensor/citybikes.py
+++ b/homeassistant/components/sensor/citybikes.py
@@ -125,7 +125,6 @@ def async_citybikes_request(hass, uri, schema):
raise CityBikesRequestError
-# pylint: disable=unused-argument
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
diff --git a/homeassistant/components/sensor/command_line.py b/homeassistant/components/sensor/command_line.py
index f326a57b137fd2..4a26a1dc9fc771 100644
--- a/homeassistant/components/sensor/command_line.py
+++ b/homeassistant/components/sensor/command_line.py
@@ -35,7 +35,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Command Sensor."""
name = config.get(CONF_NAME)
diff --git a/homeassistant/components/sensor/crimereports.py b/homeassistant/components/sensor/crimereports.py
index a2d7315a314cc5..adf7e3c0fa9719 100644
--- a/homeassistant/components/sensor/crimereports.py
+++ b/homeassistant/components/sensor/crimereports.py
@@ -41,7 +41,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Crime Reports platform."""
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
diff --git a/homeassistant/components/sensor/deluge.py b/homeassistant/components/sensor/deluge.py
index 8acbda74d7d461..b9109f6428c1df 100644
--- a/homeassistant/components/sensor/deluge.py
+++ b/homeassistant/components/sensor/deluge.py
@@ -42,7 +42,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Deluge sensors."""
from deluge_client import DelugeRPCClient
diff --git a/homeassistant/components/sensor/demo.py b/homeassistant/components/sensor/demo.py
index 325d3e0ae58050..15cc0ec46aebe4 100644
--- a/homeassistant/components/sensor/demo.py
+++ b/homeassistant/components/sensor/demo.py
@@ -10,7 +10,6 @@
from homeassistant.helpers.entity import Entity
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Demo sensors."""
add_devices([
diff --git a/homeassistant/components/sensor/eddystone_temperature.py b/homeassistant/components/sensor/eddystone_temperature.py
index 2c8ad4781d003d..978b8db669ac77 100644
--- a/homeassistant/components/sensor/eddystone_temperature.py
+++ b/homeassistant/components/sensor/eddystone_temperature.py
@@ -39,7 +39,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Validate configuration, create devices and start monitoring thread."""
bt_device_id = config.get("bt_device_id")
diff --git a/homeassistant/components/sensor/eight_sleep.py b/homeassistant/components/sensor/eight_sleep.py
index e0a42fdb6a8dea..fd7c1aee3ae67f 100644
--- a/homeassistant/components/sensor/eight_sleep.py
+++ b/homeassistant/components/sensor/eight_sleep.py
@@ -5,7 +5,6 @@
https://home-assistant.io/components/sensor.eight_sleep/
"""
import logging
-import asyncio
from homeassistant.components.eight_sleep import (
DATA_EIGHT, EightSleepHeatEntity, EightSleepUserEntity,
@@ -24,20 +23,20 @@
ATTR_SLEEP_DUR = 'Time Slept'
ATTR_LIGHT_PERC = 'Light Sleep %'
ATTR_DEEP_PERC = 'Deep Sleep %'
+ATTR_REM_PERC = 'REM Sleep %'
ATTR_TNT = 'Tosses & Turns'
ATTR_SLEEP_STAGE = 'Sleep Stage'
ATTR_TARGET_HEAT = 'Target Heating Level'
ATTR_ACTIVE_HEAT = 'Heating Active'
ATTR_DURATION_HEAT = 'Heating Time Remaining'
-ATTR_LAST_SEEN = 'Last In Bed'
ATTR_PROCESSING = 'Processing'
ATTR_SESSION_START = 'Session Start'
_LOGGER = logging.getLogger(__name__)
-@asyncio.coroutine
-def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
+async def async_setup_platform(hass, config, async_add_devices,
+ discovery_info=None):
"""Set up the eight sleep sensors."""
if discovery_info is None:
return
@@ -98,8 +97,7 @@ def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
return '%'
- @asyncio.coroutine
- def async_update(self):
+ async def async_update(self):
"""Retrieve latest state."""
_LOGGER.debug("Updating Heat sensor: %s", self._sensor)
self._state = self._usrobj.heating_level
@@ -110,7 +108,6 @@ def device_state_attributes(self):
state_attr = {ATTR_TARGET_HEAT: self._usrobj.target_heating_level}
state_attr[ATTR_ACTIVE_HEAT] = self._usrobj.now_heating
state_attr[ATTR_DURATION_HEAT] = self._usrobj.heating_remaining
- state_attr[ATTR_LAST_SEEN] = self._usrobj.last_seen
return state_attr
@@ -164,8 +161,7 @@ def icon(self):
if 'bed_temp' in self._sensor:
return 'mdi:thermometer'
- @asyncio.coroutine
- def async_update(self):
+ async def async_update(self):
"""Retrieve latest state."""
_LOGGER.debug("Updating User sensor: %s", self._sensor)
if 'current' in self._sensor:
@@ -176,10 +172,13 @@ def async_update(self):
self._attr = self._usrobj.last_values
elif 'bed_temp' in self._sensor:
temp = self._usrobj.current_values['bed_temp']
- if self._units == 'si':
- self._state = round(temp, 2)
- else:
- self._state = round((temp*1.8)+32, 2)
+ try:
+ if self._units == 'si':
+ self._state = round(temp, 2)
+ else:
+ self._state = round((temp*1.8)+32, 2)
+ except TypeError:
+ self._state = None
elif 'sleep_stage' in self._sensor:
self._state = self._usrobj.current_values['stage']
@@ -208,12 +207,27 @@ def device_state_attributes(self):
except ZeroDivisionError:
state_attr[ATTR_DEEP_PERC] = 0
- if self._units == 'si':
- room_temp = round(self._attr['room_temp'], 2)
- bed_temp = round(self._attr['bed_temp'], 2)
- else:
- room_temp = round((self._attr['room_temp']*1.8)+32, 2)
- bed_temp = round((self._attr['bed_temp']*1.8)+32, 2)
+ try:
+ state_attr[ATTR_REM_PERC] = round((
+ self._attr['breakdown']['rem'] / sleep_time) * 100, 2)
+ except ZeroDivisionError:
+ state_attr[ATTR_REM_PERC] = 0
+
+ try:
+ if self._units == 'si':
+ room_temp = round(self._attr['room_temp'], 2)
+ else:
+ room_temp = round((self._attr['room_temp']*1.8)+32, 2)
+ except TypeError:
+ room_temp = None
+
+ try:
+ if self._units == 'si':
+ bed_temp = round(self._attr['bed_temp'], 2)
+ else:
+ bed_temp = round((self._attr['bed_temp']*1.8)+32, 2)
+ except TypeError:
+ bed_temp = None
if 'current' in self._sensor_root:
state_attr[ATTR_RESP_RATE] = round(self._attr['resp_rate'], 2)
@@ -255,15 +269,17 @@ def state(self):
"""Return the state of the sensor."""
return self._state
- @asyncio.coroutine
- def async_update(self):
+ async def async_update(self):
"""Retrieve latest state."""
_LOGGER.debug("Updating Room sensor: %s", self._sensor)
temp = self._eight.room_temperature()
- if self._units == 'si':
- self._state = round(temp, 2)
- else:
- self._state = round((temp*1.8)+32, 2)
+ try:
+ if self._units == 'si':
+ self._state = round(temp, 2)
+ else:
+ self._state = round((temp*1.8)+32, 2)
+ except TypeError:
+ self._state = None
@property
def unit_of_measurement(self):
diff --git a/homeassistant/components/sensor/fedex.py b/homeassistant/components/sensor/fedex.py
index f86de1d865c964..991588f07f326c 100644
--- a/homeassistant/components/sensor/fedex.py
+++ b/homeassistant/components/sensor/fedex.py
@@ -41,7 +41,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Fedex platform."""
import fedexdeliverymanager
diff --git a/homeassistant/components/sensor/fints.py b/homeassistant/components/sensor/fints.py
index 798f74bb6548a8..1312991913939a 100644
--- a/homeassistant/components/sensor/fints.py
+++ b/homeassistant/components/sensor/fints.py
@@ -50,7 +50,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the sensors.
diff --git a/homeassistant/components/sensor/fitbit.py b/homeassistant/components/sensor/fitbit.py
index 8d64a8d8229d0b..f312d1f22cc1e1 100644
--- a/homeassistant/components/sensor/fitbit.py
+++ b/homeassistant/components/sensor/fitbit.py
@@ -156,7 +156,6 @@ def request_app_setup(hass, config, add_devices, config_path,
"""Assist user with configuring the Fitbit dev application."""
configurator = hass.components.configurator
- # pylint: disable=unused-argument
def fitbit_configuration_callback(callback_data):
"""Handle configuration updates."""
config_path = hass.config.path(FITBIT_CONFIG_FILE)
@@ -202,7 +201,6 @@ def request_oauth_completion(hass):
return
- # pylint: disable=unused-argument
def fitbit_configuration_callback(callback_data):
"""Handle configuration updates."""
diff --git a/homeassistant/components/sensor/gearbest.py b/homeassistant/components/sensor/gearbest.py
index aa1d2d9eff049e..d71419ba79e69e 100644
--- a/homeassistant/components/sensor/gearbest.py
+++ b/homeassistant/components/sensor/gearbest.py
@@ -16,7 +16,7 @@
from homeassistant.helpers.event import track_time_interval
from homeassistant.const import (CONF_NAME, CONF_ID, CONF_URL, CONF_CURRENCY)
-REQUIREMENTS = ['gearbest_parser==1.0.5']
+REQUIREMENTS = ['gearbest_parser==1.0.7']
_LOGGER = logging.getLogger(__name__)
CONF_ITEMS = 'items'
diff --git a/homeassistant/components/sensor/glances.py b/homeassistant/components/sensor/glances.py
index 0de87bd17eac84..4fed3793c50c00 100644
--- a/homeassistant/components/sensor/glances.py
+++ b/homeassistant/components/sensor/glances.py
@@ -155,9 +155,9 @@ def update(self):
self._state = value['processcount']['sleeping']
elif self.type == 'cpu_temp':
for sensor in value['sensors']:
- if sensor['label'] == 'CPU':
+ if sensor['label'] in ['CPU', "Package id 0",
+ "Physical id 0"]:
self._state = sensor['value']
- self._state = None
elif self.type == 'docker_active':
count = 0
for container in value['docker']['containers']:
diff --git a/homeassistant/components/sensor/haveibeenpwned.py b/homeassistant/components/sensor/haveibeenpwned.py
index 3b041127a5b18f..c1fe7ab4880fab 100644
--- a/homeassistant/components/sensor/haveibeenpwned.py
+++ b/homeassistant/components/sensor/haveibeenpwned.py
@@ -35,7 +35,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the HaveIBeenPwned sensor."""
emails = config.get(CONF_EMAIL)
diff --git a/homeassistant/components/sensor/hive.py b/homeassistant/components/sensor/hive.py
index 82816c83404717..8c9409ef5ffc7b 100644
--- a/homeassistant/components/sensor/hive.py
+++ b/homeassistant/components/sensor/hive.py
@@ -10,7 +10,7 @@
DEPENDENCIES = ['hive']
-FRIENDLY_NAMES = {'Hub_OnlineStatus': 'Hub Status',
+FRIENDLY_NAMES = {'Hub_OnlineStatus': 'Hive Hub Status',
'Hive_OutsideTemperature': 'Outside Temperature'}
DEVICETYPE_ICONS = {'Hub_OnlineStatus': 'mdi:switch',
'Hive_OutsideTemperature': 'mdi:thermometer'}
diff --git a/homeassistant/components/sensor/homematic.py b/homeassistant/components/sensor/homematic.py
index bdbc207a79ca9f..60741a9f3c8475 100644
--- a/homeassistant/components/sensor/homematic.py
+++ b/homeassistant/components/sensor/homematic.py
@@ -16,6 +16,9 @@
'RotaryHandleSensor': {0: 'closed',
1: 'tilted',
2: 'open'},
+ 'RotaryHandleSensorIP': {0: 'closed',
+ 1: 'tilted',
+ 2: 'open'},
'WaterSensor': {0: 'dry',
1: 'wet',
2: 'water'},
diff --git a/homeassistant/components/sensor/hp_ilo.py b/homeassistant/components/sensor/hp_ilo.py
index 922ed04a8d9e2a..acd10fe08afb3a 100644
--- a/homeassistant/components/sensor/hp_ilo.py
+++ b/homeassistant/components/sensor/hp_ilo.py
@@ -59,7 +59,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the HP ILO sensor."""
hostname = config.get(CONF_HOST)
diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py
index ecf7bc0b8c2665..ca8c19bbc7a193 100644
--- a/homeassistant/components/sensor/isy994.py
+++ b/homeassistant/components/sensor/isy994.py
@@ -235,7 +235,6 @@
}
-# pylint: disable=unused-argument
def setup_platform(hass, config: ConfigType,
add_devices: Callable[[list], None], discovery_info=None):
"""Set up the ISY994 sensor platform."""
diff --git a/homeassistant/components/sensor/kira.py b/homeassistant/components/sensor/kira.py
index b5d3073ea9a06f..74a1bd19d34428 100644
--- a/homeassistant/components/sensor/kira.py
+++ b/homeassistant/components/sensor/kira.py
@@ -18,7 +18,7 @@
CONF_SENSOR = 'sensor'
-# pylint: disable=unused-argument, too-many-function-args
+# pylint: disable=too-many-function-args
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up a Kira sensor."""
if discovery_info is not None:
diff --git a/homeassistant/components/sensor/knx.py b/homeassistant/components/sensor/knx.py
index 8eeb75fb0f17d6..925b16cb4c7d20 100644
--- a/homeassistant/components/sensor/knx.py
+++ b/homeassistant/components/sensor/knx.py
@@ -73,7 +73,6 @@ def async_register_callbacks(self):
"""Register callbacks to update hass after device was changed."""
async def after_update_callback(device):
"""Call after device was updated."""
- # pylint: disable=unused-argument
await self.async_update_ha_state()
self.device.register_device_updated_cb(after_update_callback)
diff --git a/homeassistant/components/sensor/lastfm.py b/homeassistant/components/sensor/lastfm.py
index 9fec4b4b5e3651..ee9ab146c87fcb 100644
--- a/homeassistant/components/sensor/lastfm.py
+++ b/homeassistant/components/sensor/lastfm.py
@@ -13,7 +13,7 @@
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['pylast==2.2.0']
+REQUIREMENTS = ['pylast==2.3.0']
ATTR_LAST_PLAYED = 'last_played'
ATTR_PLAY_COUNT = 'play_count'
@@ -29,7 +29,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Last.fm platform."""
import pylast as lastfm
diff --git a/homeassistant/components/sensor/mold_indicator.py b/homeassistant/components/sensor/mold_indicator.py
index 057718400c4b59..2822ce01dca456 100644
--- a/homeassistant/components/sensor/mold_indicator.py
+++ b/homeassistant/components/sensor/mold_indicator.py
@@ -41,7 +41,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up MoldIndicator sensor."""
name = config.get(CONF_NAME, DEFAULT_NAME)
diff --git a/homeassistant/components/sensor/moon.py b/homeassistant/components/sensor/moon.py
index 75b8a1f72bd9bb..0c57c98c0af3ab 100644
--- a/homeassistant/components/sensor/moon.py
+++ b/homeassistant/components/sensor/moon.py
@@ -4,7 +4,6 @@
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.moon/
"""
-import asyncio
import logging
import voluptuous as vol
@@ -26,8 +25,8 @@
})
-@asyncio.coroutine
-def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
+async def async_setup_platform(
+ hass, config, async_add_devices, discovery_info=None):
"""Set up the Moon sensor."""
name = config.get(CONF_NAME)
@@ -71,8 +70,7 @@ def icon(self):
"""Icon to use in the frontend, if any."""
return ICON
- @asyncio.coroutine
- def async_update(self):
+ async def async_update(self):
"""Get the time and updates the states."""
from astral import Astral
diff --git a/homeassistant/components/sensor/mopar.py b/homeassistant/components/sensor/mopar.py
index 99ea4ef6135ad5..3e1887cfd598b5 100644
--- a/homeassistant/components/sensor/mopar.py
+++ b/homeassistant/components/sensor/mopar.py
@@ -41,7 +41,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Mopar platform."""
import motorparts
diff --git a/homeassistant/components/sensor/mvglive.py b/homeassistant/components/sensor/mvglive.py
index 46d79c1121ba84..81c7173e4d0386 100644
--- a/homeassistant/components/sensor/mvglive.py
+++ b/homeassistant/components/sensor/mvglive.py
@@ -72,7 +72,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(sensors, True)
-# pylint: disable=too-few-public-methods
class MVGLiveSensor(Entity):
"""Implementation of an MVG Live sensor."""
diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py
index 00d18c7fe105d2..bf1b3f65c4a9fc 100644
--- a/homeassistant/components/sensor/nest.py
+++ b/homeassistant/components/sensor/nest.py
@@ -6,7 +6,8 @@
"""
import logging
-from homeassistant.components.nest import DATA_NEST, NestSensorDevice
+from homeassistant.components.nest import (
+ DATA_NEST, DATA_NEST_CONFIG, CONF_SENSORS, NestSensorDevice)
from homeassistant.const import (
TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_MONITORED_CONDITIONS,
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY)
@@ -17,9 +18,13 @@
TEMP_SENSOR_TYPES = ['temperature', 'target']
-PROTECT_SENSOR_TYPES = ['co_status', 'smoke_status', 'battery_health']
+PROTECT_SENSOR_TYPES = ['co_status',
+ 'smoke_status',
+ 'battery_health',
+ # color_status: "gray", "green", "yellow", "red"
+ 'color_status']
-STRUCTURE_SENSOR_TYPES = ['eta']
+STRUCTURE_SENSOR_TYPES = ['eta', 'security_state']
_VALID_SENSOR_TYPES = SENSOR_TYPES + TEMP_SENSOR_TYPES + PROTECT_SENSOR_TYPES \
+ STRUCTURE_SENSOR_TYPES
@@ -47,12 +52,18 @@
def setup_platform(hass, config, add_devices, discovery_info=None):
- """Set up the Nest Sensor."""
- if discovery_info is None:
- return
+ """Set up the Nest Sensor.
+ No longer used.
+ """
+
+
+async def async_setup_entry(hass, entry, async_add_devices):
+ """Set up a Nest sensor based on a config entry."""
nest = hass.data[DATA_NEST]
+ discovery_info = hass.data.get(DATA_NEST_CONFIG, {}).get(CONF_SENSORS, {})
+
# Add all available sensors if no Nest sensor config is set
if discovery_info == {}:
conditions = _VALID_SENSOR_TYPES
@@ -73,26 +84,30 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"binary_sensor.nest/ for valid options.")
_LOGGER.error(wstr)
- all_sensors = []
- for structure in nest.structures():
- all_sensors += [NestBasicSensor(structure, None, variable)
- for variable in conditions
- if variable in STRUCTURE_SENSOR_TYPES]
+ def get_sensors():
+ """Get the Nest sensors."""
+ all_sensors = []
+ for structure in nest.structures():
+ all_sensors += [NestBasicSensor(structure, None, variable)
+ for variable in conditions
+ if variable in STRUCTURE_SENSOR_TYPES]
+
+ for structure, device in nest.thermostats():
+ all_sensors += [NestBasicSensor(structure, device, variable)
+ for variable in conditions
+ if variable in SENSOR_TYPES]
+ all_sensors += [NestTempSensor(structure, device, variable)
+ for variable in conditions
+ if variable in TEMP_SENSOR_TYPES]
- for structure, device in nest.thermostats():
- all_sensors += [NestBasicSensor(structure, device, variable)
- for variable in conditions
- if variable in SENSOR_TYPES]
- all_sensors += [NestTempSensor(structure, device, variable)
- for variable in conditions
- if variable in TEMP_SENSOR_TYPES]
+ for structure, device in nest.smoke_co_alarms():
+ all_sensors += [NestBasicSensor(structure, device, variable)
+ for variable in conditions
+ if variable in PROTECT_SENSOR_TYPES]
- for structure, device in nest.smoke_co_alarms():
- all_sensors += [NestBasicSensor(structure, device, variable)
- for variable in conditions
- if variable in PROTECT_SENSOR_TYPES]
+ return all_sensors
- add_devices(all_sensors, True)
+ async_add_devices(await hass.async_add_job(get_sensors), True)
class NestBasicSensor(NestSensorDevice):
@@ -115,7 +130,8 @@ def update(self):
if self.variable in VARIABLE_NAME_MAPPING:
self._state = getattr(self.device,
VARIABLE_NAME_MAPPING[self.variable])
- elif self.variable in PROTECT_SENSOR_TYPES:
+ elif self.variable in PROTECT_SENSOR_TYPES \
+ and self.variable != 'color_status':
# keep backward compatibility
self._state = getattr(self.device, self.variable).capitalize()
else:
diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py
index f09e1d4f395464..191e587feafd1d 100644
--- a/homeassistant/components/sensor/netatmo.py
+++ b/homeassistant/components/sensor/netatmo.py
@@ -70,7 +70,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
data = NetAtmoData(netatmo.NETATMO_AUTH, config.get(CONF_STATION, None))
dev = []
- import lnetatmo
+ import pyatmo
try:
if CONF_MODULES in config:
# Iterate each module
@@ -92,7 +92,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
else:
_LOGGER.warning("Ignoring unknown var %s for mod %s",
variable, module_name)
- except lnetatmo.NoDevice:
+ except pyatmo.NoDevice:
return None
add_devices(dev, True)
@@ -305,8 +305,8 @@ def get_module_names(self):
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Call the Netatmo API to update the data."""
- import lnetatmo
- self.station_data = lnetatmo.WeatherStationData(self.auth)
+ import pyatmo
+ self.station_data = pyatmo.WeatherStationData(self.auth)
if self.station is not None:
self.data = self.station_data.lastData(
diff --git a/homeassistant/components/sensor/netgear_lte.py b/homeassistant/components/sensor/netgear_lte.py
new file mode 100644
index 00000000000000..859435edbc99ea
--- /dev/null
+++ b/homeassistant/components/sensor/netgear_lte.py
@@ -0,0 +1,85 @@
+"""Netgear LTE sensors.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.netgear_lte/
+"""
+
+import voluptuous as vol
+import attr
+
+from homeassistant.const import CONF_HOST, CONF_SENSORS
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.helpers.entity import Entity
+import homeassistant.helpers.config_validation as cv
+
+from ..netgear_lte import DATA_KEY
+
+DEPENDENCIES = ['netgear_lte']
+
+SENSOR_SMS = 'sms'
+SENSOR_USAGE = 'usage'
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Optional(CONF_HOST): cv.string,
+ vol.Required(CONF_SENSORS): vol.All(
+ cv.ensure_list, [vol.In([SENSOR_SMS, SENSOR_USAGE])])
+})
+
+
+async def async_setup_platform(
+ hass, config, async_add_devices, discovery_info):
+ """Set up Netgear LTE sensor devices."""
+ lte_data = hass.data[DATA_KEY].get(config)
+
+ sensors = []
+ for sensortype in config[CONF_SENSORS]:
+ if sensortype == SENSOR_SMS:
+ sensors.append(SMSSensor(lte_data))
+ elif sensortype == SENSOR_USAGE:
+ sensors.append(UsageSensor(lte_data))
+
+ async_add_devices(sensors, True)
+
+
+@attr.s
+class LTESensor(Entity):
+ """Data usage sensor entity."""
+
+ lte_data = attr.ib()
+
+ async def async_update(self):
+ """Update state."""
+ await self.lte_data.async_update()
+
+
+class SMSSensor(LTESensor):
+ """Unread SMS sensor entity."""
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return "Netgear LTE SMS"
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self.lte_data.unread_count
+
+
+class UsageSensor(LTESensor):
+ """Data usage sensor entity."""
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return "MiB"
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return "Netgear LTE usage"
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return round(self.lte_data.usage / 1024**2, 1)
diff --git a/homeassistant/components/sensor/nsw_fuel_station.py b/homeassistant/components/sensor/nsw_fuel_station.py
new file mode 100644
index 00000000000000..2440dac3204e6f
--- /dev/null
+++ b/homeassistant/components/sensor/nsw_fuel_station.py
@@ -0,0 +1,174 @@
+"""
+Sensor platform to display the current fuel prices at a NSW fuel station.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.nsw_fuel_station/
+"""
+import datetime
+import logging
+from typing import Optional
+
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.components.light import PLATFORM_SCHEMA
+from homeassistant.const import ATTR_ATTRIBUTION
+from homeassistant.helpers.entity import Entity
+from homeassistant.util import Throttle
+
+REQUIREMENTS = ['nsw-fuel-api-client==1.0.10']
+
+_LOGGER = logging.getLogger(__name__)
+
+ATTR_STATION_ID = 'station_id'
+ATTR_STATION_NAME = 'station_name'
+
+CONF_STATION_ID = 'station_id'
+CONF_FUEL_TYPES = 'fuel_types'
+CONF_ALLOWED_FUEL_TYPES = ["E10", "U91", "E85", "P95", "P98", "DL",
+ "PDL", "B20", "LPG", "CNG", "EV"]
+CONF_DEFAULT_FUEL_TYPES = ["E10", "U91"]
+CONF_ATTRIBUTION = "Data provided by NSW Government FuelCheck"
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_STATION_ID): cv.positive_int,
+ vol.Optional(CONF_FUEL_TYPES, default=CONF_DEFAULT_FUEL_TYPES):
+ vol.All(cv.ensure_list, [vol.In(CONF_ALLOWED_FUEL_TYPES)]),
+})
+
+MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(hours=1)
+
+NOTIFICATION_ID = 'nsw_fuel_station_notification'
+NOTIFICATION_TITLE = 'NSW Fuel Station Sensor Setup'
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the NSW Fuel Station sensor."""
+ from nsw_fuel import FuelCheckClient
+
+ station_id = config[CONF_STATION_ID]
+ fuel_types = config[CONF_FUEL_TYPES]
+
+ client = FuelCheckClient()
+ station_data = StationPriceData(client, station_id)
+ station_data.update()
+
+ if station_data.error is not None:
+ message = (
+ 'Error: {}. Check the logs for additional information.'
+ ).format(station_data.error)
+
+ hass.components.persistent_notification.create(
+ message,
+ title=NOTIFICATION_TITLE,
+ notification_id=NOTIFICATION_ID)
+ return
+
+ available_fuel_types = station_data.get_available_fuel_types()
+
+ add_devices([
+ StationPriceSensor(station_data, fuel_type)
+ for fuel_type in fuel_types
+ if fuel_type in available_fuel_types
+ ])
+
+
+class StationPriceData(object):
+ """An object to store and fetch the latest data for a given station."""
+
+ def __init__(self, client, station_id: int) -> None:
+ """Initialize the sensor."""
+ self.station_id = station_id
+ self._client = client
+ self._data = None
+ self._reference_data = None
+ self.error = None
+ self._station_name = None
+
+ @Throttle(MIN_TIME_BETWEEN_UPDATES)
+ def update(self):
+ """Update the internal data using the API client."""
+ from nsw_fuel import FuelCheckError
+
+ if self._reference_data is None:
+ try:
+ self._reference_data = self._client.get_reference_data()
+ except FuelCheckError as exc:
+ self.error = str(exc)
+ _LOGGER.error(
+ 'Failed to fetch NSW Fuel station reference data. %s', exc)
+ return
+
+ try:
+ self._data = self._client.get_fuel_prices_for_station(
+ self.station_id)
+ except FuelCheckError as exc:
+ self.error = str(exc)
+ _LOGGER.error(
+ 'Failed to fetch NSW Fuel station price data. %s', exc)
+
+ def for_fuel_type(self, fuel_type: str):
+ """Return the price of the given fuel type."""
+ if self._data is None:
+ return None
+ return next((price for price
+ in self._data if price.fuel_type == fuel_type), None)
+
+ def get_available_fuel_types(self):
+ """Return the available fuel types for the station."""
+ return [price.fuel_type for price in self._data]
+
+ def get_station_name(self) -> str:
+ """Return the name of the station."""
+ if self._station_name is None:
+ name = None
+ if self._reference_data is not None:
+ name = next((station.name for station
+ in self._reference_data.stations
+ if station.code == self.station_id), None)
+
+ self._station_name = name or 'station {}'.format(self.station_id)
+
+ return self._station_name
+
+
+class StationPriceSensor(Entity):
+ """Implementation of a sensor that reports the fuel price for a station."""
+
+ def __init__(self, station_data: StationPriceData, fuel_type: str):
+ """Initialize the sensor."""
+ self._station_data = station_data
+ self._fuel_type = fuel_type
+
+ @property
+ def name(self) -> str:
+ """Return the name of the sensor."""
+ return '{} {}'.format(
+ self._station_data.get_station_name(), self._fuel_type)
+
+ @property
+ def state(self) -> Optional[float]:
+ """Return the state of the sensor."""
+ price_info = self._station_data.for_fuel_type(self._fuel_type)
+ if price_info:
+ return price_info.price
+
+ return None
+
+ @property
+ def device_state_attributes(self) -> dict:
+ """Return the state attributes of the device."""
+ return {
+ ATTR_STATION_ID: self._station_data.station_id,
+ ATTR_STATION_NAME: self._station_data.get_station_name(),
+ ATTR_ATTRIBUTION: CONF_ATTRIBUTION
+ }
+
+ @property
+ def unit_of_measurement(self) -> str:
+ """Return the units of measurement."""
+ return '¢/L'
+
+ def update(self):
+ """Update current conditions."""
+ self._station_data.update()
diff --git a/homeassistant/components/sensor/nzbget.py b/homeassistant/components/sensor/nzbget.py
index b140d02af04587..0fa6362ad051ff 100644
--- a/homeassistant/components/sensor/nzbget.py
+++ b/homeassistant/components/sensor/nzbget.py
@@ -50,7 +50,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the NZBGet sensors."""
host = config.get(CONF_HOST)
diff --git a/homeassistant/components/sensor/octoprint.py b/homeassistant/components/sensor/octoprint.py
index 8a800e8616cc73..20d00267deef3a 100644
--- a/homeassistant/components/sensor/octoprint.py
+++ b/homeassistant/components/sensor/octoprint.py
@@ -38,7 +38,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the available OctoPrint sensors."""
octoprint_api = hass.data[DOMAIN]["api"]
diff --git a/homeassistant/components/sensor/ohmconnect.py b/homeassistant/components/sensor/ohmconnect.py
index ff465b3617c2e3..d323a21a521096 100644
--- a/homeassistant/components/sensor/ohmconnect.py
+++ b/homeassistant/components/sensor/ohmconnect.py
@@ -31,7 +31,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the OhmConnect sensor."""
name = config.get(CONF_NAME)
diff --git a/homeassistant/components/sensor/onewire.py b/homeassistant/components/sensor/onewire.py
index 43105d54e38dfb..95ad5f1713d460 100644
--- a/homeassistant/components/sensor/onewire.py
+++ b/homeassistant/components/sensor/onewire.py
@@ -47,7 +47,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the one wire Sensors."""
base_dir = config.get(CONF_MOUNT_DIR)
diff --git a/homeassistant/components/sensor/opensky.py b/homeassistant/components/sensor/opensky.py
index bd071ace57854c..af0491cc26cf96 100644
--- a/homeassistant/components/sensor/opensky.py
+++ b/homeassistant/components/sensor/opensky.py
@@ -50,7 +50,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Open Sky platform."""
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
diff --git a/homeassistant/components/sensor/pi_hole.py b/homeassistant/components/sensor/pi_hole.py
index 027c12569a609a..8e8c784e68b941 100644
--- a/homeassistant/components/sensor/pi_hole.py
+++ b/homeassistant/components/sensor/pi_hole.py
@@ -1,23 +1,26 @@
"""
-Support for getting statistical data from a Pi-Hole system.
+Support for getting statistical data from a Pi-hole system.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.pi_hole/
"""
-import logging
-import json
from datetime import timedelta
+import logging
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.entity import Entity
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
- CONF_NAME, CONF_HOST, CONF_SSL, CONF_VERIFY_SSL, CONF_MONITORED_CONDITIONS)
+ CONF_HOST, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_SSL, CONF_VERIFY_SSL)
+from homeassistant.exceptions import PlatformNotReady
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
+from homeassistant.util import Throttle
+
+REQUIREMENTS = ['pihole==0.1.2']
_LOGGER = logging.getLogger(__name__)
-_ENDPOINT = '/api.php'
ATTR_BLOCKED_DOMAINS = 'domains_blocked'
ATTR_PERCENTAGE_TODAY = 'percentage_today'
@@ -32,25 +35,27 @@
DEFAULT_SSL = False
DEFAULT_VERIFY_SSL = True
-SCAN_INTERVAL = timedelta(minutes=5)
+MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
MONITORED_CONDITIONS = {
- 'dns_queries_today': ['DNS Queries Today',
- 'queries', 'mdi:comment-question-outline'],
- 'ads_blocked_today': ['Ads Blocked Today',
- 'ads', 'mdi:close-octagon-outline'],
- 'ads_percentage_today': ['Ads Percentage Blocked Today',
- '%', 'mdi:close-octagon-outline'],
- 'domains_being_blocked': ['Domains Blocked',
- 'domains', 'mdi:block-helper'],
- 'queries_cached': ['DNS Queries Cached',
- 'queries', 'mdi:comment-question-outline'],
- 'queries_forwarded': ['DNS Queries Forwarded',
- 'queries', 'mdi:comment-question-outline'],
- 'unique_clients': ['DNS Unique Clients',
- 'clients', 'mdi:account-outline'],
- 'unique_domains': ['DNS Unique Domains',
- 'domains', 'mdi:domain'],
+ 'ads_blocked_today':
+ ['Ads Blocked Today', 'ads', 'mdi:close-octagon-outline'],
+ 'ads_percentage_today':
+ ['Ads Percentage Blocked Today', '%', 'mdi:close-octagon-outline'],
+ 'clients_ever_seen':
+ ['Seen Clients', 'clients', 'mdi:account-outline'],
+ 'dns_queries_today':
+ ['DNS Queries Today', 'queries', 'mdi:comment-question-outline'],
+ 'domains_being_blocked':
+ ['Domains Blocked', 'domains', 'mdi:block-helper'],
+ 'queries_cached':
+ ['DNS Queries Cached', 'queries', 'mdi:comment-question-outline'],
+ 'queries_forwarded':
+ ['DNS Queries Forwarded', 'queries', 'mdi:comment-question-outline'],
+ 'unique_clients':
+ ['DNS Unique Clients', 'clients', 'mdi:account-outline'],
+ 'unique_domains':
+ ['DNS Unique Domains', 'domains', 'mdi:domain'],
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@@ -65,100 +70,105 @@
})
-def setup_platform(hass, config, add_devices, discovery_info=None):
- """Set up the Pi-Hole sensor."""
+async def async_setup_platform(
+ hass, config, async_add_devices, discovery_info=None):
+ """Set up the Pi-hole sensor."""
+ from pihole import PiHole
+
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
- use_ssl = config.get(CONF_SSL)
+ use_tls = config.get(CONF_SSL)
location = config.get(CONF_LOCATION)
- verify_ssl = config.get(CONF_VERIFY_SSL)
+ verify_tls = config.get(CONF_VERIFY_SSL)
+
+ session = async_get_clientsession(hass)
+ pi_hole = PiHoleData(PiHole(
+ host, hass.loop, session, location=location, tls=use_tls,
+ verify_tls=verify_tls))
- api = PiHoleAPI('{}/{}'.format(host, location), use_ssl, verify_ssl)
+ await pi_hole.async_update()
- sensors = [PiHoleSensor(hass, api, name, condition)
+ if pi_hole.api.data is None:
+ raise PlatformNotReady
+
+ sensors = [PiHoleSensor(pi_hole, name, condition)
for condition in config[CONF_MONITORED_CONDITIONS]]
- add_devices(sensors, True)
+ async_add_devices(sensors, True)
class PiHoleSensor(Entity):
- """Representation of a Pi-Hole sensor."""
+ """Representation of a Pi-hole sensor."""
- def __init__(self, hass, api, name, variable):
- """Initialize a Pi-Hole sensor."""
- self._hass = hass
- self._api = api
+ def __init__(self, pi_hole, name, condition):
+ """Initialize a Pi-hole sensor."""
+ self.pi_hole = pi_hole
self._name = name
- self._var_id = variable
+ self._condition = condition
- variable_info = MONITORED_CONDITIONS[variable]
- self._var_name = variable_info[0]
- self._var_units = variable_info[1]
- self._var_icon = variable_info[2]
+ variable_info = MONITORED_CONDITIONS[condition]
+ self._condition_name = variable_info[0]
+ self._unit_of_measurement = variable_info[1]
+ self._icon = variable_info[2]
+ self.data = {}
@property
def name(self):
"""Return the name of the sensor."""
- return "{} {}".format(self._name, self._var_name)
+ return "{} {}".format(self._name, self._condition_name)
@property
def icon(self):
"""Icon to use in the frontend, if any."""
- return self._var_icon
+ return self._icon
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
- return self._var_units
+ return self._unit_of_measurement
- # pylint: disable=no-member
@property
def state(self):
"""Return the state of the device."""
try:
- return round(self._api.data[self._var_id], 2)
+ return round(self.data[self._condition], 2)
except TypeError:
- return self._api.data[self._var_id]
+ return self.data[self._condition]
- # pylint: disable=no-member
@property
def device_state_attributes(self):
"""Return the state attributes of the Pi-Hole."""
return {
- ATTR_BLOCKED_DOMAINS: self._api.data['domains_being_blocked'],
+ ATTR_BLOCKED_DOMAINS: self.data['domains_being_blocked'],
}
@property
def available(self):
"""Could the device be accessed during the last update call."""
- return self._api.available
+ return self.pi_hole.available
- def update(self):
- """Get the latest data from the Pi-Hole API."""
- self._api.update()
+ async def async_update(self):
+ """Get the latest data from the Pi-hole API."""
+ await self.pi_hole.async_update()
+ self.data = self.pi_hole.api.data
-class PiHoleAPI(object):
+class PiHoleData(object):
"""Get the latest data and update the states."""
- def __init__(self, host, use_ssl, verify_ssl):
+ def __init__(self, api):
"""Initialize the data object."""
- from homeassistant.components.sensor.rest import RestData
-
- uri_scheme = 'https://' if use_ssl else 'http://'
- resource = "{}{}{}".format(uri_scheme, host, _ENDPOINT)
-
- self._rest = RestData('GET', resource, None, None, None, verify_ssl)
- self.data = None
+ self.api = api
self.available = True
- self.update()
- def update(self):
- """Get the latest data from the Pi-Hole."""
+ @Throttle(MIN_TIME_BETWEEN_UPDATES)
+ async def async_update(self):
+ """Get the latest data from the Pi-hole."""
+ from pihole.exceptions import PiHoleError
+
try:
- self._rest.update()
- self.data = json.loads(self._rest.data)
+ await self.api.get_data()
self.available = True
- except TypeError:
- _LOGGER.error("Unable to fetch data from Pi-Hole")
+ except PiHoleError:
+ _LOGGER.error("Unable to fetch data from Pi-hole")
self.available = False
diff --git a/homeassistant/components/sensor/pilight.py b/homeassistant/components/sensor/pilight.py
index 596887998ecd55..9784cc3dc4ca35 100644
--- a/homeassistant/components/sensor/pilight.py
+++ b/homeassistant/components/sensor/pilight.py
@@ -30,7 +30,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Pilight Sensor."""
add_devices([PilightSensor(
diff --git a/homeassistant/components/sensor/plex.py b/homeassistant/components/sensor/plex.py
index b61e1bce0da05b..5aa156a0ac6df7 100644
--- a/homeassistant/components/sensor/plex.py
+++ b/homeassistant/components/sensor/plex.py
@@ -41,7 +41,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Plex sensor."""
name = config.get(CONF_NAME)
diff --git a/homeassistant/components/sensor/postnl.py b/homeassistant/components/sensor/postnl.py
index 63a9c1d67d5ae2..0e296fa56bd614 100644
--- a/homeassistant/components/sensor/postnl.py
+++ b/homeassistant/components/sensor/postnl.py
@@ -35,7 +35,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the PostNL sensor platform."""
from postnl_api import PostNL_API, UnauthorizedException
diff --git a/homeassistant/components/sensor/pyload.py b/homeassistant/components/sensor/pyload.py
index 9e1c0875169197..cc4ce1e64485e6 100644
--- a/homeassistant/components/sensor/pyload.py
+++ b/homeassistant/components/sensor/pyload.py
@@ -43,7 +43,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the pyLoad sensors."""
host = config.get(CONF_HOST)
diff --git a/homeassistant/components/sensor/qnap.py b/homeassistant/components/sensor/qnap.py
index 7dd795d8f8d36b..3d9704875c9cc5 100644
--- a/homeassistant/components/sensor/qnap.py
+++ b/homeassistant/components/sensor/qnap.py
@@ -102,7 +102,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the QNAP NAS sensor."""
api = QNAPStatsAPI(config)
diff --git a/homeassistant/components/sensor/rainmachine.py b/homeassistant/components/sensor/rainmachine.py
index 8faf30acc3899d..f747a26df397b4 100644
--- a/homeassistant/components/sensor/rainmachine.py
+++ b/homeassistant/components/sensor/rainmachine.py
@@ -7,7 +7,7 @@
import logging
from homeassistant.components.rainmachine import (
- DATA_RAINMACHINE, DATA_UPDATE_TOPIC, SENSORS, RainMachineEntity)
+ DATA_RAINMACHINE, SENSOR_UPDATE_TOPIC, SENSORS, RainMachineEntity)
from homeassistant.const import CONF_MONITORED_CONDITIONS
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -17,7 +17,8 @@
_LOGGER = logging.getLogger(__name__)
-def setup_platform(hass, config, add_devices, discovery_info=None):
+async def async_setup_platform(
+ hass, config, async_add_devices, discovery_info=None):
"""Set up the RainMachine Switch platform."""
if discovery_info is None:
return
@@ -30,7 +31,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
sensors.append(
RainMachineSensor(rainmachine, sensor_type, name, icon, unit))
- add_devices(sensors, True)
+ async_add_devices(sensors, True)
class RainMachineSensor(RainMachineEntity):
@@ -73,16 +74,16 @@ def unit_of_measurement(self):
return self._unit
@callback
- def update_data(self):
+ def _update_data(self):
"""Update the state."""
self.async_schedule_update_ha_state(True)
async def async_added_to_hass(self):
"""Register callbacks."""
- async_dispatcher_connect(self.hass, DATA_UPDATE_TOPIC,
- self.update_data)
+ async_dispatcher_connect(
+ self.hass, SENSOR_UPDATE_TOPIC, self._update_data)
- def update(self):
+ async def async_update(self):
"""Update the sensor's state."""
self._state = self.rainmachine.restrictions['global'][
'freezeProtectTemp']
diff --git a/homeassistant/components/sensor/simulated.py b/homeassistant/components/sensor/simulated.py
index 9dac0b48bc2c3a..419ca7c13fb175 100644
--- a/homeassistant/components/sensor/simulated.py
+++ b/homeassistant/components/sensor/simulated.py
@@ -7,6 +7,7 @@
import logging
import math
from random import Random
+from datetime import datetime
import voluptuous as vol
@@ -25,6 +26,7 @@
CONF_PHASE = 'phase'
CONF_SEED = 'seed'
CONF_UNIT = 'unit'
+CONF_RELATIVE_TO_EPOCH = 'relative_to_epoch'
DEFAULT_AMP = 1
DEFAULT_FWHM = 0
@@ -34,6 +36,7 @@
DEFAULT_PHASE = 0
DEFAULT_SEED = 999
DEFAULT_UNIT = 'value'
+DEFAULT_RELATIVE_TO_EPOCH = True
ICON = 'mdi:chart-line'
@@ -46,6 +49,8 @@
vol.Optional(CONF_PHASE, default=DEFAULT_PHASE): vol.Coerce(float),
vol.Optional(CONF_SEED, default=DEFAULT_SEED): cv.positive_int,
vol.Optional(CONF_UNIT, default=DEFAULT_UNIT): cv.string,
+ vol.Optional(CONF_RELATIVE_TO_EPOCH, default=DEFAULT_RELATIVE_TO_EPOCH):
+ cv.boolean,
})
@@ -59,15 +64,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
phase = config.get(CONF_PHASE)
fwhm = config.get(CONF_FWHM)
seed = config.get(CONF_SEED)
+ relative_to_epoch = config.get(CONF_RELATIVE_TO_EPOCH)
- sensor = SimulatedSensor(name, unit, amp, mean, period, phase, fwhm, seed)
+ sensor = SimulatedSensor(name, unit, amp, mean, period, phase, fwhm, seed,
+ relative_to_epoch)
add_devices([sensor], True)
class SimulatedSensor(Entity):
"""Class for simulated sensor."""
- def __init__(self, name, unit, amp, mean, period, phase, fwhm, seed):
+ def __init__(self, name, unit, amp, mean, period, phase, fwhm, seed,
+ relative_to_epoch):
"""Init the class."""
self._name = name
self._unit = unit
@@ -78,7 +86,11 @@ def __init__(self, name, unit, amp, mean, period, phase, fwhm, seed):
self._fwhm = fwhm
self._seed = seed
self._random = Random(seed) # A local seeded Random
- self._start_time = dt_util.utcnow()
+ self._start_time = (
+ datetime(1970, 1, 1, tzinfo=dt_util.UTC) if relative_to_epoch
+ else dt_util.utcnow()
+ )
+ self._relative_to_epoch = relative_to_epoch
self._state = None
def time_delta(self):
@@ -100,7 +112,7 @@ def signal_calc(self):
else:
periodic = amp * (math.sin((2*math.pi*time_delta/period) + phase))
noise = self._random.gauss(mu=0, sigma=fwhm)
- return mean + periodic + noise
+ return round(mean + periodic + noise, 3)
async def async_update(self):
"""Update the sensor."""
@@ -136,5 +148,6 @@ def device_state_attributes(self):
'phase': self._phase,
'spread': self._fwhm,
'seed': self._seed,
+ 'relative_to_epoch': self._relative_to_epoch,
}
return attr
diff --git a/homeassistant/components/sensor/skybeacon.py b/homeassistant/components/sensor/skybeacon.py
index 61933614a7471d..53cbaab19a58eb 100644
--- a/homeassistant/components/sensor/skybeacon.py
+++ b/homeassistant/components/sensor/skybeacon.py
@@ -39,7 +39,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Skybeacon sensor."""
# pylint: disable=unreachable
diff --git a/homeassistant/components/sensor/smappee.py b/homeassistant/components/sensor/smappee.py
index 5b84962144d5b0..783c2aad4693ae 100644
--- a/homeassistant/components/sensor/smappee.py
+++ b/homeassistant/components/sensor/smappee.py
@@ -189,8 +189,10 @@ def update(self):
data = self._smappee.sensor_consumption[self._location_id]\
.get(int(sensor_id))
if data:
- consumption = data.get('records')[-1]
- _LOGGER.debug("%s (%s) %s",
- sensor_name, sensor_id, consumption)
- value = consumption.get(self._smappe_name)
- self._state = value
+ tempdata = data.get('records')
+ if tempdata:
+ consumption = tempdata[-1]
+ _LOGGER.debug("%s (%s) %s",
+ sensor_name, sensor_id, consumption)
+ value = consumption.get(self._smappe_name)
+ self._state = value
diff --git a/homeassistant/components/sensor/spotcrime.py b/homeassistant/components/sensor/spotcrime.py
index 08177c9a7b9200..daa520f2ede077 100644
--- a/homeassistant/components/sensor/spotcrime.py
+++ b/homeassistant/components/sensor/spotcrime.py
@@ -44,7 +44,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Crime Reports platform."""
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
diff --git a/homeassistant/components/sensor/steam_online.py b/homeassistant/components/sensor/steam_online.py
index 88cb786e66d8d9..e22e1594b55479 100644
--- a/homeassistant/components/sensor/steam_online.py
+++ b/homeassistant/components/sensor/steam_online.py
@@ -36,7 +36,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Steam platform."""
import steam as steamod
diff --git a/homeassistant/components/sensor/supervisord.py b/homeassistant/components/sensor/supervisord.py
index fd0c6292de2bb4..5a302462bbf188 100644
--- a/homeassistant/components/sensor/supervisord.py
+++ b/homeassistant/components/sensor/supervisord.py
@@ -26,7 +26,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Supervisord platform."""
url = config.get(CONF_URL)
diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py
index 928d84caa2b295..72c6aa2e1a3dfd 100644
--- a/homeassistant/components/sensor/swiss_public_transport.py
+++ b/homeassistant/components/sensor/swiss_public_transport.py
@@ -16,7 +16,7 @@
from homeassistant.helpers.entity import Entity
import homeassistant.util.dt as dt_util
-REQUIREMENTS = ['python_opendata_transport==0.1.0']
+REQUIREMENTS = ['python_opendata_transport==0.1.3']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py
index 0b85de8e4f23dd..1883ee89d4e2f6 100644
--- a/homeassistant/components/sensor/systemmonitor.py
+++ b/homeassistant/components/sensor/systemmonitor.py
@@ -10,13 +10,12 @@
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import (
- CONF_RESOURCES, STATE_OFF, STATE_ON, STATE_UNKNOWN, CONF_TYPE)
+from homeassistant.const import CONF_RESOURCES, STATE_OFF, STATE_ON, CONF_TYPE
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
-REQUIREMENTS = ['psutil==5.4.5']
+REQUIREMENTS = ['psutil==5.4.6']
_LOGGER = logging.getLogger(__name__)
@@ -68,7 +67,6 @@
}
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the system monitor sensors."""
dev = []
@@ -157,19 +155,19 @@ def update(self):
counter = counters[self.argument][IO_COUNTER[self.type]]
self._state = round(counter / 1024**2, 1)
else:
- self._state = STATE_UNKNOWN
+ self._state = None
elif self.type == 'packets_out' or self.type == 'packets_in':
counters = psutil.net_io_counters(pernic=True)
if self.argument in counters:
self._state = counters[self.argument][IO_COUNTER[self.type]]
else:
- self._state = STATE_UNKNOWN
+ self._state = None
elif self.type == 'ipv4_address' or self.type == 'ipv6_address':
addresses = psutil.net_if_addrs()
if self.argument in addresses:
self._state = addresses[self.argument][IF_ADDRS[self.type]][1]
else:
- self._state = STATE_UNKNOWN
+ self._state = None
elif self.type == 'last_boot':
self._state = dt_util.as_local(
dt_util.utc_from_timestamp(psutil.boot_time())
diff --git a/homeassistant/components/sensor/tellstick.py b/homeassistant/components/sensor/tellstick.py
index 8355add47e92a5..de929aa094272b 100644
--- a/homeassistant/components/sensor/tellstick.py
+++ b/homeassistant/components/sensor/tellstick.py
@@ -37,7 +37,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Tellstick sensors."""
import tellcore.telldus as telldus
diff --git a/homeassistant/components/sensor/temper.py b/homeassistant/components/sensor/temper.py
index 973e07d9cf3435..f0a3e15834cf3e 100644
--- a/homeassistant/components/sensor/temper.py
+++ b/homeassistant/components/sensor/temper.py
@@ -33,7 +33,6 @@ def get_temper_devices():
return TemperHandler().get_devices()
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Temper sensors."""
temp_unit = hass.config.units.temperature_unit
diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py
index 65f49998dbf9fa..23c7c13f0edec1 100644
--- a/homeassistant/components/sensor/template.py
+++ b/homeassistant/components/sensor/template.py
@@ -42,7 +42,6 @@
@asyncio.coroutine
-# pylint: disable=unused-argument
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the template sensors."""
sensors = []
diff --git a/homeassistant/components/sensor/torque.py b/homeassistant/components/sensor/torque.py
index 98fad475d52a2e..4ed1b5907cf424 100644
--- a/homeassistant/components/sensor/torque.py
+++ b/homeassistant/components/sensor/torque.py
@@ -46,7 +46,6 @@ def convert_pid(value):
return int(value, 16)
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Torque platform."""
vehicle = config.get(CONF_NAME)
diff --git a/homeassistant/components/sensor/twitch.py b/homeassistant/components/sensor/twitch.py
index b3e227aea72247..250911b49b1096 100644
--- a/homeassistant/components/sensor/twitch.py
+++ b/homeassistant/components/sensor/twitch.py
@@ -31,7 +31,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Twitch platform."""
channels = config.get(CONF_CHANNELS, [])
diff --git a/homeassistant/components/sensor/ups.py b/homeassistant/components/sensor/ups.py
index c51ae67475fe70..a864df384ad011 100644
--- a/homeassistant/components/sensor/ups.py
+++ b/homeassistant/components/sensor/ups.py
@@ -38,7 +38,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the UPS platform."""
import upsmychoice
diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py
index eb8ccae768e204..4fc92db1d90092 100644
--- a/homeassistant/components/sensor/vera.py
+++ b/homeassistant/components/sensor/vera.py
@@ -25,8 +25,8 @@
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Vera controller devices."""
add_devices(
- VeraSensor(device, hass.data[VERA_CONTROLLER])
- for device in hass.data[VERA_DEVICES]['sensor'])
+ [VeraSensor(device, hass.data[VERA_CONTROLLER])
+ for device in hass.data[VERA_DEVICES]['sensor']], True)
class VeraSensor(VeraDevice, Entity):
diff --git a/homeassistant/components/sensor/verisure.py b/homeassistant/components/sensor/verisure.py
index 5ab999ccabfea7..187a9bd7935c2e 100644
--- a/homeassistant/components/sensor/verisure.py
+++ b/homeassistant/components/sensor/verisure.py
@@ -74,6 +74,7 @@ def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
return TEMP_CELSIUS
+ # pylint: disable=no-self-use
def update(self):
"""Update the sensor."""
hub.update_overview()
@@ -112,6 +113,7 @@ def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
return '%'
+ # pylint: disable=no-self-use
def update(self):
"""Update the sensor."""
hub.update_overview()
@@ -150,6 +152,7 @@ def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
return 'Mice'
+ # pylint: disable=no-self-use
def update(self):
"""Update the sensor."""
hub.update_overview()
diff --git a/homeassistant/components/sensor/wirelesstag.py b/homeassistant/components/sensor/wirelesstag.py
new file mode 100755
index 00000000000000..c93da3c791f107
--- /dev/null
+++ b/homeassistant/components/sensor/wirelesstag.py
@@ -0,0 +1,176 @@
+"""
+Sensor support for Wirelss Sensor Tags platform.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.wirelesstag/
+"""
+
+import logging
+import voluptuous as vol
+
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.const import (
+ CONF_MONITORED_CONDITIONS)
+from homeassistant.components.wirelesstag import (
+ DOMAIN as WIRELESSTAG_DOMAIN,
+ WIRELESSTAG_TYPE_13BIT, WIRELESSTAG_TYPE_WATER,
+ WIRELESSTAG_TYPE_ALSPRO,
+ WIRELESSTAG_TYPE_WEMO_DEVICE,
+ SIGNAL_TAG_UPDATE,
+ WirelessTagBaseSensor)
+import homeassistant.helpers.config_validation as cv
+from homeassistant.const import TEMP_CELSIUS
+
+DEPENDENCIES = ['wirelesstag']
+
+_LOGGER = logging.getLogger(__name__)
+
+SENSOR_TEMPERATURE = 'temperature'
+SENSOR_HUMIDITY = 'humidity'
+SENSOR_MOISTURE = 'moisture'
+SENSOR_LIGHT = 'light'
+
+SENSOR_TYPES = {
+ SENSOR_TEMPERATURE: {
+ 'unit': TEMP_CELSIUS,
+ 'attr': 'temperature'
+ },
+ SENSOR_HUMIDITY: {
+ 'unit': '%',
+ 'attr': 'humidity'
+ },
+ SENSOR_MOISTURE: {
+ 'unit': '%',
+ 'attr': 'moisture'
+ },
+ SENSOR_LIGHT: {
+ 'unit': 'lux',
+ 'attr': 'light'
+ }
+}
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_MONITORED_CONDITIONS, default=[]):
+ vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
+})
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Setup the sensor platform."""
+ platform = hass.data.get(WIRELESSTAG_DOMAIN)
+ sensors = []
+ tags = platform.tags
+ for tag in tags.values():
+ for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
+ if sensor_type in WirelessTagSensor.allowed_sensors(tag):
+ sensors.append(WirelessTagSensor(
+ platform, tag, sensor_type, hass.config))
+
+ add_devices(sensors, True)
+
+
+class WirelessTagSensor(WirelessTagBaseSensor):
+ """Representation of a Sensor."""
+
+ @classmethod
+ def allowed_sensors(cls, tag):
+ """Return array of allowed sensor types for tag."""
+ all_sensors = SENSOR_TYPES.keys()
+ sensors_per_tag_type = {
+ WIRELESSTAG_TYPE_13BIT: [
+ SENSOR_TEMPERATURE,
+ SENSOR_HUMIDITY],
+ WIRELESSTAG_TYPE_WATER: [
+ SENSOR_TEMPERATURE,
+ SENSOR_MOISTURE],
+ WIRELESSTAG_TYPE_ALSPRO: [
+ SENSOR_TEMPERATURE,
+ SENSOR_HUMIDITY,
+ SENSOR_LIGHT],
+ WIRELESSTAG_TYPE_WEMO_DEVICE: []
+ }
+
+ tag_type = tag.tag_type
+ return (
+ sensors_per_tag_type[tag_type] if tag_type in sensors_per_tag_type
+ else all_sensors)
+
+ def __init__(self, api, tag, sensor_type, config):
+ """Constructor with platform(api), tag and hass sensor type."""
+ super().__init__(api, tag)
+
+ self._sensor_type = sensor_type
+ self._tag_attr = SENSOR_TYPES[self._sensor_type]['attr']
+ self._unit_of_measurement = SENSOR_TYPES[self._sensor_type]['unit']
+ self._name = self._tag.name
+
+ # I want to see entity_id as:
+ # sensor.wirelesstag_bedroom_temperature
+ # and not as sensor.bedroom for temperature and
+ # sensor.bedroom_2 for humidity
+ self._entity_id = '{}.{}_{}_{}'.format('sensor', WIRELESSTAG_DOMAIN,
+ self.underscored_name,
+ self._sensor_type)
+
+ async def async_added_to_hass(self):
+ """Register callbacks."""
+ async_dispatcher_connect(
+ self.hass,
+ SIGNAL_TAG_UPDATE.format(self.tag_id),
+ self._update_tag_info_callback)
+
+ @property
+ def entity_id(self):
+ """Overriden version."""
+ return self._entity_id
+
+ @property
+ def underscored_name(self):
+ """Provide name savvy to be used in entity_id name of self."""
+ return self.name.lower().replace(" ", "_")
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ @property
+ def device_class(self):
+ """Return the class of the sensor."""
+ return self._sensor_type
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return self._unit_of_measurement
+
+ @property
+ def principal_value(self):
+ """Return sensor current value."""
+ return getattr(self._tag, self._tag_attr, False)
+
+ @callback
+ def _update_tag_info_callback(self, event):
+ """Handle push notification sent by tag manager."""
+ if event.data.get('id') != self.tag_id:
+ return
+
+ _LOGGER.info("Entity to update state: %s event data: %s",
+ self, event.data)
+ new_value = self.principal_value
+ try:
+ if self._sensor_type == SENSOR_TEMPERATURE:
+ new_value = event.data.get('temp')
+ elif (self._sensor_type == SENSOR_HUMIDITY or
+ self._sensor_type == SENSOR_MOISTURE):
+ new_value = event.data.get('cap')
+ elif self._sensor_type == SENSOR_LIGHT:
+ new_value = event.data.get('lux')
+ except Exception as error: # pylint: disable=W0703
+ _LOGGER.info("Unable to update value of entity: \
+ %s error: %s event: %s", self, error, event)
+
+ self._state = self.decorate_value(new_value)
+ self.async_schedule_update_ha_state()
diff --git a/homeassistant/components/sensor/worldtidesinfo.py b/homeassistant/components/sensor/worldtidesinfo.py
index 8884d790eed036..05d61173da00f1 100644
--- a/homeassistant/components/sensor/worldtidesinfo.py
+++ b/homeassistant/components/sensor/worldtidesinfo.py
@@ -31,7 +31,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the WorldTidesInfo sensor."""
name = config.get(CONF_NAME)
diff --git a/homeassistant/components/sensor/xbox_live.py b/homeassistant/components/sensor/xbox_live.py
index 0c7b8b48f624ce..250c74ee4933c4 100644
--- a/homeassistant/components/sensor/xbox_live.py
+++ b/homeassistant/components/sensor/xbox_live.py
@@ -27,7 +27,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Xbox platform."""
from xboxapi import xbox_api
diff --git a/homeassistant/components/sensor/xiaomi_miio.py b/homeassistant/components/sensor/xiaomi_miio.py
index 066dc384007f5b..a70d701fac639b 100644
--- a/homeassistant/components/sensor/xiaomi_miio.py
+++ b/homeassistant/components/sensor/xiaomi_miio.py
@@ -25,7 +25,7 @@
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
-REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
+REQUIREMENTS = ['python-miio==0.4.0', 'construct==2.9.41']
ATTR_POWER = 'power'
ATTR_CHARGING = 'charging'
@@ -36,7 +36,6 @@
SUCCESS = ['ok']
-# pylint: disable=unused-argument
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the sensor from config."""
diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml
index c0279ef1d0f28e..6b8bded59b83a8 100644
--- a/homeassistant/components/services.yaml
+++ b/homeassistant/components/services.yaml
@@ -175,6 +175,12 @@ ffmpeg:
example: 'binary_sensor.ffmpeg_noise'
logger:
+ set_default_level:
+ description: Set the default log level for components.
+ fields:
+ level:
+ description: Default severity level. Possible values are notset, debug, info, warn, warning, error, fatal, critical
+ example: 'debug'
set_level:
description: Set log level for components.
@@ -570,3 +576,28 @@ shopping_list:
name:
description: The name of the item to mark as completed.
example: Beer
+
+nest:
+ set_mode:
+ description: >
+ Set the home/away mode for a Nest structure.
+ Set to away mode will also set Estimated Arrival Time if provided.
+ Set ETA will cause the thermostat to begin warming or cooling the home before the user arrives.
+ After ETA set other Automation can read ETA sensor as a signal to prepare the home for
+ the user's arrival.
+ fields:
+ home_mode:
+ description: home or away
+ example: home
+ structure:
+ description: Optional structure name. Default set all structures managed by Home Assistant.
+ example: My Home
+ eta:
+ description: Optional Estimated Arrival Time from now.
+ example: 0:10
+ eta_window:
+ description: Optional ETA window. Default is 1 minute.
+ example: 0:5
+ trip_id:
+ description: Optional identity of a trip. Using the same trip_ID will update the estimation.
+ example: trip_back_home
diff --git a/homeassistant/components/sleepiq.py b/homeassistant/components/sleepiq.py
index 3b74b79b36b018..df36eef2f9ef9a 100644
--- a/homeassistant/components/sleepiq.py
+++ b/homeassistant/components/sleepiq.py
@@ -51,7 +51,6 @@ def setup(hass, config):
Will automatically load sensor components to support
devices discovered on the account.
"""
- # pylint: disable=global-statement
global DATA
from sleepyq import Sleepyq
diff --git a/homeassistant/components/sonos/.translations/ca.json b/homeassistant/components/sonos/.translations/ca.json
new file mode 100644
index 00000000000000..9a745784b25fd2
--- /dev/null
+++ b/homeassistant/components/sonos/.translations/ca.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "No s'han trobat dispositius Sonos a la xarxa.",
+ "single_instance_allowed": "Nom\u00e9s cal una \u00fanica configuraci\u00f3 de Sonos."
+ },
+ "step": {
+ "confirm": {
+ "description": "Voleu configurar Sonos?",
+ "title": "Sonos"
+ }
+ },
+ "title": "Sonos"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sonos/.translations/en.json b/homeassistant/components/sonos/.translations/en.json
new file mode 100644
index 00000000000000..c7aae4302f6bea
--- /dev/null
+++ b/homeassistant/components/sonos/.translations/en.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "No Sonos devices found on the network.",
+ "single_instance_allowed": "Only a single configuration of Sonos is necessary."
+ },
+ "step": {
+ "confirm": {
+ "description": "Do you want to setup Sonos?",
+ "title": "Sonos"
+ }
+ },
+ "title": "Sonos"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sonos/.translations/ko.json b/homeassistant/components/sonos/.translations/ko.json
new file mode 100644
index 00000000000000..5453e4322cd094
--- /dev/null
+++ b/homeassistant/components/sonos/.translations/ko.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Sonos \uc7a5\uce58\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.",
+ "single_instance_allowed": "Sonos\uc758 \ub2e8\uc77c \uad6c\uc131 \ub9cc \ud544\uc694\ud569\ub2c8\ub2e4."
+ },
+ "step": {
+ "confirm": {
+ "description": "Sonos\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
+ "title": "Sonos"
+ }
+ },
+ "title": "Sonos"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sonos/.translations/no.json b/homeassistant/components/sonos/.translations/no.json
new file mode 100644
index 00000000000000..c837abad499db4
--- /dev/null
+++ b/homeassistant/components/sonos/.translations/no.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Ingen Sonos enheter funnet p\u00e5 nettverket.",
+ "single_instance_allowed": "Kun en enkelt konfigurasjon av Sonos er n\u00f8dvendig."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u00d8nsker du \u00e5 sette opp Sonos?",
+ "title": "Sonos"
+ }
+ },
+ "title": "Sonos"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sonos/.translations/pl.json b/homeassistant/components/sonos/.translations/pl.json
new file mode 100644
index 00000000000000..2a0c526b9a64ac
--- /dev/null
+++ b/homeassistant/components/sonos/.translations/pl.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Nie znaleziono w sieci urz\u0105dze\u0144 Sonos.",
+ "single_instance_allowed": "Wymagana jest tylko jedna konfiguracja Sonos."
+ },
+ "step": {
+ "confirm": {
+ "description": "Chcesz skonfigurowa\u0107 Sonos?",
+ "title": "Sonos"
+ }
+ },
+ "title": "Sonos"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sonos/.translations/ru.json b/homeassistant/components/sonos/.translations/ru.json
new file mode 100644
index 00000000000000..63b6bd87c20b47
--- /dev/null
+++ b/homeassistant/components/sonos/.translations/ru.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Sonos \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.",
+ "single_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f Sonos."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Sonos?",
+ "title": "Sonos"
+ }
+ },
+ "title": "Sonos"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sonos/.translations/sv.json b/homeassistant/components/sonos/.translations/sv.json
new file mode 100644
index 00000000000000..756fe8a74832d2
--- /dev/null
+++ b/homeassistant/components/sonos/.translations/sv.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Inga Sonos-enheter hittades i n\u00e4tverket.",
+ "single_instance_allowed": "Endast en enda konfiguration av Sonos \u00e4r n\u00f6dv\u00e4ndig."
+ },
+ "step": {
+ "confirm": {
+ "description": "Vill du konfigurera Sonos?",
+ "title": "Sonos"
+ }
+ },
+ "title": "Sonos"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sonos/.translations/vi.json b/homeassistant/components/sonos/.translations/vi.json
new file mode 100644
index 00000000000000..ebeb1a8b07ce31
--- /dev/null
+++ b/homeassistant/components/sonos/.translations/vi.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Kh\u00f4ng t\u00ecm th\u1ea5y thi\u1ebft b\u1ecb Sonos n\u00e0o tr\u00ean m\u1ea1ng.",
+ "single_instance_allowed": "Ch\u1ec9 c\u1ea7n m\u1ed9t c\u1ea5u h\u00ecnh duy nh\u1ea5t c\u1ee7a Sonos l\u00e0 \u0111\u1ee7."
+ },
+ "step": {
+ "confirm": {
+ "description": "B\u1ea1n c\u00f3 mu\u1ed1n thi\u1ebft l\u1eadp Sonos kh\u00f4ng?",
+ "title": "Sonos"
+ }
+ },
+ "title": "Sonos"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sonos/.translations/zh-Hans.json b/homeassistant/components/sonos/.translations/zh-Hans.json
new file mode 100644
index 00000000000000..17c1e78d3e8922
--- /dev/null
+++ b/homeassistant/components/sonos/.translations/zh-Hans.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u6ca1\u6709\u5728\u7f51\u7edc\u4e0a\u627e\u5230 Sonos \u8bbe\u5907\u3002",
+ "single_instance_allowed": "\u53ea\u6709\u4e00\u6b21 Sonos \u914d\u7f6e\u662f\u5fc5\u8981\u7684\u3002"
+ },
+ "step": {
+ "confirm": {
+ "description": "\u60a8\u60f3\u8981\u914d\u7f6e Sonos \u5417\uff1f",
+ "title": "Sonos"
+ }
+ },
+ "title": "Sonos"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py
new file mode 100644
index 00000000000000..7c3de210768152
--- /dev/null
+++ b/homeassistant/components/sonos/__init__.py
@@ -0,0 +1,29 @@
+"""Component to embed Sonos."""
+from homeassistant.helpers import config_entry_flow
+
+
+DOMAIN = 'sonos'
+REQUIREMENTS = ['SoCo==0.14']
+
+
+async def async_setup(hass, config):
+ """Set up the Sonos component."""
+ hass.data[DOMAIN] = config.get(DOMAIN, {})
+ return True
+
+
+async def async_setup_entry(hass, entry):
+ """Set up Sonos from a config entry."""
+ hass.async_add_job(hass.config_entries.async_forward_entry_setup(
+ entry, 'media_player'))
+ return True
+
+
+async def _async_has_devices(hass):
+ """Return if there are devices that can be discovered."""
+ import soco
+
+ return await hass.async_add_job(soco.discover)
+
+
+config_entry_flow.register_discovery_flow(DOMAIN, 'Sonos', _async_has_devices)
diff --git a/homeassistant/components/sonos/strings.json b/homeassistant/components/sonos/strings.json
new file mode 100644
index 00000000000000..4aa68712d599e7
--- /dev/null
+++ b/homeassistant/components/sonos/strings.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "title": "Sonos",
+ "step": {
+ "confirm": {
+ "title": "Sonos",
+ "description": "Do you want to setup Sonos?"
+ }
+ },
+ "abort": {
+ "single_instance_allowed": "Only a single configuration of Sonos is necessary.",
+ "no_devices_found": "No Sonos devices found on the network."
+ }
+ }
+}
diff --git a/homeassistant/components/switch/bbb_gpio.py b/homeassistant/components/switch/bbb_gpio.py
index 6dc5df4ffe3513..5412f559b7391a 100644
--- a/homeassistant/components/switch/bbb_gpio.py
+++ b/homeassistant/components/switch/bbb_gpio.py
@@ -34,7 +34,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the BeagleBone Black GPIO devices."""
pins = config.get(CONF_PINS)
diff --git a/homeassistant/components/switch/command_line.py b/homeassistant/components/switch/command_line.py
index 478b1c6e9adf58..127c7578940f40 100644
--- a/homeassistant/components/switch/command_line.py
+++ b/homeassistant/components/switch/command_line.py
@@ -32,7 +32,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Find and return switches controlled by shell commands."""
devices = config.get(CONF_SWITCHES, {})
diff --git a/homeassistant/components/switch/deluge.py b/homeassistant/components/switch/deluge.py
index da0b3bf3228966..c71c3865f5dc19 100644
--- a/homeassistant/components/switch/deluge.py
+++ b/homeassistant/components/switch/deluge.py
@@ -32,7 +32,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Deluge switch."""
from deluge_client import DelugeRPCClient
diff --git a/homeassistant/components/switch/demo.py b/homeassistant/components/switch/demo.py
index 83b8ae796bb058..7e22f962330d5b 100644
--- a/homeassistant/components/switch/demo.py
+++ b/homeassistant/components/switch/demo.py
@@ -8,7 +8,6 @@
from homeassistant.const import DEVICE_DEFAULT_NAME
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Set up the demo switches."""
add_devices_callback([
diff --git a/homeassistant/components/switch/dlink.py b/homeassistant/components/switch/dlink.py
index 5d727e72138db7..1c7253c4ec378f 100644
--- a/homeassistant/components/switch/dlink.py
+++ b/homeassistant/components/switch/dlink.py
@@ -36,7 +36,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up a D-Link Smart Plug."""
from pyW215.pyW215 import SmartPlug
diff --git a/homeassistant/components/switch/doorbird.py b/homeassistant/components/switch/doorbird.py
index 9886b3a586d04a..92ba3640237edb 100644
--- a/homeassistant/components/switch/doorbird.py
+++ b/homeassistant/components/switch/doorbird.py
@@ -4,10 +4,10 @@
import voluptuous as vol
+import homeassistant.helpers.config_validation as cv
from homeassistant.components.doorbird import DOMAIN as DOORBIRD_DOMAIN
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice
from homeassistant.const import CONF_SWITCHES
-import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['doorbird']
@@ -15,7 +15,7 @@
SWITCHES = {
"open_door": {
- "name": "Open Door",
+ "name": "{} Open Door",
"icon": {
True: "lock-open",
False: "lock"
@@ -23,7 +23,7 @@
"time": datetime.timedelta(seconds=3)
},
"open_door_2": {
- "name": "Open Door 2",
+ "name": "{} Open Door 2",
"icon": {
True: "lock-open",
False: "lock"
@@ -31,7 +31,7 @@
"time": datetime.timedelta(seconds=3)
},
"light_on": {
- "name": "Light On",
+ "name": "{} Light On",
"icon": {
True: "lightbulb-on",
False: "lightbulb"
@@ -48,31 +48,36 @@
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the DoorBird switch platform."""
- device = hass.data.get(DOORBIRD_DOMAIN)
-
switches = []
- for switch in SWITCHES:
- _LOGGER.debug("Adding DoorBird switch %s", SWITCHES[switch]["name"])
- switches.append(DoorBirdSwitch(device, switch))
+
+ for doorstation in hass.data[DOORBIRD_DOMAIN]:
+
+ device = doorstation.device
+
+ for switch in SWITCHES:
+
+ _LOGGER.debug("Adding DoorBird switch %s",
+ SWITCHES[switch]["name"].format(doorstation.name))
+ switches.append(DoorBirdSwitch(device, switch, doorstation.name))
add_devices(switches)
- _LOGGER.info("Added DoorBird switches")
class DoorBirdSwitch(SwitchDevice):
"""A relay in a DoorBird device."""
- def __init__(self, device, switch):
+ def __init__(self, device, switch, name):
"""Initialize a relay in a DoorBird device."""
self._device = device
self._switch = switch
+ self._name = name
self._state = False
self._assume_off = datetime.datetime.min
@property
def name(self):
"""Return the name of the switch."""
- return SWITCHES[self._switch]["name"]
+ return SWITCHES[self._switch]["name"].format(self._name)
@property
def icon(self):
diff --git a/homeassistant/components/switch/edimax.py b/homeassistant/components/switch/edimax.py
index 40ebb54b603074..9cd7c48488649d 100644
--- a/homeassistant/components/switch/edimax.py
+++ b/homeassistant/components/switch/edimax.py
@@ -29,7 +29,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Find and return Edimax Smart Plugs."""
from pyedimax.smartplug import SmartPlug
diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py
index 21689dcca0fa5a..f57843cdaa0f7f 100644
--- a/homeassistant/components/switch/flux.py
+++ b/homeassistant/components/switch/flux.py
@@ -95,7 +95,6 @@ def set_lights_rgb(hass, lights, rgb, transition):
transition=transition)
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Flux switches."""
name = config.get(CONF_NAME)
diff --git a/homeassistant/components/switch/gc100.py b/homeassistant/components/switch/gc100.py
index f4175926aa0610..54c3b5e942aeae 100644
--- a/homeassistant/components/switch/gc100.py
+++ b/homeassistant/components/switch/gc100.py
@@ -23,7 +23,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the GC100 devices."""
switches = []
diff --git a/homeassistant/components/switch/insteon_plm.py b/homeassistant/components/switch/insteon_plm.py
index be562e9d909d67..42b4829f64ecd0 100644
--- a/homeassistant/components/switch/insteon_plm.py
+++ b/homeassistant/components/switch/insteon_plm.py
@@ -30,7 +30,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
device.address.hex, device.states[state_key].name)
new_entity = None
- if state_name in ['lightOnOff', 'outletTopOnOff', 'outletBottomOnOff']:
+ if state_name in ['lightOnOff', 'outletTopOnOff', 'outletBottomOnOff',
+ 'x10OnOffSwitch']:
new_entity = InsteonPLMSwitchDevice(device, state_key)
elif state_name == 'openClosedRelay':
new_entity = InsteonPLMOpenClosedDevice(device, state_key)
diff --git a/homeassistant/components/switch/isy994.py b/homeassistant/components/switch/isy994.py
index efdda6ed40cb79..3d29c53bd7cb08 100644
--- a/homeassistant/components/switch/isy994.py
+++ b/homeassistant/components/switch/isy994.py
@@ -15,7 +15,6 @@
_LOGGER = logging.getLogger(__name__)
-# pylint: disable=unused-argument
def setup_platform(hass, config: ConfigType,
add_devices: Callable[[list], None], discovery_info=None):
"""Set up the ISY994 switch platform."""
diff --git a/homeassistant/components/switch/kankun.py b/homeassistant/components/switch/kankun.py
index 88a07b68cd94cb..c830e2299f6ce4 100644
--- a/homeassistant/components/switch/kankun.py
+++ b/homeassistant/components/switch/kankun.py
@@ -34,7 +34,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Set up Kankun Wifi switches."""
switches = config.get('switches', {})
diff --git a/homeassistant/components/switch/knx.py b/homeassistant/components/switch/knx.py
index a96f96a9c5c242..c13631ca5e67c2 100644
--- a/homeassistant/components/switch/knx.py
+++ b/homeassistant/components/switch/knx.py
@@ -72,7 +72,6 @@ def async_register_callbacks(self):
"""Register callbacks to update hass after device was changed."""
async def after_update_callback(device):
"""Call after device was updated."""
- # pylint: disable=unused-argument
await self.async_update_ha_state()
self.device.register_device_updated_cb(after_update_callback)
diff --git a/homeassistant/components/switch/linode.py b/homeassistant/components/switch/linode.py
index 91177e321169ab..43f4bdc31b4b72 100644
--- a/homeassistant/components/switch/linode.py
+++ b/homeassistant/components/switch/linode.py
@@ -51,35 +51,23 @@ def __init__(self, li, node_id):
self._node_id = node_id
self.data = None
self._state = None
+ self._attrs = {}
+ self._name = None
@property
def name(self):
"""Return the name of the switch."""
- if self.data is not None:
- return self.data.label
+ return self._name
@property
def is_on(self):
"""Return true if switch is on."""
- if self.data is not None:
- return self.data.status == 'running'
- return False
+ return self._state
@property
def device_state_attributes(self):
"""Return the state attributes of the Linode Node."""
- if self.data:
- return {
- ATTR_CREATED: self.data.created,
- ATTR_NODE_ID: self.data.id,
- ATTR_NODE_NAME: self.data.label,
- ATTR_IPV4_ADDRESS: self.data.ipv4,
- ATTR_IPV6_ADDRESS: self.data.ipv6,
- ATTR_MEMORY: self.data.specs.memory,
- ATTR_REGION: self.data.region.country,
- ATTR_VCPUS: self.data.specs.vcpus,
- }
- return {}
+ return self._attrs
def turn_on(self, **kwargs):
"""Boot-up the Node."""
@@ -98,3 +86,16 @@ def update(self):
for node in self._linode.data:
if node.id == self._node_id:
self.data = node
+ if self.data is not None:
+ self._state = self.data.status == 'running'
+ self._attrs = {
+ ATTR_CREATED: self.data.created,
+ ATTR_NODE_ID: self.data.id,
+ ATTR_NODE_NAME: self.data.label,
+ ATTR_IPV4_ADDRESS: self.data.ipv4,
+ ATTR_IPV6_ADDRESS: self.data.ipv6,
+ ATTR_MEMORY: self.data.specs.memory,
+ ATTR_REGION: self.data.region.country,
+ ATTR_VCPUS: self.data.specs.vcpus,
+ }
+ self._name = self.data.label
diff --git a/homeassistant/components/switch/lutron_caseta.py b/homeassistant/components/switch/lutron_caseta.py
index da36c76f41dbce..f5e7cf2836f892 100644
--- a/homeassistant/components/switch/lutron_caseta.py
+++ b/homeassistant/components/switch/lutron_caseta.py
@@ -16,7 +16,6 @@
DEPENDENCIES = ['lutron_caseta']
-# pylint: disable=unused-argument
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up Lutron switch."""
diff --git a/homeassistant/components/switch/mystrom.py b/homeassistant/components/switch/mystrom.py
index 0a87d41d2fefa1..85fc546d00e591 100644
--- a/homeassistant/components/switch/mystrom.py
+++ b/homeassistant/components/switch/mystrom.py
@@ -12,7 +12,7 @@
from homeassistant.const import (CONF_NAME, CONF_HOST)
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['python-mystrom==0.4.2']
+REQUIREMENTS = ['python-mystrom==0.4.4']
DEFAULT_NAME = 'myStrom Switch'
@@ -34,8 +34,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
try:
MyStromPlug(host).get_status()
except exceptions.MyStromConnectionError:
- _LOGGER.error("No route to device '%s'", host)
- return False
+ _LOGGER.error("No route to device: %s", host)
+ return
add_devices([MyStromSwitch(name, host)])
@@ -74,8 +74,7 @@ def turn_on(self, **kwargs):
try:
self.plug.set_relay_on()
except exceptions.MyStromConnectionError:
- _LOGGER.error("No route to device '%s'. Is device offline?",
- self._resource)
+ _LOGGER.error("No route to device: %s", self._resource)
def turn_off(self, **kwargs):
"""Turn the switch off."""
@@ -83,8 +82,7 @@ def turn_off(self, **kwargs):
try:
self.plug.set_relay_off()
except exceptions.MyStromConnectionError:
- _LOGGER.error("No route to device '%s'. Is device offline?",
- self._resource)
+ _LOGGER.error("No route to device: %s", self._resource)
def update(self):
"""Get the latest data from the device and update the data."""
@@ -93,5 +91,4 @@ def update(self):
self.data = self.plug.get_status()
except exceptions.MyStromConnectionError:
self.data = {'power': 0, 'relay': False}
- _LOGGER.error("No route to device '%s'. Is device offline?",
- self._resource)
+ _LOGGER.error("No route to device: %s", self._resource)
diff --git a/homeassistant/components/switch/neato.py b/homeassistant/components/switch/neato.py
index a797abb47fcf9d..1d149383f6fad6 100644
--- a/homeassistant/components/switch/neato.py
+++ b/homeassistant/components/switch/neato.py
@@ -5,10 +5,12 @@
https://home-assistant.io/components/switch.neato/
"""
import logging
+from datetime import timedelta
import requests
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.components.neato import NEATO_ROBOTS, NEATO_LOGIN
+from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -50,6 +52,7 @@ def __init__(self, hass, robot, switch_type):
self._schedule_state = None
self._clean_state = None
+ @Throttle(timedelta(seconds=60))
def update(self):
"""Update the states of Neato switches."""
_LOGGER.debug("Running switch update")
diff --git a/homeassistant/components/switch/orvibo.py b/homeassistant/components/switch/orvibo.py
index e039a29809d4ed..fdb4752f594432 100644
--- a/homeassistant/components/switch/orvibo.py
+++ b/homeassistant/components/switch/orvibo.py
@@ -31,7 +31,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Set up S20 switches."""
from orvibo.s20 import discover, S20, S20Exception
diff --git a/homeassistant/components/switch/pulseaudio_loopback.py b/homeassistant/components/switch/pulseaudio_loopback.py
index 007e74e14fd37c..e25368f3c5cffc 100644
--- a/homeassistant/components/switch/pulseaudio_loopback.py
+++ b/homeassistant/components/switch/pulseaudio_loopback.py
@@ -54,7 +54,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Read in all of our configuration, and initialize the loopback switch."""
name = config.get(CONF_NAME)
diff --git a/homeassistant/components/switch/rainmachine.py b/homeassistant/components/switch/rainmachine.py
index bdee64a3d54ac0..b0cdf334cfa127 100644
--- a/homeassistant/components/switch/rainmachine.py
+++ b/homeassistant/components/switch/rainmachine.py
@@ -8,12 +8,12 @@
from homeassistant.components.rainmachine import (
CONF_ZONE_RUN_TIME, DATA_RAINMACHINE, DEFAULT_ZONE_RUN,
- PROGRAM_UPDATE_TOPIC, RainMachineEntity)
+ PROGRAM_UPDATE_TOPIC, ZONE_UPDATE_TOPIC, RainMachineEntity)
from homeassistant.const import ATTR_ID
from homeassistant.components.switch import SwitchDevice
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import (
- async_dispatcher_connect, dispatcher_send)
+ async_dispatcher_connect, async_dispatcher_send)
DEPENDENCIES = ['rainmachine']
@@ -39,20 +39,11 @@
ATTR_ZONES = 'zones'
DAYS = [
- 'Monday',
- 'Tuesday',
- 'Wednesday',
- 'Thursday',
- 'Friday',
- 'Saturday',
+ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday',
'Sunday'
]
-PROGRAM_STATUS_MAP = {
- 0: 'Not Running',
- 1: 'Running',
- 2: 'Queued'
-}
+PROGRAM_STATUS_MAP = {0: 'Not Running', 1: 'Running', 2: 'Queued'}
SOIL_TYPE_MAP = {
0: 'Not Set',
@@ -108,7 +99,8 @@
}
-def setup_platform(hass, config, add_devices, discovery_info=None):
+async def async_setup_platform(
+ hass, config, async_add_devices, discovery_info=None):
"""Set up the RainMachine Switch platform."""
if discovery_info is None:
return
@@ -120,21 +112,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
rainmachine = hass.data[DATA_RAINMACHINE]
entities = []
- for program in rainmachine.client.programs.all().get('programs', {}):
+
+ programs = await rainmachine.client.programs.all()
+ for program in programs:
if not program.get('active'):
continue
_LOGGER.debug('Adding program: %s', program)
entities.append(RainMachineProgram(rainmachine, program))
- for zone in rainmachine.client.zones.all().get('zones', {}):
+ zones = await rainmachine.client.zones.all()
+ for zone in zones:
if not zone.get('active'):
continue
_LOGGER.debug('Adding zone: %s', zone)
entities.append(RainMachineZone(rainmachine, zone, zone_run_time))
- add_devices(entities, True)
+ async_add_devices(entities, True)
class RainMachineSwitch(RainMachineEntity, SwitchDevice):
@@ -163,10 +158,14 @@ def is_enabled(self) -> bool:
def unique_id(self) -> str:
"""Return a unique, HASS-friendly identifier for this entity."""
return '{0}_{1}_{2}'.format(
- self.rainmachine.device_mac.replace(':', ''),
- self._switch_type,
+ self.rainmachine.device_mac.replace(':', ''), self._switch_type,
self._rainmachine_entity_id)
+ @callback
+ def _program_updated(self):
+ """Update state, trigger updates."""
+ self.async_schedule_update_ha_state(True)
+
class RainMachineProgram(RainMachineSwitch):
"""A RainMachine program."""
@@ -185,34 +184,42 @@ def zones(self) -> list:
"""Return a list of active zones associated with this program."""
return [z for z in self._obj['wateringTimes'] if z['active']]
- def turn_off(self, **kwargs) -> None:
+ async def async_added_to_hass(self):
+ """Register callbacks."""
+ async_dispatcher_connect(
+ self.hass, PROGRAM_UPDATE_TOPIC, self._program_updated)
+
+ async def async_turn_off(self, **kwargs) -> None:
"""Turn the program off."""
- from regenmaschine.exceptions import RainMachineError
+ from regenmaschine.errors import RequestError
try:
- self.rainmachine.client.programs.stop(self._rainmachine_entity_id)
- dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC)
- except RainMachineError as exc_info:
- _LOGGER.error('Unable to turn off program "%s"', self.unique_id)
- _LOGGER.debug(exc_info)
+ await self.rainmachine.client.programs.stop(
+ self._rainmachine_entity_id)
+ async_dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC)
+ except RequestError as err:
+ _LOGGER.error(
+ 'Unable to turn off program "%s": %s', self.unique_id,
+ str(err))
- def turn_on(self, **kwargs) -> None:
+ async def async_turn_on(self, **kwargs) -> None:
"""Turn the program on."""
- from regenmaschine.exceptions import RainMachineError
+ from regenmaschine.errors import RequestError
try:
- self.rainmachine.client.programs.start(self._rainmachine_entity_id)
- dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC)
- except RainMachineError as exc_info:
- _LOGGER.error('Unable to turn on program "%s"', self.unique_id)
- _LOGGER.debug(exc_info)
+ await self.rainmachine.client.programs.start(
+ self._rainmachine_entity_id)
+ async_dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC)
+ except RequestError as err:
+ _LOGGER.error(
+ 'Unable to turn on program "%s": %s', self.unique_id, str(err))
- def update(self) -> None:
+ async def async_update(self) -> None:
"""Update info for the program."""
- from regenmaschine.exceptions import RainMachineError
+ from regenmaschine.errors import RequestError
try:
- self._obj = self.rainmachine.client.programs.get(
+ self._obj = await self.rainmachine.client.programs.get(
self._rainmachine_entity_id)
self._attrs.update({
@@ -221,10 +228,10 @@ def update(self) -> None:
ATTR_STATUS: PROGRAM_STATUS_MAP[self._obj.get('status')],
ATTR_ZONES: ', '.join(z['name'] for z in self.zones)
})
- except RainMachineError as exc_info:
- _LOGGER.error('Unable to update info for program "%s"',
- self.unique_id)
- _LOGGER.debug(exc_info)
+ except RequestError as err:
+ _LOGGER.error(
+ 'Unable to update info for program "%s": %s', self.unique_id,
+ str(err))
class RainMachineZone(RainMachineSwitch):
@@ -242,62 +249,65 @@ def is_on(self) -> bool:
"""Return whether the zone is running."""
return bool(self._obj.get('state'))
- @callback
- def _program_updated(self):
- """Update state, trigger updates."""
- self.async_schedule_update_ha_state(True)
-
async def async_added_to_hass(self):
"""Register callbacks."""
- async_dispatcher_connect(self.hass, PROGRAM_UPDATE_TOPIC,
- self._program_updated)
+ async_dispatcher_connect(
+ self.hass, PROGRAM_UPDATE_TOPIC, self._program_updated)
+ async_dispatcher_connect(
+ self.hass, ZONE_UPDATE_TOPIC, self._program_updated)
- def turn_off(self, **kwargs) -> None:
+ async def async_turn_off(self, **kwargs) -> None:
"""Turn the zone off."""
- from regenmaschine.exceptions import RainMachineError
+ from regenmaschine.errors import RequestError
try:
- self.rainmachine.client.zones.stop(self._rainmachine_entity_id)
- except RainMachineError as exc_info:
- _LOGGER.error('Unable to turn off zone "%s"', self.unique_id)
- _LOGGER.debug(exc_info)
+ await self.rainmachine.client.zones.stop(
+ self._rainmachine_entity_id)
+ except RequestError as err:
+ _LOGGER.error(
+ 'Unable to turn off zone "%s": %s', self.unique_id, str(err))
- def turn_on(self, **kwargs) -> None:
+ async def async_turn_on(self, **kwargs) -> None:
"""Turn the zone on."""
- from regenmaschine.exceptions import RainMachineError
+ from regenmaschine.errors import RequestError
try:
- self.rainmachine.client.zones.start(self._rainmachine_entity_id,
- self._run_time)
- except RainMachineError as exc_info:
- _LOGGER.error('Unable to turn on zone "%s"', self.unique_id)
- _LOGGER.debug(exc_info)
+ await self.rainmachine.client.zones.start(
+ self._rainmachine_entity_id, self._run_time)
+ except RequestError as err:
+ _LOGGER.error(
+ 'Unable to turn on zone "%s": %s', self.unique_id, str(err))
- def update(self) -> None:
+ async def async_update(self) -> None:
"""Update info for the zone."""
- from regenmaschine.exceptions import RainMachineError
+ from regenmaschine.errors import RequestError
try:
- self._obj = self.rainmachine.client.zones.get(
+ self._obj = await self.rainmachine.client.zones.get(
self._rainmachine_entity_id)
- self._properties_json = self.rainmachine.client.zones.get(
- self._rainmachine_entity_id, properties=True)
+ self._properties_json = await self.rainmachine.client.zones.get(
+ self._rainmachine_entity_id, details=True)
self._attrs.update({
- ATTR_ID: self._obj['uid'],
- ATTR_AREA: self._properties_json.get('waterSense').get('area'),
- ATTR_CURRENT_CYCLE: self._obj.get('cycle'),
+ ATTR_ID:
+ self._obj['uid'],
+ ATTR_AREA:
+ self._properties_json.get('waterSense').get('area'),
+ ATTR_CURRENT_CYCLE:
+ self._obj.get('cycle'),
ATTR_FIELD_CAPACITY:
- self._properties_json.get(
- 'waterSense').get('fieldCapacity'),
- ATTR_NO_CYCLES: self._obj.get('noOfCycles'),
+ self._properties_json.get('waterSense')
+ .get('fieldCapacity'),
+ ATTR_NO_CYCLES:
+ self._obj.get('noOfCycles'),
ATTR_PRECIP_RATE:
- self._properties_json.get(
- 'waterSense').get('precipitationRate'),
- ATTR_RESTRICTIONS: self._obj.get('restriction'),
- ATTR_SLOPE: SLOPE_TYPE_MAP.get(
- self._properties_json.get('slope')),
+ self._properties_json.get('waterSense')
+ .get('precipitationRate'),
+ ATTR_RESTRICTIONS:
+ self._obj.get('restriction'),
+ ATTR_SLOPE:
+ SLOPE_TYPE_MAP.get(self._properties_json.get('slope')),
ATTR_SOIL_TYPE:
SOIL_TYPE_MAP.get(self._properties_json.get('sun')),
ATTR_SPRINKLER_TYPE:
@@ -308,7 +318,7 @@ def update(self) -> None:
ATTR_VEGETATION_TYPE:
VEGETATION_MAP.get(self._obj.get('type')),
})
- except RainMachineError as exc_info:
- _LOGGER.error('Unable to update info for zone "%s"',
- self.unique_id)
- _LOGGER.debug(exc_info)
+ except RequestError as err:
+ _LOGGER.error(
+ 'Unable to update info for zone "%s": %s', self.unique_id,
+ str(err))
diff --git a/homeassistant/components/switch/raspihats.py b/homeassistant/components/switch/raspihats.py
index 7be3a6f0baafe6..7173ad35dafbfd 100644
--- a/homeassistant/components/switch/raspihats.py
+++ b/homeassistant/components/switch/raspihats.py
@@ -39,7 +39,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the raspihats switch devices."""
I2CHatSwitch.I2C_HATS_MANAGER = hass.data[I2C_HATS_MANAGER]
diff --git a/homeassistant/components/switch/rest.py b/homeassistant/components/switch/rest.py
index 9c589d1d95b9eb..914408406a9e80 100644
--- a/homeassistant/components/switch/rest.py
+++ b/homeassistant/components/switch/rest.py
@@ -47,7 +47,6 @@
})
-# pylint: disable=unused-argument
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the RESTful switch."""
diff --git a/homeassistant/components/switch/rpi_gpio.py b/homeassistant/components/switch/rpi_gpio.py
index ac38da1c6a7b9d..26de2a78e1899a 100644
--- a/homeassistant/components/switch/rpi_gpio.py
+++ b/homeassistant/components/switch/rpi_gpio.py
@@ -34,7 +34,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Raspberry PI GPIO devices."""
invert_logic = config.get(CONF_INVERT_LOGIC)
diff --git a/homeassistant/components/switch/rpi_rf.py b/homeassistant/components/switch/rpi_rf.py
index 40200f05806343..62c92ad2d968c6 100644
--- a/homeassistant/components/switch/rpi_rf.py
+++ b/homeassistant/components/switch/rpi_rf.py
@@ -44,7 +44,7 @@
})
-# pylint: disable=unused-argument, import-error, no-member
+# pylint: disable=import-error, no-member
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Find and return switches controlled by a generic RF device via GPIO."""
import rpi_rf
diff --git a/homeassistant/components/switch/tellstick.py b/homeassistant/components/switch/tellstick.py
index ae19e77c2e5a92..5f7930a8a7c47a 100644
--- a/homeassistant/components/switch/tellstick.py
+++ b/homeassistant/components/switch/tellstick.py
@@ -10,7 +10,6 @@
from homeassistant.helpers.entity import ToggleEntity
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Tellstick switches."""
if (discovery_info is None or
diff --git a/homeassistant/components/switch/telnet.py b/homeassistant/components/switch/telnet.py
index c3a608b96924a1..381f2ec9bec829 100644
--- a/homeassistant/components/switch/telnet.py
+++ b/homeassistant/components/switch/telnet.py
@@ -38,7 +38,6 @@
SCAN_INTERVAL = timedelta(seconds=10)
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Find and return switches controlled by telnet commands."""
devices = config.get(CONF_SWITCHES, {})
diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py
index 93ebf98e9ace67..a6fa8241940b1c 100644
--- a/homeassistant/components/switch/template.py
+++ b/homeassistant/components/switch/template.py
@@ -44,7 +44,6 @@
@asyncio.coroutine
-# pylint: disable=unused-argument
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Template switch."""
switches = []
diff --git a/homeassistant/components/switch/tplink.py b/homeassistant/components/switch/tplink.py
index 1eca5284f76f2d..46682d87356c6f 100644
--- a/homeassistant/components/switch/tplink.py
+++ b/homeassistant/components/switch/tplink.py
@@ -14,7 +14,7 @@
from homeassistant.const import (CONF_HOST, CONF_NAME, ATTR_VOLTAGE)
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['pyHS100==0.3.0']
+REQUIREMENTS = ['pyHS100==0.3.1']
_LOGGER = logging.getLogger(__name__)
@@ -32,7 +32,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the TPLink switch platform."""
from pyHS100 import SmartPlug
diff --git a/homeassistant/components/switch/transmission.py b/homeassistant/components/switch/transmission.py
index 840fdae44d935e..ffe285a23f3942 100644
--- a/homeassistant/components/switch/transmission.py
+++ b/homeassistant/components/switch/transmission.py
@@ -31,7 +31,6 @@
})
-# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Transmission switch."""
import transmissionrpc
diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py
index d7c284e4ccf515..82e2756c23054e 100644
--- a/homeassistant/components/switch/vera.py
+++ b/homeassistant/components/switch/vera.py
@@ -19,8 +19,8 @@
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Vera switches."""
add_devices(
- VeraSwitch(device, hass.data[VERA_CONTROLLER]) for
- device in hass.data[VERA_DEVICES]['switch'])
+ [VeraSwitch(device, hass.data[VERA_CONTROLLER]) for
+ device in hass.data[VERA_DEVICES]['switch']], True)
class VeraSwitch(VeraDevice, SwitchDevice):
diff --git a/homeassistant/components/switch/verisure.py b/homeassistant/components/switch/verisure.py
index 810946a505883b..4b126e5d3320be 100644
--- a/homeassistant/components/switch/verisure.py
+++ b/homeassistant/components/switch/verisure.py
@@ -72,6 +72,7 @@ def turn_off(self, **kwargs):
self._state = False
self._change_timestamp = time()
+ # pylint: disable=no-self-use
def update(self):
"""Get the latest date of the smartplug."""
hub.update_overview()
diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py
index 4f06f94155831a..569566bcbfb6f4 100644
--- a/homeassistant/components/switch/wemo.py
+++ b/homeassistant/components/switch/wemo.py
@@ -33,7 +33,7 @@
WEMO_STANDBY = 8
-# pylint: disable=unused-argument, too-many-function-args
+# pylint: disable=too-many-function-args
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Set up discovered WeMo switches."""
import pywemo.discovery as discovery
diff --git a/homeassistant/components/switch/wirelesstag.py b/homeassistant/components/switch/wirelesstag.py
new file mode 100644
index 00000000000000..cce8c349a31448
--- /dev/null
+++ b/homeassistant/components/switch/wirelesstag.py
@@ -0,0 +1,118 @@
+"""
+Switch implementation for Wireless Sensor Tags (wirelesstag.net) platform.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/switch.wirelesstag/
+"""
+import logging
+
+import voluptuous as vol
+
+
+from homeassistant.components.wirelesstag import (
+ DOMAIN as WIRELESSTAG_DOMAIN,
+ WIRELESSTAG_TYPE_13BIT, WIRELESSTAG_TYPE_WATER,
+ WIRELESSTAG_TYPE_ALSPRO,
+ WIRELESSTAG_TYPE_WEMO_DEVICE,
+ WirelessTagBaseSensor)
+from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice
+from homeassistant.const import (
+ CONF_MONITORED_CONDITIONS)
+import homeassistant.helpers.config_validation as cv
+
+DEPENDENCIES = ['wirelesstag']
+
+_LOGGER = logging.getLogger(__name__)
+
+ARM_TEMPERATURE = 'temperature'
+ARM_HUMIDITY = 'humidity'
+ARM_MOTION = 'motion'
+ARM_LIGHT = 'light'
+ARM_MOISTURE = 'moisture'
+
+# Switch types: Name, tag sensor type
+SWITCH_TYPES = {
+ ARM_TEMPERATURE: ['Arm Temperature', 'temperature'],
+ ARM_HUMIDITY: ['Arm Humidity', 'humidity'],
+ ARM_MOTION: ['Arm Motion', 'motion'],
+ ARM_LIGHT: ['Arm Light', 'light'],
+ ARM_MOISTURE: ['Arm Moisture', 'moisture']
+}
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_MONITORED_CONDITIONS, default=[]):
+ vol.All(cv.ensure_list, [vol.In(SWITCH_TYPES)]),
+})
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up switches for a Wireless Sensor Tags."""
+ platform = hass.data.get(WIRELESSTAG_DOMAIN)
+
+ switches = []
+ tags = platform.load_tags()
+ for switch_type in config.get(CONF_MONITORED_CONDITIONS):
+ for _, tag in tags.items():
+ if switch_type in WirelessTagSwitch.allowed_switches(tag):
+ switches.append(WirelessTagSwitch(platform, tag, switch_type))
+
+ add_devices(switches, True)
+
+
+class WirelessTagSwitch(WirelessTagBaseSensor, SwitchDevice):
+ """A switch implementation for Wireless Sensor Tags."""
+
+ @classmethod
+ def allowed_switches(cls, tag):
+ """Return allowed switch types for wireless tag."""
+ all_sensors = SWITCH_TYPES.keys()
+ sensors_per_tag_spec = {
+ WIRELESSTAG_TYPE_13BIT: [
+ ARM_TEMPERATURE, ARM_HUMIDITY, ARM_MOTION],
+ WIRELESSTAG_TYPE_WATER: [
+ ARM_TEMPERATURE, ARM_MOISTURE],
+ WIRELESSTAG_TYPE_ALSPRO: [
+ ARM_TEMPERATURE, ARM_HUMIDITY, ARM_MOTION, ARM_LIGHT],
+ WIRELESSTAG_TYPE_WEMO_DEVICE: []
+ }
+
+ tag_type = tag.tag_type
+
+ result = (
+ sensors_per_tag_spec[tag_type]
+ if tag_type in sensors_per_tag_spec else all_sensors)
+ _LOGGER.info("Allowed switches: %s tag_type: %s",
+ str(result), tag_type)
+
+ return result
+
+ def __init__(self, api, tag, switch_type):
+ """Initialize a switch for Wireless Sensor Tag."""
+ super().__init__(api, tag)
+ self._switch_type = switch_type
+ self.sensor_type = SWITCH_TYPES[self._switch_type][1]
+ self._name = '{} {}'.format(self._tag.name,
+ SWITCH_TYPES[self._switch_type][0])
+
+ def turn_on(self, **kwargs):
+ """Turn on the switch."""
+ self._api.arm(self)
+
+ def turn_off(self, **kwargs):
+ """Turn on the switch."""
+ self._api.disarm(self)
+
+ @property
+ def is_on(self) -> bool:
+ """Return True if entity is on."""
+ return self._state
+
+ def updated_state_value(self):
+ """Provide formatted value."""
+ return self.principal_value
+
+ @property
+ def principal_value(self):
+ """Provide actual value of switch."""
+ attr_name = 'is_{}_sensor_armed'.format(self.sensor_type)
+ return getattr(self._tag, attr_name, False)
diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py
index 149acd76c07c00..37b16f44ea8eec 100644
--- a/homeassistant/components/switch/xiaomi_miio.py
+++ b/homeassistant/components/switch/xiaomi_miio.py
@@ -39,7 +39,7 @@
'chuangmi.plug.v3']),
})
-REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
+REQUIREMENTS = ['python-miio==0.4.0', 'construct==2.9.41']
ATTR_POWER = 'power'
ATTR_TEMPERATURE = 'temperature'
@@ -97,7 +97,6 @@
}
-# pylint: disable=unused-argument
async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None):
"""Set up the switch from config."""
@@ -142,7 +141,7 @@ async def async_setup_platform(hass, config, async_add_devices,
elif model in ['qmi.powerstrip.v1', 'zimi.powerstrip.v2']:
from miio import PowerStrip
- plug = PowerStrip(host, token)
+ plug = PowerStrip(host, token, model=model)
device = XiaomiPowerStripSwitch(name, plug, model, unique_id)
devices.append(device)
hass.data[DATA_KEY][host] = device
@@ -422,8 +421,11 @@ def __init__(self, name, plug, model, unique_id, channel_usb):
self._device_features = FEATURE_FLAGS_PLUG_V3
self._state_attrs.update({
ATTR_WIFI_LED: None,
- ATTR_LOAD_POWER: None,
})
+ if self._channel_usb is False:
+ self._state_attrs.update({
+ ATTR_LOAD_POWER: None,
+ })
async def async_turn_on(self, **kwargs):
"""Turn a channel on."""
@@ -477,7 +479,7 @@ async def async_update(self):
if state.wifi_led:
self._state_attrs[ATTR_WIFI_LED] = state.wifi_led
- if state.load_power:
+ if self._channel_usb is False and state.load_power:
self._state_attrs[ATTR_LOAD_POWER] = state.load_power
except DeviceException as ex:
diff --git a/homeassistant/components/tradfri.py b/homeassistant/components/tradfri.py
index 72d1b4c769f1f6..9ed613abde04ea 100644
--- a/homeassistant/components/tradfri.py
+++ b/homeassistant/components/tradfri.py
@@ -15,7 +15,7 @@
from homeassistant.components.discovery import SERVICE_IKEA_TRADFRI
from homeassistant.util.json import load_json, save_json
-REQUIREMENTS = ['pytradfri[async]==5.4.2']
+REQUIREMENTS = ['pytradfri[async]==5.5.1']
DOMAIN = 'tradfri'
GATEWAY_IDENTITY = 'homeassistant'
diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/vacuum/neato.py
index 9eba34cea321b3..128bece8494274 100644
--- a/homeassistant/components/vacuum/neato.py
+++ b/homeassistant/components/vacuum/neato.py
@@ -5,7 +5,7 @@
https://home-assistant.io/components/vacuum.neato/
"""
import logging
-
+from datetime import timedelta
import requests
from homeassistant.const import STATE_OFF, STATE_ON
@@ -15,6 +15,7 @@
SUPPORT_MAP, ATTR_STATUS, ATTR_BATTERY_LEVEL, ATTR_BATTERY_ICON)
from homeassistant.components.neato import (
NEATO_ROBOTS, NEATO_LOGIN, NEATO_MAP_DATA, ACTION, ERRORS, MODE, ALERTS)
+from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
@@ -62,6 +63,7 @@ def __init__(self, hass, robot):
self.clean_suspension_charge_count = None
self.clean_suspension_time = None
+ @Throttle(timedelta(seconds=60))
def update(self):
"""Update the states of Neato Vacuums."""
_LOGGER.debug("Running Neato Vacuums update")
diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py
index 620014a1baee71..f6789d78b9ae97 100644
--- a/homeassistant/components/vacuum/xiaomi_miio.py
+++ b/homeassistant/components/vacuum/xiaomi_miio.py
@@ -19,7 +19,7 @@
ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON)
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41']
+REQUIREMENTS = ['python-miio==0.4.0', 'construct==2.9.41']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py
index ebe92a2dcc2c1d..cbbf279bb8c8d8 100644
--- a/homeassistant/components/vera.py
+++ b/homeassistant/components/vera.py
@@ -53,7 +53,7 @@
]
-# pylint: disable=unused-argument, too-many-function-args
+# pylint: disable=too-many-function-args
def setup(hass, base_config):
"""Set up for Vera devices."""
import pyvera as veraApi
@@ -148,12 +148,10 @@ def __init__(self, vera_device, controller):
slugify(vera_device.name), vera_device.device_id)
self.controller.register(vera_device, self._update_callback)
- self.update()
def _update_callback(self, _device):
"""Update the state."""
- self.update()
- self.schedule_update_ha_state()
+ self.schedule_update_ha_state(True)
@property
def name(self):
diff --git a/homeassistant/components/watson_iot.py b/homeassistant/components/watson_iot.py
new file mode 100644
index 00000000000000..246cf3a96c28ea
--- /dev/null
+++ b/homeassistant/components/watson_iot.py
@@ -0,0 +1,214 @@
+"""
+A component which allows you to send data to the IBM Watson IoT Platform.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/watson_iot/
+"""
+
+import logging
+import queue
+import threading
+import time
+
+import voluptuous as vol
+
+from homeassistant.const import (
+ CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE,
+ CONF_TOKEN, CONF_TYPE, EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_STOP,
+ STATE_UNAVAILABLE, STATE_UNKNOWN)
+from homeassistant.helpers import state as state_helper
+import homeassistant.helpers.config_validation as cv
+
+REQUIREMENTS = ['ibmiotf==0.3.4']
+
+_LOGGER = logging.getLogger(__name__)
+
+CONF_ORG = 'organization'
+CONF_ID = 'id'
+
+DOMAIN = 'watson_iot'
+
+RETRY_DELAY = 20
+MAX_TRIES = 3
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.All(vol.Schema({
+ vol.Required(CONF_ORG): cv.string,
+ vol.Required(CONF_TYPE): cv.string,
+ vol.Required(CONF_ID): cv.string,
+ vol.Required(CONF_TOKEN): cv.string,
+ vol.Optional(CONF_EXCLUDE, default={}): vol.Schema({
+ vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
+ vol.Optional(CONF_DOMAINS, default=[]):
+ vol.All(cv.ensure_list, [cv.string])
+ }),
+ vol.Optional(CONF_INCLUDE, default={}): vol.Schema({
+ vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
+ vol.Optional(CONF_DOMAINS, default=[]):
+ vol.All(cv.ensure_list, [cv.string])
+ }),
+ })),
+}, extra=vol.ALLOW_EXTRA)
+
+
+def setup(hass, config):
+ """Set up the Watson IoT Platform component."""
+ from ibmiotf import gateway
+
+ conf = config[DOMAIN]
+
+ include = conf[CONF_INCLUDE]
+ exclude = conf[CONF_EXCLUDE]
+ whitelist_e = set(include[CONF_ENTITIES])
+ whitelist_d = set(include[CONF_DOMAINS])
+ blacklist_e = set(exclude[CONF_ENTITIES])
+ blacklist_d = set(exclude[CONF_DOMAINS])
+
+ client_args = {
+ 'org': conf[CONF_ORG],
+ 'type': conf[CONF_TYPE],
+ 'id': conf[CONF_ID],
+ 'auth-method': 'token',
+ 'auth-token': conf[CONF_TOKEN],
+ }
+ watson_gateway = gateway.Client(client_args)
+
+ def event_to_json(event):
+ """Add an event to the outgoing list."""
+ state = event.data.get('new_state')
+ if state is None or state.state in (
+ STATE_UNKNOWN, '', STATE_UNAVAILABLE) or \
+ state.entity_id in blacklist_e or state.domain in blacklist_d:
+ return
+
+ if (whitelist_e and state.entity_id not in whitelist_e) or \
+ (whitelist_d and state.domain not in whitelist_d):
+ return
+
+ try:
+ _state_as_value = float(state.state)
+ except ValueError:
+ _state_as_value = None
+
+ if _state_as_value is None:
+ try:
+ _state_as_value = float(state_helper.state_as_number(state))
+ except ValueError:
+ _state_as_value = None
+
+ out_event = {
+ 'tags': {
+ 'domain': state.domain,
+ 'entity_id': state.object_id,
+ },
+ 'time': event.time_fired.isoformat(),
+ 'fields': {
+ 'state': state.state
+ }
+ }
+ if _state_as_value is not None:
+ out_event['fields']['state_value'] = _state_as_value
+
+ for key, value in state.attributes.items():
+ if key != 'unit_of_measurement':
+ # If the key is already in fields
+ if key in out_event['fields']:
+ key = key + "_"
+ # For each value we try to cast it as float
+ # But if we can not do it we store the value
+ # as string
+ try:
+ out_event['fields'][key] = float(value)
+ except (ValueError, TypeError):
+ out_event['fields'][key] = str(value)
+
+ return out_event
+
+ instance = hass.data[DOMAIN] = WatsonIOTThread(
+ hass, watson_gateway, event_to_json)
+ instance.start()
+
+ def shutdown(event):
+ """Shut down the thread."""
+ instance.queue.put(None)
+ instance.join()
+
+ hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, shutdown)
+
+ return True
+
+
+class WatsonIOTThread(threading.Thread):
+ """A threaded event handler class."""
+
+ def __init__(self, hass, gateway, event_to_json):
+ """Initialize the listener."""
+ threading.Thread.__init__(self, name='WatsonIOT')
+ self.queue = queue.Queue()
+ self.gateway = gateway
+ self.gateway.connect()
+ self.event_to_json = event_to_json
+ self.write_errors = 0
+ self.shutdown = False
+ hass.bus.listen(EVENT_STATE_CHANGED, self._event_listener)
+
+ def _event_listener(self, event):
+ """Listen for new messages on the bus and queue them for Watson IOT."""
+ item = (time.monotonic(), event)
+ self.queue.put(item)
+
+ def get_events_json(self):
+ """Return an event formatted for writing."""
+ events = []
+
+ try:
+ item = self.queue.get()
+
+ if item is None:
+ self.shutdown = True
+ else:
+ event_json = self.event_to_json(item[1])
+ if event_json:
+ events.append(event_json)
+
+ except queue.Empty:
+ pass
+
+ return events
+
+ def write_to_watson(self, events):
+ """Write preprocessed events to watson."""
+ import ibmiotf
+
+ for event in events:
+ for retry in range(MAX_TRIES + 1):
+ try:
+ for field in event['fields']:
+ value = event['fields'][field]
+ device_success = self.gateway.publishDeviceEvent(
+ event['tags']['domain'],
+ event['tags']['entity_id'],
+ field, 'json', value)
+ if not device_success:
+ _LOGGER.error(
+ "Failed to publish message to watson iot")
+ continue
+ break
+ except (ibmiotf.MissingMessageEncoderException, IOError):
+ if retry < MAX_TRIES:
+ time.sleep(RETRY_DELAY)
+ else:
+ _LOGGER.exception(
+ "Failed to publish message to watson iot")
+
+ def run(self):
+ """Process incoming events."""
+ while not self.shutdown:
+ event = self.get_events_json()
+ if event:
+ self.write_to_watson(event)
+ self.queue.task_done()
+
+ def block_till_done(self):
+ """Block till all events processed."""
+ self.queue.join()
diff --git a/homeassistant/components/weather/ecobee.py b/homeassistant/components/weather/ecobee.py
index 80ee4c29fbe880..59737c578a5915 100644
--- a/homeassistant/components/weather/ecobee.py
+++ b/homeassistant/components/weather/ecobee.py
@@ -4,6 +4,7 @@
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/weather.ecobee/
"""
+from datetime import datetime
from homeassistant.components import ecobee
from homeassistant.components.weather import (
WeatherEntity, ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP,
@@ -134,8 +135,10 @@ def forecast(self):
try:
forecasts = []
for day in self.weather['forecasts']:
+ date_time = datetime.strptime(day['dateTime'],
+ '%Y-%m-%d %H:%M:%S').isoformat()
forecast = {
- ATTR_FORECAST_TIME: day['dateTime'],
+ ATTR_FORECAST_TIME: date_time,
ATTR_FORECAST_CONDITION: day['condition'],
ATTR_FORECAST_TEMP: float(day['tempHigh']) / 10,
}
diff --git a/homeassistant/components/weather/ipma.py b/homeassistant/components/weather/ipma.py
new file mode 100644
index 00000000000000..ef4f1b349d72a5
--- /dev/null
+++ b/homeassistant/components/weather/ipma.py
@@ -0,0 +1,172 @@
+"""
+Support for IPMA weather service.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/weather.ipma/
+"""
+import logging
+from datetime import timedelta
+
+import async_timeout
+import voluptuous as vol
+
+from homeassistant.components.weather import (
+ WeatherEntity, PLATFORM_SCHEMA, ATTR_FORECAST_CONDITION,
+ ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP,
+ ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME)
+from homeassistant.const import \
+ CONF_NAME, TEMP_CELSIUS, CONF_LATITUDE, CONF_LONGITUDE
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers import config_validation as cv
+from homeassistant.util import Throttle
+
+REQUIREMENTS = ['pyipma==1.1.3']
+
+_LOGGER = logging.getLogger(__name__)
+
+ATTRIBUTION = 'Instituto Português do Mar e Atmosfera'
+
+ATTR_WEATHER_DESCRIPTION = "description"
+
+MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30)
+
+CONDITION_CLASSES = {
+ 'cloudy': [4, 5, 24, 25, 27],
+ 'fog': [16, 17, 26],
+ 'hail': [21, 22],
+ 'lightning': [19],
+ 'lightning-rainy': [20, 23],
+ 'partlycloudy': [2, 3],
+ 'pouring': [8, 11],
+ 'rainy': [6, 7, 9, 10, 12, 13, 14, 15],
+ 'snowy': [18],
+ 'snowy-rainy': [],
+ 'sunny': [1],
+ 'windy': [],
+ 'windy-variant': [],
+ 'exceptional': [],
+}
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Optional(CONF_NAME): cv.string,
+ vol.Optional(CONF_LATITUDE): cv.latitude,
+ vol.Optional(CONF_LONGITUDE): cv.longitude,
+})
+
+
+async def async_setup_platform(hass, config, async_add_devices,
+ discovery_info=None):
+ """Set up the ipma platform."""
+ latitude = config.get(CONF_LATITUDE, hass.config.latitude)
+ longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
+
+ if None in (latitude, longitude):
+ _LOGGER.error("Latitude or longitude not set in Home Assistant config")
+ return
+
+ from pyipma import Station
+
+ websession = async_get_clientsession(hass)
+ with async_timeout.timeout(10, loop=hass.loop):
+ station = await Station.get(websession, float(latitude),
+ float(longitude))
+
+ _LOGGER.debug("Initializing ipma weather: coordinates %s, %s",
+ latitude, longitude)
+
+ async_add_devices([IPMAWeather(station, config)], True)
+
+
+class IPMAWeather(WeatherEntity):
+ """Representation of a weather condition."""
+
+ def __init__(self, station, config):
+ """Initialise the platform with a data instance and station name."""
+ self._station_name = config.get(CONF_NAME, station.local)
+ self._station = station
+ self._condition = None
+ self._forecast = None
+ self._description = None
+
+ @Throttle(MIN_TIME_BETWEEN_UPDATES)
+ async def async_update(self):
+ """Update Condition and Forecast."""
+ with async_timeout.timeout(10, loop=self.hass.loop):
+ self._condition = await self._station.observation()
+ self._forecast = await self._station.forecast()
+ self._description = self._forecast[0].description
+
+ @property
+ def attribution(self):
+ """Return the attribution."""
+ return ATTRIBUTION
+
+ @property
+ def name(self):
+ """Return the name of the station."""
+ return self._station_name
+
+ @property
+ def condition(self):
+ """Return the current condition."""
+ return next((k for k, v in CONDITION_CLASSES.items()
+ if self._forecast[0].idWeatherType in v), None)
+
+ @property
+ def temperature(self):
+ """Return the current temperature."""
+ return self._condition.temperature
+
+ @property
+ def pressure(self):
+ """Return the current pressure."""
+ return self._condition.pressure
+
+ @property
+ def humidity(self):
+ """Return the name of the sensor."""
+ return self._condition.humidity
+
+ @property
+ def wind_speed(self):
+ """Return the current windspeed."""
+ return self._condition.windspeed
+
+ @property
+ def wind_bearing(self):
+ """Return the current wind bearing (degrees)."""
+ return self._condition.winddirection
+
+ @property
+ def temperature_unit(self):
+ """Return the unit of measurement."""
+ return TEMP_CELSIUS
+
+ @property
+ def forecast(self):
+ """Return the forecast array."""
+ if self._forecast:
+ fcdata_out = []
+ for data_in in self._forecast:
+ data_out = {}
+ data_out[ATTR_FORECAST_TIME] = data_in.forecastDate
+ data_out[ATTR_FORECAST_CONDITION] =\
+ next((k for k, v in CONDITION_CLASSES.items()
+ if int(data_in.idWeatherType) in v), None)
+ data_out[ATTR_FORECAST_TEMP_LOW] = data_in.tMin
+ data_out[ATTR_FORECAST_TEMP] = data_in.tMax
+ data_out[ATTR_FORECAST_PRECIPITATION] = data_in.precipitaProb
+
+ fcdata_out.append(data_out)
+
+ return fcdata_out
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ data = dict()
+
+ if self._description:
+ data[ATTR_WEATHER_DESCRIPTION] = self._description
+
+ return data
diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py
index 909f123b52c28b..8354757ff33eed 100644
--- a/homeassistant/components/weather/openweathermap.py
+++ b/homeassistant/components/weather/openweathermap.py
@@ -11,10 +11,10 @@
from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP,
- ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity)
+ ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity)
from homeassistant.const import (
- CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, STATE_UNKNOWN,
- TEMP_CELSIUS)
+ CONF_API_KEY, TEMP_CELSIUS, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE,
+ CONF_NAME, STATE_UNKNOWN)
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
@@ -22,20 +22,25 @@
_LOGGER = logging.getLogger(__name__)
+ATTR_FORECAST_WIND_SPEED = 'wind_speed'
+ATTR_FORECAST_WIND_BEARING = 'wind_bearing'
+
ATTRIBUTION = 'Data provided by OpenWeatherMap'
+FORECAST_MODE = ['hourly', 'daily']
+
DEFAULT_NAME = 'OpenWeatherMap'
MIN_TIME_BETWEEN_FORECAST_UPDATES = timedelta(minutes=30)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
CONDITION_CLASSES = {
- 'cloudy': [804],
+ 'cloudy': [803, 804],
'fog': [701, 741],
'hail': [906],
'lightning': [210, 211, 212, 221],
'lightning-rainy': [200, 201, 202, 230, 231, 232],
- 'partlycloudy': [801, 802, 803],
+ 'partlycloudy': [801, 802],
'pouring': [504, 314, 502, 503, 522],
'rainy': [300, 301, 302, 310, 311, 312, 313, 500, 501, 520, 521],
'snowy': [600, 601, 602, 611, 612, 620, 621, 622],
@@ -51,6 +56,7 @@
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_LATITUDE): cv.latitude,
vol.Optional(CONF_LONGITUDE): cv.longitude,
+ vol.Optional(CONF_MODE, default='hourly'): vol.In(FORECAST_MODE),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
@@ -62,6 +68,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
longitude = config.get(CONF_LONGITUDE, round(hass.config.longitude, 5))
latitude = config.get(CONF_LATITUDE, round(hass.config.latitude, 5))
name = config.get(CONF_NAME)
+ mode = config.get(CONF_MODE)
try:
owm = pyowm.OWM(config.get(CONF_API_KEY))
@@ -69,20 +76,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.error("Error while connecting to OpenWeatherMap")
return False
- data = WeatherData(owm, latitude, longitude)
+ data = WeatherData(owm, latitude, longitude, mode)
add_devices([OpenWeatherMapWeather(
- name, data, hass.config.units.temperature_unit)], True)
+ name, data, hass.config.units.temperature_unit, mode)], True)
class OpenWeatherMapWeather(WeatherEntity):
"""Implementation of an OpenWeatherMap sensor."""
- def __init__(self, name, owm, temperature_unit):
+ def __init__(self, name, owm, temperature_unit, mode):
"""Initialize the sensor."""
self._name = name
self._owm = owm
self._temperature_unit = temperature_unit
+ self._mode = mode
self.data = None
self.forecast_data = None
@@ -140,15 +148,34 @@ def forecast(self):
"""Return the forecast array."""
data = []
for entry in self.forecast_data.get_weathers():
- data.append({
- ATTR_FORECAST_TIME: entry.get_reference_time('unix') * 1000,
- ATTR_FORECAST_TEMP:
- entry.get_temperature('celsius').get('temp'),
- ATTR_FORECAST_PRECIPITATION: entry.get_rain().get('3h'),
- ATTR_FORECAST_CONDITION:
- [k for k, v in CONDITION_CLASSES.items()
- if entry.get_weather_code() in v][0]
- })
+ if self._mode == 'daily':
+ data.append({
+ ATTR_FORECAST_TIME:
+ entry.get_reference_time('unix') * 1000,
+ ATTR_FORECAST_TEMP:
+ entry.get_temperature('celsius').get('day'),
+ ATTR_FORECAST_TEMP_LOW:
+ entry.get_temperature('celsius').get('night'),
+ ATTR_FORECAST_WIND_SPEED:
+ entry.get_wind().get('speed'),
+ ATTR_FORECAST_WIND_BEARING:
+ entry.get_wind().get('deg'),
+ ATTR_FORECAST_CONDITION:
+ [k for k, v in CONDITION_CLASSES.items()
+ if entry.get_weather_code() in v][0]
+ })
+ else:
+ data.append({
+ ATTR_FORECAST_TIME:
+ entry.get_reference_time('unix') * 1000,
+ ATTR_FORECAST_TEMP:
+ entry.get_temperature('celsius').get('temp'),
+ ATTR_FORECAST_PRECIPITATION:
+ entry.get_rain().get('3h'),
+ ATTR_FORECAST_CONDITION:
+ [k for k, v in CONDITION_CLASSES.items()
+ if entry.get_weather_code() in v][0]
+ })
return data
def update(self):
@@ -169,8 +196,9 @@ def update(self):
class WeatherData(object):
"""Get the latest data from OpenWeatherMap."""
- def __init__(self, owm, latitude, longitude):
+ def __init__(self, owm, latitude, longitude, mode):
"""Initialize the data object."""
+ self._mode = mode
self.owm = owm
self.latitude = latitude
self.longitude = longitude
@@ -193,8 +221,14 @@ def update_forecast(self):
from pyowm.exceptions.api_call_error import APICallError
try:
- fcd = self.owm.three_hours_forecast_at_coords(
- self.latitude, self.longitude)
+ if self._mode == 'daily':
+ fcd = self.owm.daily_forecast_at_coords(
+ self.latitude, self.longitude, 15
+ )
+ else:
+ fcd = self.owm.three_hours_forecast_at_coords(
+ self.latitude, self.longitude
+ )
except APICallError:
_LOGGER.error("Exception when calling OWM web API "
"to update forecast")
diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py
index 11094acd3e2a00..e16e5524f95456 100644
--- a/homeassistant/components/websocket_api.py
+++ b/homeassistant/components/websocket_api.py
@@ -38,6 +38,7 @@
ERR_ID_REUSE = 1
ERR_INVALID_FORMAT = 2
ERR_NOT_FOUND = 3
+ERR_UNKNOWN_COMMAND = 4
TYPE_AUTH = 'auth'
TYPE_AUTH_INVALID = 'auth_invalid'
@@ -353,8 +354,11 @@ def handle_hass_stop(event):
'Identifier values have to increase.'))
elif msg['type'] not in handlers:
- # Unknown command
- break
+ self.log_error(
+ 'Received invalid command: {}'.format(msg['type']))
+ self.to_write.put_nowait(error_message(
+ cur_id, ERR_UNKNOWN_COMMAND,
+ 'Unknown command.'))
else:
handler, schema = handlers[msg['type']]
diff --git a/homeassistant/components/wemo.py b/homeassistant/components/wemo.py
index 9929b64be7dacc..15b75b2f7a82b4 100644
--- a/homeassistant/components/wemo.py
+++ b/homeassistant/components/wemo.py
@@ -44,7 +44,7 @@
}, extra=vol.ALLOW_EXTRA)
-# pylint: disable=unused-argument, too-many-function-args
+# pylint: disable=too-many-function-args
def setup(hass, config):
"""Set up for WeMo devices."""
import pywemo
diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py
index 042943f7a3f273..e4dfc17246a427 100644
--- a/homeassistant/components/wink/__init__.py
+++ b/homeassistant/components/wink/__init__.py
@@ -26,7 +26,7 @@
from homeassistant.helpers.event import track_time_interval
from homeassistant.util.json import load_json, save_json
-REQUIREMENTS = ['python-wink==1.7.3', 'pubnubsub-handler==1.0.2']
+REQUIREMENTS = ['python-wink==1.8.0', 'pubnubsub-handler==1.0.2']
_LOGGER = logging.getLogger(__name__)
@@ -210,7 +210,6 @@ def _request_oauth_completion(hass, config):
"Failed to register, please try again.")
return
- # pylint: disable=unused-argument
def wink_configuration_callback(callback_data):
"""Call setup again."""
setup(hass, config)
diff --git a/homeassistant/components/wirelesstag.py b/homeassistant/components/wirelesstag.py
new file mode 100644
index 00000000000000..9fabcb1cd5aefb
--- /dev/null
+++ b/homeassistant/components/wirelesstag.py
@@ -0,0 +1,256 @@
+"""
+Wireless Sensor Tags platform support.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/wirelesstag/
+"""
+import logging
+
+from requests.exceptions import HTTPError, ConnectTimeout
+import voluptuous as vol
+from homeassistant.const import (
+ ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, CONF_USERNAME, CONF_PASSWORD)
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.dispatcher import (
+ dispatcher_send)
+
+REQUIREMENTS = ['wirelesstagpy==0.3.0']
+
+_LOGGER = logging.getLogger(__name__)
+
+
+# straight of signal in dBm
+ATTR_TAG_SIGNAL_STRAIGHT = 'signal_straight'
+# indicates if tag is out of range or not
+ATTR_TAG_OUT_OF_RANGE = 'out_of_range'
+# number in percents from max power of tag receiver
+ATTR_TAG_POWER_CONSUMPTION = 'power_consumption'
+
+
+NOTIFICATION_ID = 'wirelesstag_notification'
+NOTIFICATION_TITLE = "Wireless Sensor Tag Setup"
+
+DOMAIN = 'wirelesstag'
+DEFAULT_ENTITY_NAMESPACE = 'wirelesstag'
+
+WIRELESSTAG_TYPE_13BIT = 13
+WIRELESSTAG_TYPE_ALSPRO = 26
+WIRELESSTAG_TYPE_WATER = 32
+WIRELESSTAG_TYPE_WEMO_DEVICE = 82
+
+SIGNAL_TAG_UPDATE = 'wirelesstag.tag_info_updated_{}'
+SIGNAL_BINARY_EVENT_UPDATE = 'wirelesstag.binary_event_updated_{}_{}'
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.Schema({
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ }),
+}, extra=vol.ALLOW_EXTRA)
+
+
+class WirelessTagPlatform:
+ """Principal object to manage all registered in HA tags."""
+
+ def __init__(self, hass, api):
+ """Designated initializer for wirelesstags platform."""
+ self.hass = hass
+ self.api = api
+ self.tags = {}
+
+ def load_tags(self):
+ """Load tags from remote server."""
+ self.tags = self.api.load_tags()
+ return self.tags
+
+ def arm(self, switch):
+ """Arm entity sensor monitoring."""
+ func_name = 'arm_{}'.format(switch.sensor_type)
+ arm_func = getattr(self.api, func_name)
+ if arm_func is not None:
+ arm_func(switch.tag_id)
+
+ def disarm(self, switch):
+ """Disarm entity sensor monitoring."""
+ func_name = 'disarm_{}'.format(switch.sensor_type)
+ disarm_func = getattr(self.api, func_name)
+ if disarm_func is not None:
+ disarm_func(switch.tag_id)
+
+ # pylint: disable=no-self-use
+ def make_push_notitication(self, name, url, content):
+ """Factory for notification config."""
+ from wirelesstagpy import NotificationConfig
+ return NotificationConfig(name, {
+ 'url': url, 'verb': 'POST',
+ 'content': content, 'disabled': False, 'nat': True})
+
+ def install_push_notifications(self, binary_sensors):
+ """Setup local push notification from tag manager."""
+ _LOGGER.info("Registering local push notifications.")
+ configs = []
+
+ binary_url = self.binary_event_callback_url
+ for event in binary_sensors:
+ for state, name in event.binary_spec.items():
+ content = ('{"type": "' + event.device_class +
+ '", "id":{' + str(event.tag_id_index_template) +
+ '}, "state": \"' + state + '\"}')
+ config = self.make_push_notitication(name, binary_url, content)
+ configs.append(config)
+
+ content = ("{\"name\":\"{0}\",\"id\":{1},\"temp\":{2}," +
+ "\"cap\":{3},\"lux\":{4}}")
+ update_url = self.update_callback_url
+ update_config = self.make_push_notitication(
+ 'update', update_url, content)
+ configs.append(update_config)
+
+ result = self.api.install_push_notification(0, configs, True)
+ if not result:
+ self.hass.components.persistent_notification.create(
+ "Error: failed to install local push notifications
",
+ title="Wireless Sensor Tag Setup Local Push Notifications",
+ notification_id="wirelesstag_failed_push_notification")
+ else:
+ _LOGGER.info("Installed push notifications for all tags.")
+
+ @property
+ def update_callback_url(self):
+ """Return url for local push notifications(update event)."""
+ return '{}/api/events/wirelesstag_update_tags'.format(
+ self.hass.config.api.base_url)
+
+ @property
+ def binary_event_callback_url(self):
+ """Return url for local push notifications(binary event)."""
+ return '{}/api/events/wirelesstag_binary_event'.format(
+ self.hass.config.api.base_url)
+
+ def handle_update_tags_event(self, event):
+ """Main entry to handle push event from wireless tag manager."""
+ _LOGGER.info("push notification for update arrived: %s", event)
+ dispatcher_send(
+ self.hass,
+ SIGNAL_TAG_UPDATE.format(event.data.get('id')),
+ event)
+
+ def handle_binary_event(self, event):
+ """Handle push notifications for binary (on/off) events."""
+ _LOGGER.info("Push notification for binary event arrived: %s", event)
+ try:
+ tag_id = event.data.get('id')
+ event_type = event.data.get('type')
+ dispatcher_send(
+ self.hass,
+ SIGNAL_BINARY_EVENT_UPDATE.format(tag_id, event_type),
+ event)
+ except Exception as ex: # pylint: disable=W0703
+ _LOGGER.error("Unable to handle binary event:\
+ %s error: %s", str(event), str(ex))
+
+
+def setup(hass, config):
+ """Set up the Wireless Sensor Tag component."""
+ conf = config[DOMAIN]
+ username = conf.get(CONF_USERNAME)
+ password = conf.get(CONF_PASSWORD)
+
+ try:
+ from wirelesstagpy import (WirelessTags, WirelessTagsException)
+ wirelesstags = WirelessTags(username=username, password=password)
+
+ platform = WirelessTagPlatform(hass, wirelesstags)
+ platform.load_tags()
+ hass.data[DOMAIN] = platform
+ except (ConnectTimeout, HTTPError, WirelessTagsException) as ex:
+ _LOGGER.error("Unable to connect to wirelesstag.net service: %s",
+ str(ex))
+ hass.components.persistent_notification.create(
+ "Error: {}
"
+ "Please restart hass after fixing this."
+ "".format(ex),
+ title=NOTIFICATION_TITLE,
+ notification_id=NOTIFICATION_ID)
+ return False
+
+ # listen to custom events
+ hass.bus.listen('wirelesstag_update_tags',
+ hass.data[DOMAIN].handle_update_tags_event)
+ hass.bus.listen('wirelesstag_binary_event',
+ hass.data[DOMAIN].handle_binary_event)
+
+ return True
+
+
+class WirelessTagBaseSensor(Entity):
+ """Base class for HA implementation for Wireless Sensor Tag."""
+
+ def __init__(self, api, tag):
+ """Initialize a base sensor for Wireless Sensor Tag platform."""
+ self._api = api
+ self._tag = tag
+ self._uuid = self._tag.uuid
+ self.tag_id = self._tag.tag_id
+ self._name = self._tag.name
+ self._state = None
+
+ @property
+ def should_poll(self):
+ """Return the polling state."""
+ return True
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def principal_value(self):
+ """Return base value.
+
+ Subclasses need override based on type of sensor.
+ """
+ return 0
+
+ def updated_state_value(self):
+ """Default implementation formats princial value."""
+ return self.decorate_value(self.principal_value)
+
+ # pylint: disable=no-self-use
+ def decorate_value(self, value):
+ """Decorate input value to be well presented for end user."""
+ return '{:.1f}'.format(value)
+
+ @property
+ def available(self):
+ """Return True if entity is available."""
+ return self._tag.is_alive
+
+ def update(self):
+ """Update state."""
+ if not self.should_poll:
+ return
+
+ updated_tags = self._api.load_tags()
+ updated_tag = updated_tags[self._uuid]
+ if updated_tag is None:
+ _LOGGER.error('Unable to update tag: "%s"', self.name)
+ return
+
+ self._tag = updated_tag
+ self._state = self.updated_state_value()
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ return {
+ ATTR_BATTERY_LEVEL: self._tag.battery_remaining,
+ ATTR_VOLTAGE: '{:.2f}V'.format(self._tag.battery_volts),
+ ATTR_TAG_SIGNAL_STRAIGHT: '{}dBm'.format(
+ self._tag.signal_straight),
+ ATTR_TAG_OUT_OF_RANGE: not self._tag.is_in_range,
+ ATTR_TAG_POWER_CONSUMPTION: '{:.2f}%'.format(
+ self._tag.power_consumption)
+ }
diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py
index ae3a4e0be72328..2090f5227093dc 100644
--- a/homeassistant/components/xiaomi_aqara.py
+++ b/homeassistant/components/xiaomi_aqara.py
@@ -23,7 +23,7 @@
from homeassistant.util.dt import utcnow
from homeassistant.util import slugify
-REQUIREMENTS = ['PyXiaomiGateway==0.9.4']
+REQUIREMENTS = ['PyXiaomiGateway==0.9.5']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/zone/.translations/bg.json b/homeassistant/components/zone/.translations/bg.json
new file mode 100644
index 00000000000000..5770058c5ebc4f
--- /dev/null
+++ b/homeassistant/components/zone/.translations/bg.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "error": {
+ "name_exists": "\u0418\u043c\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "icon": "\u0418\u043a\u043e\u043d\u0430",
+ "latitude": "\u0428\u0438\u0440\u0438\u043d\u0430",
+ "longitude": "\u0414\u044a\u043b\u0436\u0438\u043d\u0430",
+ "name": "\u0418\u043c\u0435",
+ "passive": "\u041f\u0430\u0441\u0438\u0432\u043d\u0430",
+ "radius": "\u0420\u0430\u0434\u0438\u0443\u0441"
+ },
+ "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438\u0442\u0435 \u043d\u0430 \u0437\u043e\u043d\u0430\u0442\u0430"
+ }
+ },
+ "title": "\u0417\u043e\u043d\u0430"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zone/.translations/ca.json b/homeassistant/components/zone/.translations/ca.json
new file mode 100644
index 00000000000000..1676c8f390627a
--- /dev/null
+++ b/homeassistant/components/zone/.translations/ca.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "error": {
+ "name_exists": "El nom ja existeix"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "icon": "Icona",
+ "latitude": "Latitud",
+ "longitude": "Longitud",
+ "name": "Nom",
+ "passive": "Passiu",
+ "radius": "Radi"
+ },
+ "title": "Defineix els par\u00e0metres de la zona"
+ }
+ },
+ "title": "Zona"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zone/.translations/cs.json b/homeassistant/components/zone/.translations/cs.json
new file mode 100644
index 00000000000000..a521377e5e0a59
--- /dev/null
+++ b/homeassistant/components/zone/.translations/cs.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "error": {
+ "name_exists": "N\u00e1zev ji\u017e existuje"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "icon": "Ikona",
+ "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka",
+ "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka",
+ "name": "N\u00e1zev",
+ "passive": "Pasivn\u00ed",
+ "radius": "Polom\u011br"
+ },
+ "title": "Definujte parametry z\u00f3ny"
+ }
+ },
+ "title": "Z\u00f3na"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zone/.translations/fr.json b/homeassistant/components/zone/.translations/fr.json
new file mode 100644
index 00000000000000..eb02aba7b50c05
--- /dev/null
+++ b/homeassistant/components/zone/.translations/fr.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "error": {
+ "name_exists": "Ce nom est d\u00e9j\u00e0 utilis\u00e9"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "icon": "Ic\u00f4ne",
+ "latitude": "Latitude",
+ "longitude": "Longitude",
+ "name": "Nom",
+ "passive": "Passif",
+ "radius": "Rayon"
+ },
+ "title": "D\u00e9finir les param\u00e8tres de la zone"
+ }
+ },
+ "title": "Zone"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zone/.translations/hu.json b/homeassistant/components/zone/.translations/hu.json
new file mode 100644
index 00000000000000..0181f688c27d0d
--- /dev/null
+++ b/homeassistant/components/zone/.translations/hu.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "error": {
+ "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "icon": "Ikon",
+ "latitude": "Sz\u00e9less\u00e9g",
+ "longitude": "Hossz\u00fas\u00e1g",
+ "name": "N\u00e9v",
+ "passive": "Passz\u00edv",
+ "radius": "Sug\u00e1r"
+ },
+ "title": "Z\u00f3na param\u00e9terek megad\u00e1sa"
+ }
+ },
+ "title": "Z\u00f3na"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zone/.translations/it.json b/homeassistant/components/zone/.translations/it.json
new file mode 100644
index 00000000000000..4490124510fa4d
--- /dev/null
+++ b/homeassistant/components/zone/.translations/it.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "error": {
+ "name_exists": "Il nome \u00e8 gi\u00e0 esistente"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "icon": "Icona",
+ "latitude": "Latitudine",
+ "longitude": "Logitudine",
+ "name": "Nome",
+ "passive": "Passiva",
+ "radius": "Raggio"
+ },
+ "title": "Imposta i parametri della zona"
+ }
+ },
+ "title": "Zona"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zone/.translations/ko.json b/homeassistant/components/zone/.translations/ko.json
index 364f8f3cc77f3a..421f079a67ea48 100644
--- a/homeassistant/components/zone/.translations/ko.json
+++ b/homeassistant/components/zone/.translations/ko.json
@@ -13,7 +13,7 @@
"passive": "\uc790\ub3d9\ud654 \uc804\uc6a9",
"radius": "\ubc18\uacbd"
},
- "title": "\uad6c\uc5ed \ub9e4\uac1c \ubcc0\uc218 \uc815\uc758"
+ "title": "\uad6c\uc5ed \uc124\uc815"
}
},
"title": "\uad6c\uc5ed"
diff --git a/homeassistant/components/zone/.translations/pt-BR.json b/homeassistant/components/zone/.translations/pt-BR.json
new file mode 100644
index 00000000000000..f2a41b0b26785c
--- /dev/null
+++ b/homeassistant/components/zone/.translations/pt-BR.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "error": {
+ "name_exists": "O nome j\u00e1 existe"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "icon": "\u00cdcone",
+ "latitude": "Latitude",
+ "longitude": "Longitude",
+ "name": "Nome",
+ "passive": "Passivo",
+ "radius": "Raio"
+ },
+ "title": "Definir par\u00e2metros da zona"
+ }
+ },
+ "title": "Zona"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zone/.translations/pt.json b/homeassistant/components/zone/.translations/pt.json
index a4ced557805661..2c3292e58c192d 100644
--- a/homeassistant/components/zone/.translations/pt.json
+++ b/homeassistant/components/zone/.translations/pt.json
@@ -12,7 +12,8 @@
"name": "Nome",
"passive": "Passivo",
"radius": "Raio"
- }
+ },
+ "title": "Definir os par\u00e2metros da zona"
}
},
"title": "Zona"
diff --git a/homeassistant/components/zone/.translations/sl.json b/homeassistant/components/zone/.translations/sl.json
new file mode 100644
index 00000000000000..1885cb5d2c86bd
--- /dev/null
+++ b/homeassistant/components/zone/.translations/sl.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "error": {
+ "name_exists": "Ime \u017ee obstaja"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "icon": "Ikona",
+ "latitude": "Zemljepisna \u0161irina",
+ "longitude": "Zemljepisna dol\u017eina",
+ "name": "Ime",
+ "passive": "Pasivno",
+ "radius": "Radij"
+ },
+ "title": "Dolo\u010dite parametre obmo\u010dja"
+ }
+ },
+ "title": "Obmo\u010dje"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zone/.translations/sv.json b/homeassistant/components/zone/.translations/sv.json
new file mode 100644
index 00000000000000..55c5bcf712721c
--- /dev/null
+++ b/homeassistant/components/zone/.translations/sv.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "error": {
+ "name_exists": "Namnet finns redan"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "icon": "Ikon",
+ "latitude": "Latitud",
+ "longitude": "Longitud",
+ "name": "Namn",
+ "passive": "Passiv",
+ "radius": "Radie"
+ },
+ "title": "Definiera zonparametrar"
+ }
+ },
+ "title": "Zon"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zone/.translations/vi.json b/homeassistant/components/zone/.translations/vi.json
new file mode 100644
index 00000000000000..7217944bd6b631
--- /dev/null
+++ b/homeassistant/components/zone/.translations/vi.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "error": {
+ "name_exists": "T\u00ean \u0111\u00e3 t\u1ed3n t\u1ea1i"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "icon": "Bi\u1ec3u t\u01b0\u1ee3ng",
+ "latitude": "V\u0129 \u0111\u1ed9",
+ "longitude": "Kinh \u0111\u1ed9",
+ "name": "T\u00ean",
+ "passive": "Th\u1ee5 \u0111\u1ed9ng",
+ "radius": "B\u00e1n k\u00ednh"
+ },
+ "title": "X\u00e1c \u0111\u1ecbnh tham s\u1ed1 v\u00f9ng"
+ }
+ },
+ "title": "V\u00f9ng"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zone/.translations/zh-Hant.json b/homeassistant/components/zone/.translations/zh-Hant.json
new file mode 100644
index 00000000000000..12c1141397d7ef
--- /dev/null
+++ b/homeassistant/components/zone/.translations/zh-Hant.json
@@ -0,0 +1,21 @@
+{
+ "config": {
+ "error": {
+ "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728"
+ },
+ "step": {
+ "init": {
+ "data": {
+ "icon": "\u5716\u793a",
+ "latitude": "\u7def\u5ea6",
+ "longitude": "\u7d93\u5ea6",
+ "name": "\u540d\u7a31",
+ "passive": "\u88ab\u52d5",
+ "radius": "\u534a\u5f91"
+ },
+ "title": "\u5b9a\u7fa9\u5340\u57df\u53c3\u6578"
+ }
+ },
+ "title": "\u5340\u57df"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py
index c33a16c632e629..ee19e00266c7fc 100644
--- a/homeassistant/components/zone/__init__.py
+++ b/homeassistant/components/zone/__init__.py
@@ -73,8 +73,8 @@ async def async_setup_entry(hass, config_entry):
entry = config_entry.data
name = entry[CONF_NAME]
zone = Zone(hass, name, entry[CONF_LATITUDE], entry[CONF_LONGITUDE],
- entry.get(CONF_RADIUS), entry.get(CONF_ICON),
- entry.get(CONF_PASSIVE))
+ entry.get(CONF_RADIUS, DEFAULT_RADIUS), entry.get(CONF_ICON),
+ entry.get(CONF_PASSIVE, DEFAULT_PASSIVE))
zone.entity_id = async_generate_entity_id(
ENTITY_ID_FORMAT, name, None, hass)
hass.async_add_job(zone.async_update_ha_state())
diff --git a/homeassistant/components/zwave/discovery_schemas.py b/homeassistant/components/zwave/discovery_schemas.py
index d38fbc7079c306..fc2e7fc912d6c7 100644
--- a/homeassistant/components/zwave/discovery_schemas.py
+++ b/homeassistant/components/zwave/discovery_schemas.py
@@ -213,6 +213,7 @@
}})},
{const.DISC_COMPONENT: 'switch',
const.DISC_GENERIC_DEVICE_CLASS: [
+ const.GENERIC_TYPE_METER,
const.GENERIC_TYPE_SENSOR_ALARM,
const.GENERIC_TYPE_SENSOR_BINARY,
const.GENERIC_TYPE_SWITCH_BINARY,
diff --git a/homeassistant/config.py b/homeassistant/config.py
index 44bf542f7cd082..2906f07a307c0f 100644
--- a/homeassistant/config.py
+++ b/homeassistant/config.py
@@ -548,15 +548,15 @@ def _identify_config_schema(module):
return '', schema
-def _recursive_merge(pack_name, comp_name, config, conf, package):
+def _recursive_merge(conf, package):
"""Merge package into conf, recursively."""
+ error = False
for key, pack_conf in package.items():
if isinstance(pack_conf, dict):
if not pack_conf:
continue
conf[key] = conf.get(key, OrderedDict())
- _recursive_merge(pack_name, comp_name, config,
- conf=conf[key], package=pack_conf)
+ error = _recursive_merge(conf=conf[key], package=pack_conf)
elif isinstance(pack_conf, list):
if not pack_conf:
@@ -566,11 +566,10 @@ def _recursive_merge(pack_name, comp_name, config, conf, package):
else:
if conf.get(key) is not None:
- _log_pkg_error(
- pack_name, comp_name, config,
- 'has keys that are defined multiple times')
+ return key
else:
conf[key] = pack_conf
+ return error
def merge_packages_config(hass, config, packages,
@@ -605,39 +604,34 @@ def merge_packages_config(hass, config, packages,
config[comp_name].extend(cv.ensure_list(comp_conf))
continue
- if merge_type == 'dict':
- if comp_conf is None:
- comp_conf = OrderedDict()
-
- if not isinstance(comp_conf, dict):
- _log_pkg_error(
- pack_name, comp_name, config,
- "cannot be merged. Expected a dict.")
- continue
-
- if comp_name not in config:
- config[comp_name] = OrderedDict()
-
- if not isinstance(config[comp_name], dict):
- _log_pkg_error(
- pack_name, comp_name, config,
- "cannot be merged. Dict expected in main config.")
- continue
-
- for key, val in comp_conf.items():
- if key in config[comp_name]:
- _log_pkg_error(pack_name, comp_name, config,
- "duplicate key '{}'".format(key))
- continue
- config[comp_name][key] = val
- continue
+ if comp_conf is None:
+ comp_conf = OrderedDict()
+
+ if not isinstance(comp_conf, dict):
+ _log_pkg_error(
+ pack_name, comp_name, config,
+ "cannot be merged. Expected a dict.")
+ continue
- # The last merge type are sections that require recursive merging
- if comp_name in config:
- _recursive_merge(pack_name, comp_name, config,
- conf=config[comp_name], package=comp_conf)
+ if comp_name not in config or config[comp_name] is None:
+ config[comp_name] = OrderedDict()
+
+ if not isinstance(config[comp_name], dict):
+ _log_pkg_error(
+ pack_name, comp_name, config,
+ "cannot be merged. Dict expected in main config.")
continue
- config[comp_name] = comp_conf
+ if not isinstance(comp_conf, dict):
+ _log_pkg_error(
+ pack_name, comp_name, config,
+ "cannot be merged. Dict expected in package.")
+ continue
+
+ error = _recursive_merge(conf=config[comp_name],
+ package=comp_conf)
+ if error:
+ _log_pkg_error(pack_name, comp_name, config,
+ "has duplicate key '{}'".format(error))
return config
diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py
index 8a73e424fb5133..db2912d7b42297 100644
--- a/homeassistant/config_entries.py
+++ b/homeassistant/config_entries.py
@@ -127,8 +127,11 @@ async def async_step_discovery(info):
HANDLERS = Registry()
# Components that have config flows. In future we will auto-generate this list.
FLOWS = [
+ 'cast',
'deconz',
'hue',
+ 'nest',
+ 'sonos',
'zone',
]
@@ -143,7 +146,12 @@ async def async_step_discovery(info):
ENTRY_STATE_FAILED_UNLOAD = 'failed_unload'
DISCOVERY_NOTIFICATION_ID = 'config_entry_discovery'
-DISCOVERY_SOURCES = (data_entry_flow.SOURCE_DISCOVERY,)
+DISCOVERY_SOURCES = (
+ data_entry_flow.SOURCE_DISCOVERY,
+ data_entry_flow.SOURCE_IMPORT,
+)
+
+EVENT_FLOW_DISCOVERED = 'config_entry_discovered'
class ConfigEntry:
@@ -398,6 +406,7 @@ async def _async_create_flow(self, handler, *, source, data):
# Create notification.
if source in DISCOVERY_SOURCES:
+ self.hass.bus.async_fire(EVENT_FLOW_DISCOVERED)
self.hass.components.persistent_notification.async_create(
title='New devices discovered',
message=("We have discovered new devices on your network. "
diff --git a/homeassistant/const.py b/homeassistant/const.py
index 552b63925958d7..a22605c37f49a4 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -1,7 +1,7 @@
# coding: utf-8
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
-MINOR_VERSION = 71
+MINOR_VERSION = 72
PATCH_VERSION = '0'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
diff --git a/homeassistant/core.py b/homeassistant/core.py
index bc3b598180c0b3..5e6dcd81310b0d 100644
--- a/homeassistant/core.py
+++ b/homeassistant/core.py
@@ -4,7 +4,7 @@
Home Assistant is a Home Automation framework for observing the state
of entities and react to changes.
"""
-# pylint: disable=unused-import, too-many-lines
+# pylint: disable=unused-import
import asyncio
from concurrent.futures import ThreadPoolExecutor
import enum
diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py
index 5095297e79543d..e51ba4d97186f1 100644
--- a/homeassistant/data_entry_flow.py
+++ b/homeassistant/data_entry_flow.py
@@ -9,6 +9,7 @@
SOURCE_USER = 'user'
SOURCE_DISCOVERY = 'discovery'
+SOURCE_IMPORT = 'import'
RESULT_TYPE_FORM = 'form'
RESULT_TYPE_CREATE_ENTRY = 'create_entry'
@@ -132,7 +133,8 @@ class FlowHandler:
VERSION = 1
@callback
- def async_show_form(self, *, step_id, data_schema=None, errors=None):
+ def async_show_form(self, *, step_id, data_schema=None, errors=None,
+ description_placeholders=None):
"""Return the definition of a form to gather user input."""
return {
'type': RESULT_TYPE_FORM,
@@ -141,6 +143,7 @@ def async_show_form(self, *, step_id, data_schema=None, errors=None):
'step_id': step_id,
'data_schema': data_schema,
'errors': errors,
+ 'description_placeholders': description_placeholders,
}
@callback
diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py
new file mode 100644
index 00000000000000..2a4ec2966df967
--- /dev/null
+++ b/homeassistant/helpers/config_entry_flow.py
@@ -0,0 +1,85 @@
+"""Helpers for data entry flows for config entries."""
+from functools import partial
+
+from homeassistant.core import callback
+from homeassistant import config_entries, data_entry_flow
+
+
+def register_discovery_flow(domain, title, discovery_function):
+ """Register flow for discovered integrations that not require auth."""
+ config_entries.HANDLERS.register(domain)(
+ partial(DiscoveryFlowHandler, domain, title, discovery_function))
+
+
+class DiscoveryFlowHandler(data_entry_flow.FlowHandler):
+ """Handle a discovery config flow."""
+
+ VERSION = 1
+
+ def __init__(self, domain, title, discovery_function):
+ """Initialize the discovery config flow."""
+ self._domain = domain
+ self._title = title
+ self._discovery_function = discovery_function
+
+ async def async_step_init(self, user_input=None):
+ """Handle a flow initialized by the user."""
+ if self._async_current_entries():
+ return self.async_abort(
+ reason='single_instance_allowed'
+ )
+
+ # Get current discovered entries.
+ in_progress = self._async_in_progress()
+
+ has_devices = in_progress
+ if not has_devices:
+ has_devices = await self.hass.async_add_job(
+ self._discovery_function, self.hass)
+
+ if not has_devices:
+ return self.async_abort(
+ reason='no_devices_found'
+ )
+
+ # Cancel the discovered one.
+ for flow in in_progress:
+ self.hass.config_entries.flow.async_abort(flow['flow_id'])
+
+ return self.async_create_entry(
+ title=self._title,
+ data={},
+ )
+
+ async def async_step_confirm(self, user_input=None):
+ """Confirm setup."""
+ if user_input is not None:
+ return self.async_create_entry(
+ title=self._title,
+ data={},
+ )
+
+ return self.async_show_form(
+ step_id='confirm',
+ )
+
+ async def async_step_discovery(self, discovery_info):
+ """Handle a flow initialized by discovery."""
+ if self._async_in_progress() or self._async_current_entries():
+ return self.async_abort(
+ reason='single_instance_allowed'
+ )
+
+ return await self.async_step_confirm()
+
+ @callback
+ def _async_current_entries(self):
+ """Return current entries."""
+ return self.hass.config_entries.async_entries(self._domain)
+
+ @callback
+ def _async_in_progress(self):
+ """Return other in progress flows for current domain."""
+ return [flw for flw in self.hass.config_entries.flow.async_progress()
+ if flw['handler'] == self._domain and
+ flw['flow_id'] != self.flow_id]
diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py
index efaefc26184644..85050b5736f4ec 100644
--- a/homeassistant/helpers/entity.py
+++ b/homeassistant/helpers/entity.py
@@ -171,13 +171,6 @@ def supported_features(self) -> int:
"""Flag supported features."""
return None
- def update(self):
- """Retrieve latest state.
-
- For asyncio use coroutine async_update.
- """
- pass
-
# DO NOT OVERWRITE
# These properties and methods are either managed by Home Assistant or they
# are used to perform a very specific function. Overwriting these may
@@ -320,10 +313,10 @@ def async_device_update(self, warning=True):
)
try:
+ # pylint: disable=no-member
if hasattr(self, 'async_update'):
- # pylint: disable=no-member
yield from self.async_update()
- else:
+ elif hasattr(self, 'update'):
yield from self.hass.async_add_job(self.update)
finally:
self._update_staged = False
diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py
index c82ae2a46f0831..4ac3a147296f5b 100644
--- a/homeassistant/helpers/entity_component.py
+++ b/homeassistant/helpers/entity_component.py
@@ -108,7 +108,8 @@ async def async_setup_entry(self, config_entry):
raise ValueError('Config entry has already been setup!')
self._platforms[key] = self._async_init_entity_platform(
- platform_type, platform
+ platform_type, platform,
+ scan_interval=getattr(platform, 'SCAN_INTERVAL', None),
)
return await self._platforms[key].async_setup_entry(config_entry)
diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py
index 00a7e49840e159..472a88888d88f5 100644
--- a/homeassistant/helpers/entity_platform.py
+++ b/homeassistant/helpers/entity_platform.py
@@ -216,6 +216,10 @@ async def async_add_entities(self, new_entities, update_before_add=False):
component_entities, registry)
for entity in new_entities]
+ # No entities for processing
+ if not tasks:
+ return
+
await asyncio.wait(tasks, loop=self.hass.loop)
self.async_entities_added_callback()
@@ -260,9 +264,15 @@ async def _async_add_entity(self, entity, update_before_add,
suggested_object_id = '{} {}'.format(
self.entity_namespace, suggested_object_id)
+ if self.config_entry is not None:
+ config_entry_id = self.config_entry.entry_id
+ else:
+ config_entry_id = None
+
entry = registry.async_get_or_create(
self.domain, self.platform_name, entity.unique_id,
- suggested_object_id=suggested_object_id)
+ suggested_object_id=suggested_object_id,
+ config_entry_id=config_entry_id)
if entry.disabled:
self.logger.info(
diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py
index 35cc1015aaf722..4a2cd5fa50c31d 100644
--- a/homeassistant/helpers/entity_registry.py
+++ b/homeassistant/helpers/entity_registry.py
@@ -43,6 +43,7 @@ class RegistryEntry:
unique_id = attr.ib(type=str)
platform = attr.ib(type=str)
name = attr.ib(type=str, default=None)
+ config_entry_id = attr.ib(type=str, default=None)
disabled_by = attr.ib(
type=str, default=None,
validator=attr.validators.in_((DISABLED_HASS, DISABLED_USER, None)))
@@ -106,7 +107,7 @@ def async_generate_entity_id(self, domain, suggested_object_id):
@callback
def async_get_or_create(self, domain, platform, unique_id, *,
- suggested_object_id=None):
+ suggested_object_id=None, config_entry_id=None):
"""Get entity. Create if it doesn't exist."""
entity_id = self.async_get_entity_id(domain, platform, unique_id)
if entity_id:
@@ -114,8 +115,10 @@ def async_get_or_create(self, domain, platform, unique_id, *,
entity_id = self.async_generate_entity_id(
domain, suggested_object_id or '{}_{}'.format(platform, unique_id))
+
entity = RegistryEntry(
entity_id=entity_id,
+ config_entry_id=config_entry_id,
unique_id=unique_id,
platform=platform,
)
@@ -179,6 +182,7 @@ async def _async_load(self):
for entity_id, info in data.items():
entities[entity_id] = RegistryEntry(
entity_id=entity_id,
+ config_entry_id=info.get('config_entry_id'),
unique_id=info['unique_id'],
platform=info['platform'],
name=info.get('name'),
@@ -205,6 +209,7 @@ async def _async_save(self):
for entry in self.entities.values():
data[entry.entity_id] = {
+ 'config_entry_id': entry.config_entry_id,
'unique_id': entry.unique_id,
'platform': entry.platform,
'name': entry.name,
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index e76dc24d9ddcb2..5e7386242baa1c 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -1,4 +1,4 @@
-aiohttp==3.2.1
+aiohttp==3.3.2
astral==1.6.1
async_timeout==3.0.0
attrs==18.1.0
diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py
index 815a5c8e55f630..7aba3b2561cbaa 100644
--- a/homeassistant/scripts/__init__.py
+++ b/homeassistant/scripts/__init__.py
@@ -1,5 +1,6 @@
"""Home Assistant command line scripts."""
import argparse
+import asyncio
import importlib
import logging
import os
@@ -7,10 +8,10 @@
from typing import List
-from homeassistant.bootstrap import mount_local_lib_path
+from homeassistant.bootstrap import async_mount_local_lib_path
from homeassistant.config import get_default_config_dir
from homeassistant import requirements
-from homeassistant.util.package import install_package
+from homeassistant.util.package import install_package, is_virtual_env
def run(args: List) -> int:
@@ -38,7 +39,11 @@ def run(args: List) -> int:
script = importlib.import_module('homeassistant.scripts.' + args[0])
config_dir = extract_config_dir()
- mount_local_lib_path(config_dir)
+
+ if not is_virtual_env():
+ asyncio.get_event_loop().run_until_complete(
+ async_mount_local_lib_path(config_dir))
+
pip_kwargs = requirements.pip_kwargs(config_dir)
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py
index 7b5b996a3a3555..cd440783cc3e01 100644
--- a/homeassistant/util/dt.py
+++ b/homeassistant/util/dt.py
@@ -27,7 +27,7 @@ def set_default_time_zone(time_zone: dt.tzinfo) -> None:
Async friendly.
"""
- global DEFAULT_TIME_ZONE # pylint: disable=global-statement
+ global DEFAULT_TIME_ZONE
# NOTE: Remove in the future in favour of typing
assert isinstance(time_zone, dt.tzinfo)
diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py
index a2f707c54f5032..d1d398020dee17 100644
--- a/homeassistant/util/package.py
+++ b/homeassistant/util/package.py
@@ -77,32 +77,16 @@ def check_package_exists(package: str) -> bool:
return any(dist in req for dist in env[req.project_name])
-def _get_user_site(deps_dir: str) -> tuple:
- """Get arguments and environment for subprocess used in get_user_site."""
- env = os.environ.copy()
- env['PYTHONUSERBASE'] = os.path.abspath(deps_dir)
- args = [sys.executable, '-m', 'site', '--user-site']
- return args, env
-
-
-def get_user_site(deps_dir: str) -> str:
- """Return user local library path."""
- args, env = _get_user_site(deps_dir)
- process = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env)
- stdout, _ = process.communicate()
- lib_dir = stdout.decode().strip()
- return lib_dir
-
-
-async def async_get_user_site(deps_dir: str,
- loop: asyncio.AbstractEventLoop) -> str:
+async def async_get_user_site(deps_dir: str) -> str:
"""Return user local library path.
This function is a coroutine.
"""
- args, env = _get_user_site(deps_dir)
+ env = os.environ.copy()
+ env['PYTHONUSERBASE'] = os.path.abspath(deps_dir)
+ args = [sys.executable, '-m', 'site', '--user-site']
process = await asyncio.create_subprocess_exec(
- *args, loop=loop, stdin=asyncio.subprocess.PIPE,
+ *args, stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL,
env=env)
stdout, _ = await process.communicate()
diff --git a/requirements_all.txt b/requirements_all.txt
index f90f4d8c23b207..52a5e0525604de 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1,5 +1,5 @@
# Home Assistant core
-aiohttp==3.2.1
+aiohttp==3.3.2
astral==1.6.1
async_timeout==3.0.0
attrs==18.1.0
@@ -31,7 +31,7 @@ DoorBirdPy==0.1.3
HAP-python==2.2.2
# homeassistant.components.notify.mastodon
-Mastodon.py==1.2.2
+Mastodon.py==1.3.0
# homeassistant.components.isy994
PyISY==1.1.0
@@ -46,7 +46,7 @@ PyMVGLive==1.1.4
PyMata==2.14
# homeassistant.components.xiaomi_aqara
-PyXiaomiGateway==0.9.4
+PyXiaomiGateway==0.9.5
# homeassistant.components.rpi_gpio
# RPi.GPIO==0.6.1
@@ -54,7 +54,7 @@ PyXiaomiGateway==0.9.4
# homeassistant.components.remember_the_milk
RtmAPI==0.7.0
-# homeassistant.components.media_player.sonos
+# homeassistant.components.sonos
SoCo==0.14
# homeassistant.components.sensor.travisci
@@ -81,6 +81,12 @@ aioautomatic==0.6.5
# homeassistant.components.sensor.dnsip
aiodns==1.1.1
+# homeassistant.components.device_tracker.freebox
+aiofreepybox==0.0.3
+
+# homeassistant.components.camera.yi
+aioftp==0.10.1
+
# homeassistant.components.emulated_hue
# homeassistant.components.http
aiohttp_cors==0.7.0
@@ -92,7 +98,7 @@ aiohue==1.5.0
aioimaplib==0.7.13
# homeassistant.components.light.lifx
-aiolifx==0.6.1
+aiolifx==0.6.3
# homeassistant.components.light.lifx
aiolifx_effects==0.1.2
@@ -107,7 +113,7 @@ alarmdecoder==1.13.2
alpha_vantage==2.0.0
# homeassistant.components.amcrest
-amcrest==1.2.2
+amcrest==1.2.3
# homeassistant.components.media_player.anthemav
anthemav==1.1.8
@@ -246,7 +252,7 @@ defusedxml==0.5.0
deluge-client==1.4.0
# homeassistant.components.media_player.denonavr
-denonavr==0.7.2
+denonavr==0.7.3
# homeassistant.components.media_player.directv
directpy==0.5
@@ -276,6 +282,9 @@ dsmr_parser==0.11
# homeassistant.components.sensor.dweet
dweepy==0.3.0
+# homeassistant.components.media_player.horizon
+einder==0.3.1
+
# homeassistant.components.sensor.eliqonline
eliqonline==1.0.14
@@ -288,6 +297,12 @@ enocean==0.40
# homeassistant.components.sensor.season
ephem==3.7.6.0
+# homeassistant.components.media_player.epson
+epson-projector==0.1.3
+
+# homeassistant.components.netgear_lte
+eternalegypt==0.0.1
+
# homeassistant.components.keyboard_remote
# evdev==0.6.1
@@ -341,7 +356,7 @@ gTTS-token==1.1.1
# gattlib==0.20150805
# homeassistant.components.sensor.gearbest
-gearbest_parser==1.0.5
+gearbest_parser==1.0.7
# homeassistant.components.sensor.gitter
gitterpy==0.1.7
@@ -389,7 +404,7 @@ hipnotify==1.0.8
holidays==0.9.5
# homeassistant.components.frontend
-home-assistant-frontend==20180608.0b0
+home-assistant-frontend==20180622.1
# homeassistant.components.homekit_controller
# homekit==0.6
@@ -409,14 +424,8 @@ httplib2==0.10.3
# homeassistant.components.media_player.braviatv
https://github.com/aparraga/braviarc/archive/0.3.7.zip#braviarc==0.3.7
-# homeassistant.components.media_player.spotify
-https://github.com/happyleavesaoc/spotipy/archive/544614f4b1d508201d363e84e871f86c90aa26b2.zip#spotipy==2.4.4
-
-# homeassistant.components.netatmo
-https://github.com/jabesq/netatmo-api-python/archive/v0.9.2.1.zip#lnetatmo==0.9.2.1
-
# homeassistant.components.neato
-https://github.com/jabesq/pybotvac/archive/v0.0.5.zip#pybotvac==0.0.5
+https://github.com/jabesq/pybotvac/archive/v0.0.6.zip#pybotvac==0.0.6
# homeassistant.components.switch.anel_pwrctrl
https://github.com/mweinelt/anel-pwrctrl/archive/ed26e8830e28a2bfa4260a9002db23ce3e7e63d7.zip#anel_pwrctrl==0.0.1
@@ -438,6 +447,9 @@ hydrawiser==0.1.1
# homeassistant.components.sensor.htu21d
# i2csense==0.0.4
+# homeassistant.components.watson_iot
+ibmiotf==0.3.4
+
# homeassistant.components.light.iglo
iglo==1.2.7
@@ -452,7 +464,7 @@ influxdb==5.0.0
insteonlocal==0.53
# homeassistant.components.insteon_plm
-insteonplm==0.9.2
+insteonplm==0.10.0
# homeassistant.components.sensor.iperf3
iperf3==0.1.10
@@ -473,6 +485,9 @@ keyring==12.2.1
# homeassistant.scripts.keyring
keyrings.alt==3.1
+# homeassistant.components.lock.kiwi
+kiwiki-client==0.1.1
+
# homeassistant.components.konnected
konnected==0.1.2
@@ -579,6 +594,9 @@ neurio==0.3.1
# homeassistant.components.sensor.nederlandse_spoorwegen
nsapi==2.7.4
+# homeassistant.components.sensor.nsw_fuel_station
+nsw-fuel-api-client==1.0.10
+
# homeassistant.components.nuheat
nuheat==0.3.0
@@ -633,6 +651,9 @@ pifacedigitalio==3.0.5
# homeassistant.components.light.piglow
piglow==1.2.4
+# homeassistant.components.sensor.pi_hole
+pihole==0.1.2
+
# homeassistant.components.pilight
pilight==0.1.1
@@ -663,7 +684,7 @@ proliphix==0.4.1
prometheus_client==0.1.0
# homeassistant.components.sensor.systemmonitor
-psutil==5.4.5
+psutil==5.4.6
# homeassistant.components.wink
pubnubsub-handler==1.0.2
@@ -698,7 +719,7 @@ pyCEC==0.4.13
# homeassistant.components.light.tplink
# homeassistant.components.switch.tplink
-pyHS100==0.3.0
+pyHS100==0.3.1
# homeassistant.components.rfxtrx
pyRFXtrx==0.22.1
@@ -716,13 +737,13 @@ py_ryobi_gdo==0.0.10
pyads==2.2.6
# homeassistant.components.sensor.airvisual
-pyairvisual==1.0.0
+pyairvisual==2.0.1
# homeassistant.components.alarm_control_panel.alarmdotcom
pyalarmdotcom==0.3.2
# homeassistant.components.arlo
-pyarlo==0.1.2
+pyarlo==0.1.7
# homeassistant.components.notify.xmpp
pyasn1-modules==0.1.5
@@ -730,6 +751,9 @@ pyasn1-modules==0.1.5
# homeassistant.components.notify.xmpp
pyasn1==0.3.7
+# homeassistant.components.netatmo
+pyatmo==1.0.0
+
# homeassistant.components.apple_tv
pyatv==0.3.10
@@ -746,7 +770,7 @@ pyblackbird==0.5
# homeassistant.components.media_player.channels
pychannels==1.0.0
-# homeassistant.components.media_player.cast
+# homeassistant.components.cast
pychromecast==2.1.0
# homeassistant.components.media_player.cmus
@@ -784,7 +808,7 @@ pyeconet==0.0.5
pyedimax==0.1
# homeassistant.components.eight_sleep
-pyeight==0.0.8
+pyeight==0.0.9
# homeassistant.components.media_player.emby
pyemby==1.5
@@ -834,6 +858,9 @@ pyialarm==0.2
# homeassistant.components.device_tracker.icloud
pyicloud==0.9.1
+# homeassistant.components.weather.ipma
+pyipma==1.1.3
+
# homeassistant.components.sensor.irish_rail_transport
pyirishrail==0.0.2
@@ -853,7 +880,7 @@ pykwb==0.0.8
pylacrosse==0.3.1
# homeassistant.components.sensor.lastfm
-pylast==2.2.0
+pylast==2.3.0
# homeassistant.components.media_player.webostv
# homeassistant.components.notify.webostv
@@ -878,7 +905,7 @@ pymailgunner==1.4
pymediaroom==0.6.3
# homeassistant.components.media_player.xiaomi_tv
-pymitv==1.0.0
+pymitv==1.4.0
# homeassistant.components.mochad
pymochad==0.2.0
@@ -893,7 +920,7 @@ pymonoprice==0.3
pymusiccast==0.1.6
# homeassistant.components.cover.myq
-pymyq==0.0.8
+pymyq==0.0.11
# homeassistant.components.mysensors
pymysensors==0.14.0
@@ -1023,17 +1050,17 @@ python-juicenet==0.0.5
# homeassistant.components.sensor.xiaomi_miio
# homeassistant.components.switch.xiaomi_miio
# homeassistant.components.vacuum.xiaomi_miio
-python-miio==0.3.9
+python-miio==0.4.0
# homeassistant.components.media_player.mpd
python-mpd2==1.0.0
# homeassistant.components.light.mystrom
# homeassistant.components.switch.mystrom
-python-mystrom==0.4.2
+python-mystrom==0.4.4
# homeassistant.components.nest
-python-nest==4.0.1
+python-nest==4.0.2
# homeassistant.components.device_tracker.nmap_tracker
python-nmap==0.6.1
@@ -1072,10 +1099,10 @@ python-velbus==2.0.11
python-vlc==1.1.2
# homeassistant.components.wink
-python-wink==1.7.3
+python-wink==1.8.0
# homeassistant.components.sensor.swiss_public_transport
-python_opendata_transport==0.1.0
+python_opendata_transport==0.1.3
# homeassistant.components.zwave
python_openzwave==0.4.3
@@ -1096,7 +1123,7 @@ pytouchline==0.7
pytrackr==0.0.5
# homeassistant.components.tradfri
-pytradfri[async]==5.4.2
+pytradfri[async]==5.5.1
# homeassistant.components.device_tracker.unifi
pyunifi==2.13
@@ -1104,6 +1131,9 @@ pyunifi==2.13
# homeassistant.components.upnp
pyupnp-async==0.1.0.2
+# homeassistant.components.binary_sensor.uptimerobot
+pyuptimerobot==0.0.5
+
# homeassistant.components.keyboard
# pyuserinput==0.1.11
@@ -1141,13 +1171,13 @@ rachiopy==0.1.2
radiotherm==1.3
# homeassistant.components.raincloud
-raincloudy==0.0.4
+raincloudy==0.0.5
# homeassistant.components.raspihats
# raspihats==2.2.3
# homeassistant.components.rainmachine
-regenmaschine==0.4.2
+regenmaschine==1.0.2
# homeassistant.components.python_script
restrictedpython==4.0b4
@@ -1156,7 +1186,7 @@ restrictedpython==4.0b4
rflink==0.0.37
# homeassistant.components.ring
-ring_doorbell==0.1.8
+ring_doorbell==0.2.1
# homeassistant.components.notify.rocketchat
rocketchat-API==0.6.1
@@ -1189,7 +1219,7 @@ schiene==0.22
scsgate==0.1.0
# homeassistant.components.notify.sendgrid
-sendgrid==5.3.0
+sendgrid==5.4.0
# homeassistant.components.light.sensehat
# homeassistant.components.sensor.sensehat
@@ -1248,6 +1278,9 @@ speedtest-cli==2.0.2
# homeassistant.components.sensor.spotcrime
spotcrime==1.0.3
+# homeassistant.components.media_player.spotify
+spotipy-homeassistant==2.4.4.dev1
+
# homeassistant.components.recorder
# homeassistant.scripts.db_migrator
# homeassistant.components.sensor.sql
@@ -1366,6 +1399,9 @@ websocket-client==0.37.0
# homeassistant.components.media_player.webostv
websockets==3.2
+# homeassistant.components.wirelesstag
+wirelesstagpy==0.3.0
+
# homeassistant.components.zigbee
xbee-helper==0.0.7
@@ -1405,6 +1441,9 @@ zengge==0.2
# homeassistant.components.zeroconf
zeroconf==0.20.0
+# homeassistant.components.climate.zhong_hong
+zhong_hong_hvac==1.0.9
+
# homeassistant.components.media_player.ziggo_mediabox_xl
ziggo-mediabox-xl==1.0.0
diff --git a/requirements_test.txt b/requirements_test.txt
index 0a4a0bcb5b04c7..7ee0e166cf2839 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -1,17 +1,17 @@
# linters such as flake8 and pylint should be pinned, as new releases
# make new things fail. Manually update these pins when pulling in a
# new version
-asynctest>=0.11.1
+asynctest==0.12.1
coveralls==1.2.0
flake8-docstrings==1.0.3
flake8==3.5
mock-open==1.3.1
mypy==0.590
pydocstyle==1.1.1
-pylint==1.8.4
+pylint==1.9.2
pytest-aiohttp==0.3.0
pytest-cov==2.5.1
pytest-sugar==0.9.1
-pytest-timeout>=1.2.1
-pytest==3.4.2
+pytest-timeout==1.3.0
+pytest==3.6.1
requests_mock==1.5
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 9c3486d104e02e..a38c7f259b4782 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -2,19 +2,19 @@
# linters such as flake8 and pylint should be pinned, as new releases
# make new things fail. Manually update these pins when pulling in a
# new version
-asynctest>=0.11.1
+asynctest==0.12.1
coveralls==1.2.0
flake8-docstrings==1.0.3
flake8==3.5
mock-open==1.3.1
mypy==0.590
pydocstyle==1.1.1
-pylint==1.8.4
+pylint==1.9.2
pytest-aiohttp==0.3.0
pytest-cov==2.5.1
pytest-sugar==0.9.1
-pytest-timeout>=1.2.1
-pytest==3.4.2
+pytest-timeout==1.3.0
+pytest==3.6.1
requests_mock==1.5
@@ -24,7 +24,7 @@ HAP-python==2.2.2
# homeassistant.components.notify.html5
PyJWT==1.6.0
-# homeassistant.components.media_player.sonos
+# homeassistant.components.sonos
SoCo==0.14
# homeassistant.components.device_tracker.automatic
@@ -81,7 +81,7 @@ hbmqtt==0.9.2
holidays==0.9.5
# homeassistant.components.frontend
-home-assistant-frontend==20180608.0b0
+home-assistant-frontend==20180622.1
# homeassistant.components.influxdb
# homeassistant.components.sensor.influxdb
@@ -155,9 +155,15 @@ pyqwikswitch==0.8
# homeassistant.components.weather.darksky
python-forecastio==1.4.0
+# homeassistant.components.nest
+python-nest==4.0.2
+
# homeassistant.components.sensor.whois
pythonwhois==2.4.3
+# homeassistant.components.tradfri
+pytradfri[async]==5.5.1
+
# homeassistant.components.device_tracker.unifi
pyunifi==2.13
@@ -174,7 +180,7 @@ restrictedpython==4.0b4
rflink==0.0.37
# homeassistant.components.ring
-ring_doorbell==0.1.8
+ring_doorbell==0.2.1
# homeassistant.components.media_player.yamaha
rxv==0.5.1
diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py
index b5b636dc8745d1..7bf87c74de7266 100755
--- a/script/gen_requirements_all.py
+++ b/script/gen_requirements_all.py
@@ -77,6 +77,8 @@
'pynx584',
'pyqwikswitch',
'python-forecastio',
+ 'python-nest',
+ 'pytradfri\[async\]',
'pyunifi',
'pyupnp-async',
'pywebpush',
diff --git a/script/version_bump.py b/script/version_bump.py
index 59060a7075b024..e324b231d0667d 100755
--- a/script/version_bump.py
+++ b/script/version_bump.py
@@ -2,6 +2,7 @@
"""Helper script to bump the current version."""
import argparse
import re
+import subprocess
from packaging.version import Version
@@ -117,12 +118,21 @@ def main():
help="The type of the bump the version to.",
choices=['beta', 'dev', 'patch', 'minor'],
)
+ parser.add_argument(
+ '--commit', action='store_true',
+ help='Create a version bump commit.')
arguments = parser.parse_args()
current = Version(const.__version__)
bumped = bump_version(current, arguments.type)
assert bumped > current, 'BUG! New version is not newer than old version'
write_version(bumped)
+ if not arguments.commit:
+ return
+
+ subprocess.run([
+ 'git', 'commit', '-am', 'Bumped version to {}'.format(bumped)])
+
def test_bump_version():
"""Make sure it all works."""
diff --git a/setup.py b/setup.py
index 4390b980f9e948..f914e032fd7325 100755
--- a/setup.py
+++ b/setup.py
@@ -42,7 +42,7 @@
PACKAGES = find_packages(exclude=['tests', 'tests.*'])
REQUIRES = [
- 'aiohttp==3.2.1',
+ 'aiohttp==3.3.2',
'astral==1.6.1',
'async_timeout==3.0.0',
'attrs==18.1.0',
diff --git a/tests/common.py b/tests/common.py
index f53d1c2be2ba51..556935a6ac173a 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -373,13 +373,16 @@ class MockPlatform(object):
# pylint: disable=invalid-name
def __init__(self, setup_platform=None, dependencies=None,
platform_schema=None, async_setup_platform=None,
- async_setup_entry=None):
+ async_setup_entry=None, scan_interval=None):
"""Initialize the platform."""
self.DEPENDENCIES = dependencies or []
if platform_schema is not None:
self.PLATFORM_SCHEMA = platform_schema
+ if scan_interval is not None:
+ self.SCAN_INTERVAL = scan_interval
+
if setup_platform is not None:
# We run this in executor, wrap it in function
self.setup_platform = lambda *args: setup_platform(*args)
diff --git a/tests/components/binary_sensor/test_mqtt.py b/tests/components/binary_sensor/test_mqtt.py
index 9b5cf7aa736eeb..71eba2df95039f 100644
--- a/tests/components/binary_sensor/test_mqtt.py
+++ b/tests/components/binary_sensor/test_mqtt.py
@@ -77,6 +77,25 @@ def test_invalid_device_class(self):
state = self.hass.states.get('binary_sensor.test')
self.assertIsNone(state)
+ def test_unique_id(self):
+ """Test unique id option only creates one sensor per unique_id."""
+ assert setup_component(self.hass, binary_sensor.DOMAIN, {
+ binary_sensor.DOMAIN: [{
+ 'platform': 'mqtt',
+ 'name': 'Test 1',
+ 'state_topic': 'test-topic',
+ 'unique_id': 'TOTALLY_UNIQUE'
+ }, {
+ 'platform': 'mqtt',
+ 'name': 'Test 2',
+ 'state_topic': 'test-topic',
+ 'unique_id': 'TOTALLY_UNIQUE'
+ }]
+ })
+ fire_mqtt_message(self.hass, 'test-topic', 'payload')
+ self.hass.block_till_done()
+ assert len(self.hass.states.all()) == 1
+
def test_availability_without_topic(self):
"""Test availability without defined availability topic."""
self.assertTrue(setup_component(self.hass, binary_sensor.DOMAIN, {
diff --git a/tests/components/binary_sensor/test_ring.py b/tests/components/binary_sensor/test_ring.py
index 889282b56dd98f..e557050ae48782 100644
--- a/tests/components/binary_sensor/test_ring.py
+++ b/tests/components/binary_sensor/test_ring.py
@@ -44,6 +44,8 @@ def tearDown(self):
@requests_mock.Mocker()
def test_binary_sensor(self, mock):
"""Test the Ring sensor class and methods."""
+ mock.post('https://oauth.ring.com/oauth/token',
+ text=load_fixture('ring_oauth.json'))
mock.post('https://api.ring.com/clients_api/session',
text=load_fixture('ring_session.json'))
mock.get('https://api.ring.com/clients_api/ring_devices',
diff --git a/tests/components/calendar/test_caldav.py b/tests/components/calendar/test_caldav.py
index 11dd0cb963535d..c5dadbc56eaca2 100644
--- a/tests/components/calendar/test_caldav.py
+++ b/tests/components/calendar/test_caldav.py
@@ -19,7 +19,7 @@
DEVICE_DATA = {
"name": "Private Calendar",
- "device_id": "Private Calendar"
+ "device_id": "Private Calendar",
}
EVENTS = [
@@ -163,6 +163,7 @@ class TestComponentsWebDavCalendar(unittest.TestCase):
def setUp(self):
"""Set up things to be run when tests are started."""
self.hass = get_test_home_assistant()
+ self.hass.http = Mock()
self.calendar = _mock_calendar("Private")
# pylint: disable=invalid-name
@@ -255,7 +256,7 @@ def test_ongoing_event(self, mock_now):
"start_time": "2017-11-27 17:00:00",
"end_time": "2017-11-27 18:00:00",
"location": "Hamburg",
- "description": "Surprisingly rainy"
+ "description": "Surprisingly rainy",
})
@patch('homeassistant.util.dt.now', return_value=_local_datetime(17, 30))
@@ -274,7 +275,7 @@ def test_just_ended_event(self, mock_now):
"start_time": "2017-11-27 17:00:00",
"end_time": "2017-11-27 18:00:00",
"location": "Hamburg",
- "description": "Surprisingly rainy"
+ "description": "Surprisingly rainy",
})
@patch('homeassistant.util.dt.now', return_value=_local_datetime(17, 00))
@@ -293,7 +294,7 @@ def test_ongoing_event_different_tz(self, mock_now):
"start_time": "2017-11-27 16:30:00",
"description": "Sunny day",
"end_time": "2017-11-27 17:30:00",
- "location": "San Francisco"
+ "location": "San Francisco",
})
@patch('homeassistant.util.dt.now', return_value=_local_datetime(8, 30))
@@ -311,7 +312,7 @@ def test_ongoing_event_with_offset(self, mock_now):
"start_time": "2017-11-27 10:00:00",
"end_time": "2017-11-27 11:00:00",
"location": "Hamburg",
- "description": "Surprisingly shiny"
+ "description": "Surprisingly shiny",
})
@patch('homeassistant.util.dt.now', return_value=_local_datetime(12, 00))
@@ -332,7 +333,7 @@ def test_matching_filter(self, mock_now):
"start_time": "2017-11-27 17:00:00",
"end_time": "2017-11-27 18:00:00",
"location": "Hamburg",
- "description": "Surprisingly rainy"
+ "description": "Surprisingly rainy",
})
@patch('homeassistant.util.dt.now', return_value=_local_datetime(12, 00))
@@ -353,7 +354,7 @@ def test_matching_filter_real_regexp(self, mock_now):
"start_time": "2017-11-27 17:00:00",
"end_time": "2017-11-27 18:00:00",
"location": "Hamburg",
- "description": "Surprisingly rainy"
+ "description": "Surprisingly rainy",
})
@patch('homeassistant.util.dt.now', return_value=_local_datetime(20, 00))
@@ -395,5 +396,5 @@ def test_all_day_event_returned(self, mock_now):
"start_time": "2017-11-27 00:00:00",
"end_time": "2017-11-28 00:00:00",
"location": "Hamburg",
- "description": "What a beautiful day"
+ "description": "What a beautiful day",
})
diff --git a/tests/components/calendar/test_demo.py b/tests/components/calendar/test_demo.py
new file mode 100644
index 00000000000000..09c6a06a54ec0c
--- /dev/null
+++ b/tests/components/calendar/test_demo.py
@@ -0,0 +1 @@
+"""The tests for the demo calendar component."""
diff --git a/tests/components/calendar/test_google.py b/tests/components/calendar/test_google.py
index 9f94ea9f44c370..d176cd758b43a9 100644
--- a/tests/components/calendar/test_google.py
+++ b/tests/components/calendar/test_google.py
@@ -27,6 +27,7 @@ class TestComponentsGoogleCalendar(unittest.TestCase):
def setUp(self):
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
+ self.hass.http = Mock()
# Set our timezone to CST/Regina so we can check calculations
# This keeps UTC-6 all year round
@@ -99,7 +100,7 @@ def test_all_day_event(self, mock_next_event):
'start_time': '{} 00:00:00'.format(event['start']['date']),
'end_time': '{} 00:00:00'.format(event['end']['date']),
'location': event['location'],
- 'description': event['description']
+ 'description': event['description'],
})
@patch('homeassistant.components.calendar.google.GoogleCalendarData')
@@ -160,7 +161,7 @@ def test_future_event(self, mock_next_event):
(one_hour_from_now + dt_util.dt.timedelta(minutes=60))
.strftime(DATE_STR_FORMAT),
'location': '',
- 'description': ''
+ 'description': '',
})
@patch('homeassistant.components.calendar.google.GoogleCalendarData')
@@ -222,7 +223,7 @@ def test_in_progress_event(self, mock_next_event):
(middle_of_event + dt_util.dt.timedelta(minutes=60))
.strftime(DATE_STR_FORMAT),
'location': '',
- 'description': ''
+ 'description': '',
})
@patch('homeassistant.components.calendar.google.GoogleCalendarData')
@@ -285,7 +286,7 @@ def test_offset_in_progress_event(self, mock_next_event):
(middle_of_event + dt_util.dt.timedelta(minutes=60))
.strftime(DATE_STR_FORMAT),
'location': '',
- 'description': ''
+ 'description': '',
})
@pytest.mark.skip
@@ -352,7 +353,7 @@ def test_all_day_offset_in_progress_event(self, mock_next_event):
'start_time': '{} 06:00:00'.format(event['start']['date']),
'end_time': '{} 06:00:00'.format(event['end']['date']),
'location': event['location'],
- 'description': event['description']
+ 'description': event['description'],
})
@patch('homeassistant.components.calendar.google.GoogleCalendarData')
@@ -419,7 +420,7 @@ def test_all_day_offset_event(self, mock_next_event):
'start_time': '{} 00:00:00'.format(event['start']['date']),
'end_time': '{} 00:00:00'.format(event['end']['date']),
'location': event['location'],
- 'description': event['description']
+ 'description': event['description'],
})
@MockDependency("httplib2")
diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py
index 164c3f57f52495..a5f6a751b46f2e 100644
--- a/tests/components/calendar/test_init.py
+++ b/tests/components/calendar/test_init.py
@@ -1 +1,38 @@
"""The tests for the calendar component."""
+from datetime import timedelta
+
+from homeassistant.bootstrap import async_setup_component
+import homeassistant.util.dt as dt_util
+
+
+async def test_events_http_api(hass, aiohttp_client):
+ """Test the calendar demo view."""
+ await async_setup_component(hass, 'calendar',
+ {'calendar': {'platform': 'demo'}})
+ client = await aiohttp_client(hass.http.app)
+ response = await client.get(
+ '/api/calendars/calendar.calendar_2')
+ assert response.status == 400
+ start = dt_util.now()
+ end = start + timedelta(days=1)
+ response = await client.get(
+ '/api/calendars/calendar.calendar_1?start={}&end={}'.format(
+ start.isoformat(), end.isoformat()))
+ assert response.status == 200
+ events = await response.json()
+ assert events[0]['summary'] == 'Future Event'
+ assert events[0]['title'] == 'Future Event'
+
+
+async def test_calendars_http_api(hass, aiohttp_client):
+ """Test the calendar demo view."""
+ await async_setup_component(hass, 'calendar',
+ {'calendar': {'platform': 'demo'}})
+ client = await aiohttp_client(hass.http.app)
+ response = await client.get('/api/calendars')
+ assert response.status == 200
+ data = await response.json()
+ assert data == [
+ {'entity_id': 'calendar.calendar_1', 'name': 'Calendar 1'},
+ {'entity_id': 'calendar.calendar_2', 'name': 'Calendar 2'}
+ ]
diff --git a/tests/components/camera/test_uvc.py b/tests/components/camera/test_uvc.py
index dabad953bea13a..2de0782fd9109d 100644
--- a/tests/components/camera/test_uvc.py
+++ b/tests/components/camera/test_uvc.py
@@ -7,6 +7,7 @@
from uvcclient import camera
from uvcclient import nvr
+from homeassistant.exceptions import PlatformNotReady
from homeassistant.setup import setup_component
from homeassistant.components.camera import uvc
from tests.common import get_test_home_assistant
@@ -34,21 +35,21 @@ def test_setup_full_config(self, mock_uvc, mock_remote):
'port': 123,
'key': 'secret',
}
- fake_cameras = [
+ mock_cameras = [
{'uuid': 'one', 'name': 'Front', 'id': 'id1'},
{'uuid': 'two', 'name': 'Back', 'id': 'id2'},
{'uuid': 'three', 'name': 'Old AirCam', 'id': 'id3'},
]
- def fake_get_camera(uuid):
- """Create a fake camera."""
+ def mock_get_camera(uuid):
+ """Create a mock camera."""
if uuid == 'id3':
return {'model': 'airCam'}
else:
return {'model': 'UVC'}
- mock_remote.return_value.index.return_value = fake_cameras
- mock_remote.return_value.get_camera.side_effect = fake_get_camera
+ mock_remote.return_value.index.return_value = mock_cameras
+ mock_remote.return_value.get_camera.side_effect = mock_get_camera
mock_remote.return_value.server_version = (3, 2, 0)
assert setup_component(self.hass, 'camera', {'camera': config})
@@ -71,11 +72,11 @@ def test_setup_partial_config(self, mock_uvc, mock_remote):
'nvr': 'foo',
'key': 'secret',
}
- fake_cameras = [
+ mock_cameras = [
{'uuid': 'one', 'name': 'Front', 'id': 'id1'},
{'uuid': 'two', 'name': 'Back', 'id': 'id2'},
]
- mock_remote.return_value.index.return_value = fake_cameras
+ mock_remote.return_value.index.return_value = mock_cameras
mock_remote.return_value.get_camera.return_value = {'model': 'UVC'}
mock_remote.return_value.server_version = (3, 2, 0)
@@ -99,11 +100,11 @@ def test_setup_partial_config_v31x(self, mock_uvc, mock_remote):
'nvr': 'foo',
'key': 'secret',
}
- fake_cameras = [
+ mock_cameras = [
{'uuid': 'one', 'name': 'Front', 'id': 'id1'},
{'uuid': 'two', 'name': 'Back', 'id': 'id2'},
]
- mock_remote.return_value.index.return_value = fake_cameras
+ mock_remote.return_value.index.return_value = mock_cameras
mock_remote.return_value.get_camera.return_value = {'model': 'UVC'}
mock_remote.return_value.server_version = (3, 1, 3)
@@ -133,19 +134,62 @@ def test_setup_incomplete_config(self, mock_uvc):
@mock.patch.object(uvc, 'UnifiVideoCamera')
@mock.patch('uvcclient.nvr.UVCRemote')
- def test_setup_nvr_errors(self, mock_remote, mock_uvc):
- """Test for NVR errors."""
- errors = [nvr.NotAuthorized, nvr.NvrError,
- requests.exceptions.ConnectionError]
+ def setup_nvr_errors_during_indexing(self, error, mock_remote, mock_uvc):
+ """Setup test for NVR errors during indexing."""
config = {
'platform': 'uvc',
'nvr': 'foo',
'key': 'secret',
}
- for error in errors:
- mock_remote.return_value.index.side_effect = error
- assert setup_component(self.hass, 'camera', config)
- assert not mock_uvc.called
+ mock_remote.return_value.index.side_effect = error
+ assert setup_component(self.hass, 'camera', {'camera': config})
+ assert not mock_uvc.called
+
+ def test_setup_nvr_error_during_indexing_notauthorized(self):
+ """Test for error: nvr.NotAuthorized."""
+ self.setup_nvr_errors_during_indexing(nvr.NotAuthorized)
+
+ def test_setup_nvr_error_during_indexing_nvrerror(self):
+ """Test for error: nvr.NvrError."""
+ self.setup_nvr_errors_during_indexing(nvr.NvrError)
+ self.assertRaises(PlatformNotReady)
+
+ def test_setup_nvr_error_during_indexing_connectionerror(self):
+ """Test for error: requests.exceptions.ConnectionError."""
+ self.setup_nvr_errors_during_indexing(
+ requests.exceptions.ConnectionError)
+ self.assertRaises(PlatformNotReady)
+
+ @mock.patch.object(uvc, 'UnifiVideoCamera')
+ @mock.patch('uvcclient.nvr.UVCRemote.__init__')
+ def setup_nvr_errors_during_initialization(self, error, mock_remote,
+ mock_uvc):
+ """Setup test for NVR errors during initialization."""
+ config = {
+ 'platform': 'uvc',
+ 'nvr': 'foo',
+ 'key': 'secret',
+ }
+ mock_remote.return_value = None
+ mock_remote.side_effect = error
+ assert setup_component(self.hass, 'camera', {'camera': config})
+ assert not mock_remote.index.called
+ assert not mock_uvc.called
+
+ def test_setup_nvr_error_during_initialization_notauthorized(self):
+ """Test for error: nvr.NotAuthorized."""
+ self.setup_nvr_errors_during_initialization(nvr.NotAuthorized)
+
+ def test_setup_nvr_error_during_initialization_nvrerror(self):
+ """Test for error: nvr.NvrError."""
+ self.setup_nvr_errors_during_initialization(nvr.NvrError)
+ self.assertRaises(PlatformNotReady)
+
+ def test_setup_nvr_error_during_initialization_connectionerror(self):
+ """Test for error: requests.exceptions.ConnectionError."""
+ self.setup_nvr_errors_during_initialization(
+ requests.exceptions.ConnectionError)
+ self.assertRaises(PlatformNotReady)
class TestUVC(unittest.TestCase):
@@ -208,8 +252,8 @@ def test_login_tries_both_addrs_and_caches(self, mock_camera, mock_store):
"""Test the login tries."""
responses = [0]
- def fake_login(*a):
- """Fake login."""
+ def mock_login(*a):
+ """Mock login."""
try:
responses.pop(0)
raise socket.error
@@ -217,7 +261,7 @@ def fake_login(*a):
pass
mock_store.return_value.get_camera_password.return_value = None
- mock_camera.return_value.login.side_effect = fake_login
+ mock_camera.return_value.login.side_effect = mock_login
self.uvc._login()
self.assertEqual(2, mock_camera.call_count)
self.assertEqual('host-b', self.uvc._connect_addr)
@@ -263,8 +307,8 @@ def test_camera_image_reauths(self):
"""Test the re-authentication."""
responses = [0]
- def fake_snapshot():
- """Fake snapshot."""
+ def mock_snapshot():
+ """Mock snapshot."""
try:
responses.pop()
raise camera.CameraAuthError()
@@ -273,7 +317,7 @@ def fake_snapshot():
return 'image'
self.uvc._camera = mock.MagicMock()
- self.uvc._camera.get_snapshot.side_effect = fake_snapshot
+ self.uvc._camera.get_snapshot.side_effect = mock_snapshot
with mock.patch.object(self.uvc, '_login') as mock_login:
self.assertEqual('image', self.uvc.camera_image())
self.assertEqual(mock_login.call_count, 1)
diff --git a/tests/components/cast/__init__.py b/tests/components/cast/__init__.py
new file mode 100644
index 00000000000000..7e904dce00af63
--- /dev/null
+++ b/tests/components/cast/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Cast component."""
diff --git a/tests/components/cast/test_init.py b/tests/components/cast/test_init.py
new file mode 100644
index 00000000000000..260856c6742e2c
--- /dev/null
+++ b/tests/components/cast/test_init.py
@@ -0,0 +1,22 @@
+"""Tests for the Cast config flow."""
+from unittest.mock import patch
+
+from homeassistant import data_entry_flow
+from homeassistant.components import cast
+
+from tests.common import MockDependency, mock_coro
+
+
+async def test_creating_entry_sets_up_media_player(hass):
+ """Test setting up Cast loads the media player."""
+ with patch('homeassistant.components.media_player.cast.async_setup_entry',
+ return_value=mock_coro(True)) as mock_setup, \
+ MockDependency('pychromecast', 'discovery'), \
+ patch('pychromecast.discovery.discover_chromecasts',
+ return_value=True):
+ result = await hass.config_entries.flow.async_init(cast.DOMAIN)
+ assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+ await hass.async_block_till_done()
+
+ assert len(mock_setup.mock_calls) == 1
diff --git a/tests/components/climate/test_fritzbox.py b/tests/components/climate/test_fritzbox.py
new file mode 100644
index 00000000000000..ccffef9e547b9e
--- /dev/null
+++ b/tests/components/climate/test_fritzbox.py
@@ -0,0 +1,172 @@
+"""The tests for the demo climate component."""
+import unittest
+from unittest.mock import Mock, patch
+
+import requests
+
+from homeassistant.components.climate.fritzbox import FritzboxThermostat
+
+
+class TestFritzboxClimate(unittest.TestCase):
+ """Test Fritz!Box heating thermostats."""
+
+ def setUp(self):
+ """Create a mock device to test on."""
+ self.device = Mock()
+ self.device.name = 'Test Thermostat'
+ self.device.actual_temperature = 18.0
+ self.device.target_temperature = 19.5
+ self.device.comfort_temperature = 22.0
+ self.device.eco_temperature = 16.0
+ self.device.present = True
+ self.device.device_lock = True
+ self.device.lock = False
+ self.device.battery_low = True
+ self.device.set_target_temperature = Mock()
+ self.device.update = Mock()
+ mock_fritz = Mock()
+ mock_fritz.login = Mock()
+ self.thermostat = FritzboxThermostat(self.device, mock_fritz)
+
+ def test_init(self):
+ """Test instance creation."""
+ self.assertEqual(18.0, self.thermostat._current_temperature)
+ self.assertEqual(19.5, self.thermostat._target_temperature)
+ self.assertEqual(22.0, self.thermostat._comfort_temperature)
+ self.assertEqual(16.0, self.thermostat._eco_temperature)
+
+ def test_supported_features(self):
+ """Test supported features property."""
+ self.assertEqual(129, self.thermostat.supported_features)
+
+ def test_available(self):
+ """Test available property."""
+ self.assertTrue(self.thermostat.available)
+ self.thermostat._device.present = False
+ self.assertFalse(self.thermostat.available)
+
+ def test_name(self):
+ """Test name property."""
+ self.assertEqual('Test Thermostat', self.thermostat.name)
+
+ def test_temperature_unit(self):
+ """Test temperature_unit property."""
+ self.assertEqual('°C', self.thermostat.temperature_unit)
+
+ def test_precision(self):
+ """Test precision property."""
+ self.assertEqual(0.5, self.thermostat.precision)
+
+ def test_current_temperature(self):
+ """Test current_temperature property incl. special temperatures."""
+ self.assertEqual(18, self.thermostat.current_temperature)
+
+ def test_target_temperature(self):
+ """Test target_temperature property."""
+ self.assertEqual(19.5, self.thermostat.target_temperature)
+
+ self.thermostat._target_temperature = 126.5
+ self.assertEqual(None, self.thermostat.target_temperature)
+
+ self.thermostat._target_temperature = 127.0
+ self.assertEqual(None, self.thermostat.target_temperature)
+
+ @patch.object(FritzboxThermostat, 'set_operation_mode')
+ def test_set_temperature_operation_mode(self, mock_set_op):
+ """Test set_temperature by operation_mode."""
+ self.thermostat.set_temperature(operation_mode='test_mode')
+ mock_set_op.assert_called_once_with('test_mode')
+
+ def test_set_temperature_temperature(self):
+ """Test set_temperature by temperature."""
+ self.thermostat.set_temperature(temperature=23.0)
+ self.thermostat._device.set_target_temperature.\
+ assert_called_once_with(23.0)
+
+ @patch.object(FritzboxThermostat, 'set_operation_mode')
+ def test_set_temperature_none(self, mock_set_op):
+ """Test set_temperature with no arguments."""
+ self.thermostat.set_temperature()
+ mock_set_op.assert_not_called()
+ self.thermostat._device.set_target_temperature.assert_not_called()
+
+ @patch.object(FritzboxThermostat, 'set_operation_mode')
+ def test_set_temperature_operation_mode_precedence(self, mock_set_op):
+ """Test set_temperature for precedence of operation_mode arguement."""
+ self.thermostat.set_temperature(operation_mode='test_mode',
+ temperature=23.0)
+ mock_set_op.assert_called_once_with('test_mode')
+ self.thermostat._device.set_target_temperature.assert_not_called()
+
+ def test_current_operation(self):
+ """Test operation mode property for different temperatures."""
+ self.thermostat._target_temperature = 127.0
+ self.assertEqual('on', self.thermostat.current_operation)
+ self.thermostat._target_temperature = 126.5
+ self.assertEqual('off', self.thermostat.current_operation)
+ self.thermostat._target_temperature = 22.0
+ self.assertEqual('heat', self.thermostat.current_operation)
+ self.thermostat._target_temperature = 16.0
+ self.assertEqual('eco', self.thermostat.current_operation)
+ self.thermostat._target_temperature = 12.5
+ self.assertEqual('manual', self.thermostat.current_operation)
+
+ def test_operation_list(self):
+ """Test operation_list property."""
+ self.assertEqual(['heat', 'eco', 'off', 'on'],
+ self.thermostat.operation_list)
+
+ @patch.object(FritzboxThermostat, 'set_temperature')
+ def test_set_operation_mode(self, mock_set_temp):
+ """Test set_operation_mode by all modes and with a non-existing one."""
+ values = {
+ 'heat': 22.0,
+ 'eco': 16.0,
+ 'on': 30.0,
+ 'off': 0.0}
+ for mode, temp in values.items():
+ print(mode, temp)
+
+ mock_set_temp.reset_mock()
+ self.thermostat.set_operation_mode(mode)
+ mock_set_temp.assert_called_once_with(temperature=temp)
+
+ mock_set_temp.reset_mock()
+ self.thermostat.set_operation_mode('non_existing_mode')
+ mock_set_temp.assert_not_called()
+
+ def test_min_max_temperature(self):
+ """Test min_temp and max_temp properties."""
+ self.assertEqual(8.0, self.thermostat.min_temp)
+ self.assertEqual(28.0, self.thermostat.max_temp)
+
+ def test_device_state_attributes(self):
+ """Test device_state property."""
+ attr = self.thermostat.device_state_attributes
+ self.assertEqual(attr['device_locked'], True)
+ self.assertEqual(attr['locked'], False)
+ self.assertEqual(attr['battery_low'], True)
+
+ def test_update(self):
+ """Test update function."""
+ device = Mock()
+ device.update = Mock()
+ device.actual_temperature = 10.0
+ device.target_temperature = 11.0
+ device.comfort_temperature = 12.0
+ device.eco_temperature = 13.0
+ self.thermostat._device = device
+
+ self.thermostat.update()
+
+ device.update.assert_called_once_with()
+ self.assertEqual(10.0, self.thermostat._current_temperature)
+ self.assertEqual(11.0, self.thermostat._target_temperature)
+ self.assertEqual(12.0, self.thermostat._comfort_temperature)
+ self.assertEqual(13.0, self.thermostat._eco_temperature)
+
+ def test_update_http_error(self):
+ """Test exception handling of update function."""
+ self.device.update.side_effect = requests.exceptions.HTTPError
+ self.thermostat.update()
+ self.thermostat._fritz.login.assert_called_once_with()
diff --git a/tests/components/climate/test_mqtt.py b/tests/components/climate/test_mqtt.py
index 663393503aca86..255d482d584394 100644
--- a/tests/components/climate/test_mqtt.py
+++ b/tests/components/climate/test_mqtt.py
@@ -9,9 +9,9 @@
from homeassistant.components import climate
from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE
from homeassistant.components.climate import (
- SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE,
- SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_HOLD_MODE,
- SUPPORT_AWAY_MODE, SUPPORT_AUX_HEAT)
+ SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE,
+ SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_HOLD_MODE,
+ SUPPORT_AWAY_MODE, SUPPORT_AUX_HEAT, DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP)
from tests.common import (get_test_home_assistant, mock_mqtt_component,
fire_mqtt_message, mock_component)
@@ -53,6 +53,8 @@ def test_setup_params(self):
self.assertEqual("low", state.attributes.get('fan_mode'))
self.assertEqual("off", state.attributes.get('swing_mode'))
self.assertEqual("off", state.attributes.get('operation_mode'))
+ self.assertEqual(DEFAULT_MIN_TEMP, state.attributes.get('min_temp'))
+ self.assertEqual(DEFAULT_MAX_TEMP, state.attributes.get('max_temp'))
def test_supported_features(self):
"""Test the supported_features."""
@@ -541,3 +543,29 @@ def test_set_with_templates(self):
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual(74656, state.attributes.get('current_temperature'))
+
+ def test_min_temp_custom(self):
+ """Test a custom min temp."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config['climate']['min_temp'] = 26
+
+ assert setup_component(self.hass, climate.DOMAIN, config)
+
+ state = self.hass.states.get(ENTITY_CLIMATE)
+ min_temp = state.attributes.get('min_temp')
+
+ self.assertIsInstance(min_temp, float)
+ self.assertEqual(26, state.attributes.get('min_temp'))
+
+ def test_max_temp_custom(self):
+ """Test a custom max temp."""
+ config = copy.deepcopy(DEFAULT_CONFIG)
+ config['climate']['max_temp'] = 60
+
+ assert setup_component(self.hass, climate.DOMAIN, config)
+
+ state = self.hass.states.get(ENTITY_CLIMATE)
+ max_temp = state.attributes.get('max_temp')
+
+ self.assertIsInstance(max_temp, float)
+ self.assertEqual(60, max_temp)
diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py
index 84d15578e13d99..82c747da01c750 100644
--- a/tests/components/config/test_config_entries.py
+++ b/tests/components/config/test_config_entries.py
@@ -110,6 +110,9 @@ def async_step_init(self, user_input=None):
return self.async_show_form(
step_id='init',
data_schema=schema,
+ description_placeholders={
+ 'url': 'https://example.com',
+ },
errors={
'username': 'Should be unique.'
}
@@ -140,6 +143,9 @@ def async_step_init(self, user_input=None):
'type': 'string'
}
],
+ 'description_placeholders': {
+ 'url': 'https://example.com',
+ },
'errors': {
'username': 'Should be unique.'
}
@@ -242,6 +248,7 @@ def async_step_account(self, user_input=None):
'type': 'string'
}
],
+ 'description_placeholders': None,
'errors': None
}
diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py
index fd7c69994776d8..1591b8da1d2c34 100644
--- a/tests/components/config/test_entity_registry.py
+++ b/tests/components/config/test_entity_registry.py
@@ -1,18 +1,16 @@
"""Test entity_registry API."""
import pytest
-from homeassistant.setup import async_setup_component
from homeassistant.helpers.entity_registry import RegistryEntry
from homeassistant.components.config import entity_registry
from tests.common import mock_registry, MockEntity, MockEntityPlatform
@pytest.fixture
-def client(hass, aiohttp_client):
+def client(hass, hass_ws_client):
"""Fixture that can interact with the config manager API."""
- hass.loop.run_until_complete(async_setup_component(hass, 'http', {}))
hass.loop.run_until_complete(entity_registry.async_setup(hass))
- yield hass.loop.run_until_complete(aiohttp_client(hass.http.app))
+ yield hass.loop.run_until_complete(hass_ws_client(hass))
async def test_get_entity(hass, client):
@@ -31,20 +29,26 @@ async def test_get_entity(hass, client):
),
})
- resp = await client.get(
- '/api/config/entity_registry/test_domain.name')
- assert resp.status == 200
- data = await resp.json()
- assert data == {
+ await client.send_json({
+ 'id': 5,
+ 'type': 'config/entity_registry/get',
+ 'entity_id': 'test_domain.name',
+ })
+ msg = await client.receive_json()
+
+ assert msg['result'] == {
'entity_id': 'test_domain.name',
'name': 'Hello World'
}
- resp = await client.get(
- '/api/config/entity_registry/test_domain.no_name')
- assert resp.status == 200
- data = await resp.json()
- assert data == {
+ await client.send_json({
+ 'id': 6,
+ 'type': 'config/entity_registry/get',
+ 'entity_id': 'test_domain.no_name',
+ })
+ msg = await client.receive_json()
+
+ assert msg['result'] == {
'entity_id': 'test_domain.no_name',
'name': None
}
@@ -69,13 +73,16 @@ async def test_update_entity(hass, client):
assert state is not None
assert state.name == 'before update'
- resp = await client.post(
- '/api/config/entity_registry/test_domain.world', json={
- 'name': 'after update'
- })
- assert resp.status == 200
- data = await resp.json()
- assert data == {
+ await client.send_json({
+ 'id': 6,
+ 'type': 'config/entity_registry/update',
+ 'entity_id': 'test_domain.world',
+ 'name': 'after update',
+ })
+
+ msg = await client.receive_json()
+
+ assert msg['result'] == {
'entity_id': 'test_domain.world',
'name': 'after update'
}
@@ -103,13 +110,16 @@ async def test_update_entity_no_changes(hass, client):
assert state is not None
assert state.name == 'name of entity'
- resp = await client.post(
- '/api/config/entity_registry/test_domain.world', json={
- 'name': 'name of entity'
- })
- assert resp.status == 200
- data = await resp.json()
- assert data == {
+ await client.send_json({
+ 'id': 6,
+ 'type': 'config/entity_registry/update',
+ 'entity_id': 'test_domain.world',
+ 'name': 'name of entity',
+ })
+
+ msg = await client.receive_json()
+
+ assert msg['result'] == {
'entity_id': 'test_domain.world',
'name': 'name of entity'
}
@@ -120,15 +130,24 @@ async def test_update_entity_no_changes(hass, client):
async def test_get_nonexisting_entity(client):
"""Test get entry."""
- resp = await client.get(
- '/api/config/entity_registry/test_domain.non_existing')
- assert resp.status == 404
+ await client.send_json({
+ 'id': 6,
+ 'type': 'config/entity_registry/get',
+ 'entity_id': 'test_domain.no_name',
+ })
+ msg = await client.receive_json()
+
+ assert not msg['success']
async def test_update_nonexisting_entity(client):
"""Test get entry."""
- resp = await client.post(
- '/api/config/entity_registry/test_domain.non_existing', json={
- 'name': 'some name'
- })
- assert resp.status == 404
+ await client.send_json({
+ 'id': 6,
+ 'type': 'config/entity_registry/update',
+ 'entity_id': 'test_domain.no_name',
+ 'name': 'new-name'
+ })
+ msg = await client.receive_json()
+
+ assert not msg['success']
diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py
index df3310f3d6f233..111cfbe969763e 100644
--- a/tests/components/deconz/test_config_flow.py
+++ b/tests/components/deconz/test_config_flow.py
@@ -23,7 +23,7 @@ async def test_flow_works(hass, aioclient_mock):
await flow.async_step_init()
await flow.async_step_link(user_input={})
result = await flow.async_step_options(
- user_input={'allow_clip_sensor': True})
+ user_input={'allow_clip_sensor': True, 'allow_deconz_groups': True})
assert result['type'] == 'create_entry'
assert result['title'] == 'deCONZ-id'
@@ -32,7 +32,8 @@ async def test_flow_works(hass, aioclient_mock):
'host': '1.2.3.4',
'port': 80,
'api_key': '1234567890ABCDEF',
- 'allow_clip_sensor': True
+ 'allow_clip_sensor': True,
+ 'allow_deconz_groups': True
}
@@ -149,6 +150,7 @@ async def test_bridge_discovery_config_file(hass):
'port': 80,
'serial': 'id'
})
+
assert result['type'] == 'create_entry'
assert result['title'] == 'deCONZ-id'
assert result['data'] == {
@@ -156,7 +158,8 @@ async def test_bridge_discovery_config_file(hass):
'host': '1.2.3.4',
'port': 80,
'api_key': '1234567890ABCDEF',
- 'allow_clip_sensor': True
+ 'allow_clip_sensor': True,
+ 'allow_deconz_groups': True
}
@@ -217,6 +220,7 @@ async def test_import_with_api_key(hass):
'port': 80,
'api_key': '1234567890ABCDEF'
})
+
assert result['type'] == 'create_entry'
assert result['title'] == 'deCONZ-id'
assert result['data'] == {
@@ -224,7 +228,8 @@ async def test_import_with_api_key(hass):
'host': '1.2.3.4',
'port': 80,
'api_key': '1234567890ABCDEF',
- 'allow_clip_sensor': True
+ 'allow_clip_sensor': True,
+ 'allow_deconz_groups': True
}
@@ -238,7 +243,7 @@ async def test_options(hass, aioclient_mock):
'port': 80,
'api_key': '1234567890ABCDEF'}
result = await flow.async_step_options(
- user_input={'allow_clip_sensor': False})
+ user_input={'allow_clip_sensor': False, 'allow_deconz_groups': False})
assert result['type'] == 'create_entry'
assert result['title'] == 'deCONZ-id'
assert result['data'] == {
@@ -246,5 +251,6 @@ async def test_options(hass, aioclient_mock):
'host': '1.2.3.4',
'port': 80,
'api_key': '1234567890ABCDEF',
- 'allow_clip_sensor': False
+ 'allow_clip_sensor': False,
+ 'allow_deconz_groups': False
}
diff --git a/tests/components/frontend/__init__.py b/tests/components/frontend/__init__.py
new file mode 100644
index 00000000000000..991a74dee7a1c7
--- /dev/null
+++ b/tests/components/frontend/__init__.py
@@ -0,0 +1 @@
+"""Tests for the frontend component."""
diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py
new file mode 100644
index 00000000000000..2125668facb8a9
--- /dev/null
+++ b/tests/components/frontend/test_init.py
@@ -0,0 +1,338 @@
+"""The tests for Home Assistant frontend."""
+import asyncio
+import re
+from unittest.mock import patch
+
+import pytest
+
+from homeassistant.exceptions import HomeAssistantError
+from homeassistant.setup import async_setup_component
+from homeassistant.components.frontend import (
+ DOMAIN, CONF_JS_VERSION, CONF_THEMES, CONF_EXTRA_HTML_URL,
+ CONF_EXTRA_HTML_URL_ES5)
+from homeassistant.components import websocket_api as wapi
+
+from tests.common import mock_coro
+
+
+CONFIG_THEMES = {
+ DOMAIN: {
+ CONF_THEMES: {
+ 'happy': {
+ 'primary-color': 'red'
+ }
+ }
+ }
+}
+
+
+@pytest.fixture
+def mock_http_client(hass, aiohttp_client):
+ """Start the Hass HTTP component."""
+ hass.loop.run_until_complete(async_setup_component(hass, 'frontend', {}))
+ return hass.loop.run_until_complete(aiohttp_client(hass.http.app))
+
+
+@pytest.fixture
+def mock_http_client_with_themes(hass, aiohttp_client):
+ """Start the Hass HTTP component."""
+ hass.loop.run_until_complete(async_setup_component(hass, 'frontend', {
+ DOMAIN: {
+ CONF_THEMES: {
+ 'happy': {
+ 'primary-color': 'red'
+ }
+ }
+ }}))
+ return hass.loop.run_until_complete(aiohttp_client(hass.http.app))
+
+
+@pytest.fixture
+def mock_http_client_with_urls(hass, aiohttp_client):
+ """Start the Hass HTTP component."""
+ hass.loop.run_until_complete(async_setup_component(hass, 'frontend', {
+ DOMAIN: {
+ CONF_JS_VERSION: 'auto',
+ CONF_EXTRA_HTML_URL: ["https://domain.com/my_extra_url.html"],
+ CONF_EXTRA_HTML_URL_ES5:
+ ["https://domain.com/my_extra_url_es5.html"]
+ }}))
+ return hass.loop.run_until_complete(aiohttp_client(hass.http.app))
+
+
+@asyncio.coroutine
+def test_frontend_and_static(mock_http_client):
+ """Test if we can get the frontend."""
+ resp = yield from mock_http_client.get('')
+ assert resp.status == 200
+ assert 'cache-control' not in resp.headers
+
+ text = yield from resp.text()
+
+ # Test we can retrieve frontend.js
+ frontendjs = re.search(
+ r'(?P\/frontend_es5\/app-[A-Za-z0-9]{32}.js)', text)
+
+ assert frontendjs is not None
+ resp = yield from mock_http_client.get(frontendjs.groups(0)[0])
+ assert resp.status == 200
+ assert 'public' in resp.headers.get('cache-control')
+
+
+@asyncio.coroutine
+def test_dont_cache_service_worker(mock_http_client):
+ """Test that we don't cache the service worker."""
+ resp = yield from mock_http_client.get('/service_worker_es5.js')
+ assert resp.status == 200
+ assert 'cache-control' not in resp.headers
+
+ resp = yield from mock_http_client.get('/service_worker.js')
+ assert resp.status == 200
+ assert 'cache-control' not in resp.headers
+
+
+@asyncio.coroutine
+def test_404(mock_http_client):
+ """Test for HTTP 404 error."""
+ resp = yield from mock_http_client.get('/not-existing')
+ assert resp.status == 404
+
+
+@asyncio.coroutine
+def test_we_cannot_POST_to_root(mock_http_client):
+ """Test that POST is not allow to root."""
+ resp = yield from mock_http_client.post('/')
+ assert resp.status == 405
+
+
+@asyncio.coroutine
+def test_states_routes(mock_http_client):
+ """All served by index."""
+ resp = yield from mock_http_client.get('/states')
+ assert resp.status == 200
+
+ resp = yield from mock_http_client.get('/states/group.existing')
+ assert resp.status == 200
+
+
+async def test_themes_api(hass, hass_ws_client):
+ """Test that /api/themes returns correct data."""
+ assert await async_setup_component(hass, 'frontend', CONFIG_THEMES)
+ client = await hass_ws_client(hass)
+
+ await client.send_json({
+ 'id': 5,
+ 'type': 'frontend/get_themes',
+ })
+ msg = await client.receive_json()
+
+ assert msg['result']['default_theme'] == 'default'
+ assert msg['result']['themes'] == {'happy': {'primary-color': 'red'}}
+
+
+async def test_themes_set_theme(hass, hass_ws_client):
+ """Test frontend.set_theme service."""
+ assert await async_setup_component(hass, 'frontend', CONFIG_THEMES)
+ client = await hass_ws_client(hass)
+
+ await hass.services.async_call(
+ DOMAIN, 'set_theme', {'name': 'happy'}, blocking=True)
+
+ await client.send_json({
+ 'id': 5,
+ 'type': 'frontend/get_themes',
+ })
+ msg = await client.receive_json()
+
+ assert msg['result']['default_theme'] == 'happy'
+
+ await hass.services.async_call(
+ DOMAIN, 'set_theme', {'name': 'default'}, blocking=True)
+
+ await client.send_json({
+ 'id': 6,
+ 'type': 'frontend/get_themes',
+ })
+ msg = await client.receive_json()
+
+ assert msg['result']['default_theme'] == 'default'
+
+
+async def test_themes_set_theme_wrong_name(hass, hass_ws_client):
+ """Test frontend.set_theme service called with wrong name."""
+ assert await async_setup_component(hass, 'frontend', CONFIG_THEMES)
+ client = await hass_ws_client(hass)
+
+ await hass.services.async_call(
+ DOMAIN, 'set_theme', {'name': 'wrong'}, blocking=True)
+
+ await client.send_json({
+ 'id': 5,
+ 'type': 'frontend/get_themes',
+ })
+
+ msg = await client.receive_json()
+
+ assert msg['result']['default_theme'] == 'default'
+
+
+async def test_themes_reload_themes(hass, hass_ws_client):
+ """Test frontend.reload_themes service."""
+ assert await async_setup_component(hass, 'frontend', CONFIG_THEMES)
+ client = await hass_ws_client(hass)
+
+ with patch('homeassistant.components.frontend.load_yaml_config_file',
+ return_value={DOMAIN: {
+ CONF_THEMES: {
+ 'sad': {'primary-color': 'blue'}
+ }}}):
+ await hass.services.async_call(
+ DOMAIN, 'set_theme', {'name': 'happy'}, blocking=True)
+ await hass.services.async_call(DOMAIN, 'reload_themes', blocking=True)
+
+ await client.send_json({
+ 'id': 5,
+ 'type': 'frontend/get_themes',
+ })
+
+ msg = await client.receive_json()
+
+ assert msg['result']['themes'] == {'sad': {'primary-color': 'blue'}}
+ assert msg['result']['default_theme'] == 'default'
+
+
+async def test_missing_themes(hass, hass_ws_client):
+ """Test that themes API works when themes are not defined."""
+ await async_setup_component(hass, 'frontend')
+
+ client = await hass_ws_client(hass)
+ await client.send_json({
+ 'id': 5,
+ 'type': 'frontend/get_themes',
+ })
+
+ msg = await client.receive_json()
+
+ assert msg['id'] == 5
+ assert msg['type'] == wapi.TYPE_RESULT
+ assert msg['success']
+ assert msg['result']['default_theme'] == 'default'
+ assert msg['result']['themes'] == {}
+
+
+@asyncio.coroutine
+def test_extra_urls(mock_http_client_with_urls):
+ """Test that extra urls are loaded."""
+ resp = yield from mock_http_client_with_urls.get('/states?latest')
+ assert resp.status == 200
+ text = yield from resp.text()
+ assert text.find('href="https://domain.com/my_extra_url.html"') >= 0
+
+
+@asyncio.coroutine
+def test_extra_urls_es5(mock_http_client_with_urls):
+ """Test that es5 extra urls are loaded."""
+ resp = yield from mock_http_client_with_urls.get('/states?es5')
+ assert resp.status == 200
+ text = yield from resp.text()
+ assert text.find('href="https://domain.com/my_extra_url_es5.html"') >= 0
+
+
+async def test_get_panels(hass, hass_ws_client):
+ """Test get_panels command."""
+ await async_setup_component(hass, 'frontend')
+ await hass.components.frontend.async_register_built_in_panel(
+ 'map', 'Map', 'mdi:account-location')
+
+ client = await hass_ws_client(hass)
+ await client.send_json({
+ 'id': 5,
+ 'type': 'get_panels',
+ })
+
+ msg = await client.receive_json()
+
+ assert msg['id'] == 5
+ assert msg['type'] == wapi.TYPE_RESULT
+ assert msg['success']
+ assert msg['result']['map']['component_name'] == 'map'
+ assert msg['result']['map']['url_path'] == 'map'
+ assert msg['result']['map']['icon'] == 'mdi:account-location'
+ assert msg['result']['map']['title'] == 'Map'
+
+
+async def test_get_translations(hass, hass_ws_client):
+ """Test get_translations command."""
+ await async_setup_component(hass, 'frontend')
+ client = await hass_ws_client(hass)
+
+ with patch('homeassistant.components.frontend.async_get_translations',
+ side_effect=lambda hass, lang: mock_coro({'lang': lang})):
+ await client.send_json({
+ 'id': 5,
+ 'type': 'frontend/get_translations',
+ 'language': 'nl',
+ })
+ msg = await client.receive_json()
+
+ assert msg['id'] == 5
+ assert msg['type'] == wapi.TYPE_RESULT
+ assert msg['success']
+ assert msg['result'] == {'resources': {'lang': 'nl'}}
+
+
+async def test_lovelace_ui(hass, hass_ws_client):
+ """Test lovelace_ui command."""
+ await async_setup_component(hass, 'frontend')
+ client = await hass_ws_client(hass)
+
+ with patch('homeassistant.components.frontend.load_yaml',
+ return_value={'hello': 'world'}):
+ await client.send_json({
+ 'id': 5,
+ 'type': 'frontend/lovelace_config',
+ })
+ msg = await client.receive_json()
+
+ assert msg['id'] == 5
+ assert msg['type'] == wapi.TYPE_RESULT
+ assert msg['success']
+ assert msg['result'] == {'hello': 'world'}
+
+
+async def test_lovelace_ui_not_found(hass, hass_ws_client):
+ """Test lovelace_ui command cannot find file."""
+ await async_setup_component(hass, 'frontend')
+ client = await hass_ws_client(hass)
+
+ with patch('homeassistant.components.frontend.load_yaml',
+ side_effect=FileNotFoundError):
+ await client.send_json({
+ 'id': 5,
+ 'type': 'frontend/lovelace_config',
+ })
+ msg = await client.receive_json()
+
+ assert msg['id'] == 5
+ assert msg['type'] == wapi.TYPE_RESULT
+ assert msg['success'] is False
+ assert msg['error']['code'] == 'file_not_found'
+
+
+async def test_lovelace_ui_load_err(hass, hass_ws_client):
+ """Test lovelace_ui command cannot find file."""
+ await async_setup_component(hass, 'frontend')
+ client = await hass_ws_client(hass)
+
+ with patch('homeassistant.components.frontend.load_yaml',
+ side_effect=HomeAssistantError):
+ await client.send_json({
+ 'id': 5,
+ 'type': 'frontend/lovelace_config',
+ })
+ msg = await client.receive_json()
+
+ assert msg['id'] == 5
+ assert msg['type'] == wapi.TYPE_RESULT
+ assert msg['success'] is False
+ assert msg['error']['code'] == 'load_error'
diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py
index ac90deb9f737d1..ce260225097a12 100644
--- a/tests/components/hassio/test_http.py
+++ b/tests/components/hassio/test_http.py
@@ -47,8 +47,8 @@ def test_auth_required_forward_request(hassio_client):
@asyncio.coroutine
@pytest.mark.parametrize(
'build_type', [
- 'es5/index.html', 'es5/hassio-app.html', 'latest/index.html',
- 'latest/hassio-app.html', 'es5/some-chunk.js', 'es5/app.js',
+ 'app/index.html', 'app/hassio-app.html', 'app/index.html',
+ 'app/hassio-app.html', 'app/some-chunk.js', 'app/app.js',
])
def test_forward_request_no_auth_for_panel(hassio_client, build_type):
"""Test no auth needed for ."""
@@ -61,7 +61,7 @@ def test_forward_request_no_auth_for_panel(hassio_client, build_type):
'_create_response') as mresp:
mresp.return_value = 'response'
resp = yield from hassio_client.get(
- '/api/hassio/app-{}'.format(build_type))
+ '/api/hassio/{}'.format(build_type))
# Check we got right response
assert resp.status == 200
diff --git a/tests/components/image_processing/test_facebox.py b/tests/components/image_processing/test_facebox.py
index cdc19a3d8d1bfa..9449ebf5f71de7 100644
--- a/tests/components/image_processing/test_facebox.py
+++ b/tests/components/image_processing/test_facebox.py
@@ -7,7 +7,7 @@
from homeassistant.core import callback
from homeassistant.const import (
- ATTR_ENTITY_ID, CONF_FRIENDLY_NAME,
+ ATTR_ENTITY_ID, ATTR_NAME, CONF_FRIENDLY_NAME,
CONF_IP_ADDRESS, CONF_PORT, STATE_UNKNOWN)
from homeassistant.setup import async_setup_component
import homeassistant.components.image_processing as ip
@@ -16,6 +16,7 @@
MOCK_IP = '192.168.0.1'
MOCK_PORT = '8080'
+# Mock data returned by the facebox API.
MOCK_FACE = {'confidence': 0.5812028911604818,
'id': 'john.jpg',
'matched': True,
@@ -28,6 +29,20 @@
"faces": [MOCK_FACE]
}
+# Faces data after parsing.
+PARSED_FACES = [{ATTR_NAME: 'John Lennon',
+ fb.ATTR_IMAGE_ID: 'john.jpg',
+ fb.ATTR_CONFIDENCE: 58.12,
+ fb.ATTR_MATCHED: True,
+ fb.ATTR_BOUNDING_BOX: {
+ 'height': 75,
+ 'left': 63,
+ 'top': 262,
+ 'width': 74},
+ }]
+
+MATCHED_FACES = {'John Lennon': 58.12}
+
VALID_ENTITY_ID = 'image_processing.facebox_demo_camera'
VALID_CONFIG = {
ip.DOMAIN: {
@@ -45,12 +60,14 @@
def test_encode_image():
"""Test that binary data is encoded correctly."""
- assert fb.encode_image(b'test')["base64"] == 'dGVzdA=='
+ assert fb.encode_image(b'test') == 'dGVzdA=='
-def test_get_matched_faces():
- """Test that matched faces are parsed correctly."""
- assert fb.get_matched_faces([MOCK_FACE]) == {MOCK_FACE['name']: 0.58}
+def test_parse_faces():
+ """Test parsing of raw face data, and generation of matched_faces."""
+ parsed_faces = fb.parse_faces(MOCK_JSON['faces'])
+ assert parsed_faces == PARSED_FACES
+ assert fb.get_matched_faces(parsed_faces) == MATCHED_FACES
@pytest.fixture
@@ -92,16 +109,21 @@ def mock_face_event(event):
state = hass.states.get(VALID_ENTITY_ID)
assert state.state == '1'
- assert state.attributes.get('matched_faces') == {MOCK_FACE['name']: 0.58}
+ assert state.attributes.get('matched_faces') == MATCHED_FACES
- MOCK_FACE[ATTR_ENTITY_ID] = VALID_ENTITY_ID # Update.
- assert state.attributes.get('faces') == [MOCK_FACE]
+ PARSED_FACES[0][ATTR_ENTITY_ID] = VALID_ENTITY_ID # Update.
+ assert state.attributes.get('faces') == PARSED_FACES
assert state.attributes.get(CONF_FRIENDLY_NAME) == 'facebox demo_camera'
assert len(face_events) == 1
- assert face_events[0].data['name'] == MOCK_FACE['name']
- assert face_events[0].data['confidence'] == MOCK_FACE['confidence']
- assert face_events[0].data['entity_id'] == VALID_ENTITY_ID
+ assert face_events[0].data[ATTR_NAME] == PARSED_FACES[0][ATTR_NAME]
+ assert (face_events[0].data[fb.ATTR_CONFIDENCE]
+ == PARSED_FACES[0][fb.ATTR_CONFIDENCE])
+ assert face_events[0].data[ATTR_ENTITY_ID] == VALID_ENTITY_ID
+ assert (face_events[0].data[fb.ATTR_IMAGE_ID] ==
+ PARSED_FACES[0][fb.ATTR_IMAGE_ID])
+ assert (face_events[0].data[fb.ATTR_BOUNDING_BOX] ==
+ PARSED_FACES[0][fb.ATTR_BOUNDING_BOX])
async def test_connection_error(hass, mock_image):
diff --git a/tests/components/light/test_deconz.py b/tests/components/light/test_deconz.py
index 2608d77ce2ae68..d7d609f820eb5f 100644
--- a/tests/components/light/test_deconz.py
+++ b/tests/components/light/test_deconz.py
@@ -38,7 +38,7 @@
}
-async def setup_bridge(hass, data):
+async def setup_bridge(hass, data, allow_deconz_groups=True):
"""Load the deCONZ light platform."""
from pydeconz import DeconzSession
loop = Mock()
@@ -53,7 +53,9 @@ async def setup_bridge(hass, data):
hass.data[deconz.DATA_DECONZ_UNSUB] = []
hass.data[deconz.DATA_DECONZ_ID] = {}
config_entry = config_entries.ConfigEntry(
- 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test')
+ 1, deconz.DOMAIN, 'Mock Title',
+ {'host': 'mock-host', 'allow_deconz_groups': allow_deconz_groups},
+ 'test')
await hass.config_entries.async_forward_entry_setup(config_entry, 'light')
# To flush out the service call to update the group
await hass.async_block_till_done()
@@ -98,3 +100,15 @@ async def test_add_new_group(hass):
async_dispatcher_send(hass, 'deconz_new_group', [group])
await hass.async_block_till_done()
assert "light.name" in hass.data[deconz.DATA_DECONZ_ID]
+
+
+async def test_do_not_add_deconz_groups(hass):
+ """Test that clip sensors can be ignored."""
+ data = {}
+ await setup_bridge(hass, data, allow_deconz_groups=False)
+ group = Mock()
+ group.name = 'name'
+ group.register_async_callback = Mock()
+ async_dispatcher_send(hass, 'deconz_new_group', [group])
+ await hass.async_block_till_done()
+ assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0
diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py
index 8b51adb2187399..49bcd8a73ecc0c 100644
--- a/tests/components/light/test_mqtt.py
+++ b/tests/components/light/test_mqtt.py
@@ -523,24 +523,24 @@ def test_sending_mqtt_commands_and_optimistic(self): \
self.mock_publish.reset_mock()
light.turn_on(self.hass, 'light.test',
brightness=50, xy_color=[0.123, 0.123])
- light.turn_on(self.hass, 'light.test', rgb_color=[75, 75, 75],
+ light.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 0],
white_value=80)
self.hass.block_till_done()
self.mock_publish.async_publish.assert_has_calls([
mock.call('test_light_rgb/set', 'on', 2, False),
- mock.call('test_light_rgb/rgb/set', '50,50,50', 2, False),
+ mock.call('test_light_rgb/rgb/set', '255,128,0', 2, False),
mock.call('test_light_rgb/brightness/set', 50, 2, False),
mock.call('test_light_rgb/white_value/set', 80, 2, False),
- mock.call('test_light_rgb/xy/set', '0.323,0.329', 2, False),
+ mock.call('test_light_rgb/xy/set', '0.14,0.131', 2, False),
], any_order=True)
state = self.hass.states.get('light.test')
self.assertEqual(STATE_ON, state.state)
- self.assertEqual((255, 255, 255), state.attributes['rgb_color'])
+ self.assertEqual((255, 128, 0), state.attributes['rgb_color'])
self.assertEqual(50, state.attributes['brightness'])
self.assertEqual(80, state.attributes['white_value'])
- self.assertEqual((0.323, 0.329), state.attributes['xy_color'])
+ self.assertEqual((0.611, 0.375), state.attributes['xy_color'])
def test_sending_mqtt_rgb_command_with_template(self):
"""Test the sending of RGB command with template."""
@@ -808,11 +808,11 @@ def test_on_command_brightness(self):
# Turn on w/ just a color to insure brightness gets
# added and sent.
- light.turn_on(self.hass, 'light.test', rgb_color=[75, 75, 75])
+ light.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 0])
self.hass.block_till_done()
self.mock_publish.async_publish.assert_has_calls([
- mock.call('test_light/rgb', '50,50,50', 0, False),
+ mock.call('test_light/rgb', '255,128,0', 0, False),
mock.call('test_light/bright', 50, 0, False)
], any_order=True)
diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py
index 275fb42ede917c..af560bff9c3224 100644
--- a/tests/components/light/test_mqtt_json.py
+++ b/tests/components/light/test_mqtt_json.py
@@ -381,8 +381,8 @@ def test_sending_mqtt_commands_and_optimistic(self): \
self.assertEqual(50, message_json["brightness"])
self.assertEqual({
'r': 0,
- 'g': 50,
- 'b': 4,
+ 'g': 255,
+ 'b': 21,
}, message_json["color"])
self.assertEqual("ON", message_json["state"])
diff --git a/tests/components/light/test_tradfri.py b/tests/components/light/test_tradfri.py
new file mode 100644
index 00000000000000..8ef5d17452a639
--- /dev/null
+++ b/tests/components/light/test_tradfri.py
@@ -0,0 +1,548 @@
+"""Tradfri lights platform tests."""
+
+from copy import deepcopy
+from unittest.mock import Mock, MagicMock, patch, PropertyMock
+
+import pytest
+from pytradfri.device import Device, LightControl, Light
+from pytradfri import RequestError
+
+from homeassistant.components import tradfri
+from homeassistant.setup import async_setup_component
+
+
+DEFAULT_TEST_FEATURES = {'can_set_dimmer': False,
+ 'can_set_color': False,
+ 'can_set_temp': False}
+# [
+# {bulb features},
+# {turn_on arguments},
+# {expected result}
+# ]
+TURN_ON_TEST_CASES = [
+ # Turn On
+ [
+ {},
+ {},
+ {'state': 'on'},
+ ],
+ # Brightness > 0
+ [
+ {'can_set_dimmer': True},
+ {'brightness': 100},
+ {
+ 'state': 'on',
+ 'brightness': 100
+ }
+ ],
+ # Brightness == 0
+ [
+ {'can_set_dimmer': True},
+ {'brightness': 0},
+ {
+ 'brightness': 0
+ }
+ ],
+ # Brightness < 0
+ [
+ {'can_set_dimmer': True},
+ {'brightness': -1},
+ {
+ 'brightness': 0
+ }
+ ],
+ # Brightness > 254
+ [
+ {'can_set_dimmer': True},
+ {'brightness': 1000},
+ {
+ 'brightness': 254
+ }
+ ],
+ # color_temp
+ [
+ {'can_set_temp': True},
+ {'color_temp': 250},
+ {'color_temp': 250},
+ ],
+ # color_temp < 250
+ [
+ {'can_set_temp': True},
+ {'color_temp': 1},
+ {'color_temp': 250},
+ ],
+ # color_temp > 454
+ [
+ {'can_set_temp': True},
+ {'color_temp': 1000},
+ {'color_temp': 454},
+ ],
+ # hs color
+ [
+ {'can_set_color': True},
+ {'hs_color': [300, 100]},
+ {
+ 'state': 'on',
+ 'hs_color': [300, 100]
+ }
+ ],
+ # ct + brightness
+ [
+ {
+ 'can_set_dimmer': True,
+ 'can_set_temp': True
+ },
+ {
+ 'color_temp': 250,
+ 'brightness': 200
+ },
+ {
+ 'state': 'on',
+ 'color_temp': 250,
+ 'brightness': 200
+ }
+ ],
+ # ct + brightness (no temp support)
+ [
+ {
+ 'can_set_dimmer': True,
+ 'can_set_temp': False,
+ 'can_set_color': True
+ },
+ {
+ 'color_temp': 250,
+ 'brightness': 200
+ },
+ {
+ 'state': 'on',
+ 'hs_color': [26.807, 34.869],
+ 'brightness': 200
+ }
+ ],
+ # ct + brightness (no temp or color support)
+ [
+ {
+ 'can_set_dimmer': True,
+ 'can_set_temp': False,
+ 'can_set_color': False
+ },
+ {
+ 'color_temp': 250,
+ 'brightness': 200
+ },
+ {
+ 'state': 'on',
+ 'brightness': 200
+ }
+ ],
+ # hs + brightness
+ [
+ {
+ 'can_set_dimmer': True,
+ 'can_set_color': True
+ },
+ {
+ 'hs_color': [300, 100],
+ 'brightness': 200
+ },
+ {
+ 'state': 'on',
+ 'hs_color': [300, 100],
+ 'brightness': 200
+ }
+ ]
+]
+
+# Result of transition is not tested, but data is passed to turn on service.
+TRANSITION_CASES_FOR_TESTS = [None, 0, 1]
+
+
+@pytest.fixture(autouse=True, scope='module')
+def setup(request):
+ """Set up patches for pytradfri methods."""
+ p_1 = patch('pytradfri.device.LightControl.raw',
+ new_callable=PropertyMock,
+ return_value=[{'mock': 'mock'}])
+ p_2 = patch('pytradfri.device.LightControl.lights')
+ p_1.start()
+ p_2.start()
+
+ def teardown():
+ """Remove patches for pytradfri methods."""
+ p_1.stop()
+ p_2.stop()
+
+ request.addfinalizer(teardown)
+
+
+@pytest.fixture
+def mock_gateway():
+ """Mock a Tradfri gateway."""
+ def get_devices():
+ """Return mock devices."""
+ return gateway.mock_devices
+
+ def get_groups():
+ """Return mock groups."""
+ return gateway.mock_groups
+
+ gateway = Mock(
+ get_devices=get_devices,
+ get_groups=get_groups,
+ mock_devices=[],
+ mock_groups=[],
+ mock_responses=[]
+ )
+ return gateway
+
+
+@pytest.fixture
+def mock_api(mock_gateway):
+ """Mock api."""
+ async def api(self, command):
+ """Mock api function."""
+ # Store the data for "real" command objects.
+ if(hasattr(command, '_data') and not isinstance(command, Mock)):
+ mock_gateway.mock_responses.append(command._data)
+ return command
+ return api
+
+
+async def generate_psk(self, code):
+ """Mock psk."""
+ return "mock"
+
+
+async def setup_gateway(hass, mock_gateway, mock_api,
+ generate_psk=generate_psk,
+ known_hosts=None):
+ """Load the Tradfri platform with a mock gateway."""
+ def request_config(_, callback, description, submit_caption, fields):
+ """Mock request_config."""
+ hass.async_add_job(callback, {'security_code': 'mock'})
+
+ if known_hosts is None:
+ known_hosts = {}
+
+ with patch('pytradfri.api.aiocoap_api.APIFactory.generate_psk',
+ generate_psk), \
+ patch('pytradfri.api.aiocoap_api.APIFactory.request', mock_api), \
+ patch('pytradfri.Gateway', return_value=mock_gateway), \
+ patch.object(tradfri, 'load_json', return_value=known_hosts), \
+ patch.object(hass.components.configurator, 'request_config',
+ request_config):
+
+ await async_setup_component(hass, tradfri.DOMAIN,
+ {
+ tradfri.DOMAIN: {
+ 'host': 'mock-host',
+ 'allow_tradfri_groups': True
+ }
+ })
+ await hass.async_block_till_done()
+
+
+async def test_setup_gateway(hass, mock_gateway, mock_api):
+ """Test that the gateway can be setup without errors."""
+ await setup_gateway(hass, mock_gateway, mock_api)
+
+
+async def test_setup_gateway_known_host(hass, mock_gateway, mock_api):
+ """Test gateway setup with a known host."""
+ await setup_gateway(hass, mock_gateway, mock_api,
+ known_hosts={
+ 'mock-host': {
+ 'identity': 'mock',
+ 'key': 'mock-key'
+ }
+ })
+
+
+async def test_incorrect_security_code(hass, mock_gateway, mock_api):
+ """Test that an error is shown if the security code is incorrect."""
+ async def psk_error(self, code):
+ """Raise RequestError when called."""
+ raise RequestError
+
+ with patch.object(hass.components.configurator, 'async_notify_errors') \
+ as notify_error:
+ await setup_gateway(hass, mock_gateway, mock_api,
+ generate_psk=psk_error)
+ assert len(notify_error.mock_calls) > 0
+
+
+def mock_light(test_features={}, test_state={}, n=0):
+ """Mock a tradfri light."""
+ mock_light_data = Mock(
+ **test_state
+ )
+
+ mock_light = Mock(
+ id='mock-light-id-{}'.format(n),
+ reachable=True,
+ observe=Mock(),
+ device_info=MagicMock()
+ )
+ mock_light.name = 'tradfri_light_{}'.format(n)
+
+ # Set supported features for the light.
+ features = {**DEFAULT_TEST_FEATURES, **test_features}
+ lc = LightControl(mock_light)
+ for k, v in features.items():
+ setattr(lc, k, v)
+ # Store the initial state.
+ setattr(lc, 'lights', [mock_light_data])
+ mock_light.light_control = lc
+ return mock_light
+
+
+async def test_light(hass, mock_gateway, mock_api):
+ """Test that lights are correctly added."""
+ features = {
+ 'can_set_dimmer': True,
+ 'can_set_color': True,
+ 'can_set_temp': True
+ }
+
+ state = {
+ 'state': True,
+ 'dimmer': 100,
+ 'color_temp': 250,
+ 'hsb_xy_color': (100, 100, 100, 100, 100)
+ }
+
+ mock_gateway.mock_devices.append(
+ mock_light(test_features=features, test_state=state)
+ )
+ await setup_gateway(hass, mock_gateway, mock_api)
+
+ lamp_1 = hass.states.get('light.tradfri_light_0')
+ assert lamp_1 is not None
+ assert lamp_1.state == 'on'
+ assert lamp_1.attributes['brightness'] == 100
+ assert lamp_1.attributes['hs_color'] == (0.549, 0.153)
+
+
+async def test_light_observed(hass, mock_gateway, mock_api):
+ """Test that lights are correctly observed."""
+ light = mock_light()
+ mock_gateway.mock_devices.append(light)
+ await setup_gateway(hass, mock_gateway, mock_api)
+ assert len(light.observe.mock_calls) > 0
+
+
+async def test_light_available(hass, mock_gateway, mock_api):
+ """Test light available property."""
+ light = mock_light({'state': True}, n=1)
+ light.reachable = True
+
+ light2 = mock_light({'state': True}, n=2)
+ light2.reachable = False
+
+ mock_gateway.mock_devices.append(light)
+ mock_gateway.mock_devices.append(light2)
+ await setup_gateway(hass, mock_gateway, mock_api)
+
+ assert (hass.states.get('light.tradfri_light_1')
+ .state == 'on')
+
+ assert (hass.states.get('light.tradfri_light_2')
+ .state == 'unavailable')
+
+
+# Combine TURN_ON_TEST_CASES and TRANSITION_CASES_FOR_TESTS
+ALL_TURN_ON_TEST_CASES = [
+ ["test_features", "test_data", "expected_result", "id"],
+ []
+]
+
+idx = 1
+for tc in TURN_ON_TEST_CASES:
+ for trans in TRANSITION_CASES_FOR_TESTS:
+ case = deepcopy(tc)
+ if trans is not None:
+ case[1]['transition'] = trans
+ case.append(idx)
+ idx = idx + 1
+ ALL_TURN_ON_TEST_CASES[1].append(case)
+
+
+@pytest.mark.parametrize(*ALL_TURN_ON_TEST_CASES)
+async def test_turn_on(hass,
+ mock_gateway,
+ mock_api,
+ test_features,
+ test_data,
+ expected_result,
+ id):
+ """Test turning on a light."""
+ # Note pytradfri style, not hass. Values not really important.
+ initial_state = {
+ 'state': False,
+ 'dimmer': 0,
+ 'color_temp': 250,
+ 'hsb_xy_color': (100, 100, 100, 100, 100)
+ }
+
+ # Setup the gateway with a mock light.
+ light = mock_light(test_features=test_features,
+ test_state=initial_state,
+ n=id)
+ mock_gateway.mock_devices.append(light)
+ await setup_gateway(hass, mock_gateway, mock_api)
+
+ # Use the turn_on service call to change the light state.
+ await hass.services.async_call('light', 'turn_on', {
+ 'entity_id': 'light.tradfri_light_{}'.format(id),
+ **test_data
+ }, blocking=True)
+ await hass.async_block_till_done()
+
+ # Check that the light is observed.
+ mock_func = light.observe
+ assert len(mock_func.mock_calls) > 0
+ _, callkwargs = mock_func.call_args
+ assert 'callback' in callkwargs
+ # Callback function to refresh light state.
+ cb = callkwargs['callback']
+
+ responses = mock_gateway.mock_responses
+ # State on command data.
+ data = {'3311': [{'5850': 1}]}
+ # Add data for all sent commands.
+ for r in responses:
+ data['3311'][0] = {**data['3311'][0], **r['3311'][0]}
+
+ # Use the callback function to update the light state.
+ dev = Device(data)
+ light_data = Light(dev, 0)
+ light.light_control.lights[0] = light_data
+ cb(light)
+ await hass.async_block_till_done()
+
+ # Check that the state is correct.
+ states = hass.states.get('light.tradfri_light_{}'.format(id))
+ for k, v in expected_result.items():
+ if k == 'state':
+ assert states.state == v
+ else:
+ # Allow some rounding error in color conversions.
+ assert states.attributes[k] == pytest.approx(v, abs=0.01)
+
+
+async def test_turn_off(hass, mock_gateway, mock_api):
+ """Test turning off a light."""
+ state = {
+ 'state': True,
+ 'dimmer': 100,
+ }
+
+ light = mock_light(test_state=state)
+ mock_gateway.mock_devices.append(light)
+ await setup_gateway(hass, mock_gateway, mock_api)
+
+ # Use the turn_off service call to change the light state.
+ await hass.services.async_call('light', 'turn_off', {
+ 'entity_id': 'light.tradfri_light_0'}, blocking=True)
+ await hass.async_block_till_done()
+
+ # Check that the light is observed.
+ mock_func = light.observe
+ assert len(mock_func.mock_calls) > 0
+ _, callkwargs = mock_func.call_args
+ assert 'callback' in callkwargs
+ # Callback function to refresh light state.
+ cb = callkwargs['callback']
+
+ responses = mock_gateway.mock_responses
+ data = {'3311': [{}]}
+ # Add data for all sent commands.
+ for r in responses:
+ data['3311'][0] = {**data['3311'][0], **r['3311'][0]}
+
+ # Use the callback function to update the light state.
+ dev = Device(data)
+ light_data = Light(dev, 0)
+ light.light_control.lights[0] = light_data
+ cb(light)
+ await hass.async_block_till_done()
+
+ # Check that the state is correct.
+ states = hass.states.get('light.tradfri_light_0')
+ assert states.state == 'off'
+
+
+def mock_group(test_state={}, n=0):
+ """Mock a Tradfri group."""
+ default_state = {
+ 'state': False,
+ 'dimmer': 0,
+ }
+
+ state = {**default_state, **test_state}
+
+ mock_group = Mock(
+ member_ids=[],
+ observe=Mock(),
+ **state
+ )
+ mock_group.name = 'tradfri_group_{}'.format(n)
+ return mock_group
+
+
+async def test_group(hass, mock_gateway, mock_api):
+ """Test that groups are correctly added."""
+ mock_gateway.mock_groups.append(mock_group())
+ state = {'state': True, 'dimmer': 100}
+ mock_gateway.mock_groups.append(mock_group(state, 1))
+ await setup_gateway(hass, mock_gateway, mock_api)
+
+ group = hass.states.get('light.tradfri_group_0')
+ assert group is not None
+ assert group.state == 'off'
+
+ group = hass.states.get('light.tradfri_group_1')
+ assert group is not None
+ assert group.state == 'on'
+ assert group.attributes['brightness'] == 100
+
+
+async def test_group_turn_on(hass, mock_gateway, mock_api):
+ """Test turning on a group."""
+ group = mock_group()
+ group2 = mock_group(n=1)
+ group3 = mock_group(n=2)
+ mock_gateway.mock_groups.append(group)
+ mock_gateway.mock_groups.append(group2)
+ mock_gateway.mock_groups.append(group3)
+ await setup_gateway(hass, mock_gateway, mock_api)
+
+ # Use the turn_off service call to change the light state.
+ await hass.services.async_call('light', 'turn_on', {
+ 'entity_id': 'light.tradfri_group_0'}, blocking=True)
+ await hass.services.async_call('light', 'turn_on', {
+ 'entity_id': 'light.tradfri_group_1',
+ 'brightness': 100}, blocking=True)
+ await hass.services.async_call('light', 'turn_on', {
+ 'entity_id': 'light.tradfri_group_2',
+ 'brightness': 100,
+ 'transition': 1}, blocking=True)
+ await hass.async_block_till_done()
+
+ group.set_state.assert_called_with(1)
+ group2.set_dimmer.assert_called_with(100)
+ group3.set_dimmer.assert_called_with(100, transition_time=10)
+
+
+async def test_group_turn_off(hass, mock_gateway, mock_api):
+ """Test turning off a group."""
+ group = mock_group({'state': True})
+ mock_gateway.mock_groups.append(group)
+ await setup_gateway(hass, mock_gateway, mock_api)
+
+ # Use the turn_off service call to change the light state.
+ await hass.services.async_call('light', 'turn_off', {
+ 'entity_id': 'light.tradfri_group_0'}, blocking=True)
+ await hass.async_block_till_done()
+
+ group.set_state.assert_called_with(0)
diff --git a/tests/components/media_player/test_samsungtv.py b/tests/components/media_player/test_samsungtv.py
index c3753eb53b52b7..b5baf8b078b6ff 100644
--- a/tests/components/media_player/test_samsungtv.py
+++ b/tests/components/media_player/test_samsungtv.py
@@ -29,7 +29,15 @@
}
-class PackageException(Exception):
+class AccessDenied(Exception):
+ """Dummy Exception."""
+
+
+class ConnectionClosed(Exception):
+ """Dummy Exception."""
+
+
+class UnhandledResponse(Exception):
"""Dummy Exception."""
@@ -45,9 +53,9 @@ def setUp(self, samsung_mock, wol_mock):
self.hass.block_till_done()
self.device = SamsungTVDevice(**WORKING_CONFIG)
self.device._exceptions_class = mock.Mock()
- self.device._exceptions_class.UnhandledResponse = PackageException
- self.device._exceptions_class.AccessDenied = PackageException
- self.device._exceptions_class.ConnectionClosed = PackageException
+ self.device._exceptions_class.UnhandledResponse = UnhandledResponse
+ self.device._exceptions_class.AccessDenied = AccessDenied
+ self.device._exceptions_class.ConnectionClosed = ConnectionClosed
def tearDown(self):
"""Tear down test data."""
@@ -123,22 +131,46 @@ def test_send_key(self):
def test_send_key_broken_pipe(self):
"""Testing broken pipe Exception."""
_remote = mock.Mock()
- self.device.get_remote = mock.Mock()
_remote.control = mock.Mock(
- side_effect=BrokenPipeError("Boom"))
- self.device.get_remote.return_value = _remote
- self.device.send_key("HELLO")
+ side_effect=BrokenPipeError('Boom'))
+ self.device.get_remote = mock.Mock(return_value=_remote)
+ self.device.send_key('HELLO')
+ self.assertIsNone(self.device._remote)
+ self.assertEqual(STATE_ON, self.device._state)
+
+ def test_send_key_connection_closed_retry_succeed(self):
+ """Test retry on connection closed."""
+ _remote = mock.Mock()
+ _remote.control = mock.Mock(side_effect=[
+ self.device._exceptions_class.ConnectionClosed('Boom'),
+ mock.DEFAULT])
+ self.device.get_remote = mock.Mock(return_value=_remote)
+ command = 'HELLO'
+ self.device.send_key(command)
+ self.assertEqual(STATE_ON, self.device._state)
+ # verify that _remote.control() get called twice because of retry logic
+ expected = [mock.call(command),
+ mock.call(command)]
+ self.assertEqual(expected, _remote.control.call_args_list)
+
+ def test_send_key_unhandled_response(self):
+ """Testing unhandled response exception."""
+ _remote = mock.Mock()
+ _remote.control = mock.Mock(
+ side_effect=self.device._exceptions_class.UnhandledResponse('Boom')
+ )
+ self.device.get_remote = mock.Mock(return_value=_remote)
+ self.device.send_key('HELLO')
self.assertIsNone(self.device._remote)
self.assertEqual(STATE_ON, self.device._state)
def test_send_key_os_error(self):
"""Testing broken pipe Exception."""
_remote = mock.Mock()
- self.device.get_remote = mock.Mock()
_remote.control = mock.Mock(
- side_effect=OSError("Boom"))
- self.device.get_remote.return_value = _remote
- self.device.send_key("HELLO")
+ side_effect=OSError('Boom'))
+ self.device.get_remote = mock.Mock(return_value=_remote)
+ self.device.send_key('HELLO')
self.assertIsNone(self.device._remote)
self.assertEqual(STATE_OFF, self.device._state)
diff --git a/tests/components/nest/__init__.py b/tests/components/nest/__init__.py
new file mode 100644
index 00000000000000..313cfccc76169d
--- /dev/null
+++ b/tests/components/nest/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Nest component."""
diff --git a/tests/components/nest/test_config_flow.py b/tests/components/nest/test_config_flow.py
new file mode 100644
index 00000000000000..e80d18a98626e7
--- /dev/null
+++ b/tests/components/nest/test_config_flow.py
@@ -0,0 +1,218 @@
+"""Tests for the Nest config flow."""
+import asyncio
+from unittest.mock import Mock, patch
+
+from homeassistant import data_entry_flow
+from homeassistant.setup import async_setup_component
+from homeassistant.components.nest import config_flow, DOMAIN
+
+from tests.common import mock_coro
+
+
+async def test_abort_if_no_implementation_registered(hass):
+ """Test we abort if no implementation is registered."""
+ flow = config_flow.NestFlowHandler()
+ flow.hass = hass
+ result = await flow.async_step_init()
+
+ assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result['reason'] == 'no_flows'
+
+
+async def test_abort_if_already_setup(hass):
+ """Test we abort if Nest is already setup."""
+ flow = config_flow.NestFlowHandler()
+ flow.hass = hass
+
+ with patch.object(hass.config_entries, 'async_entries', return_value=[{}]):
+ result = await flow.async_step_init()
+
+ assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result['reason'] == 'already_setup'
+
+
+async def test_full_flow_implementation(hass):
+ """Test registering an implementation and finishing flow works."""
+ gen_authorize_url = Mock(return_value=mock_coro('https://example.com'))
+ convert_code = Mock(return_value=mock_coro({'access_token': 'yoo'}))
+ config_flow.register_flow_implementation(
+ hass, 'test', 'Test', gen_authorize_url, convert_code)
+ config_flow.register_flow_implementation(
+ hass, 'test-other', 'Test Other', None, None)
+
+ flow = config_flow.NestFlowHandler()
+ flow.hass = hass
+ result = await flow.async_step_init()
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['step_id'] == 'init'
+
+ result = await flow.async_step_init({'flow_impl': 'test'})
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['step_id'] == 'link'
+ assert result['description_placeholders'] == {
+ 'url': 'https://example.com',
+ }
+
+ result = await flow.async_step_link({'code': '123ABC'})
+ assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result['data']['tokens'] == {'access_token': 'yoo'}
+ assert result['data']['impl_domain'] == 'test'
+ assert result['title'] == 'Nest (via Test)'
+
+
+async def test_not_pick_implementation_if_only_one(hass):
+ """Test we allow picking implementation if we have two."""
+ gen_authorize_url = Mock(return_value=mock_coro('https://example.com'))
+ config_flow.register_flow_implementation(
+ hass, 'test', 'Test', gen_authorize_url, None)
+
+ flow = config_flow.NestFlowHandler()
+ flow.hass = hass
+ result = await flow.async_step_init()
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['step_id'] == 'link'
+
+
+async def test_abort_if_timeout_generating_auth_url(hass):
+ """Test we abort if generating authorize url fails."""
+ gen_authorize_url = Mock(side_effect=asyncio.TimeoutError)
+ config_flow.register_flow_implementation(
+ hass, 'test', 'Test', gen_authorize_url, None)
+
+ flow = config_flow.NestFlowHandler()
+ flow.hass = hass
+ result = await flow.async_step_init()
+ assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result['reason'] == 'authorize_url_timeout'
+
+
+async def test_abort_if_exception_generating_auth_url(hass):
+ """Test we abort if generating authorize url blows up."""
+ gen_authorize_url = Mock(side_effect=ValueError)
+ config_flow.register_flow_implementation(
+ hass, 'test', 'Test', gen_authorize_url, None)
+
+ flow = config_flow.NestFlowHandler()
+ flow.hass = hass
+ result = await flow.async_step_init()
+ assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result['reason'] == 'authorize_url_fail'
+
+
+async def test_verify_code_timeout(hass):
+ """Test verify code timing out."""
+ gen_authorize_url = Mock(return_value=mock_coro('https://example.com'))
+ convert_code = Mock(side_effect=asyncio.TimeoutError)
+ config_flow.register_flow_implementation(
+ hass, 'test', 'Test', gen_authorize_url, convert_code)
+
+ flow = config_flow.NestFlowHandler()
+ flow.hass = hass
+ result = await flow.async_step_init()
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['step_id'] == 'link'
+
+ result = await flow.async_step_link({'code': '123ABC'})
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['step_id'] == 'link'
+ assert result['errors'] == {'code': 'timeout'}
+
+
+async def test_verify_code_invalid(hass):
+ """Test verify code invalid."""
+ gen_authorize_url = Mock(return_value=mock_coro('https://example.com'))
+ convert_code = Mock(side_effect=config_flow.CodeInvalid)
+ config_flow.register_flow_implementation(
+ hass, 'test', 'Test', gen_authorize_url, convert_code)
+
+ flow = config_flow.NestFlowHandler()
+ flow.hass = hass
+ result = await flow.async_step_init()
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['step_id'] == 'link'
+
+ result = await flow.async_step_link({'code': '123ABC'})
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['step_id'] == 'link'
+ assert result['errors'] == {'code': 'invalid_code'}
+
+
+async def test_verify_code_unknown_error(hass):
+ """Test verify code unknown error."""
+ gen_authorize_url = Mock(return_value=mock_coro('https://example.com'))
+ convert_code = Mock(side_effect=config_flow.NestAuthError)
+ config_flow.register_flow_implementation(
+ hass, 'test', 'Test', gen_authorize_url, convert_code)
+
+ flow = config_flow.NestFlowHandler()
+ flow.hass = hass
+ result = await flow.async_step_init()
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['step_id'] == 'link'
+
+ result = await flow.async_step_link({'code': '123ABC'})
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['step_id'] == 'link'
+ assert result['errors'] == {'code': 'unknown'}
+
+
+async def test_verify_code_exception(hass):
+ """Test verify code blows up."""
+ gen_authorize_url = Mock(return_value=mock_coro('https://example.com'))
+ convert_code = Mock(side_effect=ValueError)
+ config_flow.register_flow_implementation(
+ hass, 'test', 'Test', gen_authorize_url, convert_code)
+
+ flow = config_flow.NestFlowHandler()
+ flow.hass = hass
+ result = await flow.async_step_init()
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['step_id'] == 'link'
+
+ result = await flow.async_step_link({'code': '123ABC'})
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['step_id'] == 'link'
+ assert result['errors'] == {'code': 'internal_error'}
+
+
+async def test_step_import(hass):
+ """Test that we trigger import when configuring with client."""
+ with patch('os.path.isfile', return_value=False):
+ assert await async_setup_component(hass, DOMAIN, {
+ DOMAIN: {
+ 'client_id': 'bla',
+ 'client_secret': 'bla',
+ },
+ })
+ await hass.async_block_till_done()
+
+ flow = hass.config_entries.flow.async_progress()[0]
+ result = await hass.config_entries.flow.async_configure(flow['flow_id'])
+
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['step_id'] == 'link'
+
+
+async def test_step_import_with_token_cache(hass):
+ """Test that we import existing token cache."""
+ with patch('os.path.isfile', return_value=True), \
+ patch('homeassistant.components.nest.config_flow.load_json',
+ return_value={'access_token': 'yo'}), \
+ patch('homeassistant.components.nest.async_setup_entry',
+ return_value=mock_coro(True)):
+ assert await async_setup_component(hass, DOMAIN, {
+ DOMAIN: {
+ 'client_id': 'bla',
+ 'client_secret': 'bla',
+ },
+ })
+ await hass.async_block_till_done()
+
+ entry = hass.config_entries.async_entries(DOMAIN)[0]
+
+ assert entry.data == {
+ 'impl_domain': 'nest',
+ 'tokens': {
+ 'access_token': 'yo'
+ }
+ }
diff --git a/tests/components/nest/test_local_auth.py b/tests/components/nest/test_local_auth.py
new file mode 100644
index 00000000000000..44a5299b33dbd3
--- /dev/null
+++ b/tests/components/nest/test_local_auth.py
@@ -0,0 +1,51 @@
+"""Test Nest local auth."""
+from homeassistant.components.nest import const, config_flow, local_auth
+from urllib.parse import parse_qsl
+
+import pytest
+
+import requests_mock as rmock
+
+
+@pytest.fixture
+def registered_flow(hass):
+ """Mock a registered flow."""
+ local_auth.initialize(hass, 'TEST-CLIENT-ID', 'TEST-CLIENT-SECRET')
+ return hass.data[config_flow.DATA_FLOW_IMPL][const.DOMAIN]
+
+
+async def test_generate_auth_url(registered_flow):
+ """Test generating an auth url.
+
+ Mainly testing that it doesn't blow up.
+ """
+ url = await registered_flow['gen_authorize_url']('TEST-FLOW-ID')
+ assert url is not None
+
+
+async def test_convert_code(requests_mock, registered_flow):
+ """Test converting a code."""
+ from nest.nest import ACCESS_TOKEN_URL
+
+ def token_matcher(request):
+ """Match a fetch token request."""
+ if request.url != ACCESS_TOKEN_URL:
+ return None
+
+ assert dict(parse_qsl(request.text)) == {
+ 'client_id': 'TEST-CLIENT-ID',
+ 'client_secret': 'TEST-CLIENT-SECRET',
+ 'code': 'TEST-CODE',
+ 'grant_type': 'authorization_code'
+ }
+
+ return rmock.create_response(request, json={
+ 'access_token': 'TEST-ACCESS-TOKEN'
+ })
+
+ requests_mock.add_matcher(token_matcher)
+
+ tokens = await registered_flow['convert_code']('TEST-CODE')
+ assert tokens == {
+ 'access_token': 'TEST-ACCESS-TOKEN'
+ }
diff --git a/tests/components/sensor/test_nsw_fuel_station.py b/tests/components/sensor/test_nsw_fuel_station.py
new file mode 100644
index 00000000000000..1ee314d9eee094
--- /dev/null
+++ b/tests/components/sensor/test_nsw_fuel_station.py
@@ -0,0 +1,117 @@
+"""The tests for the NSW Fuel Station sensor platform."""
+import unittest
+from unittest.mock import patch
+
+from homeassistant.components import sensor
+from homeassistant.setup import setup_component
+from tests.common import (
+ get_test_home_assistant, assert_setup_component, MockDependency)
+
+VALID_CONFIG = {
+ 'platform': 'nsw_fuel_station',
+ 'station_id': 350,
+ 'fuel_types': ['E10', 'P95'],
+}
+
+
+class MockPrice():
+ """Mock Price implementation."""
+
+ def __init__(self, price, fuel_type, last_updated,
+ price_unit, station_code):
+ """Initialize a mock price instance."""
+ self.price = price
+ self.fuel_type = fuel_type
+ self.last_updated = last_updated
+ self.price_unit = price_unit
+ self.station_code = station_code
+
+
+class MockStation():
+ """Mock Station implementation."""
+
+ def __init__(self, name, code):
+ """Initialize a mock Station instance."""
+ self.name = name
+ self.code = code
+
+
+class MockGetReferenceDataResponse():
+ """Mock GetReferenceDataResponse implementation."""
+
+ def __init__(self, stations):
+ """Initialize a mock GetReferenceDataResponse instance."""
+ self.stations = stations
+
+
+class FuelCheckClientMock():
+ """Mock FuelCheckClient implementation."""
+
+ def get_fuel_prices_for_station(self, station):
+ """Return a fake fuel prices response."""
+ return [
+ MockPrice(
+ price=150.0,
+ fuel_type='P95',
+ last_updated=None,
+ price_unit=None,
+ station_code=350
+ ),
+ MockPrice(
+ price=140.0,
+ fuel_type='E10',
+ last_updated=None,
+ price_unit=None,
+ station_code=350
+ )
+ ]
+
+ def get_reference_data(self):
+ """Return a fake reference data response."""
+ return MockGetReferenceDataResponse(
+ stations=[
+ MockStation(code=350, name="My Fake Station")
+ ]
+ )
+
+
+class TestNSWFuelStation(unittest.TestCase):
+ """Test the NSW Fuel Station sensor platform."""
+
+ def setUp(self):
+ """Set up things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+ self.config = VALID_CONFIG
+
+ def tearDown(self):
+ """Stop everything that was started."""
+ self.hass.stop()
+
+ @MockDependency('nsw_fuel')
+ @patch('nsw_fuel.FuelCheckClient', new=FuelCheckClientMock)
+ def test_setup(self, mock_nsw_fuel):
+ """Test the setup with custom settings."""
+ with assert_setup_component(1, sensor.DOMAIN):
+ self.assertTrue(setup_component(self.hass, sensor.DOMAIN, {
+ 'sensor': VALID_CONFIG}))
+
+ fake_entities = [
+ 'my_fake_station_p95',
+ 'my_fake_station_e10'
+ ]
+
+ for entity_id in fake_entities:
+ state = self.hass.states.get('sensor.{}'.format(entity_id))
+ self.assertIsNotNone(state)
+
+ @MockDependency('nsw_fuel')
+ @patch('nsw_fuel.FuelCheckClient', new=FuelCheckClientMock)
+ def test_sensor_values(self, mock_nsw_fuel):
+ """Test retrieval of sensor values."""
+ self.assertTrue(setup_component(
+ self.hass, sensor.DOMAIN, {'sensor': VALID_CONFIG}))
+
+ self.assertEqual('140.0', self.hass.states.get(
+ 'sensor.my_fake_station_e10').state)
+ self.assertEqual('150.0', self.hass.states.get(
+ 'sensor.my_fake_station_p95').state)
diff --git a/tests/components/sensor/test_rflink.py b/tests/components/sensor/test_rflink.py
index a99d14cc735007..a250a75ab99912 100644
--- a/tests/components/sensor/test_rflink.py
+++ b/tests/components/sensor/test_rflink.py
@@ -8,6 +8,9 @@
import asyncio
from ..test_rflink import mock_rflink
+from homeassistant.components.rflink import (
+ CONF_RECONNECT_INTERVAL)
+from homeassistant.const import STATE_UNKNOWN
DOMAIN = 'sensor'
@@ -32,7 +35,7 @@
def test_default_setup(hass, monkeypatch):
"""Test all basic functionality of the rflink sensor component."""
# setup mocking rflink module
- event_callback, create, _, _ = yield from mock_rflink(
+ event_callback, create, _, disconnect_callback = yield from mock_rflink(
hass, CONFIG, DOMAIN, monkeypatch)
# make sure arguments are passed
@@ -100,3 +103,38 @@ def test_disable_automatic_add(hass, monkeypatch):
# make sure new device is not added
assert not hass.states.get('sensor.test2')
+
+
+@asyncio.coroutine
+def test_entity_availability(hass, monkeypatch):
+ """If Rflink device is disconnected, entities should become unavailable."""
+ # Make sure Rflink mock does not 'recover' to quickly from the
+ # disconnect or else the unavailability cannot be measured
+ config = CONFIG
+ failures = [True, True]
+ config[CONF_RECONNECT_INTERVAL] = 60
+
+ # Create platform and entities
+ event_callback, create, _, disconnect_callback = yield from mock_rflink(
+ hass, config, DOMAIN, monkeypatch, failures=failures)
+
+ # Entities are available by default
+ assert hass.states.get('sensor.test').state == STATE_UNKNOWN
+
+ # Mock a disconnect of the Rflink device
+ disconnect_callback()
+
+ # Wait for dispatch events to propagate
+ yield from hass.async_block_till_done()
+
+ # Entity should be unavailable
+ assert hass.states.get('sensor.test').state == 'unavailable'
+
+ # Reconnect the Rflink device
+ disconnect_callback()
+
+ # Wait for dispatch events to propagate
+ yield from hass.async_block_till_done()
+
+ # Entities should be available again
+ assert hass.states.get('sensor.test').state == STATE_UNKNOWN
diff --git a/tests/components/sensor/test_ring.py b/tests/components/sensor/test_ring.py
index 0cce0ea681d318..4d34018ce52c85 100644
--- a/tests/components/sensor/test_ring.py
+++ b/tests/components/sensor/test_ring.py
@@ -51,6 +51,8 @@ def tearDown(self):
@requests_mock.Mocker()
def test_sensor(self, mock):
"""Test the Ring sensor class and methods."""
+ mock.post('https://oauth.ring.com/oauth/token',
+ text=load_fixture('ring_oauth.json'))
mock.post('https://api.ring.com/clients_api/session',
text=load_fixture('ring_session.json'))
mock.get('https://api.ring.com/clients_api/ring_devices',
diff --git a/tests/components/sensor/test_simulated.py b/tests/components/sensor/test_simulated.py
index 3bfccc629fdd52..50552baa33e24c 100644
--- a/tests/components/sensor/test_simulated.py
+++ b/tests/components/sensor/test_simulated.py
@@ -1,13 +1,14 @@
"""The tests for the simulated sensor."""
import unittest
+from tests.common import get_test_home_assistant
+
from homeassistant.components.sensor.simulated import (
- CONF_UNIT, CONF_AMP, CONF_MEAN, CONF_PERIOD, CONF_PHASE, CONF_FWHM,
- CONF_SEED, DEFAULT_NAME, DEFAULT_AMP, DEFAULT_MEAN,
- DEFAULT_PHASE, DEFAULT_FWHM, DEFAULT_SEED)
+ CONF_AMP, CONF_FWHM, CONF_MEAN, CONF_PERIOD, CONF_PHASE, CONF_SEED,
+ CONF_UNIT, CONF_RELATIVE_TO_EPOCH, DEFAULT_AMP, DEFAULT_FWHM, DEFAULT_MEAN,
+ DEFAULT_NAME, DEFAULT_PHASE, DEFAULT_SEED, DEFAULT_RELATIVE_TO_EPOCH)
from homeassistant.const import CONF_FRIENDLY_NAME
from homeassistant.setup import setup_component
-from tests.common import get_test_home_assistant
class TestSimulatedSensor(unittest.TestCase):
@@ -27,24 +28,19 @@ def test_default_config(self):
'sensor': {
'platform': 'simulated'}
}
- self.assertTrue(
- setup_component(self.hass, 'sensor', config))
+ self.assertTrue(setup_component(self.hass, 'sensor', config))
self.hass.block_till_done()
+
assert len(self.hass.states.entity_ids()) == 1
state = self.hass.states.get('sensor.simulated')
- assert state.attributes.get(
- CONF_FRIENDLY_NAME) == DEFAULT_NAME
- assert state.attributes.get(
- CONF_AMP) == DEFAULT_AMP
- assert state.attributes.get(
- CONF_UNIT) is None
- assert state.attributes.get(
- CONF_MEAN) == DEFAULT_MEAN
- assert state.attributes.get(
- CONF_PERIOD) == 60.0
- assert state.attributes.get(
- CONF_PHASE) == DEFAULT_PHASE
- assert state.attributes.get(
- CONF_FWHM) == DEFAULT_FWHM
- assert state.attributes.get(
- CONF_SEED) == DEFAULT_SEED
+
+ assert state.attributes.get(CONF_FRIENDLY_NAME) == DEFAULT_NAME
+ assert state.attributes.get(CONF_AMP) == DEFAULT_AMP
+ assert state.attributes.get(CONF_UNIT) is None
+ assert state.attributes.get(CONF_MEAN) == DEFAULT_MEAN
+ assert state.attributes.get(CONF_PERIOD) == 60.0
+ assert state.attributes.get(CONF_PHASE) == DEFAULT_PHASE
+ assert state.attributes.get(CONF_FWHM) == DEFAULT_FWHM
+ assert state.attributes.get(CONF_SEED) == DEFAULT_SEED
+ assert state.attributes.get(
+ CONF_RELATIVE_TO_EPOCH) == DEFAULT_RELATIVE_TO_EPOCH
diff --git a/tests/components/sonos/__init__.py b/tests/components/sonos/__init__.py
new file mode 100644
index 00000000000000..878e0c17318946
--- /dev/null
+++ b/tests/components/sonos/__init__.py
@@ -0,0 +1 @@
+"""Tests for the Sonos component."""
diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py
new file mode 100644
index 00000000000000..2cbc2360fd44d9
--- /dev/null
+++ b/tests/components/sonos/test_init.py
@@ -0,0 +1,20 @@
+"""Tests for the Sonos config flow."""
+from unittest.mock import patch
+
+from homeassistant import data_entry_flow
+from homeassistant.components import sonos
+
+from tests.common import mock_coro
+
+
+async def test_creating_entry_sets_up_media_player(hass):
+ """Test setting up Sonos loads the media player."""
+ with patch('homeassistant.components.media_player.sonos.async_setup_entry',
+ return_value=mock_coro(True)) as mock_setup, \
+ patch('soco.discover', return_value=True):
+ result = await hass.config_entries.flow.async_init(sonos.DOMAIN)
+ assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+ await hass.async_block_till_done()
+
+ assert len(mock_setup.mock_calls) == 1
diff --git a/tests/components/test_feedreader.py b/tests/components/test_feedreader.py
index c20b297017c027..336d19664b42ff 100644
--- a/tests/components/test_feedreader.py
+++ b/tests/components/test_feedreader.py
@@ -1,6 +1,6 @@
"""The tests for the feedreader component."""
import time
-from datetime import datetime, timedelta
+from datetime import timedelta
import unittest
from genericpath import exists
@@ -118,9 +118,11 @@ def test_feed(self):
assert events[0].data.description == "Description 1"
assert events[0].data.link == "http://www.example.com/link/1"
assert events[0].data.id == "GUID 1"
- assert datetime.fromtimestamp(
- time.mktime(events[0].data.published_parsed)) == \
- datetime(2018, 4, 30, 5, 10, 0)
+ assert events[0].data.published_parsed.tm_year == 2018
+ assert events[0].data.published_parsed.tm_mon == 4
+ assert events[0].data.published_parsed.tm_mday == 30
+ assert events[0].data.published_parsed.tm_hour == 5
+ assert events[0].data.published_parsed.tm_min == 10
assert manager.last_update_successful is True
def test_feed_updates(self):
diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py
deleted file mode 100644
index 657497b868bf34..00000000000000
--- a/tests/components/test_frontend.py
+++ /dev/null
@@ -1,215 +0,0 @@
-"""The tests for Home Assistant frontend."""
-import asyncio
-import re
-from unittest.mock import patch
-
-import pytest
-
-from homeassistant.setup import async_setup_component
-from homeassistant.components.frontend import (
- DOMAIN, CONF_JS_VERSION, CONF_THEMES, CONF_EXTRA_HTML_URL,
- CONF_EXTRA_HTML_URL_ES5, DATA_PANELS)
-from homeassistant.components import websocket_api as wapi
-
-
-@pytest.fixture
-def mock_http_client(hass, aiohttp_client):
- """Start the Hass HTTP component."""
- hass.loop.run_until_complete(async_setup_component(hass, 'frontend', {}))
- return hass.loop.run_until_complete(aiohttp_client(hass.http.app))
-
-
-@pytest.fixture
-def mock_http_client_with_themes(hass, aiohttp_client):
- """Start the Hass HTTP component."""
- hass.loop.run_until_complete(async_setup_component(hass, 'frontend', {
- DOMAIN: {
- CONF_THEMES: {
- 'happy': {
- 'primary-color': 'red'
- }
- }
- }}))
- return hass.loop.run_until_complete(aiohttp_client(hass.http.app))
-
-
-@pytest.fixture
-def mock_http_client_with_urls(hass, aiohttp_client):
- """Start the Hass HTTP component."""
- hass.loop.run_until_complete(async_setup_component(hass, 'frontend', {
- DOMAIN: {
- CONF_JS_VERSION: 'auto',
- CONF_EXTRA_HTML_URL: ["https://domain.com/my_extra_url.html"],
- CONF_EXTRA_HTML_URL_ES5:
- ["https://domain.com/my_extra_url_es5.html"]
- }}))
- return hass.loop.run_until_complete(aiohttp_client(hass.http.app))
-
-
-@asyncio.coroutine
-def test_frontend_and_static(mock_http_client):
- """Test if we can get the frontend."""
- resp = yield from mock_http_client.get('')
- assert resp.status == 200
- assert 'cache-control' not in resp.headers
-
- text = yield from resp.text()
-
- # Test we can retrieve frontend.js
- frontendjs = re.search(
- r'(?P\/frontend_es5\/app-[A-Za-z0-9]{32}.js)', text)
-
- assert frontendjs is not None
- resp = yield from mock_http_client.get(frontendjs.groups(0)[0])
- assert resp.status == 200
- assert 'public' in resp.headers.get('cache-control')
-
-
-@asyncio.coroutine
-def test_dont_cache_service_worker(mock_http_client):
- """Test that we don't cache the service worker."""
- resp = yield from mock_http_client.get('/service_worker_es5.js')
- assert resp.status == 200
- assert 'cache-control' not in resp.headers
-
- resp = yield from mock_http_client.get('/service_worker.js')
- assert resp.status == 200
- assert 'cache-control' not in resp.headers
-
-
-@asyncio.coroutine
-def test_404(mock_http_client):
- """Test for HTTP 404 error."""
- resp = yield from mock_http_client.get('/not-existing')
- assert resp.status == 404
-
-
-@asyncio.coroutine
-def test_we_cannot_POST_to_root(mock_http_client):
- """Test that POST is not allow to root."""
- resp = yield from mock_http_client.post('/')
- assert resp.status == 405
-
-
-@asyncio.coroutine
-def test_states_routes(mock_http_client):
- """All served by index."""
- resp = yield from mock_http_client.get('/states')
- assert resp.status == 200
-
- resp = yield from mock_http_client.get('/states/group.existing')
- assert resp.status == 200
-
-
-@asyncio.coroutine
-def test_themes_api(mock_http_client_with_themes):
- """Test that /api/themes returns correct data."""
- resp = yield from mock_http_client_with_themes.get('/api/themes')
- json = yield from resp.json()
- assert json['default_theme'] == 'default'
- assert json['themes'] == {'happy': {'primary-color': 'red'}}
-
-
-@asyncio.coroutine
-def test_themes_set_theme(hass, mock_http_client_with_themes):
- """Test frontend.set_theme service."""
- yield from hass.services.async_call(DOMAIN, 'set_theme', {'name': 'happy'})
- yield from hass.async_block_till_done()
- resp = yield from mock_http_client_with_themes.get('/api/themes')
- json = yield from resp.json()
- assert json['default_theme'] == 'happy'
-
- yield from hass.services.async_call(
- DOMAIN, 'set_theme', {'name': 'default'})
- yield from hass.async_block_till_done()
- resp = yield from mock_http_client_with_themes.get('/api/themes')
- json = yield from resp.json()
- assert json['default_theme'] == 'default'
-
-
-@asyncio.coroutine
-def test_themes_set_theme_wrong_name(hass, mock_http_client_with_themes):
- """Test frontend.set_theme service called with wrong name."""
- yield from hass.services.async_call(DOMAIN, 'set_theme', {'name': 'wrong'})
- yield from hass.async_block_till_done()
- resp = yield from mock_http_client_with_themes.get('/api/themes')
- json = yield from resp.json()
- assert json['default_theme'] == 'default'
-
-
-@asyncio.coroutine
-def test_themes_reload_themes(hass, mock_http_client_with_themes):
- """Test frontend.reload_themes service."""
- with patch('homeassistant.components.frontend.load_yaml_config_file',
- return_value={DOMAIN: {
- CONF_THEMES: {
- 'sad': {'primary-color': 'blue'}
- }}}):
- yield from hass.services.async_call(DOMAIN, 'set_theme',
- {'name': 'happy'})
- yield from hass.services.async_call(DOMAIN, 'reload_themes')
- yield from hass.async_block_till_done()
- resp = yield from mock_http_client_with_themes.get('/api/themes')
- json = yield from resp.json()
- assert json['themes'] == {'sad': {'primary-color': 'blue'}}
- assert json['default_theme'] == 'default'
-
-
-@asyncio.coroutine
-def test_missing_themes(mock_http_client):
- """Test that themes API works when themes are not defined."""
- resp = yield from mock_http_client.get('/api/themes')
- assert resp.status == 200
- json = yield from resp.json()
- assert json['default_theme'] == 'default'
- assert json['themes'] == {}
-
-
-@asyncio.coroutine
-def test_extra_urls(mock_http_client_with_urls):
- """Test that extra urls are loaded."""
- resp = yield from mock_http_client_with_urls.get('/states?latest')
- assert resp.status == 200
- text = yield from resp.text()
- assert text.find('href="https://domain.com/my_extra_url.html"') >= 0
-
-
-@asyncio.coroutine
-def test_extra_urls_es5(mock_http_client_with_urls):
- """Test that es5 extra urls are loaded."""
- resp = yield from mock_http_client_with_urls.get('/states?es5')
- assert resp.status == 200
- text = yield from resp.text()
- assert text.find('href="https://domain.com/my_extra_url_es5.html"') >= 0
-
-
-@asyncio.coroutine
-def test_panel_without_path(hass):
- """Test panel registration without file path."""
- yield from hass.components.frontend.async_register_panel(
- 'test_component', 'nonexistant_file')
- yield from async_setup_component(hass, 'frontend', {})
- assert 'test_component' not in hass.data[DATA_PANELS]
-
-
-async def test_get_panels(hass, hass_ws_client):
- """Test get_panels command."""
- await async_setup_component(hass, 'frontend')
- await hass.components.frontend.async_register_built_in_panel(
- 'map', 'Map', 'mdi:account-location')
-
- client = await hass_ws_client(hass)
- await client.send_json({
- 'id': 5,
- 'type': 'get_panels',
- })
-
- msg = await client.receive_json()
-
- assert msg['id'] == 5
- assert msg['type'] == wapi.TYPE_RESULT
- assert msg['success']
- assert msg['result']['map']['component_name'] == 'map'
- assert msg['result']['map']['url_path'] == 'map'
- assert msg['result']['map']['icon'] == 'mdi:account-location'
- assert msg['result']['map']['title'] == 'Map'
diff --git a/tests/components/test_init.py b/tests/components/test_init.py
index c8c7e0d809b5a9..1e565054637766 100644
--- a/tests/components/test_init.py
+++ b/tests/components/test_init.py
@@ -74,30 +74,6 @@ def test_toggle(self):
self.hass.block_till_done()
self.assertEqual(1, len(calls))
- @patch('homeassistant.core.ServiceRegistry.call')
- async def test_turn_on_to_not_block_for_domains_without_service(self,
- mock_call):
- """Test if turn_on is blocking domain with no service."""
- async_mock_service(self.hass, 'light', SERVICE_TURN_ON)
-
- # We can't test if our service call results in services being called
- # because by mocking out the call service method, we mock out all
- # So we mimic how the service registry calls services
- service_call = ha.ServiceCall('homeassistant', 'turn_on', {
- 'entity_id': ['light.test', 'sensor.bla', 'light.bla']
- })
- service = self.hass.services._services['homeassistant']['turn_on']
- await service.func(service_call)
-
- self.assertEqual(2, mock_call.call_count)
- self.assertEqual(
- ('light', 'turn_on', {'entity_id': ['light.bla', 'light.test']},
- True),
- mock_call.call_args_list[0][0])
- self.assertEqual(
- ('sensor', 'turn_on', {'entity_id': ['sensor.bla']}, False),
- mock_call.call_args_list[1][0])
-
@patch('homeassistant.config.os.path.isfile', Mock(return_value=True))
def test_reload_core_conf(self):
"""Test reload core conf service."""
@@ -284,3 +260,29 @@ async def test_turn_on_multiple_intent(hass):
assert call.domain == 'light'
assert call.service == 'turn_on'
assert call.data == {'entity_id': ['light.test_lights_2']}
+
+
+async def test_turn_on_to_not_block_for_domains_without_service(hass):
+ """Test if turn_on is blocking domain with no service."""
+ await comps.async_setup(hass, {})
+ async_mock_service(hass, 'light', SERVICE_TURN_ON)
+ hass.states.async_set('light.Bowl', STATE_ON)
+ hass.states.async_set('light.Ceiling', STATE_OFF)
+
+ # We can't test if our service call results in services being called
+ # because by mocking out the call service method, we mock out all
+ # So we mimic how the service registry calls services
+ service_call = ha.ServiceCall('homeassistant', 'turn_on', {
+ 'entity_id': ['light.test', 'sensor.bla', 'light.bla']
+ })
+ service = hass.services._services['homeassistant']['turn_on']
+
+ with patch('homeassistant.core.ServiceRegistry.async_call',
+ side_effect=lambda *args: mock_coro()) as mock_call:
+ await service.func(service_call)
+
+ assert mock_call.call_count == 2
+ assert mock_call.call_args_list[0][0] == (
+ 'light', 'turn_on', {'entity_id': ['light.bla', 'light.test']}, True)
+ assert mock_call.call_args_list[1][0] == (
+ 'sensor', 'turn_on', {'entity_id': ['sensor.bla']}, False)
diff --git a/tests/components/test_logger.py b/tests/components/test_logger.py
index 61cb42e8bb5bcd..a55a66c6505fe5 100644
--- a/tests/components/test_logger.py
+++ b/tests/components/test_logger.py
@@ -10,6 +10,7 @@
RECORD = namedtuple('record', ('name', 'levelno'))
+NO_DEFAULT_CONFIG = {'logger': {}}
NO_LOGS_CONFIG = {'logger': {'default': 'info'}}
TEST_CONFIG = {
'logger': {
@@ -99,3 +100,29 @@ def test_set_filter(self):
self.assert_logged('asdf', logging.DEBUG)
self.assert_logged('dummy', logging.WARNING)
+
+ def test_set_default_filter_empty_config(self):
+ """Test change default log level from empty configuration."""
+ self.setup_logger(NO_DEFAULT_CONFIG)
+
+ self.assert_logged('test', logging.DEBUG)
+
+ self.hass.services.call(
+ logger.DOMAIN, 'set_default_level', {'level': 'warning'})
+ self.hass.block_till_done()
+
+ self.assert_not_logged('test', logging.DEBUG)
+
+ def test_set_default_filter(self):
+ """Test change default log level with existing default."""
+ self.setup_logger(TEST_CONFIG)
+
+ self.assert_not_logged('asdf', logging.DEBUG)
+ self.assert_logged('dummy', logging.WARNING)
+
+ self.hass.services.call(
+ logger.DOMAIN, 'set_default_level', {'level': 'debug'})
+ self.hass.block_till_done()
+
+ self.assert_logged('asdf', logging.DEBUG)
+ self.assert_logged('dummy', logging.WARNING)
diff --git a/tests/components/test_ring.py b/tests/components/test_ring.py
index 3837ec130611e7..7b974686a4e132 100644
--- a/tests/components/test_ring.py
+++ b/tests/components/test_ring.py
@@ -42,6 +42,8 @@ def tearDown(self): # pylint: disable=invalid-name
@requests_mock.Mocker()
def test_setup(self, mock):
"""Test the setup."""
+ mock.post('https://oauth.ring.com/oauth/token',
+ text=load_fixture('ring_oauth.json'))
mock.post('https://api.ring.com/clients_api/session',
text=load_fixture('ring_session.json'))
response = ring.setup(self.hass, self.config)
diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py
index cff103142b0b92..fbd8584a7d14ae 100644
--- a/tests/components/test_websocket_api.py
+++ b/tests/components/test_websocket_api.py
@@ -311,8 +311,9 @@ def test_unknown_command(websocket_client):
'type': 'unknown_command',
})
- msg = yield from websocket_client.receive()
- assert msg.type == WSMsgType.close
+ msg = yield from websocket_client.receive_json()
+ assert not msg['success']
+ assert msg['error']['code'] == wapi.ERR_UNKNOWN_COMMAND
async def test_auth_with_token(hass, aiohttp_client, hass_access_token):
diff --git a/tests/components/weather/test_ipma.py b/tests/components/weather/test_ipma.py
new file mode 100644
index 00000000000000..7df6166a2b6a18
--- /dev/null
+++ b/tests/components/weather/test_ipma.py
@@ -0,0 +1,85 @@
+"""The tests for the IPMA weather component."""
+import unittest
+from unittest.mock import patch
+from collections import namedtuple
+
+from homeassistant.components import weather
+from homeassistant.components.weather import (
+ ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE,
+ ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED)
+from homeassistant.util.unit_system import METRIC_SYSTEM
+from homeassistant.setup import setup_component
+
+from tests.common import get_test_home_assistant, MockDependency
+
+
+class MockStation():
+ """Mock Station from pyipma."""
+
+ @classmethod
+ async def get(cls, websession, lat, lon):
+ """Mock Factory."""
+ return MockStation()
+
+ async def observation(self):
+ """Mock Observation."""
+ Observation = namedtuple('Observation', ['temperature', 'humidity',
+ 'windspeed', 'winddirection',
+ 'precipitation', 'pressure',
+ 'description'])
+
+ return Observation(18, 71.0, 3.94, 'NW', 0, 1000.0, '---')
+
+ async def forecast(self):
+ """Mock Forecast."""
+ Forecast = namedtuple('Forecast', ['precipitaProb', 'tMin', 'tMax',
+ 'predWindDir', 'idWeatherType',
+ 'classWindSpeed', 'longitude',
+ 'forecastDate', 'classPrecInt',
+ 'latitude', 'description'])
+
+ return [Forecast(73.0, 13.7, 18.7, 'NW', 6, 2, -8.64,
+ '2018-05-31', 2, 40.61,
+ 'Aguaceiros, com vento Moderado de Noroeste')]
+
+ @property
+ def local(self):
+ """Mock location."""
+ return "HomeTown"
+
+
+class TestIPMA(unittest.TestCase):
+ """Test the IPMA weather component."""
+
+ def setUp(self):
+ """Setup things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+ self.hass.config.units = METRIC_SYSTEM
+ self.lat = self.hass.config.latitude = 40.00
+ self.lon = self.hass.config.longitude = -8.00
+
+ def tearDown(self):
+ """Stop down everything that was started."""
+ self.hass.stop()
+
+ @MockDependency("pyipma")
+ @patch("pyipma.Station", new=MockStation)
+ def test_setup(self, mock_pyipma):
+ """Test for successfully setting up the IPMA platform."""
+ self.assertTrue(setup_component(self.hass, weather.DOMAIN, {
+ 'weather': {
+ 'name': 'HomeTown',
+ 'platform': 'ipma',
+ }
+ }))
+
+ state = self.hass.states.get('weather.hometown')
+ self.assertEqual(state.state, 'rainy')
+
+ data = state.attributes
+ self.assertEqual(data.get(ATTR_WEATHER_TEMPERATURE), 18.0)
+ self.assertEqual(data.get(ATTR_WEATHER_HUMIDITY), 71)
+ self.assertEqual(data.get(ATTR_WEATHER_PRESSURE), 1000.0)
+ self.assertEqual(data.get(ATTR_WEATHER_WIND_SPEED), 3.94)
+ self.assertEqual(data.get(ATTR_WEATHER_WIND_BEARING), 'NW')
+ self.assertEqual(state.attributes.get('friendly_name'), 'HomeTown')
diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py
index c26b3375f3ace4..92dee05818dedb 100644
--- a/tests/components/zone/test_init.py
+++ b/tests/components/zone/test_init.py
@@ -17,7 +17,6 @@ async def test_setup_entry_successful(hass):
zone.CONF_NAME: 'Test Zone',
zone.CONF_LATITUDE: 1.1,
zone.CONF_LONGITUDE: -2.2,
- zone.CONF_RADIUS: 250,
zone.CONF_RADIUS: True
}
hass.data[zone.DOMAIN] = {}
diff --git a/tests/fixtures/ring_oauth.json b/tests/fixtures/ring_oauth.json
new file mode 100644
index 00000000000000..5e69ddde065272
--- /dev/null
+++ b/tests/fixtures/ring_oauth.json
@@ -0,0 +1,8 @@
+{
+ "access_token": "eyJ0eWfvEQwqfJNKyQ9999",
+ "token_type": "bearer",
+ "expires_in": 3600,
+ "refresh_token": "67695a26bdefc1ac8999",
+ "scope": "client",
+ "created_at": 1529099870
+}
diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py
new file mode 100644
index 00000000000000..d3f13ac43021bc
--- /dev/null
+++ b/tests/helpers/test_config_entry_flow.py
@@ -0,0 +1,116 @@
+"""Tests for the Config Entry Flow helper."""
+from unittest.mock import patch
+
+import pytest
+
+from homeassistant import config_entries, data_entry_flow, loader
+from homeassistant.helpers import config_entry_flow
+from tests.common import MockConfigEntry, MockModule
+
+
+@pytest.fixture
+def flow_conf(hass):
+ """Register a handler."""
+ handler_conf = {
+ 'discovered': False,
+ }
+
+ async def has_discovered_devices(hass):
+ """Mock if we have discovered devices."""
+ return handler_conf['discovered']
+
+ with patch.dict(config_entries.HANDLERS):
+ config_entry_flow.register_discovery_flow(
+ 'test', 'Test', has_discovered_devices)
+ yield handler_conf
+
+
+async def test_single_entry_allowed(hass, flow_conf):
+ """Test only a single entry is allowed."""
+ flow = config_entries.HANDLERS['test']()
+ flow.hass = hass
+
+ MockConfigEntry(domain='test').add_to_hass(hass)
+ result = await flow.async_step_init()
+
+ assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result['reason'] == 'single_instance_allowed'
+
+
+async def test_user_no_devices_found(hass, flow_conf):
+ """Test if no devices found."""
+ flow = config_entries.HANDLERS['test']()
+ flow.hass = hass
+
+ result = await flow.async_step_init()
+
+ assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result['reason'] == 'no_devices_found'
+
+
+async def test_user_no_confirmation(hass, flow_conf):
+ """Test user requires no confirmation to setup."""
+ flow = config_entries.HANDLERS['test']()
+ flow.hass = hass
+ flow_conf['discovered'] = True
+
+ result = await flow.async_step_init()
+
+ assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+
+async def test_discovery_single_instance(hass, flow_conf):
+ """Test we ask for confirmation via discovery."""
+ flow = config_entries.HANDLERS['test']()
+ flow.hass = hass
+
+ MockConfigEntry(domain='test').add_to_hass(hass)
+ result = await flow.async_step_discovery({})
+
+ assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result['reason'] == 'single_instance_allowed'
+
+
+async def test_discovery_confirmation(hass, flow_conf):
+ """Test we ask for confirmation via discovery."""
+ flow = config_entries.HANDLERS['test']()
+ flow.hass = hass
+
+ result = await flow.async_step_discovery({})
+
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['step_id'] == 'confirm'
+
+ result = await flow.async_step_confirm({})
+ assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+
+async def test_multiple_discoveries(hass, flow_conf):
+ """Test we only create one instance for multiple discoveries."""
+ loader.set_component(hass, 'test', MockModule('test'))
+
+ result = await hass.config_entries.flow.async_init(
+ 'test', source=data_entry_flow.SOURCE_DISCOVERY, data={})
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+
+ # Second discovery
+ result = await hass.config_entries.flow.async_init(
+ 'test', source=data_entry_flow.SOURCE_DISCOVERY, data={})
+ assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
+
+
+async def test_user_init_trumps_discovery(hass, flow_conf):
+ """Test a user initialized one will finish and cancel discovered one."""
+ loader.set_component(hass, 'test', MockModule('test'))
+
+ # Discovery starts flow
+ result = await hass.config_entries.flow.async_init(
+ 'test', source=data_entry_flow.SOURCE_DISCOVERY, data={})
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+
+ # User starts flow
+ result = await hass.config_entries.flow.async_init('test', data={})
+ assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+
+ # Discovery flow has been aborted
+ assert len(hass.config_entries.flow.async_progress()) == 0
diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py
index 504f31cc9875c8..b4910723c8dfa8 100644
--- a/tests/helpers/test_entity_component.py
+++ b/tests/helpers/test_entity_component.py
@@ -346,7 +346,8 @@ async def test_setup_entry(hass):
mock_setup_entry = Mock(return_value=mock_coro(True))
loader.set_component(
hass, 'test_domain.entry_domain',
- MockPlatform(async_setup_entry=mock_setup_entry))
+ MockPlatform(async_setup_entry=mock_setup_entry,
+ scan_interval=timedelta(seconds=5)))
component = EntityComponent(_LOGGER, DOMAIN, hass)
entry = MockConfigEntry(domain='entry_domain')
@@ -357,6 +358,9 @@ async def test_setup_entry(hass):
assert p_hass is hass
assert p_entry is entry
+ assert component._platforms[entry.entry_id].scan_interval == \
+ timedelta(seconds=5)
+
async def test_setup_entry_platform_not_exist(hass):
"""Test setup entry fails if platform doesnt exist."""
diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py
index 4e09f9576f2c61..2d2f148189f683 100644
--- a/tests/helpers/test_entity_platform.py
+++ b/tests/helpers/test_entity_platform.py
@@ -16,7 +16,7 @@
from tests.common import (
get_test_home_assistant, MockPlatform, fire_time_changed, mock_registry,
- MockEntity, MockEntityPlatform, MockConfigEntry, mock_coro)
+ MockEntity, MockEntityPlatform, MockConfigEntry)
_LOGGER = logging.getLogger(__name__)
DOMAIN = "test_domain"
@@ -516,11 +516,19 @@ async def test_entity_registry_updates(hass):
async def test_setup_entry(hass):
"""Test we can setup an entry."""
- async_setup_entry = Mock(return_value=mock_coro(True))
+ registry = mock_registry(hass)
+
+ async def async_setup_entry(hass, config_entry, async_add_devices):
+ """Mock setup entry method."""
+ async_add_devices([
+ MockEntity(name='test1', unique_id='unique')
+ ])
+ return True
+
platform = MockPlatform(
async_setup_entry=async_setup_entry
)
- config_entry = MockConfigEntry()
+ config_entry = MockConfigEntry(entry_id='super-mock-id')
entity_platform = MockEntityPlatform(
hass,
platform_name=config_entry.domain,
@@ -528,10 +536,13 @@ async def test_setup_entry(hass):
)
assert await entity_platform.async_setup_entry(config_entry)
-
+ await hass.async_block_till_done()
full_name = '{}.{}'.format(entity_platform.domain, config_entry.domain)
assert full_name in hass.config.components
- assert len(async_setup_entry.mock_calls) == 1
+ assert len(hass.states.async_entity_ids()) == 1
+ assert len(registry.entities) == 1
+ assert registry.entities['test_domain.test1'].config_entry_id == \
+ 'super-mock-id'
async def test_setup_entry_platform_not_ready(hass, caplog):
@@ -581,3 +592,13 @@ async def test_reset_cancels_retry_setup(hass):
assert len(mock_call_later.return_value.mock_calls) == 1
assert ent_platform._async_cancel_retry_setup is None
+
+
+@asyncio.coroutine
+def test_not_fails_with_adding_empty_entities_(hass):
+ """Test for not fails on empty entities list."""
+ component = EntityComponent(_LOGGER, DOMAIN, hass)
+
+ yield from component.async_add_entities([])
+
+ assert len(hass.states.async_entity_ids()) == 0
diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py
index 492b97f63873b9..6808206243f8d1 100644
--- a/tests/helpers/test_entity_registry.py
+++ b/tests/helpers/test_entity_registry.py
@@ -86,7 +86,8 @@ def test_save_timer_reset_on_subsequent_save(hass, registry):
def test_loading_saving_data(hass, registry):
"""Test that we load/save data correctly."""
orig_entry1 = registry.async_get_or_create('light', 'hue', '1234')
- orig_entry2 = registry.async_get_or_create('light', 'hue', '5678')
+ orig_entry2 = registry.async_get_or_create(
+ 'light', 'hue', '5678', config_entry_id='mock-id')
assert len(registry.entities) == 2
diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py
index 3e4d47397799a7..e329f835f84b71 100644
--- a/tests/test_bootstrap.py
+++ b/tests/test_bootstrap.py
@@ -9,7 +9,7 @@
from homeassistant import bootstrap
import homeassistant.util.dt as dt_util
-from tests.common import patch_yaml_files, get_test_config_dir
+from tests.common import patch_yaml_files, get_test_config_dir, mock_coro
ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE
VERSION_PATH = os.path.join(get_test_config_dir(), config_util.VERSION_FILE)
@@ -52,3 +52,55 @@ def test_home_assistant_core_config_validation(hass):
}
}, hass)
assert result is None
+
+
+def test_from_config_dict_not_mount_deps_folder(loop):
+ """Test that we do not mount the deps folder inside from_config_dict."""
+ with patch('homeassistant.bootstrap.is_virtual_env', return_value=False), \
+ patch('homeassistant.core.HomeAssistant',
+ return_value=Mock(loop=loop)), \
+ patch('homeassistant.bootstrap.async_mount_local_lib_path',
+ return_value=mock_coro()) as mock_mount, \
+ patch('homeassistant.bootstrap.async_from_config_dict',
+ return_value=mock_coro()):
+
+ bootstrap.from_config_dict({}, config_dir='.')
+ assert len(mock_mount.mock_calls) == 1
+
+ with patch('homeassistant.bootstrap.is_virtual_env', return_value=True), \
+ patch('homeassistant.core.HomeAssistant',
+ return_value=Mock(loop=loop)), \
+ patch('homeassistant.bootstrap.async_mount_local_lib_path',
+ return_value=mock_coro()) as mock_mount, \
+ patch('homeassistant.bootstrap.async_from_config_dict',
+ return_value=mock_coro()):
+
+ bootstrap.from_config_dict({}, config_dir='.')
+ assert len(mock_mount.mock_calls) == 0
+
+
+async def test_async_from_config_file_not_mount_deps_folder(loop):
+ """Test that we not mount the deps folder inside async_from_config_file."""
+ hass = Mock(async_add_job=Mock(side_effect=lambda *args: mock_coro()))
+
+ with patch('homeassistant.bootstrap.is_virtual_env', return_value=False), \
+ patch('homeassistant.bootstrap.async_enable_logging',
+ return_value=mock_coro()), \
+ patch('homeassistant.bootstrap.async_mount_local_lib_path',
+ return_value=mock_coro()) as mock_mount, \
+ patch('homeassistant.bootstrap.async_from_config_dict',
+ return_value=mock_coro()):
+
+ await bootstrap.async_from_config_file('mock-path', hass)
+ assert len(mock_mount.mock_calls) == 1
+
+ with patch('homeassistant.bootstrap.is_virtual_env', return_value=True), \
+ patch('homeassistant.bootstrap.async_enable_logging',
+ return_value=mock_coro()), \
+ patch('homeassistant.bootstrap.async_mount_local_lib_path',
+ return_value=mock_coro()) as mock_mount, \
+ patch('homeassistant.bootstrap.async_from_config_dict',
+ return_value=mock_coro()):
+
+ await bootstrap.async_from_config_file('mock-path', hass)
+ assert len(mock_mount.mock_calls) == 0
diff --git a/tests/test_config.py b/tests/test_config.py
index d22d6b2acfd1a9..717a3f62ec9a33 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -589,7 +589,7 @@ def test_merge(merge_log_err, hass):
assert len(config['input_boolean']) == 2
assert len(config['input_select']) == 1
assert len(config['light']) == 3
- assert config['wake_on_lan'] is None
+ assert isinstance(config['wake_on_lan'], OrderedDict)
def test_merge_try_falsy(merge_log_err, hass):
@@ -656,6 +656,14 @@ def test_merge_type_mismatch(merge_log_err, hass):
def test_merge_once_only_keys(merge_log_err, hass):
"""Test if we have a merge for a comp that may occur only once. Keys."""
+ packages = {'pack_2': {'api': None}}
+ config = {
+ config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages},
+ 'api': None,
+ }
+ config_util.merge_packages_config(hass, config, packages)
+ assert config['api'] == OrderedDict()
+
packages = {'pack_2': {'api': {
'key_3': 3,
}}}
@@ -755,7 +763,7 @@ def test_merge_duplicate_keys(merge_log_err, hass):
}
config = {
config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages},
- 'input_select': {'ib1': None},
+ 'input_select': {'ib1': 1},
}
config_util.merge_packages_config(hass, config, packages)
diff --git a/tests/util/test_package.py b/tests/util/test_package.py
index 33db052f45acae..ab9f9f0ad2c5e3 100644
--- a/tests/util/test_package.py
+++ b/tests/util/test_package.py
@@ -201,20 +201,8 @@ def test_check_package_zip():
assert not package.check_package_exists(TEST_ZIP_REQ)
-def test_get_user_site(deps_dir, lib_dir, mock_popen, mock_env_copy):
- """Test get user site directory."""
- env = mock_env_copy()
- env['PYTHONUSERBASE'] = os.path.abspath(deps_dir)
- args = [sys.executable, '-m', 'site', '--user-site']
- ret = package.get_user_site(deps_dir)
- assert mock_popen.call_count == 1
- assert mock_popen.call_args == call(
- args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env)
- assert ret == lib_dir
-
-
@asyncio.coroutine
-def test_async_get_user_site(hass, mock_env_copy):
+def test_async_get_user_site(mock_env_copy):
"""Test async get user site directory."""
deps_dir = '/deps_dir'
env = mock_env_copy()
@@ -222,10 +210,10 @@ def test_async_get_user_site(hass, mock_env_copy):
args = [sys.executable, '-m', 'site', '--user-site']
with patch('homeassistant.util.package.asyncio.create_subprocess_exec',
return_value=mock_async_subprocess()) as popen_mock:
- ret = yield from package.async_get_user_site(deps_dir, hass.loop)
+ ret = yield from package.async_get_user_site(deps_dir)
assert popen_mock.call_count == 1
assert popen_mock.call_args == call(
- *args, loop=hass.loop, stdin=asyncio.subprocess.PIPE,
+ *args, stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL,
env=env)
assert ret == os.path.join(deps_dir, 'lib_dir')