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')