diff --git a/.coveragerc b/.coveragerc index a8d7d89544d..d5296455981 100644 --- a/.coveragerc +++ b/.coveragerc @@ -28,6 +28,9 @@ omit = homeassistant/components/apple_tv.py homeassistant/components/*/apple_tv.py + homeassistant/components/aqualogic.py + homeassistant/components/*/aqualogic.py + homeassistant/components/arduino.py homeassistant/components/*/arduino.py @@ -53,7 +56,7 @@ omit = homeassistant/components/bbb_gpio.py homeassistant/components/*/bbb_gpio.py - homeassistant/components/blink.py + homeassistant/components/blink/* homeassistant/components/*/blink.py homeassistant/components/bloomsky.py @@ -105,8 +108,11 @@ omit = homeassistant/components/envisalink.py homeassistant/components/*/envisalink.py + homeassistant/components/evohome.py + homeassistant/components/*/evohome.py + homeassistant/components/fritzbox.py - homeassistant/components/switch/fritzbox.py + homeassistant/components/*/fritzbox.py homeassistant/components/ecovacs.py homeassistant/components/*/ecovacs.py @@ -310,6 +316,9 @@ omit = homeassistant/components/*/thinkingcleaner.py + homeassistant/components/tibber/* + homeassistant/components/*/tibber.py + homeassistant/components/toon.py homeassistant/components/*/toon.py @@ -510,6 +519,7 @@ omit = homeassistant/components/light/lw12wifi.py homeassistant/components/light/mystrom.py homeassistant/components/light/nanoleaf_aurora.py + homeassistant/components/light/opple.py homeassistant/components/light/osramlightify.py homeassistant/components/light/piglow.py homeassistant/components/light/rpi_gpio_pwm.py @@ -620,7 +630,6 @@ omit = homeassistant/components/notify/telstra.py homeassistant/components/notify/twitter.py homeassistant/components/notify/xmpp.py - homeassistant/components/notify/yessssms.py homeassistant/components/nuimo_controller.py homeassistant/components/prometheus.py homeassistant/components/rainbird.py @@ -679,6 +688,7 @@ omit = homeassistant/components/sensor/fritzbox_netmonitor.py homeassistant/components/sensor/gearbest.py homeassistant/components/sensor/geizhals.py + homeassistant/components/sensor/gitlab_ci.py homeassistant/components/sensor/gitter.py homeassistant/components/sensor/glances.py homeassistant/components/sensor/google_travel_time.py @@ -687,6 +697,7 @@ omit = homeassistant/components/sensor/haveibeenpwned.py homeassistant/components/sensor/hp_ilo.py homeassistant/components/sensor/htu21d.py + homeassistant/components/sensor/upnp.py homeassistant/components/sensor/imap_email_content.py homeassistant/components/sensor/imap.py homeassistant/components/sensor/influxdb.py @@ -709,6 +720,7 @@ omit = homeassistant/components/sensor/mqtt_room.py homeassistant/components/sensor/mvglive.py homeassistant/components/sensor/nederlandse_spoorwegen.py + homeassistant/components/sensor/netatmo_public.py homeassistant/components/sensor/netdata.py homeassistant/components/sensor/netdata_public.py homeassistant/components/sensor/neurio_energy.py @@ -764,7 +776,6 @@ omit = homeassistant/components/sensor/tank_utility.py homeassistant/components/sensor/ted5000.py homeassistant/components/sensor/temper.py - homeassistant/components/sensor/tibber.py homeassistant/components/sensor/time_date.py homeassistant/components/sensor/torque.py homeassistant/components/sensor/trafikverket_weatherstation.py @@ -772,7 +783,6 @@ omit = homeassistant/components/sensor/travisci.py homeassistant/components/sensor/twitch.py homeassistant/components/sensor/uber.py - homeassistant/components/sensor/upnp.py homeassistant/components/sensor/ups.py homeassistant/components/sensor/uscis.py homeassistant/components/sensor/vasttrafik.py diff --git a/CODEOWNERS b/CODEOWNERS index b6ce8c04909..91d5fd67670 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -41,6 +41,7 @@ homeassistant/components/hassio.py @home-assistant/hassio # Individual components homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell +homeassistant/components/alarm_control_panel/simplisafe.py @bachya homeassistant/components/binary_sensor/hikvision.py @mezz64 homeassistant/components/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/camera/yi.py @bachya @@ -50,6 +51,7 @@ homeassistant/components/climate/sensibo.py @andrey-git homeassistant/components/cover/group.py @cdce8p homeassistant/components/cover/template.py @PhracturedBlue homeassistant/components/device_tracker/automatic.py @armills +homeassistant/components/device_tracker/huawei_router.py @abmantis homeassistant/components/device_tracker/tile.py @bachya homeassistant/components/history_graph.py @andrey-git homeassistant/components/light/lifx.py @amelchio @@ -80,19 +82,21 @@ homeassistant/components/sensor/qnap.py @colinodell homeassistant/components/sensor/sma.py @kellerza homeassistant/components/sensor/sql.py @dgomes homeassistant/components/sensor/sytadin.py @gautric -homeassistant/components/sensor/tibber.py @danielhiversen -homeassistant/components/sensor/upnp.py @dgomes homeassistant/components/sensor/waqi.py @andrey-git homeassistant/components/switch/tplink.py @rytilahti homeassistant/components/vacuum/roomba.py @pschmitt homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi homeassistant/components/*/axis.py @kane610 +homeassistant/components/blink/* @fronzbot +homeassistant/components/*/blink.py @fronzbot homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/*/broadlink.py @danielhiversen homeassistant/components/*/deconz.py @kane610 homeassistant/components/ecovacs.py @OverloadUT homeassistant/components/*/ecovacs.py @OverloadUT +homeassistant/components/edp_redy.py @abmantis +homeassistant/components/*/edp_redy.py @abmantis homeassistant/components/eight_sleep.py @mezz64 homeassistant/components/*/eight_sleep.py @mezz64 homeassistant/components/hive.py @Rendili @KJonline @@ -106,7 +110,7 @@ homeassistant/components/konnected.py @heythisisnate homeassistant/components/*/konnected.py @heythisisnate homeassistant/components/matrix.py @tinloaf homeassistant/components/*/matrix.py @tinloaf -homeassistant/components/openuv.py @bachya +homeassistant/components/openuv/* @bachya homeassistant/components/*/openuv.py @bachya homeassistant/components/qwikswitch.py @kellerza homeassistant/components/*/qwikswitch.py @kellerza @@ -119,6 +123,8 @@ homeassistant/components/tesla.py @zabuldon homeassistant/components/*/tesla.py @zabuldon homeassistant/components/tellduslive.py @molobrakos @fredrike homeassistant/components/*/tellduslive.py @molobrakos @fredrike +homeassistant/components/tibber/* @danielhiversen +homeassistant/components/*/tibber.py @danielhiversen homeassistant/components/*/tradfri.py @ggravlingen homeassistant/components/upcloud.py @scop homeassistant/components/*/upcloud.py @scop diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index fb4700c806f..54c34d8ec2c 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -1,9 +1,9 @@ """Storage for auth models.""" from collections import OrderedDict from datetime import timedelta +import hmac from logging import getLogger from typing import Any, Dict, List, Optional # noqa: F401 -import hmac from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION from homeassistant.core import HomeAssistant, callback @@ -28,7 +28,8 @@ class AuthStore: """Initialize the auth store.""" self.hass = hass self._users = None # type: Optional[Dict[str, models.User]] - self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY, + private=True) async def async_get_users(self) -> List[models.User]: """Retrieve all users.""" @@ -213,14 +214,24 @@ class AuthStore: if self._users is not None: return - users = OrderedDict() # type: Dict[str, models.User] - if data is None: - self._users = users + self._set_defaults() return + users = OrderedDict() # type: Dict[str, models.User] + + # When creating objects we mention each attribute explicetely. This + # prevents crashing if user rolls back HA version after a new property + # was added. + for user_dict in data['users']: - users[user_dict['id']] = models.User(**user_dict) + users[user_dict['id']] = models.User( + name=user_dict['name'], + id=user_dict['id'], + is_owner=user_dict['is_owner'], + is_active=user_dict['is_active'], + system_generated=user_dict['system_generated'], + ) for cred_dict in data['credentials']: users[cred_dict['user_id']].credentials.append(models.Credentials( @@ -340,3 +351,7 @@ class AuthStore: 'credentials': credentials, 'refresh_tokens': refresh_tokens, } + + def _set_defaults(self) -> None: + """Set default values for auth store.""" + self._users = OrderedDict() # type: Dict[str, models.User] diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 84f9de614c1..03be4c74d32 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -85,7 +85,7 @@ class NotifyAuthModule(MultiFactorAuthModule): super().__init__(hass, config) self._user_settings = None # type: Optional[_UsersDict] self._user_store = hass.helpers.storage.Store( - STORAGE_VERSION, STORAGE_KEY) + STORAGE_VERSION, STORAGE_KEY, private=True) self._include = config.get(CONF_INCLUDE, []) self._exclude = config.get(CONF_EXCLUDE, []) self._message_template = config[CONF_MESSAGE] diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index 625cc0302e1..9b5896ef666 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -67,7 +67,7 @@ class TotpAuthModule(MultiFactorAuthModule): super().__init__(hass, config) self._users = None # type: Optional[Dict[str, str]] self._user_store = hass.helpers.storage.Store( - STORAGE_VERSION, STORAGE_KEY) + STORAGE_VERSION, STORAGE_KEY, private=True) @property def input_schema(self) -> vol.Schema: diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index b0f4024c3ab..bd00ca72b83 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -19,19 +19,19 @@ class User: """A user.""" name = attr.ib(type=str) # type: Optional[str] - id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) + id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex) is_owner = attr.ib(type=bool, default=False) is_active = attr.ib(type=bool, default=False) system_generated = attr.ib(type=bool, default=False) # List of credentials of a user. credentials = attr.ib( - type=list, default=attr.Factory(list), cmp=False + type=list, factory=list, cmp=False ) # type: List[Credentials] # Tokens associated with a user. refresh_tokens = attr.ib( - type=dict, default=attr.Factory(dict), cmp=False + type=dict, factory=dict, cmp=False ) # type: Dict[str, RefreshToken] @@ -48,12 +48,10 @@ class RefreshToken: validator=attr.validators.in_(( TOKEN_TYPE_NORMAL, TOKEN_TYPE_SYSTEM, TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN))) - id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) - created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow)) - token = attr.ib(type=str, - default=attr.Factory(lambda: generate_secret(64))) - jwt_key = attr.ib(type=str, - default=attr.Factory(lambda: generate_secret(64))) + id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex) + created_at = attr.ib(type=datetime, factory=dt_util.utcnow) + token = attr.ib(type=str, factory=lambda: generate_secret(64)) + jwt_key = attr.ib(type=str, factory=lambda: generate_secret(64)) last_used_at = attr.ib(type=Optional[datetime], default=None) last_used_ip = attr.ib(type=Optional[str], default=None) @@ -69,7 +67,7 @@ class Credentials: # Allow the auth provider to store data to represent their auth. data = attr.ib(type=dict) - id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) + id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex) is_new = attr.ib(type=bool, default=True) diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index c743a5b7f65..8710e7c60bc 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -52,7 +52,8 @@ class Data: def __init__(self, hass: HomeAssistant) -> None: """Initialize the user data store.""" self.hass = hass - self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY, + private=True) self._data = None # type: Optional[Dict[str, Any]] async def async_load(self) -> None: diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index bf1577cbf01..e2701ee37f1 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -59,61 +59,9 @@ def is_on(hass, entity_id=None): return False -def turn_on(hass, entity_id=None, **service_data): - """Turn specified entity on if possible.""" - if entity_id is not None: - service_data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(ha.DOMAIN, SERVICE_TURN_ON, service_data) - - -def turn_off(hass, entity_id=None, **service_data): - """Turn specified entity off.""" - if entity_id is not None: - service_data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(ha.DOMAIN, SERVICE_TURN_OFF, service_data) - - -def toggle(hass, entity_id=None, **service_data): - """Toggle specified entity.""" - if entity_id is not None: - service_data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(ha.DOMAIN, SERVICE_TOGGLE, service_data) - - -def stop(hass): - """Stop Home Assistant.""" - hass.services.call(ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP) - - -def restart(hass): - """Stop Home Assistant.""" - hass.services.call(ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART) - - -def check_config(hass): - """Check the config files.""" - hass.services.call(ha.DOMAIN, SERVICE_CHECK_CONFIG) - - -def reload_core_config(hass): - """Reload the core config.""" - hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG) - - -@asyncio.coroutine -def async_reload_core_config(hass): - """Reload the core config.""" - yield from hass.services.async_call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG) - - -@asyncio.coroutine -def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: +async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: """Set up general services related to Home Assistant.""" - @asyncio.coroutine - def async_handle_turn_service(service): + async def async_handle_turn_service(service): """Handle calls to homeassistant.turn_on/off.""" entity_ids = extract_entity_ids(hass, service) @@ -148,7 +96,7 @@ def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: tasks.append(hass.services.async_call( domain, service.service, data, blocking)) - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service) @@ -164,15 +112,14 @@ def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: hass.helpers.intent.async_register(intent.ServiceIntentHandler( intent.INTENT_TOGGLE, ha.DOMAIN, SERVICE_TOGGLE, "Toggled {}")) - @asyncio.coroutine - def async_handle_core_service(call): + async def async_handle_core_service(call): """Service handler for handling core services.""" if call.service == SERVICE_HOMEASSISTANT_STOP: hass.async_create_task(hass.async_stop()) return try: - errors = yield from conf_util.async_check_ha_config_file(hass) + errors = await conf_util.async_check_ha_config_file(hass) except HomeAssistantError: return @@ -193,16 +140,15 @@ def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: hass.services.async_register( ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service) - @asyncio.coroutine - def async_handle_reload_config(call): + async def async_handle_reload_config(call): """Service handler for reloading core config.""" try: - conf = yield from conf_util.async_hass_config_yaml(hass) + conf = await conf_util.async_hass_config_yaml(hass) except HomeAssistantError as err: _LOGGER.error(err) return - yield from conf_util.async_process_ha_core_config( + await conf_util.async_process_ha_core_config( hass, conf.get(ha.DOMAIN) or {}) hass.services.async_register( diff --git a/homeassistant/components/abode.py b/homeassistant/components/abode.py index bafbc0781ca..64bedb4ac7c 100644 --- a/homeassistant/components/abode.py +++ b/homeassistant/components/abode.py @@ -4,7 +4,6 @@ This component provides basic support for Abode Home Security system. For more details about this component, please refer to the documentation at https://home-assistant.io/components/abode/ """ -import asyncio import logging from functools import partial from requests.exceptions import HTTPError, ConnectTimeout @@ -261,8 +260,7 @@ class AbodeDevice(Entity): self._data = data self._device = device - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe Abode events.""" self.hass.async_add_job( self._data.abode.events.add_device_callback, @@ -308,8 +306,7 @@ class AbodeAutomation(Entity): self._automation = automation self._event = event - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe Abode events.""" if self._event: self.hass.async_add_job( diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 63977ed88c7..a42e6e880b5 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -4,7 +4,6 @@ Component to interface with an alarm control panel. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel/ """ -import asyncio from datetime import timedelta import logging @@ -14,7 +13,6 @@ from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER, SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS) -from homeassistant.loader import bind_hass from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -32,85 +30,12 @@ ALARM_SERVICE_SCHEMA = vol.Schema({ }) -@bind_hass -def alarm_disarm(hass, code=None, entity_id=None): - """Send the alarm the command for disarm.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_ALARM_DISARM, data) - - -@bind_hass -def alarm_arm_home(hass, code=None, entity_id=None): - """Send the alarm the command for arm home.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_ALARM_ARM_HOME, data) - - -@bind_hass -def alarm_arm_away(hass, code=None, entity_id=None): - """Send the alarm the command for arm away.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data) - - -@bind_hass -def alarm_arm_night(hass, code=None, entity_id=None): - """Send the alarm the command for arm night.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_ALARM_ARM_NIGHT, data) - - -@bind_hass -def alarm_trigger(hass, code=None, entity_id=None): - """Send the alarm the command for trigger.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data) - - -@bind_hass -def alarm_arm_custom_bypass(hass, code=None, entity_id=None): - """Send the alarm the command for arm custom bypass.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_ALARM_ARM_CUSTOM_BYPASS, data) - - -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Track states and offer events for sensors.""" component = hass.data[DOMAIN] = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) - yield from component.async_setup(config) + await component.async_setup(config) component.async_register_entity_service( SERVICE_ALARM_DISARM, ALARM_SERVICE_SCHEMA, diff --git a/homeassistant/components/alarm_control_panel/alarmdecoder.py b/homeassistant/components/alarm_control_panel/alarmdecoder.py index 5606209d1e6..25496dff0eb 100644 --- a/homeassistant/components/alarm_control_panel/alarmdecoder.py +++ b/homeassistant/components/alarm_control_panel/alarmdecoder.py @@ -4,7 +4,6 @@ Support for AlarmDecoder-based alarm control panels (Honeywell/DSC). For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.alarmdecoder/ """ -import asyncio import logging import voluptuous as vol @@ -59,8 +58,7 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): self._ready = None self._zone_bypassed = None - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( SIGNAL_PANEL_MESSAGE, self._message_callback) diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py index 98766deb3b6..9b07dc41690 100644 --- a/homeassistant/components/alarm_control_panel/alarmdotcom.py +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -4,7 +4,6 @@ Interfaces with Alarm.com alarm control panels. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.alarmdotcom/ """ -import asyncio import logging import re @@ -32,9 +31,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up a Alarm.com control panel.""" name = config.get(CONF_NAME) code = config.get(CONF_CODE) @@ -42,7 +40,7 @@ def async_setup_platform(hass, config, async_add_entities, password = config.get(CONF_PASSWORD) alarmdotcom = AlarmDotCom(hass, name, code, username, password) - yield from alarmdotcom.async_login() + await alarmdotcom.async_login() async_add_entities([alarmdotcom]) @@ -63,15 +61,13 @@ class AlarmDotCom(alarm.AlarmControlPanel): self._alarm = Alarmdotcom( username, password, self._websession, hass.loop) - @asyncio.coroutine - def async_login(self): + async def async_login(self): """Login to Alarm.com.""" - yield from self._alarm.async_login() + await self._alarm.async_login() - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Fetch the latest state.""" - yield from self._alarm.async_update() + await self._alarm.async_update() return self._alarm.state @property @@ -106,23 +102,20 @@ class AlarmDotCom(alarm.AlarmControlPanel): 'sensor_status': self._alarm.sensor_status } - @asyncio.coroutine - def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code=None): """Send disarm command.""" if self._validate_code(code): - yield from self._alarm.async_alarm_disarm() + await self._alarm.async_alarm_disarm() - @asyncio.coroutine - def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code=None): """Send arm hom command.""" if self._validate_code(code): - yield from self._alarm.async_alarm_arm_home() + await self._alarm.async_alarm_arm_home() - @asyncio.coroutine - def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code=None): """Send arm away command.""" if self._validate_code(code): - yield from self._alarm.async_alarm_arm_away() + await self._alarm.async_alarm_arm_away() def _validate_code(self, code): """Validate given code.""" diff --git a/homeassistant/components/alarm_control_panel/blink.py b/homeassistant/components/alarm_control_panel/blink.py new file mode 100644 index 00000000000..850ac52fda4 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/blink.py @@ -0,0 +1,86 @@ +""" +Support for Blink Alarm Control Panel. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/alarm_control_panel.blink/ +""" +import logging + +from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.blink import ( + BLINK_DATA, DEFAULT_ATTRIBUTION) +from homeassistant.const import ( + ATTR_ATTRIBUTION, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['blink'] + +ICON = 'mdi:security' + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Arlo Alarm Control Panels.""" + if discovery_info is None: + return + data = hass.data[BLINK_DATA] + + # Current version of blinkpy API only supports one sync module. When + # support for additional models is added, the sync module name should + # come from the API. + sync_modules = [] + sync_modules.append(BlinkSyncModule(data, 'sync')) + add_entities(sync_modules, True) + + +class BlinkSyncModule(AlarmControlPanel): + """Representation of a Blink Alarm Control Panel.""" + + def __init__(self, data, name): + """Initialize the alarm control panel.""" + self.data = data + self.sync = data.sync + self._name = name + self._state = None + + @property + def icon(self): + """Return icon.""" + return ICON + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def name(self): + """Return the name of the panel.""" + return "{} {}".format(BLINK_DATA, self._name) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION, + } + + def update(self): + """Update the state of the device.""" + _LOGGER.debug("Updating Blink Alarm Control Panel %s", self._name) + self.data.refresh() + mode = self.sync.arm + if mode: + self._state = STATE_ALARM_ARMED_AWAY + else: + self._state = STATE_ALARM_DISARMED + + def alarm_disarm(self, code=None): + """Send disarm command.""" + self.sync.arm = False + self.sync.refresh() + + def alarm_arm_away(self, code=None): + """Send arm command.""" + self.sync.arm = True + self.sync.refresh() diff --git a/homeassistant/components/alarm_control_panel/egardia.py b/homeassistant/components/alarm_control_panel/egardia.py index 4e278c10e07..dfd60c4abde 100644 --- a/homeassistant/components/alarm_control_panel/egardia.py +++ b/homeassistant/components/alarm_control_panel/egardia.py @@ -4,7 +4,6 @@ Interfaces with Egardia/Woonveilig alarm control panel. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.egardia/ """ -import asyncio import logging import requests @@ -61,8 +60,7 @@ class EgardiaAlarm(alarm.AlarmControlPanel): self._rs_codes = rs_codes self._rs_port = rs_port - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Add Egardiaserver callback if enabled.""" if self._rs_enabled: _LOGGER.debug("Registering callback to Egardiaserver") diff --git a/homeassistant/components/alarm_control_panel/envisalink.py b/homeassistant/components/alarm_control_panel/envisalink.py index df91884b32c..f0f3d2a43f7 100644 --- a/homeassistant/components/alarm_control_panel/envisalink.py +++ b/homeassistant/components/alarm_control_panel/envisalink.py @@ -4,7 +4,6 @@ Support for Envisalink-based alarm control panels (Honeywell/DSC). For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.envisalink/ """ -import asyncio import logging import voluptuous as vol @@ -32,9 +31,8 @@ ALARM_KEYPRESS_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Perform the setup for Envisalink alarm panels.""" configured_partitions = discovery_info['partitions'] code = discovery_info[CONF_CODE] @@ -88,8 +86,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): _LOGGER.debug("Setting up alarm: %s", alarm_name) super().__init__(alarm_name, info, controller) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" async_dispatcher_connect( self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback) @@ -128,8 +125,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): state = STATE_ALARM_DISARMED return state - @asyncio.coroutine - def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code=None): """Send disarm command.""" if code: self.hass.data[DATA_EVL].disarm_partition( @@ -138,8 +134,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): self.hass.data[DATA_EVL].disarm_partition( str(self._code), self._partition_number) - @asyncio.coroutine - def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code=None): """Send arm home command.""" if code: self.hass.data[DATA_EVL].arm_stay_partition( @@ -148,8 +143,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): self.hass.data[DATA_EVL].arm_stay_partition( str(self._code), self._partition_number) - @asyncio.coroutine - def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code=None): """Send arm away command.""" if code: self.hass.data[DATA_EVL].arm_away_partition( @@ -158,8 +152,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): self.hass.data[DATA_EVL].arm_away_partition( str(self._code), self._partition_number) - @asyncio.coroutine - def async_alarm_trigger(self, code=None): + async def async_alarm_trigger(self, code=None): """Alarm trigger command. Will be used to trigger a panic alarm.""" self.hass.data[DATA_EVL].panic_alarm(self._panic_type) diff --git a/homeassistant/components/alarm_control_panel/manual_mqtt.py b/homeassistant/components/alarm_control_panel/manual_mqtt.py index 7bf9443424c..834a502baa0 100644 --- a/homeassistant/components/alarm_control_panel/manual_mqtt.py +++ b/homeassistant/components/alarm_control_panel/manual_mqtt.py @@ -4,7 +4,6 @@ Support for manual alarms controllable via MQTT. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.manual_mqtt/ """ -import asyncio import copy import datetime import logging @@ -363,8 +362,8 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel): return mqtt.async_subscribe( self.hass, self._command_topic, message_received, self._qos) - @asyncio.coroutine - def _async_state_changed_listener(self, entity_id, old_state, new_state): + async def _async_state_changed_listener(self, entity_id, old_state, + new_state): """Publish state change to MQTT.""" mqtt.async_publish( self.hass, self._state_topic, new_state.state, self._qos, True) diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index e5ad54c4147..ad1c0d1e3b8 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -4,7 +4,6 @@ This platform enables the possibility to control a MQTT alarm. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.mqtt/ """ -import asyncio import logging import re @@ -18,10 +17,13 @@ from homeassistant.const import ( STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN, CONF_NAME, CONF_CODE) from homeassistant.components.mqtt import ( - CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, - CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, - CONF_RETAIN, MqttAvailability) + ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, + CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, + CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate) +from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType, ConfigType _LOGGER = logging.getLogger(__name__) @@ -46,13 +48,28 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the MQTT Alarm Control Panel platform.""" - if discovery_info is not None: - config = PLATFORM_SCHEMA(discovery_info) +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_entities, discovery_info=None): + """Set up MQTT alarm control panel through configuration.yaml.""" + await _async_setup_entity(hass, config, async_add_entities) + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT alarm control panel dynamically through MQTT discovery.""" + async def async_discover(discovery_payload): + """Discover and add an MQTT alarm control panel.""" + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(hass, config, async_add_entities, + discovery_payload[ATTR_DISCOVERY_HASH]) + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(alarm.DOMAIN, 'mqtt'), + async_discover) + + +async def _async_setup_entity(hass, config, async_add_entities, + discovery_hash=None): + """Set up the MQTT Alarm Control Panel platform.""" async_add_entities([MqttAlarm( config.get(CONF_NAME), config.get(CONF_STATE_TOPIC), @@ -65,18 +82,22 @@ def async_setup_platform(hass, config, async_add_entities, config.get(CONF_CODE), config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_PAYLOAD_AVAILABLE), - config.get(CONF_PAYLOAD_NOT_AVAILABLE))]) + config.get(CONF_PAYLOAD_NOT_AVAILABLE), + discovery_hash,)]) -class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel): +class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate, + alarm.AlarmControlPanel): """Representation of a MQTT alarm status.""" def __init__(self, name, state_topic, command_topic, qos, retain, payload_disarm, payload_arm_home, payload_arm_away, code, - availability_topic, payload_available, payload_not_available): + availability_topic, payload_available, payload_not_available, + discovery_hash): """Init the MQTT Alarm Control Panel.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_hash) self._state = STATE_UNKNOWN self._name = name self._state_topic = state_topic @@ -87,11 +108,12 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel): self._payload_arm_home = payload_arm_home self._payload_arm_away = payload_arm_away self._code = code + self._discovery_hash = discovery_hash - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe mqtt events.""" - yield from super().async_added_to_hass() + await MqttAvailability.async_added_to_hass(self) + await MqttDiscoveryUpdate.async_added_to_hass(self) @callback def message_received(topic, payload, qos): @@ -104,7 +126,7 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel): self._state = payload self.async_schedule_update_ha_state() - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._state_topic, message_received, self._qos) @property @@ -131,8 +153,7 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel): return 'Number' return 'Any' - @asyncio.coroutine - def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code=None): """Send disarm command. This method is a coroutine. @@ -143,8 +164,7 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel): self.hass, self._command_topic, self._payload_disarm, self._qos, self._retain) - @asyncio.coroutine - def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code=None): """Send arm home command. This method is a coroutine. @@ -155,8 +175,7 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel): self.hass, self._command_topic, self._payload_arm_home, self._qos, self._retain) - @asyncio.coroutine - def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code=None): """Send arm away command. This method is a coroutine. diff --git a/homeassistant/components/alarm_control_panel/satel_integra.py b/homeassistant/components/alarm_control_panel/satel_integra.py index 86603763396..c4e42855d8a 100644 --- a/homeassistant/components/alarm_control_panel/satel_integra.py +++ b/homeassistant/components/alarm_control_panel/satel_integra.py @@ -4,7 +4,6 @@ Support for Satel Integra alarm, using ETHM module: https://www.satel.pl/en/ . For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.satel_integra/ """ -import asyncio import logging import homeassistant.components.alarm_control_panel as alarm @@ -18,9 +17,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['satel_integra'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up for Satel Integra alarm panels.""" if not discovery_info: return @@ -39,8 +37,7 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel): self._state = None self._arm_home_mode = arm_home_mode - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" async_dispatcher_connect( self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback) @@ -74,21 +71,18 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel): """Return the state of the device.""" return self._state - @asyncio.coroutine - def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code=None): """Send disarm command.""" if code: - yield from self.hass.data[DATA_SATEL].disarm(code) + await self.hass.data[DATA_SATEL].disarm(code) - @asyncio.coroutine - def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code=None): """Send arm away command.""" if code: - yield from self.hass.data[DATA_SATEL].arm(code) + await self.hass.data[DATA_SATEL].arm(code) - @asyncio.coroutine - def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code=None): """Send arm home command.""" if code: - yield from self.hass.data[DATA_SATEL].arm( + await self.hass.data[DATA_SATEL].arm( code, self._arm_home_mode) diff --git a/homeassistant/components/alarm_control_panel/simplisafe.py b/homeassistant/components/alarm_control_panel/simplisafe.py index 2c3b25330d9..34c68f26c2a 100644 --- a/homeassistant/components/alarm_control_panel/simplisafe.py +++ b/homeassistant/components/alarm_control_panel/simplisafe.py @@ -12,75 +12,100 @@ import voluptuous as vol from homeassistant.components.alarm_control_panel import ( PLATFORM_SCHEMA, AlarmControlPanel) from homeassistant.const import ( - CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_DISARMED, STATE_UNKNOWN) -import homeassistant.helpers.config_validation as cv + CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED) +from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['simplisafe-python==2.0.2'] +REQUIREMENTS = ['simplisafe-python==3.1.2'] _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'SimpliSafe' - ATTR_ALARM_ACTIVE = "alarm_active" ATTR_TEMPERATURE = "temperature" +DATA_FILE = '.simplisafe' + +DEFAULT_NAME = 'SimpliSafe' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_CODE): cv.string, + vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_CODE): cv.string, }) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the SimpliSafe platform.""" - from simplipy.api import SimpliSafeApiInterface, SimpliSafeAPIException + from simplipy import API + from simplipy.errors import SimplipyError + + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] name = config.get(CONF_NAME) code = config.get(CONF_CODE) - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) + + websession = aiohttp_client.async_get_clientsession(hass) + + config_data = await hass.async_add_executor_job( + load_json, hass.config.path(DATA_FILE)) try: - simplisafe = SimpliSafeApiInterface(username, password) - except SimpliSafeAPIException: - _LOGGER.error("Failed to set up SimpliSafe") + if config_data: + try: + simplisafe = await API.login_via_token( + config_data['refresh_token'], websession) + _LOGGER.debug('Logging in with refresh token') + except SimplipyError: + _LOGGER.info('Refresh token expired; attempting credentials') + simplisafe = await API.login_via_credentials( + username, password, websession) + else: + simplisafe = await API.login_via_credentials( + username, password, websession) + _LOGGER.debug('Logging in with credentials') + except SimplipyError as err: + _LOGGER.error("There was an error during setup: %s", err) return - systems = [] + config_data = {'refresh_token': simplisafe.refresh_token} + await hass.async_add_executor_job( + save_json, hass.config.path(DATA_FILE), config_data) - for system in simplisafe.get_systems(): - systems.append(SimpliSafeAlarm(system, name, code)) - - add_entities(systems) + systems = await simplisafe.get_systems() + async_add_entities( + [SimpliSafeAlarm(system, name, code) for system in systems], True) class SimpliSafeAlarm(AlarmControlPanel): """Representation of a SimpliSafe alarm.""" - def __init__(self, simplisafe, name, code): + def __init__(self, system, name, code): """Initialize the SimpliSafe alarm.""" - self.simplisafe = simplisafe - self._name = name + self._attrs = {} self._code = str(code) if code else None + self._name = name + self._system = system + self._state = None @property def unique_id(self): """Return the unique ID.""" - return self.simplisafe.location_id + return self._system.system_id @property def name(self): """Return the name of the device.""" - if self._name is not None: + if self._name: return self._name - return 'Alarm {}'.format(self.simplisafe.location_id) + return 'Alarm {}'.format(self._system.system_id) @property def code_format(self): """Return one or more digits/characters.""" - if self._code is None: + if not self._code: return None if isinstance(self._code, str) and re.search('^\\d+$', self._code): return 'Number' @@ -89,53 +114,12 @@ class SimpliSafeAlarm(AlarmControlPanel): @property def state(self): """Return the state of the device.""" - status = self.simplisafe.state - if status.lower() == 'off': - state = STATE_ALARM_DISARMED - elif status.lower() == 'home' or status.lower() == 'home_count': - state = STATE_ALARM_ARMED_HOME - elif (status.lower() == 'away' or status.lower() == 'exitDelay' or - status.lower() == 'away_count'): - state = STATE_ALARM_ARMED_AWAY - else: - state = STATE_UNKNOWN - return state + return self._state @property def device_state_attributes(self): """Return the state attributes.""" - attributes = {} - - attributes[ATTR_ALARM_ACTIVE] = self.simplisafe.alarm_active - if self.simplisafe.temperature is not None: - attributes[ATTR_TEMPERATURE] = self.simplisafe.temperature - - return attributes - - def update(self): - """Update alarm status.""" - self.simplisafe.update() - - def alarm_disarm(self, code=None): - """Send disarm command.""" - if not self._validate_code(code, 'disarming'): - return - self.simplisafe.set_state('off') - _LOGGER.info("SimpliSafe alarm disarming") - - def alarm_arm_home(self, code=None): - """Send arm home command.""" - if not self._validate_code(code, 'arming home'): - return - self.simplisafe.set_state('home') - _LOGGER.info("SimpliSafe alarm arming home") - - def alarm_arm_away(self, code=None): - """Send arm away command.""" - if not self._validate_code(code, 'arming away'): - return - self.simplisafe.set_state('away') - _LOGGER.info("SimpliSafe alarm arming away") + return self._attrs def _validate_code(self, code, state): """Validate given code.""" @@ -143,3 +127,46 @@ class SimpliSafeAlarm(AlarmControlPanel): if not check: _LOGGER.warning("Wrong code entered for %s", state) return check + + async def async_alarm_disarm(self, code=None): + """Send disarm command.""" + if not self._validate_code(code, 'disarming'): + return + + await self._system.set_off() + + async def async_alarm_arm_home(self, code=None): + """Send arm home command.""" + if not self._validate_code(code, 'arming home'): + return + + await self._system.set_home() + + async def async_alarm_arm_away(self, code=None): + """Send arm away command.""" + if not self._validate_code(code, 'arming away'): + return + + await self._system.set_away() + + async def async_update(self): + """Update alarm status.""" + await self._system.update() + + if self._system.state == self._system.SystemStates.off: + self._state = STATE_ALARM_DISARMED + elif self._system.state in ( + self._system.SystemStates.home, + self._system.SystemStates.home_count): + self._state = STATE_ALARM_ARMED_HOME + elif self._system.state in ( + self._system.SystemStates.away, + self._system.SystemStates.away_count, + self._system.SystemStates.exit_delay): + self._state = STATE_ALARM_ARMED_AWAY + else: + self._state = None + + self._attrs[ATTR_ALARM_ACTIVE] = self._system.alarm_going_off + if self._system.temperature: + self._attrs[ATTR_TEMPERATURE] = self._system.temperature diff --git a/homeassistant/components/alarm_control_panel/spc.py b/homeassistant/components/alarm_control_panel/spc.py index 9150518022f..b4c49d4d190 100644 --- a/homeassistant/components/alarm_control_panel/spc.py +++ b/homeassistant/components/alarm_control_panel/spc.py @@ -9,8 +9,7 @@ import logging import homeassistant.components.alarm_control_panel as alarm from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.core import callback -from homeassistant.components.spc import ( - ATTR_DISCOVER_AREAS, DATA_API, SIGNAL_UPDATE_ALARM) +from homeassistant.components.spc import (DATA_API, SIGNAL_UPDATE_ALARM) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED) @@ -37,12 +36,11 @@ def _get_alarm_state(area): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the SPC alarm control panel platform.""" - if (discovery_info is None or - discovery_info[ATTR_DISCOVER_AREAS] is None): + if discovery_info is None: return - - async_add_entities([SpcAlarm(area=area, api=hass.data[DATA_API]) - for area in discovery_info[ATTR_DISCOVER_AREAS]]) + api = hass.data[DATA_API] + async_add_entities([SpcAlarm(area=area, api=api) + for area in api.areas.values()]) class SpcAlarm(alarm.AlarmControlPanel): diff --git a/homeassistant/components/alarm_control_panel/wink.py b/homeassistant/components/alarm_control_panel/wink.py index d75fad30c96..001c6fad85c 100644 --- a/homeassistant/components/alarm_control_panel/wink.py +++ b/homeassistant/components/alarm_control_panel/wink.py @@ -4,7 +4,6 @@ Interfaces with Wink Cameras. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.wink/ """ -import asyncio import logging import homeassistant.components.alarm_control_panel as alarm @@ -38,8 +37,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel): """Representation a Wink camera alarm.""" - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['alarm_control_panel'].append(self) diff --git a/homeassistant/components/alert.py b/homeassistant/components/alert.py index 3ec01fc6ab8..e224351f9db 100644 --- a/homeassistant/components/alert.py +++ b/homeassistant/components/alert.py @@ -10,7 +10,8 @@ import logging import voluptuous as vol -from homeassistant.core import callback +from homeassistant.components.notify import ( + ATTR_MESSAGE, DOMAIN as DOMAIN_NOTIFY) from homeassistant.const import ( CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID) @@ -59,53 +60,12 @@ def is_on(hass, entity_id): return hass.states.is_state(entity_id, STATE_ON) -def turn_on(hass, entity_id): - """Reset the alert.""" - hass.add_job(async_turn_on, hass, entity_id) - - -@callback -def async_turn_on(hass, entity_id): - """Async reset the alert.""" - data = {ATTR_ENTITY_ID: entity_id} - hass.async_create_task( - hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)) - - -def turn_off(hass, entity_id): - """Acknowledge alert.""" - hass.add_job(async_turn_off, hass, entity_id) - - -@callback -def async_turn_off(hass, entity_id): - """Async acknowledge the alert.""" - data = {ATTR_ENTITY_ID: entity_id} - hass.async_create_task( - hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data)) - - -def toggle(hass, entity_id): - """Toggle acknowledgement of alert.""" - hass.add_job(async_toggle, hass, entity_id) - - -@callback -def async_toggle(hass, entity_id): - """Async toggle acknowledgement of alert.""" - data = {ATTR_ENTITY_ID: entity_id} - hass.async_create_task( - hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data)) - - -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the Alert component.""" alerts = config.get(DOMAIN) all_alerts = {} - @asyncio.coroutine - def async_handle_alert_service(service_call): + async def async_handle_alert_service(service_call): """Handle calls to alert services.""" alert_ids = service.extract_entity_ids(hass, service_call) @@ -113,11 +73,11 @@ def async_setup(hass, config): alert = all_alerts[alert_id] alert.async_set_context(service_call.context) if service_call.service == SERVICE_TURN_ON: - yield from alert.async_turn_on() + await alert.async_turn_on() elif service_call.service == SERVICE_TOGGLE: - yield from alert.async_toggle() + await alert.async_toggle() else: - yield from alert.async_turn_off() + await alert.async_turn_off() # Setup alerts for entity_id, alert in alerts.items(): @@ -141,7 +101,7 @@ def async_setup(hass, config): tasks = [alert.async_update_ha_state() for alert in all_alerts.values()] if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) return True @@ -196,17 +156,15 @@ class Alert(ToggleEntity): """Hide the alert when it is not firing.""" return not self._can_ack or not self._firing - @asyncio.coroutine - def watched_entity_change(self, entity, from_state, to_state): + async def watched_entity_change(self, entity, from_state, to_state): """Determine if the alert should start or stop.""" _LOGGER.debug("Watched entity (%s) has changed", entity) if to_state.state == self._alert_state and not self._firing: - yield from self.begin_alerting() + await self.begin_alerting() if to_state.state != self._alert_state and self._firing: - yield from self.end_alerting() + await self.end_alerting() - @asyncio.coroutine - def begin_alerting(self): + async def begin_alerting(self): """Begin the alert procedures.""" _LOGGER.debug("Beginning Alert: %s", self._name) self._ack = False @@ -214,25 +172,23 @@ class Alert(ToggleEntity): self._next_delay = 0 if not self._skip_first: - yield from self._notify() + await self._notify() else: - yield from self._schedule_notify() + await self._schedule_notify() self.async_schedule_update_ha_state() - @asyncio.coroutine - def end_alerting(self): + async def end_alerting(self): """End the alert procedures.""" _LOGGER.debug("Ending Alert: %s", self._name) self._cancel() self._ack = False self._firing = False if self._done_message and self._send_done_message: - yield from self._notify_done_message() + await self._notify_done_message() self.async_schedule_update_ha_state() - @asyncio.coroutine - def _schedule_notify(self): + async def _schedule_notify(self): """Schedule a notification.""" delay = self._delay[self._next_delay] next_msg = datetime.now() + delay @@ -240,8 +196,7 @@ class Alert(ToggleEntity): event.async_track_point_in_time(self.hass, self._notify, next_msg) self._next_delay = min(self._next_delay + 1, len(self._delay) - 1) - @asyncio.coroutine - def _notify(self, *args): + async def _notify(self, *args): """Send the alert notification.""" if not self._firing: return @@ -250,36 +205,32 @@ class Alert(ToggleEntity): _LOGGER.info("Alerting: %s", self._name) self._send_done_message = True for target in self._notifiers: - yield from self.hass.services.async_call( - 'notify', target, {'message': self._name}) - yield from self._schedule_notify() + await self.hass.services.async_call( + DOMAIN_NOTIFY, target, {ATTR_MESSAGE: self._name}) + await self._schedule_notify() - @asyncio.coroutine - def _notify_done_message(self, *args): + async def _notify_done_message(self, *args): """Send notification of complete alert.""" _LOGGER.info("Alerting: %s", self._done_message) self._send_done_message = False for target in self._notifiers: - yield from self.hass.services.async_call( - 'notify', target, {'message': self._done_message}) + await self.hass.services.async_call( + DOMAIN_NOTIFY, target, {ATTR_MESSAGE: self._done_message}) - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Async Unacknowledge alert.""" _LOGGER.debug("Reset Alert: %s", self._name) self._ack = False - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Async Acknowledge alert.""" _LOGGER.debug("Acknowledged Alert: %s", self._name) self._ack = True - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_toggle(self, **kwargs): + async def async_toggle(self, **kwargs): """Async toggle alert.""" if self._ack: - return self.async_turn_on() - return self.async_turn_off() + return await self.async_turn_on() + return await self.async_turn_off() diff --git a/homeassistant/components/alexa/__init__.py b/homeassistant/components/alexa/__init__.py index d120270650f..337d8993b28 100644 --- a/homeassistant/components/alexa/__init__.py +++ b/homeassistant/components/alexa/__init__.py @@ -4,7 +4,6 @@ Support for Alexa skill service end point. For more details about this component, please refer to the documentation at https://home-assistant.io/components/alexa/ """ -import asyncio import logging import voluptuous as vol @@ -53,8 +52,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Activate Alexa component.""" config = config.get(DOMAIN, {}) flash_briefings_config = config.get(CONF_FLASH_BRIEFINGS) diff --git a/homeassistant/components/alexa/intent.py b/homeassistant/components/alexa/intent.py index 8d4520d74e8..85cb4f105cd 100644 --- a/homeassistant/components/alexa/intent.py +++ b/homeassistant/components/alexa/intent.py @@ -4,7 +4,6 @@ Support for Alexa skill service end point. For more details about this component, please refer to the documentation at https://home-assistant.io/components/alexa/ """ -import asyncio import enum import logging @@ -59,16 +58,15 @@ class AlexaIntentsView(http.HomeAssistantView): url = INTENTS_API_ENDPOINT name = 'api:alexa' - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Handle Alexa.""" hass = request.app['hass'] - message = yield from request.json() + message = await request.json() _LOGGER.debug("Received Alexa request: %s", message) try: - response = yield from async_handle_message(hass, message) + response = await async_handle_message(hass, message) return b'' if response is None else self.json(response) except UnknownRequest as err: _LOGGER.warning(str(err)) @@ -101,8 +99,7 @@ def intent_error_response(hass, message, error): return alexa_response.as_dict() -@asyncio.coroutine -def async_handle_message(hass, message): +async def async_handle_message(hass, message): """Handle an Alexa intent. Raises: @@ -120,20 +117,18 @@ def async_handle_message(hass, message): if not handler: raise UnknownRequest('Received unknown request {}'.format(req_type)) - return (yield from handler(hass, message)) + return await handler(hass, message) @HANDLERS.register('SessionEndedRequest') -@asyncio.coroutine -def async_handle_session_end(hass, message): +async def async_handle_session_end(hass, message): """Handle a session end request.""" return None @HANDLERS.register('IntentRequest') @HANDLERS.register('LaunchRequest') -@asyncio.coroutine -def async_handle_intent(hass, message): +async def async_handle_intent(hass, message): """Handle an intent request. Raises: @@ -153,7 +148,7 @@ def async_handle_intent(hass, message): else: intent_name = alexa_intent_info['name'] - intent_response = yield from intent.async_handle( + intent_response = await intent.async_handle( hass, DOMAIN, intent_name, {key: {'value': value} for key, value in alexa_response.variables.items()}) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 176c286ebc3..f88d81ab851 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -1,5 +1,4 @@ """Support for alexa Smart Home Skill API.""" -import asyncio import logging import math from datetime import datetime @@ -695,8 +694,7 @@ class SmartHomeView(http.HomeAssistantView): """Initialize.""" self.smart_home_config = smart_home_config - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Handle Alexa Smart Home requests. The Smart Home API requires the endpoint to be implemented in AWS @@ -704,11 +702,11 @@ class SmartHomeView(http.HomeAssistantView): the response. """ hass = request.app['hass'] - message = yield from request.json() + message = await request.json() _LOGGER.debug("Received Alexa Smart Home request: %s", message) - response = yield from async_handle_message( + response = await async_handle_message( hass, self.smart_home_config, message) _LOGGER.debug("Sending Alexa Smart Home response: %s", response) return b'' if response is None else self.json(response) diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py index 5da117e74c3..b8a2d461489 100644 --- a/homeassistant/components/android_ip_webcam.py +++ b/homeassistant/components/android_ip_webcam.py @@ -149,16 +149,14 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the IP Webcam component.""" from pydroid_ipcam import PyDroidIPCam webcams = hass.data[DATA_IP_WEBCAM] = {} websession = async_get_clientsession(hass) - @asyncio.coroutine - def async_setup_ipcamera(cam_config): + async def async_setup_ipcamera(cam_config): """Set up an IP camera.""" host = cam_config[CONF_HOST] username = cam_config.get(CONF_USERNAME) @@ -188,16 +186,15 @@ def async_setup(hass, config): if motion is None: motion = 'motion_active' in cam.enabled_sensors - @asyncio.coroutine - def async_update_data(now): + async def async_update_data(now): """Update data from IP camera in SCAN_INTERVAL.""" - yield from cam.update() + await cam.update() async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host) async_track_point_in_utc_time( hass, async_update_data, utcnow() + interval) - yield from async_update_data(None) + await async_update_data(None) # Load platforms webcams[host] = cam @@ -242,7 +239,7 @@ def async_setup(hass, config): tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]] if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) return True @@ -255,8 +252,7 @@ class AndroidIPCamEntity(Entity): self._host = host self._ipcam = ipcam - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register update dispatcher.""" @callback def async_ipcam_update(host): diff --git a/homeassistant/components/apple_tv.py b/homeassistant/components/apple_tv.py index 21ff0e3286d..012e71a08a7 100644 --- a/homeassistant/components/apple_tv.py +++ b/homeassistant/components/apple_tv.py @@ -77,14 +77,13 @@ def request_configuration(hass, config, atv, credentials): """Request configuration steps from the user.""" configurator = hass.components.configurator - @asyncio.coroutine - def configuration_callback(callback_data): + async def configuration_callback(callback_data): """Handle the submitted configuration.""" from pyatv import exceptions pin = callback_data.get('pin') try: - yield from atv.airplay.finish_authentication(pin) + await atv.airplay.finish_authentication(pin) hass.components.persistent_notification.async_create( 'Authentication succeeded!

Add the following ' 'to credentials: in your apple_tv configuration:

' @@ -108,11 +107,10 @@ def request_configuration(hass, config, atv, credentials): ) -@asyncio.coroutine -def scan_for_apple_tvs(hass): +async def scan_for_apple_tvs(hass): """Scan for devices and present a notification of the ones found.""" import pyatv - atvs = yield from pyatv.scan_for_apple_tvs(hass.loop, timeout=3) + atvs = await pyatv.scan_for_apple_tvs(hass.loop, timeout=3) devices = [] for atv in atvs: @@ -132,14 +130,12 @@ def scan_for_apple_tvs(hass): notification_id=NOTIFICATION_SCAN_ID) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the Apple TV component.""" if DATA_APPLE_TV not in hass.data: hass.data[DATA_APPLE_TV] = {} - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Handle service calls.""" entity_ids = service.data.get(ATTR_ENTITY_ID) @@ -158,17 +154,16 @@ def async_setup(hass, config): continue atv = device.atv - credentials = yield from atv.airplay.generate_credentials() - yield from atv.airplay.load_credentials(credentials) + credentials = await atv.airplay.generate_credentials() + await atv.airplay.load_credentials(credentials) _LOGGER.debug('Generated new credentials: %s', credentials) - yield from atv.airplay.start_authentication() + await atv.airplay.start_authentication() hass.async_add_job(request_configuration, hass, config, atv, credentials) - @asyncio.coroutine - def atv_discovered(service, info): + async def atv_discovered(service, info): """Set up an Apple TV that was auto discovered.""" - yield from _setup_atv(hass, { + await _setup_atv(hass, { CONF_NAME: info['name'], CONF_HOST: info['host'], CONF_LOGIN_ID: info['properties']['hG'], @@ -179,7 +174,7 @@ def async_setup(hass, config): tasks = [_setup_atv(hass, conf) for conf in config.get(DOMAIN, [])] if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SCAN, async_service_handler, @@ -192,8 +187,7 @@ def async_setup(hass, config): return True -@asyncio.coroutine -def _setup_atv(hass, atv_config): +async def _setup_atv(hass, atv_config): """Set up an Apple TV.""" import pyatv name = atv_config.get(CONF_NAME) @@ -209,7 +203,7 @@ def _setup_atv(hass, atv_config): session = async_get_clientsession(hass) atv = pyatv.connect_to_apple_tv(details, hass.loop, session=session) if credentials: - yield from atv.airplay.load_credentials(credentials) + await atv.airplay.load_credentials(credentials) power = AppleTVPowerManager(hass, atv, start_off) hass.data[DATA_APPLE_TV][host] = { @@ -258,4 +252,4 @@ class AppleTVPowerManager: self.atv.push_updater.start() for listener in self.listeners: - self.hass.async_add_job(listener.async_update_ha_state()) + self.hass.async_create_task(listener.async_update_ha_state()) diff --git a/homeassistant/components/aqualogic.py b/homeassistant/components/aqualogic.py new file mode 100644 index 00000000000..abb61d42ca3 --- /dev/null +++ b/homeassistant/components/aqualogic.py @@ -0,0 +1,95 @@ +""" +Support for AquaLogic component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/aqualogic/ +""" +from datetime import timedelta +import logging +import time +import threading + +import voluptuous as vol + +from homeassistant.const import (CONF_HOST, CONF_PORT, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP) +from homeassistant.helpers import config_validation as cv + +REQUIREMENTS = ["aqualogic==1.0"] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "aqualogic" +UPDATE_TOPIC = DOMAIN + "_update" +CONF_UNIT = "unit" +RECONNECT_INTERVAL = timedelta(seconds=10) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT): cv.port + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up AquaLogic platform.""" + host = config[DOMAIN][CONF_HOST] + port = config[DOMAIN][CONF_PORT] + processor = AquaLogicProcessor(hass, host, port) + hass.data[DOMAIN] = processor + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, + processor.start_listen) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, + processor.shutdown) + _LOGGER.debug("AquaLogicProcessor %s:%i initialized", host, port) + return True + + +class AquaLogicProcessor(threading.Thread): + """AquaLogic event processor thread.""" + + def __init__(self, hass, host, port): + """Initialize the data object.""" + super().__init__(daemon=True) + self._hass = hass + self._host = host + self._port = port + self._shutdown = False + self._panel = None + + def start_listen(self, event): + """Start event-processing thread.""" + _LOGGER.debug("Event processing thread started") + self.start() + + def shutdown(self, event): + """Signal shutdown of processing event.""" + _LOGGER.debug("Event processing signaled exit") + self._shutdown = True + + def data_changed(self, panel): + """Aqualogic data changed callback.""" + self._hass.helpers.dispatcher.dispatcher_send(UPDATE_TOPIC) + + def run(self): + """Event thread.""" + from aqualogic.core import AquaLogic + + while True: + self._panel = AquaLogic() + self._panel.connect(self._host, self._port) + self._panel.process(self.data_changed) + + if self._shutdown: + return + + _LOGGER.error("Connection to %s:%d lost", + self._host, self._port) + time.sleep(RECONNECT_INTERVAL.seconds) + + @property + def panel(self): + """Retrieve the AquaLogic object.""" + return self._panel diff --git a/homeassistant/components/auth/.translations/de.json b/homeassistant/components/auth/.translations/de.json index 21c83290629..4ef4a5bf9e8 100644 --- a/homeassistant/components/auth/.translations/de.json +++ b/homeassistant/components/auth/.translations/de.json @@ -16,7 +16,8 @@ "description": "Ein Einmal-Passwort wurde per ** notify gesendet. {notify_service} **. Bitte gebe es unten ein:", "title": "\u00dcberpr\u00fcfe das Setup" } - } + }, + "title": "Benachrichtig f\u00fcr One-Time Password" }, "totp": { "error": { diff --git a/homeassistant/components/auth/.translations/fr.json b/homeassistant/components/auth/.translations/fr.json index 85540314af0..cf0a1888495 100644 --- a/homeassistant/components/auth/.translations/fr.json +++ b/homeassistant/components/auth/.translations/fr.json @@ -1,11 +1,23 @@ { "mfa_setup": { "notify": { + "abort": { + "no_available_service": "Aucun service de notification disponible." + }, + "error": { + "invalid_code": "Code invalide. Veuillez essayer \u00e0 nouveau." + }, "step": { + "init": { + "description": "Veuillez s\u00e9lectionner l'un des services de notification:", + "title": "Configurer un mot de passe \u00e0 usage unique d\u00e9livr\u00e9 par le composant notify" + }, "setup": { - "description": "Un mot de passe unique a \u00e9t\u00e9 envoy\u00e9 par **notify.{notify_service}**. Veuillez le saisir ci-dessous :" + "description": "Un mot de passe unique a \u00e9t\u00e9 envoy\u00e9 par **notify.{notify_service}**. Veuillez le saisir ci-dessous :", + "title": "V\u00e9rifier la configuration" } - } + }, + "title": "Notifier un mot de passe unique" }, "totp": { "error": { diff --git a/homeassistant/components/auth/.translations/hu.json b/homeassistant/components/auth/.translations/hu.json index 4500098553e..0a3a3c58820 100644 --- a/homeassistant/components/auth/.translations/hu.json +++ b/homeassistant/components/auth/.translations/hu.json @@ -1,5 +1,21 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Nincs el\u00e9rhet\u0151 \u00e9rtes\u00edt\u00e9si szolg\u00e1ltat\u00e1s." + }, + "error": { + "invalid_code": "\u00c9rv\u00e9nytelen k\u00f3d, pr\u00f3b\u00e1ld \u00fajra." + }, + "step": { + "init": { + "description": "V\u00e1lassz \u00e9rtes\u00edt\u00e9si szolg\u00e1ltat\u00e1st:" + }, + "setup": { + "title": "Be\u00e1ll\u00edt\u00e1s ellen\u0151rz\u00e9se" + } + } + }, "totp": { "error": { "invalid_code": "\u00c9rv\u00e9nytelen k\u00f3d, pr\u00f3b\u00e1ld \u00fajra. Ha ez a hiba folyamatosan el\u0151fordul, akkor gy\u0151z\u0151dj meg r\u00f3la, hogy a Home Assistant rendszered \u00f3r\u00e1ja pontosan j\u00e1r." diff --git a/homeassistant/components/auth/.translations/ko.json b/homeassistant/components/auth/.translations/ko.json index e1f26e88bc7..7efc50d534c 100644 --- a/homeassistant/components/auth/.translations/ko.json +++ b/homeassistant/components/auth/.translations/ko.json @@ -26,7 +26,7 @@ "step": { "init": { "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574 \uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", - "title": "TOTP \ub97c \uc0ac\uc6a9\ud558\uc5ec 2 \ub2e8\uacc4 \uc778\uc99d \uad6c\uc131" + "title": "TOTP \ub97c \uc0ac\uc6a9\ud558\uc5ec 2\ub2e8\uacc4 \uc778\uc99d \uad6c\uc131" } }, "title": "TOTP (\uc2dc\uac04 \uae30\ubc18 OTP)" diff --git a/homeassistant/components/auth/.translations/nl.json b/homeassistant/components/auth/.translations/nl.json index 40a873023dd..9ec8006507b 100644 --- a/homeassistant/components/auth/.translations/nl.json +++ b/homeassistant/components/auth/.translations/nl.json @@ -1,5 +1,24 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Geen meldingsservices beschikbaar." + }, + "error": { + "invalid_code": "Ongeldige code, probeer opnieuw." + }, + "step": { + "init": { + "description": "Selecteer een van de meldingsdiensten:", + "title": "Stel een \u00e9\u00e9nmalig wachtwoord in dat wordt afgegeven door een meldingscomponent" + }, + "setup": { + "description": "Een \u00e9\u00e9nmalig wachtwoord is verzonden via **notify. {notify_service}**. Voer het hieronder in:", + "title": "Controleer de instellingen" + } + }, + "title": "Eenmalig wachtwoord melden" + }, "totp": { "error": { "invalid_code": "Ongeldige code, probeer het opnieuw. Als u deze fout blijft krijgen, controleer dan of de klok van uw Home Assistant systeem correct is ingesteld." diff --git a/homeassistant/components/auth/.translations/pl.json b/homeassistant/components/auth/.translations/pl.json index 3e320ba8d62..6adaaa019c5 100644 --- a/homeassistant/components/auth/.translations/pl.json +++ b/homeassistant/components/auth/.translations/pl.json @@ -13,10 +13,11 @@ "title": "Skonfiguruj has\u0142o jednorazowe dostarczone przez komponent powiadomie\u0144" }, "setup": { - "description": "Has\u0142o jednorazowe zosta\u0142o wys\u0142ane przez ** powiadom. {notify_service} **. Wpisz je poni\u017cej:", + "description": "Has\u0142o jednorazowe zosta\u0142o wys\u0142ane przez **notify.{notify_service}**. Wpisz je poni\u017cej:", "title": "Sprawd\u017a konfiguracj\u0119" } - } + }, + "title": "Powiadomienie z has\u0142em jednorazowym" }, "totp": { "error": { diff --git a/homeassistant/components/auth/.translations/pt.json b/homeassistant/components/auth/.translations/pt.json index 474dbe488be..5401c0117e6 100644 --- a/homeassistant/components/auth/.translations/pt.json +++ b/homeassistant/components/auth/.translations/pt.json @@ -1,5 +1,24 @@ { "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Nenhum servi\u00e7o de notifica\u00e7\u00e3o dispon\u00edvel." + }, + "error": { + "invalid_code": "C\u00f3digo inv\u00e1lido, por favor tente novamente." + }, + "step": { + "init": { + "description": "Por favor, selecione um dos servi\u00e7os de notifica\u00e7\u00e3o:", + "title": "Configurar uma palavra passe entregue pela componente de notifica\u00e7\u00e3o" + }, + "setup": { + "description": "Foi enviada uma palavra passe atrav\u00e9s de **notify.{notify_service}**. Por favor, insira-a:", + "title": "Verificar a configura\u00e7\u00e3o" + } + }, + "title": "Notificar palavra passe de uso \u00fanico" + }, "totp": { "error": { "invalid_code": "C\u00f3digo inv\u00e1lido, por favor, tente novamente. Se receber este erro constantemente, por favor, certifique-se de que o rel\u00f3gio do sistema que hospeda o Home Assistent \u00e9 preciso." diff --git a/homeassistant/components/auth/.translations/sv.json b/homeassistant/components/auth/.translations/sv.json index 604ae3c4fe5..9246a88c512 100644 --- a/homeassistant/components/auth/.translations/sv.json +++ b/homeassistant/components/auth/.translations/sv.json @@ -8,11 +8,16 @@ "invalid_code": "Ogiltig kod, var god f\u00f6rs\u00f6k igen." }, "step": { + "init": { + "description": "Var god v\u00e4lj en av notifieringstj\u00e4nsterna:", + "title": "Konfigurera ett eng\u00e5ngsl\u00f6senord levererat genom notifieringskomponenten" + }, "setup": { "description": "Ett eng\u00e5ngsl\u00f6senord har skickats av **notify.{notify_service}**. V\u00e4nligen ange det nedan:", - "title": "Verifiera installationen" + "title": "Verifiera inst\u00e4llningen" } - } + }, + "title": "Meddela eng\u00e5ngsl\u00f6senord" }, "totp": { "error": { diff --git a/homeassistant/components/auth/.translations/zh-Hant.json b/homeassistant/components/auth/.translations/zh-Hant.json index e791f20a738..b7a26f5079c 100644 --- a/homeassistant/components/auth/.translations/zh-Hant.json +++ b/homeassistant/components/auth/.translations/zh-Hant.json @@ -25,7 +25,7 @@ }, "step": { "init": { - "description": "\u6b32\u555f\u7528\u4e00\u6b21\u6027\u4e14\u5177\u6642\u6548\u6027\u7684\u5bc6\u78bc\u4e4b\u5169\u6b65\u9a5f\u9a57\u8b49\u529f\u80fd\uff0c\u8acb\u4f7f\u7528\u60a8\u7684\u9a57\u8b49 App \u6383\u7784\u4e0b\u65b9\u7684 QR code \u3002\u5018\u82e5\u60a8\u5c1a\u672a\u5b89\u88dd\u4efb\u4f55 App\uff0c\u63a8\u85a6\u60a8\u4f7f\u7528 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \u6216 [Authy](https://authy.com/)\u3002\n\n{qr_code}\n\n\u65bc\u6383\u63cf\u4e4b\u5f8c\uff0c\u8f38\u5165 App \u4e2d\u7684\u516d\u4f4d\u6578\u5b57\u9032\u884c\u8a2d\u5b9a\u9a57\u8b49\u3002\u5047\u5982\u6383\u63cf\u51fa\u73fe\u554f\u984c\uff0c\u8acb\u624b\u52d5\u8f38\u5165\u4ee5\u4e0b\u9a57\u8b49\u78bc **`{code}`**\u3002", + "description": "\u6b32\u555f\u7528\u4e00\u6b21\u6027\u4e14\u5177\u6642\u6548\u6027\u7684\u5bc6\u78bc\u4e4b\u5169\u6b65\u9a5f\u9a57\u8b49\u529f\u80fd\uff0c\u8acb\u4f7f\u7528\u60a8\u7684\u9a57\u8b49 App \u6383\u7784\u4e0b\u65b9\u7684 QR code \u3002\u5018\u82e5\u60a8\u5c1a\u672a\u5b89\u88dd\u4efb\u4f55 App\uff0c\u63a8\u85a6\u60a8\u4f7f\u7528 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \u6216 [Authy](https://authy.com/)\u3002\n\n{qr_code}\n\n\u65bc\u641c\u5c0b\u4e4b\u5f8c\uff0c\u8f38\u5165 App \u4e2d\u7684\u516d\u4f4d\u6578\u5b57\u9032\u884c\u8a2d\u5b9a\u9a57\u8b49\u3002\u5047\u5982\u641c\u5c0b\u51fa\u73fe\u554f\u984c\uff0c\u8acb\u624b\u52d5\u8f38\u5165\u4ee5\u4e0b\u9a57\u8b49\u78bc **`{code}`**\u3002", "title": "\u4f7f\u7528 TOTP \u8a2d\u5b9a\u5169\u6b65\u9a5f\u9a57\u8b49" } }, diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index bee72d8e4fc..58be53d4122 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -105,7 +105,6 @@ Home Assistant. User need to record the token in secure place. "id": 11, "type": "auth/long_lived_access_token", "client_name": "GPS Logger", - "client_icon": null, "lifespan": 365 } @@ -433,7 +432,7 @@ def websocket_current_user( """Get current user.""" enabled_modules = await hass.auth.async_get_enabled_mfa(user) - connection.send_message_outside( + connection.send_message( websocket_api.result_message(msg['id'], { 'id': user.id, 'name': user.name, @@ -468,7 +467,7 @@ def websocket_create_long_lived_access_token( access_token = hass.auth.async_create_access_token( refresh_token) - connection.send_message_outside( + connection.send_message( websocket_api.result_message(msg['id'], access_token)) hass.async_create_task( @@ -480,8 +479,8 @@ def websocket_create_long_lived_access_token( def websocket_refresh_tokens( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg): """Return metadata of users refresh tokens.""" - current_id = connection.request.get('refresh_token_id') - connection.to_write.put_nowait(websocket_api.result_message(msg['id'], [{ + current_id = connection.refresh_token_id + connection.send_message(websocket_api.result_message(msg['id'], [{ 'id': refresh.id, 'client_id': refresh.client_id, 'client_name': refresh.client_name, @@ -509,7 +508,7 @@ def websocket_delete_refresh_token( await hass.auth.async_remove_refresh_token(refresh_token) - connection.send_message_outside( + connection.send_message( websocket_api.result_message(msg['id'], {})) hass.async_create_task( diff --git a/homeassistant/components/auth/indieauth.py b/homeassistant/components/auth/indieauth.py index bcf73258ffa..30432a612a4 100644 --- a/homeassistant/components/auth/indieauth.py +++ b/homeassistant/components/auth/indieauth.py @@ -1,24 +1,13 @@ """Helpers to resolve client ID/secret.""" import asyncio +from ipaddress import ip_address from html.parser import HTMLParser -from ipaddress import ip_address, ip_network from urllib.parse import urlparse, urljoin import aiohttp from aiohttp.client_exceptions import ClientError -# IP addresses of loopback interfaces -ALLOWED_IPS = ( - ip_address('127.0.0.1'), - ip_address('::1'), -) - -# RFC1918 - Address allocation for Private Internets -ALLOWED_NETWORKS = ( - ip_network('10.0.0.0/8'), - ip_network('172.16.0.0/12'), - ip_network('192.168.0.0/16'), -) +from homeassistant.util.network import is_local async def verify_redirect_uri(hass, client_id, redirect_uri): @@ -185,9 +174,7 @@ def _parse_client_id(client_id): # Not an ip address pass - if (address is None or - address in ALLOWED_IPS or - any(address in network for network in ALLOWED_NETWORKS)): + if address is None or is_local(address): return parts raise ValueError('Hostname should be a domain name or local IP address') diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py index 82eb913d890..121d95aede3 100644 --- a/homeassistant/components/auth/mfa_setup_flow.py +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -64,7 +64,7 @@ def websocket_setup_mfa( if flow_id is not None: result = await flow_manager.async_configure( flow_id, msg.get('user_input')) - connection.send_message_outside( + connection.send_message( websocket_api.result_message( msg['id'], _prepare_result_json(result))) return @@ -72,7 +72,7 @@ def websocket_setup_mfa( mfa_module_id = msg.get('mfa_module_id') mfa_module = hass.auth.get_auth_mfa_module(mfa_module_id) if mfa_module is None: - connection.send_message_outside(websocket_api.error_message( + connection.send_message(websocket_api.error_message( msg['id'], 'no_module', 'MFA module {} is not found'.format(mfa_module_id))) return @@ -80,7 +80,7 @@ def websocket_setup_mfa( result = await flow_manager.async_init( mfa_module_id, data={'user_id': connection.user.id}) - connection.send_message_outside( + connection.send_message( websocket_api.result_message( msg['id'], _prepare_result_json(result))) @@ -99,13 +99,13 @@ def websocket_depose_mfa( await hass.auth.async_disable_user_mfa( connection.user, msg['mfa_module_id']) except ValueError as err: - connection.send_message_outside(websocket_api.error_message( + connection.send_message(websocket_api.error_message( msg['id'], 'disable_failed', 'Cannot disable MFA Module {}: {}'.format( mfa_module_id, err))) return - connection.send_message_outside( + connection.send_message( websocket_api.result_message( msg['id'], 'done')) diff --git a/homeassistant/components/auth/strings.json b/homeassistant/components/auth/strings.json index 2b1fc0c94f6..57f5ed659b0 100644 --- a/homeassistant/components/auth/strings.json +++ b/homeassistant/components/auth/strings.json @@ -17,15 +17,15 @@ "step": { "init": { "title": "Set up one-time password delivered by notify component", - "description": "Please select one of notify service:" + "description": "Please select one of the notification services:" }, "setup": { "title": "Verify setup", - "description": "A one-time password have sent by **notify.{notify_service}**. Please input it in below:" + "description": "A one-time password has been sent via **notify.{notify_service}**. Please enter it below:" } }, "abort": { - "no_available_service": "No available notify services." + "no_available_service": "No notification services available." }, "error": { "invalid_code": "Invalid code, please try again." diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 43fd4cedb88..a1f1563f5e1 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -17,7 +17,6 @@ from homeassistant.loader import bind_hass from homeassistant.const import ( ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START, CONF_ID) -from homeassistant.components import logbook from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import extract_domain_configs, script, condition from homeassistant.helpers.entity import ToggleEntity @@ -115,49 +114,6 @@ def is_on(hass, entity_id): return hass.states.is_state(entity_id, STATE_ON) -@bind_hass -def turn_on(hass, entity_id=None): - """Turn on specified automation or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TURN_ON, data) - - -@bind_hass -def turn_off(hass, entity_id=None): - """Turn off specified automation or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) - - -@bind_hass -def toggle(hass, entity_id=None): - """Toggle specified automation or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TOGGLE, data) - - -@bind_hass -def trigger(hass, entity_id=None): - """Trigger specified automation or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TRIGGER, data) - - -@bind_hass -def reload(hass): - """Reload the automation from config.""" - hass.services.call(DOMAIN, SERVICE_RELOAD) - - -@bind_hass -def async_reload(hass): - """Reload the automation from config. - - Returns a coroutine object. - """ - return hass.services.async_call(DOMAIN, SERVICE_RELOAD) - - async def async_setup(hass, config): """Set up the automation.""" component = EntityComponent(_LOGGER, DOMAIN, hass, @@ -412,8 +368,8 @@ def _async_get_action(hass, config, name): async def action(entity_id, variables, context): """Execute an action.""" _LOGGER.info('Executing %s', name) - logbook.async_log_entry( - hass, name, 'has been triggered', DOMAIN, entity_id) + hass.components.logbook.async_log_entry( + name, 'has been triggered', DOMAIN, entity_id) await script_obj.async_run(variables, context) return action diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index e19a85edae6..a9605f343fd 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -4,7 +4,6 @@ Offer event listening automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/docs/automation/trigger/#event-trigger """ -import asyncio import logging import voluptuous as vol @@ -25,8 +24,7 @@ TRIGGER_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_trigger(hass, config, action): +async def async_trigger(hass, config, action): """Listen for events based on configuration.""" event_type = config.get(CONF_EVENT_TYPE) event_data_schema = vol.Schema( diff --git a/homeassistant/components/automation/homeassistant.py b/homeassistant/components/automation/homeassistant.py index b55d99f706a..30ab979d6f4 100644 --- a/homeassistant/components/automation/homeassistant.py +++ b/homeassistant/components/automation/homeassistant.py @@ -4,7 +4,6 @@ Offer Home Assistant core automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/components/automation/#homeassistant-trigger """ -import asyncio import logging import voluptuous as vol @@ -23,8 +22,7 @@ TRIGGER_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_trigger(hass, config, action): +async def async_trigger(hass, config, action): """Listen for events based on configuration.""" event = config.get(CONF_EVENT) diff --git a/homeassistant/components/automation/litejet.py b/homeassistant/components/automation/litejet.py index c827fe8f7a4..c0d2dd99ba2 100644 --- a/homeassistant/components/automation/litejet.py +++ b/homeassistant/components/automation/litejet.py @@ -4,7 +4,6 @@ Trigger an automation when a LiteJet switch is released. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/automation.litejet/ """ -import asyncio import logging import voluptuous as vol @@ -33,8 +32,7 @@ TRIGGER_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_trigger(hass, config, action): +async def async_trigger(hass, config, action): """Listen for events based on configuration.""" number = config.get(CONF_NUMBER) held_more_than = config.get(CONF_HELD_MORE_THAN) diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index 60c33ca9b0e..99d5ab8674c 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -4,7 +4,6 @@ Offer MQTT listening automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/docs/automation/trigger/#mqtt-trigger """ -import asyncio import json import voluptuous as vol @@ -25,8 +24,7 @@ TRIGGER_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_trigger(hass, config, action): +async def async_trigger(hass, config, action): """Listen for state changes based on configuration.""" topic = config.get(CONF_TOPIC) payload = config.get(CONF_PAYLOAD) @@ -51,6 +49,6 @@ def async_trigger(hass, config, action): 'trigger': data }) - remove = yield from mqtt.async_subscribe( + remove = await mqtt.async_subscribe( hass, topic, mqtt_automation_listener) return remove diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index f0dcbf0be57..675b6f3653a 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -4,7 +4,6 @@ Offer numeric state listening automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/docs/automation/trigger/#numeric-state-trigger """ -import asyncio import logging import voluptuous as vol @@ -29,8 +28,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({ _LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_trigger(hass, config, action): +async def async_trigger(hass, config, action): """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) below = config.get(CONF_BELOW) diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 263d4158e25..46c5cafa071 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -4,7 +4,6 @@ Offer state listening automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/docs/automation/trigger/#state-trigger """ -import asyncio import voluptuous as vol from homeassistant.core import callback @@ -27,8 +26,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({ }), cv.key_dependency(CONF_FOR, CONF_TO)) -@asyncio.coroutine -def async_trigger(hass, config, action): +async def async_trigger(hass, config, action): """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) from_state = config.get(CONF_FROM, MATCH_ALL) diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 497b8453267..7cefe6953a1 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -4,7 +4,6 @@ Offer sun based automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/docs/automation/trigger/#sun-trigger """ -import asyncio from datetime import timedelta import logging @@ -25,8 +24,7 @@ TRIGGER_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_trigger(hass, config, action): +async def async_trigger(hass, config, action): """Listen for events based on configuration.""" event = config.get(CONF_EVENT) offset = config.get(CONF_OFFSET) diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index 67a44f1a347..c0d83b1067f 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -4,7 +4,6 @@ Offer template automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/docs/automation/trigger/#template-trigger """ -import asyncio import logging import voluptuous as vol @@ -23,8 +22,7 @@ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_trigger(hass, config, action): +async def async_trigger(hass, config, action): """Listen for state changes based on configuration.""" value_template = config.get(CONF_VALUE_TEMPLATE) value_template.hass = hass diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index a3a8496c3c5..eccc31581a0 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -4,7 +4,6 @@ Offer time listening automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/docs/automation/trigger/#time-trigger """ -import asyncio import logging import voluptuous as vol @@ -29,8 +28,7 @@ TRIGGER_SCHEMA = vol.All(vol.Schema({ }), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AT)) -@asyncio.coroutine -def async_trigger(hass, config, action): +async def async_trigger(hass, config, action): """Listen for state changes based on configuration.""" if CONF_AT in config: at_time = config.get(CONF_AT) diff --git a/homeassistant/components/automation/webhook.py b/homeassistant/components/automation/webhook.py new file mode 100644 index 00000000000..2c9c331cdc5 --- /dev/null +++ b/homeassistant/components/automation/webhook.py @@ -0,0 +1,54 @@ +""" +Offer webhook triggered automation rules. + +For more details about this automation rule, please refer to the documentation +at https://home-assistant.io/docs/automation/trigger/#webhook-trigger +""" +from functools import partial +import logging + +from aiohttp import hdrs +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.const import CONF_PLATFORM +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ('webhook',) + +_LOGGER = logging.getLogger(__name__) +CONF_WEBHOOK_ID = 'webhook_id' + +TRIGGER_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): 'webhook', + vol.Required(CONF_WEBHOOK_ID): cv.string, +}) + + +async def _handle_webhook(action, hass, webhook_id, request): + """Handle incoming webhook.""" + result = { + 'platform': 'webhook', + 'webhook_id': webhook_id, + } + + if 'json' in request.headers.get(hdrs.CONTENT_TYPE, ''): + result['json'] = await request.json() + else: + result['data'] = await request.post() + + hass.async_run_job(action, {'trigger': result}) + + +async def async_trigger(hass, config, action): + """Trigger based on incoming webhooks.""" + webhook_id = config.get(CONF_WEBHOOK_ID) + hass.components.webhook.async_register( + webhook_id, partial(_handle_webhook, action)) + + @callback + def unregister(): + """Unregister webhook.""" + hass.components.webhook.async_unregister(webhook_id) + + return unregister diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index f30dfe753cb..dfc9cc418bf 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -4,7 +4,6 @@ Offer zone automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/docs/automation/trigger/#zone-trigger """ -import asyncio import voluptuous as vol from homeassistant.core import callback @@ -27,8 +26,7 @@ TRIGGER_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_trigger(hass, config, action): +async def async_trigger(hass, config, action): """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) zone_entity_id = config.get(CONF_ZONE) diff --git a/homeassistant/components/binary_sensor/ads.py b/homeassistant/components/binary_sensor/ads.py index d46ff5ec2ee..1ee56cac9d3 100644 --- a/homeassistant/components/binary_sensor/ads.py +++ b/homeassistant/components/binary_sensor/ads.py @@ -4,7 +4,6 @@ Support for ADS binary sensors. For more details about this platform, please refer to the documentation. https://home-assistant.io/components/binary_sensor.ads/ """ -import asyncio import logging import voluptuous as vol @@ -50,8 +49,7 @@ class AdsBinarySensor(BinarySensorDevice): self._ads_hub = ads_hub self.ads_var = ads_var - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register device notification.""" def update(name, value): """Handle device notifications.""" diff --git a/homeassistant/components/binary_sensor/alarmdecoder.py b/homeassistant/components/binary_sensor/alarmdecoder.py index 82bcc50259f..1b50d6c6c72 100644 --- a/homeassistant/components/binary_sensor/alarmdecoder.py +++ b/homeassistant/components/binary_sensor/alarmdecoder.py @@ -4,7 +4,6 @@ Support for AlarmDecoder zone states- represented as binary sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.alarmdecoder/ """ -import asyncio import logging from homeassistant.components.binary_sensor import BinarySensorDevice @@ -64,8 +63,7 @@ class AlarmDecoderBinarySensor(BinarySensorDevice): self._relay_addr = relay_addr self._relay_chan = relay_chan - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( SIGNAL_ZONE_FAULT, self._fault_callback) diff --git a/homeassistant/components/binary_sensor/android_ip_webcam.py b/homeassistant/components/binary_sensor/android_ip_webcam.py index 58de81c30e7..085bafd3ae3 100644 --- a/homeassistant/components/binary_sensor/android_ip_webcam.py +++ b/homeassistant/components/binary_sensor/android_ip_webcam.py @@ -4,8 +4,6 @@ Support for IP Webcam binary sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.android_ip_webcam/ """ -import asyncio - from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.android_ip_webcam import ( KEY_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, CONF_NAME) @@ -13,9 +11,8 @@ from homeassistant.components.android_ip_webcam import ( DEPENDENCIES = ['android_ip_webcam'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the IP Webcam binary sensors.""" if discovery_info is None: return @@ -51,8 +48,7 @@ class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice): """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.""" state, _ = self._ipcam.export_sensor(self._sensor) self._state = state == 1.0 diff --git a/homeassistant/components/binary_sensor/bayesian.py b/homeassistant/components/binary_sensor/bayesian.py index 88669d67d80..f7802f0f29d 100644 --- a/homeassistant/components/binary_sensor/bayesian.py +++ b/homeassistant/components/binary_sensor/bayesian.py @@ -4,7 +4,6 @@ Use Bayesian Inference to trigger a binary sensor. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.bayesian/ """ -import asyncio import logging from collections import OrderedDict @@ -74,9 +73,8 @@ def update_probability(prior, prob_true, prob_false): return probability -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Bayesian Binary sensor.""" name = config.get(CONF_NAME) observations = config.get(CONF_OBSERVATIONS) @@ -119,8 +117,7 @@ class BayesianBinarySensor(BinarySensorDevice): 'state': self._process_state } - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity about to be added.""" @callback def async_threshold_sensor_state_listener(entity, old_state, @@ -214,7 +211,6 @@ class BayesianBinarySensor(BinarySensorDevice): ATTR_PROBABILITY_THRESHOLD: self._probability_threshold, } - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data and update the states.""" self._deviation = bool(self.probability >= self._probability_threshold) diff --git a/homeassistant/components/binary_sensor/blink.py b/homeassistant/components/binary_sensor/blink.py index 6ade20b72b9..6519d09a29a 100644 --- a/homeassistant/components/binary_sensor/blink.py +++ b/homeassistant/components/binary_sensor/blink.py @@ -2,10 +2,11 @@ Support for Blink system camera control. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.blink/ +https://home-assistant.io/components/binary_sensor.blink. """ -from homeassistant.components.blink import DOMAIN +from homeassistant.components.blink import BLINK_DATA, BINARY_SENSORS from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.const import CONF_MONITORED_CONDITIONS DEPENDENCIES = ['blink'] @@ -14,24 +15,27 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the blink binary sensors.""" if discovery_info is None: return + data = hass.data[BLINK_DATA] - data = hass.data[DOMAIN].blink - devs = list() - for name in data.cameras: - devs.append(BlinkCameraMotionSensor(name, data)) - devs.append(BlinkSystemSensor(data)) + devs = [] + for camera in data.sync.cameras: + for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: + devs.append(BlinkBinarySensor(data, camera, sensor_type)) add_entities(devs, True) -class BlinkCameraMotionSensor(BinarySensorDevice): +class BlinkBinarySensor(BinarySensorDevice): """Representation of a Blink binary sensor.""" - def __init__(self, name, data): + def __init__(self, data, camera, sensor_type): """Initialize the sensor.""" - self._name = 'blink_' + name + '_motion_enabled' - self._camera_name = name self.data = data - self._state = self.data.cameras[self._camera_name].armed + self._type = sensor_type + name, icon = BINARY_SENSORS[sensor_type] + self._name = "{} {} {}".format(BLINK_DATA, camera, name) + self._icon = icon + self._camera = data.sync.cameras[camera] + self._state = None @property def name(self): @@ -46,29 +50,4 @@ class BlinkCameraMotionSensor(BinarySensorDevice): def update(self): """Update sensor state.""" self.data.refresh() - self._state = self.data.cameras[self._camera_name].armed - - -class BlinkSystemSensor(BinarySensorDevice): - """A representation of a Blink system sensor.""" - - def __init__(self, data): - """Initialize the sensor.""" - self._name = 'blink armed status' - self.data = data - self._state = self.data.arm - - @property - def name(self): - """Return the name of the blink sensor.""" - return self._name.replace(" ", "_") - - @property - def is_on(self): - """Return the status of the sensor.""" - return self._state - - def update(self): - """Update sensor state.""" - self.data.refresh() - self._state = self.data.arm + self._state = self._camera.attributes[self._type] diff --git a/homeassistant/components/binary_sensor/bmw_connected_drive.py b/homeassistant/components/binary_sensor/bmw_connected_drive.py index 36229828d63..3fe8136c93b 100644 --- a/homeassistant/components/binary_sensor/bmw_connected_drive.py +++ b/homeassistant/components/binary_sensor/bmw_connected_drive.py @@ -4,7 +4,6 @@ Reads vehicle status from BMW connected drive portal. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.bmw_connected_drive/ """ -import asyncio import logging from homeassistant.components.binary_sensor import BinarySensorDevice @@ -124,7 +123,10 @@ class BMWConnectedDriveSensor(BinarySensorDevice): if not check_control_messages: result['check_control_messages'] = 'OK' else: - result['check_control_messages'] = check_control_messages + cbs_list = [] + for message in check_control_messages: + cbs_list.append(message['ccmDescriptionShort']) + result['check_control_messages'] = cbs_list elif self._attribute == 'charging_status': result['charging_status'] = vehicle_state.charging_status.value # pylint: disable=protected-access @@ -190,8 +192,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice): """Schedule a state update.""" self.schedule_update_ha_state(True) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Add callback after being added to hass. Show latest data after startup. diff --git a/homeassistant/components/binary_sensor/egardia.py b/homeassistant/components/binary_sensor/egardia.py index 0db2cac667f..56d7dda17ba 100644 --- a/homeassistant/components/binary_sensor/egardia.py +++ b/homeassistant/components/binary_sensor/egardia.py @@ -4,7 +4,6 @@ Interfaces with Egardia/Woonveilig alarm control panel. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.egardia/ """ -import asyncio import logging from homeassistant.components.binary_sensor import BinarySensorDevice @@ -18,9 +17,8 @@ EGARDIA_TYPE_TO_DEVICE_CLASS = {'IR Sensor': 'motion', 'IR': 'motion'} -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Initialize the platform.""" if (discovery_info is None or discovery_info[ATTR_DISCOVER_DEVICES] is None): diff --git a/homeassistant/components/binary_sensor/envisalink.py b/homeassistant/components/binary_sensor/envisalink.py index 2568879bcc6..276ace8dd51 100644 --- a/homeassistant/components/binary_sensor/envisalink.py +++ b/homeassistant/components/binary_sensor/envisalink.py @@ -4,7 +4,6 @@ Support for Envisalink zone states- represented as binary sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.envisalink/ """ -import asyncio import logging import datetime @@ -22,9 +21,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['envisalink'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Envisalink binary sensor devices.""" configured_zones = discovery_info['zones'] @@ -56,8 +54,7 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice): _LOGGER.debug('Setting up zone: %s', zone_name) super().__init__(zone_name, info, controller) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" async_dispatcher_connect( self.hass, SIGNAL_ZONE_UPDATE, self._update_callback) diff --git a/homeassistant/components/binary_sensor/ffmpeg_motion.py b/homeassistant/components/binary_sensor/ffmpeg_motion.py index 365bcafbd69..df811d47e56 100644 --- a/homeassistant/components/binary_sensor/ffmpeg_motion.py +++ b/homeassistant/components/binary_sensor/ffmpeg_motion.py @@ -4,7 +4,6 @@ Provides a binary sensor which is a collection of ffmpeg tools. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.ffmpeg_motion/ """ -import asyncio import logging import voluptuous as vol @@ -46,13 +45,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the FFmpeg binary motion sensor.""" manager = hass.data[DATA_FFMPEG] - if not manager.async_run_test(config.get(CONF_INPUT)): + if not await manager.async_run_test(config.get(CONF_INPUT)): return entity = FFmpegMotion(hass, manager, config) @@ -98,8 +96,7 @@ class FFmpegMotion(FFmpegBinarySensor): self.ffmpeg = SensorMotion( manager.binary, hass.loop, self._async_callback) - @asyncio.coroutine - def _async_start_ffmpeg(self, entity_ids): + async def _async_start_ffmpeg(self, entity_ids): """Start a FFmpeg instance. This method is a coroutine. @@ -116,7 +113,7 @@ class FFmpegMotion(FFmpegBinarySensor): ) # run - yield from self.ffmpeg.open_sensor( + await self.ffmpeg.open_sensor( input_source=self._config.get(CONF_INPUT), extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS), ) diff --git a/homeassistant/components/binary_sensor/ffmpeg_noise.py b/homeassistant/components/binary_sensor/ffmpeg_noise.py index 73c84ac336d..a2625c3de8d 100644 --- a/homeassistant/components/binary_sensor/ffmpeg_noise.py +++ b/homeassistant/components/binary_sensor/ffmpeg_noise.py @@ -4,7 +4,6 @@ Provides a binary sensor which is a collection of ffmpeg tools. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.ffmpeg_noise/ """ -import asyncio import logging import voluptuous as vol @@ -43,13 +42,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the FFmpeg noise binary sensor.""" manager = hass.data[DATA_FFMPEG] - if not manager.async_run_test(config.get(CONF_INPUT)): + if not await manager.async_run_test(config.get(CONF_INPUT)): return entity = FFmpegNoise(hass, manager, config) @@ -67,8 +65,7 @@ class FFmpegNoise(FFmpegBinarySensor): self.ffmpeg = SensorNoise( manager.binary, hass.loop, self._async_callback) - @asyncio.coroutine - def _async_start_ffmpeg(self, entity_ids): + async def _async_start_ffmpeg(self, entity_ids): """Start a FFmpeg instance. This method is a coroutine. @@ -82,7 +79,7 @@ class FFmpegNoise(FFmpegBinarySensor): peak=self._config.get(CONF_PEAK), ) - yield from self.ffmpeg.open_sensor( + await self.ffmpeg.open_sensor( input_source=self._config.get(CONF_INPUT), output_dest=self._config.get(CONF_OUTPUT), extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS), diff --git a/homeassistant/components/binary_sensor/fritzbox.py b/homeassistant/components/binary_sensor/fritzbox.py new file mode 100644 index 00000000000..ab58e6e84bc --- /dev/null +++ b/homeassistant/components/binary_sensor/fritzbox.py @@ -0,0 +1,64 @@ +""" +Support for Fritzbox binary sensors. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.fritzbox/ +""" +import logging + +import requests + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.fritzbox import DOMAIN as FRITZBOX_DOMAIN + +DEPENDENCIES = ['fritzbox'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Fritzbox binary sensor platform.""" + devices = [] + fritz_list = hass.data[FRITZBOX_DOMAIN] + + for fritz in fritz_list: + device_list = fritz.get_devices() + for device in device_list: + if device.has_alarm: + devices.append(FritzboxBinarySensor(device, fritz)) + + add_entities(devices, True) + + +class FritzboxBinarySensor(BinarySensorDevice): + """Representation of a binary Fritzbox device.""" + + def __init__(self, device, fritz): + """Initialize the Fritzbox binary sensor.""" + self._device = device + self._fritz = fritz + + @property + def name(self): + """Return the name of the entity.""" + return self._device.name + + @property + def device_class(self): + """Return the class of this sensor.""" + return 'window' + + @property + def is_on(self): + """Return true if sensor is on.""" + if not self._device.present: + return False + return self._device.alert_state + + def update(self): + """Get latest data from the Fritzbox.""" + try: + self._device.update() + except requests.exceptions.HTTPError as ex: + _LOGGER.warning("Connection error: %s", ex) + self._fritz.login() diff --git a/homeassistant/components/binary_sensor/insteon.py b/homeassistant/components/binary_sensor/insteon.py index 533ff2d76c0..c399d31a95b 100644 --- a/homeassistant/components/binary_sensor/insteon.py +++ b/homeassistant/components/binary_sensor/insteon.py @@ -4,7 +4,6 @@ Support for INSTEON dimmers via PowerLinc Modem. For more details about this component, please refer to the documentation at https://home-assistant.io/components/binary_sensor.insteon/ """ -import asyncio import logging from homeassistant.components.binary_sensor import BinarySensorDevice @@ -22,9 +21,8 @@ SENSOR_TYPES = {'openClosedSensor': 'opening', 'batterySensor': 'battery'} -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the INSTEON device class for the hass platform.""" insteon_modem = hass.data['insteon'].get('modem') diff --git a/homeassistant/components/binary_sensor/isy994.py b/homeassistant/components/binary_sensor/isy994.py index 36dacb06738..31a9606d950 100644 --- a/homeassistant/components/binary_sensor/isy994.py +++ b/homeassistant/components/binary_sensor/isy994.py @@ -4,8 +4,6 @@ Support for ISY994 binary sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.isy994/ """ - -import asyncio import logging from datetime import timedelta from typing import Callable @@ -121,10 +119,9 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): self._computed_state = bool(self._node.status._val) self._status_was_unknown = False - @asyncio.coroutine - def async_added_to_hass(self) -> None: + async def async_added_to_hass(self) -> None: """Subscribe to the node and subnode event emitters.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() self._node.controlEvents.subscribe(self._positive_node_control_handler) @@ -261,10 +258,9 @@ class ISYBinarySensorHeartbeat(ISYDevice, BinarySensorDevice): self._parent_device = parent_device self._heartbeat_timer = None - @asyncio.coroutine - def async_added_to_hass(self) -> None: + async def async_added_to_hass(self) -> None: """Subscribe to the node and subnode event emitters.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() self._node.controlEvents.subscribe( self._heartbeat_node_control_handler) diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index 37a26a27214..baaf6a9a567 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -4,23 +4,26 @@ Support for MQTT binary sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.mqtt/ """ -import asyncio import logging from typing import Optional import voluptuous as vol from homeassistant.core import callback -from homeassistant.components import mqtt +from homeassistant.components import mqtt, binary_sensor from homeassistant.components.binary_sensor import ( BinarySensorDevice, DEVICE_CLASSES_SCHEMA) from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF, CONF_DEVICE_CLASS) from homeassistant.components.mqtt import ( - CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE, - CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability) + ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, + CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, + MqttAvailability, MqttDiscoveryUpdate) +from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType, ConfigType _LOGGER = logging.getLogger(__name__) @@ -44,13 +47,28 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the MQTT binary sensor.""" - if discovery_info is not None: - config = PLATFORM_SCHEMA(discovery_info) +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_entities, discovery_info=None): + """Set up MQTT binary sensor through configuration.yaml.""" + await _async_setup_entity(hass, config, async_add_entities) + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT binary sensor dynamically through MQTT discovery.""" + async def async_discover(discovery_payload): + """Discover and add a MQTT binary sensor.""" + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(hass, config, async_add_entities, + discovery_payload[ATTR_DISCOVERY_HASH]) + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(binary_sensor.DOMAIN, 'mqtt'), + async_discover) + + +async def _async_setup_entity(hass, config, async_add_entities, + discovery_hash=None): + """Set up the MQTT binary sensor.""" value_template = config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = hass @@ -68,19 +86,22 @@ def async_setup_platform(hass, config, async_add_entities, config.get(CONF_PAYLOAD_NOT_AVAILABLE), value_template, config.get(CONF_UNIQUE_ID), + discovery_hash, )]) -class MqttBinarySensor(MqttAvailability, BinarySensorDevice): +class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate, + BinarySensorDevice): """Representation a binary sensor that is updated by MQTT.""" def __init__(self, name, state_topic, availability_topic, device_class, qos, force_update, payload_on, payload_off, payload_available, payload_not_available, value_template, - unique_id: Optional[str]): + unique_id: Optional[str], discovery_hash): """Initialize the MQTT binary sensor.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_hash) self._name = name self._state = None self._state_topic = state_topic @@ -91,11 +112,12 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice): self._force_update = force_update self._template = value_template self._unique_id = unique_id + self._discovery_hash = discovery_hash - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe mqtt events.""" - yield from super().async_added_to_hass() + await MqttAvailability.async_added_to_hass(self) + await MqttDiscoveryUpdate.async_added_to_hass(self) @callback def state_message_received(topic, payload, qos): @@ -115,7 +137,7 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice): self.async_schedule_update_ha_state() - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._state_topic, state_message_received, self._qos) @property diff --git a/homeassistant/components/binary_sensor/mychevy.py b/homeassistant/components/binary_sensor/mychevy.py index d1438379da1..c1e3b6f0aac 100644 --- a/homeassistant/components/binary_sensor/mychevy.py +++ b/homeassistant/components/binary_sensor/mychevy.py @@ -3,8 +3,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.mychevy/ """ - -import asyncio import logging from homeassistant.components.mychevy import ( @@ -22,9 +20,8 @@ SENSORS = [ ] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the MyChevy sensors.""" if discovery_info is None: return @@ -75,8 +72,7 @@ class EVBinarySensor(BinarySensorDevice): """Return the car.""" return self._conn.get_car(self._car_vid) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( UPDATE_TOPIC, self.async_update_callback) diff --git a/homeassistant/components/binary_sensor/mystrom.py b/homeassistant/components/binary_sensor/mystrom.py index 23f40ce0a7f..5785ed464fd 100644 --- a/homeassistant/components/binary_sensor/mystrom.py +++ b/homeassistant/components/binary_sensor/mystrom.py @@ -4,7 +4,6 @@ Support for the myStrom buttons. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.mystrom/ """ -import asyncio import logging from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice @@ -16,9 +15,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['http'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up myStrom Binary Sensor.""" hass.http.register_view(MyStromView(async_add_entities)) @@ -37,14 +35,12 @@ class MyStromView(HomeAssistantView): self.buttons = {} self.add_entities = add_entities - @asyncio.coroutine - def get(self, request): + async def get(self, request): """Handle the GET request received from a myStrom button.""" - res = yield from self._handle(request.app['hass'], request.query) + res = await self._handle(request.app['hass'], request.query) return res - @asyncio.coroutine - def _handle(self, hass, data): + async def _handle(self, hass, data): """Handle requests to the myStrom endpoint.""" button_action = next(( parameter for parameter in data diff --git a/homeassistant/components/binary_sensor/openuv.py b/homeassistant/components/binary_sensor/openuv.py index c7c27d73ee4..bd6e4d1d5dc 100644 --- a/homeassistant/components/binary_sensor/openuv.py +++ b/homeassistant/components/binary_sensor/openuv.py @@ -93,7 +93,11 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice): async def async_update(self): """Update the state.""" - data = self.openuv.data[DATA_PROTECTION_WINDOW]['result'] + data = self.openuv.data[DATA_PROTECTION_WINDOW] + + if not data: + return + if self._sensor_type == TYPE_PROTECTION_WINDOW: self._state = parse_datetime( data['from_time']) <= utcnow() <= parse_datetime( diff --git a/homeassistant/components/binary_sensor/ping.py b/homeassistant/components/binary_sensor/ping.py index 4c597dd63e1..f12957e6129 100644 --- a/homeassistant/components/binary_sensor/ping.py +++ b/homeassistant/components/binary_sensor/ping.py @@ -4,18 +4,19 @@ Tracks the latency of a host by sending ICMP echo requests (ping). For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.ping/ """ -import logging -import subprocess -import re -import sys +import asyncio from datetime import timedelta +import logging +import re +import subprocess +import sys import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA) -from homeassistant.const import CONF_NAME, CONF_HOST + PLATFORM_SCHEMA, BinarySensorDevice) +from homeassistant.const import CONF_HOST, CONF_NAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -48,13 +49,14 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): """Set up the Ping Binary sensor.""" name = config.get(CONF_NAME) host = config.get(CONF_HOST) count = config.get(CONF_PING_COUNT) - add_entities([PingBinarySensor(name, PingData(host, count))], True) + async_add_entities([PingBinarySensor(name, PingData(host, count))], True) class PingBinarySensor(BinarySensorDevice): @@ -91,9 +93,9 @@ class PingBinarySensor(BinarySensorDevice): ATTR_ROUND_TRIP_TIME_MIN: self.ping.data['min'], } - def update(self): + async def async_update(self): """Get the latest data.""" - self.ping.update() + await self.ping.update() class PingData: @@ -114,12 +116,13 @@ class PingData: 'ping', '-n', '-q', '-c', str(self._count), '-W1', self._ip_address] - def ping(self): + async def ping(self): """Send ICMP echo request and return details if success.""" - pinger = subprocess.Popen( - self._ping_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + pinger = await asyncio.create_subprocess_shell( + ' '.join(self._ping_cmd), stdout=subprocess.PIPE, + stderr=subprocess.PIPE) try: - out = pinger.communicate() + out = await pinger.communicate() _LOGGER.debug("Output is %s", str(out)) if sys.platform == 'win32': match = WIN32_PING_MATCHER.search(str(out).split('\n')[-1]) @@ -128,7 +131,8 @@ class PingData: 'min': rtt_min, 'avg': rtt_avg, 'max': rtt_max, - 'mdev': ''} + 'mdev': '', + } if 'max/' not in str(out): match = PING_MATCHER_BUSYBOX.search(str(out).split('\n')[-1]) rtt_min, rtt_avg, rtt_max = match.groups() @@ -136,18 +140,20 @@ class PingData: 'min': rtt_min, 'avg': rtt_avg, 'max': rtt_max, - 'mdev': ''} + 'mdev': '', + } match = PING_MATCHER.search(str(out).split('\n')[-1]) rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups() return { 'min': rtt_min, 'avg': rtt_avg, 'max': rtt_max, - 'mdev': rtt_mdev} + 'mdev': rtt_mdev, + } except (subprocess.CalledProcessError, AttributeError): return False - def update(self): + async def update(self): """Retrieve the latest details from the host.""" - self.data = self.ping() + self.data = await self.ping() self.available = bool(self.data) diff --git a/homeassistant/components/binary_sensor/rachio.py b/homeassistant/components/binary_sensor/rachio.py index 798b6a754d1..36a32c79c5c 100644 --- a/homeassistant/components/binary_sensor/rachio.py +++ b/homeassistant/components/binary_sensor/rachio.py @@ -92,6 +92,11 @@ class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor): """Return the name of this sensor including the controller name.""" return "{} online".format(self._controller.name) + @property + def unique_id(self) -> str: + """Return a unique id for this entity.""" + return "{}-online".format(self._controller.controller_id) + @property def device_class(self) -> str: """Return the class of this device, from component DEVICE_CLASSES.""" diff --git a/homeassistant/components/binary_sensor/satel_integra.py b/homeassistant/components/binary_sensor/satel_integra.py index 3500f0a0576..8aff02d55a7 100644 --- a/homeassistant/components/binary_sensor/satel_integra.py +++ b/homeassistant/components/binary_sensor/satel_integra.py @@ -4,7 +4,6 @@ Support for Satel Integra zone states- represented as binary sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.satel_integra/ """ -import asyncio import logging from homeassistant.components.binary_sensor import BinarySensorDevice @@ -20,9 +19,8 @@ DEPENDENCIES = ['satel_integra'] _LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Satel Integra binary sensor devices.""" if not discovery_info: return @@ -50,8 +48,7 @@ class SatelIntegraBinarySensor(BinarySensorDevice): self._zone_type = zone_type self._state = 0 - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" async_dispatcher_connect( self.hass, SIGNAL_ZONES_UPDATED, self._zones_updated) diff --git a/homeassistant/components/binary_sensor/spc.py b/homeassistant/components/binary_sensor/spc.py index c1be72db374..baa25266804 100644 --- a/homeassistant/components/binary_sensor/spc.py +++ b/homeassistant/components/binary_sensor/spc.py @@ -9,8 +9,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.core import callback -from homeassistant.components.spc import ( - ATTR_DISCOVER_DEVICES, SIGNAL_UPDATE_SENSOR) +from homeassistant.components.spc import (DATA_API, SIGNAL_UPDATE_SENSOR) _LOGGER = logging.getLogger(__name__) @@ -27,13 +26,12 @@ def _get_device_class(zone_type): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the SPC binary sensor.""" - if (discovery_info is None or - discovery_info[ATTR_DISCOVER_DEVICES] is None): + if discovery_info is None: return - - async_add_entities(SpcBinarySensor(zone) - for zone in discovery_info[ATTR_DISCOVER_DEVICES] - if _get_device_class(zone.type)) + api = hass.data[DATA_API] + async_add_entities([SpcBinarySensor(zone) + for zone in api.zones.values() + if _get_device_class(zone.type)]) class SpcBinarySensor(BinarySensorDevice): diff --git a/homeassistant/components/binary_sensor/template.py b/homeassistant/components/binary_sensor/template.py index c5bfa593022..89547dffbc9 100644 --- a/homeassistant/components/binary_sensor/template.py +++ b/homeassistant/components/binary_sensor/template.py @@ -4,7 +4,6 @@ Support for exposing a templated binary sensor. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.template/ """ -import asyncio import logging import voluptuous as vol @@ -46,9 +45,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up template binary sensors.""" sensors = [] @@ -109,8 +107,7 @@ class BinarySensorTemplate(BinarySensorDevice): self._delay_on = delay_on self._delay_off = delay_off - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" @callback def template_bsensor_state_listener(entity, old_state, new_state): diff --git a/homeassistant/components/binary_sensor/threshold.py b/homeassistant/components/binary_sensor/threshold.py index fd7ead08822..0dadf3a61fd 100644 --- a/homeassistant/components/binary_sensor/threshold.py +++ b/homeassistant/components/binary_sensor/threshold.py @@ -4,7 +4,6 @@ Support for monitoring if a sensor value is below/above a threshold. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.threshold/ """ -import asyncio import logging import voluptuous as vol @@ -54,9 +53,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Threshold sensor.""" entity_id = config.get(CONF_ENTITY_ID) name = config.get(CONF_NAME) @@ -147,8 +145,7 @@ class ThresholdSensor(BinarySensorDevice): ATTR_UPPER: self._threshold_upper, } - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data and updates the states.""" def below(threshold): """Determine if the sensor value is below a threshold.""" diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py index ae6fd5562bf..0b168e45b4d 100644 --- a/homeassistant/components/binary_sensor/trend.py +++ b/homeassistant/components/binary_sensor/trend.py @@ -4,7 +4,6 @@ A sensor that monitors trends in other components. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.trend/ """ -import asyncio from collections import deque import logging import math @@ -138,8 +137,7 @@ class SensorTrend(BinarySensorDevice): """No polling needed.""" return False - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Complete device setup after being added to hass.""" @callback def trend_sensor_state_listener(entity, old_state, new_state): @@ -160,8 +158,7 @@ class SensorTrend(BinarySensorDevice): self.hass, self._entity_id, trend_sensor_state_listener) - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data and update the states.""" # Remove outdated samples if self._sample_duration > 0: @@ -173,7 +170,7 @@ class SensorTrend(BinarySensorDevice): return # Calculate gradient of linear trend - yield from self.hass.async_add_job(self._calculate_gradient) + await self.hass.async_add_job(self._calculate_gradient) # Update state self._state = ( diff --git a/homeassistant/components/binary_sensor/wink.py b/homeassistant/components/binary_sensor/wink.py index 1976e49f446..a950289789e 100644 --- a/homeassistant/components/binary_sensor/wink.py +++ b/homeassistant/components/binary_sensor/wink.py @@ -4,7 +4,6 @@ Support for Wink binary sensors. For more details about this platform, please refer to the documentation at at https://home-assistant.io/components/binary_sensor.wink/ """ -import asyncio import logging from homeassistant.components.binary_sensor import BinarySensorDevice @@ -101,8 +100,7 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice): else: self.capability = None - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['binary_sensor'].append(self) diff --git a/homeassistant/components/binary_sensor/workday.py b/homeassistant/components/binary_sensor/workday.py index 1d85d9c9a47..82b5e66629a 100644 --- a/homeassistant/components/binary_sensor/workday.py +++ b/homeassistant/components/binary_sensor/workday.py @@ -4,7 +4,6 @@ Sensor to indicate whether the current day is a workday. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.workday/ """ -import asyncio import logging from datetime import datetime, timedelta @@ -162,8 +161,7 @@ class IsWorkdaySensor(BinarySensorDevice): CONF_OFFSET: self._days_offset } - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get date and look whether it is a holiday.""" # Default is no workday self._state = False diff --git a/homeassistant/components/blink.py b/homeassistant/components/blink.py deleted file mode 100644 index e84643711eb..00000000000 --- a/homeassistant/components/blink.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -Support for Blink Home Camera System. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/blink/ -""" -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, ATTR_FRIENDLY_NAME, ATTR_ARMED) -from homeassistant.helpers import discovery - -REQUIREMENTS = ['blinkpy==0.6.0'] - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'blink' - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string - }) -}, extra=vol.ALLOW_EXTRA) - -ARM_SYSTEM_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ARMED): cv.boolean -}) - -ARM_CAMERA_SCHEMA = vol.Schema({ - vol.Required(ATTR_FRIENDLY_NAME): cv.string, - vol.Optional(ATTR_ARMED): cv.boolean -}) - -SNAP_PICTURE_SCHEMA = vol.Schema({ - vol.Required(ATTR_FRIENDLY_NAME): cv.string -}) - - -class BlinkSystem: - """Blink System class.""" - - def __init__(self, config_info): - """Initialize the system.""" - import blinkpy - self.blink = blinkpy.Blink(username=config_info[DOMAIN][CONF_USERNAME], - password=config_info[DOMAIN][CONF_PASSWORD]) - self.blink.setup_system() - - -def setup(hass, config): - """Set up Blink System.""" - hass.data[DOMAIN] = BlinkSystem(config) - discovery.load_platform(hass, 'camera', DOMAIN, {}, config) - discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) - discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) - - def snap_picture(call): - """Take a picture.""" - cameras = hass.data[DOMAIN].blink.cameras - name = call.data.get(ATTR_FRIENDLY_NAME, '') - if name in cameras: - cameras[name].snap_picture() - - def arm_camera(call): - """Arm a camera.""" - cameras = hass.data[DOMAIN].blink.cameras - name = call.data.get(ATTR_FRIENDLY_NAME, '') - value = call.data.get(ATTR_ARMED, True) - if name in cameras: - cameras[name].set_motion_detect(value) - - def arm_system(call): - """Arm the system.""" - value = call.data.get(ATTR_ARMED, True) - hass.data[DOMAIN].blink.arm = value - hass.data[DOMAIN].blink.refresh() - - hass.services.register( - DOMAIN, 'snap_picture', snap_picture, schema=SNAP_PICTURE_SCHEMA) - hass.services.register( - DOMAIN, 'arm_camera', arm_camera, schema=ARM_CAMERA_SCHEMA) - hass.services.register( - DOMAIN, 'arm_system', arm_system, schema=ARM_SYSTEM_SCHEMA) - - return True diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py new file mode 100644 index 00000000000..1d84b5be113 --- /dev/null +++ b/homeassistant/components/blink/__init__.py @@ -0,0 +1,161 @@ +""" +Support for Blink Home Camera System. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/blink/ +""" +import logging +from datetime import timedelta +import voluptuous as vol + +from homeassistant.helpers import ( + config_validation as cv, discovery) +from homeassistant.const import ( + CONF_USERNAME, CONF_PASSWORD, CONF_NAME, CONF_SCAN_INTERVAL, + CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME, + CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT) + +REQUIREMENTS = ['blinkpy==0.9.0'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'blink' +BLINK_DATA = 'blink' + +CONF_CAMERA = 'camera' +CONF_ALARM_CONTROL_PANEL = 'alarm_control_panel' + +DEFAULT_BRAND = 'Blink' +DEFAULT_ATTRIBUTION = "Data provided by immedia-semi.com" +SIGNAL_UPDATE_BLINK = "blink_update" + +DEFAULT_SCAN_INTERVAL = timedelta(seconds=60) + +TYPE_CAMERA_ARMED = 'motion_enabled' +TYPE_MOTION_DETECTED = 'motion_detected' +TYPE_TEMPERATURE = 'temperature' +TYPE_BATTERY = 'battery' +TYPE_WIFI_STRENGTH = 'wifi_strength' +TYPE_STATUS = 'status' + +SERVICE_REFRESH = 'blink_update' +SERVICE_TRIGGER = 'trigger_camera' +SERVICE_SAVE_VIDEO = 'save_video' + +BINARY_SENSORS = { + TYPE_CAMERA_ARMED: ['Camera Armed', 'mdi:verified'], + TYPE_MOTION_DETECTED: ['Motion Detected', 'mdi:run-fast'], +} + +SENSORS = { + TYPE_TEMPERATURE: ['Temperature', TEMP_FAHRENHEIT, 'mdi:thermometer'], + TYPE_BATTERY: ['Battery', '%', 'mdi:battery-80'], + TYPE_WIFI_STRENGTH: ['Wifi Signal', 'dBm', 'mdi:wifi-strength-2'], + TYPE_STATUS: ['Status', '', 'mdi:bell'] +} + +BINARY_SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSORS)): + vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]) +}) + +SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): + vol.All(cv.ensure_list, [vol.In(SENSORS)]) +}) + +SERVICE_TRIGGER_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string +}) + +SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_FILENAME): cv.string, +}) + +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=DEFAULT_SCAN_INTERVAL): + cv.time_period, + vol.Optional(CONF_BINARY_SENSORS, default={}): + BINARY_SENSOR_SCHEMA, + vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, + }) + }, + extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up Blink System.""" + from blinkpy import blinkpy + conf = config[BLINK_DATA] + username = conf[CONF_USERNAME] + password = conf[CONF_PASSWORD] + scan_interval = conf[CONF_SCAN_INTERVAL] + hass.data[BLINK_DATA] = blinkpy.Blink(username=username, + password=password) + hass.data[BLINK_DATA].refresh_rate = scan_interval.total_seconds() + hass.data[BLINK_DATA].start() + + platforms = [ + ('alarm_control_panel', {}), + ('binary_sensor', conf[CONF_BINARY_SENSORS]), + ('camera', {}), + ('sensor', conf[CONF_SENSORS]), + ] + + for component, schema in platforms: + discovery.load_platform(hass, component, DOMAIN, schema, config) + + def trigger_camera(call): + """Trigger a camera.""" + cameras = hass.data[BLINK_DATA].sync.cameras + name = call.data[CONF_NAME] + if name in cameras: + cameras[name].snap_picture() + hass.data[BLINK_DATA].refresh(force_cache=True) + + def blink_refresh(event_time): + """Call blink to refresh info.""" + hass.data[BLINK_DATA].refresh(force_cache=True) + + async def async_save_video(call): + """Call save video service handler.""" + await async_handle_save_video_service(hass, call) + + hass.services.register(DOMAIN, SERVICE_REFRESH, blink_refresh) + hass.services.register(DOMAIN, + SERVICE_TRIGGER, + trigger_camera, + schema=SERVICE_TRIGGER_SCHEMA) + hass.services.register(DOMAIN, + SERVICE_SAVE_VIDEO, + async_save_video, + schema=SERVICE_SAVE_VIDEO_SCHEMA) + return True + + +async def async_handle_save_video_service(hass, call): + """Handle save video service calls.""" + camera_name = call.data[CONF_NAME] + video_path = call.data[CONF_FILENAME] + if not hass.config.is_allowed_path(video_path): + _LOGGER.error( + "Can't write %s, no access to path!", video_path) + return + + def _write_video(camera_name, video_path): + """Call video write.""" + all_cameras = hass.data[BLINK_DATA].sync.cameras + if camera_name in all_cameras: + all_cameras[camera_name].video_to_file(video_path) + + try: + await hass.async_add_executor_job( + _write_video, camera_name, video_path) + except OSError as err: + _LOGGER.error("Can't write image to file: %s", err) diff --git a/homeassistant/components/blink/services.yaml b/homeassistant/components/blink/services.yaml new file mode 100644 index 00000000000..fc042b0d598 --- /dev/null +++ b/homeassistant/components/blink/services.yaml @@ -0,0 +1,21 @@ +# Describes the format for available Blink services + +blink_update: + description: Force a refresh. + +trigger_camera: + description: Request named camera to take new image. + fields: + name: + description: Name of camera to take new image. + example: 'Living Room' + +save_video: + description: Save last recorded video clip to local file. + fields: + name: + description: Name of camera to grab video from. + example: 'Living Room' + filename: + description: Filename to writable path (directory may need to be included in whitelist_dirs in config) + example: '/tmp/video.mp4' diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 12363627003..dce5961d70d 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -14,7 +14,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers.event import track_utc_time_change import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['bimmer_connected==0.5.2'] +REQUIREMENTS = ['bimmer_connected==0.5.3'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index a8a486013d4..5897d972b31 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -83,62 +83,6 @@ class Image: content = attr.ib(type=bytes) -@bind_hass -def turn_off(hass, entity_id=None): - """Turn off camera.""" - hass.add_job(async_turn_off, hass, entity_id) - - -@bind_hass -async def async_turn_off(hass, entity_id=None): - """Turn off camera.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data) - - -@bind_hass -def turn_on(hass, entity_id=None): - """Turn on camera.""" - hass.add_job(async_turn_on, hass, entity_id) - - -@bind_hass -async def async_turn_on(hass, entity_id=None): - """Turn on camera, and set operation mode.""" - data = {} - if entity_id is not None: - data[ATTR_ENTITY_ID] = entity_id - - await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data) - - -@bind_hass -def enable_motion_detection(hass, entity_id=None): - """Enable Motion Detection.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_ENABLE_MOTION, data)) - - -@bind_hass -def disable_motion_detection(hass, entity_id=None): - """Disable Motion Detection.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_DISABLE_MOTION, data)) - - -@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 {} - data[ATTR_FILENAME] = filename - - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_SNAPSHOT, data)) - - @bind_hass async def async_get_image(hass, entity_id, timeout=10): """Fetch an image from a camera entity.""" @@ -239,7 +183,7 @@ async def async_setup(hass, config): """Update tokens of the entities.""" for entity in component.entities: entity.async_update_token() - hass.async_add_job(entity.async_update_ha_state()) + hass.async_create_task(entity.async_update_ha_state()) hass.helpers.event.async_track_time_interval( update_tokens, TOKEN_CHANGE_INTERVAL) @@ -508,27 +452,23 @@ class CameraMjpegStream(CameraView): raise web.HTTPBadRequest() -@callback -def websocket_camera_thumbnail(hass, connection, msg): +@websocket_api.async_response +async def websocket_camera_thumbnail(hass, connection, msg): """Handle get camera thumbnail websocket command. Async friendly. """ - async def send_camera_still(): - """Send a camera still.""" - try: - image = await async_get_image(hass, msg['entity_id']) - connection.send_message_outside(websocket_api.result_message( - msg['id'], { - 'content_type': image.content_type, - 'content': base64.b64encode(image.content).decode('utf-8') - } - )) - except HomeAssistantError: - connection.send_message_outside(websocket_api.error_message( - msg['id'], 'image_fetch_failed', 'Unable to fetch image')) - - hass.async_add_job(send_camera_still()) + try: + image = await async_get_image(hass, msg['entity_id']) + connection.send_message(websocket_api.result_message( + msg['id'], { + 'content_type': image.content_type, + 'content': base64.b64encode(image.content).decode('utf-8') + } + )) + except HomeAssistantError: + connection.send_message(websocket_api.error_message( + msg['id'], 'image_fetch_failed', 'Unable to fetch image')) async def async_handle_snapshot_service(camera, service): diff --git a/homeassistant/components/camera/abode.py b/homeassistant/components/camera/abode.py index fbab1620a39..39681760d4d 100644 --- a/homeassistant/components/camera/abode.py +++ b/homeassistant/components/camera/abode.py @@ -4,7 +4,6 @@ This component provides HA camera support for Abode Security System. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.abode/ """ -import asyncio import logging from datetime import timedelta @@ -51,10 +50,9 @@ class AbodeCamera(AbodeDevice, Camera): self._event = event self._response = None - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe Abode events.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() self.hass.async_add_job( self._data.abode.events.add_timeline_callback, diff --git a/homeassistant/components/camera/amcrest.py b/homeassistant/components/camera/amcrest.py index 9f4b84db2cc..55a9c2e4294 100644 --- a/homeassistant/components/camera/amcrest.py +++ b/homeassistant/components/camera/amcrest.py @@ -4,7 +4,6 @@ This component provides basic support for Amcrest IP cameras. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.amcrest/ """ -import asyncio import logging from homeassistant.components.amcrest import ( @@ -21,9 +20,8 @@ DEPENDENCIES = ['amcrest', 'ffmpeg'] _LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up an Amcrest IP Camera.""" if discovery_info is None: return @@ -57,12 +55,11 @@ class AmcrestCam(Camera): response = self._camera.snapshot(channel=self._resolution) return response.data - @asyncio.coroutine - def handle_async_mjpeg_stream(self, request): + async def handle_async_mjpeg_stream(self, request): """Return an MJPEG stream.""" # The snapshot implementation is handled by the parent class if self._stream_source == STREAM_SOURCE_LIST['snapshot']: - yield from super().handle_async_mjpeg_stream(request) + await super().handle_async_mjpeg_stream(request) return if self._stream_source == STREAM_SOURCE_LIST['mjpeg']: @@ -72,7 +69,7 @@ class AmcrestCam(Camera): stream_coro = websession.get( streaming_url, auth=self._token, timeout=TIMEOUT) - yield from async_aiohttp_proxy_web(self.hass, request, stream_coro) + await async_aiohttp_proxy_web(self.hass, request, stream_coro) else: # streaming via fmpeg @@ -80,13 +77,13 @@ class AmcrestCam(Camera): streaming_url = self._camera.rtsp_url(typeno=self._resolution) stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) - yield from stream.open_camera( + await stream.open_camera( streaming_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): diff --git a/homeassistant/components/camera/blink.py b/homeassistant/components/camera/blink.py index 217849138c3..5a728e92ce3 100644 --- a/homeassistant/components/camera/blink.py +++ b/homeassistant/components/camera/blink.py @@ -4,31 +4,27 @@ Support for Blink system camera. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.blink/ """ -from datetime import timedelta import logging -import requests - -from homeassistant.components.blink import DOMAIN +from homeassistant.components.blink import BLINK_DATA, DEFAULT_BRAND from homeassistant.components.camera import Camera -from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['blink'] -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90) +ATTR_VIDEO_CLIP = 'video' +ATTR_IMAGE = 'image' def setup_platform(hass, config, add_entities, discovery_info=None): """Set up a Blink Camera.""" if discovery_info is None: return - - data = hass.data[DOMAIN].blink - devs = list() - for name in data.cameras: - devs.append(BlinkCamera(hass, config, data, name)) + data = hass.data[BLINK_DATA] + devs = [] + for name, camera in data.sync.cameras.items(): + devs.append(BlinkCamera(data, name, camera)) add_entities(devs) @@ -36,15 +32,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BlinkCamera(Camera): """An implementation of a Blink Camera.""" - def __init__(self, hass, config, data, name): + def __init__(self, data, name, camera): """Initialize a camera.""" super().__init__() self.data = data - self.hass = hass - self._name = name - self.notifications = self.data.cameras[self._name].notifications + self._name = "{} {}".format(BLINK_DATA, name) + self._camera = camera self.response = None - + self.current_image = None + self.last_image = None _LOGGER.debug("Initialized blink camera %s", self._name) @property @@ -52,30 +48,29 @@ class BlinkCamera(Camera): """Return the camera name.""" return self._name - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def request_image(self): - """Request a new image from Blink servers.""" - _LOGGER.debug("Requesting new image from blink servers") - image_url = self.check_for_motion() - header = self.data.cameras[self._name].header - self.response = requests.get(image_url, headers=header, stream=True) + @property + def device_state_attributes(self): + """Return the camera attributes.""" + return self._camera.attributes - def check_for_motion(self): - """Check if motion has been detected since last update.""" - self.data.refresh() - notifs = self.data.cameras[self._name].notifications - if notifs > self.notifications: - # We detected motion at some point - self.data.last_motion() - self.notifications = notifs - # Returning motion image currently not working - # return self.data.cameras[self._name].motion['image'] - elif notifs < self.notifications: - self.notifications = notifs + def enable_motion_detection(self): + """Enable motion detection for the camera.""" + self._camera.set_motion_detect(True) - return self.data.camera_thumbs[self._name] + def disable_motion_detection(self): + """Disable motion detection for the camera.""" + self._camera.set_motion_detect(False) + + @property + def motion_detection_enabled(self): + """Return the state of the camera.""" + return self._camera.armed + + @property + def brand(self): + """Return the camera brand.""" + return DEFAULT_BRAND def camera_image(self): """Return a still image response from the camera.""" - self.request_image() - return self.response.content + return self._camera.image_from_cache.content diff --git a/homeassistant/components/camera/canary.py b/homeassistant/components/camera/canary.py index 9031c27b1a9..b9951d8efa2 100644 --- a/homeassistant/components/camera/canary.py +++ b/homeassistant/components/camera/canary.py @@ -75,35 +75,33 @@ class CanaryCamera(Camera): """Return the camera motion detection status.""" return not self._location.is_recording - @asyncio.coroutine - def async_camera_image(self): + async def async_camera_image(self): """Return a still image response from the camera.""" self.renew_live_stream_session() from haffmpeg import ImageFrame, IMAGE_JPEG ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop) - image = yield from asyncio.shield(ffmpeg.get_image( + image = await asyncio.shield(ffmpeg.get_image( self._live_stream_session.live_stream_url, output_format=IMAGE_JPEG, extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop) return image - @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.""" if self._live_stream_session is None: return from haffmpeg import CameraMjpeg stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) - yield from stream.open_camera( + await stream.open_camera( self._live_stream_session.live_stream_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() @Throttle(MIN_TIME_BETWEEN_SESSION_RENEW) def renew_live_stream_session(self): diff --git a/homeassistant/components/camera/doorbird.py b/homeassistant/components/camera/doorbird.py index 7af3e7634d0..8982e6d0847 100644 --- a/homeassistant/components/camera/doorbird.py +++ b/homeassistant/components/camera/doorbird.py @@ -27,9 +27,8 @@ _LOGGER = logging.getLogger(__name__) _TIMEOUT = 10 # seconds -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the DoorBird camera platform.""" for doorstation in hass.data[DOORBIRD_DOMAIN]: device = doorstation.device @@ -66,8 +65,7 @@ class DoorBirdCamera(Camera): """Get the name of the camera.""" return self._name - @asyncio.coroutine - def async_camera_image(self): + async def async_camera_image(self): """Pull a still image from the camera.""" now = datetime.datetime.now() @@ -77,9 +75,9 @@ class DoorBirdCamera(Camera): try: websession = async_get_clientsession(self.hass) with async_timeout.timeout(_TIMEOUT, loop=self.hass.loop): - response = yield from websession.get(self._url) + response = await websession.get(self._url) - self._last_image = yield from response.read() + self._last_image = await response.read() self._last_update = now return self._last_image except asyncio.TimeoutError: diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index b707c913435..f89e5ff29c2 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -46,9 +46,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up a generic IP Camera.""" async_add_entities([GenericCamera(hass, config)]) @@ -93,8 +92,7 @@ class GenericCamera(Camera): return run_coroutine_threadsafe( self.async_camera_image(), self.hass.loop).result() - @asyncio.coroutine - def async_camera_image(self): + async def async_camera_image(self): """Return a still image response from the camera.""" try: url = self._still_image_url.async_render() @@ -118,7 +116,7 @@ class GenericCamera(Camera): _LOGGER.error("Error getting camera image: %s", error) return self._last_image - self._last_image = yield from self.hass.async_add_job( + self._last_image = await self.hass.async_add_job( fetch) # async else: @@ -126,9 +124,9 @@ class GenericCamera(Camera): websession = async_get_clientsession( self.hass, verify_ssl=self.verify_ssl) with async_timeout.timeout(10, loop=self.hass.loop): - response = yield from websession.get( + response = await websession.get( url, auth=self._auth) - self._last_image = yield from response.read() + self._last_image = await response.read() except asyncio.TimeoutError: _LOGGER.error("Timeout getting camera image") return self._last_image diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index f1917aaf23e..1df06b546cd 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -41,10 +41,17 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up a MJPEG IP Camera.""" + # Filter header errors from urllib3 due to a urllib3 bug + urllib3_logger = logging.getLogger("urllib3.connectionpool") + if not any(isinstance(x, NoHeaderErrorFilter) + for x in urllib3_logger.filters): + urllib3_logger.addFilter( + NoHeaderErrorFilter() + ) + if discovery_info: config = PLATFORM_SCHEMA(discovery_info) async_add_entities([MjpegCamera(config)]) @@ -82,23 +89,22 @@ class MjpegCamera(Camera): self._username, password=self._password ) - @asyncio.coroutine - def async_camera_image(self): + async def async_camera_image(self): """Return a still image response from the camera.""" # DigestAuth is not supported if self._authentication == HTTP_DIGEST_AUTHENTICATION or \ self._still_image_url is None: - image = yield from self.hass.async_add_job( + image = await self.hass.async_add_job( self.camera_image) return image websession = async_get_clientsession(self.hass) try: with async_timeout.timeout(10, loop=self.hass.loop): - response = yield from websession.get( + response = await websession.get( self._still_image_url, auth=self._auth) - image = yield from response.read() + image = await response.read() return image except asyncio.TimeoutError: @@ -141,3 +147,11 @@ class MjpegCamera(Camera): def name(self): """Return the name of this camera.""" return self._name + + +class NoHeaderErrorFilter(logging.Filter): + """Filter out urllib3 Header Parsing Errors due to a urllib3 bug.""" + + def filter(self, record): + """Filter out Header Parsing Errors.""" + return "Failed to parse headers" not in record.getMessage() diff --git a/homeassistant/components/camera/mqtt.py b/homeassistant/components/camera/mqtt.py index 13c1745615d..42ad7d6fa66 100644 --- a/homeassistant/components/camera/mqtt.py +++ b/homeassistant/components/camera/mqtt.py @@ -10,10 +10,13 @@ import logging import voluptuous as vol +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.core import callback -from homeassistant.components import mqtt from homeassistant.const import CONF_NAME +from homeassistant.components import mqtt, camera from homeassistant.components.camera import Camera, PLATFORM_SCHEMA +from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -31,13 +34,26 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the MQTT Camera.""" - if discovery_info is not None: - config = PLATFORM_SCHEMA(discovery_info) +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_entities, discovery_info=None): + """Set up MQTT camera through configuration.yaml.""" + await _async_setup_entity(hass, config, async_add_entities) + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT camera dynamically through MQTT discovery.""" + async def async_discover(discovery_payload): + """Discover and add a MQTT camera.""" + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(hass, config, async_add_entities) + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(camera.DOMAIN, 'mqtt'), + async_discover) + + +async def _async_setup_entity(hass, config, async_add_entities): + """Set up the MQTT Camera.""" async_add_entities([MqttCamera( config.get(CONF_NAME), config.get(CONF_UNIQUE_ID), diff --git a/homeassistant/components/camera/onvif.py b/homeassistant/components/camera/onvif.py index 9cf21dca9f9..2576dfa7f92 100644 --- a/homeassistant/components/camera/onvif.py +++ b/homeassistant/components/camera/onvif.py @@ -46,6 +46,7 @@ DIR_LEFT = "LEFT" DIR_RIGHT = "RIGHT" ZOOM_OUT = "ZOOM_OUT" ZOOM_IN = "ZOOM_IN" +PTZ_NONE = "NONE" SERVICE_PTZ = "onvif_ptz" @@ -65,9 +66,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ SERVICE_PTZ_SCHEMA = vol.Schema({ ATTR_ENTITY_ID: cv.entity_ids, - ATTR_PAN: vol.In([DIR_LEFT, DIR_RIGHT]), - ATTR_TILT: vol.In([DIR_UP, DIR_DOWN]), - ATTR_ZOOM: vol.In([ZOOM_OUT, ZOOM_IN]) + ATTR_PAN: vol.In([DIR_LEFT, DIR_RIGHT, PTZ_NONE]), + ATTR_TILT: vol.In([DIR_UP, DIR_DOWN, PTZ_NONE]), + ATTR_ZOOM: vol.In([ZOOM_OUT, ZOOM_IN, PTZ_NONE]) }) diff --git a/homeassistant/components/camera/ring.py b/homeassistant/components/camera/ring.py index d0cb6443fc7..ae886bd0669 100644 --- a/homeassistant/components/camera/ring.py +++ b/homeassistant/components/camera/ring.py @@ -110,8 +110,7 @@ class RingCam(Camera): 'video_url': self._video_url, } - @asyncio.coroutine - def async_camera_image(self): + async def async_camera_image(self): """Return a still image response from the camera.""" from haffmpeg import ImageFrame, IMAGE_JPEG ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop) @@ -119,13 +118,12 @@ class RingCam(Camera): if self._video_url is None: return - image = yield from asyncio.shield(ffmpeg.get_image( + image = await asyncio.shield(ffmpeg.get_image( self._video_url, output_format=IMAGE_JPEG, extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop) return image - @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 @@ -133,13 +131,13 @@ class RingCam(Camera): return stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) - yield from stream.open_camera( + await stream.open_camera( self._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 should_poll(self): diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index 3e587fff234..b504fe34d86 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -4,7 +4,6 @@ Support for Synology Surveillance Station Cameras. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.synology/ """ -import asyncio import logging import requests @@ -38,9 +37,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up a Synology IP Camera.""" verify_ssl = config.get(CONF_VERIFY_SSL) timeout = config.get(CONF_TIMEOUT) @@ -87,15 +85,14 @@ class SynologyCamera(Camera): """Return bytes of camera image.""" return self._surveillance.get_camera_image(self._camera_id) - @asyncio.coroutine - def handle_async_mjpeg_stream(self, request): + async def handle_async_mjpeg_stream(self, request): """Return a MJPEG stream image response directly from the camera.""" streaming_url = self._camera.video_stream_url websession = async_get_clientsession(self.hass, self._verify_ssl) stream_coro = websession.get(streaming_url) - yield from async_aiohttp_proxy_web(self.hass, request, stream_coro) + await async_aiohttp_proxy_web(self.hass, request, stream_coro) @property def name(self): diff --git a/homeassistant/components/cast/.translations/ru.json b/homeassistant/components/cast/.translations/ru.json index 9c9353da37e..da03eae701d 100644 --- a/homeassistant/components/cast/.translations/ru.json +++ b/homeassistant/components/cast/.translations/ru.json @@ -2,11 +2,11 @@ "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." + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "step": { "confirm": { - "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Google Cast?", + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Google Cast?", "title": "Google Cast" } }, diff --git a/homeassistant/components/cast/.translations/zh-Hant.json b/homeassistant/components/cast/.translations/zh-Hant.json index 711ac320397..d5383fb1a2b 100644 --- a/homeassistant/components/cast/.translations/zh-Hant.json +++ b/homeassistant/components/cast/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Google Cast \u8a2d\u5099\u3002", + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Google Cast \u88dd\u7f6e\u3002", "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 Google Cast \u5373\u53ef\u3002" }, "step": { diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index a3273f67cc2..98483c454bc 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -10,7 +10,6 @@ import functools as ft import voluptuous as vol -from homeassistant.loader import bind_hass from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.util.temperature import convert as convert_temperature from homeassistant.helpers.entity_component import EntityComponent @@ -20,7 +19,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS, PRECISION_WHOLE, - PRECISION_TENTHS, ) + PRECISION_TENTHS) DEFAULT_MIN_TEMP = 7 DEFAULT_MAX_TEMP = 35 @@ -142,107 +141,6 @@ SET_SWING_MODE_SCHEMA = vol.Schema({ }) -@bind_hass -def set_away_mode(hass, away_mode, entity_id=None): - """Turn all or specified climate devices away mode on.""" - data = { - ATTR_AWAY_MODE: away_mode - } - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_AWAY_MODE, data) - - -@bind_hass -def set_hold_mode(hass, hold_mode, entity_id=None): - """Set new hold mode.""" - data = { - ATTR_HOLD_MODE: hold_mode - } - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_HOLD_MODE, data) - - -@bind_hass -def set_aux_heat(hass, aux_heat, entity_id=None): - """Turn all or specified climate devices auxiliary heater on.""" - data = { - ATTR_AUX_HEAT: aux_heat - } - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data) - - -@bind_hass -def set_temperature(hass, temperature=None, entity_id=None, - target_temp_high=None, target_temp_low=None, - operation_mode=None): - """Set new target temperature.""" - kwargs = { - key: value for key, value in [ - (ATTR_TEMPERATURE, temperature), - (ATTR_TARGET_TEMP_HIGH, target_temp_high), - (ATTR_TARGET_TEMP_LOW, target_temp_low), - (ATTR_ENTITY_ID, entity_id), - (ATTR_OPERATION_MODE, operation_mode) - ] if value is not None - } - _LOGGER.debug("set_temperature start data=%s", kwargs) - hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, kwargs) - - -@bind_hass -def set_humidity(hass, humidity, entity_id=None): - """Set new target humidity.""" - data = {ATTR_HUMIDITY: humidity} - - if entity_id is not None: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_HUMIDITY, data) - - -@bind_hass -def set_fan_mode(hass, fan, entity_id=None): - """Set all or specified climate devices fan mode on.""" - data = {ATTR_FAN_MODE: fan} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_FAN_MODE, data) - - -@bind_hass -def set_operation_mode(hass, operation_mode, entity_id=None): - """Set new target operation mode.""" - data = {ATTR_OPERATION_MODE: operation_mode} - - if entity_id is not None: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, data) - - -@bind_hass -def set_swing_mode(hass, swing_mode, entity_id=None): - """Set new target swing mode.""" - data = {ATTR_SWING_MODE: swing_mode} - - if entity_id is not None: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data) - - async def async_setup(hass, config): """Set up climate devices.""" component = hass.data[DOMAIN] = \ diff --git a/homeassistant/components/climate/evohome.py b/homeassistant/components/climate/evohome.py new file mode 100644 index 00000000000..f0631228fd8 --- /dev/null +++ b/homeassistant/components/climate/evohome.py @@ -0,0 +1,371 @@ +"""Support for Honeywell evohome (EMEA/EU-based systems only). + +Support for a temperature control system (TCS, controller) with 0+ heating +zones (e.g. TRVs, relays) and, optionally, a DHW controller. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/climate.evohome/ +""" + +from datetime import datetime, timedelta +import logging + +from requests.exceptions import HTTPError + +from homeassistant.components.climate import ( + ClimateDevice, + STATE_AUTO, + STATE_ECO, + STATE_OFF, + SUPPORT_OPERATION_MODE, + SUPPORT_AWAY_MODE, +) +from homeassistant.components.evohome import ( + CONF_LOCATION_IDX, + DATA_EVOHOME, + MAX_TEMP, + MIN_TEMP, + SCAN_INTERVAL_MAX +) +from homeassistant.const import ( + CONF_SCAN_INTERVAL, + PRECISION_TENTHS, + TEMP_CELSIUS, + HTTP_TOO_MANY_REQUESTS, +) +_LOGGER = logging.getLogger(__name__) + +# these are for the controller's opmode/state and the zone's state +EVO_RESET = 'AutoWithReset' +EVO_AUTO = 'Auto' +EVO_AUTOECO = 'AutoWithEco' +EVO_AWAY = 'Away' +EVO_DAYOFF = 'DayOff' +EVO_CUSTOM = 'Custom' +EVO_HEATOFF = 'HeatingOff' + +EVO_STATE_TO_HA = { + EVO_RESET: STATE_AUTO, + EVO_AUTO: STATE_AUTO, + EVO_AUTOECO: STATE_ECO, + EVO_AWAY: STATE_AUTO, + EVO_DAYOFF: STATE_AUTO, + EVO_CUSTOM: STATE_AUTO, + EVO_HEATOFF: STATE_OFF +} + +HA_STATE_TO_EVO = { + STATE_AUTO: EVO_AUTO, + STATE_ECO: EVO_AUTOECO, + STATE_OFF: EVO_HEATOFF +} + +HA_OP_LIST = list(HA_STATE_TO_EVO) + +# these are used to help prevent E501 (line too long) violations +GWS = 'gateways' +TCS = 'temperatureControlSystems' + +# debug codes - these happen occasionally, but the cause is unknown +EVO_DEBUG_NO_RECENT_UPDATES = '0x01' +EVO_DEBUG_NO_STATUS = '0x02' + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Create a Honeywell (EMEA/EU) evohome CH/DHW system. + + An evohome system consists of: a controller, with 0-12 heating zones (e.g. + TRVs, relays) and, optionally, a DHW controller (a HW boiler). + + Here, we add the controller only. + """ + evo_data = hass.data[DATA_EVOHOME] + + client = evo_data['client'] + loc_idx = evo_data['params'][CONF_LOCATION_IDX] + + # evohomeclient has no defined way of accessing non-default location other + # than using a protected member, such as below + tcs_obj_ref = client.locations[loc_idx]._gateways[0]._control_systems[0] # noqa E501; pylint: disable=protected-access + + _LOGGER.debug( + "setup_platform(): Found Controller: id: %s [%s], type: %s", + tcs_obj_ref.systemId, + tcs_obj_ref.location.name, + tcs_obj_ref.modelType + ) + parent = EvoController(evo_data, client, tcs_obj_ref) + add_entities([parent], update_before_add=True) + + +class EvoController(ClimateDevice): + """Base for a Honeywell evohome hub/Controller device. + + The Controller (aka TCS, temperature control system) is the parent of all + the child (CH/DHW) devices. + """ + + def __init__(self, evo_data, client, obj_ref): + """Initialize the evohome entity. + + Most read-only properties are set here. So are pseudo read-only, + for example name (which _could_ change between update()s). + """ + self.client = client + self._obj = obj_ref + + self._id = obj_ref.systemId + self._name = evo_data['config']['locationInfo']['name'] + + self._config = evo_data['config'][GWS][0][TCS][0] + self._params = evo_data['params'] + self._timers = evo_data['timers'] + + self._timers['statusUpdated'] = datetime.min + self._status = {} + + self._available = False # should become True after first update() + + def _handle_requests_exceptions(self, err): + # evohomeclient v2 api (>=0.2.7) exposes requests exceptions, incl.: + # - HTTP_BAD_REQUEST, is usually Bad user credentials + # - HTTP_TOO_MANY_REQUESTS, is api usuage limit exceeded + # - HTTP_SERVICE_UNAVAILABLE, is often Vendor's fault + + if err.response.status_code == HTTP_TOO_MANY_REQUESTS: + # execute a back off: pause, and reduce rate + old_scan_interval = self._params[CONF_SCAN_INTERVAL] + new_scan_interval = min(old_scan_interval * 2, SCAN_INTERVAL_MAX) + self._params[CONF_SCAN_INTERVAL] = new_scan_interval + + _LOGGER.warning( + "API rate limit has been exceeded: increasing '%s' from %s to " + "%s seconds, and suspending polling for %s seconds.", + CONF_SCAN_INTERVAL, + old_scan_interval, + new_scan_interval, + new_scan_interval * 3 + ) + + self._timers['statusUpdated'] = datetime.now() + \ + timedelta(seconds=new_scan_interval * 3) + + else: + raise err + + @property + def name(self): + """Return the name to use in the frontend UI.""" + return self._name + + @property + def available(self): + """Return True if the device is available. + + All evohome entities are initially unavailable. Once HA has started, + state data is then retrieved by the Controller, and then the children + will get a state (e.g. operating_mode, current_temperature). + + However, evohome entities can become unavailable for other reasons. + """ + return self._available + + @property + def supported_features(self): + """Get the list of supported features of the Controller.""" + return SUPPORT_OPERATION_MODE | SUPPORT_AWAY_MODE + + @property + def device_state_attributes(self): + """Return the device state attributes of the controller. + + This is operating mode state data that is not available otherwise, due + to the restrictions placed upon ClimateDevice properties, etc by HA. + """ + data = {} + data['systemMode'] = self._status['systemModeStatus']['mode'] + data['isPermanent'] = self._status['systemModeStatus']['isPermanent'] + if 'timeUntil' in self._status['systemModeStatus']: + data['timeUntil'] = self._status['systemModeStatus']['timeUntil'] + data['activeFaults'] = self._status['activeFaults'] + return data + + @property + def operation_list(self): + """Return the list of available operations.""" + return HA_OP_LIST + + @property + def current_operation(self): + """Return the operation mode of the evohome entity.""" + return EVO_STATE_TO_HA.get(self._status['systemModeStatus']['mode']) + + @property + def target_temperature(self): + """Return the average target temperature of the Heating/DHW zones.""" + temps = [zone['setpointStatus']['targetHeatTemperature'] + for zone in self._status['zones']] + + avg_temp = round(sum(temps) / len(temps), 1) if temps else None + return avg_temp + + @property + def current_temperature(self): + """Return the average current temperature of the Heating/DHW zones.""" + tmp_list = [x for x in self._status['zones'] + if x['temperatureStatus']['isAvailable'] is True] + temps = [zone['temperatureStatus']['temperature'] for zone in tmp_list] + + avg_temp = round(sum(temps) / len(temps), 1) if temps else None + return avg_temp + + @property + def temperature_unit(self): + """Return the temperature unit to use in the frontend UI.""" + return TEMP_CELSIUS + + @property + def precision(self): + """Return the temperature precision to use in the frontend UI.""" + return PRECISION_TENTHS + + @property + def min_temp(self): + """Return the minimum target temp (setpoint) of a evohome entity.""" + return MIN_TEMP + + @property + def max_temp(self): + """Return the maximum target temp (setpoint) of a evohome entity.""" + return MAX_TEMP + + @property + def is_on(self): + """Return true as evohome controllers are always on. + + Operating modes can include 'HeatingOff', but (for example) DHW would + remain on. + """ + return True + + @property + def is_away_mode_on(self): + """Return true if away mode is on.""" + return self._status['systemModeStatus']['mode'] == EVO_AWAY + + def turn_away_mode_on(self): + """Turn away mode on.""" + self._set_operation_mode(EVO_AWAY) + + def turn_away_mode_off(self): + """Turn away mode off.""" + self._set_operation_mode(EVO_AUTO) + + def _set_operation_mode(self, operation_mode): + # Set new target operation mode for the TCS. + _LOGGER.debug( + "_set_operation_mode(): API call [1 request(s)]: " + "tcs._set_status(%s)...", + operation_mode + ) + try: + self._obj._set_status(operation_mode) # noqa: E501; pylint: disable=protected-access + except HTTPError as err: + self._handle_requests_exceptions(err) + + def set_operation_mode(self, operation_mode): + """Set new target operation mode for the TCS. + + Currently limited to 'Auto', 'AutoWithEco' & 'HeatingOff'. If 'Away' + mode is needed, it can be enabled via turn_away_mode_on method. + """ + self._set_operation_mode(HA_STATE_TO_EVO.get(operation_mode)) + + def _update_state_data(self, evo_data): + client = evo_data['client'] + loc_idx = evo_data['params'][CONF_LOCATION_IDX] + + _LOGGER.debug( + "_update_state_data(): API call [1 request(s)]: " + "client.locations[loc_idx].status()..." + ) + + try: + evo_data['status'].update( + client.locations[loc_idx].status()[GWS][0][TCS][0]) + except HTTPError as err: # check if we've exceeded the api rate limit + self._handle_requests_exceptions(err) + else: + evo_data['timers']['statusUpdated'] = datetime.now() + + _LOGGER.debug( + "_update_state_data(): evo_data['status'] = %s", + evo_data['status'] + ) + + def update(self): + """Get the latest state data of the installation. + + This includes state data for the Controller and its child devices, such + as the operating_mode of the Controller and the current_temperature + of its children. + + This is not asyncio-friendly due to the underlying client api. + """ + evo_data = self.hass.data[DATA_EVOHOME] + + timeout = datetime.now() + timedelta(seconds=55) + expired = timeout > self._timers['statusUpdated'] + \ + timedelta(seconds=evo_data['params'][CONF_SCAN_INTERVAL]) + + if not expired: + return + + was_available = self._available or \ + self._timers['statusUpdated'] == datetime.min + + self._update_state_data(evo_data) + self._status = evo_data['status'] + + if _LOGGER.isEnabledFor(logging.DEBUG): + tmp_dict = dict(self._status) + if 'zones' in tmp_dict: + tmp_dict['zones'] = '...' + if 'dhw' in tmp_dict: + tmp_dict['dhw'] = '...' + + _LOGGER.debug( + "update(%s), self._status = %s", + self._id + " [" + self._name + "]", + tmp_dict + ) + + no_recent_updates = self._timers['statusUpdated'] < datetime.now() - \ + timedelta(seconds=self._params[CONF_SCAN_INTERVAL] * 3.1) + + if no_recent_updates: + self._available = False + debug_code = EVO_DEBUG_NO_RECENT_UPDATES + + elif not self._status: + # unavailable because no status (but how? other than at startup?) + self._available = False + debug_code = EVO_DEBUG_NO_STATUS + + else: + self._available = True + + if not self._available and was_available: + # only warn if available went from True to False + _LOGGER.warning( + "The entity, %s, has become unavailable, debug code is: %s", + self._id + " [" + self._name + "]", + debug_code + ) + + elif self._available and not was_available: + # this isn't the first re-available (e.g. _after_ STARTUP) + _LOGGER.debug( + "The entity, %s, has become available", + self._id + " [" + self._name + "]" + ) diff --git a/homeassistant/components/climate/fritzbox.py b/homeassistant/components/climate/fritzbox.py index 3eedb89a3b7..f2d13ee92f6 100644 --- a/homeassistant/components/climate/fritzbox.py +++ b/homeassistant/components/climate/fritzbox.py @@ -10,13 +10,15 @@ import requests from homeassistant.components.fritzbox import DOMAIN as FRITZBOX_DOMAIN from homeassistant.components.fritzbox import ( - ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_BATTERY_LOW, ATTR_STATE_LOCKED) + ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_BATTERY_LOW, ATTR_STATE_HOLIDAY_MODE, + ATTR_STATE_LOCKED, ATTR_STATE_SUMMER_MODE, + ATTR_STATE_WINDOW_OPEN) from homeassistant.components.climate import ( ATTR_OPERATION_MODE, ClimateDevice, STATE_ECO, STATE_HEAT, STATE_MANUAL, STATE_OFF, STATE_ON, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE) from homeassistant.const import ( - ATTR_TEMPERATURE, PRECISION_HALVES, TEMP_CELSIUS) + ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, PRECISION_HALVES, TEMP_CELSIUS) DEPENDENCIES = ['fritzbox'] _LOGGER = logging.getLogger(__name__) @@ -151,10 +153,21 @@ class FritzboxThermostat(ClimateDevice): def device_state_attributes(self): """Return the device specific state attributes.""" attrs = { + ATTR_STATE_BATTERY_LOW: self._device.battery_low, ATTR_STATE_DEVICE_LOCKED: self._device.device_lock, ATTR_STATE_LOCKED: self._device.lock, - ATTR_STATE_BATTERY_LOW: self._device.battery_low, } + + # the following attributes are available since fritzos 7 + if self._device.battery_level is not None: + attrs[ATTR_BATTERY_LEVEL] = self._device.battery_level + if self._device.holiday_active is not None: + attrs[ATTR_STATE_HOLIDAY_MODE] = self._device.holiday_active + if self._device.summer_active is not None: + attrs[ATTR_STATE_SUMMER_MODE] = self._device.summer_active + if ATTR_STATE_WINDOW_OPEN is not None: + attrs[ATTR_STATE_WINDOW_OPEN] = self._device.window_open + return attrs def update(self): diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index 85879b8122a..258699ff90a 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -67,9 +67,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the generic thermostat platform.""" name = config.get(CONF_NAME) heater_entity_id = config.get(CONF_HEATER) @@ -147,12 +146,10 @@ class GenericThermostat(ClimateDevice): if sensor_state and sensor_state.state != STATE_UNKNOWN: self._async_update_temp(sensor_state) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Run when entity about to be added.""" # Check If we have an old state - old_state = yield from async_get_last_state(self.hass, - self.entity_id) + old_state = await async_get_last_state(self.hass, self.entity_id) if old_state is not None: # If we have no initial temperature, restore if self._target_temp is None: diff --git a/homeassistant/components/climate/honeywell.py b/homeassistant/components/climate/honeywell.py index 6d54695fa7a..c445a495073 100644 --- a/homeassistant/components/climate/honeywell.py +++ b/homeassistant/components/climate/honeywell.py @@ -20,7 +20,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE, CONF_REGION) -REQUIREMENTS = ['evohomeclient==0.2.5', 'somecomfort==0.5.2'] +REQUIREMENTS = ['evohomeclient==0.2.7', 'somecomfort==0.5.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py index 9e227e002b5..79c49db7955 100644 --- a/homeassistant/components/climate/mqtt.py +++ b/homeassistant/components/climate/mqtt.py @@ -4,13 +4,12 @@ Support for MQTT climate devices. For more details about this platform, please refer to the documentation https://home-assistant.io/components/climate.mqtt/ """ -import asyncio import logging import voluptuous as vol from homeassistant.core import callback -from homeassistant.components import mqtt +from homeassistant.components import mqtt, climate from homeassistant.components.climate import ( STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, ClimateDevice, @@ -21,9 +20,13 @@ from homeassistant.components.climate import ( from homeassistant.const import ( STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME, CONF_VALUE_TEMPLATE) from homeassistant.components.mqtt import ( - CONF_AVAILABILITY_TOPIC, CONF_QOS, CONF_RETAIN, CONF_PAYLOAD_AVAILABLE, - CONF_PAYLOAD_NOT_AVAILABLE, MQTT_BASE_PLATFORM_SCHEMA, MqttAvailability) + ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_QOS, CONF_RETAIN, + CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, + MQTT_BASE_PLATFORM_SCHEMA, MqttAvailability, MqttDiscoveryUpdate) +from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH) @@ -126,13 +129,28 @@ PLATFORM_SCHEMA = SCHEMA_BASE.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the MQTT climate devices.""" - if discovery_info is not None: - config = PLATFORM_SCHEMA(discovery_info) +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_entities, discovery_info=None): + """Set up MQTT climate device through configuration.yaml.""" + await _async_setup_entity(hass, config, async_add_entities) + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT climate device dynamically through MQTT discovery.""" + async def async_discover(discovery_payload): + """Discover and add a MQTT climate device.""" + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(hass, config, async_add_entities, + discovery_payload[ATTR_DISCOVERY_HASH]) + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(climate.DOMAIN, 'mqtt'), + async_discover) + + +async def _async_setup_entity(hass, config, async_add_entities, + discovery_hash=None): + """Set up the MQTT climate devices.""" template_keys = ( CONF_POWER_STATE_TEMPLATE, CONF_MODE_STATE_TEMPLATE, @@ -194,11 +212,12 @@ def async_setup_platform(hass, config, async_add_entities, config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE), config.get(CONF_MIN_TEMP), - config.get(CONF_MAX_TEMP)) - ]) + config.get(CONF_MAX_TEMP), + discovery_hash, + )]) -class MqttClimate(MqttAvailability, ClimateDevice): +class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice): """Representation of an MQTT climate device.""" def __init__(self, hass, name, topic, value_templates, qos, retain, @@ -207,10 +226,11 @@ class MqttClimate(MqttAvailability, ClimateDevice): current_swing_mode, current_operation, aux, send_if_off, payload_on, payload_off, availability_topic, payload_available, payload_not_available, - min_temp, max_temp): + min_temp, max_temp, discovery_hash): """Initialize the climate device.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_hash) self.hass = hass self._name = name self._topic = topic @@ -235,11 +255,12 @@ class MqttClimate(MqttAvailability, ClimateDevice): self._payload_off = payload_off self._min_temp = min_temp self._max_temp = max_temp + self._discovery_hash = discovery_hash - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Handle being added to home assistant.""" - yield from super().async_added_to_hass() + await MqttAvailability.async_added_to_hass(self) + await MqttDiscoveryUpdate.async_added_to_hass(self) @callback def handle_current_temp_received(topic, payload, qos): @@ -256,7 +277,7 @@ class MqttClimate(MqttAvailability, ClimateDevice): _LOGGER.error("Could not parse temperature from %s", payload) if self._topic[CONF_CURRENT_TEMPERATURE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_CURRENT_TEMPERATURE_TOPIC], handle_current_temp_received, self._qos) @@ -274,7 +295,7 @@ class MqttClimate(MqttAvailability, ClimateDevice): self.async_schedule_update_ha_state() if self._topic[CONF_MODE_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_MODE_STATE_TOPIC], handle_mode_received, self._qos) @@ -293,7 +314,7 @@ class MqttClimate(MqttAvailability, ClimateDevice): _LOGGER.error("Could not parse temperature from %s", payload) if self._topic[CONF_TEMPERATURE_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_TEMPERATURE_STATE_TOPIC], handle_temperature_received, self._qos) @@ -312,7 +333,7 @@ class MqttClimate(MqttAvailability, ClimateDevice): self.async_schedule_update_ha_state() if self._topic[CONF_FAN_MODE_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_FAN_MODE_STATE_TOPIC], handle_fan_mode_received, self._qos) @@ -331,7 +352,7 @@ class MqttClimate(MqttAvailability, ClimateDevice): self.async_schedule_update_ha_state() if self._topic[CONF_SWING_MODE_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_SWING_MODE_STATE_TOPIC], handle_swing_mode_received, self._qos) @@ -357,7 +378,7 @@ class MqttClimate(MqttAvailability, ClimateDevice): self.async_schedule_update_ha_state() if self._topic[CONF_AWAY_MODE_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_AWAY_MODE_STATE_TOPIC], handle_away_mode_received, self._qos) @@ -382,7 +403,7 @@ class MqttClimate(MqttAvailability, ClimateDevice): self.async_schedule_update_ha_state() if self._topic[CONF_AUX_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_AUX_STATE_TOPIC], handle_aux_mode_received, self._qos) @@ -397,7 +418,7 @@ class MqttClimate(MqttAvailability, ClimateDevice): self.async_schedule_update_ha_state() if self._topic[CONF_HOLD_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_HOLD_STATE_TOPIC], handle_hold_mode_received, self._qos) @@ -466,12 +487,11 @@ class MqttClimate(MqttAvailability, ClimateDevice): """Return the list of available fan modes.""" return self._fan_list - @asyncio.coroutine - def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): """Set new target temperatures.""" if kwargs.get(ATTR_OPERATION_MODE) is not None: operation_mode = kwargs.get(ATTR_OPERATION_MODE) - yield from self.async_set_operation_mode(operation_mode) + await self.async_set_operation_mode(operation_mode) if kwargs.get(ATTR_TEMPERATURE) is not None: if self._topic[CONF_TEMPERATURE_STATE_TOPIC] is None: @@ -485,8 +505,7 @@ class MqttClimate(MqttAvailability, ClimateDevice): self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_set_swing_mode(self, swing_mode): + async def async_set_swing_mode(self, swing_mode): """Set new swing mode.""" if self._send_if_off or self._current_operation != STATE_OFF: mqtt.async_publish( @@ -497,8 +516,7 @@ class MqttClimate(MqttAvailability, ClimateDevice): self._current_swing_mode = swing_mode self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode): """Set new target temperature.""" if self._send_if_off or self._current_operation != STATE_OFF: mqtt.async_publish( @@ -509,8 +527,7 @@ class MqttClimate(MqttAvailability, ClimateDevice): self._current_fan_mode = fan_mode self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_set_operation_mode(self, operation_mode) -> None: + async def async_set_operation_mode(self, operation_mode) -> None: """Set new operation mode.""" if self._topic[CONF_POWER_COMMAND_TOPIC] is not None: if (self._current_operation == STATE_OFF and @@ -543,8 +560,7 @@ class MqttClimate(MqttAvailability, ClimateDevice): """List of available swing modes.""" return self._swing_list - @asyncio.coroutine - def async_turn_away_mode_on(self): + async def async_turn_away_mode_on(self): """Turn away mode on.""" if self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None: mqtt.async_publish(self.hass, @@ -555,8 +571,7 @@ class MqttClimate(MqttAvailability, ClimateDevice): self._away = True self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_turn_away_mode_off(self): + async def async_turn_away_mode_off(self): """Turn away mode off.""" if self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None: mqtt.async_publish(self.hass, @@ -567,8 +582,7 @@ class MqttClimate(MqttAvailability, ClimateDevice): self._away = False self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_set_hold_mode(self, hold_mode): + async def async_set_hold_mode(self, hold_mode): """Update hold mode on.""" if self._topic[CONF_HOLD_COMMAND_TOPIC] is not None: mqtt.async_publish(self.hass, @@ -579,8 +593,7 @@ class MqttClimate(MqttAvailability, ClimateDevice): self._hold = hold_mode self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_turn_aux_heat_on(self): + async def async_turn_aux_heat_on(self): """Turn auxiliary heater on.""" if self._topic[CONF_AUX_COMMAND_TOPIC] is not None: mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC], @@ -590,8 +603,7 @@ class MqttClimate(MqttAvailability, ClimateDevice): self._aux = True self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_turn_aux_heat_off(self): + async def async_turn_aux_heat_off(self): """Turn auxiliary heater off.""" if self._topic[CONF_AUX_COMMAND_TOPIC] is not None: mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC], diff --git a/homeassistant/components/climate/radiotherm.py b/homeassistant/components/climate/radiotherm.py index 14cd2a0f02e..f914b9b4762 100644 --- a/homeassistant/components/climate/radiotherm.py +++ b/homeassistant/components/climate/radiotherm.py @@ -4,7 +4,6 @@ Support for Radio Thermostat wifi-enabled home thermostats. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/climate.radiotherm/ """ -import asyncio import datetime import logging @@ -145,8 +144,7 @@ class RadioThermostat(ClimateDevice): """Return the list of supported features.""" return SUPPORT_FLAGS - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" # Set the time on the device. This shouldn't be in the # constructor because it's a network call. We can't put it in diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py index ef33ee8495e..8532c611d25 100644 --- a/homeassistant/components/climate/sensibo.py +++ b/homeassistant/components/climate/sensibo.py @@ -58,9 +58,8 @@ FIELD_TO_FLAG = { } -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up Sensibo devices.""" import pysensibo @@ -70,7 +69,7 @@ def async_setup_platform(hass, config, async_add_entities, devices = [] try: for dev in ( - yield from client.async_get_devices(_INITIAL_FETCH_FIELDS)): + await client.async_get_devices(_INITIAL_FETCH_FIELDS)): if config[CONF_ID] == ALL or dev['id'] in config[CONF_ID]: devices.append(SensiboClimate( client, dev, hass.config.units.temperature_unit)) @@ -82,8 +81,7 @@ def async_setup_platform(hass, config, async_add_entities, if devices: async_add_entities(devices) - @asyncio.coroutine - def async_assume_state(service): + async def async_assume_state(service): """Set state according to external service call..""" entity_ids = service.data.get(ATTR_ENTITY_ID) if entity_ids: @@ -94,12 +92,12 @@ def async_setup_platform(hass, config, async_add_entities, update_tasks = [] for climate in target_climate: - yield from climate.async_assume_state( + await climate.async_assume_state( service.data.get(ATTR_STATE)) update_tasks.append(climate.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) hass.services.async_register( DOMAIN, SERVICE_ASSUME_STATE, async_assume_state, schema=ASSUME_STATE_SCHEMA) @@ -262,8 +260,7 @@ class SensiboClimate(ClimateDevice): """Return unique ID based on Sensibo ID.""" return self._id - @asyncio.coroutine - def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is None: @@ -283,52 +280,46 @@ class SensiboClimate(ClimateDevice): return with async_timeout.timeout(TIMEOUT): - yield from self._client.async_set_ac_state_property( + await self._client.async_set_ac_state_property( self._id, 'targetTemperature', temperature, self._ac_states) - @asyncio.coroutine - def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode): """Set new target fan mode.""" with async_timeout.timeout(TIMEOUT): - yield from self._client.async_set_ac_state_property( + await self._client.async_set_ac_state_property( self._id, 'fanLevel', fan_mode, self._ac_states) - @asyncio.coroutine - def async_set_operation_mode(self, operation_mode): + async def async_set_operation_mode(self, operation_mode): """Set new target operation mode.""" with async_timeout.timeout(TIMEOUT): - yield from self._client.async_set_ac_state_property( + await self._client.async_set_ac_state_property( self._id, 'mode', operation_mode, self._ac_states) - @asyncio.coroutine - def async_set_swing_mode(self, swing_mode): + async def async_set_swing_mode(self, swing_mode): """Set new target swing operation.""" with async_timeout.timeout(TIMEOUT): - yield from self._client.async_set_ac_state_property( + await self._client.async_set_ac_state_property( self._id, 'swing', swing_mode, self._ac_states) - @asyncio.coroutine - def async_turn_on(self): + async def async_turn_on(self): """Turn Sensibo unit on.""" with async_timeout.timeout(TIMEOUT): - yield from self._client.async_set_ac_state_property( + await self._client.async_set_ac_state_property( self._id, 'on', True, self._ac_states) - @asyncio.coroutine - def async_turn_off(self): + async def async_turn_off(self): """Turn Sensibo unit on.""" with async_timeout.timeout(TIMEOUT): - yield from self._client.async_set_ac_state_property( + await self._client.async_set_ac_state_property( self._id, 'on', False, self._ac_states) - @asyncio.coroutine - def async_assume_state(self, state): + async def async_assume_state(self, state): """Set external state.""" change_needed = (state != STATE_OFF and not self.is_on) \ or (state == STATE_OFF and self.is_on) if change_needed: with async_timeout.timeout(TIMEOUT): - yield from self._client.async_set_ac_state_property( + await self._client.async_set_ac_state_property( self._id, 'on', state != STATE_OFF, # value @@ -341,12 +332,11 @@ class SensiboClimate(ClimateDevice): else: self._external_state = state - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Retrieve latest state.""" try: with async_timeout.timeout(TIMEOUT): - data = yield from self._client.async_get_device( + data = await self._client.async_get_device( self._id, _FETCH_FIELDS) self._do_update(data) except aiohttp.client_exceptions.ClientError: diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py index 3013a155380..cb6204d3ba3 100644 --- a/homeassistant/components/climate/wink.py +++ b/homeassistant/components/climate/wink.py @@ -4,7 +4,6 @@ Support for Wink thermostats, Air Conditioners, and Water Heaters. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/climate.wink/ """ -import asyncio import logging from homeassistant.components.climate import ( @@ -92,8 +91,7 @@ class WinkThermostat(WinkDevice, ClimateDevice): """Return the list of supported features.""" return SUPPORT_FLAGS_THERMOSTAT - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['climate'].append(self) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 33a939bf9d0..217b39aff62 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -92,8 +92,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Initialize the Home Assistant cloud.""" if DOMAIN in config: kwargs = dict(config[DOMAIN]) @@ -112,7 +111,7 @@ def async_setup(hass, config): cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, cloud.async_start) - yield from http_api.async_setup(hass) + await http_api.async_setup(hass) return True @@ -226,17 +225,16 @@ class Cloud: 'authorization': self.id_token }) - @asyncio.coroutine - def logout(self): + async def logout(self): """Close connection and remove all credentials.""" - yield from self.iot.disconnect() + await self.iot.disconnect() self.id_token = None self.access_token = None self.refresh_token = None self._gactions_config = None - yield from self.hass.async_add_job( + await self.hass.async_add_job( lambda: os.remove(self.user_info_path)) def write_user_info(self): @@ -313,8 +311,7 @@ class Cloud: self._prefs[STORAGE_ENABLE_ALEXA] = alexa_enabled await self._store.async_save(self._prefs) - @asyncio.coroutine - def _fetch_jwt_keyset(self): + async def _fetch_jwt_keyset(self): """Fetch the JWT keyset for the Cognito instance.""" session = async_get_clientsession(self.hass) url = ("https://cognito-idp.us-east-1.amazonaws.com/" @@ -322,8 +319,8 @@ class Cloud: try: with async_timeout.timeout(10, loop=self.hass.loop): - req = yield from session.get(url) - self.jwt_keyset = yield from req.json() + req = await session.get(url) + self.jwt_keyset = await req.json() return True diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index c81ec38bace..720ca00cf52 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -231,7 +231,7 @@ def websocket_cloud_status(hass, connection, msg): Async friendly. """ cloud = hass.data[DOMAIN] - connection.to_write.put_nowait( + connection.send_message( websocket_api.result_message(msg['id'], _account_data(cloud))) @@ -241,7 +241,7 @@ async def websocket_subscription(hass, connection, msg): cloud = hass.data[DOMAIN] if not cloud.is_logged_in: - connection.to_write.put_nowait(websocket_api.error_message( + connection.send_message(websocket_api.error_message( msg['id'], 'not_logged_in', 'You need to be logged in to the cloud.')) return @@ -250,10 +250,10 @@ async def websocket_subscription(hass, connection, msg): response = await cloud.fetch_subscription_info() if response.status == 200: - connection.send_message_outside(websocket_api.result_message( + connection.send_message(websocket_api.result_message( msg['id'], await response.json())) else: - connection.send_message_outside(websocket_api.error_message( + connection.send_message(websocket_api.error_message( msg['id'], 'request_failed', 'Failed to request subscription')) @@ -263,7 +263,7 @@ async def websocket_update_prefs(hass, connection, msg): cloud = hass.data[DOMAIN] if not cloud.is_logged_in: - connection.to_write.put_nowait(websocket_api.error_message( + connection.send_message(websocket_api.error_message( msg['id'], 'not_logged_in', 'You need to be logged in to the cloud.')) return @@ -273,7 +273,7 @@ async def websocket_update_prefs(hass, connection, msg): changes.pop('type') await cloud.update_preferences(**changes) - connection.send_message_outside(websocket_api.result_message( + connection.send_message(websocket_api.result_message( msg['id'], {'success': True})) diff --git a/homeassistant/components/cloud/iot.py b/homeassistant/components/cloud/iot.py index fd525ed33a8..fe89c263488 100644 --- a/homeassistant/components/cloud/iot.py +++ b/homeassistant/components/cloud/iot.py @@ -79,7 +79,7 @@ class CloudIoT: try: # Sleep 2^tries seconds between retries - self.retry_task = hass.async_add_job(asyncio.sleep( + self.retry_task = hass.async_create_task(asyncio.sleep( 2**min(9, self.tries), loop=hass.loop)) yield from self.retry_task self.retry_task = None @@ -106,7 +106,7 @@ class CloudIoT: 'cloud_subscription_expired') # Don't await it because it will cancel this task - hass.async_add_job(self.cloud.logout()) + hass.async_create_task(self.cloud.logout()) return except auth_api.CloudError as err: _LOGGER.warning("Unable to refresh token: %s", err) diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index df0e2f13ac1..f2cfff1f342 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -52,7 +52,7 @@ async def async_setup(hass, config): """Respond to components being loaded.""" panel_name = event.data.get(ATTR_COMPONENT) if panel_name in ON_DEMAND: - hass.async_add_job(setup_panel(panel_name)) + hass.async_create_task(setup_panel(panel_name)) hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded) @@ -136,7 +136,7 @@ class BaseEditConfigView(HomeAssistantView): await hass.async_add_job(_write, path, current) if self.post_write_hook is not None: - hass.async_add_job(self.post_write_hook(hass)) + hass.async_create_task(self.post_write_hook(hass)) return self.json({ 'result': 'ok', diff --git a/homeassistant/components/config/auth.py b/homeassistant/components/config/auth.py index 6f00b03dedb..fb60b4075ef 100644 --- a/homeassistant/components/config/auth.py +++ b/homeassistant/components/config/auth.py @@ -1,7 +1,6 @@ """Offer API to configure Home Assistant auth.""" import voluptuous as vol -from homeassistant.core import callback from homeassistant.components import websocket_api @@ -40,61 +39,49 @@ async def async_setup(hass): return True -@callback @websocket_api.require_owner -def websocket_list(hass, connection, msg): +@websocket_api.async_response +async def websocket_list(hass, connection, msg): """Return a list of users.""" - async def send_users(): - """Send users.""" - result = [_user_info(u) for u in await hass.auth.async_get_users()] + result = [_user_info(u) for u in await hass.auth.async_get_users()] - connection.send_message_outside( - websocket_api.result_message(msg['id'], result)) - - hass.async_add_job(send_users()) + connection.send_message( + websocket_api.result_message(msg['id'], result)) -@callback @websocket_api.require_owner -def websocket_delete(hass, connection, msg): +@websocket_api.async_response +async def websocket_delete(hass, connection, msg): """Delete a user.""" - async def delete_user(): - """Delete user.""" - if msg['user_id'] == connection.request.get('hass_user').id: - connection.send_message_outside(websocket_api.error_message( - msg['id'], 'no_delete_self', - 'Unable to delete your own account')) - return + if msg['user_id'] == connection.user.id: + connection.send_message(websocket_api.error_message( + msg['id'], 'no_delete_self', + 'Unable to delete your own account')) + return - user = await hass.auth.async_get_user(msg['user_id']) + user = await hass.auth.async_get_user(msg['user_id']) - if not user: - connection.send_message_outside(websocket_api.error_message( - msg['id'], 'not_found', 'User not found')) - return + if not user: + connection.send_message(websocket_api.error_message( + msg['id'], 'not_found', 'User not found')) + return - await hass.auth.async_remove_user(user) + await hass.auth.async_remove_user(user) - connection.send_message_outside( - websocket_api.result_message(msg['id'])) - - hass.async_add_job(delete_user()) + connection.send_message( + websocket_api.result_message(msg['id'])) -@callback @websocket_api.require_owner -def websocket_create(hass, connection, msg): +@websocket_api.async_response +async def websocket_create(hass, connection, msg): """Create a user.""" - async def create_user(): - """Create a user.""" - user = await hass.auth.async_create_user(msg['name']) + user = await hass.auth.async_create_user(msg['name']) - connection.send_message_outside( - websocket_api.result_message(msg['id'], { - 'user': _user_info(user) - })) - - hass.async_add_job(create_user()) + connection.send_message( + websocket_api.result_message(msg['id'], { + 'user': _user_info(user) + })) def _user_info(user): diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py index 960e8f5e7b4..3495a959f49 100644 --- a/homeassistant/components/config/auth_provider_homeassistant.py +++ b/homeassistant/components/config/auth_provider_homeassistant.py @@ -2,8 +2,8 @@ import voluptuous as vol from homeassistant.auth.providers import homeassistant as auth_ha -from homeassistant.core import callback from homeassistant.components import websocket_api +from homeassistant.components.websocket_api.decorators import require_owner WS_TYPE_CREATE = 'config/auth_provider/homeassistant/create' @@ -54,121 +54,109 @@ def _get_provider(hass): raise RuntimeError('Provider not found') -@callback -@websocket_api.require_owner -def websocket_create(hass, connection, msg): +@require_owner +@websocket_api.async_response +async def websocket_create(hass, connection, msg): """Create credentials and attach to a user.""" - async def create_creds(): - """Create credentials.""" - provider = _get_provider(hass) - await provider.async_initialize() + provider = _get_provider(hass) + await provider.async_initialize() - user = await hass.auth.async_get_user(msg['user_id']) + user = await hass.auth.async_get_user(msg['user_id']) - if user is None: - connection.send_message_outside(websocket_api.error_message( - msg['id'], 'not_found', 'User not found')) - return + if user is None: + connection.send_message(websocket_api.error_message( + msg['id'], 'not_found', 'User not found')) + return - if user.system_generated: - connection.send_message_outside(websocket_api.error_message( - msg['id'], 'system_generated', - 'Cannot add credentials to a system generated user.')) - return - - try: - await hass.async_add_executor_job( - provider.data.add_auth, msg['username'], msg['password']) - except auth_ha.InvalidUser: - connection.send_message_outside(websocket_api.error_message( - msg['id'], 'username_exists', 'Username already exists')) - return - - credentials = await provider.async_get_or_create_credentials({ - 'username': msg['username'] - }) - await hass.auth.async_link_user(user, credentials) - - await provider.data.async_save() - connection.to_write.put_nowait(websocket_api.result_message(msg['id'])) - - hass.async_add_job(create_creds()) - - -@callback -@websocket_api.require_owner -def websocket_delete(hass, connection, msg): - """Delete username and related credential.""" - async def delete_creds(): - """Delete user credentials.""" - provider = _get_provider(hass) - await provider.async_initialize() - - credentials = await provider.async_get_or_create_credentials({ - 'username': msg['username'] - }) - - # if not new, an existing credential exists. - # Removing the credential will also remove the auth. - if not credentials.is_new: - await hass.auth.async_remove_credentials(credentials) - - connection.to_write.put_nowait( - websocket_api.result_message(msg['id'])) - return - - try: - provider.data.async_remove_auth(msg['username']) - await provider.data.async_save() - except auth_ha.InvalidUser: - connection.to_write.put_nowait(websocket_api.error_message( - msg['id'], 'auth_not_found', 'Given username was not found.')) - return - - connection.to_write.put_nowait( - websocket_api.result_message(msg['id'])) - - hass.async_add_job(delete_creds()) - - -@callback -def websocket_change_password(hass, connection, msg): - """Change user password.""" - async def change_password(): - """Change user password.""" - user = connection.request.get('hass_user') - if user is None: - connection.send_message_outside(websocket_api.error_message( - msg['id'], 'user_not_found', 'User not found')) - return - - provider = _get_provider(hass) - await provider.async_initialize() - - username = None - for credential in user.credentials: - if credential.auth_provider_type == provider.type: - username = credential.data['username'] - break - - if username is None: - connection.send_message_outside(websocket_api.error_message( - msg['id'], 'credentials_not_found', 'Credentials not found')) - return - - try: - await provider.async_validate_login( - username, msg['current_password']) - except auth_ha.InvalidAuth: - connection.send_message_outside(websocket_api.error_message( - msg['id'], 'invalid_password', 'Invalid password')) - return + if user.system_generated: + connection.send_message(websocket_api.error_message( + msg['id'], 'system_generated', + 'Cannot add credentials to a system generated user.')) + return + try: await hass.async_add_executor_job( - provider.data.change_password, username, msg['new_password']) - await provider.data.async_save() + provider.data.add_auth, msg['username'], msg['password']) + except auth_ha.InvalidUser: + connection.send_message(websocket_api.error_message( + msg['id'], 'username_exists', 'Username already exists')) + return - connection.send_message_outside( + credentials = await provider.async_get_or_create_credentials({ + 'username': msg['username'] + }) + await hass.auth.async_link_user(user, credentials) + + await provider.data.async_save() + connection.send_message(websocket_api.result_message(msg['id'])) + + +@require_owner +@websocket_api.async_response +async def websocket_delete(hass, connection, msg): + """Delete username and related credential.""" + provider = _get_provider(hass) + await provider.async_initialize() + + credentials = await provider.async_get_or_create_credentials({ + 'username': msg['username'] + }) + + # if not new, an existing credential exists. + # Removing the credential will also remove the auth. + if not credentials.is_new: + await hass.auth.async_remove_credentials(credentials) + + connection.send_message( websocket_api.result_message(msg['id'])) + return - hass.async_add_job(change_password()) + try: + provider.data.async_remove_auth(msg['username']) + await provider.data.async_save() + except auth_ha.InvalidUser: + connection.send_message(websocket_api.error_message( + msg['id'], 'auth_not_found', 'Given username was not found.')) + return + + connection.send_message( + websocket_api.result_message(msg['id'])) + + +@websocket_api.async_response +async def websocket_change_password(hass, connection, msg): + """Change user password.""" + user = connection.user + if user is None: + connection.send_message(websocket_api.error_message( + msg['id'], 'user_not_found', 'User not found')) + return + + provider = _get_provider(hass) + await provider.async_initialize() + + username = None + for credential in user.credentials: + if credential.auth_provider_type == provider.type: + username = credential.data['username'] + break + + if username is None: + connection.send_message(websocket_api.error_message( + msg['id'], 'credentials_not_found', 'Credentials not found')) + return + + try: + await provider.async_validate_login( + username, msg['current_password']) + except auth_ha.InvalidAuth: + connection.send_message(websocket_api.error_message( + msg['id'], 'invalid_password', 'Invalid password')) + return + + await hass.async_add_executor_job( + provider.data.change_password, username, msg['new_password']) + await provider.data.async_save() + + connection.send_message( + websocket_api.result_message(msg['id'])) diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 223159eb415..7836cba6cf9 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -1,24 +1,25 @@ """Provide configuration end points for Automations.""" -import asyncio from collections import OrderedDict import uuid -from homeassistant.const import CONF_ID from homeassistant.components.config import EditIdBasedConfigView -from homeassistant.components.automation import ( - PLATFORM_SCHEMA, DOMAIN, async_reload) +from homeassistant.const import CONF_ID, SERVICE_RELOAD +from homeassistant.components.automation import DOMAIN, PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv CONFIG_PATH = 'automations.yaml' -@asyncio.coroutine -def async_setup(hass): +async def async_setup(hass): """Set up the Automation config API.""" + async def hook(hass): + """post_write_hook for Config View that reloads automations.""" + await hass.services.async_call(DOMAIN, SERVICE_RELOAD) + hass.http.register_view(EditAutomationConfigView( DOMAIN, 'config', CONFIG_PATH, cv.string, - PLATFORM_SCHEMA, post_write_hook=async_reload + PLATFORM_SCHEMA, post_write_hook=hook )) return True diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 73b2767be4b..644990d7185 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -1,5 +1,4 @@ """Http views to control the config manager.""" -import asyncio from homeassistant import config_entries, data_entry_flow from homeassistant.components.http import HomeAssistantView @@ -7,8 +6,7 @@ from homeassistant.helpers.data_entry_flow import ( FlowManagerIndexView, FlowManagerResourceView) -@asyncio.coroutine -def async_setup(hass): +async def async_setup(hass): """Enable the Home Assistant views.""" hass.http.register_view(ConfigManagerEntryIndexView) hass.http.register_view(ConfigManagerEntryResourceView) @@ -44,8 +42,7 @@ class ConfigManagerEntryIndexView(HomeAssistantView): url = '/api/config/config_entries/entry' name = 'api:config:config_entries:entry' - @asyncio.coroutine - def get(self, request): + async def get(self, request): """List flows in progress.""" hass = request.app['hass'] return self.json([{ @@ -64,13 +61,12 @@ class ConfigManagerEntryResourceView(HomeAssistantView): url = '/api/config/config_entries/entry/{entry_id}' name = 'api:config:config_entries:entry:resource' - @asyncio.coroutine - def delete(self, request, entry_id): + async def delete(self, request, entry_id): """Delete a config entry.""" hass = request.app['hass'] try: - result = yield from hass.config_entries.async_remove(entry_id) + result = await hass.config_entries.async_remove(entry_id) except config_entries.UnknownEntry: return self.json_message('Invalid entry specified', 404) @@ -83,8 +79,7 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView): url = '/api/config/config_entries/flow' name = 'api:config:config_entries:flow' - @asyncio.coroutine - def get(self, request): + async def get(self, request): """List flows that are in progress but not started by a user. Example of a non-user initiated flow is a discovered Hue hub that @@ -110,7 +105,6 @@ class ConfigManagerAvailableFlowView(HomeAssistantView): url = '/api/config/config_entries/flow_handlers' name = 'api:config:config_entries:flow_handlers' - @asyncio.coroutine - def get(self, request): + async def get(self, request): """List available flow handlers.""" return self.json(config_entries.FLOWS) diff --git a/homeassistant/components/config/core.py b/homeassistant/components/config/core.py index 4ff530ad2bc..ce7675c41f4 100644 --- a/homeassistant/components/config/core.py +++ b/homeassistant/components/config/core.py @@ -1,12 +1,10 @@ """Component to interact with Hassbian tools.""" -import asyncio from homeassistant.components.http import HomeAssistantView from homeassistant.config import async_check_ha_config_file -@asyncio.coroutine -def async_setup(hass): +async def async_setup(hass): """Set up the Hassbian config.""" hass.http.register_view(CheckConfigView) return True @@ -18,10 +16,9 @@ class CheckConfigView(HomeAssistantView): url = '/api/config/core/check_config' name = 'api:config:core:check_config' - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Validate configuration and return results.""" - errors = yield from async_check_ha_config_file(request.app['hass']) + errors = await async_check_ha_config_file(request.app['hass']) state = 'invalid' if errors else 'valid' diff --git a/homeassistant/components/config/customize.py b/homeassistant/components/config/customize.py index d25992ecc90..b7a8c9c070a 100644 --- a/homeassistant/components/config/customize.py +++ b/homeassistant/components/config/customize.py @@ -1,21 +1,24 @@ """Provide configuration end points for Customize.""" -import asyncio from homeassistant.components.config import EditKeyBasedConfigView -from homeassistant.components import async_reload_core_config +from homeassistant.components import SERVICE_RELOAD_CORE_CONFIG from homeassistant.config import DATA_CUSTOMIZE +from homeassistant.core import DOMAIN import homeassistant.helpers.config_validation as cv CONFIG_PATH = 'customize.yaml' -@asyncio.coroutine -def async_setup(hass): +async def async_setup(hass): """Set up the Customize config API.""" + async def hook(hass): + """post_write_hook for Config View that reloads groups.""" + await hass.services.async_call(DOMAIN, SERVICE_RELOAD_CORE_CONFIG) + hass.http.register_view(CustomizeConfigView( 'customize', 'config', CONFIG_PATH, cv.entity_id, dict, - post_write_hook=async_reload_core_config + post_write_hook=hook )) return True diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 88aa5727a97..ecbac703296 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -1,7 +1,6 @@ """HTTP views to interact with the device registry.""" import voluptuous as vol -from homeassistant.core import callback from homeassistant.helpers.device_registry import async_get_registry from homeassistant.components import websocket_api @@ -22,26 +21,19 @@ async def async_setup(hass): return True -@callback -def websocket_list_devices(hass, connection, msg): - """Handle list devices command. - - Async friendly. - """ - async def retrieve_entities(): - """Get devices from registry.""" - registry = await async_get_registry(hass) - connection.send_message_outside(websocket_api.result_message( - msg['id'], [{ - 'config_entries': list(entry.config_entries), - 'connections': list(entry.connections), - 'manufacturer': entry.manufacturer, - 'model': entry.model, - 'name': entry.name, - 'sw_version': entry.sw_version, - 'id': entry.id, - 'hub_device_id': entry.hub_device_id, - } for entry in registry.devices.values()] - )) - - hass.async_add_job(retrieve_entities()) +@websocket_api.async_response +async def websocket_list_devices(hass, connection, msg): + """Handle list devices command.""" + registry = await async_get_registry(hass) + connection.send_message(websocket_api.result_message( + msg['id'], [{ + 'config_entries': list(entry.config_entries), + 'connections': list(entry.connections), + 'manufacturer': entry.manufacturer, + 'model': entry.model, + 'name': entry.name, + 'sw_version': entry.sw_version, + 'id': entry.id, + 'hub_device_id': entry.hub_device_id, + } for entry in registry.devices.values()] + )) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 0f9abf167e5..1ede76d0fd8 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -4,6 +4,8 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.components import websocket_api +from homeassistant.components.websocket_api.const import ERR_NOT_FOUND +from homeassistant.components.websocket_api.decorators import async_response from homeassistant.helpers import config_validation as cv DEPENDENCIES = ['websocket_api'] @@ -46,89 +48,77 @@ async def async_setup(hass): return True -@callback -def websocket_list_entities(hass, connection, msg): +@async_response +async def websocket_list_entities(hass, connection, msg): """Handle list registry entries command. Async friendly. """ - async def retrieve_entities(): - """Get entities from registry.""" - registry = await async_get_registry(hass) - connection.send_message_outside(websocket_api.result_message( - msg['id'], [{ - 'config_entry_id': entry.config_entry_id, - 'device_id': entry.device_id, - 'disabled_by': entry.disabled_by, - 'entity_id': entry.entity_id, - 'name': entry.name, - 'platform': entry.platform, - } for entry in registry.entities.values()] - )) - - hass.async_add_job(retrieve_entities()) + registry = await async_get_registry(hass) + connection.send_message(websocket_api.result_message( + msg['id'], [{ + 'config_entry_id': entry.config_entry_id, + 'device_id': entry.device_id, + 'disabled_by': entry.disabled_by, + 'entity_id': entry.entity_id, + 'name': entry.name, + 'platform': entry.platform, + } for entry in registry.entities.values()] + )) -@callback -def websocket_get_entity(hass, connection, msg): +@async_response +async def websocket_get_entity(hass, connection, msg): """Handle get entity registry entry command. Async friendly. """ - async def retrieve_entity(): - """Get entity from registry.""" - registry = await async_get_registry(hass) - entry = registry.entities.get(msg['entity_id']) + registry = await async_get_registry(hass) + entry = registry.entities.get(msg['entity_id']) - if entry is None: - connection.send_message_outside(websocket_api.error_message( - msg['id'], websocket_api.ERR_NOT_FOUND, 'Entity not found')) - return + if entry is None: + connection.send_message(websocket_api.error_message( + msg['id'], ERR_NOT_FOUND, 'Entity not found')) + return - connection.send_message_outside(websocket_api.result_message( - msg['id'], _entry_dict(entry) - )) - - hass.async_add_job(retrieve_entity()) + connection.send_message(websocket_api.result_message( + msg['id'], _entry_dict(entry) + )) -@callback -def websocket_update_entity(hass, connection, msg): +@async_response +async 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) + registry = await async_get_registry(hass) - 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 + if msg['entity_id'] not in registry.entities: + connection.send_message(websocket_api.error_message( + msg['id'], ERR_NOT_FOUND, 'Entity not found')) + return - changes = {} + changes = {} - if 'name' in msg: - changes['name'] = msg['name'] + if 'name' in msg: + changes['name'] = msg['name'] - if 'new_entity_id' in msg: - changes['new_entity_id'] = msg['new_entity_id'] + if 'new_entity_id' in msg: + changes['new_entity_id'] = msg['new_entity_id'] - try: - if changes: - entry = registry.async_update_entity( - msg['entity_id'], **changes) - except ValueError as err: - connection.send_message_outside(websocket_api.error_message( - msg['id'], 'invalid_info', str(err) - )) - else: - connection.send_message_outside(websocket_api.result_message( - msg['id'], _entry_dict(entry) - )) - - hass.async_create_task(update_entity()) + try: + if changes: + entry = registry.async_update_entity( + msg['entity_id'], **changes) + except ValueError as err: + connection.send_message(websocket_api.error_message( + msg['id'], 'invalid_info', str(err) + )) + else: + connection.send_message(websocket_api.result_message( + msg['id'], _entry_dict(entry) + )) @callback diff --git a/homeassistant/components/config/group.py b/homeassistant/components/config/group.py index 8b327faa95f..f9b9a2c4918 100644 --- a/homeassistant/components/config/group.py +++ b/homeassistant/components/config/group.py @@ -1,5 +1,4 @@ """Provide configuration end points for Groups.""" -import asyncio from homeassistant.const import SERVICE_RELOAD from homeassistant.components.config import EditKeyBasedConfigView from homeassistant.components.group import DOMAIN, GROUP_SCHEMA @@ -9,13 +8,11 @@ import homeassistant.helpers.config_validation as cv CONFIG_PATH = 'groups.yaml' -@asyncio.coroutine -def async_setup(hass): +async def async_setup(hass): """Set up the Group config API.""" - @asyncio.coroutine - def hook(hass): + async def hook(hass): """post_write_hook for Config View that reloads groups.""" - yield from hass.services.async_call(DOMAIN, SERVICE_RELOAD) + await hass.services.async_call(DOMAIN, SERVICE_RELOAD) hass.http.register_view(EditKeyBasedConfigView( 'group', 'config', CONFIG_PATH, cv.slug, GROUP_SCHEMA, diff --git a/homeassistant/components/config/hassbian.py b/homeassistant/components/config/hassbian.py index 8de5f62d915..c475dc317f7 100644 --- a/homeassistant/components/config/hassbian.py +++ b/homeassistant/components/config/hassbian.py @@ -1,5 +1,4 @@ """Component to interact with Hassbian tools.""" -import asyncio import json import os @@ -30,8 +29,7 @@ _TEST_OUTPUT = """ """ # noqa -@asyncio.coroutine -def async_setup(hass): +async def async_setup(hass): """Set up the Hassbian config.""" # Test if is Hassbian test_mode = 'FORCE_HASSBIAN' in os.environ @@ -46,8 +44,7 @@ def async_setup(hass): return True -@asyncio.coroutine -def hassbian_status(hass, test_mode=False): +async def hassbian_status(hass, test_mode=False): """Query for the Hassbian status.""" # Fetch real output when not in test mode if test_mode: @@ -66,10 +63,9 @@ class HassbianSuitesView(HomeAssistantView): """Initialize suites view.""" self._test_mode = test_mode - @asyncio.coroutine - def get(self, request): + async def get(self, request): """Request suite status.""" - inp = yield from hassbian_status(request.app['hass'], self._test_mode) + inp = await hassbian_status(request.app['hass'], self._test_mode) return self.json(inp['suites']) @@ -84,8 +80,7 @@ class HassbianSuiteInstallView(HomeAssistantView): """Initialize suite view.""" self._test_mode = test_mode - @asyncio.coroutine - def post(self, request, suite): + async def post(self, request, suite): """Request suite status.""" # do real install if not in test mode return self.json({"status": "ok"}) diff --git a/homeassistant/components/config/script.py b/homeassistant/components/config/script.py index 345c8e4a849..3adc6f14233 100644 --- a/homeassistant/components/config/script.py +++ b/homeassistant/components/config/script.py @@ -1,19 +1,22 @@ """Provide configuration end points for scripts.""" -import asyncio from homeassistant.components.config import EditKeyBasedConfigView -from homeassistant.components.script import SCRIPT_ENTRY_SCHEMA, async_reload +from homeassistant.components.script import DOMAIN, SCRIPT_ENTRY_SCHEMA +from homeassistant.const import SERVICE_RELOAD import homeassistant.helpers.config_validation as cv CONFIG_PATH = 'scripts.yaml' -@asyncio.coroutine -def async_setup(hass): +async def async_setup(hass): """Set up the script config API.""" + async def hook(hass): + """post_write_hook for Config View that reloads scripts.""" + await hass.services.async_call(DOMAIN, SERVICE_RELOAD) + hass.http.register_view(EditKeyBasedConfigView( 'script', 'config', CONFIG_PATH, cv.slug, SCRIPT_ENTRY_SCHEMA, - post_write_hook=async_reload + post_write_hook=hook )) return True diff --git a/homeassistant/components/config/zwave.py b/homeassistant/components/config/zwave.py index fcdab835052..57123ee12de 100644 --- a/homeassistant/components/config/zwave.py +++ b/homeassistant/components/config/zwave.py @@ -1,5 +1,4 @@ """Provide configuration end points for Z-Wave.""" -import asyncio import logging from collections import deque @@ -16,8 +15,7 @@ CONFIG_PATH = 'zwave_device_config.yaml' OZW_LOG_FILENAME = 'OZW_Log.txt' -@asyncio.coroutine -def async_setup(hass): +async def async_setup(hass): """Set up the Z-Wave config API.""" hass.http.register_view(EditKeyBasedConfigView( 'zwave', 'device_config', CONFIG_PATH, cv.entity_id, @@ -41,8 +39,7 @@ class ZWaveLogView(HomeAssistantView): name = "api:zwave:ozwlog" # pylint: disable=no-self-use - @asyncio.coroutine - def get(self, request): + async def get(self, request): """Retrieve the lines from ZWave log.""" try: lines = int(request.query.get('lines', 0)) @@ -50,7 +47,7 @@ class ZWaveLogView(HomeAssistantView): return Response(text='Invalid datetime', status=400) hass = request.app['hass'] - response = yield from hass.async_add_job(self._get_log, hass, lines) + response = await hass.async_add_job(self._get_log, hass, lines) return Response(text='\n'.join(response)) diff --git a/homeassistant/components/configurator.py b/homeassistant/components/configurator.py index 56fb7b4247b..74d8339b1fa 100644 --- a/homeassistant/components/configurator.py +++ b/homeassistant/components/configurator.py @@ -6,7 +6,6 @@ This will return a request id that has to be used for future calls. A callback has to be provided to `request_config` which will be called when the user has submitted configuration information. """ -import asyncio import functools as ft import logging @@ -122,8 +121,7 @@ def request_done(hass, request_id): ).result() -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the configurator component.""" return True @@ -207,8 +205,7 @@ class Configurator: self.hass.bus.async_listen_once(EVENT_TIME_CHANGED, deferred_remove) - @asyncio.coroutine - def async_handle_service_call(self, call): + async def async_handle_service_call(self, call): """Handle a configure service call.""" request_id = call.data.get(ATTR_CONFIGURE_ID) @@ -220,8 +217,8 @@ class Configurator: # field validation goes here? if callback: - yield from self.hass.async_add_job(callback, - call.data.get(ATTR_FIELDS, {})) + await self.hass.async_add_job(callback, + call.data.get(ATTR_FIELDS, {})) def _generate_unique_id(self): """Generate a unique configurator ID.""" diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index d720819a0ab..d67c93c0d6e 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -9,12 +9,10 @@ import logging import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME -from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import async_get_last_state -from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) @@ -22,6 +20,7 @@ ATTR_INITIAL = 'initial' ATTR_STEP = 'step' CONF_INITIAL = 'initial' +CONF_RESTORE = 'restore' CONF_STEP = 'step' DEFAULT_INITIAL = 0 @@ -45,54 +44,13 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL): cv.positive_int, vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_RESTORE, default=True): cv.boolean, vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.positive_int, }, None) }) }, extra=vol.ALLOW_EXTRA) -@bind_hass -def increment(hass, entity_id): - """Increment a counter.""" - hass.add_job(async_increment, hass, entity_id) - - -@callback -@bind_hass -def async_increment(hass, entity_id): - """Increment a counter.""" - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_INCREMENT, {ATTR_ENTITY_ID: entity_id})) - - -@bind_hass -def decrement(hass, entity_id): - """Decrement a counter.""" - hass.add_job(async_decrement, hass, entity_id) - - -@callback -@bind_hass -def async_decrement(hass, entity_id): - """Decrement a counter.""" - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_DECREMENT, {ATTR_ENTITY_ID: entity_id})) - - -@bind_hass -def reset(hass, entity_id): - """Reset a counter.""" - hass.add_job(async_reset, hass, entity_id) - - -@callback -@bind_hass -def async_reset(hass, entity_id): - """Reset a counter.""" - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_RESET, {ATTR_ENTITY_ID: entity_id})) - - async def async_setup(hass, config): """Set up the counters.""" component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -105,10 +63,11 @@ async def async_setup(hass, config): name = cfg.get(CONF_NAME) initial = cfg.get(CONF_INITIAL) + restore = cfg.get(CONF_RESTORE) step = cfg.get(CONF_STEP) icon = cfg.get(CONF_ICON) - entities.append(Counter(object_id, name, initial, step, icon)) + entities.append(Counter(object_id, name, initial, restore, step, icon)) if not entities: return False @@ -130,10 +89,11 @@ async def async_setup(hass, config): class Counter(Entity): """Representation of a counter.""" - def __init__(self, object_id, name, initial, step, icon): + def __init__(self, object_id, name, initial, restore, step, icon): """Initialize a counter.""" self.entity_id = ENTITY_ID_FORMAT.format(object_id) self._name = name + self._restore = restore self._step = step self._state = self._initial = initial self._icon = icon @@ -168,12 +128,12 @@ class Counter(Entity): async def async_added_to_hass(self): """Call when entity about to be added to Home Assistant.""" - # If not None, we got an initial value. - if self._state is not None: - return - - state = await async_get_last_state(self.hass, self.entity_id) - self._state = state and state.state == state + # __init__ will set self._state to self._initial, only override + # if needed. + if self._restore: + state = await async_get_last_state(self.hass, self.entity_id) + if state is not None: + self._state = int(state.state) async def async_decrement(self): """Decrement the counter.""" diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index e9a33c27d34..ec11b139f6b 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -81,64 +81,6 @@ def is_closed(hass, entity_id=None): return hass.states.is_state(entity_id, STATE_CLOSED) -@bind_hass -def open_cover(hass, entity_id=None): - """Open all or specified cover.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_OPEN_COVER, data) - - -@bind_hass -def close_cover(hass, entity_id=None): - """Close all or specified cover.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_CLOSE_COVER, data) - - -@bind_hass -def set_cover_position(hass, position, entity_id=None): - """Move to specific position all or specified cover.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - data[ATTR_POSITION] = position - hass.services.call(DOMAIN, SERVICE_SET_COVER_POSITION, data) - - -@bind_hass -def stop_cover(hass, entity_id=None): - """Stop all or specified cover.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_STOP_COVER, data) - - -@bind_hass -def open_cover_tilt(hass, entity_id=None): - """Open all or specified cover tilt.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_OPEN_COVER_TILT, data) - - -@bind_hass -def close_cover_tilt(hass, entity_id=None): - """Close all or specified cover tilt.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_CLOSE_COVER_TILT, data) - - -@bind_hass -def set_cover_tilt_position(hass, tilt_position, entity_id=None): - """Move to specific tilt position all or specified cover.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - data[ATTR_TILT_POSITION] = tilt_position - hass.services.call(DOMAIN, SERVICE_SET_COVER_TILT_POSITION, data) - - -@bind_hass -def stop_cover_tilt(hass, entity_id=None): - """Stop all or specified cover tilt.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_STOP_COVER_TILT, data) - - async def async_setup(hass, config): """Track states and offer events for covers.""" component = hass.data[DOMAIN] = EntityComponent( diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 977353cb318..cbc8fbee274 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -5,11 +5,12 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.mqtt/ """ import logging +from typing import Optional import voluptuous as vol from homeassistant.core import callback -from homeassistant.components import mqtt +from homeassistant.components import mqtt, cover from homeassistant.components.cover import ( CoverDevice, ATTR_TILT_POSITION, SUPPORT_OPEN_TILT, SUPPORT_CLOSE_TILT, SUPPORT_STOP_TILT, SUPPORT_SET_TILT_POSITION, @@ -20,10 +21,14 @@ from homeassistant.const import ( CONF_NAME, CONF_VALUE_TEMPLATE, CONF_OPTIMISTIC, STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN) from homeassistant.components.mqtt import ( - CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, - CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, - valid_publish_topic, valid_subscribe_topic, MqttAvailability) + ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, + CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, + CONF_QOS, CONF_RETAIN, valid_publish_topic, valid_subscribe_topic, + MqttAvailability, MqttDiscoveryUpdate) +from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType, ConfigType _LOGGER = logging.getLogger(__name__) @@ -45,6 +50,7 @@ CONF_TILT_MIN = 'tilt_min' CONF_TILT_MAX = 'tilt_max' CONF_TILT_STATE_OPTIMISTIC = 'tilt_optimistic' CONF_TILT_INVERT_STATE = 'tilt_invert_state' +CONF_UNIQUE_ID = 'unique_id' DEFAULT_NAME = 'MQTT Cover' DEFAULT_PAYLOAD_OPEN = 'OPEN' @@ -89,15 +95,32 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ default=DEFAULT_TILT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_TILT_INVERT_STATE, default=DEFAULT_TILT_INVERT_STATE): cv.boolean, + vol.Optional(CONF_UNIQUE_ID): cv.string, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the MQTT Cover.""" - if discovery_info is not None: - config = PLATFORM_SCHEMA(discovery_info) +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_entities, discovery_info=None): + """Set up MQTT cover through configuration.yaml.""" + await _async_setup_entity(hass, config, async_add_entities) + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT cover dynamically through MQTT discovery.""" + async def async_discover(discovery_payload): + """Discover and add an MQTT cover.""" + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(hass, config, async_add_entities, + discovery_payload[ATTR_DISCOVERY_HASH]) + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(cover.DOMAIN, 'mqtt'), + async_discover) + + +async def _async_setup_entity(hass, config, async_add_entities, + discovery_hash=None): + """Set up the MQTT Cover.""" value_template = config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = hass @@ -131,10 +154,12 @@ async def async_setup_platform(hass, config, async_add_entities, config.get(CONF_TILT_INVERT_STATE), config.get(CONF_POSITION_TOPIC), set_position_template, + config.get(CONF_UNIQUE_ID), + discovery_hash )]) -class MqttCover(MqttAvailability, CoverDevice): +class MqttCover(MqttAvailability, MqttDiscoveryUpdate, CoverDevice): """Representation of a cover that can be controlled using MQTT.""" def __init__(self, name, state_topic, command_topic, availability_topic, @@ -143,10 +168,12 @@ class MqttCover(MqttAvailability, CoverDevice): payload_stop, payload_available, payload_not_available, optimistic, value_template, tilt_open_position, tilt_closed_position, tilt_min, tilt_max, tilt_optimistic, - tilt_invert, position_topic, set_position_template): + tilt_invert, position_topic, set_position_template, + unique_id: Optional[str], discovery_hash): """Initialize the cover.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_hash) self._position = None self._state = None self._name = name @@ -172,10 +199,13 @@ class MqttCover(MqttAvailability, CoverDevice): self._tilt_invert = tilt_invert self._position_topic = position_topic self._set_position_template = set_position_template + self._unique_id = unique_id + self._discovery_hash = discovery_hash async def async_added_to_hass(self): """Subscribe MQTT events.""" - await super().async_added_to_hass() + await MqttAvailability.async_added_to_hass(self) + await MqttDiscoveryUpdate.async_added_to_hass(self) @callback def tilt_updated(topic, payload, qos): @@ -387,3 +417,8 @@ class MqttCover(MqttAvailability, CoverDevice): if self._tilt_invert: position = self._tilt_max - position + offset return position + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id diff --git a/homeassistant/components/cover/myq.py b/homeassistant/components/cover/myq.py index 78b6f891f11..5ceb4260d0c 100644 --- a/homeassistant/components/cover/myq.py +++ b/homeassistant/components/cover/myq.py @@ -11,8 +11,8 @@ import voluptuous as vol from homeassistant.components.cover import ( CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN) from homeassistant.const import ( - CONF_PASSWORD, CONF_TYPE, CONF_USERNAME, STATE_CLOSED, STATE_OPEN, - STATE_CLOSING, STATE_OPENING) + CONF_PASSWORD, CONF_TYPE, CONF_USERNAME, STATE_CLOSED, STATE_CLOSING, + STATE_OPEN, STATE_OPENING) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['pymyq==0.0.15'] @@ -23,8 +23,8 @@ DEFAULT_NAME = 'myq' MYQ_TO_HASS = { 'closed': STATE_CLOSED, - 'open': STATE_OPEN, 'closing': STATE_CLOSING, + 'open': STATE_OPEN, 'opening': STATE_OPENING } @@ -76,7 +76,7 @@ class MyQDevice(CoverDevice): self.myq = myq self.device_id = device['deviceid'] self._name = device['name'] - self._status = STATE_CLOSED + self._status = None @property def device_class(self): @@ -96,17 +96,19 @@ class MyQDevice(CoverDevice): @property def is_closed(self): """Return true if cover is closed, else False.""" - return MYQ_TO_HASS[self._status] == STATE_CLOSED + if self._status in [None, False]: + return None + return MYQ_TO_HASS.get(self._status) == STATE_CLOSED @property def is_closing(self): """Return if the cover is closing or not.""" - return MYQ_TO_HASS[self._status] == STATE_CLOSING + return MYQ_TO_HASS.get(self._status) == STATE_CLOSING @property def is_opening(self): """Return if the cover is opening or not.""" - return MYQ_TO_HASS[self._status] == STATE_OPENING + return MYQ_TO_HASS.get(self._status) == STATE_OPENING def close_cover(self, **kwargs): """Issue close command to cover.""" diff --git a/homeassistant/components/deconz/.translations/hu.json b/homeassistant/components/deconz/.translations/hu.json index c1fd76c5035..ca2466e9921 100644 --- a/homeassistant/components/deconz/.translations/hu.json +++ b/homeassistant/components/deconz/.translations/hu.json @@ -19,8 +19,13 @@ "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" + }, + "options": { + "data": { + "allow_clip_sensor": "Virtu\u00e1lis szenzorok import\u00e1l\u00e1s\u00e1nak enged\u00e9lyez\u00e9se" + } } }, - "title": "deCONZ" + "title": "deCONZ Zigbee gateway" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index 56490f67cb3..4cbc9594ead 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u0428\u043b\u044e\u0437 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b", "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 deCONZ" }, @@ -28,6 +28,6 @@ "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" + "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0448\u043b\u044e\u0437 deCONZ" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index 5cd1a14d499..524f68d41bc 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -3,7 +3,7 @@ "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" + "one_instance_only": "\u7d44\u4ef6\u50c5\u652f\u63f4\u4e00\u7d44 deCONZ \u7269\u4ef6" }, "error": { "no_key": "\u7121\u6cd5\u53d6\u5f97 API key" diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index c2c7866148f..8999087a137 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -35,8 +35,7 @@ COMPONENTS_WITH_DEMO_PLATFORM = [ ] -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the demo environment.""" group = hass.components.group configurator = hass.components.configurator @@ -101,7 +100,7 @@ def async_setup(hass, config): {'weblink': {'entities': [{'name': 'Router', 'url': 'http://192.168.1.1'}]}})) - results = yield from asyncio.gather(*tasks, loop=hass.loop) + results = await asyncio.gather(*tasks, loop=hass.loop) if any(not result for result in results): return False @@ -192,7 +191,7 @@ def async_setup(hass, config): 'climate.ecobee', ], view=True)) - results = yield from asyncio.gather(*tasks2, loop=hass.loop) + results = await asyncio.gather(*tasks2, loop=hass.loop) if any(not result for result in results): return False diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index 641ade7308b..40a602056bf 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -4,7 +4,6 @@ Provides functionality to turn on lights based on the states. For more details about this component, please refer to the documentation at https://home-assistant.io/components/device_sun_light_trigger/ """ -import asyncio import logging from datetime import timedelta @@ -12,7 +11,11 @@ import voluptuous as vol from homeassistant.core import callback import homeassistant.util.dt as dt_util -from homeassistant.const import STATE_HOME, STATE_NOT_HOME +from homeassistant.components.light import ( + ATTR_PROFILE, ATTR_TRANSITION, DOMAIN as DOMAIN_LIGHT) +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_HOME, + STATE_NOT_HOME) from homeassistant.helpers.event import ( async_track_point_in_utc_time, async_track_state_change) from homeassistant.helpers.sun import is_up, get_astral_event_next @@ -43,8 +46,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the triggers to control lights based on device presence.""" logger = logging.getLogger(__name__) device_tracker = hass.components.device_tracker @@ -86,9 +88,12 @@ def async_setup(hass, config): """Turn on lights.""" if not device_tracker.is_on() or light.is_on(light_id): return - light.async_turn_on(light_id, - transition=LIGHT_TRANSITION_TIME.seconds, - profile=light_profile) + hass.async_create_task( + hass.services.async_call( + DOMAIN_LIGHT, SERVICE_TURN_ON, { + ATTR_ENTITY_ID: light_id, + ATTR_TRANSITION: LIGHT_TRANSITION_TIME.seconds, + ATTR_PROFILE: light_profile})) def async_turn_on_factory(light_id): """Generate turn on callbacks as factory.""" @@ -138,7 +143,10 @@ def async_setup(hass, config): # Do we need lights? if light_needed: logger.info("Home coming event for %s. Turning lights on", entity) - light.async_turn_on(light_ids, profile=light_profile) + hass.async_create_task( + hass.services.async_call( + DOMAIN_LIGHT, SERVICE_TURN_ON, + {ATTR_ENTITY_ID: light_ids, ATTR_PROFILE: light_profile})) # Are we in the time span were we would turn on the lights # if someone would be home? @@ -151,7 +159,10 @@ def async_setup(hass, config): # when the fading in started and turn it on if so for index, light_id in enumerate(light_ids): if now > start_point + index * LIGHT_TRANSITION_TIME: - light.async_turn_on(light_id) + hass.async_create_task( + hass.services.async_call( + DOMAIN_LIGHT, SERVICE_TURN_ON, + {ATTR_ENTITY_ID: light_id})) else: # If this light didn't happen to be turned on yet so @@ -173,7 +184,9 @@ def async_setup(hass, config): logger.info( "Everyone has left but there are lights on. Turning them off") - light.async_turn_off(light_ids) + hass.async_create_task( + hass.services.async_call( + DOMAIN_LIGHT, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: light_ids})) async_track_state_change( hass, device_group, turn_off_lights_when_all_leave, diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 408672a974f..cbf32b4cd5a 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -15,6 +15,9 @@ from homeassistant.setup import async_prepare_setup_platform from homeassistant.core import callback from homeassistant.loader import bind_hass from homeassistant.components import group, zone +from homeassistant.components.group import ( + ATTR_ADD_ENTITIES, ATTR_ENTITIES, ATTR_OBJECT_ID, ATTR_VISIBLE, + DOMAIN as DOMAIN_GROUP, SERVICE_SET) from homeassistant.components.zone.zone import async_active_zone from homeassistant.config import load_yaml_config_file, async_log_exception from homeassistant.exceptions import HomeAssistantError @@ -31,9 +34,9 @@ from homeassistant.util.yaml import dump from homeassistant.helpers.event import async_track_utc_time_change from homeassistant.const import ( - ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME, CONF_MAC, - DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID, - CONF_ICON, ATTR_ICON, ATTR_NAME) + ATTR_ENTITY_ID, ATTR_GPS_ACCURACY, ATTR_ICON, ATTR_LATITUDE, + ATTR_LONGITUDE, ATTR_NAME, CONF_ICON, CONF_MAC, CONF_NAME, + DEVICE_DEFAULT_NAME, STATE_NOT_HOME, STATE_HOME) _LOGGER = logging.getLogger(__name__) @@ -138,8 +141,7 @@ def see(hass: HomeAssistantType, mac: str = None, dev_id: str = None, hass.services.call(DOMAIN, SERVICE_SEE, data) -@asyncio.coroutine -def async_setup(hass: HomeAssistantType, config: ConfigType): +async def async_setup(hass: HomeAssistantType, config: ConfigType): """Set up the device tracker.""" yaml_path = hass.config.path(YAML_DEVICES) @@ -152,14 +154,13 @@ def async_setup(hass: HomeAssistantType, config: ConfigType): if track_new is None: track_new = defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) - devices = yield from async_load_config(yaml_path, hass, consider_home) + devices = await async_load_config(yaml_path, hass, consider_home) tracker = DeviceTracker( hass, consider_home, track_new, defaults, devices) - @asyncio.coroutine - def async_setup_platform(p_type, p_config, disc_info=None): + async def async_setup_platform(p_type, p_config, disc_info=None): """Set up a device tracker platform.""" - platform = yield from async_prepare_setup_platform( + platform = await async_prepare_setup_platform( hass, config, DOMAIN, p_type) if platform is None: return @@ -169,16 +170,16 @@ def async_setup(hass: HomeAssistantType, config: ConfigType): scanner = None setup = None if hasattr(platform, 'async_get_scanner'): - scanner = yield from platform.async_get_scanner( + scanner = await platform.async_get_scanner( hass, {DOMAIN: p_config}) elif hasattr(platform, 'get_scanner'): - scanner = yield from hass.async_add_job( + scanner = await hass.async_add_job( platform.get_scanner, hass, {DOMAIN: p_config}) elif hasattr(platform, 'async_setup_scanner'): - setup = yield from platform.async_setup_scanner( + setup = await platform.async_setup_scanner( hass, p_config, tracker.async_see, disc_info) elif hasattr(platform, 'setup_scanner'): - setup = yield from hass.async_add_job( + setup = await hass.async_add_job( platform.setup_scanner, hass, p_config, tracker.see, disc_info) else: @@ -199,14 +200,13 @@ def async_setup(hass: HomeAssistantType, config: ConfigType): setup_tasks = [async_setup_platform(p_type, p_config) for p_type, p_config in config_per_platform(config, DOMAIN)] if setup_tasks: - yield from asyncio.wait(setup_tasks, loop=hass.loop) + await asyncio.wait(setup_tasks, loop=hass.loop) tracker.async_setup_group() - @asyncio.coroutine - def async_platform_discovered(platform, info): + async def async_platform_discovered(platform, info): """Load a platform.""" - yield from async_setup_platform(platform, {}, disc_info=info) + await async_setup_platform(platform, {}, disc_info=info) discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered) @@ -214,20 +214,19 @@ def async_setup(hass: HomeAssistantType, config: ConfigType): async_track_utc_time_change( hass, tracker.async_update_stale, second=range(0, 60, 5)) - @asyncio.coroutine - def async_see_service(call): + async def async_see_service(call): """Service to see a device.""" # Temp workaround for iOS, introduced in 0.65 data = dict(call.data) data.pop('hostname', None) data.pop('battery_status', None) - yield from tracker.async_see(**data) + await tracker.async_see(**data) hass.services.async_register( DOMAIN, SERVICE_SEE, async_see_service, SERVICE_SEE_PAYLOAD_SCHEMA) # restore - yield from tracker.async_setup_tracked_device() + await tracker.async_setup_tracked_device() return True @@ -268,8 +267,7 @@ class DeviceTracker: picture, icon, consider_home) ) - @asyncio.coroutine - def async_see( + async def async_see( self, mac: str = None, dev_id: str = None, host_name: str = None, location_name: str = None, gps: GPSType = None, gps_accuracy: int = None, battery: int = None, @@ -292,11 +290,11 @@ class DeviceTracker: device = self.devices.get(dev_id) if device: - yield from device.async_seen( + await device.async_seen( host_name, location_name, gps, gps_accuracy, battery, attributes, source_type, consider_home) if device.track: - yield from device.async_update_ha_state() + await device.async_update_ha_state() return # If no device can be found, create it @@ -310,18 +308,22 @@ class DeviceTracker: if mac is not None: self.mac_to_dev[mac] = device - yield from device.async_seen( + await device.async_seen( host_name, location_name, gps, gps_accuracy, battery, attributes, source_type) if device.track: - yield from device.async_update_ha_state() + await device.async_update_ha_state() # During init, we ignore the group if self.group and self.track_new: - self.group.async_set_group( - util.slugify(GROUP_NAME_ALL_DEVICES), visible=False, - name=GROUP_NAME_ALL_DEVICES, add=[device.entity_id]) + self.hass.async_create_task( + self.hass.async_call( + DOMAIN_GROUP, SERVICE_SET, { + ATTR_OBJECT_ID: util.slugify(GROUP_NAME_ALL_DEVICES), + ATTR_VISIBLE: False, + ATTR_NAME: GROUP_NAME_ALL_DEVICES, + ATTR_ADD_ENTITIES: [device.entity_id]})) self.hass.bus.async_fire(EVENT_NEW_DEVICE, { ATTR_ENTITY_ID: device.entity_id, @@ -354,10 +356,13 @@ class DeviceTracker: entity_ids = [dev.entity_id for dev in self.devices.values() if dev.track] - self.group = self.hass.components.group - self.group.async_set_group( - util.slugify(GROUP_NAME_ALL_DEVICES), visible=False, - name=GROUP_NAME_ALL_DEVICES, entity_ids=entity_ids) + self.hass.async_create_task( + self.hass.services.async_call( + DOMAIN_GROUP, SERVICE_SET, { + ATTR_OBJECT_ID: util.slugify(GROUP_NAME_ALL_DEVICES), + ATTR_VISIBLE: False, + ATTR_NAME: GROUP_NAME_ALL_DEVICES, + ATTR_ENTITIES: entity_ids})) @callback def async_update_stale(self, now: dt_util.dt.datetime): @@ -368,28 +373,26 @@ class DeviceTracker: for device in self.devices.values(): if (device.track and device.last_update_home) and \ device.stale(now): - self.hass.async_add_job(device.async_update_ha_state(True)) + self.hass.async_create_task(device.async_update_ha_state(True)) - @asyncio.coroutine - def async_setup_tracked_device(self): + async def async_setup_tracked_device(self): """Set up all not exists tracked devices. This method is a coroutine. """ - @asyncio.coroutine - def async_init_single_device(dev): + async def async_init_single_device(dev): """Init a single device_tracker entity.""" - yield from dev.async_added_to_hass() - yield from dev.async_update_ha_state() + await dev.async_added_to_hass() + await dev.async_update_ha_state() tasks = [] for device in self.devices.values(): if device.track and not device.last_seen: - tasks.append(self.hass.async_add_job( + tasks.append(self.hass.async_create_task( async_init_single_device(device))) if tasks: - yield from asyncio.wait(tasks, loop=self.hass.loop) + await asyncio.wait(tasks, loop=self.hass.loop) class Device(Entity): @@ -487,12 +490,12 @@ class Device(Entity): """If device should be hidden.""" return self.away_hide and self.state != STATE_HOME - @asyncio.coroutine - def async_seen(self, host_name: str = None, location_name: str = None, - gps: GPSType = None, gps_accuracy=0, battery: int = None, - attributes: dict = None, - source_type: str = SOURCE_TYPE_GPS, - consider_home: timedelta = None): + async def async_seen( + self, host_name: str = None, location_name: str = None, + gps: GPSType = None, gps_accuracy=0, battery: int = None, + attributes: dict = None, + source_type: str = SOURCE_TYPE_GPS, + consider_home: timedelta = None): """Mark the device as seen.""" self.source_type = source_type self.last_seen = dt_util.utcnow() @@ -518,7 +521,7 @@ class Device(Entity): "Could not parse gps value for %s: %s", self.dev_id, gps) # pylint: disable=not-an-iterable - yield from self.async_update() + await self.async_update() def stale(self, now: dt_util.dt.datetime = None): """Return if device state is stale. @@ -528,8 +531,7 @@ class Device(Entity): return self.last_seen and \ (now or dt_util.utcnow()) - self.last_seen > self.consider_home - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update state of entity. This method is a coroutine. @@ -555,10 +557,9 @@ class Device(Entity): self._state = STATE_HOME self.last_update_home = True - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Add an entity.""" - state = yield from async_get_last_state(self.hass, self.entity_id) + state = await async_get_last_state(self.hass, self.entity_id) if not state: return self._state = state.state @@ -621,9 +622,8 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta): async_load_config(path, hass, consider_home), hass.loop).result() -@asyncio.coroutine -def async_load_config(path: str, hass: HomeAssistantType, - consider_home: timedelta): +async def async_load_config(path: str, hass: HomeAssistantType, + consider_home: timedelta): """Load devices from YAML configuration file. This method is a coroutine. @@ -643,7 +643,7 @@ def async_load_config(path: str, hass: HomeAssistantType, try: result = [] try: - devices = yield from hass.async_add_job( + devices = await hass.async_add_job( load_yaml_config_file, path) except HomeAssistantError as err: _LOGGER.error("Unable to load %s: %s", path, str(err)) @@ -720,10 +720,10 @@ def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType, zone_home.attributes[ATTR_LONGITUDE]] kwargs['gps_accuracy'] = 0 - hass.async_add_job(async_see_device(**kwargs)) + hass.async_create_task(async_see_device(**kwargs)) async_track_time_interval(hass, async_device_tracker_scan, interval) - hass.async_add_job(async_device_tracker_scan(None)) + hass.async_create_task(async_device_tracker_scan(None)) def update_config(path: str, dev_id: str, device: Device): diff --git a/homeassistant/components/device_tracker/automatic.py b/homeassistant/components/device_tracker/automatic.py index 4fcc550d7db..9f20eb6d493 100644 --- a/homeassistant/components/device_tracker/automatic.py +++ b/homeassistant/components/device_tracker/automatic.py @@ -113,7 +113,7 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None): # Load the initial vehicle data vehicles = yield from session.get_vehicles() for vehicle in vehicles: - hass.async_add_job(data.load_vehicle(vehicle)) + hass.async_create_task(data.load_vehicle(vehicle)) # Create a task instead of adding a tracking job, since this task will # run until the websocket connection is closed. @@ -188,7 +188,7 @@ class AutomaticAuthCallbackView(HomeAssistantView): code = params['code'] state = params['state'] initialize_callback = hass.data[DATA_CONFIGURING][state] - hass.async_add_job(initialize_callback(code, state)) + hass.async_create_task(initialize_callback(code, state)) return response @@ -209,7 +209,7 @@ class AutomaticData: self.ws_close_requested = False self.client.on_app_event( - lambda name, event: self.hass.async_add_job( + lambda name, event: self.hass.async_create_task( self.handle_event(name, event))) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.ws_close()) diff --git a/homeassistant/components/device_tracker/geofency.py b/homeassistant/components/device_tracker/geofency.py index 7231c5127be..3687571c118 100644 --- a/homeassistant/components/device_tracker/geofency.py +++ b/homeassistant/components/device_tracker/geofency.py @@ -4,7 +4,6 @@ Support for the Geofency platform. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.geofency/ """ -import asyncio from functools import partial import logging @@ -58,10 +57,9 @@ class GeofencyView(HomeAssistantView): self.see = see self.mobile_beacons = [slugify(beacon) for beacon in mobile_beacons] - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Handle Geofency requests.""" - data = yield from request.post() + data = await request.post() hass = request.app['hass'] data = self._validate_data(data) @@ -69,7 +67,7 @@ class GeofencyView(HomeAssistantView): return ("Invalid data", HTTP_UNPROCESSABLE_ENTITY) if self._is_mobile_beacon(data): - return (yield from self._set_location(hass, data, None)) + return await self._set_location(hass, data, None) if data['entry'] == LOCATION_ENTRY: location_name = data['name'] else: @@ -78,7 +76,7 @@ class GeofencyView(HomeAssistantView): data[ATTR_LATITUDE] = data[ATTR_CURRENT_LATITUDE] data[ATTR_LONGITUDE] = data[ATTR_CURRENT_LONGITUDE] - return (yield from self._set_location(hass, data, location_name)) + return await self._set_location(hass, data, location_name) @staticmethod def _validate_data(data): @@ -121,12 +119,11 @@ class GeofencyView(HomeAssistantView): return "{}_{}".format(BEACON_DEV_PREFIX, data['name']) return data['device'] - @asyncio.coroutine - def _set_location(self, hass, data, location_name): + async def _set_location(self, hass, data, location_name): """Fire HA event to set location.""" device = self._device_name(data) - yield from hass.async_add_job( + await hass.async_add_job( partial(self.see, dev_id=device, gps=(data[ATTR_LATITUDE], data[ATTR_LONGITUDE]), location_name=location_name, diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py index 170d3de6800..77f499dcf6b 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -11,13 +11,15 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA, SOURCE_TYPE_GPS) -from homeassistant.const import ATTR_ID, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + ATTR_ID, CONF_PASSWORD, CONF_USERNAME, ATTR_BATTERY_CHARGING, + ATTR_BATTERY_LEVEL) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify, dt as dt_util -REQUIREMENTS = ['locationsharinglib==2.0.11'] +REQUIREMENTS = ['locationsharinglib==3.0.3'] _LOGGER = logging.getLogger(__name__) @@ -94,6 +96,8 @@ class GoogleMapsScanner: ATTR_ID: person.id, ATTR_LAST_SEEN: dt_util.as_utc(person.datetime), ATTR_NICKNAME: person.nickname, + ATTR_BATTERY_CHARGING: person.charging, + ATTR_BATTERY_LEVEL: person.battery_level } self.see( dev_id=dev_id, diff --git a/homeassistant/components/device_tracker/gpslogger.py b/homeassistant/components/device_tracker/gpslogger.py index 6336ba51d23..f39684aa834 100644 --- a/homeassistant/components/device_tracker/gpslogger.py +++ b/homeassistant/components/device_tracker/gpslogger.py @@ -98,7 +98,7 @@ class GPSLoggerView(HomeAssistantView): if 'activity' in data: attrs['activity'] = data['activity'] - hass.async_add_job(self.async_see( + hass.async_create_task(self.async_see( dev_id=device, gps=gps_location, battery=battery, gps_accuracy=accuracy, diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index 354d3b0980c..aa91f0d3d71 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -4,7 +4,6 @@ Support for the Locative platform. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.locative/ """ -import asyncio from functools import partial import logging @@ -38,21 +37,18 @@ class LocativeView(HomeAssistantView): """Initialize Locative URL endpoints.""" self.see = see - @asyncio.coroutine - def get(self, request): + async def get(self, request): """Locative message received as GET.""" - res = yield from self._handle(request.app['hass'], request.query) + res = await self._handle(request.app['hass'], request.query) return res - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Locative message received.""" - data = yield from request.post() - res = yield from self._handle(request.app['hass'], data) + data = await request.post() + res = await self._handle(request.app['hass'], data) return res - @asyncio.coroutine - def _handle(self, hass, data): + async def _handle(self, hass, data): """Handle locative request.""" if 'latitude' not in data or 'longitude' not in data: return ('Latitude and longitude not specified.', @@ -79,7 +75,7 @@ class LocativeView(HomeAssistantView): gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE]) if direction == 'enter': - yield from hass.async_add_job( + await hass.async_add_job( partial(self.see, dev_id=device, location_name=location_name, gps=gps_location)) return 'Setting location to {}'.format(location_name) @@ -90,7 +86,7 @@ class LocativeView(HomeAssistantView): if current_state is None or current_state.state == location_name: location_name = STATE_NOT_HOME - yield from hass.async_add_job( + await hass.async_add_job( partial(self.see, dev_id=device, location_name=location_name, gps=gps_location)) return 'Setting location to not home' diff --git a/homeassistant/components/device_tracker/meraki.py b/homeassistant/components/device_tracker/meraki.py index c996b7e643b..d12aff1127a 100644 --- a/homeassistant/components/device_tracker/meraki.py +++ b/homeassistant/components/device_tracker/meraki.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.meraki/ """ -import asyncio import logging import json @@ -33,8 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_scanner(hass, config, async_see, discovery_info=None): +async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Set up an endpoint for the Meraki tracker.""" hass.http.register_view( MerakiView(config, async_see)) @@ -54,16 +52,14 @@ class MerakiView(HomeAssistantView): self.validator = config[CONF_VALIDATOR] self.secret = config[CONF_SECRET] - @asyncio.coroutine - def get(self, request): + async def get(self, request): """Meraki message received as GET.""" return self.validator - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Meraki CMX message received.""" try: - data = yield from request.json() + data = await request.json() except ValueError: return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) _LOGGER.debug("Meraki Data from Post: %s", json.dumps(data)) @@ -125,7 +121,7 @@ class MerakiView(HomeAssistantView): attrs['seenTime'] = i['seenTime'] if i.get('ssid', False): attrs['ssid'] = i['ssid'] - hass.async_add_job(self.async_see( + hass.async_create_task(self.async_see( gps=gps_location, mac=mac, source_type=SOURCE_TYPE_ROUTER, diff --git a/homeassistant/components/device_tracker/mqtt.py b/homeassistant/components/device_tracker/mqtt.py index b5031e8ccfb..06bd6d771a4 100644 --- a/homeassistant/components/device_tracker/mqtt.py +++ b/homeassistant/components/device_tracker/mqtt.py @@ -4,7 +4,6 @@ Support for tracking MQTT enabled devices. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.mqtt/ """ -import asyncio import logging import voluptuous as vol @@ -25,8 +24,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend({ }) -@asyncio.coroutine -def async_setup_scanner(hass, config, async_see, discovery_info=None): +async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Set up the MQTT tracker.""" devices = config[CONF_DEVICES] qos = config[CONF_QOS] @@ -35,10 +33,10 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None): @callback def async_message_received(topic, payload, qos, dev_id=dev_id): """Handle received MQTT message.""" - hass.async_add_job( + hass.async_create_task( async_see(dev_id=dev_id, location_name=payload)) - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( hass, topic, async_message_received, qos) return True diff --git a/homeassistant/components/device_tracker/mqtt_json.py b/homeassistant/components/device_tracker/mqtt_json.py index 7e5ae7c9227..3a820d189f4 100644 --- a/homeassistant/components/device_tracker/mqtt_json.py +++ b/homeassistant/components/device_tracker/mqtt_json.py @@ -4,7 +4,6 @@ Support for GPS tracking MQTT enabled devices. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.mqtt_json/ """ -import asyncio import json import logging @@ -35,8 +34,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend({ }) -@asyncio.coroutine -def async_setup_scanner(hass, config, async_see, discovery_info=None): +async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Set up the MQTT JSON tracker.""" devices = config[CONF_DEVICES] qos = config[CONF_QOS] @@ -57,9 +55,9 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None): return kwargs = _parse_see_args(dev_id, data) - hass.async_add_job(async_see(**kwargs)) + hass.async_create_task(async_see(**kwargs)) - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( hass, topic, async_message_received, qos) return True diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index 87be70b2040..2e1b96dffad 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_SSL, CONF_DEVICES, CONF_EXCLUDE) -REQUIREMENTS = ['pynetgear==0.4.1'] +REQUIREMENTS = ['pynetgear==0.4.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index 2d7f1e80406..10f71450f69 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -4,7 +4,6 @@ Device tracker platform that adds support for OwnTracks over MQTT. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.owntracks/ """ -import asyncio import base64 import json import logging @@ -73,13 +72,11 @@ def get_cipher(): return (KEYLEN, decrypt) -@asyncio.coroutine -def async_setup_scanner(hass, config, async_see, discovery_info=None): +async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Set up an OwnTracks tracker.""" context = context_from_config(async_see, config) - @asyncio.coroutine - def async_handle_mqtt_message(topic, payload, qos): + async def async_handle_mqtt_message(topic, payload, qos): """Handle incoming OwnTracks message.""" try: message = json.loads(payload) @@ -90,9 +87,9 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None): message['topic'] = topic - yield from async_handle_message(hass, context, message) + await async_handle_message(hass, context, message) - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( hass, context.mqtt_topic, async_handle_mqtt_message, 1) return True @@ -266,8 +263,7 @@ class OwnTracksContext: return True - @asyncio.coroutine - def async_see_beacons(self, hass, dev_id, kwargs_param): + async def async_see_beacons(self, hass, dev_id, kwargs_param): """Set active beacons to the current location.""" kwargs = kwargs_param.copy() @@ -290,12 +286,11 @@ class OwnTracksContext: for beacon in self.mobile_beacons_active[dev_id]: kwargs['dev_id'] = "{}_{}".format(BEACON_DEV_ID, beacon) kwargs['host_name'] = beacon - yield from self.async_see(**kwargs) + await self.async_see(**kwargs) @HANDLERS.register('location') -@asyncio.coroutine -def async_handle_location_message(hass, context, message): +async def async_handle_location_message(hass, context, message): """Handle a location message.""" if not context.async_valid_accuracy(message): return @@ -312,12 +307,11 @@ def async_handle_location_message(hass, context, message): context.regions_entered[-1]) return - yield from context.async_see(**kwargs) - yield from context.async_see_beacons(hass, dev_id, kwargs) + await context.async_see(**kwargs) + await context.async_see_beacons(hass, dev_id, kwargs) -@asyncio.coroutine -def _async_transition_message_enter(hass, context, message, location): +async def _async_transition_message_enter(hass, context, message, location): """Execute enter event.""" zone = hass.states.get("zone.{}".format(slugify(location))) dev_id, kwargs = _parse_see_args(message, context.mqtt_topic) @@ -331,7 +325,7 @@ def _async_transition_message_enter(hass, context, message, location): if location not in beacons: beacons.add(location) _LOGGER.info("Added beacon %s", location) - yield from context.async_see_beacons(hass, dev_id, kwargs) + await context.async_see_beacons(hass, dev_id, kwargs) else: # Normal region regions = context.regions_entered[dev_id] @@ -339,12 +333,11 @@ def _async_transition_message_enter(hass, context, message, location): regions.append(location) _LOGGER.info("Enter region %s", location) _set_gps_from_zone(kwargs, location, zone) - yield from context.async_see(**kwargs) - yield from context.async_see_beacons(hass, dev_id, kwargs) + await context.async_see(**kwargs) + await context.async_see_beacons(hass, dev_id, kwargs) -@asyncio.coroutine -def _async_transition_message_leave(hass, context, message, location): +async def _async_transition_message_leave(hass, context, message, location): """Execute leave event.""" dev_id, kwargs = _parse_see_args(message, context.mqtt_topic) regions = context.regions_entered[dev_id] @@ -356,7 +349,7 @@ def _async_transition_message_leave(hass, context, message, location): if location in beacons: beacons.remove(location) _LOGGER.info("Remove beacon %s", location) - yield from context.async_see_beacons(hass, dev_id, kwargs) + await context.async_see_beacons(hass, dev_id, kwargs) else: new_region = regions[-1] if regions else None if new_region: @@ -365,21 +358,20 @@ def _async_transition_message_leave(hass, context, message, location): "zone.{}".format(slugify(new_region))) _set_gps_from_zone(kwargs, new_region, zone) _LOGGER.info("Exit to %s", new_region) - yield from context.async_see(**kwargs) - yield from context.async_see_beacons(hass, dev_id, kwargs) + await context.async_see(**kwargs) + await context.async_see_beacons(hass, dev_id, kwargs) return _LOGGER.info("Exit to GPS") # Check for GPS accuracy if context.async_valid_accuracy(message): - yield from context.async_see(**kwargs) - yield from context.async_see_beacons(hass, dev_id, kwargs) + await context.async_see(**kwargs) + await context.async_see_beacons(hass, dev_id, kwargs) @HANDLERS.register('transition') -@asyncio.coroutine -def async_handle_transition_message(hass, context, message): +async def async_handle_transition_message(hass, context, message): """Handle a transition message.""" if message.get('desc') is None: _LOGGER.error( @@ -399,10 +391,10 @@ def async_handle_transition_message(hass, context, message): location = STATE_HOME if message['event'] == 'enter': - yield from _async_transition_message_enter( + await _async_transition_message_enter( hass, context, message, location) elif message['event'] == 'leave': - yield from _async_transition_message_leave( + await _async_transition_message_leave( hass, context, message, location) else: _LOGGER.error( @@ -410,8 +402,7 @@ def async_handle_transition_message(hass, context, message): message['event']) -@asyncio.coroutine -def async_handle_waypoint(hass, name_base, waypoint): +async def async_handle_waypoint(hass, name_base, waypoint): """Handle a waypoint.""" name = waypoint['desc'] pretty_name = '{} - {}'.format(name_base, name) @@ -429,13 +420,12 @@ def async_handle_waypoint(hass, name_base, waypoint): zone = zone_comp.Zone(hass, pretty_name, lat, lon, rad, zone_comp.ICON_IMPORT, False) zone.entity_id = entity_id - yield from zone.async_update_ha_state() + await zone.async_update_ha_state() @HANDLERS.register('waypoint') @HANDLERS.register('waypoints') -@asyncio.coroutine -def async_handle_waypoints_message(hass, context, message): +async def async_handle_waypoints_message(hass, context, message): """Handle a waypoints message.""" if not context.import_waypoints: return @@ -456,12 +446,11 @@ def async_handle_waypoints_message(hass, context, message): name_base = ' '.join(_parse_topic(message['topic'], context.mqtt_topic)) for wayp in wayps: - yield from async_handle_waypoint(hass, name_base, wayp) + await async_handle_waypoint(hass, name_base, wayp) @HANDLERS.register('encrypted') -@asyncio.coroutine -def async_handle_encrypted_message(hass, context, message): +async def async_handle_encrypted_message(hass, context, message): """Handle an encrypted message.""" plaintext_payload = _decrypt_payload(context.secret, message['topic'], message['data']) @@ -472,7 +461,7 @@ def async_handle_encrypted_message(hass, context, message): decrypted = json.loads(plaintext_payload) decrypted['topic'] = message['topic'] - yield from async_handle_message(hass, context, decrypted) + await async_handle_message(hass, context, decrypted) @HANDLERS.register('lwt') @@ -481,24 +470,21 @@ def async_handle_encrypted_message(hass, context, message): @HANDLERS.register('cmd') @HANDLERS.register('steps') @HANDLERS.register('card') -@asyncio.coroutine -def async_handle_not_impl_msg(hass, context, message): +async def async_handle_not_impl_msg(hass, context, message): """Handle valid but not implemented message types.""" _LOGGER.debug('Not handling %s message: %s', message.get("_type"), message) -@asyncio.coroutine -def async_handle_unsupported_msg(hass, context, message): +async def async_handle_unsupported_msg(hass, context, message): """Handle an unsupported or invalid message type.""" _LOGGER.warning('Received unsupported message type: %s.', message.get('_type')) -@asyncio.coroutine -def async_handle_message(hass, context, message): +async def async_handle_message(hass, context, message): """Handle an OwnTracks message.""" msgtype = message.get('_type') handler = HANDLERS.get(msgtype, async_handle_unsupported_msg) - yield from handler(hass, context, message) + await handler(hass, context, message) diff --git a/homeassistant/components/device_tracker/owntracks_http.py b/homeassistant/components/device_tracker/owntracks_http.py index d74e1fc6d95..b9a813738ad 100644 --- a/homeassistant/components/device_tracker/owntracks_http.py +++ b/homeassistant/components/device_tracker/owntracks_http.py @@ -4,7 +4,6 @@ Device tracker platform that adds support for OwnTracks over HTTP. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.owntracks_http/ """ -import asyncio import re from aiohttp.web_exceptions import HTTPInternalServerError @@ -19,8 +18,7 @@ from .owntracks import ( # NOQA DEPENDENCIES = ['http'] -@asyncio.coroutine -def async_setup_scanner(hass, config, async_see, discovery_info=None): +async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Set up an OwnTracks tracker.""" context = context_from_config(async_see, config) @@ -39,19 +37,18 @@ class OwnTracksView(HomeAssistantView): """Initialize OwnTracks URL endpoints.""" self.context = context - @asyncio.coroutine - def post(self, request, user, device): + async def post(self, request, user, device): """Handle an OwnTracks message.""" hass = request.app['hass'] subscription = self.context.mqtt_topic topic = re.sub('/#$', '', subscription) - message = yield from request.json() + message = await request.json() message['topic'] = '{}/{}/{}'.format(topic, user, device) try: - yield from async_handle_message(hass, self.context, message) + await async_handle_message(hass, self.context, message) return self.json([]) except ValueError: diff --git a/homeassistant/components/device_tracker/tile.py b/homeassistant/components/device_tracker/tile.py index 07f15e7e88a..224aee4363b 100644 --- a/homeassistant/components/device_tracker/tile.py +++ b/homeassistant/components/device_tracker/tile.py @@ -29,10 +29,12 @@ ATTR_IS_DEAD = 'is_dead' ATTR_IS_LOST = 'is_lost' ATTR_RING_STATE = 'ring_state' ATTR_VOIP_STATE = 'voip_state' +ATTR_TILE_ID = 'tile_identifier' +ATTR_TILE_NAME = 'tile_name' CONF_SHOW_INACTIVE = 'show_inactive' -DEFAULT_ICON = 'mdi:bluetooth' +DEFAULT_ICON = 'mdi:view-grid' DEFAULT_SCAN_INTERVAL = timedelta(minutes=2) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -50,8 +52,10 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): websession = aiohttp_client.async_get_clientsession(hass) + config_file = hass.config.path(".{}{}".format( + slugify(config[CONF_USERNAME]), CLIENT_UUID_CONFIG_FILE)) config_data = await hass.async_add_job( - load_json, hass.config.path(CLIENT_UUID_CONFIG_FILE)) + load_json, config_file) if config_data: client = Client( config[CONF_USERNAME], @@ -63,10 +67,7 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): config[CONF_USERNAME], config[CONF_PASSWORD], websession) config_data = {'client_uuid': client.client_uuid} - config_saved = await hass.async_add_job( - save_json, hass.config.path(CLIENT_UUID_CONFIG_FILE), config_data) - if not config_saved: - _LOGGER.error('Failed to save the client UUID') + await hass.async_add_job(save_json, config_file, config_data) scanner = TileScanner( client, hass, async_see, config[CONF_MONITORED_VARIABLES], @@ -125,7 +126,7 @@ class TileScanner: for tile in tiles: await self._async_see( - dev_id='tile_{0}'.format(slugify(tile['name'])), + dev_id='tile_{0}'.format(slugify(tile['tile_uuid'])), gps=( tile['tileState']['latitude'], tile['tileState']['longitude'] @@ -138,5 +139,7 @@ class TileScanner: ATTR_IS_LOST: tile['tileState']['is_lost'], ATTR_RING_STATE: tile['tileState']['ring_state'], ATTR_VOIP_STATE: tile['tileState']['voip_state'], + ATTR_TILE_ID: tile['tile_uuid'], + ATTR_TILE_NAME: tile['name'] }, icon=DEFAULT_ICON) diff --git a/homeassistant/components/device_tracker/upc_connect.py b/homeassistant/components/device_tracker/upc_connect.py index ea0645e012f..2ee6d64730d 100644 --- a/homeassistant/components/device_tracker/upc_connect.py +++ b/homeassistant/components/device_tracker/upc_connect.py @@ -31,11 +31,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_get_scanner(hass, config): +async def async_get_scanner(hass, config): """Return the UPC device scanner.""" scanner = UPCDeviceScanner(hass, config[DOMAIN]) - success_init = yield from scanner.async_initialize_token() + success_init = await scanner.async_initialize_token() return scanner if success_init else None @@ -61,18 +60,17 @@ class UPCDeviceScanner(DeviceScanner): self.websession = async_get_clientsession(hass) - @asyncio.coroutine - def async_scan_devices(self): + async def async_scan_devices(self): """Scan for new devices and return a list with found device IDs.""" import defusedxml.ElementTree as ET if self.token is None: - token_initialized = yield from self.async_initialize_token() + token_initialized = await self.async_initialize_token() if not token_initialized: _LOGGER.error("Not connected to %s", self.host) return [] - raw = yield from self._async_ws_function(CMD_DEVICES) + raw = await self._async_ws_function(CMD_DEVICES) try: xml_root = ET.fromstring(raw) @@ -82,22 +80,20 @@ class UPCDeviceScanner(DeviceScanner): self.token = None return [] - @asyncio.coroutine - def async_get_device_name(self, device): + async def async_get_device_name(self, device): """Get the device name (the name of the wireless device not used).""" return None - @asyncio.coroutine - def async_initialize_token(self): + async def async_initialize_token(self): """Get first token.""" try: # get first token with async_timeout.timeout(10, loop=self.hass.loop): - response = yield from self.websession.get( + response = await self.websession.get( "http://{}/common_page/login.html".format(self.host), headers=self.headers) - yield from response.text() + await response.text() self.token = response.cookies['sessionToken'].value @@ -107,14 +103,13 @@ class UPCDeviceScanner(DeviceScanner): _LOGGER.error("Can not load login page from %s", self.host) return False - @asyncio.coroutine - def _async_ws_function(self, function): + async def _async_ws_function(self, function): """Execute a command on UPC firmware webservice.""" try: with async_timeout.timeout(10, loop=self.hass.loop): # The 'token' parameter has to be first, and 'fun' second # or the UPC firmware will return an error - response = yield from self.websession.post( + response = await self.websession.post( "http://{}/xml/getter.xml".format(self.host), data="token={}&fun={}".format(self.token, function), headers=self.headers, allow_redirects=False) @@ -127,7 +122,7 @@ class UPCDeviceScanner(DeviceScanner): # Load data, store token for next request self.token = response.cookies['sessionToken'].value - return (yield from response.text()) + return await response.text() except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Error on %s", function) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 91f9dea704b..0640eb262cd 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -50,6 +50,7 @@ CONFIG_ENTRY_HANDLERS = { SERVICE_HUE: 'hue', SERVICE_IKEA_TRADFRI: 'tradfri', 'sonos': 'sonos', + 'igd': 'upnp', } SERVICE_HANDLERS = { @@ -168,7 +169,7 @@ async def async_setup(hass, config): results = await hass.async_add_job(_discover, netdisco) for result in results: - hass.async_add_job(new_service_found(*result)) + hass.async_create_task(new_service_found(*result)) async_track_point_in_utc_time(hass, scan_devices, dt_util.utcnow() + SCAN_INTERVAL) @@ -180,7 +181,7 @@ async def async_setup(hass, config): # discovery local services if 'HASSIO' in os.environ: - hass.async_add_job(new_service_found(SERVICE_HASSIO, {})) + hass.async_create_task(new_service_found(SERVICE_HASSIO, {})) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, schedule_first) diff --git a/homeassistant/components/doorbird.py b/homeassistant/components/doorbird.py index c97289b9f07..ab929eb90bb 100644 --- a/homeassistant/components/doorbird.py +++ b/homeassistant/components/doorbird.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/doorbird/ """ import logging -import asyncio import voluptuous as vol from homeassistant.components.http import HomeAssistantView @@ -170,8 +169,7 @@ class DoorbirdRequestView(HomeAssistantView): extra_urls = [API_URL + '/{sensor}'] # pylint: disable=no-self-use - @asyncio.coroutine - def get(self, request, sensor): + async def get(self, request, sensor): """Respond to requests from the device.""" hass = request.app['hass'] diff --git a/homeassistant/components/duckdns.py b/homeassistant/components/duckdns.py index 178e1579538..3420bbed1bc 100644 --- a/homeassistant/components/duckdns.py +++ b/homeassistant/components/duckdns.py @@ -4,14 +4,12 @@ Integrate with DuckDNS. For more details about this component, please refer to the documentation at https://home-assistant.io/components/duckdns/ """ -import asyncio from datetime import timedelta import logging import voluptuous as vol from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN -from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -40,36 +38,24 @@ SERVICE_TXT_SCHEMA = vol.Schema({ }) -@bind_hass -@asyncio.coroutine -def async_set_txt(hass, txt): - """Set the txt record. Pass in None to remove it.""" - yield from hass.services.async_call(DOMAIN, SERVICE_SET_TXT, { - ATTR_TXT: txt - }, blocking=True) - - -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Initialize the DuckDNS component.""" domain = config[DOMAIN][CONF_DOMAIN] token = config[DOMAIN][CONF_ACCESS_TOKEN] session = async_get_clientsession(hass) - result = yield from _update_duckdns(session, domain, token) + result = await _update_duckdns(session, domain, token) if not result: return False - @asyncio.coroutine - def update_domain_interval(now): + async def update_domain_interval(now): """Update the DuckDNS entry.""" - yield from _update_duckdns(session, domain, token) + await _update_duckdns(session, domain, token) - @asyncio.coroutine - def update_domain_service(call): + async def update_domain_service(call): """Update the DuckDNS entry.""" - yield from _update_duckdns( + await _update_duckdns( session, domain, token, txt=call.data[ATTR_TXT]) async_track_time_interval(hass, update_domain_interval, INTERVAL) @@ -83,8 +69,8 @@ def async_setup(hass, config): _SENTINEL = object() -@asyncio.coroutine -def _update_duckdns(session, domain, token, *, txt=_SENTINEL, clear=False): +async def _update_duckdns(session, domain, token, *, txt=_SENTINEL, + clear=False): """Update DuckDNS.""" params = { 'domains': domain, @@ -102,8 +88,8 @@ def _update_duckdns(session, domain, token, *, txt=_SENTINEL, clear=False): if clear: params['clear'] = 'true' - resp = yield from session.get(UPDATE_URL, params=params) - body = yield from resp.text() + resp = await session.get(UPDATE_URL, params=params) + body = await resp.text() if body != 'OK': _LOGGER.warning("Updating DuckDNS domain failed: %s", domain) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 8a67b933b9f..5f1d61dd602 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -18,6 +18,8 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.deprecation import get_deprecated import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json +from homeassistant.components.http import real_ip + from .hue_api import ( HueUsernameView, HueAllLightsStateView, HueOneLightStateView, HueOneLightChangeView, HueGroupView) @@ -81,12 +83,20 @@ ATTR_EMULATED_HUE_NAME = 'emulated_hue_name' ATTR_EMULATED_HUE_HIDDEN = 'emulated_hue_hidden' -def setup(hass, yaml_config): +async def async_setup(hass, yaml_config): """Activate the emulated_hue component.""" config = Config(hass, yaml_config.get(DOMAIN, {})) app = web.Application() app['hass'] = hass + + real_ip.setup_real_ip(app, False, []) + # We misunderstood the startup signal. You're not allowed to change + # anything during startup. Temp workaround. + # pylint: disable=protected-access + app._on_startup.freeze() + await app.startup() + handler = None server = None @@ -131,7 +141,8 @@ def setup(hass, yaml_config): hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, stop_emulated_hue_bridge) - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, + start_emulated_hue_bridge) return True diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index f7fbe2e15e3..3699a45ef30 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -1,5 +1,4 @@ """Provides a Hue API to control Home Assistant.""" -import asyncio import logging from aiohttp import web @@ -21,6 +20,9 @@ from homeassistant.components.fan import ( SPEED_MEDIUM, SPEED_HIGH ) from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http.const import KEY_REAL_IP +from homeassistant.util.network import is_local + _LOGGER = logging.getLogger(__name__) @@ -36,11 +38,10 @@ class HueUsernameView(HomeAssistantView): extra_urls = ['/api/'] requires_auth = False - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Handle a POST request.""" try: - data = yield from request.json() + data = await request.json() except ValueError: return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) @@ -48,6 +49,10 @@ class HueUsernameView(HomeAssistantView): return self.json_message('devicetype not specified', HTTP_BAD_REQUEST) + if not is_local(request[KEY_REAL_IP]): + return self.json_message('only local IPs allowed', + HTTP_BAD_REQUEST) + return self.json([{'success': {'username': '12345678901234567890'}}]) @@ -65,6 +70,10 @@ class HueGroupView(HomeAssistantView): @core.callback def put(self, request, username): """Process a request to make the Logitech Pop working.""" + if not is_local(request[KEY_REAL_IP]): + return self.json_message('only local IPs allowed', + HTTP_BAD_REQUEST) + return self.json([{ 'error': { 'address': '/groups/0/action/scene', @@ -88,6 +97,10 @@ class HueAllLightsStateView(HomeAssistantView): @core.callback def get(self, request, username): """Process a request to get the list of available lights.""" + if not is_local(request[KEY_REAL_IP]): + return self.json_message('only local IPs allowed', + HTTP_BAD_REQUEST) + hass = request.app['hass'] json_response = {} @@ -116,6 +129,10 @@ class HueOneLightStateView(HomeAssistantView): @core.callback def get(self, request, username, entity_id): """Process a request to get the state of an individual light.""" + if not is_local(request[KEY_REAL_IP]): + return self.json_message('only local IPs allowed', + HTTP_BAD_REQUEST) + hass = request.app['hass'] entity_id = self.config.number_to_entity_id(entity_id) entity = hass.states.get(entity_id) @@ -146,9 +163,12 @@ class HueOneLightChangeView(HomeAssistantView): """Initialize the instance of the view.""" self.config = config - @asyncio.coroutine - def put(self, request, username, entity_number): + async def put(self, request, username, entity_number): """Process a request to set the state of an individual light.""" + if not is_local(request[KEY_REAL_IP]): + return self.json_message('only local IPs allowed', + HTTP_BAD_REQUEST) + config = self.config hass = request.app['hass'] entity_id = config.number_to_entity_id(entity_number) @@ -168,7 +188,7 @@ class HueOneLightChangeView(HomeAssistantView): return web.Response(text="Entity not exposed", status=404) try: - request_json = yield from request.json() + request_json = await request.json() except ValueError: _LOGGER.error('Received invalid json') return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) @@ -257,11 +277,11 @@ class HueOneLightChangeView(HomeAssistantView): # Separate call to turn on needed if turn_on_needed: - hass.async_add_job(hass.services.async_call( + hass.async_create_task(hass.services.async_call( core.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True)) - hass.async_add_job(hass.services.async_call( + hass.async_create_task(hass.services.async_call( domain, service, data, blocking=True)) json_response = \ diff --git a/homeassistant/components/envisalink.py b/homeassistant/components/envisalink.py index 9b5b25c934c..e96810a8083 100644 --- a/homeassistant/components/envisalink.py +++ b/homeassistant/components/envisalink.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send -REQUIREMENTS = ['pyenvisalink==2.3'] +REQUIREMENTS = ['pyenvisalink==3.7'] _LOGGER = logging.getLogger(__name__) @@ -81,8 +81,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up for Envisalink devices.""" from pyenvisalink import EnvisalinkAlarmPanel @@ -165,7 +164,7 @@ def async_setup(hass, config): _LOGGER.info("Start envisalink.") controller.start() - result = yield from sync_connect + result = await sync_connect if not result: return False diff --git a/homeassistant/components/evohome.py b/homeassistant/components/evohome.py new file mode 100644 index 00000000000..ceeec407b05 --- /dev/null +++ b/homeassistant/components/evohome.py @@ -0,0 +1,145 @@ +"""Support for Honeywell evohome (EMEA/EU-based systems only). + +Support for a temperature control system (TCS, controller) with 0+ heating +zones (e.g. TRVs, relays) and, optionally, a DHW controller. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/evohome/ +""" + +# Glossary: +# TCS - temperature control system (a.k.a. Controller, Parent), which can +# have up to 13 Children: +# 0-12 Heating zones (a.k.a. Zone), and +# 0-1 DHW controller, (a.k.a. Boiler) + +import logging + +from requests.exceptions import HTTPError +import voluptuous as vol + +from homeassistant.const import ( + CONF_USERNAME, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + HTTP_BAD_REQUEST +) + +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform + +REQUIREMENTS = ['evohomeclient==0.2.7'] +# If ever > 0.2.7, re-check the work-around wrapper is still required when +# instantiating the client, below. + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'evohome' +DATA_EVOHOME = 'data_' + DOMAIN + +CONF_LOCATION_IDX = 'location_idx' +MAX_TEMP = 28 +MIN_TEMP = 5 +SCAN_INTERVAL_DEFAULT = 180 +SCAN_INTERVAL_MAX = 300 + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_LOCATION_IDX, default=0): cv.positive_int, + }), +}, extra=vol.ALLOW_EXTRA) + +# These are used to help prevent E501 (line too long) violations. +GWS = 'gateways' +TCS = 'temperatureControlSystems' + + +def setup(hass, config): + """Create a Honeywell (EMEA/EU) evohome CH/DHW system. + + One controller with 0+ heating zones (e.g. TRVs, relays) and, optionally, a + DHW controller. Does not work for US-based systems. + """ + evo_data = hass.data[DATA_EVOHOME] = {} + evo_data['timers'] = {} + + evo_data['params'] = dict(config[DOMAIN]) + evo_data['params'][CONF_SCAN_INTERVAL] = SCAN_INTERVAL_DEFAULT + + from evohomeclient2 import EvohomeClient + + _LOGGER.debug("setup(): API call [4 request(s)]: client.__init__()...") + + try: + # There's a bug in evohomeclient2 v0.2.7: the client.__init__() sets + # the root loglevel when EvohomeClient(debug=?), so remember it now... + log_level = logging.getLogger().getEffectiveLevel() + + client = EvohomeClient( + evo_data['params'][CONF_USERNAME], + evo_data['params'][CONF_PASSWORD], + debug=False + ) + # ...then restore it to what it was before instantiating the client + logging.getLogger().setLevel(log_level) + + except HTTPError as err: + if err.response.status_code == HTTP_BAD_REQUEST: + _LOGGER.error( + "Failed to establish a connection with evohome web servers, " + "Check your username (%s), and password are correct." + "Unable to continue. Resolve any errors and restart HA.", + evo_data['params'][CONF_USERNAME] + ) + return False # unable to continue + + raise # we dont handle any other HTTPErrors + + finally: # Redact username, password as no longer needed. + evo_data['params'][CONF_USERNAME] = 'REDACTED' + evo_data['params'][CONF_PASSWORD] = 'REDACTED' + + evo_data['client'] = client + + # Redact any installation data we'll never need. + if client.installation_info[0]['locationInfo']['locationId'] != 'REDACTED': + for loc in client.installation_info: + loc['locationInfo']['streetAddress'] = 'REDACTED' + loc['locationInfo']['city'] = 'REDACTED' + loc['locationInfo']['locationOwner'] = 'REDACTED' + loc[GWS][0]['gatewayInfo'] = 'REDACTED' + + # Pull down the installation configuration. + loc_idx = evo_data['params'][CONF_LOCATION_IDX] + + try: + evo_data['config'] = client.installation_info[loc_idx] + + except IndexError: + _LOGGER.warning( + "setup(): Parameter '%s' = %s , is outside its range (0-%s)", + CONF_LOCATION_IDX, + loc_idx, + len(client.installation_info) - 1 + ) + + return False # unable to continue + + evo_data['status'] = {} + + if _LOGGER.isEnabledFor(logging.DEBUG): + tmp_loc = dict(evo_data['config']) + tmp_loc['locationInfo']['postcode'] = 'REDACTED' + tmp_tcs = tmp_loc[GWS][0][TCS][0] + if 'zones' in tmp_tcs: + tmp_tcs['zones'] = '...' + if 'dhw' in tmp_tcs: + tmp_tcs['dhw'] = '...' + + _LOGGER.debug("setup(), location = %s", tmp_loc) + + load_platform(hass, 'climate', DOMAIN) + + return True diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index f2704e84bc5..36b075747e0 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -4,7 +4,6 @@ Provides functionality to interact with fans. For more details about this component, please refer to the documentation at https://home-assistant.io/components/fan/ """ -import asyncio from datetime import timedelta import functools as ft import logging @@ -98,84 +97,12 @@ def is_on(hass, entity_id: str = None) -> bool: return state.attributes[ATTR_SPEED] not in [SPEED_OFF, STATE_UNKNOWN] -@bind_hass -def turn_on(hass, entity_id: str = None, speed: str = None) -> None: - """Turn all or specified fan on.""" - data = { - key: value for key, value in [ - (ATTR_ENTITY_ID, entity_id), - (ATTR_SPEED, speed), - ] if value is not None - } - - hass.services.call(DOMAIN, SERVICE_TURN_ON, data) - - -@bind_hass -def turn_off(hass, entity_id: str = None) -> None: - """Turn all or specified fan off.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - - hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) - - -@bind_hass -def toggle(hass, entity_id: str = None) -> None: - """Toggle all or specified fans.""" - data = { - ATTR_ENTITY_ID: entity_id - } - - hass.services.call(DOMAIN, SERVICE_TOGGLE, data) - - -@bind_hass -def oscillate(hass, entity_id: str = None, - should_oscillate: bool = True) -> None: - """Set oscillation on all or specified fan.""" - data = { - key: value for key, value in [ - (ATTR_ENTITY_ID, entity_id), - (ATTR_OSCILLATING, should_oscillate), - ] if value is not None - } - - hass.services.call(DOMAIN, SERVICE_OSCILLATE, data) - - -@bind_hass -def set_speed(hass, entity_id: str = None, speed: str = None) -> None: - """Set speed for all or specified fan.""" - data = { - key: value for key, value in [ - (ATTR_ENTITY_ID, entity_id), - (ATTR_SPEED, speed), - ] if value is not None - } - - hass.services.call(DOMAIN, SERVICE_SET_SPEED, data) - - -@bind_hass -def set_direction(hass, entity_id: str = None, direction: str = None) -> None: - """Set direction for all or specified fan.""" - data = { - key: value for key, value in [ - (ATTR_ENTITY_ID, entity_id), - (ATTR_DIRECTION, direction), - ] if value is not None - } - - hass.services.call(DOMAIN, SERVICE_SET_DIRECTION, data) - - -@asyncio.coroutine -def async_setup(hass, config: dict): +async def async_setup(hass, config: dict): """Expose fan control via statemachine and services.""" - component = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_FANS) - yield from component.async_setup(config) + await component.async_setup(config) component.async_register_entity_service( SERVICE_TURN_ON, FAN_TURN_ON_SCHEMA, @@ -205,6 +132,16 @@ def async_setup(hass, config: dict): return True +async def async_setup_entry(hass, entry): + """Set up 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 FanEntity(ToggleEntity): """Representation of a fan.""" diff --git a/homeassistant/components/fan/dyson.py b/homeassistant/components/fan/dyson.py index 9f505c87b3d..ef517021178 100644 --- a/homeassistant/components/fan/dyson.py +++ b/homeassistant/components/fan/dyson.py @@ -3,7 +3,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/fan.dyson/ """ -import asyncio import logging import voluptuous as vol @@ -77,8 +76,7 @@ class DysonPureCoolLinkDevice(FanEntity): self.hass = hass self._device = device - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.async_add_job( self._device.add_message_listener, self.on_message) diff --git a/homeassistant/components/fan/insteon.py b/homeassistant/components/fan/insteon.py index f938ae7aec1..604063a9aa3 100644 --- a/homeassistant/components/fan/insteon.py +++ b/homeassistant/components/fan/insteon.py @@ -4,7 +4,6 @@ Support for INSTEON fans via PowerLinc Modem. For more details about this component, please refer to the documentation at https://home-assistant.io/components/fan.insteon/ """ -import asyncio import logging from homeassistant.components.fan import (SPEED_OFF, @@ -28,9 +27,8 @@ FAN_SPEEDS = [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] _LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the INSTEON device class for the hass platform.""" insteon_modem = hass.data['insteon'].get('modem') @@ -64,20 +62,17 @@ class InsteonFan(InsteonEntity, FanEntity): """Flag supported features.""" return SUPPORT_SET_SPEED - @asyncio.coroutine - def async_turn_on(self, speed: str = None, **kwargs) -> None: + async def async_turn_on(self, speed: str = None, **kwargs) -> None: """Turn on the entity.""" if speed is None: speed = SPEED_MEDIUM - yield from self.async_set_speed(speed) + await self.async_set_speed(speed) - @asyncio.coroutine - def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs) -> None: """Turn off the entity.""" - yield from self.async_set_speed(SPEED_OFF) + await self.async_set_speed(SPEED_OFF) - @asyncio.coroutine - def async_set_speed(self, speed: str) -> None: + async def async_set_speed(self, speed: str) -> None: """Set the speed of the fan.""" fan_speed = SPEED_TO_HEX[speed] if fan_speed == 0x00: diff --git a/homeassistant/components/fan/mqtt.py b/homeassistant/components/fan/mqtt.py index db3cfab3608..3e1ad2704e7 100644 --- a/homeassistant/components/fan/mqtt.py +++ b/homeassistant/components/fan/mqtt.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation https://home-assistant.io/components/fan.mqtt/ """ import logging +from typing import Optional import voluptuous as vol @@ -14,9 +15,9 @@ from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, STATE_ON, STATE_OFF, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON) from homeassistant.components.mqtt import ( - CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, - CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, - MqttAvailability) + ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, + CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, + CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, @@ -41,6 +42,7 @@ CONF_PAYLOAD_LOW_SPEED = 'payload_low_speed' CONF_PAYLOAD_MEDIUM_SPEED = 'payload_medium_speed' CONF_PAYLOAD_HIGH_SPEED = 'payload_high_speed' CONF_SPEED_LIST = 'speeds' +CONF_UNIQUE_ID = 'unique_id' DEFAULT_NAME = 'MQTT Fan' DEFAULT_PAYLOAD_ON = 'ON' @@ -74,6 +76,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ default=[SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]): cv.ensure_list, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_UNIQUE_ID): cv.string, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @@ -83,6 +86,10 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) + discovery_hash = None + if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info: + discovery_hash = discovery_info[ATTR_DISCOVERY_HASH] + async_add_entities([MqttFan( config.get(CONF_NAME), { @@ -116,18 +123,22 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE), + config.get(CONF_UNIQUE_ID), + discovery_hash, )]) -class MqttFan(MqttAvailability, FanEntity): +class MqttFan(MqttAvailability, MqttDiscoveryUpdate, FanEntity): """A MQTT fan component.""" def __init__(self, name, topic, templates, qos, retain, payload, speed_list, optimistic, availability_topic, payload_available, - payload_not_available): + payload_not_available, unique_id: Optional[str], + discovery_hash): """Initialize the MQTT fan.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_hash) self._name = name self._topic = topic self._qos = qos @@ -148,10 +159,13 @@ class MqttFan(MqttAvailability, FanEntity): is not None and SUPPORT_OSCILLATE) self._supported_features |= (topic[CONF_SPEED_STATE_TOPIC] is not None and SUPPORT_SET_SPEED) + self._unique_id = unique_id + self._discovery_hash = discovery_hash async def async_added_to_hass(self): """Subscribe to MQTT events.""" - await super().async_added_to_hass() + await MqttAvailability.async_added_to_hass(self) + await MqttDiscoveryUpdate.async_added_to_hass(self) templates = {} for key, tpl in list(self._templates.items()): @@ -315,3 +329,8 @@ class MqttFan(MqttAvailability, FanEntity): if self._optimistic_oscillation: self._oscillation = oscillating self.async_schedule_update_ha_state() + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id diff --git a/homeassistant/components/fan/wink.py b/homeassistant/components/fan/wink.py index 480801c48c0..d0dc386d74d 100644 --- a/homeassistant/components/fan/wink.py +++ b/homeassistant/components/fan/wink.py @@ -4,7 +4,6 @@ Support for Wink fans. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/fan.wink/ """ -import asyncio import logging from homeassistant.components.fan import ( @@ -33,8 +32,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class WinkFanDevice(WinkDevice, FanEntity): """Representation of a Wink fan.""" - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['fan'].append(self) diff --git a/homeassistant/components/ffmpeg.py b/homeassistant/components/ffmpeg.py index 9aaae16ee21..f28dbd52336 100644 --- a/homeassistant/components/ffmpeg.py +++ b/homeassistant/components/ffmpeg.py @@ -4,7 +4,6 @@ Component that will help set the FFmpeg component. For more details about this component, please refer to the documentation at https://home-assistant.io/components/ffmpeg/ """ -import asyncio import logging import voluptuous as vol @@ -55,29 +54,7 @@ SERVICE_FFMPEG_SCHEMA = vol.Schema({ }) -@callback -def async_start(hass, entity_id=None): - """Start a FFmpeg process on entity.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_START, data)) - - -@callback -def async_stop(hass, entity_id=None): - """Stop a FFmpeg process on entity.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_STOP, data)) - - -@callback -def async_restart(hass, entity_id=None): - """Restart a FFmpeg process on entity.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_RESTART, data)) - - -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the FFmpeg component.""" conf = config.get(DOMAIN, {}) @@ -88,8 +65,7 @@ def async_setup(hass, config): ) # Register service - @asyncio.coroutine - def async_service_handle(service): + async def async_service_handle(service): """Handle service ffmpeg process.""" entity_ids = service.data.get(ATTR_ENTITY_ID) @@ -131,8 +107,7 @@ class FFmpegManager: """Return ffmpeg binary from config.""" return self._bin - @asyncio.coroutine - def async_run_test(self, input_source): + async def async_run_test(self, input_source): """Run test on this input. TRUE is deactivate or run correct. This method must be run in the event loop. @@ -146,7 +121,7 @@ class FFmpegManager: # run test ffmpeg_test = Test(self.binary, loop=self.hass.loop) - success = yield from ffmpeg_test.run_test(input_source) + success = await ffmpeg_test.run_test(input_source) if not success: _LOGGER.error("FFmpeg '%s' test fails!", input_source) self._cache[input_source] = False @@ -163,8 +138,7 @@ class FFmpegBase(Entity): self.ffmpeg = None self.initial_state = initial_state - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register dispatcher & events. This method is a coroutine. @@ -189,40 +163,36 @@ class FFmpegBase(Entity): """Return True if entity has to be polled for state.""" return False - @asyncio.coroutine - def _async_start_ffmpeg(self, entity_ids): + async def _async_start_ffmpeg(self, entity_ids): """Start a FFmpeg process. This method is a coroutine. """ raise NotImplementedError() - @asyncio.coroutine - def _async_stop_ffmpeg(self, entity_ids): + async def _async_stop_ffmpeg(self, entity_ids): """Stop a FFmpeg process. This method is a coroutine. """ if entity_ids is None or self.entity_id in entity_ids: - yield from self.ffmpeg.close() + await self.ffmpeg.close() - @asyncio.coroutine - def _async_restart_ffmpeg(self, entity_ids): + async def _async_restart_ffmpeg(self, entity_ids): """Stop a FFmpeg process. This method is a coroutine. """ if entity_ids is None or self.entity_id in entity_ids: - yield from self._async_stop_ffmpeg(None) - yield from self._async_start_ffmpeg(None) + await self._async_stop_ffmpeg(None) + await self._async_start_ffmpeg(None) @callback def _async_register_events(self): """Register a FFmpeg process/device.""" - @asyncio.coroutine - def async_shutdown_handle(event): + async def async_shutdown_handle(event): """Stop FFmpeg process.""" - yield from self._async_stop_ffmpeg(None) + await self._async_stop_ffmpeg(None) self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, async_shutdown_handle) @@ -231,10 +201,9 @@ class FFmpegBase(Entity): if not self.initial_state: return - @asyncio.coroutine - def async_start_handle(event): + async def async_start_handle(event): """Start FFmpeg process.""" - yield from self._async_start_ffmpeg(None) + await self._async_start_ffmpeg(None) self.async_schedule_update_ha_state() self.hass.bus.async_listen_once( diff --git a/homeassistant/components/foursquare.py b/homeassistant/components/foursquare.py index 2c10df327f4..a4a7395adc4 100644 --- a/homeassistant/components/foursquare.py +++ b/homeassistant/components/foursquare.py @@ -4,7 +4,6 @@ Support for the Foursquare (Swarm) API. For more details about this component, please refer to the documentation at https://home-assistant.io/components/foursquare/ """ -import asyncio import logging import requests @@ -85,11 +84,10 @@ class FoursquarePushReceiver(HomeAssistantView): """Initialize the OAuth callback view.""" self.push_secret = push_secret - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Accept the POST from Foursquare.""" try: - data = yield from request.json() + data = await request.json() except ValueError: return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) diff --git a/homeassistant/components/freedns.py b/homeassistant/components/freedns.py index 0512030bdcb..0b5cbeda01a 100644 --- a/homeassistant/components/freedns.py +++ b/homeassistant/components/freedns.py @@ -37,8 +37,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Initialize the FreeDNS component.""" url = config[DOMAIN].get(CONF_URL) auth_token = config[DOMAIN].get(CONF_ACCESS_TOKEN) @@ -46,16 +45,15 @@ def async_setup(hass, config): session = hass.helpers.aiohttp_client.async_get_clientsession() - result = yield from _update_freedns( + result = await _update_freedns( hass, session, url, auth_token) if result is False: return False - @asyncio.coroutine - def update_domain_callback(now): + async def update_domain_callback(now): """Update the FreeDNS entry.""" - yield from _update_freedns(hass, session, url, auth_token) + await _update_freedns(hass, session, url, auth_token) hass.helpers.event.async_track_time_interval( update_domain_callback, update_interval) @@ -63,8 +61,7 @@ def async_setup(hass, config): return True -@asyncio.coroutine -def _update_freedns(hass, session, url, auth_token): +async def _update_freedns(hass, session, url, auth_token): """Update FreeDNS.""" params = None @@ -77,8 +74,8 @@ def _update_freedns(hass, session, url, auth_token): try: with async_timeout.timeout(TIMEOUT, loop=hass.loop): - resp = yield from session.get(url, params=params) - body = yield from resp.text() + resp = await session.get(url, params=params) + body = await resp.text() if "has not changed" in body: # IP has not changed. diff --git a/homeassistant/components/fritzbox.py b/homeassistant/components/fritzbox.py index a3c35aaa597..e6f121799df 100644 --- a/homeassistant/components/fritzbox.py +++ b/homeassistant/components/fritzbox.py @@ -16,15 +16,18 @@ from homeassistant.helpers import discovery _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pyfritzhome==0.3.7'] +REQUIREMENTS = ['pyfritzhome==0.4.0'] -SUPPORTED_DOMAINS = ['climate', 'switch'] +SUPPORTED_DOMAINS = ['binary_sensor', 'climate', 'switch'] DOMAIN = 'fritzbox' -ATTR_STATE_DEVICE_LOCKED = 'device_locked' -ATTR_STATE_LOCKED = 'locked' ATTR_STATE_BATTERY_LOW = 'battery_low' +ATTR_STATE_DEVICE_LOCKED = 'device_locked' +ATTR_STATE_HOLIDAY_MODE = 'holiday_mode' +ATTR_STATE_LOCKED = 'locked' +ATTR_STATE_SUMMER_MODE = 'summer_mode' +ATTR_STATE_WINDOW_OPEN = 'window_open' CONFIG_SCHEMA = vol.Schema({ diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 9bd13f316b6..c06f659573e 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -21,16 +21,14 @@ from homeassistant.components import websocket_api 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==20180927.0'] +REQUIREMENTS = ['home-assistant-frontend==20181012.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', - 'auth', 'onboarding'] + 'auth', 'onboarding', 'lovelace'] CONF_THEMES = 'themes' CONF_EXTRA_HTML_URL = 'extra_html_url' @@ -108,10 +106,6 @@ 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 Panel: @@ -151,7 +145,7 @@ class Panel: index_view.get) @callback - def to_response(self, hass, request): + def to_response(self): """Panel as dictionary.""" return { 'component_name': self.component_name, @@ -208,9 +202,6 @@ async def async_setup(hass, config): 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, {}) @@ -353,11 +344,12 @@ class AuthorizeView(HomeAssistantView): _is_latest(self.js_option, request) if latest: - location = '/frontend_latest/authorize.html' + base = 'frontend_latest' else: - location = '/frontend_es5/authorize.html' + base = 'frontend_es5' - location += '?{}'.format(request.query_string) + location = "/{}/authorize.html{}".format( + base, str(request.url.relative())[15:]) return web.Response(status=302, headers={ 'location': location @@ -493,12 +485,10 @@ def websocket_get_panels(hass, connection, msg): Async friendly. """ panels = { - panel: - connection.hass.data[DATA_PANELS][panel].to_response( - connection.hass, connection.request) + panel: connection.hass.data[DATA_PANELS][panel].to_response() for panel in connection.hass.data[DATA_PANELS]} - connection.to_write.put_nowait(websocket_api.result_message( + connection.send_message(websocket_api.result_message( msg['id'], panels)) @@ -508,50 +498,21 @@ def websocket_get_themes(hass, connection, msg): Async friendly. """ - connection.to_write.put_nowait(websocket_api.result_message(msg['id'], { + connection.send_message(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): +@websocket_api.async_response +async def websocket_get_translations(hass, connection, msg): """Handle get translations command. Async friendly. """ - async def send_translations(): - """Send a translation.""" - 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()) + resources = await async_get_translations(hass, msg['language']) + connection.send_message(websocket_api.result_message( + msg['id'], { + 'resources': resources, + } + )) diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 22569af1f86..8d4ac9f01c9 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -14,21 +14,18 @@ import async_timeout import voluptuous as vol # Typing imports -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.const import CONF_NAME from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.loader import bind_hass from .const import ( - DOMAIN, CONF_PROJECT_ID, CONF_CLIENT_ID, CONF_ACCESS_TOKEN, - CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS, - DEFAULT_EXPOSED_DOMAINS, CONF_AGENT_USER_ID, CONF_API_KEY, + DOMAIN, CONF_PROJECT_ID, CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT, + CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS, CONF_API_KEY, SERVICE_REQUEST_SYNC, REQUEST_SYNC_BASE_URL, CONF_ENTITY_CONFIG, CONF_EXPOSE, CONF_ALIASES, CONF_ROOM_HINT ) -from .auth import GoogleAssistantAuthView from .http import async_register_http _LOGGER = logging.getLogger(__name__) @@ -44,40 +41,28 @@ ENTITY_SCHEMA = vol.Schema({ vol.Optional(CONF_ROOM_HINT): cv.string }) -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: { - vol.Required(CONF_PROJECT_ID): cv.string, - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_ACCESS_TOKEN): cv.string, - vol.Optional(CONF_EXPOSE_BY_DEFAULT, - default=DEFAULT_EXPOSE_BY_DEFAULT): cv.boolean, - vol.Optional(CONF_EXPOSED_DOMAINS, - default=DEFAULT_EXPOSED_DOMAINS): cv.ensure_list, - vol.Optional(CONF_AGENT_USER_ID, - default=DEFAULT_AGENT_USER_ID): cv.string, - vol.Optional(CONF_API_KEY): cv.string, - vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ENTITY_SCHEMA} - } - }, - extra=vol.ALLOW_EXTRA) +GOOGLE_ASSISTANT_SCHEMA = vol.Schema({ + vol.Required(CONF_PROJECT_ID): cv.string, + vol.Optional(CONF_EXPOSE_BY_DEFAULT, + default=DEFAULT_EXPOSE_BY_DEFAULT): cv.boolean, + vol.Optional(CONF_EXPOSED_DOMAINS, + default=DEFAULT_EXPOSED_DOMAINS): cv.ensure_list, + vol.Optional(CONF_API_KEY): cv.string, + vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ENTITY_SCHEMA} +}, extra=vol.PREVENT_EXTRA) - -@bind_hass -def request_sync(hass): - """Request sync.""" - hass.services.call(DOMAIN, SERVICE_REQUEST_SYNC) +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: GOOGLE_ASSISTANT_SCHEMA +}, extra=vol.ALLOW_EXTRA) async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): """Activate Google Actions component.""" config = yaml_config.get(DOMAIN, {}) - agent_user_id = config.get(CONF_AGENT_USER_ID) api_key = config.get(CONF_API_KEY) - hass.http.register_view(GoogleAssistantAuthView(hass, config)) async_register_http(hass, config) - async def request_sync_service_handler(call): + async def request_sync_service_handler(call: ServiceCall): """Handle request sync service calls.""" websession = async_get_clientsession(hass) try: @@ -85,7 +70,7 @@ async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): res = await websession.post( REQUEST_SYNC_BASE_URL, params={'key': api_key}, - json={'agent_user_id': agent_user_id}) + json={'agent_user_id': call.context.user_id}) _LOGGER.info("Submitted request_sync request to Google") res.raise_for_status() except aiohttp.ClientResponseError: diff --git a/homeassistant/components/google_assistant/auth.py b/homeassistant/components/google_assistant/auth.py deleted file mode 100644 index 5b98e25014d..00000000000 --- a/homeassistant/components/google_assistant/auth.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Google Assistant OAuth View.""" - -import logging -from typing import Dict, Any - -# Typing imports -# if False: -from aiohttp.web import Request, Response - -from homeassistant.core import HomeAssistant -from homeassistant.components.http import HomeAssistantView -from homeassistant.const import ( - HTTP_BAD_REQUEST, - HTTP_UNAUTHORIZED, - HTTP_MOVED_PERMANENTLY, -) - -from .const import ( - GOOGLE_ASSISTANT_API_ENDPOINT, - CONF_PROJECT_ID, CONF_CLIENT_ID, CONF_ACCESS_TOKEN -) - -BASE_OAUTH_URL = 'https://oauth-redirect.googleusercontent.com' -REDIRECT_TEMPLATE_URL = \ - '{}/r/{}#access_token={}&token_type=bearer&state={}' - -_LOGGER = logging.getLogger(__name__) - - -class GoogleAssistantAuthView(HomeAssistantView): - """Handle Google Actions auth requests.""" - - url = GOOGLE_ASSISTANT_API_ENDPOINT + '/auth' - name = 'api:google_assistant:auth' - requires_auth = False - - def __init__(self, hass: HomeAssistant, cfg: Dict[str, Any]) -> None: - """Initialize instance of the view.""" - super().__init__() - - self.project_id = cfg.get(CONF_PROJECT_ID) - self.client_id = cfg.get(CONF_CLIENT_ID) - self.access_token = cfg.get(CONF_ACCESS_TOKEN) - - async def get(self, request: Request) -> Response: - """Handle oauth token request.""" - query = request.query - redirect_uri = query.get('redirect_uri') - if not redirect_uri: - msg = 'missing redirect_uri field' - _LOGGER.warning(msg) - return self.json_message(msg, status_code=HTTP_BAD_REQUEST) - - if self.project_id not in redirect_uri: - msg = 'missing project_id in redirect_uri' - _LOGGER.warning(msg) - return self.json_message(msg, status_code=HTTP_BAD_REQUEST) - - state = query.get('state') - if not state: - msg = 'oauth request missing state' - _LOGGER.warning(msg) - return self.json_message(msg, status_code=HTTP_BAD_REQUEST) - - client_id = query.get('client_id') - if self.client_id != client_id: - msg = 'invalid client id' - _LOGGER.warning(msg) - return self.json_message(msg, status_code=HTTP_UNAUTHORIZED) - - generated_url = redirect_url(self.project_id, self.access_token, state) - - _LOGGER.info('user login in from Google Assistant') - return self.json_message( - 'redirect success', - status_code=HTTP_MOVED_PERMANENTLY, - headers={'Location': generated_url}) - - -def redirect_url(project_id: str, access_token: str, state: str) -> str: - """Generate the redirect format for the oauth request.""" - return REDIRECT_TEMPLATE_URL.format(BASE_OAUTH_URL, project_id, - access_token, state) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 12888ea2cf6..485b98e8e22 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -8,10 +8,7 @@ CONF_ENTITY_CONFIG = 'entity_config' CONF_EXPOSE_BY_DEFAULT = 'expose_by_default' CONF_EXPOSED_DOMAINS = 'exposed_domains' CONF_PROJECT_ID = 'project_id' -CONF_ACCESS_TOKEN = 'access_token' -CONF_CLIENT_ID = 'client_id' CONF_ALIASES = 'aliases' -CONF_AGENT_USER_ID = 'agent_user_id' CONF_API_KEY = 'api_key' CONF_ROOM_HINT = 'room' diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 05bc3cbd01c..65af7b932b0 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/google_assistant/ """ import logging -from aiohttp.hdrs import AUTHORIZATION from aiohttp.web import Request, Response # Typing imports @@ -15,10 +14,8 @@ from homeassistant.core import callback from .const import ( GOOGLE_ASSISTANT_API_ENDPOINT, - CONF_ACCESS_TOKEN, CONF_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS, - CONF_AGENT_USER_ID, CONF_ENTITY_CONFIG, CONF_EXPOSE, ) @@ -31,10 +28,8 @@ _LOGGER = logging.getLogger(__name__) @callback def async_register_http(hass, cfg): """Register HTTP views for Google Assistant.""" - access_token = cfg.get(CONF_ACCESS_TOKEN) expose_by_default = cfg.get(CONF_EXPOSE_BY_DEFAULT) exposed_domains = cfg.get(CONF_EXPOSED_DOMAINS) - agent_user_id = cfg.get(CONF_AGENT_USER_ID) entity_config = cfg.get(CONF_ENTITY_CONFIG) or {} def is_exposed(entity) -> bool: @@ -57,9 +52,8 @@ def async_register_http(hass, cfg): return is_default_exposed or explicit_expose - gass_config = Config(is_exposed, agent_user_id, entity_config) hass.http.register_view( - GoogleAssistantView(access_token, gass_config)) + GoogleAssistantView(is_exposed, entity_config)) class GoogleAssistantView(HomeAssistantView): @@ -67,20 +61,19 @@ class GoogleAssistantView(HomeAssistantView): url = GOOGLE_ASSISTANT_API_ENDPOINT name = 'api:google_assistant' - requires_auth = False # Uses access token from oauth flow + requires_auth = True - def __init__(self, access_token, gass_config): + def __init__(self, is_exposed, entity_config): """Initialize the Google Assistant request handler.""" - self.access_token = access_token - self.gass_config = gass_config + self.is_exposed = is_exposed + self.entity_config = entity_config async def post(self, request: Request) -> Response: """Handle Google Assistant requests.""" - auth = request.headers.get(AUTHORIZATION, None) - if 'Bearer {}'.format(self.access_token) != auth: - return self.json_message("missing authorization", status_code=401) - message = await request.json() # type: dict + config = Config(self.is_exposed, + request['hass_user'].id, + self.entity_config) result = await async_handle_message( - request.app['hass'], self.gass_config, message) + request.app['hass'], config, message) return self.json(result) diff --git a/homeassistant/components/google_domains.py b/homeassistant/components/google_domains.py index 3b414306be5..32bdb79557a 100644 --- a/homeassistant/components/google_domains.py +++ b/homeassistant/components/google_domains.py @@ -36,8 +36,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Initialize the Google Domains component.""" domain = config[DOMAIN].get(CONF_DOMAIN) user = config[DOMAIN].get(CONF_USERNAME) @@ -46,16 +45,15 @@ def async_setup(hass, config): session = hass.helpers.aiohttp_client.async_get_clientsession() - result = yield from _update_google_domains( + result = await _update_google_domains( hass, session, domain, user, password, timeout) if not result: return False - @asyncio.coroutine - def update_domain_interval(now): + async def update_domain_interval(now): """Update the Google Domains entry.""" - yield from _update_google_domains( + await _update_google_domains( hass, session, domain, user, password, timeout) hass.helpers.event.async_track_time_interval( @@ -64,8 +62,8 @@ def async_setup(hass, config): return True -@asyncio.coroutine -def _update_google_domains(hass, session, domain, user, password, timeout): +async def _update_google_domains(hass, session, domain, user, password, + timeout): """Update Google Domains.""" url = UPDATE_URL.format(user, password) @@ -75,8 +73,8 @@ def _update_google_domains(hass, session, domain, user, password, timeout): try: with async_timeout.timeout(timeout, loop=hass.loop): - resp = yield from session.get(url, params=params) - body = yield from resp.text() + resp = await session.get(url, params=params) + body = await resp.text() if body.startswith('good') or body.startswith('nochg'): return True diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index eda65d1895d..39fd7567c98 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -120,70 +120,6 @@ def is_on(hass, entity_id): return False -@bind_hass -def reload(hass): - """Reload the automation from config.""" - hass.add_job(async_reload, hass) - - -@callback -@bind_hass -def async_reload(hass): - """Reload the automation from config.""" - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_RELOAD)) - - -@bind_hass -def set_visibility(hass, entity_id=None, visible=True): - """Hide or shows a group.""" - data = {ATTR_ENTITY_ID: entity_id, ATTR_VISIBLE: visible} - hass.services.call(DOMAIN, SERVICE_SET_VISIBILITY, data) - - -@bind_hass -def set_group(hass, object_id, name=None, entity_ids=None, visible=None, - icon=None, view=None, control=None, add=None): - """Create/Update a group.""" - hass.add_job( - async_set_group, hass, object_id, name, entity_ids, visible, icon, - view, control, add) - - -@callback -@bind_hass -def async_set_group(hass, object_id, name=None, entity_ids=None, visible=None, - icon=None, view=None, control=None, add=None): - """Create/Update a group.""" - data = { - key: value for key, value in [ - (ATTR_OBJECT_ID, object_id), - (ATTR_NAME, name), - (ATTR_ENTITIES, entity_ids), - (ATTR_VISIBLE, visible), - (ATTR_ICON, icon), - (ATTR_VIEW, view), - (ATTR_CONTROL, control), - (ATTR_ADD_ENTITIES, add), - ] if value is not None - } - - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_SET, data)) - - -@bind_hass -def remove(hass, name): - """Remove a user group.""" - hass.add_job(async_remove, hass, name) - - -@callback -@bind_hass -def async_remove(hass, object_id): - """Remove a user group.""" - data = {ATTR_OBJECT_ID: object_id} - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_REMOVE, data)) - - @bind_hass def expand_entity_ids(hass, entity_ids): """Return entity_ids with group entity ids replaced by their members. diff --git a/homeassistant/components/hangouts/.translations/de.json b/homeassistant/components/hangouts/.translations/de.json index a2ed8d21230..e0f18b6cccf 100644 --- a/homeassistant/components/hangouts/.translations/de.json +++ b/homeassistant/components/hangouts/.translations/de.json @@ -14,6 +14,7 @@ "data": { "2fa": "2FA PIN" }, + "description": "Leer", "title": "2-Faktor-Authentifizierung" }, "user": { @@ -21,6 +22,7 @@ "email": "E-Mail-Adresse", "password": "Passwort" }, + "description": "Leer", "title": "Google Hangouts Login" } }, diff --git a/homeassistant/components/hangouts/.translations/ko.json b/homeassistant/components/hangouts/.translations/ko.json index aabf977a8cc..af0e76829e5 100644 --- a/homeassistant/components/hangouts/.translations/ko.json +++ b/homeassistant/components/hangouts/.translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Google Hangouts \uc740 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", - "unknown": "\uc54c \uc218\uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { "invalid_2fa": "2\ub2e8\uacc4 \uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574 \uc8fc\uc138\uc694.", diff --git a/homeassistant/components/hangouts/.translations/nl.json b/homeassistant/components/hangouts/.translations/nl.json index cf73210aa3b..da9bc9edd7b 100644 --- a/homeassistant/components/hangouts/.translations/nl.json +++ b/homeassistant/components/hangouts/.translations/nl.json @@ -14,6 +14,7 @@ "data": { "2fa": "2FA pin" }, + "description": "Leeg", "title": "Twee-factor-authenticatie" }, "user": { @@ -21,6 +22,7 @@ "email": "E-mailadres", "password": "Wachtwoord" }, + "description": "Leeg", "title": "Google Hangouts inlog" } }, diff --git a/homeassistant/components/hangouts/.translations/no.json b/homeassistant/components/hangouts/.translations/no.json index c2cdb93c005..d75092da759 100644 --- a/homeassistant/components/hangouts/.translations/no.json +++ b/homeassistant/components/hangouts/.translations/no.json @@ -14,6 +14,7 @@ "data": { "2fa": "2FA Pin" }, + "description": "Tom", "title": "Tofaktorautentisering" }, "user": { @@ -21,6 +22,7 @@ "email": "E-postadresse", "password": "Passord" }, + "description": "Tom", "title": "Google Hangouts p\u00e5logging" } }, diff --git a/homeassistant/components/hangouts/.translations/ru.json b/homeassistant/components/hangouts/.translations/ru.json index c3363215201..6d93ec0d18f 100644 --- a/homeassistant/components/hangouts/.translations/ru.json +++ b/homeassistant/components/hangouts/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Google Hangouts \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" }, "error": { diff --git a/homeassistant/components/hangouts/.translations/sv.json b/homeassistant/components/hangouts/.translations/sv.json index 90bf4e97712..ae03fdbf722 100644 --- a/homeassistant/components/hangouts/.translations/sv.json +++ b/homeassistant/components/hangouts/.translations/sv.json @@ -14,6 +14,7 @@ "data": { "2fa": "2FA Pinkod" }, + "description": "Missing english translation", "title": "Tv\u00e5faktorsautentisering" }, "user": { @@ -21,6 +22,7 @@ "email": "E-postadress", "password": "L\u00f6senord" }, + "description": "Missing english translation", "title": "Google Hangouts-inloggning" } }, diff --git a/homeassistant/components/hangouts/const.py b/homeassistant/components/hangouts/const.py index caae0de169b..5a527fae260 100644 --- a/homeassistant/components/hangouts/const.py +++ b/homeassistant/components/hangouts/const.py @@ -3,7 +3,8 @@ import logging import voluptuous as vol -from homeassistant.components.notify import ATTR_MESSAGE, ATTR_TARGET +from homeassistant.components.notify \ + import ATTR_MESSAGE, ATTR_TARGET, ATTR_DATA import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger('homeassistant.components.hangouts') @@ -56,10 +57,15 @@ MESSAGE_SEGMENT_SCHEMA = vol.Schema({ vol.Optional('parse_str'): cv.boolean, vol.Optional('link_target'): cv.string }) +MESSAGE_DATA_SCHEMA = vol.Schema({ + vol.Optional('image_file'): cv.string, + vol.Optional('image_url'): cv.string +}) MESSAGE_SCHEMA = vol.Schema({ vol.Required(ATTR_TARGET): [TARGETS_SCHEMA], - vol.Required(ATTR_MESSAGE): [MESSAGE_SEGMENT_SCHEMA] + vol.Required(ATTR_MESSAGE): [MESSAGE_SEGMENT_SCHEMA], + vol.Optional(ATTR_DATA): MESSAGE_DATA_SCHEMA }) INTENT_SCHEMA = vol.All( diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index 7edc8898c8c..8747bff9ba7 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -1,10 +1,13 @@ """The Hangouts Bot.""" +import io import logging - +import asyncio +import aiohttp +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers import dispatcher, intent from .const import ( - ATTR_MESSAGE, ATTR_TARGET, CONF_CONVERSATIONS, DOMAIN, + ATTR_MESSAGE, ATTR_TARGET, ATTR_DATA, CONF_CONVERSATIONS, DOMAIN, EVENT_HANGOUTS_CONNECTED, EVENT_HANGOUTS_CONVERSATIONS_CHANGED, EVENT_HANGOUTS_DISCONNECTED, EVENT_HANGOUTS_MESSAGE_RECEIVED, CONF_MATCHERS, CONF_CONVERSATION_ID, @@ -146,7 +149,8 @@ class HangoutsBot: is_error and conv_id in self._error_suppressed_conv_ids): await self._async_send_message( [{'text': message, 'parse_str': True}], - [{CONF_CONVERSATION_ID: conv_id}]) + [{CONF_CONVERSATION_ID: conv_id}], + None) async def _async_process(self, intents, text, conv_id): """Detect a matching intent.""" @@ -203,7 +207,7 @@ class HangoutsBot: """Run once when Home Assistant stops.""" await self.async_disconnect() - async def _async_send_message(self, message, targets): + async def _async_send_message(self, message, targets, data): conversations = [] for target in targets: conversation = None @@ -233,10 +237,48 @@ class HangoutsBot: del segment['parse_str'] messages.append(ChatMessageSegment(**segment)) + image_file = None + if data: + if data.get('image_url'): + uri = data.get('image_url') + try: + websession = async_get_clientsession(self.hass) + async with websession.get(uri, timeout=5) as response: + if response.status != 200: + _LOGGER.error( + 'Fetch image failed, %s, %s', + response.status, + response + ) + image_file = None + else: + image_data = await response.read() + image_file = io.BytesIO(image_data) + image_file.name = "image.png" + except (asyncio.TimeoutError, aiohttp.ClientError) as error: + _LOGGER.error( + 'Failed to fetch image, %s', + type(error) + ) + image_file = None + elif data.get('image_file'): + uri = data.get('image_file') + if self.hass.config.is_allowed_path(uri): + try: + image_file = open(uri, 'rb') + except IOError as error: + _LOGGER.error( + 'Image file I/O error(%s): %s', + error.errno, + error.strerror + ) + else: + _LOGGER.error('Path "%s" not allowed', uri) + if not messages: return False for conv in conversations: - await conv.send_message(messages) + await conv.send_message(messages, image_file) async def _async_list_conversations(self): import hangups @@ -261,7 +303,8 @@ class HangoutsBot: async def async_handle_send_message(self, service): """Handle the send_message service.""" await self._async_send_message(service.data[ATTR_MESSAGE], - service.data[ATTR_TARGET]) + service.data[ATTR_TARGET], + service.data[ATTR_DATA]) async def async_handle_update_users_and_conversations(self, _=None): """Handle the update_users_and_conversations service.""" diff --git a/homeassistant/components/hangouts/services.yaml b/homeassistant/components/hangouts/services.yaml index 5d314bc2479..d07f1d65688 100644 --- a/homeassistant/components/hangouts/services.yaml +++ b/homeassistant/components/hangouts/services.yaml @@ -9,4 +9,7 @@ send_message: example: '[{"id": "UgxrXzVrARmjx_C6AZx4AaABAagBo-6UCw"}, {"name": "Test Conversation"}]' message: description: List of message segments, only the "text" field is required in every segment. [Required] - example: '[{"text":"test", "is_bold": false, "is_italic": false, "is_strikethrough": false, "is_underline": false, "parse_str": false, "link_target": "http://google.com"}, ...]' \ No newline at end of file + example: '[{"text":"test", "is_bold": false, "is_italic": false, "is_strikethrough": false, "is_underline": false, "parse_str": false, "link_target": "http://google.com"}, ...]' + data: + description: Other options ['image_file' / 'image_url'] + example: '{ "image_file": "file" } or { "image_url": "url" }' diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index e0356017e3e..9516675480a 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -4,7 +4,6 @@ Exposes regular REST commands as services. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/hassio/ """ -import asyncio from datetime import timedelta import logging import os @@ -20,7 +19,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass from homeassistant.util.dt import utcnow -from .handler import HassIO +from .auth import async_setup_auth +from .handler import HassIO, HassioAPIError +from .discovery import async_setup_discovery from .http import HassIOView _LOGGER = logging.getLogger(__name__) @@ -134,58 +135,54 @@ def is_hassio(hass): @bind_hass -@asyncio.coroutine -def async_check_config(hass): +async def async_check_config(hass): """Check configuration over Hass.io API.""" hassio = hass.data[DOMAIN] - result = yield from hassio.check_homeassistant_config() - if not result: - return "Hass.io config check API error" + try: + result = await hassio.check_homeassistant_config() + except HassioAPIError as err: + _LOGGER.error("Error on Hass.io API: %s", err) + if result['result'] == "error": return result['message'] return None -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the Hass.io component.""" - try: - host = os.environ['HASSIO'] - except KeyError: - _LOGGER.error("Missing HASSIO environment variable.") - return False - - try: - os.environ['HASSIO_TOKEN'] - except KeyError: - _LOGGER.error("Missing HASSIO_TOKEN environment variable.") + # Check local setup + for env in ('HASSIO', 'HASSIO_TOKEN'): + if os.environ.get(env): + continue + _LOGGER.error("Missing %s environment variable.", env) return False + host = os.environ['HASSIO'] websession = hass.helpers.aiohttp_client.async_get_clientsession() hass.data[DOMAIN] = hassio = HassIO(hass.loop, websession, host) - if not (yield from hassio.is_connected()): + if not await hassio.is_connected(): _LOGGER.error("Not connected with Hass.io") return False store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) - data = yield from store.async_load() + data = await store.async_load() if data is None: data = {} refresh_token = None if 'hassio_user' in data: - user = yield from hass.auth.async_get_user(data['hassio_user']) + user = await hass.auth.async_get_user(data['hassio_user']) if user and user.refresh_tokens: refresh_token = list(user.refresh_tokens.values())[0] if refresh_token is None: - user = yield from hass.auth.async_create_system_user('Hass.io') - refresh_token = yield from hass.auth.async_create_refresh_token(user) + user = await hass.auth.async_create_system_user('Hass.io') + refresh_token = await hass.auth.async_create_refresh_token(user) data['hassio_user'] = user.id - yield from store.async_save(data) + await store.async_save(data) # This overrides the normal API call that would be forwarded development_repo = config.get(DOMAIN, {}).get(CONF_FRONTEND_REPO) @@ -197,7 +194,7 @@ def async_setup(hass, config): hass.http.register_view(HassIOView(host, websession)) if 'frontend' in hass.config.components: - yield from hass.components.panel_custom.async_register_panel( + await hass.components.panel_custom.async_register_panel( frontend_url_path='hassio', webcomponent_name='hassio-main', sidebar_title='Hass.io', @@ -212,13 +209,12 @@ def async_setup(hass, config): else: token = None - yield from hassio.update_hass_api(config.get('http', {}), token) + await hassio.update_hass_api(config.get('http', {}), token) if 'homeassistant' in config: - yield from hassio.update_hass_timezone(config['homeassistant']) + await hassio.update_hass_timezone(config['homeassistant']) - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Handle service calls for Hass.io.""" api_command = MAP_SERVICE_API[service.service][0] data = service.data.copy() @@ -233,39 +229,39 @@ def async_setup(hass, config): payload = data # Call API - ret = yield from hassio.send_command( - api_command.format(addon=addon, snapshot=snapshot), - payload=payload, timeout=MAP_SERVICE_API[service.service][2] - ) - - if not ret or ret['result'] != "ok": - _LOGGER.error("Error on Hass.io API: %s", ret['message']) + try: + await hassio.send_command( + api_command.format(addon=addon, snapshot=snapshot), + payload=payload, timeout=MAP_SERVICE_API[service.service][2] + ) + except HassioAPIError as err: + _LOGGER.error("Error on Hass.io API: %s", err) for service, settings in MAP_SERVICE_API.items(): hass.services.async_register( DOMAIN, service, async_service_handler, schema=settings[1]) - @asyncio.coroutine - def update_homeassistant_version(now): + async def update_homeassistant_version(now): """Update last available Home Assistant version.""" - data = yield from hassio.get_homeassistant_info() - if data: + try: + data = await hassio.get_homeassistant_info() hass.data[DATA_HOMEASSISTANT_VERSION] = data['last_version'] + except HassioAPIError as err: + _LOGGER.warning("Can't read last version: %s", err) hass.helpers.event.async_track_point_in_utc_time( update_homeassistant_version, utcnow() + HASSIO_UPDATE_INTERVAL) # Fetch last version - yield from update_homeassistant_version(None) + await update_homeassistant_version(None) - @asyncio.coroutine - def async_handle_core_service(call): + async def async_handle_core_service(call): """Service handler for handling core services.""" if call.service == SERVICE_HOMEASSISTANT_STOP: - yield from hassio.stop_homeassistant() + await hassio.stop_homeassistant() return - error = yield from async_check_config(hass) + error = await async_check_config(hass) if error: _LOGGER.error(error) hass.components.persistent_notification.async_create( @@ -274,7 +270,7 @@ def async_setup(hass, config): return if call.service == SERVICE_HOMEASSISTANT_RESTART: - yield from hassio.restart_homeassistant() + await hassio.restart_homeassistant() # Mock core services for service in (SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART, @@ -282,4 +278,10 @@ def async_setup(hass, config): hass.services.async_register( HASS_DOMAIN, service, async_handle_core_service) + # Init discovery Hass.io feature + async_setup_discovery(hass, hassio, config) + + # Init auth Hass.io feature + async_setup_auth(hass) + return True diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py new file mode 100644 index 00000000000..4be3ba9956c --- /dev/null +++ b/homeassistant/components/hassio/auth.py @@ -0,0 +1,74 @@ +"""Implement the auth feature from Hass.io for Add-ons.""" +import logging +from ipaddress import ip_address +import os + +from aiohttp import web +from aiohttp.web_exceptions import HTTPForbidden, HTTPNotFound +import voluptuous as vol + +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.exceptions import HomeAssistantError +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http.const import KEY_REAL_IP +from homeassistant.components.http.data_validator import RequestDataValidator + +from .const import ATTR_USERNAME, ATTR_PASSWORD, ATTR_ADDON + +_LOGGER = logging.getLogger(__name__) + + +SCHEMA_API_AUTH = vol.Schema({ + vol.Required(ATTR_USERNAME): cv.string, + vol.Required(ATTR_PASSWORD): cv.string, + vol.Required(ATTR_ADDON): cv.string, +}, extra=vol.ALLOW_EXTRA) + + +@callback +def async_setup_auth(hass): + """Auth setup.""" + hassio_auth = HassIOAuth(hass) + hass.http.register_view(hassio_auth) + + +class HassIOAuth(HomeAssistantView): + """Hass.io view to handle base part.""" + + name = "api:hassio_auth" + url = "/api/hassio_auth" + + def __init__(self, hass): + """Initialize WebView.""" + self.hass = hass + + @RequestDataValidator(SCHEMA_API_AUTH) + async def post(self, request, data): + """Handle new discovery requests.""" + hassio_ip = os.environ['HASSIO'].split(':')[0] + if request[KEY_REAL_IP] != ip_address(hassio_ip): + _LOGGER.error( + "Invalid auth request from %s", request[KEY_REAL_IP]) + raise HTTPForbidden() + + await self._check_login(data[ATTR_USERNAME], data[ATTR_PASSWORD]) + return web.Response(status=200) + + def _get_provider(self): + """Return Homeassistant auth provider.""" + for prv in self.hass.auth.auth_providers: + if prv.type == 'homeassistant': + return prv + + _LOGGER.error("Can't find Home Assistant auth.") + raise HTTPNotFound() + + async def _check_login(self, username, password): + """Check User credentials.""" + provider = self._get_provider() + + try: + await provider.async_validate_login(username, password) + except HomeAssistantError: + raise HTTPForbidden() from None diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py new file mode 100644 index 00000000000..c539169ebe3 --- /dev/null +++ b/homeassistant/components/hassio/const.py @@ -0,0 +1,12 @@ +"""Hass.io const variables.""" + +ATTR_DISCOVERY = 'discovery' +ATTR_ADDON = 'addon' +ATTR_NAME = 'name' +ATTR_SERVICE = 'service' +ATTR_CONFIG = 'config' +ATTR_UUID = 'uuid' +ATTR_USERNAME = 'username' +ATTR_PASSWORD = 'password' + +X_HASSIO = 'X-HASSIO-KEY' diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py new file mode 100644 index 00000000000..4c7c5a6597f --- /dev/null +++ b/homeassistant/components/hassio/discovery.py @@ -0,0 +1,114 @@ +"""Implement the serivces discovery feature from Hass.io for Add-ons.""" +import asyncio +import logging + +from aiohttp import web +from aiohttp.web_exceptions import HTTPServiceUnavailable + +from homeassistant.core import callback, CoreState +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.components.http import HomeAssistantView + +from .handler import HassioAPIError +from .const import ( + ATTR_DISCOVERY, ATTR_ADDON, ATTR_NAME, ATTR_SERVICE, ATTR_CONFIG, + ATTR_UUID) + +_LOGGER = logging.getLogger(__name__) + + +@callback +def async_setup_discovery(hass, hassio, config): + """Discovery setup.""" + hassio_discovery = HassIODiscovery(hass, hassio, config) + + # Handle exists discovery messages + async def async_discovery_start_handler(event): + """Process all exists discovery on startup.""" + try: + data = await hassio.retrieve_discovery_messages() + except HassioAPIError as err: + _LOGGER.error("Can't read discover info: %s", err) + return + + jobs = [hassio_discovery.async_process_new(discovery) + for discovery in data[ATTR_DISCOVERY]] + if jobs: + await asyncio.wait(jobs) + + if hass.state == CoreState.running: + hass.async_create_task(async_discovery_start_handler(None)) + else: + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, async_discovery_start_handler) + + hass.http.register_view(hassio_discovery) + + +class HassIODiscovery(HomeAssistantView): + """Hass.io view to handle base part.""" + + name = "api:hassio_push:discovery" + url = "/api/hassio_push/discovery/{uuid}" + + def __init__(self, hass, hassio, config): + """Initialize WebView.""" + self.hass = hass + self.hassio = hassio + self.config = config + + async def post(self, request, uuid): + """Handle new discovery requests.""" + # Fetch discovery data and prevent injections + try: + data = await self.hassio.get_discovery_message(uuid) + except HassioAPIError as err: + _LOGGER.error("Can't read discovey data: %s", err) + raise HTTPServiceUnavailable() from None + + await self.async_process_new(data) + return web.Response() + + async def delete(self, request, uuid): + """Handle remove discovery requests.""" + data = request.json() + + await self.async_process_del(data) + return web.Response() + + async def async_process_new(self, data): + """Process add discovery entry.""" + service = data[ATTR_SERVICE] + config_data = data[ATTR_CONFIG] + + # Read addinional Add-on info + try: + addon_info = await self.hassio.get_addon_info(data[ATTR_ADDON]) + except HassioAPIError as err: + _LOGGER.error("Can't read add-on info: %s", err) + return + config_data[ATTR_ADDON] = addon_info[ATTR_NAME] + + # Use config flow + await self.hass.config_entries.flow.async_init( + service, context={'source': 'hassio'}, data=config_data) + + async def async_process_del(self, data): + """Process remove discovery entry.""" + service = data[ATTR_SERVICE] + uuid = data[ATTR_UUID] + + # Check if realy deletet / prevent injections + try: + data = await self.hassio.get_discovery_message(uuid) + except HassioAPIError: + pass + else: + _LOGGER.warning("Retrieve wrong unload for %s", service) + return + + # Use config flow + for entry in self.hass.config_entries.async_entries(service): + if entry.source != 'hassio': + continue + await self.hass.config_entries.async_remove(entry) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index d75529a99b0..91019776eeb 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -16,17 +16,24 @@ from homeassistant.components.http import ( CONF_SSL_CERTIFICATE) from homeassistant.const import CONF_TIME_ZONE, SERVER_PORT +from .const import X_HASSIO + _LOGGER = logging.getLogger(__name__) -X_HASSIO = 'X-HASSIO-KEY' + +class HassioAPIError(RuntimeError): + """Return if a API trow a error.""" def _api_bool(funct): """Return a boolean.""" async def _wrapper(*argv, **kwargs): """Wrap function.""" - data = await funct(*argv, **kwargs) - return data and data['result'] == "ok" + try: + data = await funct(*argv, **kwargs) + return data['result'] == "ok" + except HassioAPIError: + return False return _wrapper @@ -36,9 +43,9 @@ def _api_data(funct): async def _wrapper(*argv, **kwargs): """Wrap function.""" data = await funct(*argv, **kwargs) - if data and data['result'] == "ok": + if data['result'] == "ok": return data['data'] - return None + raise HassioAPIError(data['message']) return _wrapper @@ -68,6 +75,15 @@ class HassIO: """ return self.send_command("/homeassistant/info", method="get") + @_api_data + def get_addon_info(self, addon): + """Return data for a Add-on. + + This method return a coroutine. + """ + return self.send_command( + "/addons/{}/info".format(addon), method="get") + @_api_bool def restart_homeassistant(self): """Restart Home-Assistant container. @@ -91,6 +107,22 @@ class HassIO: """ return self.send_command("/homeassistant/check", timeout=300) + @_api_data + def retrieve_discovery_messages(self): + """Return all discovery data from Hass.io API. + + This method return a coroutine. + """ + return self.send_command("/discovery", method="get") + + @_api_data + def get_discovery_message(self, uuid): + """Return a single discovery data message. + + This method return a coroutine. + """ + return self.send_command("/discovery/{}".format(uuid), method="get") + @_api_bool async def update_hass_api(self, http_config, refresh_token): """Update Home Assistant API data on Hass.io.""" @@ -120,15 +152,15 @@ class HassIO: 'timezone': core_config.get(CONF_TIME_ZONE) }) - @asyncio.coroutine - def send_command(self, command, method="post", payload=None, timeout=10): + async def send_command(self, command, method="post", payload=None, + timeout=10): """Send API command to Hass.io. This method is a coroutine. """ try: with async_timeout.timeout(timeout, loop=self.loop): - request = yield from self.websession.request( + request = await self.websession.request( method, "http://{}{}".format(self._ip, command), json=payload, headers={ X_HASSIO: os.environ.get('HASSIO_TOKEN', "") @@ -137,9 +169,9 @@ class HassIO: if request.status not in (200, 400): _LOGGER.error( "%s return code %d.", command, request.status) - return None + raise HassioAPIError() - answer = yield from request.json() + answer = await request.json() return answer except asyncio.TimeoutError: @@ -148,4 +180,4 @@ class HassIO: except aiohttp.ClientError as err: _LOGGER.error("Client error on %s request %s", command, err) - return None + raise HassioAPIError() diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 55cc7f54787..c3bd18fa9bb 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -18,9 +18,10 @@ from aiohttp.web_exceptions import HTTPBadGateway from homeassistant.const import CONTENT_TYPE_TEXT_PLAIN from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView +from .const import X_HASSIO + _LOGGER = logging.getLogger(__name__) -X_HASSIO = 'X-HASSIO-KEY' NO_TIMEOUT = re.compile( r'^(?:' @@ -54,15 +55,14 @@ class HassIOView(HomeAssistantView): self._host = host self._websession = websession - @asyncio.coroutine - def _handle(self, request, path): + async def _handle(self, request, path): """Route data to Hass.io.""" if _need_auth(path) and not request[KEY_AUTHENTICATED]: return web.Response(status=401) - client = yield from self._command_proxy(path, request) + client = await self._command_proxy(path, request) - data = yield from client.read() + data = await client.read() if path.endswith('/logs'): return _create_response_log(client, data) return _create_response(client, data) @@ -70,8 +70,7 @@ class HassIOView(HomeAssistantView): get = _handle post = _handle - @asyncio.coroutine - def _command_proxy(self, path, request): + async def _command_proxy(self, path, request): """Return a client request with proxy origin for Hass.io supervisor. This method is a coroutine. @@ -83,14 +82,14 @@ class HassIOView(HomeAssistantView): data = None headers = {X_HASSIO: os.environ.get('HASSIO_TOKEN', "")} with async_timeout.timeout(10, loop=hass.loop): - data = yield from request.read() + data = await request.read() if data: headers[CONTENT_TYPE] = request.content_type else: data = None method = getattr(self._websession, request.method.lower()) - client = yield from method( + client = await method( "http://{}/{}".format(self._host, path), data=data, headers=headers, timeout=read_timeout ) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 8c12243ee8f..5d7733584be 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -24,14 +24,17 @@ from .const import ( BRIDGE_NAME, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FEATURE_LIST, CONF_FILTER, DEFAULT_AUTO_START, DEFAULT_PORT, DEVICE_CLASS_CO, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, DOMAIN, HOMEKIT_FILE, - SERVICE_HOMEKIT_START, TYPE_OUTLET, TYPE_SWITCH) + SERVICE_HOMEKIT_START, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, + TYPE_SPRINKLER, TYPE_SWITCH, TYPE_VALVE) from .util import ( show_setup_message, validate_entity_config, validate_media_player_features) -TYPES = Registry() +REQUIREMENTS = ['HAP-python==2.2.2'] + _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['HAP-python==2.2.2'] +MAX_DEVICES = 100 +TYPES = Registry() # #### Driver Status #### STATUS_READY = 0 @@ -39,8 +42,13 @@ STATUS_RUNNING = 1 STATUS_STOPPED = 2 STATUS_WAIT = 3 -SWITCH_TYPES = {TYPE_OUTLET: 'Outlet', - TYPE_SWITCH: 'Switch'} +SWITCH_TYPES = { + TYPE_FAUCET: 'Valve', + TYPE_OUTLET: 'Outlet', + TYPE_SHOWER: 'Valve', + TYPE_SPRINKLER: 'Valve', + TYPE_SWITCH: 'Switch', + TYPE_VALVE: 'Valve'} CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.All({ @@ -239,6 +247,10 @@ class HomeKit(): if not self.driver.state.paired: show_setup_message(self.hass, self.driver.state.pincode) + if len(self.bridge.accessories) > MAX_DEVICES: + _LOGGER.warning('You have exceeded the device limit, which might ' + 'cause issues. Consider using the filter option.') + _LOGGER.debug('Driver start') self.hass.add_job(self.driver.start) self.status = STATUS_RUNNING diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index df488d4a73a..617dd3f4f22 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -32,8 +32,12 @@ BRIDGE_SERIAL_NUMBER = 'homekit.bridge' MANUFACTURER = 'Home Assistant' # #### Switch Types #### +TYPE_FAUCET = 'faucet' TYPE_OUTLET = 'outlet' +TYPE_SHOWER = 'shower' +TYPE_SPRINKLER = 'sprinkler' TYPE_SWITCH = 'switch' +TYPE_VALVE = 'valve' # #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' @@ -57,6 +61,7 @@ SERV_SMOKE_SENSOR = 'SmokeSensor' SERV_SWITCH = 'Switch' SERV_TEMPERATURE_SENSOR = 'TemperatureSensor' SERV_THERMOSTAT = 'Thermostat' +SERV_VALVE = 'Valve' SERV_WINDOW_COVERING = 'WindowCovering' # #### Characteristics #### @@ -85,6 +90,7 @@ CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' CHAR_FIRMWARE_REVISION = 'FirmwareRevision' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' CHAR_HUE = 'Hue' +CHAR_IN_USE = 'InUse' CHAR_LEAK_DETECTED = 'LeakDetected' CHAR_LOCK_CURRENT_STATE = 'LockCurrentState' CHAR_LOCK_TARGET_STATE = 'LockTargetState' @@ -109,6 +115,7 @@ CHAR_TARGET_POSITION = 'TargetPosition' CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState' CHAR_TARGET_TEMPERATURE = 'TargetTemperature' CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits' +CHAR_VALVE_TYPE = 'ValveType' # #### Properties #### PROP_MAX_VALUE = 'maxValue' diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index a5724057eee..82a5d68d644 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -5,15 +5,29 @@ from pyhap.const import CATEGORY_OUTLET, CATEGORY_SWITCH from homeassistant.components.switch import DOMAIN from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON) + ATTR_ENTITY_ID, CONF_TYPE, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON) from homeassistant.core import split_entity_id from . import TYPES from .accessories import HomeAccessory -from .const import CHAR_ON, CHAR_OUTLET_IN_USE, SERV_OUTLET, SERV_SWITCH +from .const import ( + CHAR_ACTIVE, CHAR_IN_USE, CHAR_ON, CHAR_OUTLET_IN_USE, CHAR_VALVE_TYPE, + SERV_OUTLET, SERV_SWITCH, SERV_VALVE, TYPE_FAUCET, TYPE_SHOWER, + TYPE_SPRINKLER, TYPE_VALVE) _LOGGER = logging.getLogger(__name__) +CATEGORY_SPRINKLER = 28 +CATEGORY_FAUCET = 29 +CATEGORY_SHOWER_HEAD = 30 + +VALVE_TYPE = { + TYPE_FAUCET: (CATEGORY_FAUCET, 3), + TYPE_SHOWER: (CATEGORY_SHOWER_HEAD, 2), + TYPE_SPRINKLER: (CATEGORY_SPRINKLER, 1), + TYPE_VALVE: (CATEGORY_FAUCET, 0), +} + @TYPES.register('Outlet') class Outlet(HomeAccessory): @@ -80,3 +94,43 @@ class Switch(HomeAccessory): self.entity_id, current_state) self.char_on.set_value(current_state) self.flag_target_state = False + + +@TYPES.register('Valve') +class Valve(HomeAccessory): + """Generate a Valve accessory.""" + + def __init__(self, *args): + """Initialize a Valve accessory object.""" + super().__init__(*args) + self.flag_target_state = False + valve_type = self.config[CONF_TYPE] + self.category = VALVE_TYPE[valve_type][0] + + serv_valve = self.add_preload_service(SERV_VALVE) + self.char_active = serv_valve.configure_char( + CHAR_ACTIVE, value=False, setter_callback=self.set_state) + self.char_in_use = serv_valve.configure_char( + CHAR_IN_USE, value=False) + self.char_valve_type = serv_valve.configure_char( + CHAR_VALVE_TYPE, value=VALVE_TYPE[valve_type][1]) + + def set_state(self, value): + """Move value state to value if call came from HomeKit.""" + _LOGGER.debug('%s: Set switch state to %s', + self.entity_id, value) + self.flag_target_state = True + self.char_in_use.set_value(value) + params = {ATTR_ENTITY_ID: self.entity_id} + service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF + self.hass.services.call(DOMAIN, service, params) + + def update_state(self, new_state): + """Update switch state after state changed.""" + current_state = (new_state.state == STATE_ON) + if not self.flag_target_state: + _LOGGER.debug('%s: Set current state to %s', + self.entity_id, current_state) + self.char_active.set_value(current_state) + self.char_in_use.set_value(current_state) + self.flag_target_state = False diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 9d60530edd7..4dd7396cf8d 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -11,8 +11,8 @@ import homeassistant.helpers.config_validation as cv import homeassistant.util.temperature as temp_util from .const import ( CONF_FEATURE, CONF_FEATURE_LIST, HOMEKIT_NOTIFY_ID, FEATURE_ON_OFF, - FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, TYPE_OUTLET, - TYPE_SWITCH) + FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, TYPE_FAUCET, + TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER, TYPE_SWITCH, TYPE_VALVE) _LOGGER = logging.getLogger(__name__) @@ -38,12 +38,17 @@ MEDIA_PLAYER_SCHEMA = vol.Schema({ SWITCH_TYPE_SCHEMA = BASIC_INFO_SCHEMA.extend({ vol.Optional(CONF_TYPE, default=TYPE_SWITCH): vol.All( - cv.string, vol.In((TYPE_OUTLET, TYPE_SWITCH))), + cv.string, vol.In(( + TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER, + TYPE_SWITCH, TYPE_VALVE))), }) def validate_entity_config(values): """Validate config entry for CONF_ENTITY.""" + if not isinstance(values, dict): + raise vol.Invalid('expected a dictionary') + entities = {} for entity_id, config in values.items(): entity = cv.entity_id(entity_id) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 5e24fe82340..5431dd4a61a 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -13,6 +13,7 @@ import uuid from homeassistant.components.discovery import SERVICE_HOMEKIT from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import call_later REQUIREMENTS = ['homekit==0.10'] @@ -37,6 +38,13 @@ KNOWN_DEVICES = "{}-devices".format(DOMAIN) _LOGGER = logging.getLogger(__name__) +REQUEST_TIMEOUT = 5 # seconds +RETRY_INTERVAL = 60 # seconds + + +class HomeKitConnectionError(ConnectionError): + """Raised when unable to connect to target device.""" + def homekit_http_send(self, message_body=None, encode_chunked=False): r"""Send the currently buffered request and clear the buffer. @@ -89,6 +97,9 @@ class HKDevice(): self.config_num = config_num self.config = config self.configurator = hass.components.configurator + self.conn = None + self.securecon = None + self._connection_warning_logged = False data_dir = os.path.join(hass.config.path(), HOMEKIT_DIR) if not os.path.isdir(data_dir): @@ -101,23 +112,35 @@ class HKDevice(): # pylint: disable=protected-access http.client.HTTPConnection._send_output = homekit_http_send - self.conn = http.client.HTTPConnection(self.host, port=self.port) if self.pairing_data is not None: self.accessory_setup() else: self.configure() + def connect(self): + """Open the connection to the HomeKit device.""" + # pylint: disable=import-error + import homekit + + self.conn = http.client.HTTPConnection( + self.host, port=self.port, timeout=REQUEST_TIMEOUT) + if self.pairing_data is not None: + controllerkey, accessorykey = \ + homekit.get_session_keys(self.conn, self.pairing_data) + self.securecon = homekit.SecureHttp( + self.conn.sock, accessorykey, controllerkey) + def accessory_setup(self): """Handle setup of a HomeKit accessory.""" # pylint: disable=import-error import homekit - self.controllerkey, self.accessorykey = \ - homekit.get_session_keys(self.conn, self.pairing_data) - self.securecon = homekit.SecureHttp(self.conn.sock, - self.accessorykey, - self.controllerkey) - response = self.securecon.get('/accessories') - data = json.loads(response.read().decode()) + + try: + data = self.get_json('/accessories') + except HomeKitConnectionError: + call_later( + self.hass, RETRY_INTERVAL, lambda _: self.accessory_setup()) + return for accessory in data['accessories']: serial = get_serial(accessory) if serial in self.hass.data[KNOWN_ACCESSORIES]: @@ -135,6 +158,31 @@ class HKDevice(): discovery.load_platform(self.hass, component, DOMAIN, service_info, self.config) + def get_json(self, target): + """Get JSON data from the device.""" + try: + if self.conn is None: + self.connect() + response = self.securecon.get(target) + data = json.loads(response.read().decode()) + + # After a successful connection, clear the warning logged status + self._connection_warning_logged = False + + return data + except (ConnectionError, OSError, json.JSONDecodeError) as ex: + # Mark connection as failed + if not self._connection_warning_logged: + _LOGGER.warning("Failed to connect to homekit device", + exc_info=ex) + self._connection_warning_logged = True + else: + _LOGGER.debug("Failed to connect to homekit device", + exc_info=ex) + self.conn = None + self.securecon = None + raise HomeKitConnectionError() from ex + def device_config_callback(self, callback_data): """Handle initial pairing.""" # pylint: disable=import-error @@ -142,6 +190,7 @@ class HKDevice(): pairing_id = str(uuid.uuid4()) code = callback_data.get('code').strip() try: + self.connect() self.pairing_data = homekit.perform_pair_setup(self.conn, code, pairing_id) except homekit.exception.UnavailableError: @@ -192,7 +241,7 @@ class HomeKitEntity(Entity): def __init__(self, accessory, devinfo): """Initialise a generic HomeKit device.""" self._name = accessory.model - self._securecon = accessory.securecon + self._accessory = accessory self._aid = devinfo['aid'] self._iid = devinfo['iid'] self._address = "homekit-{}-{}".format(devinfo['serial'], self._iid) @@ -201,8 +250,10 @@ class HomeKitEntity(Entity): def update(self): """Obtain a HomeKit device's state.""" - response = self._securecon.get('/accessories') - data = json.loads(response.read().decode()) + try: + data = self._accessory.get_json('/accessories') + except HomeKitConnectionError: + return for accessory in data['accessories']: if accessory['aid'] != self._aid: continue @@ -222,6 +273,11 @@ class HomeKitEntity(Entity): """Return the name of the device if any.""" return self._name + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._accessory.conn is not None + def update_characteristics(self, characteristics): """Synchronise a HomeKit device state with Home Assistant.""" raise NotImplementedError @@ -229,7 +285,7 @@ class HomeKitEntity(Entity): def put_characteristics(self, characteristics): """Control a HomeKit device state from Home Assistant.""" body = json.dumps({'characteristics': characteristics}) - self._securecon.put('/characteristics', body) + self._accessory.securecon.put('/characteristics', body) def setup(hass, config): diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 4e6b3f04ee1..927f86b590d 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -4,7 +4,6 @@ Support for HomeMatic devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/homematic/ """ -import asyncio from datetime import timedelta from functools import partial import logging @@ -18,9 +17,8 @@ from homeassistant.const import ( from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.loader import bind_hass -REQUIREMENTS = ['pyhomematic==0.1.49'] +REQUIREMENTS = ['pyhomematic==0.1.50'] _LOGGER = logging.getLogger(__name__) @@ -245,78 +243,6 @@ SCHEMA_SERVICE_PUT_PARAMSET = vol.Schema({ }) -@bind_hass -def virtualkey(hass, address, channel, param, interface=None): - """Send virtual keypress to homematic controller.""" - data = { - ATTR_ADDRESS: address, - ATTR_CHANNEL: channel, - ATTR_PARAM: param, - ATTR_INTERFACE: interface, - } - - hass.services.call(DOMAIN, SERVICE_VIRTUALKEY, data) - - -@bind_hass -def set_variable_value(hass, entity_id, value): - """Change value of a Homematic system variable.""" - data = { - ATTR_ENTITY_ID: entity_id, - ATTR_VALUE: value, - } - - hass.services.call(DOMAIN, SERVICE_SET_VARIABLE_VALUE, data) - - -@bind_hass -def set_device_value(hass, address, channel, param, value, interface=None): - """Call setValue XML-RPC method of supplied interface.""" - data = { - ATTR_ADDRESS: address, - ATTR_CHANNEL: channel, - ATTR_PARAM: param, - ATTR_VALUE: value, - ATTR_INTERFACE: interface, - } - - hass.services.call(DOMAIN, SERVICE_SET_DEVICE_VALUE, data) - - -@bind_hass -def put_paramset(hass, interface, address, paramset_key, paramset): - """Call putParamset XML-RPC method of supplied interface.""" - data = { - ATTR_INTERFACE: interface, - ATTR_ADDRESS: address, - ATTR_PARAMSET_KEY: paramset_key, - ATTR_PARAMSET: paramset, - } - - hass.services.call(DOMAIN, SERVICE_PUT_PARAMSET, data) - - -@bind_hass -def set_install_mode(hass, interface, mode=None, time=None, address=None): - """Call setInstallMode XML-RPC method of supplied interface.""" - data = { - key: value for key, value in ( - (ATTR_INTERFACE, interface), - (ATTR_MODE, mode), - (ATTR_TIME, time), - (ATTR_ADDRESS, address) - ) if value - } - - hass.services.call(DOMAIN, SERVICE_SET_INSTALL_MODE, data) - - -@bind_hass -def reconnect(hass): - """Reconnect to CCU/Homegear.""" - hass.services.call(DOMAIN, SERVICE_RECONNECT, {}) - - def setup(hass, config): """Set up the Homematic component.""" from pyhomematic import HMConnection @@ -788,10 +714,9 @@ class HMDevice(Entity): if self._state: self._state = self._state.upper() - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Load data init callbacks.""" - yield from self.hass.async_add_job(self.link_homematic) + await self.hass.async_add_job(self.link_homematic) @property def unique_id(self): diff --git a/homeassistant/components/homematicip_cloud/.translations/hu.json b/homeassistant/components/homematicip_cloud/.translations/hu.json index f2f22e6a49d..cfb4f5e87fd 100644 --- a/homeassistant/components/homematicip_cloud/.translations/hu.json +++ b/homeassistant/components/homematicip_cloud/.translations/hu.json @@ -1,5 +1,21 @@ { "config": { + "abort": { + "connection_aborted": "Nem siker\u00fclt csatlakozni a HMIP szerverhez", + "unknown": "Unknown error occurred." + }, + "error": { + "invalid_pin": "\u00c9rv\u00e9nytelen PIN, pr\u00f3b\u00e1lkozz \u00fajra.", + "press_the_button": "Nyomd meg a k\u00e9k gombot.", + "register_failed": "Regisztr\u00e1ci\u00f3 nem siker\u00fclt, pr\u00f3b\u00e1ld \u00fajra." + }, + "step": { + "init": { + "data": { + "pin": "Pin k\u00f3d (opcion\u00e1lis)" + } + } + }, "title": "HomematicIP Felh\u0151" } } \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/ko.json b/homeassistant/components/homematicip_cloud/.translations/ko.json index 7b8dc8b5087..46ef55c9eca 100644 --- a/homeassistant/components/homematicip_cloud/.translations/ko.json +++ b/homeassistant/components/homematicip_cloud/.translations/ko.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\uc561\uc138\uc2a4 \ud3ec\uc778\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "connection_aborted": "HMIP \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", - "unknown": "\uc54c \uc218\uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { "invalid_pin": "PIN\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", diff --git a/homeassistant/components/homematicip_cloud/.translations/ru.json b/homeassistant/components/homematicip_cloud/.translations/ru.json index ef2b3be4a64..ae67c616f3f 100644 --- a/homeassistant/components/homematicip_cloud/.translations/ru.json +++ b/homeassistant/components/homematicip_cloud/.translations/ru.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u0422\u043e\u0447\u043a\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430", "connection_aborted": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 HMIP", - "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" + "unknown": "\u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { "invalid_pin": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 PIN-\u043a\u043e\u0434, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", @@ -18,7 +18,7 @@ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u043f\u0440\u0435\u0444\u0438\u043a\u0441 \u0434\u043b\u044f \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u0432\u0441\u0435\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432)", "pin": "PIN-\u043a\u043e\u0434 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" }, - "title": "\u0412\u044b\u0431\u0438\u0440\u0438\u0442\u0435 \u0442\u043e\u0447\u043a\u0443 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 HomematicIP" + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u043e\u0447\u043a\u0443 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 HomematicIP" }, "link": { "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u0441\u0438\u043d\u044e\u044e \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0442\u043e\u0447\u043a\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0438 \u043a\u043d\u043e\u043f\u043a\u0443 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c HomematicIP \u0432 Home Assistant. \n\n ![\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043a\u043d\u043e\u043f\u043a\u0438](/static/images/config_flows/config_homematicip_cloud.png)", diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 9c335befda4..c43f0e24e2b 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -36,7 +36,7 @@ class HomematicipGenericDevice(Entity): """Register callbacks.""" self._device.on_update(self._device_changed) - def _device_changed(self, json, **kwargs): + def _device_changed(self, *args, **kwargs): """Handle device state changes.""" _LOGGER.debug("Event %s (%s)", self.name, self._device.modelType) self.async_schedule_update_ha_state() @@ -61,6 +61,11 @@ class HomematicipGenericDevice(Entity): """Device available.""" return not self._device.unreach + @property + def unique_id(self): + """Return a unique ID.""" + return "{}_{}".format(self.__class__.__name__, self._device.id) + @property def icon(self): """Return the icon.""" diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 6fddc7c001e..d79e7c1ee14 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -142,7 +142,7 @@ class HomematicipHAP: # Explicitly getting an update as device states might have # changed during access point disconnect.""" - job = self.hass.async_add_job(self.get_state()) + job = self.hass.async_create_task(self.get_state()) job.add_done_callback(self.get_state_finished) async def get_state(self): @@ -161,7 +161,7 @@ class HomematicipHAP: # so reconnect loop is taking over. _LOGGER.error( "Updating state after HMIP access point reconnect failed") - self.hass.async_add_job(self.home.disable_events()) + self.hass.async_create_task(self.home.disable_events()) def set_all_to_unavailable(self): """Set all devices to unavailable and tell Home Assistant.""" @@ -212,7 +212,7 @@ class HomematicipHAP: "Retrying in %d seconds", self.config_entry.data.get(HMIPC_HAPID), retry_delay) try: - self._retry_task = self.hass.async_add_job(asyncio.sleep( + self._retry_task = self.hass.async_create_task(asyncio.sleep( retry_delay, loop=self.hass.loop)) await self._retry_task except asyncio.CancelledError: diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index a18b4de7a10..bcc86b36dbe 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -112,6 +112,7 @@ async def async_validate_auth_header(request, api_password=None): if refresh_token is None: return False + request['hass_refresh_token'] = refresh_token request['hass_user'] = refresh_token.user return True diff --git a/homeassistant/components/huawei_lte.py b/homeassistant/components/huawei_lte.py index 33da6be56db..ad134d8c60e 100644 --- a/homeassistant/components/huawei_lte.py +++ b/homeassistant/components/huawei_lte.py @@ -21,7 +21,7 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['huawei-lte-api==1.0.12'] +REQUIREMENTS = ['huawei-lte-api==1.0.16'] MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) diff --git a/homeassistant/components/hue/.translations/ko.json b/homeassistant/components/hue/.translations/ko.json index 47306a35414..a4a8051663e 100644 --- a/homeassistant/components/hue/.translations/ko.json +++ b/homeassistant/components/hue/.translations/ko.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "all_configured": "\ubaa8\ub4e0 \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", - "already_configured": "\ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", + "all_configured": "\ubaa8\ub4e0 \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_configured": "\ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "cannot_connect": "\ube0c\ub9ac\uc9c0\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "discover_timeout": "Hue \ube0c\ub9bf\uc9c0\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "no_bridges": "\ubc1c\uacac\ub41c \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", - "unknown": "\uc54c \uc218\uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "linking": "\uc54c \uc218\uc5c6\ub294 \uc5f0\uacb0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", + "linking": "\uc54c \uc218 \uc5c6\ub294 \uc5f0\uacb0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "register_failed": "\ub4f1\ub85d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\ubcf4\uc138\uc694" }, "step": { diff --git a/homeassistant/components/hue/.translations/no.json b/homeassistant/components/hue/.translations/no.json index 309e9f6a299..02dd6ef7128 100644 --- a/homeassistant/components/hue/.translations/no.json +++ b/homeassistant/components/hue/.translations/no.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/ru.json b/homeassistant/components/hue/.translations/ru.json index b471dd1a0cd..4b2581dde65 100644 --- a/homeassistant/components/hue/.translations/ru.json +++ b/homeassistant/components/hue/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "all_configured": "\u0412\u0441\u0435 Philips Hue \u0448\u043b\u044e\u0437\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b", - "already_configured": "\u0428\u043b\u044e\u0437 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "cannot_connect": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443", "discover_timeout": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u0448\u043b\u044e\u0437\u044b Philips Hue", "no_bridges": "\u0428\u043b\u044e\u0437\u044b Philips Hue \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b", diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 7a781c99f53..9c28d08054b 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -12,9 +12,9 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_FILENAME, CONF_HOST from homeassistant.helpers import ( - aiohttp_client, config_validation as cv, device_registry as dr) + config_validation as cv, device_registry as dr) -from .const import DOMAIN, API_NUPNP +from .const import DOMAIN from .bridge import HueBridge # Loading the config flow file will register the flow from .config_flow import configured_hosts @@ -62,37 +62,11 @@ async def async_setup(hass, config): configured = configured_hosts(hass) # User has configured bridges - if CONF_BRIDGES in conf: - bridges = conf[CONF_BRIDGES] - - # Component is part of config but no bridges specified, discover. - elif DOMAIN in config: - # discover from nupnp - websession = aiohttp_client.async_get_clientsession(hass) - - async with websession.get(API_NUPNP) as req: - hosts = await req.json() - - bridges = [] - for entry in hosts: - # Filter out already configured hosts - if entry['internalipaddress'] in configured: - continue - - # Run through config schema to populate defaults - bridges.append(BRIDGE_CONFIG_SCHEMA({ - CONF_HOST: entry['internalipaddress'], - # Careful with using entry['id'] for other reasons. The - # value is in lowercase but is returned uppercase from hub. - CONF_FILENAME: '.hue_{}.conf'.format(entry['id']), - })) - else: - # Component not specified in config, we're loaded via discovery - bridges = [] - - if not bridges: + if CONF_BRIDGES not in conf: return True + bridges = conf[CONF_BRIDGES] + for bridge_conf in bridges: host = bridge_conf[CONF_HOST] @@ -108,7 +82,7 @@ async def async_setup(hass, config): # this component we'll have to use hass.async_add_job to avoid a # 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( + hass.async_create_task(hass.config_entries.flow.async_init( DOMAIN, context={'source': config_entries.SOURCE_IMPORT}, data={ 'host': bridge_conf[CONF_HOST], diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 874c18aaa7e..93241622f0b 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -50,7 +50,7 @@ class HueBridge: # We are going to fail the config entry setup and initiate a new # linking procedure. When linking succeeds, it will remove the # old config entry. - hass.async_add_job(hass.config_entries.flow.async_init( + hass.async_create_task(hass.config_entries.flow.async_init( DOMAIN, context={'source': config_entries.SOURCE_IMPORT}, data={ 'host': host, diff --git a/homeassistant/components/hydrawise.py b/homeassistant/components/hydrawise.py index 0c4db63034e..5a045a083b3 100644 --- a/homeassistant/components/hydrawise.py +++ b/homeassistant/components/hydrawise.py @@ -4,7 +4,6 @@ Support for Hydrawise cloud. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/hydrawise/ """ -import asyncio from datetime import timedelta import logging @@ -127,8 +126,7 @@ class HydrawiseEntity(Entity): """Return the name of the sensor.""" return self._name - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" async_dispatcher_connect( self.hass, SIGNAL_UPDATE_HYDRAWISE, self._update_callback) diff --git a/homeassistant/components/ifttt.py b/homeassistant/components/ifttt.py deleted file mode 100644 index 9497282ab21..00000000000 --- a/homeassistant/components/ifttt.py +++ /dev/null @@ -1,74 +0,0 @@ -""" -Support to trigger Maker IFTTT recipes. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/ifttt/ -""" -import logging - -import requests -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv - -REQUIREMENTS = ['pyfttt==0.3'] - -_LOGGER = logging.getLogger(__name__) - -ATTR_EVENT = 'event' -ATTR_VALUE1 = 'value1' -ATTR_VALUE2 = 'value2' -ATTR_VALUE3 = 'value3' - -CONF_KEY = 'key' - -DOMAIN = 'ifttt' - -SERVICE_TRIGGER = 'trigger' - -SERVICE_TRIGGER_SCHEMA = vol.Schema({ - vol.Required(ATTR_EVENT): cv.string, - vol.Optional(ATTR_VALUE1): cv.string, - vol.Optional(ATTR_VALUE2): cv.string, - vol.Optional(ATTR_VALUE3): cv.string, -}) - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_KEY): cv.string, - }), -}, extra=vol.ALLOW_EXTRA) - - -def trigger(hass, event, value1=None, value2=None, value3=None): - """Trigger a Maker IFTTT recipe.""" - data = { - ATTR_EVENT: event, - ATTR_VALUE1: value1, - ATTR_VALUE2: value2, - ATTR_VALUE3: value3, - } - hass.services.call(DOMAIN, SERVICE_TRIGGER, data) - - -def setup(hass, config): - """Set up the IFTTT service component.""" - key = config[DOMAIN][CONF_KEY] - - def trigger_service(call): - """Handle IFTTT trigger service calls.""" - event = call.data[ATTR_EVENT] - value1 = call.data.get(ATTR_VALUE1) - value2 = call.data.get(ATTR_VALUE2) - value3 = call.data.get(ATTR_VALUE3) - - try: - import pyfttt - pyfttt.send_event(key, event, value1, value2, value3) - except requests.exceptions.RequestException: - _LOGGER.exception("Error communicating with IFTTT") - - hass.services.register(DOMAIN, SERVICE_TRIGGER, trigger_service, - schema=SERVICE_TRIGGER_SCHEMA) - - return True diff --git a/homeassistant/components/ifttt/.translations/ca.json b/homeassistant/components/ifttt/.translations/ca.json new file mode 100644 index 00000000000..f93fbe19078 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La vostra inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de IFTTT.", + "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." + }, + "create_entry": { + "default": "Per enviar esdeveniments a Home Assistant, necessitareu utilitzar l'acci\u00f3 \"Make a web resquest\" de [IFTTT Webhook applet]({applet_url}). \n\n Ompliu la informaci\u00f3 seg\u00fcent: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\nConsulteu [la documentaci\u00f3]({docs_url}) sobre com configurar els automatismes per gestionar les dades entrants." + }, + "step": { + "user": { + "description": "Esteu segur que voleu configurar IFTTT?", + "title": "Configureu la miniaplicaci\u00f3 Webhook de IFTTT" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/de.json b/homeassistant/components/ifttt/.translations/de.json new file mode 100644 index 00000000000..b8fdc819753 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Auf Ihre Home Assistant-Instanz muss vom Internet aus zugegriffen werden k\u00f6nnen, um IFTTT-Nachrichten zu empfangen.", + "one_instance_allowed": "Nur eine einzige Instanz ist notwendig." + }, + "create_entry": { + "default": "Um Ereignisse an den Home Assistant zu senden, m\u00fcssen Sie die Aktion \"Eine Webanforderung erstellen\" aus dem [IFTTT Webhook Applet]({applet_url}) ausw\u00e4hlen.\n\nF\u00fcllen Sie folgende Informationen aus: \n- URL: `{webhook_url}`\n- Methode: POST\n- Inhaltstyp: application/json\n\nIn der Dokumentation ({docs_url}) finden Sie Informationen zur Konfiguration der Automation eingehender Daten." + }, + "step": { + "user": { + "description": "Bist du sicher, dass du IFTTT einrichten m\u00f6chtest?", + "title": "Einrichten des IFTTT Webhook Applets" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/en.json b/homeassistant/components/ifttt/.translations/en.json new file mode 100644 index 00000000000..dae4b24de47 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive IFTTT messages.", + "one_instance_allowed": "Only a single instance is necessary." + }, + "create_entry": { + "default": "To send events to Home Assistant, you will need to use the \"Make a web request\" action from the [IFTTT Webhook applet]({applet_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data." + }, + "step": { + "user": { + "description": "Are you sure you want to set up IFTTT?", + "title": "Set up the IFTTT Webhook Applet" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/fr.json b/homeassistant/components/ifttt/.translations/fr.json new file mode 100644 index 00000000000..d083a624d70 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Votre instance Home Assistant doit \u00eatre accessible \u00e0 partir d'Internet pour recevoir les messages IFTTT.", + "one_instance_allowed": "Une seule instance est n\u00e9cessaire." + }, + "create_entry": { + "default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez utiliser l'action \"Effectuer une demande Web\" \u00e0 partir de [l'applet IFTTT Webhook] ( {applet_url} ). \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n - Type de contenu: application / json \n\n Voir [la documentation] ( {docs_url} ) pour savoir comment configurer les automatisations pour g\u00e9rer les donn\u00e9es entrantes." + }, + "step": { + "user": { + "description": "\u00cates-vous s\u00fbr de vouloir configurer IFTTT?", + "title": "Configurer l'applet IFTTT Webhook" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/hu.json b/homeassistant/components/ifttt/.translations/hu.json new file mode 100644 index 00000000000..a131f848d45 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/hu.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani az IFTTT-t?", + "title": "IFTTT Webhook Applet be\u00e1ll\u00edt\u00e1sa" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/ko.json b/homeassistant/components/ifttt/.translations/ko.json new file mode 100644 index 00000000000..832123d5065 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/ko.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "IFTTT \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4.", + "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." + }, + "create_entry": { + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\uae30 \uc704\ud574\uc11c\ub294 [IFTTT Webhook \uc560\ud50c\ub9bf]({applet_url}) \uc5d0\uc11c \"Make a web request\" \ub97c \uc0ac\uc6a9\ud574\uc57c \ud569\ub2c8\ub2e4. \n\n \ub2e4\uc74c\uc758 \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694.\n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\n Home Assistant \ub85c \ub4e4\uc5b4\uc624\ub294 \ub370\uc774\ud130\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc790\ub3d9\ud654\ub97c \uad6c\uc131\ud558\ub294 \ubc29\ubc95\uc740 [\ubcf8 \ubb38\uc11c]({docs_url}) \ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694." + }, + "step": { + "user": { + "description": "IFTTT \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "IFTTT Webhook \uc560\ud50c\ub9bf \uc124\uc815" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/lb.json b/homeassistant/components/ifttt/.translations/lb.json new file mode 100644 index 00000000000..74e6b4926ef --- /dev/null +++ b/homeassistant/components/ifttt/.translations/lb.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u00c4r Home Assistant Instanz muss iwwert Internet accessibel si fir IFTTT Noriichten z'empf\u00e4nken.", + "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg." + }, + "create_entry": { + "default": "Fir Evenementer un Home Assistant ze sch\u00e9ckemusst dir d'Aktioun \"Make a web request\" vum [IFTTT Webhook applet] ({applet_url}) benotzen.\n\nGitt folgend Informatiounen un:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nKuckt iech [Dokumentatioun]({docs_url}) w\u00e9i een Automatisatioune mat empfaangene Donn\u00e9e konfigur\u00e9iert." + }, + "step": { + "user": { + "description": "S\u00e9cher fir IFTTT anzeriichten?", + "title": "IFTTT Webhook Applet ariichten" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/nl.json b/homeassistant/components/ifttt/.translations/nl.json new file mode 100644 index 00000000000..9188b1f6b08 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Uw Home Assistant-instantie moet via internet toegankelijk zijn om IFTTT-berichten te ontvangen.", + "one_instance_allowed": "Slechts \u00e9\u00e9n instantie is nodig." + }, + "create_entry": { + "default": "Om evenementen naar de Home Assistant te verzenden, moet u de actie \"Een webverzoek doen\" gebruiken vanuit de [IFTTT Webhook-applet]({applet_url}). \n\n Vul de volgende info in: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\nZie [the documentation]({docs_url}) voor informatie over het configureren van automatiseringen om inkomende gegevens te verwerken." + }, + "step": { + "user": { + "description": "Weet je zeker dat u IFTTT wilt instellen?", + "title": "Stel de IFTTT Webhook-applet in" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/no.json b/homeassistant/components/ifttt/.translations/no.json new file mode 100644 index 00000000000..481ab372e91 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Din Home Assistant enhet m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta IFTTT-meldinger.", + "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + }, + "create_entry": { + "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du bruke \"Make a web request\" handlingen fra [IFTTT Webhook applet]({applet_url}).\n\nFyll ut f\u00f8lgende informasjon:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSe [dokumentasjonen]({docs_url}) om hvordan du konfigurerer automatiseringer for \u00e5 h\u00e5ndtere innkommende data." + }, + "step": { + "user": { + "description": "Er du sikker p\u00e5 at du vil sette opp IFTTT?", + "title": "Sett opp IFTTT Webhook Applet" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/pl.json b/homeassistant/components/ifttt/.translations/pl.json new file mode 100644 index 00000000000..3c3c2182503 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty IFTTT.", + "one_instance_allowed": "Wymagana jest tylko jedna instancja." + }, + "create_entry": { + "default": "Aby wys\u0142a\u0107 zdarzenia do Home Assistant'a, b\u0119dziesz musia\u0142 u\u017cy\u0107 akcji \"Make a web request\" z [IFTTT Webhook apletu]({applet_url}). \n\n Podaj nast\u0119puj\u0105ce informacje:\n\n - URL: `{webhook_url}`\n - Metoda: POST\n - Typ zawarto\u015bci: application/json\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}) na temat konfiguracji automatyzacji by obs\u0142u\u017cy\u0107 przychodz\u0105ce dane." + }, + "step": { + "user": { + "description": "Jeste\u015b pewny, \u017ce chcesz skonfigurowa\u0107 IFTTT?", + "title": "Konfigurowanie apletu Webhook IFTTT" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/ru.json b/homeassistant/components/ifttt/.translations/ru.json new file mode 100644 index 00000000000..3c1d7b580e4 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 IFTTT.", + "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \"Make a web request\" \u0438\u0437 [IFTTT Webhook applet]({applet_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." + }, + "step": { + "user": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c IFTTT?", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 IFTTT Webhook Applet" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/sl.json b/homeassistant/components/ifttt/.translations/sl.json new file mode 100644 index 00000000000..f5cc1dc572e --- /dev/null +++ b/homeassistant/components/ifttt/.translations/sl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Va\u0161 Home Assistent mora biti dostopek prek interneta, da boste lahko prejemali IFTTT sporo\u010dila.", + "one_instance_allowed": "Potrebna je samo ena instanca." + }, + "create_entry": { + "default": "\u010ce \u017eelite poslati dogodke Home Assistent-u, boste morali uporabiti akcijo \u00bbNaredi spletno zahtevo\u00ab iz orodja [IFTTT Webhook applet] ( {applet_url} ). \n\n Izpolnite naslednje podatke: \n\n - URL: ` {webhook_url} ` \n - Metoda: POST \n - Vrsta vsebine: application/json \n\n Poglejte si [dokumentacijo] ( {docs_url} ) o tem, kako konfigurirati avtomatizacijo za obdelavo dohodnih podatkov." + }, + "step": { + "user": { + "description": "Ali ste prepri\u010dani, da \u017eelite nastaviti IFTTT?", + "title": "Nastavite IFTTT Webhook Applet" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/sv.json b/homeassistant/components/ifttt/.translations/sv.json new file mode 100644 index 00000000000..883bb042822 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Din Home Assistant instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot IFTTT meddelanden.", + "one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig." + }, + "create_entry": { + "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du anv\u00e4nda \u00e5tg\u00e4rden \"G\u00f6r en webbf\u00f6rfr\u00e5gan\" fr\u00e5n [IFTTT Webhook applet] ( {applet_url} ).\n\n Fyll i f\u00f6ljande information:\n \n - URL: ` {webhook_url} `\n - Metod: POST\n - Inneh\u00e5llstyp: application / json\n\n Se [dokumentationen] ( {docs_url} ) om hur du konfigurerar automatiseringar f\u00f6r att hantera inkommande data." + }, + "step": { + "user": { + "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill st\u00e4lla in IFTTT?", + "title": "St\u00e4lla in IFTTT Webhook Applet" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/zh-Hans.json b/homeassistant/components/ifttt/.translations/zh-Hans.json new file mode 100644 index 00000000000..e9f7aeb36d4 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/zh-Hans.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u60a8\u7684 Home Assistant \u5b9e\u4f8b\u9700\u8981\u53ef\u4ece\u4e92\u8054\u7f51\u8bbf\u95ee\u4ee5\u63a5\u6536 IFTTT \u6d88\u606f\u3002", + "one_instance_allowed": "\u53ea\u6709\u4e00\u4e2a\u5b9e\u4f8b\u662f\u5fc5\u9700\u7684\u3002" + }, + "create_entry": { + "default": "\u8981\u5411 Home Assistant \u53d1\u9001\u4e8b\u4ef6\uff0c\u60a8\u9700\u8981\u4f7f\u7528 [IFTTT Webhook applet]({applet_url}) \u4e2d\u7684 \"Make a web request\" \u52a8\u4f5c\u3002\n\n\u586b\u5199\u4ee5\u4e0b\u4fe1\u606f\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u6709\u5173\u5982\u4f55\u914d\u7f6e\u81ea\u52a8\u5316\u4ee5\u5904\u7406\u4f20\u5165\u7684\u6570\u636e\uff0c\u8bf7\u53c2\u9605[\u6587\u6863]({docs_url})\u3002" + }, + "step": { + "user": { + "description": "\u60a8\u786e\u5b9a\u8981\u8bbe\u7f6e IFTTT \u5417\uff1f", + "title": "\u8bbe\u7f6e IFTTT Webhook Applet" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/zh-Hant.json b/homeassistant/components/ifttt/.translations/zh-Hant.json new file mode 100644 index 00000000000..8610351f43b --- /dev/null +++ b/homeassistant/components/ifttt/.translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 IFTTT \u8a0a\u606f\u3002", + "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" + }, + "create_entry": { + "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u8981\u7531 [IFTTT Webhook applet]({applet_url}) \u547c\u53eb\u300c\u9032\u884c Web \u8acb\u6c42\u300d\u52d5\u4f5c\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u95dc\u65bc\u5982\u4f55\u50b3\u5165\u8cc7\u6599\u81ea\u52d5\u5316\u8a2d\u5b9a\uff0c\u8acb\u53c3\u95b1[\u6587\u4ef6]({docs_url})\u4ee5\u9032\u884c\u4e86\u89e3\u3002" + }, + "step": { + "user": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a IFTTT\uff1f", + "title": "\u8a2d\u5b9a IFTTT Webhook Applet" + } + }, + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py new file mode 100644 index 00000000000..60748d6ff13 --- /dev/null +++ b/homeassistant/components/ifttt/__init__.py @@ -0,0 +1,142 @@ +""" +Support to trigger Maker IFTTT recipes. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/ifttt/ +""" +from ipaddress import ip_address +import json +import logging +from urllib.parse import urlparse + +import requests +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant import config_entries +from homeassistant.util.network import is_local + +REQUIREMENTS = ['pyfttt==0.3'] +DEPENDENCIES = ['webhook'] + +_LOGGER = logging.getLogger(__name__) + +EVENT_RECEIVED = 'ifttt_webhook_received' + +ATTR_EVENT = 'event' +ATTR_VALUE1 = 'value1' +ATTR_VALUE2 = 'value2' +ATTR_VALUE3 = 'value3' + +CONF_KEY = 'key' +CONF_WEBHOOK_ID = 'webhook_id' + +DOMAIN = 'ifttt' + +SERVICE_TRIGGER = 'trigger' + +SERVICE_TRIGGER_SCHEMA = vol.Schema({ + vol.Required(ATTR_EVENT): cv.string, + vol.Optional(ATTR_VALUE1): cv.string, + vol.Optional(ATTR_VALUE2): cv.string, + vol.Optional(ATTR_VALUE3): cv.string, +}) + +CONFIG_SCHEMA = vol.Schema({ + vol.Optional(DOMAIN): vol.Schema({ + vol.Required(CONF_KEY): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the IFTTT service component.""" + if DOMAIN not in config: + return True + + key = config[DOMAIN][CONF_KEY] + + def trigger_service(call): + """Handle IFTTT trigger service calls.""" + event = call.data[ATTR_EVENT] + value1 = call.data.get(ATTR_VALUE1) + value2 = call.data.get(ATTR_VALUE2) + value3 = call.data.get(ATTR_VALUE3) + + try: + import pyfttt + pyfttt.send_event(key, event, value1, value2, value3) + except requests.exceptions.RequestException: + _LOGGER.exception("Error communicating with IFTTT") + + hass.services.async_register(DOMAIN, SERVICE_TRIGGER, trigger_service, + schema=SERVICE_TRIGGER_SCHEMA) + + return True + + +async def handle_webhook(hass, webhook_id, request): + """Handle webhook callback.""" + body = await request.text() + try: + data = json.loads(body) if body else {} + except ValueError: + return None + + if isinstance(data, dict): + data['webhook_id'] = webhook_id + hass.bus.async_fire(EVENT_RECEIVED, data) + + +async def async_setup_entry(hass, entry): + """Configure based on config entry.""" + hass.components.webhook.async_register( + entry.data['webhook_id'], handle_webhook) + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + hass.components.webhook.async_unregister(entry.data['webhook_id']) + return True + + +@config_entries.HANDLERS.register(DOMAIN) +class ConfigFlow(config_entries.ConfigFlow): + """Handle an IFTTT config flow.""" + + async def async_step_user(self, user_input=None): + """Handle a user initiated set up flow.""" + if self._async_current_entries(): + return self.async_abort(reason='one_instance_allowed') + + try: + url_parts = urlparse(self.hass.config.api.base_url) + + if is_local(ip_address(url_parts.hostname)): + return self.async_abort(reason='not_internet_accessible') + except ValueError: + # If it's not an IP address, it's very likely publicly accessible + pass + + if user_input is None: + return self.async_show_form( + step_id='user', + ) + + webhook_id = self.hass.components.webhook.async_generate_id() + webhook_url = \ + self.hass.components.webhook.async_generate_url(webhook_id) + + return self.async_create_entry( + title='IFTTT Webhook', + data={ + CONF_WEBHOOK_ID: webhook_id + }, + description_placeholders={ + 'applet_url': 'https://ifttt.com/maker_webhooks', + 'webhook_url': webhook_url, + 'docs_url': + 'https://www.home-assistant.io/components/ifttt/' + } + ) diff --git a/homeassistant/components/ifttt/strings.json b/homeassistant/components/ifttt/strings.json new file mode 100644 index 00000000000..9fc47504b9b --- /dev/null +++ b/homeassistant/components/ifttt/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "title": "IFTTT", + "step": { + "user": { + "title": "Set up the IFTTT Webhook Applet", + "description": "Are you sure you want to set up IFTTT?" + } + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary.", + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive IFTTT messages." + }, + "create_entry": { + "default": "To send events to Home Assistant, you will need to use the \"Make a web request\" action from the [IFTTT Webhook applet]({applet_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data." + } + } +} diff --git a/homeassistant/components/ihc/ihcdevice.py b/homeassistant/components/ihc/ihcdevice.py index 93ab81850c9..26ee2fb14fc 100644 --- a/homeassistant/components/ihc/ihcdevice.py +++ b/homeassistant/components/ihc/ihcdevice.py @@ -1,5 +1,4 @@ """Implementation of a base class for all IHC devices.""" -import asyncio from homeassistant.helpers.entity import Entity @@ -28,8 +27,7 @@ class IHCDevice(Entity): self.ihc_note = '' self.ihc_position = '' - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Add callback for IHC changes.""" self.ihc_controller.add_notify_event( self.ihc_id, self.on_ihc_change, True) diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 480ec31da7d..84d92361541 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -17,7 +17,6 @@ from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.loader import bind_hass from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) @@ -67,20 +66,6 @@ SERVICE_SCAN_SCHEMA = vol.Schema({ }) -@bind_hass -def scan(hass, entity_id=None): - """Force process of all cameras or given entity.""" - hass.add_job(async_scan, hass, entity_id) - - -@callback -@bind_hass -def async_scan(hass, entity_id=None): - """Force process of all cameras or given entity.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_SCAN, data)) - - async def async_setup(hass, config): """Set up the image processing.""" component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) diff --git a/homeassistant/components/image_processing/microsoft_face_detect.py b/homeassistant/components/image_processing/microsoft_face_detect.py index 7e10d05c5b6..69bd8a8f931 100644 --- a/homeassistant/components/image_processing/microsoft_face_detect.py +++ b/homeassistant/components/image_processing/microsoft_face_detect.py @@ -4,7 +4,6 @@ Component that will help set the Microsoft face detect processing. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/image_processing.microsoft_face_detect/ """ -import asyncio import logging import voluptuous as vol @@ -45,9 +44,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Microsoft Face detection platform.""" api = hass.data[DATA_MICROSOFT_FACE] attributes = config[CONF_ATTRIBUTES] @@ -88,15 +86,14 @@ class MicrosoftFaceDetectEntity(ImageProcessingFaceEntity): """Return the name of the entity.""" return self._name - @asyncio.coroutine - def async_process_image(self, image): + async def async_process_image(self, image): """Process image. This method is a coroutine. """ face_data = None try: - face_data = yield from self._api.call_api( + face_data = await self._api.call_api( 'post', 'detect', image, binary=True, params={'returnFaceAttributes': ",".join(self._attributes)}) diff --git a/homeassistant/components/image_processing/microsoft_face_identify.py b/homeassistant/components/image_processing/microsoft_face_identify.py index fae11a3dfa9..0a5b7725260 100644 --- a/homeassistant/components/image_processing/microsoft_face_identify.py +++ b/homeassistant/components/image_processing/microsoft_face_identify.py @@ -4,7 +4,6 @@ Component that will help set the Microsoft face for verify processing. For more details about this component, please refer to the documentation at https://home-assistant.io/components/image_processing.microsoft_face_identify/ """ -import asyncio import logging import voluptuous as vol @@ -29,9 +28,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Microsoft Face identify platform.""" api = hass.data[DATA_MICROSOFT_FACE] face_group = config[CONF_GROUP] @@ -80,22 +78,21 @@ class MicrosoftFaceIdentifyEntity(ImageProcessingFaceEntity): """Return the name of the entity.""" return self._name - @asyncio.coroutine - def async_process_image(self, image): + async def async_process_image(self, image): """Process image. This method is a coroutine. """ detect = None try: - face_data = yield from self._api.call_api( + face_data = await self._api.call_api( 'post', 'detect', image, binary=True) if not face_data: return face_ids = [data['faceId'] for data in face_data] - detect = yield from self._api.call_api( + detect = await self._api.call_api( 'post', 'identify', {'faceIds': face_ids, 'personGroupId': self._face_group}) diff --git a/homeassistant/components/image_processing/openalpr_cloud.py b/homeassistant/components/image_processing/openalpr_cloud.py index 3daaeb6fa01..dea55b76025 100644 --- a/homeassistant/components/image_processing/openalpr_cloud.py +++ b/homeassistant/components/image_processing/openalpr_cloud.py @@ -48,9 +48,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the OpenALPR cloud API platform.""" confidence = config[CONF_CONFIDENCE] params = { @@ -101,8 +100,7 @@ class OpenAlprCloudEntity(ImageProcessingAlprEntity): """Return the name of the entity.""" return self._name - @asyncio.coroutine - def async_process_image(self, image): + async def async_process_image(self, image): """Process image. This method is a coroutine. @@ -116,11 +114,11 @@ class OpenAlprCloudEntity(ImageProcessingAlprEntity): try: with async_timeout.timeout(self.timeout, loop=self.hass.loop): - request = yield from websession.post( + request = await websession.post( OPENALPR_API_URL, params=params, data=body ) - data = yield from request.json() + data = await request.json() if request.status != 200: _LOGGER.error("Error %d -> %s.", diff --git a/homeassistant/components/image_processing/openalpr_local.py b/homeassistant/components/image_processing/openalpr_local.py index 901533d1da4..9d5ebf2e2b9 100644 --- a/homeassistant/components/image_processing/openalpr_local.py +++ b/homeassistant/components/image_processing/openalpr_local.py @@ -55,9 +55,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the OpenALPR local platform.""" command = [config[CONF_ALPR_BIN], '-c', config[CONF_REGION], '-'] confidence = config[CONF_CONFIDENCE] @@ -173,8 +172,7 @@ class OpenAlprLocalEntity(ImageProcessingAlprEntity): """Return the name of the entity.""" return self._name - @asyncio.coroutine - def async_process_image(self, image): + async def async_process_image(self, image): """Process image. This method is a coroutine. @@ -182,7 +180,7 @@ class OpenAlprLocalEntity(ImageProcessingAlprEntity): result = {} vehicles = 0 - alpr = yield from asyncio.create_subprocess_exec( + alpr = await asyncio.create_subprocess_exec( *self._cmd, loop=self.hass.loop, stdin=asyncio.subprocess.PIPE, @@ -191,7 +189,7 @@ class OpenAlprLocalEntity(ImageProcessingAlprEntity): ) # Send image - stdout, _ = yield from alpr.communicate(input=image) + stdout, _ = await alpr.communicate(input=image) stdout = io.StringIO(str(stdout, 'utf-8')) while True: diff --git a/homeassistant/components/image_processing/seven_segments.py b/homeassistant/components/image_processing/seven_segments.py index fb6f41b4a49..1f56ba6b572 100644 --- a/homeassistant/components/image_processing/seven_segments.py +++ b/homeassistant/components/image_processing/seven_segments.py @@ -4,7 +4,6 @@ Local optical character recognition processing of seven segments displays. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/image_processing.seven_segments/ """ -import asyncio import logging import io import os @@ -44,9 +43,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Seven segments OCR platform.""" entities = [] for camera in config[CONF_SOURCE]: diff --git a/homeassistant/components/input_boolean.py b/homeassistant/components/input_boolean.py index b9c4dcc685e..18c9808c6d2 100644 --- a/homeassistant/components/input_boolean.py +++ b/homeassistant/components/input_boolean.py @@ -46,24 +46,6 @@ def is_on(hass, entity_id): return hass.states.is_state(entity_id, STATE_ON) -@bind_hass -def turn_on(hass, entity_id): - """Set input_boolean to True.""" - hass.services.call(DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}) - - -@bind_hass -def turn_off(hass, entity_id): - """Set input_boolean to False.""" - hass.services.call(DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}) - - -@bind_hass -def toggle(hass, entity_id): - """Set input_boolean to False.""" - hass.services.call(DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}) - - async def async_setup(hass, config): """Set up an input boolean.""" component = EntityComponent(_LOGGER, DOMAIN, hass) diff --git a/homeassistant/components/input_number.py b/homeassistant/components/input_number.py index 2f25ca143b8..9630e943bf4 100644 --- a/homeassistant/components/input_number.py +++ b/homeassistant/components/input_number.py @@ -4,7 +4,6 @@ Component to offer a way to set a numeric value from a slider or text box. For more details about this component, please refer to the documentation at https://home-assistant.io/components/input_number/ """ -import asyncio import logging import voluptuous as vol @@ -15,7 +14,6 @@ from homeassistant.const import ( from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import async_get_last_state -from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) @@ -82,33 +80,7 @@ CONFIG_SCHEMA = vol.Schema({ }, required=True, extra=vol.ALLOW_EXTRA) -@bind_hass -def set_value(hass, entity_id, value): - """Set input_number to value.""" - hass.services.call(DOMAIN, SERVICE_SET_VALUE, { - ATTR_ENTITY_ID: entity_id, - ATTR_VALUE: value, - }) - - -@bind_hass -def increment(hass, entity_id): - """Increment value of entity.""" - hass.services.call(DOMAIN, SERVICE_INCREMENT, { - ATTR_ENTITY_ID: entity_id - }) - - -@bind_hass -def decrement(hass, entity_id): - """Decrement value of entity.""" - hass.services.call(DOMAIN, SERVICE_DECREMENT, { - ATTR_ENTITY_ID: entity_id - }) - - -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up an input slider.""" component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -146,7 +118,7 @@ def async_setup(hass, config): 'async_decrement' ) - yield from component.async_add_entities(entities) + await component.async_add_entities(entities) return True @@ -201,13 +173,12 @@ class InputNumber(Entity): ATTR_MODE: self._mode, } - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Run when entity about to be added to hass.""" if self._current_value is not None: return - state = yield from async_get_last_state(self.hass, self.entity_id) + state = await async_get_last_state(self.hass, self.entity_id) value = state and float(state.state) # Check against None because value can be 0 @@ -216,8 +187,7 @@ class InputNumber(Entity): else: self._current_value = self._minimum - @asyncio.coroutine - def async_set_value(self, value): + async def async_set_value(self, value): """Set new value.""" num_value = float(value) if num_value < self._minimum or num_value > self._maximum: @@ -225,10 +195,9 @@ class InputNumber(Entity): num_value, self._minimum, self._maximum) return self._current_value = num_value - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_increment(self): + async def async_increment(self): """Increment value.""" new_value = self._current_value + self._step if new_value > self._maximum: @@ -236,10 +205,9 @@ class InputNumber(Entity): new_value, self._minimum, self._maximum) return self._current_value = new_value - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_decrement(self): + async def async_decrement(self): """Decrement value.""" new_value = self._current_value - self._step if new_value < self._minimum: @@ -247,4 +215,4 @@ class InputNumber(Entity): new_value, self._minimum, self._maximum) return self._current_value = new_value - yield from self.async_update_ha_state() + await self.async_update_ha_state() diff --git a/homeassistant/components/input_select.py b/homeassistant/components/input_select.py index 04e9b04787c..b8398e1be3d 100644 --- a/homeassistant/components/input_select.py +++ b/homeassistant/components/input_select.py @@ -4,13 +4,11 @@ Component to offer a way to select an option from a list. For more details about this component, please refer to the documentation at https://home-assistant.io/components/input_select/ """ -import asyncio import logging import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME -from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -78,42 +76,7 @@ CONFIG_SCHEMA = vol.Schema({ }, required=True, extra=vol.ALLOW_EXTRA) -@bind_hass -def select_option(hass, entity_id, option): - """Set value of input_select.""" - hass.services.call(DOMAIN, SERVICE_SELECT_OPTION, { - ATTR_ENTITY_ID: entity_id, - ATTR_OPTION: option, - }) - - -@bind_hass -def select_next(hass, entity_id): - """Set next value of input_select.""" - hass.services.call(DOMAIN, SERVICE_SELECT_NEXT, { - ATTR_ENTITY_ID: entity_id, - }) - - -@bind_hass -def select_previous(hass, entity_id): - """Set previous value of input_select.""" - hass.services.call(DOMAIN, SERVICE_SELECT_PREVIOUS, { - ATTR_ENTITY_ID: entity_id, - }) - - -@bind_hass -def set_options(hass, entity_id, options): - """Set options of input_select.""" - hass.services.call(DOMAIN, SERVICE_SET_OPTIONS, { - ATTR_ENTITY_ID: entity_id, - ATTR_OPTIONS: options, - }) - - -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up an input select.""" component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -149,7 +112,7 @@ def async_setup(hass, config): 'async_set_options' ) - yield from component.async_add_entities(entities) + await component.async_add_entities(entities) return True @@ -164,13 +127,12 @@ class InputSelect(Entity): self._options = options self._icon = icon - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Run when entity about to be added.""" if self._current_option is not None: return - state = yield from async_get_last_state(self.hass, self.entity_id) + state = await async_get_last_state(self.hass, self.entity_id) if not state or state.state not in self._options: self._current_option = self._options[0] else: @@ -203,27 +165,24 @@ class InputSelect(Entity): ATTR_OPTIONS: self._options, } - @asyncio.coroutine - def async_select_option(self, option): + async def async_select_option(self, option): """Select new option.""" if option not in self._options: _LOGGER.warning('Invalid option: %s (possible options: %s)', option, ', '.join(self._options)) return self._current_option = option - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_offset_index(self, offset): + async def async_offset_index(self, offset): """Offset current index.""" current_index = self._options.index(self._current_option) new_index = (current_index + offset) % len(self._options) self._current_option = self._options[new_index] - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_set_options(self, options): + async def async_set_options(self, options): """Set options.""" self._current_option = options[0] self._options = options - yield from self.async_update_ha_state() + await self.async_update_ha_state() diff --git a/homeassistant/components/input_text.py b/homeassistant/components/input_text.py index 2cb4f58a130..956d9a6466d 100644 --- a/homeassistant/components/input_text.py +++ b/homeassistant/components/input_text.py @@ -4,7 +4,6 @@ Component to offer a way to enter a value into a text box. For more details about this component, please refer to the documentation at https://home-assistant.io/components/input_text/ """ -import asyncio import logging import voluptuous as vol @@ -12,7 +11,6 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_NAME, CONF_MODE) -from homeassistant.loader import bind_hass from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import async_get_last_state @@ -74,17 +72,7 @@ CONFIG_SCHEMA = vol.Schema({ }, required=True, extra=vol.ALLOW_EXTRA) -@bind_hass -def set_value(hass, entity_id, value): - """Set input_text to value.""" - hass.services.call(DOMAIN, SERVICE_SET_VALUE, { - ATTR_ENTITY_ID: entity_id, - ATTR_VALUE: value, - }) - - -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up an input text box.""" component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -112,7 +100,7 @@ def async_setup(hass, config): 'async_set_value' ) - yield from component.async_add_entities(entities) + await component.async_add_entities(entities) return True @@ -167,25 +155,23 @@ class InputText(Entity): ATTR_MODE: self._mode, } - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Run when entity about to be added to hass.""" if self._current_value is not None: return - state = yield from async_get_last_state(self.hass, self.entity_id) + state = await async_get_last_state(self.hass, self.entity_id) value = state and state.state # Check against None because value can be 0 if value is not None and self._minimum <= len(value) <= self._maximum: self._current_value = value - @asyncio.coroutine - def async_set_value(self, value): + async def async_set_value(self, value): """Select new value.""" if len(value) < self._minimum or len(value) > self._maximum: _LOGGER.warning("Invalid value: %s (length range %s - %s)", value, self._minimum, self._maximum) return self._current_value = value - yield from self.async_update_ha_state() + await self.async_update_ha_state() diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 749d167e6de..924baeaa560 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -4,7 +4,6 @@ Support for INSTEON Modems (PLM and Hub). For more details about this component, please refer to the documentation at https://home-assistant.io/components/insteon/ """ -import asyncio import collections import logging from typing import Dict @@ -149,8 +148,7 @@ X10_HOUSECODE_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the connection to the modem.""" import insteonplm @@ -292,7 +290,7 @@ def async_setup(hass, config): if host: _LOGGER.info('Connecting to Insteon Hub on %s', host) - conn = yield from insteonplm.Connection.create( + conn = await insteonplm.Connection.create( host=host, port=ip_port, username=username, @@ -302,7 +300,7 @@ def async_setup(hass, config): workdir=hass.config.config_dir) else: _LOGGER.info("Looking for Insteon PLM on %s", port) - conn = yield from insteonplm.Connection.create( + conn = await insteonplm.Connection.create( device=port, loop=hass.loop, workdir=hass.config.config_dir) @@ -494,8 +492,7 @@ class InsteonEntity(Entity): deviceid.human, group, val) self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register INSTEON update events.""" _LOGGER.debug('Tracking updates for device %s group %d statename %s', self.address, self.group, diff --git a/homeassistant/components/insteon_plm.py b/homeassistant/components/insteon_plm.py index b89e5679a63..b3011e9d7bd 100644 --- a/homeassistant/components/insteon_plm.py +++ b/homeassistant/components/insteon_plm.py @@ -4,14 +4,12 @@ Support for INSTEON PowerLinc Modem. For more details about this component, please refer to the documentation at https://home-assistant.io/components/insteon_plm/ """ -import asyncio import logging _LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the insteon_plm component. This component is deprecated as of release 0.77 and should be removed in diff --git a/homeassistant/components/intent_script.py b/homeassistant/components/intent_script.py index 91489e188c5..9c63141e496 100644 --- a/homeassistant/components/intent_script.py +++ b/homeassistant/components/intent_script.py @@ -1,5 +1,4 @@ """Handle intents with scripts.""" -import asyncio import copy import logging @@ -45,8 +44,7 @@ CONFIG_SCHEMA = vol.Schema({ _LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Activate Alexa component.""" intents = copy.deepcopy(config[DOMAIN]) template.attach(hass, intents) @@ -69,8 +67,7 @@ class ScriptIntentHandler(intent.IntentHandler): self.intent_type = intent_type self.config = config - @asyncio.coroutine - def async_handle(self, intent_obj): + async def async_handle(self, intent_obj): """Handle the intent.""" speech = self.config.get(CONF_SPEECH) card = self.config.get(CONF_CARD) @@ -81,9 +78,9 @@ class ScriptIntentHandler(intent.IntentHandler): if action is not None: if is_async_action: - intent_obj.hass.async_add_job(action.async_run(slots)) + intent_obj.hass.async_create_task(action.async_run(slots)) else: - yield from action.async_run(slots) + await action.async_run(slots) response = intent_obj.create_response() diff --git a/homeassistant/components/introduction.py b/homeassistant/components/introduction.py index cc3e00c4475..17de7fcd6ca 100644 --- a/homeassistant/components/introduction.py +++ b/homeassistant/components/introduction.py @@ -4,7 +4,6 @@ Component that will help guide the user taking its first steps. For more details about this component, please refer to the documentation at https://home-assistant.io/components/introduction/ """ -import asyncio import logging import voluptuous as vol @@ -16,8 +15,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config=None): +async def async_setup(hass, config=None): """Set up the introduction component.""" log = logging.getLogger(__name__) log.info(""" diff --git a/homeassistant/components/ios/.translations/de.json b/homeassistant/components/ios/.translations/de.json index e9e592d18c2..18ffda135ee 100644 --- a/homeassistant/components/ios/.translations/de.json +++ b/homeassistant/components/ios/.translations/de.json @@ -5,8 +5,10 @@ }, "step": { "confirm": { - "description": "M\u00f6chtest du die Home Assistant iOS-Komponente einrichten?" + "description": "M\u00f6chtest du die Home Assistant iOS-Komponente einrichten?", + "title": "Home Assistant iOS" } - } + }, + "title": "Home Assistant iOS" } } \ No newline at end of file diff --git a/homeassistant/components/ios/.translations/ru.json b/homeassistant/components/ios/.translations/ru.json index 7030f18b729..fdcc964a0e6 100644 --- a/homeassistant/components/ios/.translations/ru.json +++ b/homeassistant/components/ios/.translations/ru.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "\u0414\u043e\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f Home Assistant iOS." + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "step": { "confirm": { - "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Home Assistant iOS?", + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Home Assistant iOS?", "title": "Home Assistant iOS" } }, diff --git a/homeassistant/components/ios/.translations/sv.json b/homeassistant/components/ios/.translations/sv.json index 6806f9bab90..5a605ed8987 100644 --- a/homeassistant/components/ios/.translations/sv.json +++ b/homeassistant/components/ios/.translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Endast en enda konfiguration av Home Assistant iOS \u00e4r n\u00f6dv\u00e4ndig." + }, "step": { "confirm": { "description": "Vill du konfigurera Home Assistants iOS komponent?", diff --git a/homeassistant/components/ios/__init__.py b/homeassistant/components/ios/__init__.py index a67be0a63de..0b1282b605a 100644 --- a/homeassistant/components/ios/__init__.py +++ b/homeassistant/components/ios/__init__.py @@ -4,7 +4,6 @@ Native Home Assistant iOS app component. For more details about this component, please refer to the documentation at https://home-assistant.io/ecosystem/ios/ """ -import asyncio import logging import datetime @@ -259,11 +258,10 @@ class iOSIdentifyDeviceView(HomeAssistantView): """Initiliaze the view.""" self._config_path = config_path - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Handle the POST request for device identification.""" try: - data = yield from request.json() + data = await request.json() except ValueError: return self.json_message("Invalid JSON", HTTP_BAD_REQUEST) diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index d8afb7be5da..9b539b0690a 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -4,7 +4,6 @@ Support the ISY-994 controllers. For configuration details please visit the documentation for this component at https://home-assistant.io/components/isy994/ """ -import asyncio from collections import namedtuple import logging from urllib.parse import urlparse @@ -414,8 +413,7 @@ class ISYDevice(Entity): self._change_handler = None self._control_handler = None - @asyncio.coroutine - def async_added_to_hass(self) -> None: + async def async_added_to_hass(self) -> None: """Subscribe to the node change events.""" self._change_handler = self._node.status.subscribe( 'changed', self.on_update) diff --git a/homeassistant/components/keyboard.py b/homeassistant/components/keyboard.py index 697b5b6873c..58bb1fa5f42 100644 --- a/homeassistant/components/keyboard.py +++ b/homeassistant/components/keyboard.py @@ -18,36 +18,6 @@ DOMAIN = 'keyboard' TAP_KEY_SCHEMA = vol.Schema({}) -def volume_up(hass): - """Press the keyboard button for volume up.""" - hass.services.call(DOMAIN, SERVICE_VOLUME_UP) - - -def volume_down(hass): - """Press the keyboard button for volume down.""" - hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN) - - -def volume_mute(hass): - """Press the keyboard button for muting volume.""" - hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE) - - -def media_play_pause(hass): - """Press the keyboard button for play/pause.""" - hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE) - - -def media_next_track(hass): - """Press the keyboard button for next track.""" - hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK) - - -def media_prev_track(hass): - """Press the keyboard button for prev track.""" - hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK) - - def setup(hass, config): """Listen for keyboard events.""" # pylint: disable=import-error diff --git a/homeassistant/components/lifx/.translations/ca.json b/homeassistant/components/lifx/.translations/ca.json new file mode 100644 index 00000000000..b3896d49e1d --- /dev/null +++ b/homeassistant/components/lifx/.translations/ca.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No s'han trobat dispositius LIFX a la xarxa.", + "single_instance_allowed": "Nom\u00e9s \u00e9s possible una \u00fanica configuraci\u00f3 de LIFX." + }, + "step": { + "confirm": { + "description": "Voleu configurar LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/en.json b/homeassistant/components/lifx/.translations/en.json new file mode 100644 index 00000000000..64fdc7516ea --- /dev/null +++ b/homeassistant/components/lifx/.translations/en.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No LIFX devices found on the network.", + "single_instance_allowed": "Only a single configuration of LIFX is possible." + }, + "step": { + "confirm": { + "description": "Do you want to set up LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/fr.json b/homeassistant/components/lifx/.translations/fr.json new file mode 100644 index 00000000000..96a264fa6b2 --- /dev/null +++ b/homeassistant/components/lifx/.translations/fr.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Aucun p\u00e9riph\u00e9rique LIFX trouv\u00e9 sur le r\u00e9seau.", + "single_instance_allowed": "Une seule configuration de LIFX est possible." + }, + "step": { + "confirm": { + "description": "Voulez-vous configurer LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/ko.json b/homeassistant/components/lifx/.translations/ko.json new file mode 100644 index 00000000000..c795c54badb --- /dev/null +++ b/homeassistant/components/lifx/.translations/ko.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "LIFX \uc7a5\uce58\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "single_instance_allowed": "\ud558\ub098\uc758 LIFX \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "LIFX \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/lb.json b/homeassistant/components/lifx/.translations/lb.json new file mode 100644 index 00000000000..2e033280e46 --- /dev/null +++ b/homeassistant/components/lifx/.translations/lb.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keng LIFX Apparater am Netzwierk fonnt.", + "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun LIFX ass erlaabt." + }, + "step": { + "confirm": { + "description": "Soll LIFX konfigur\u00e9iert ginn?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/nl.json b/homeassistant/components/lifx/.translations/nl.json new file mode 100644 index 00000000000..a23502729d6 --- /dev/null +++ b/homeassistant/components/lifx/.translations/nl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Geen LIFX-apparaten gevonden op het netwerk.", + "single_instance_allowed": "Slechts een enkele configuratie van LIFX is mogelijk." + }, + "step": { + "confirm": { + "description": "Wilt u LIFX instellen?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/pl.json b/homeassistant/components/lifx/.translations/pl.json new file mode 100644 index 00000000000..f13c0b54bbd --- /dev/null +++ b/homeassistant/components/lifx/.translations/pl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nie znaleziono w sieci urz\u0105dze\u0144 LIFX.", + "single_instance_allowed": "Wymagana jest tylko jedna konfiguracja LIFX." + }, + "step": { + "confirm": { + "description": "Czy chcesz skonfigurowa\u0107 LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/ru.json b/homeassistant/components/lifx/.translations/ru.json new file mode 100644 index 00000000000..5ad351b7a90 --- /dev/null +++ b/homeassistant/components/lifx/.translations/ru.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 LIFX \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430" + }, + "step": { + "confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/sl.json b/homeassistant/components/lifx/.translations/sl.json new file mode 100644 index 00000000000..492bf9010dd --- /dev/null +++ b/homeassistant/components/lifx/.translations/sl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "V omre\u017eju ni najdenih naprav LIFX.", + "single_instance_allowed": "Mo\u017ena je samo ena konfiguracija LIFX-a." + }, + "step": { + "confirm": { + "description": "Ali \u017eelite nastaviti LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/sv.json b/homeassistant/components/lifx/.translations/sv.json new file mode 100644 index 00000000000..a935e209bb4 --- /dev/null +++ b/homeassistant/components/lifx/.translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga LIFX enheter hittas i n\u00e4tverket.", + "single_instance_allowed": "Endast en enda konfiguration av LIFX \u00e4r m\u00f6jlig." + }, + "step": { + "confirm": { + "description": "Vill du st\u00e4lla in LIFX?", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/zh-Hans.json b/homeassistant/components/lifx/.translations/zh-Hans.json new file mode 100644 index 00000000000..bc9375d807d --- /dev/null +++ b/homeassistant/components/lifx/.translations/zh-Hans.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u6ca1\u6709\u5728\u7f51\u7edc\u4e0a\u627e\u5230 LIFX \u8bbe\u5907\u3002", + "single_instance_allowed": "LIFX \u53ea\u80fd\u914d\u7f6e\u4e00\u6b21\u3002" + }, + "step": { + "confirm": { + "description": "\u60a8\u60f3\u8981\u914d\u7f6e LIFX \u5417\uff1f", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/zh-Hant.json b/homeassistant/components/lifx/.translations/zh-Hant.json new file mode 100644 index 00000000000..4c66f0d0133 --- /dev/null +++ b/homeassistant/components/lifx/.translations/zh-Hant.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 LIFX \u88dd\u7f6e\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 LIFX\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a LIFX\uff1f", + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index bc7f136322b..41dbbcd6d0c 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -17,7 +17,6 @@ from homeassistant.components.group import \ from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON) -from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.entity import ToggleEntity @@ -142,90 +141,6 @@ def is_on(hass, entity_id=None): return hass.states.is_state(entity_id, STATE_ON) -@bind_hass -def turn_on(hass, entity_id=None, transition=None, brightness=None, - brightness_pct=None, rgb_color=None, xy_color=None, hs_color=None, - color_temp=None, kelvin=None, white_value=None, - profile=None, flash=None, effect=None, color_name=None): - """Turn all or specified light on.""" - hass.add_job( - async_turn_on, hass, entity_id, transition, brightness, brightness_pct, - rgb_color, xy_color, hs_color, color_temp, kelvin, white_value, - profile, flash, effect, color_name) - - -@callback -@bind_hass -def async_turn_on(hass, entity_id=None, transition=None, brightness=None, - brightness_pct=None, rgb_color=None, xy_color=None, - hs_color=None, color_temp=None, kelvin=None, - white_value=None, profile=None, flash=None, effect=None, - color_name=None): - """Turn all or specified light on.""" - data = { - key: value for key, value in [ - (ATTR_ENTITY_ID, entity_id), - (ATTR_PROFILE, profile), - (ATTR_TRANSITION, transition), - (ATTR_BRIGHTNESS, brightness), - (ATTR_BRIGHTNESS_PCT, brightness_pct), - (ATTR_RGB_COLOR, rgb_color), - (ATTR_XY_COLOR, xy_color), - (ATTR_HS_COLOR, hs_color), - (ATTR_COLOR_TEMP, color_temp), - (ATTR_KELVIN, kelvin), - (ATTR_WHITE_VALUE, white_value), - (ATTR_FLASH, flash), - (ATTR_EFFECT, effect), - (ATTR_COLOR_NAME, color_name), - ] if value is not None - } - - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)) - - -@bind_hass -def turn_off(hass, entity_id=None, transition=None): - """Turn all or specified light off.""" - hass.add_job(async_turn_off, hass, entity_id, transition) - - -@callback -@bind_hass -def async_turn_off(hass, entity_id=None, transition=None): - """Turn all or specified light off.""" - data = { - key: value for key, value in [ - (ATTR_ENTITY_ID, entity_id), - (ATTR_TRANSITION, transition), - ] if value is not None - } - - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, data)) - - -@callback -@bind_hass -def async_toggle(hass, entity_id=None, transition=None): - """Toggle all or specified light.""" - data = { - key: value for key, value in [ - (ATTR_ENTITY_ID, entity_id), - (ATTR_TRANSITION, transition), - ] if value is not None - } - - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_TOGGLE, data)) - - -@bind_hass -def toggle(hass, entity_id=None, transition=None): - """Toggle all or specified light.""" - hass.add_job(async_toggle, hass, entity_id, transition) - - def preprocess_turn_on_alternatives(params): """Process extra data for turn light on request.""" profile = Profiles.get(params.pop(ATTR_PROFILE, None)) diff --git a/homeassistant/components/light/ads.py b/homeassistant/components/light/ads.py index 65569f6b2d5..0d97efa16a2 100644 --- a/homeassistant/components/light/ads.py +++ b/homeassistant/components/light/ads.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation. https://home-assistant.io/components/light.ads/ """ -import asyncio import logging import voluptuous as vol from homeassistant.components.light import Light, ATTR_BRIGHTNESS, \ @@ -50,8 +49,7 @@ class AdsLight(Light): self.ads_var_enable = ads_var_enable self.ads_var_brightness = ads_var_brightness - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register device notification.""" def update_on_state(name, value): """Handle device notifications for state.""" diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 958abaca033..686fc01caf9 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -7,6 +7,7 @@ https://home-assistant.io/components/light.hue/ import asyncio from datetime import timedelta import logging +from time import monotonic import random import async_timeout @@ -159,18 +160,23 @@ async def async_update_items(hass, bridge, async_add_entities, import aiohue if is_group: + api_type = 'group' api = bridge.api.groups else: + api_type = 'light' api = bridge.api.lights try: + start = monotonic() with async_timeout.timeout(4): await api.update() - except (asyncio.TimeoutError, aiohue.AiohueException): + except (asyncio.TimeoutError, aiohue.AiohueException) as err: + _LOGGER.debug('Failed to fetch %s: %s', api_type, err) + if not bridge.available: return - _LOGGER.error('Unable to reach bridge %s', bridge.host) + _LOGGER.error('Unable to reach bridge %s (%s)', bridge.host, err) bridge.available = False for light_id, light in current.items(): @@ -179,6 +185,10 @@ async def async_update_items(hass, bridge, async_add_entities, return + finally: + _LOGGER.debug('Finished %s request in %.3f seconds', + api_type, monotonic() - start) + if not bridge.available: _LOGGER.info('Reconnected to bridge %s', bridge.host) bridge.available = True diff --git a/homeassistant/components/light/insteon.py b/homeassistant/components/light/insteon.py index 82f455c821e..4829ce631a6 100644 --- a/homeassistant/components/light/insteon.py +++ b/homeassistant/components/light/insteon.py @@ -4,7 +4,6 @@ Support for Insteon lights via PowerLinc Modem. For more details about this component, please refer to the documentation at https://home-assistant.io/components/light.insteon/ """ -import asyncio import logging from homeassistant.components.insteon import InsteonEntity @@ -18,9 +17,8 @@ DEPENDENCIES = ['insteon'] MAX_BRIGHTNESS = 255 -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Insteon component.""" insteon_modem = hass.data['insteon'].get('modem') @@ -55,8 +53,7 @@ class InsteonDimmerDevice(InsteonEntity, Light): """Flag supported features.""" return SUPPORT_BRIGHTNESS - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn device on.""" if ATTR_BRIGHTNESS in kwargs: brightness = int(kwargs[ATTR_BRIGHTNESS]) @@ -64,7 +61,6 @@ class InsteonDimmerDevice(InsteonEntity, Light): else: self._insteon_device_state.on() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn device off.""" self._insteon_device_state.off() diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index bea39354e1b..9dcd2ae4cc2 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -30,7 +30,7 @@ import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['aiolifx==0.6.3', 'aiolifx_effects==0.1.2'] +REQUIREMENTS = ['aiolifx==0.6.3', 'aiolifx_effects==0.2.1'] UDP_BROADCAST_PORT = 56700 @@ -300,7 +300,7 @@ class LIFXManager: @callback def register(self, bulb): """Handle aiolifx detected bulb.""" - self.hass.async_add_job(self.register_new_bulb(bulb)) + self.hass.async_create_task(self.register_new_bulb(bulb)) async def register_new_bulb(self, bulb): """Handle newly detected bulb.""" @@ -344,7 +344,7 @@ class LIFXManager: entity = self.entities[bulb.mac_addr] _LOGGER.debug("%s unregister", entity.who) entity.registered = False - self.hass.async_add_job(entity.async_update_ha_state()) + self.hass.async_create_task(entity.async_update_ha_state()) class AwaitAioLIFX: @@ -484,12 +484,12 @@ class LIFXLight(Light): async def async_turn_on(self, **kwargs): """Turn the light on.""" kwargs[ATTR_POWER] = True - self.hass.async_add_job(self.set_state(**kwargs)) + self.hass.async_create_task(self.set_state(**kwargs)) async def async_turn_off(self, **kwargs): """Turn the light off.""" kwargs[ATTR_POWER] = False - self.hass.async_add_job(self.set_state(**kwargs)) + self.hass.async_create_task(self.set_state(**kwargs)) async def set_state(self, **kwargs): """Set a color on the light and turn it on/off.""" diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index 9400932802a..a5aeabba84d 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -4,7 +4,6 @@ Support for LimitlessLED bulbs. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.limitlessled/ """ -import asyncio import logging import voluptuous as vol @@ -188,10 +187,9 @@ class LimitlessLEDGroup(Light): self._color = None self._effect = None - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Handle entity about to be added to hass event.""" - last_state = yield from async_get_last_state(self.hass, self.entity_id) + last_state = await async_get_last_state(self.hass, self.entity_id) if last_state: self._is_on = (last_state.state == STATE_ON) self._brightness = last_state.attributes.get('brightness') diff --git a/homeassistant/components/light/lutron_caseta.py b/homeassistant/components/light/lutron_caseta.py index f345748683b..21360e71c42 100644 --- a/homeassistant/components/light/lutron_caseta.py +++ b/homeassistant/components/light/lutron_caseta.py @@ -4,7 +4,6 @@ Support for Lutron Caseta lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.lutron_caseta/ """ -import asyncio import logging from homeassistant.components.light import ( @@ -19,9 +18,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['lutron_caseta'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Lutron Caseta lights.""" devs = [] bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] @@ -46,15 +44,13 @@ class LutronCasetaLight(LutronCasetaDevice, Light): """Return the brightness of the light.""" return to_hass_level(self._state["current_state"]) - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the light on.""" brightness = kwargs.get(ATTR_BRIGHTNESS, 255) self._smartbridge.set_value(self._device_id, to_lutron_level(brightness)) - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the light off.""" self._smartbridge.set_value(self._device_id, 0) @@ -63,8 +59,7 @@ class LutronCasetaLight(LutronCasetaDevice, Light): """Return true if device is on.""" return self._state["current_state"] > 0 - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Call when forcing a refresh of the device.""" self._state = self._smartbridge.get_device_by_id(self._device_id) _LOGGER.debug(self._state) diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index 64331411f7f..3b095aa4bfd 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -9,7 +9,7 @@ import logging import voluptuous as vol from homeassistant.core import callback -from homeassistant.components import mqtt +from homeassistant.components import mqtt, light from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, ATTR_WHITE_VALUE, Light, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, @@ -19,10 +19,13 @@ from homeassistant.const import ( CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, STATE_ON, CONF_RGB, CONF_STATE, CONF_VALUE_TEMPLATE, CONF_WHITE_VALUE, CONF_XY) from homeassistant.components.mqtt import ( - CONF_AVAILABILITY_TOPIC, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, - CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - MqttAvailability) + ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_COMMAND_TOPIC, + CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, + CONF_STATE_TOPIC, MqttAvailability, MqttDiscoveryUpdate) +from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW from homeassistant.helpers.restore_state import async_get_last_state +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType, ConfigType import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util @@ -102,12 +105,28 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up a MQTT Light.""" - if discovery_info is not None: - config = PLATFORM_SCHEMA(discovery_info) +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_entities, discovery_info=None): + """Set up MQTT light through configuration.yaml.""" + await _async_setup_entity(hass, config, async_add_entities) + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT light dynamically through MQTT discovery.""" + async def async_discover(discovery_payload): + """Discover and add a MQTT light.""" + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(hass, config, async_add_entities, + discovery_payload[ATTR_DISCOVERY_HASH]) + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(light.DOMAIN, 'mqtt'), + async_discover) + + +async def _async_setup_entity(hass, config, async_add_entities, + discovery_hash=None): + """Set up a MQTT Light.""" config.setdefault( CONF_STATE_VALUE_TEMPLATE, config.get(CONF_VALUE_TEMPLATE)) @@ -156,19 +175,21 @@ async def async_setup_platform(hass, config, async_add_entities, config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE), + discovery_hash, )]) -class MqttLight(MqttAvailability, Light): +class MqttLight(MqttAvailability, MqttDiscoveryUpdate, Light): """Representation of a MQTT light.""" def __init__(self, name, unique_id, effect_list, topic, templates, qos, retain, payload, optimistic, brightness_scale, white_value_scale, on_command_type, availability_topic, - payload_available, payload_not_available): + payload_available, payload_not_available, discovery_hash): """Initialize MQTT light.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_hash) self._name = name self._unique_id = unique_id self._effect_list = effect_list @@ -216,10 +237,12 @@ class MqttLight(MqttAvailability, Light): SUPPORT_WHITE_VALUE) self._supported_features |= ( topic[CONF_XY_COMMAND_TOPIC] is not None and SUPPORT_COLOR) + self._discovery_hash = discovery_hash async def async_added_to_hass(self): """Subscribe to MQTT events.""" - await super().async_added_to_hass() + await MqttAvailability.async_added_to_hass(self) + await MqttDiscoveryUpdate.async_added_to_hass(self) templates = {} for key, tpl in list(self._templates.items()): @@ -235,6 +258,10 @@ class MqttLight(MqttAvailability, Light): def state_received(topic, payload, qos): """Handle new MQTT messages.""" payload = templates[CONF_STATE](payload) + if not payload: + _LOGGER.debug("Ignoring empty state message from '%s'", topic) + return + if payload == self._payload['on']: self._state = True elif payload == self._payload['off']: @@ -251,7 +278,13 @@ class MqttLight(MqttAvailability, Light): @callback def brightness_received(topic, payload, qos): """Handle new MQTT messages for the brightness.""" - device_value = float(templates[CONF_BRIGHTNESS](payload)) + payload = templates[CONF_BRIGHTNESS](payload) + if not payload: + _LOGGER.debug("Ignoring empty brightness message from '%s'", + topic) + return + + device_value = float(payload) percent_bright = device_value / self._brightness_scale self._brightness = int(percent_bright * 255) self.async_schedule_update_ha_state() @@ -272,8 +305,12 @@ class MqttLight(MqttAvailability, Light): @callback def rgb_received(topic, payload, qos): """Handle new MQTT messages for RGB.""" - rgb = [int(val) for val in - templates[CONF_RGB](payload).split(',')] + payload = templates[CONF_RGB](payload) + if not payload: + _LOGGER.debug("Ignoring empty rgb message from '%s'", topic) + return + + rgb = [int(val) for val in payload.split(',')] self._hs = color_util.color_RGB_to_hs(*rgb) self.async_schedule_update_ha_state() @@ -291,7 +328,13 @@ class MqttLight(MqttAvailability, Light): @callback def color_temp_received(topic, payload, qos): """Handle new MQTT messages for color temperature.""" - self._color_temp = int(templates[CONF_COLOR_TEMP](payload)) + payload = templates[CONF_COLOR_TEMP](payload) + if not payload: + _LOGGER.debug("Ignoring empty color temp message from '%s'", + topic) + return + + self._color_temp = int(payload) self.async_schedule_update_ha_state() if self._topic[CONF_COLOR_TEMP_STATE_TOPIC] is not None: @@ -310,7 +353,12 @@ class MqttLight(MqttAvailability, Light): @callback def effect_received(topic, payload, qos): """Handle new MQTT messages for effect.""" - self._effect = templates[CONF_EFFECT](payload) + payload = templates[CONF_EFFECT](payload) + if not payload: + _LOGGER.debug("Ignoring empty effect message from '%s'", topic) + return + + self._effect = payload self.async_schedule_update_ha_state() if self._topic[CONF_EFFECT_STATE_TOPIC] is not None: @@ -329,7 +377,13 @@ class MqttLight(MqttAvailability, Light): @callback def white_value_received(topic, payload, qos): """Handle new MQTT messages for white value.""" - device_value = float(templates[CONF_WHITE_VALUE](payload)) + payload = templates[CONF_WHITE_VALUE](payload) + if not payload: + _LOGGER.debug("Ignoring empty white value message from '%s'", + topic) + return + + device_value = float(payload) percent_white = device_value / self._white_value_scale self._white_value = int(percent_white * 255) self.async_schedule_update_ha_state() @@ -350,8 +404,13 @@ class MqttLight(MqttAvailability, Light): @callback def xy_received(topic, payload, qos): """Handle new MQTT messages for color.""" - xy_color = [float(val) for val in - templates[CONF_XY](payload).split(',')] + payload = templates[CONF_XY](payload) + if not payload: + _LOGGER.debug("Ignoring empty xy-color message from '%s'", + topic) + return + + xy_color = [float(val) for val in payload.split(',')] self._hs = color_util.color_xy_to_hs(*xy_color) self.async_schedule_update_ha_state() diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index ed4d350d96d..1ed43a6385a 100644 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -4,29 +4,31 @@ Support for MQTT JSON lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.mqtt_json/ """ -import logging import json +import logging +from typing import Optional + import voluptuous as vol -from homeassistant.core import callback from homeassistant.components import mqtt from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, - ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_HS_COLOR, - FLASH_LONG, FLASH_SHORT, Light, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_COLOR, - SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE) + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, + ATTR_TRANSITION, ATTR_WHITE_VALUE, FLASH_LONG, FLASH_SHORT, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, + SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, + Light) from homeassistant.components.light.mqtt import CONF_BRIGHTNESS_SCALE -from homeassistant.const import ( - CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, STATE_ON, - CONF_NAME, CONF_OPTIMISTIC, CONF_RGB, CONF_WHITE_VALUE, CONF_XY) from homeassistant.components.mqtt import ( - CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, + ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, - MqttAvailability) + CONF_STATE_TOPIC, MqttAvailability, MqttDiscoveryUpdate) +from homeassistant.const import ( + CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, CONF_NAME, CONF_OPTIMISTIC, + CONF_RGB, CONF_WHITE_VALUE, CONF_XY, STATE_ON) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.helpers.restore_state import async_get_last_state +from homeassistant.helpers.typing import ConfigType, HomeAssistantType import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -87,6 +89,11 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, """Set up a MQTT JSON Light.""" if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) + + discovery_hash = None + if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info: + discovery_hash = discovery_info[ATTR_DISCOVERY_HASH] + async_add_entities([MqttJson( config.get(CONF_NAME), config.get(CONF_UNIQUE_ID), @@ -116,20 +123,23 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE), - config.get(CONF_BRIGHTNESS_SCALE) + config.get(CONF_BRIGHTNESS_SCALE), + discovery_hash, )]) -class MqttJson(MqttAvailability, Light): +class MqttJson(MqttAvailability, MqttDiscoveryUpdate, Light): """Representation of a MQTT JSON light.""" def __init__(self, name, unique_id, effect_list, topic, qos, retain, optimistic, brightness, color_temp, effect, rgb, white_value, xy, hs, flash_times, availability_topic, payload_available, - payload_not_available, brightness_scale): + payload_not_available, brightness_scale, + discovery_hash: Optional[str]): """Initialize MQTT JSON light.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_hash) self._name = name self._unique_id = unique_id self._effect_list = effect_list @@ -180,7 +190,8 @@ class MqttJson(MqttAvailability, Light): async def async_added_to_hass(self): """Subscribe to MQTT events.""" - await super().async_added_to_hass() + await MqttAvailability.async_added_to_hass(self) + await MqttDiscoveryUpdate.async_added_to_hass(self) last_state = await async_get_last_state(self.hass, self.entity_id) diff --git a/homeassistant/components/light/opple.py b/homeassistant/components/light/opple.py new file mode 100644 index 00000000000..66850d04406 --- /dev/null +++ b/homeassistant/components/light/opple.py @@ -0,0 +1,147 @@ +""" +Support for the Opple light. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.opple/ +""" + +import logging + +import voluptuous as vol + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR_TEMP, Light) +from homeassistant.const import CONF_HOST, CONF_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.util.color import \ + color_temperature_kelvin_to_mired as kelvin_to_mired +from homeassistant.util.color import \ + color_temperature_mired_to_kelvin as mired_to_kelvin + +REQUIREMENTS = ['pyoppleio==1.0.5'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "opple light" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up Opple light platform.""" + name = config[CONF_NAME] + host = config[CONF_HOST] + entity = OppleLight(name, host) + add_entities([entity]) + + _LOGGER.debug("Init light %s %s", host, entity.unique_id) + + +class OppleLight(Light): + """Opple light device.""" + + def __init__(self, name, host): + """Initialize an Opple light.""" + from pyoppleio.OppleLightDevice import OppleLightDevice + self._device = OppleLightDevice(host) + + self._name = name + self._is_on = None + self._brightness = None + self._color_temp = None + + @property + def available(self): + """Return True if light is available.""" + return self._device.is_online + + @property + def unique_id(self): + """Return unique ID for light.""" + return self._device.mac + + @property + def name(self): + """Return the display name of this light.""" + return self._name + + @property + def is_on(self): + """Return true if light is on.""" + return self._is_on + + @property + def brightness(self): + """Return the brightness of the light.""" + return self._brightness + + @property + def color_temp(self): + """Return the color temperature of this light.""" + return kelvin_to_mired(self._color_temp) + + @property + def min_mireds(self): + """Return minimum supported color temperature.""" + return 175 + + @property + def max_mireds(self): + """Return maximum supported color temperature.""" + return 333 + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP + + def turn_on(self, **kwargs): + """Instruct the light to turn on.""" + _LOGGER.debug("Turn on light %s %s", self._device.ip, kwargs) + if not self.is_on: + self._device.power_on = True + + if ATTR_BRIGHTNESS in kwargs and \ + self.brightness != kwargs[ATTR_BRIGHTNESS]: + self._device.brightness = kwargs[ATTR_BRIGHTNESS] + + if ATTR_COLOR_TEMP in kwargs and \ + self.brightness != kwargs[ATTR_COLOR_TEMP]: + color_temp = mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) + self._device.color_temperature = color_temp + + def turn_off(self, **kwargs): + """Instruct the light to turn off.""" + self._device.power_on = False + _LOGGER.debug("Turn off light %s", self._device.ip) + + def update(self): + """Synchronize state with light.""" + prev_available = self.available + self._device.update() + + if prev_available == self.available and \ + self._is_on == self._device.power_on and \ + self._brightness == self._device.brightness and \ + self._color_temp == self._device.color_temperature: + return + + if not self.available: + _LOGGER.debug("Light %s is offline", self._device.ip) + return + + self._is_on = self._device.power_on + self._brightness = self._device.brightness + self._color_temp = self._device.color_temperature + + if not self.is_on: + _LOGGER.debug("Update light %s success: power off", + self._device.ip) + else: + _LOGGER.debug("Update light %s success: power on brightness %s " + "color temperature %s", + self._device.ip, self._brightness, self._color_temp) diff --git a/homeassistant/components/light/rflink.py b/homeassistant/components/light/rflink.py index b410fdceff7..d9f9dd589ec 100644 --- a/homeassistant/components/light/rflink.py +++ b/homeassistant/components/light/rflink.py @@ -4,7 +4,6 @@ Support for Rflink lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.rflink/ """ -import asyncio import logging from homeassistant.components.light import ( @@ -155,14 +154,12 @@ def devices_from_config(domain_config, hass=None): return devices -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Rflink light platform.""" async_add_entities(devices_from_config(config, hass)) - @asyncio.coroutine - def add_new_device(event): + async def add_new_device(event): """Check if device is known, otherwise add to list of known devices.""" device_id = event[EVENT_KEY_ID] @@ -195,15 +192,14 @@ class DimmableRflinkLight(SwitchableRflinkDevice, Light): _brightness = 255 - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on.""" if ATTR_BRIGHTNESS in kwargs: # rflink only support 16 brightness levels self._brightness = int(kwargs[ATTR_BRIGHTNESS] / 17) * 17 # Turn on light at the requested dim level - yield from self._async_handle_command('dim', self._brightness) + await self._async_handle_command('dim', self._brightness) @property def brightness(self): @@ -233,8 +229,7 @@ class HybridRflinkLight(SwitchableRflinkDevice, Light): _brightness = 255 - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on and set dim level.""" if ATTR_BRIGHTNESS in kwargs: # rflink only support 16 brightness levels @@ -242,12 +237,12 @@ class HybridRflinkLight(SwitchableRflinkDevice, Light): # if receiver supports dimming this will turn on the light # at the requested dim level - yield from self._async_handle_command('dim', self._brightness) + await self._async_handle_command('dim', self._brightness) # if the receiving device does not support dimlevel this # will ensure it is turned on when full brightness is set if self._brightness == 255: - yield from self._async_handle_command('turn_on') + await self._async_handle_command('turn_on') @property def brightness(self): @@ -284,12 +279,10 @@ class ToggleRflinkLight(SwitchableRflinkDevice, Light): # if the state is true, it gets set as false self._state = self._state in [STATE_UNKNOWN, False] - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on.""" - yield from self._async_handle_command('toggle') + await self._async_handle_command('toggle') - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the device off.""" - yield from self._async_handle_command('toggle') + await self._async_handle_command('toggle') diff --git a/homeassistant/components/light/template.py b/homeassistant/components/light/template.py index 9be6eb99acc..8aff85c6001 100644 --- a/homeassistant/components/light/template.py +++ b/homeassistant/components/light/template.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.template/ """ import logging -import asyncio import voluptuous as vol @@ -49,9 +48,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Template Lights.""" lights = [] @@ -182,8 +180,7 @@ class LightTemplate(Light): """Return the entity picture to use in the frontend, if any.""" return self._entity_picture - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" @callback def template_light_state_listener(entity, old_state, new_state): @@ -203,8 +200,7 @@ class LightTemplate(Light): self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_START, template_light_startup) - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the light on.""" optimistic_set = False # set optimistic states @@ -219,24 +215,22 @@ class LightTemplate(Light): optimistic_set = True if ATTR_BRIGHTNESS in kwargs and self._level_script: - self.hass.async_add_job(self._level_script.async_run( + self.hass.async_create_task(self._level_script.async_run( {"brightness": kwargs[ATTR_BRIGHTNESS]})) else: - yield from self._on_script.async_run() + await self._on_script.async_run() if optimistic_set: self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the light off.""" - yield from self._off_script.async_run() + await self._off_script.async_run() if self._template is None: self._state = False self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update the state from the template.""" if self._template is not None: try: diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py index b62900b204c..a26a2eb828a 100644 --- a/homeassistant/components/light/tradfri.py +++ b/homeassistant/components/light/tradfri.py @@ -127,7 +127,7 @@ class TradfriGroup(Light): cmd = self._group.observe(callback=self._observe_update, err_callback=self._async_start_observe, duration=0) - self.hass.async_add_job(self._api(cmd)) + self.hass.async_create_task(self._api(cmd)) except PytradfriError as err: _LOGGER.warning("Observation failed, trying again", exc_info=err) self._async_start_observe() @@ -346,7 +346,7 @@ class TradfriLight(Light): cmd = self._light.observe(callback=self._observe_update, err_callback=self._async_start_observe, duration=0) - self.hass.async_add_job(self._api(cmd)) + self.hass.async_create_task(self._api(cmd)) except PytradfriError as err: _LOGGER.warning("Observation failed, trying again", exc_info=err) self._async_start_observe() diff --git a/homeassistant/components/light/wemo.py b/homeassistant/components/light/wemo.py index 72279fbe1a4..55a4836e148 100644 --- a/homeassistant/components/light/wemo.py +++ b/homeassistant/components/light/wemo.py @@ -4,7 +4,6 @@ Support for Belkin WeMo lights. For more details about this component, please refer to the documentation at https://home-assistant.io/components/light.wemo/ """ -import asyncio import logging from datetime import timedelta import requests @@ -160,13 +159,12 @@ class WemoDimmer(Light): self._brightness = None self._state = None - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register update callback.""" wemo = self.hass.components.wemo # The register method uses a threading condition, so call via executor. - # and yield from to wait until the task is done. - yield from self.hass.async_add_job( + # and await to wait until the task is done. + await self.hass.async_add_job( wemo.SUBSCRIPTION_REGISTRY.register, self.wemo) # The on method just appends to a defaultdict list. wemo.SUBSCRIPTION_REGISTRY.on(self.wemo, None, self._update_callback) diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index ee8c2aca8b5..96c8f20679e 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -4,7 +4,6 @@ Support for Wink lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.wink/ """ -import asyncio from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, @@ -34,8 +33,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class WinkLight(WinkDevice, Light): """Representation of a Wink light.""" - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['light'].append(self) diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index 51c36fc2dd0..8bc2497a3e5 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -713,7 +713,7 @@ class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight): _LOGGER.debug("Got new state: %s", state) self._available = True - self._state = state.eyecare + self._state = state.ambient self._brightness = ceil((255 / 100.0) * state.ambient_brightness) except DeviceException as ex: diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 3c4ff7cdedd..e9904f0163a 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -4,7 +4,6 @@ Component to interface with various locks that can be controlled remotely. For more details about this component, please refer to the documentation at https://home-assistant.io/components/lock/ """ -import asyncio from datetime import timedelta import functools as ft import logging @@ -57,49 +56,12 @@ def is_locked(hass, entity_id=None): return hass.states.is_state(entity_id, STATE_LOCKED) -@bind_hass -def lock(hass, entity_id=None, code=None): - """Lock all or specified locks.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_LOCK, data) - - -@bind_hass -def unlock(hass, entity_id=None, code=None): - """Unlock all or specified locks.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_UNLOCK, data) - - -@bind_hass -def open_lock(hass, entity_id=None, code=None): - """Open all or specified locks.""" - data = {} - if code: - data[ATTR_CODE] = code - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_OPEN, data) - - -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Track states and offer events for locks.""" component = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LOCKS) - yield from component.async_setup(config) + await component.async_setup(config) component.async_register_entity_service( SERVICE_UNLOCK, LOCK_SERVICE_SCHEMA, diff --git a/homeassistant/components/lock/bmw_connected_drive.py b/homeassistant/components/lock/bmw_connected_drive.py index c84df54cfba..b13665610d8 100644 --- a/homeassistant/components/lock/bmw_connected_drive.py +++ b/homeassistant/components/lock/bmw_connected_drive.py @@ -4,7 +4,6 @@ Support for BMW cars with BMW ConnectedDrive. For more details about this component, please refer to the documentation at https://home-assistant.io/components/lock.bmw_connected_drive/ """ -import asyncio import logging from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN @@ -111,8 +110,7 @@ class BMWLock(LockDevice): """Schedule a state update.""" self.schedule_update_ha_state(True) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Add callback after being added to hass. Show latest data after startup. diff --git a/homeassistant/components/lock/mqtt.py b/homeassistant/components/lock/mqtt.py index 103864a6bfd..ee43eb942c4 100644 --- a/homeassistant/components/lock/mqtt.py +++ b/homeassistant/components/lock/mqtt.py @@ -4,7 +4,6 @@ Support for MQTT locks. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/lock.mqtt/ """ -import asyncio import logging import voluptuous as vol @@ -12,9 +11,9 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.components.lock import LockDevice from homeassistant.components.mqtt import ( - CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, - CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, - MqttAvailability) + ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, + CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, + CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate) from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE) from homeassistant.components import mqtt @@ -41,9 +40,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the MQTT lock.""" if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) @@ -52,6 +50,10 @@ def async_setup_platform(hass, config, async_add_entities, if value_template is not None: value_template.hass = hass + discovery_hash = None + if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info: + discovery_hash = discovery_info[ATTR_DISCOVERY_HASH] + async_add_entities([MqttLock( config.get(CONF_NAME), config.get(CONF_STATE_TOPIC), @@ -64,19 +66,22 @@ def async_setup_platform(hass, config, async_add_entities, value_template, config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_PAYLOAD_AVAILABLE), - config.get(CONF_PAYLOAD_NOT_AVAILABLE) + config.get(CONF_PAYLOAD_NOT_AVAILABLE), + discovery_hash, )]) -class MqttLock(MqttAvailability, LockDevice): +class MqttLock(MqttAvailability, MqttDiscoveryUpdate, LockDevice): """Representation of a lock that can be toggled using MQTT.""" def __init__(self, name, state_topic, command_topic, qos, retain, payload_lock, payload_unlock, optimistic, value_template, - availability_topic, payload_available, payload_not_available): + availability_topic, payload_available, payload_not_available, + discovery_hash): """Initialize the lock.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_hash) self._state = False self._name = name self._state_topic = state_topic @@ -87,11 +92,12 @@ class MqttLock(MqttAvailability, LockDevice): self._payload_unlock = payload_unlock self._optimistic = optimistic self._template = value_template + self._discovery_hash = discovery_hash - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to MQTT events.""" - yield from super().async_added_to_hass() + await MqttAvailability.async_added_to_hass(self) + await MqttDiscoveryUpdate.async_added_to_hass(self) @callback def message_received(topic, payload, qos): @@ -110,7 +116,7 @@ class MqttLock(MqttAvailability, LockDevice): # Force into optimistic mode. self._optimistic = True else: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._state_topic, message_received, self._qos) @property @@ -133,8 +139,7 @@ class MqttLock(MqttAvailability, LockDevice): """Return true if we do optimistic updates.""" return self._optimistic - @asyncio.coroutine - def async_lock(self, **kwargs): + async def async_lock(self, **kwargs): """Lock the device. This method is a coroutine. @@ -147,8 +152,7 @@ class MqttLock(MqttAvailability, LockDevice): self._state = True self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_unlock(self, **kwargs): + async def async_unlock(self, **kwargs): """Unlock the device. This method is a coroutine. diff --git a/homeassistant/components/lock/nuki.py b/homeassistant/components/lock/nuki.py index 6cf58dda04c..689ec31fc7c 100644 --- a/homeassistant/components/lock/nuki.py +++ b/homeassistant/components/lock/nuki.py @@ -4,7 +4,6 @@ Nuki.io lock platform. For more details about this platform, please refer to the documentation https://home-assistant.io/components/lock.nuki/ """ -import asyncio from datetime import timedelta import logging @@ -92,8 +91,7 @@ class NukiLock(LockDevice): self._name = nuki_lock.name self._battery_critical = nuki_lock.battery_critical - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" if NUKI_DATA not in self.hass.data: self.hass.data[NUKI_DATA] = {} diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/lock/wink.py index 03de8fc5919..68cc7a79ae6 100644 --- a/homeassistant/components/lock/wink.py +++ b/homeassistant/components/lock/wink.py @@ -4,7 +4,6 @@ Support for Wink locks. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/lock.wink/ """ -import asyncio import logging import voluptuous as vol @@ -131,8 +130,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class WinkLockDevice(WinkDevice, LockDevice): """Representation of a Wink lock.""" - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['lock'].append(self) diff --git a/homeassistant/components/lock/zwave.py b/homeassistant/components/lock/zwave.py index 5ee88f053b5..4a2b71f0b48 100644 --- a/homeassistant/components/lock/zwave.py +++ b/homeassistant/components/lock/zwave.py @@ -4,7 +4,6 @@ Z-Wave platform that handles simple door locks. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/lock.zwave/ """ -import asyncio import logging import voluptuous as vol @@ -119,11 +118,10 @@ CLEAR_USERCODE_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Z-Wave Lock platform.""" - yield from zwave.async_setup_platform( + await zwave.async_setup_platform( hass, config, async_add_entities, discovery_info) network = hass.data[zwave.const.DATA_NETWORK] diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index c4fcf53a9c1..5cbd2b9432b 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -10,6 +10,7 @@ import logging import voluptuous as vol +from homeassistant.loader import bind_hass from homeassistant.components import sun from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( @@ -17,8 +18,9 @@ from homeassistant.const import ( CONF_INCLUDE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_LOGBOOK_ENTRY, EVENT_STATE_CHANGED, HTTP_BAD_REQUEST, STATE_NOT_HOME, STATE_OFF, STATE_ON) -from homeassistant.core import DOMAIN as HA_DOMAIN -from homeassistant.core import State, callback, split_entity_id +from homeassistant.core import ( + DOMAIN as HA_DOMAIN, State, callback, split_entity_id) +from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util @@ -53,7 +55,8 @@ CONFIG_SCHEMA = vol.Schema({ ALL_EVENT_TYPES = [ EVENT_STATE_CHANGED, EVENT_LOGBOOK_ENTRY, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + EVENT_ALEXA_SMART_HOME ] LOG_MESSAGE_SCHEMA = vol.Schema({ @@ -64,11 +67,13 @@ LOG_MESSAGE_SCHEMA = vol.Schema({ }) +@bind_hass def log_entry(hass, name, message, domain=None, entity_id=None): """Add an entry to the logbook.""" hass.add_job(async_log_entry, hass, name, message, domain, entity_id) +@bind_hass def async_log_entry(hass, name, message, domain=None, entity_id=None): """Add an entry to the logbook.""" data = { @@ -128,42 +133,26 @@ class LogbookView(HomeAssistantView): else: datetime = dt_util.start_of_local_day() - start_day = dt_util.as_utc(datetime) - end_day = start_day + timedelta(days=1) + period = request.query.get('period') + if period is None: + period = 1 + else: + period = int(period) + + entity_id = request.query.get('entity') + start_day = dt_util.as_utc(datetime) - timedelta(days=period - 1) + end_day = start_day + timedelta(days=period) hass = request.app['hass'] def json_events(): """Fetch events and generate JSON.""" return self.json(list( - _get_events(hass, self.config, start_day, end_day))) + _get_events(hass, self.config, start_day, end_day, entity_id))) return await hass.async_add_job(json_events) -class Entry: - """A human readable version of the log.""" - - def __init__(self, when=None, name=None, message=None, domain=None, - entity_id=None): - """Initialize the entry.""" - self.when = when - self.name = name - self.message = message - self.domain = domain - self.entity_id = entity_id - - def as_dict(self): - """Convert entry to a dict to be used within JSON.""" - return { - 'when': self.when, - 'name': self.name, - 'message': self.message, - 'domain': self.domain, - 'entity_id': self.entity_id, - } - - -def humanify(events): +def humanify(hass, events): """Generate a converted list of events into Entry objects. Will try to group events if possible: @@ -224,20 +213,28 @@ def humanify(events): to_state.attributes.get('unit_of_measurement'): continue - yield Entry( - event.time_fired, - name=to_state.name, - message=_entry_message_from_state(domain, to_state), - domain=domain, - entity_id=to_state.entity_id) + yield { + 'when': event.time_fired, + 'name': to_state.name, + 'message': _entry_message_from_state(domain, to_state), + 'domain': domain, + 'entity_id': to_state.entity_id, + 'context_id': event.context.id, + 'context_user_id': event.context.user_id + } elif event.event_type == EVENT_HOMEASSISTANT_START: if start_stop_events.get(event.time_fired.minute) == 2: continue - yield Entry( - event.time_fired, "Home Assistant", "started", - domain=HA_DOMAIN) + yield { + 'when': event.time_fired, + 'name': "Home Assistant", + 'message': "started", + 'domain': HA_DOMAIN, + 'context_id': event.context.id, + 'context_user_id': event.context.user_id + } elif event.event_type == EVENT_HOMEASSISTANT_STOP: if start_stop_events.get(event.time_fired.minute) == 2: @@ -245,9 +242,14 @@ def humanify(events): else: action = "stopped" - yield Entry( - event.time_fired, "Home Assistant", action, - domain=HA_DOMAIN) + yield { + 'when': event.time_fired, + 'name': "Home Assistant", + 'message': action, + 'domain': HA_DOMAIN, + 'context_id': event.context.id, + 'context_user_id': event.context.user_id + } elif event.event_type == EVENT_LOGBOOK_ENTRY: domain = event.data.get(ATTR_DOMAIN) @@ -258,13 +260,42 @@ def humanify(events): except IndexError: pass - yield Entry( - event.time_fired, event.data.get(ATTR_NAME), - event.data.get(ATTR_MESSAGE), domain, - entity_id) + yield { + 'when': event.time_fired, + 'name': event.data.get(ATTR_NAME), + 'message': event.data.get(ATTR_MESSAGE), + 'domain': domain, + 'entity_id': entity_id, + 'context_id': event.context.id, + 'context_user_id': event.context.user_id + } + + elif event.event_type == EVENT_ALEXA_SMART_HOME: + data = event.data + entity_id = data['request'].get('entity_id') + + if entity_id: + state = hass.states.get(entity_id) + name = state.name if state else entity_id + message = "send command {}/{} for {}".format( + data['request']['namespace'], + data['request']['name'], name) + else: + message = "send command {}/{}".format( + data['request']['namespace'], data['request']['name']) + + yield { + 'when': event.time_fired, + 'name': 'Amazon Alexa', + 'message': message, + 'domain': 'alexa', + 'entity_id': entity_id, + 'context_id': event.context.id, + 'context_user_id': event.context.user_id + } -def _get_events(hass, config, start_day, end_day): +def _get_events(hass, config, start_day, end_day, entity_id=None): """Get events for a period of time.""" from homeassistant.components.recorder.models import Events, States from homeassistant.components.recorder.util import ( @@ -278,8 +309,12 @@ def _get_events(hass, config, start_day, end_day): & (Events.time_fired < end_day)) \ .filter((States.last_updated == States.last_changed) | (States.state_id.is_(None))) + + if entity_id is not None: + query = query.filter(States.entity_id == entity_id.lower()) + events = execute(query) - return humanify(_exclude_events(events, config)) + return humanify(hass, _exclude_events(events, config)) def _exclude_events(events, config): diff --git a/homeassistant/components/logger.py b/homeassistant/components/logger.py index 0baca2f341c..21ae7595ab8 100644 --- a/homeassistant/components/logger.py +++ b/homeassistant/components/logger.py @@ -47,11 +47,6 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -def set_level(hass, logs): - """Set log level for components.""" - hass.services.call(DOMAIN, SERVICE_SET_LEVEL, logs) - - class HomeAssistantLogFilter(logging.Filter): """A log filter.""" diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py new file mode 100644 index 00000000000..a24c8eb9e91 --- /dev/null +++ b/homeassistant/components/lovelace/__init__.py @@ -0,0 +1,51 @@ +"""Lovelace UI.""" +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.util.yaml import load_yaml +from homeassistant.exceptions import HomeAssistantError + +DOMAIN = 'lovelace' + +OLD_WS_TYPE_GET_LOVELACE_UI = 'frontend/lovelace_config' +WS_TYPE_GET_LOVELACE_UI = 'lovelace/config' +SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): vol.Any(WS_TYPE_GET_LOVELACE_UI, + OLD_WS_TYPE_GET_LOVELACE_UI), +}) + + +async def async_setup(hass, config): + """Set up the Lovelace commands.""" + # Backwards compat. Added in 0.80. Remove after 0.85 + hass.components.websocket_api.async_register_command( + OLD_WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config, + SCHEMA_GET_LOVELACE_UI) + + hass.components.websocket_api.async_register_command( + WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config, + SCHEMA_GET_LOVELACE_UI) + + return True + + +@websocket_api.async_response +async def websocket_lovelace_config(hass, connection, msg): + """Send lovelace UI config over websocket config.""" + error = None + try: + config = await hass.async_add_executor_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(message) diff --git a/homeassistant/components/lutron.py b/homeassistant/components/lutron.py index bef821220b3..2e49d7ce690 100644 --- a/homeassistant/components/lutron.py +++ b/homeassistant/components/lutron.py @@ -4,7 +4,6 @@ Component for interacting with a Lutron RadioRA 2 system. For more details about this component, please refer to the documentation at https://home-assistant.io/components/lutron/ """ -import asyncio import logging import voluptuous as vol @@ -69,8 +68,7 @@ class LutronDevice(Entity): self._controller = controller self._area_name = area_name - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" self.hass.async_add_job( self._controller.subscribe, self._lutron_device, diff --git a/homeassistant/components/lutron_caseta.py b/homeassistant/components/lutron_caseta.py index 2535fb76120..eb4010e43a1 100644 --- a/homeassistant/components/lutron_caseta.py +++ b/homeassistant/components/lutron_caseta.py @@ -4,7 +4,6 @@ Component for interacting with a Lutron Caseta system. For more details about this component, please refer to the documentation at https://home-assistant.io/components/lutron_caseta/ """ -import asyncio import logging import voluptuous as vol @@ -40,8 +39,7 @@ LUTRON_CASETA_COMPONENTS = [ ] -@asyncio.coroutine -def async_setup(hass, base_config): +async def async_setup(hass, base_config): """Set up the Lutron component.""" from pylutron_caseta.smartbridge import Smartbridge @@ -54,7 +52,7 @@ def async_setup(hass, base_config): certfile=certfile, ca_certs=ca_certs) hass.data[LUTRON_CASETA_SMARTBRIDGE] = bridge - yield from bridge.connect() + await bridge.connect() if not hass.data[LUTRON_CASETA_SMARTBRIDGE].is_connected(): _LOGGER.error("Unable to connect to Lutron smartbridge at %s", config[CONF_HOST]) @@ -85,8 +83,7 @@ class LutronCasetaDevice(Entity): self._state = None self._smartbridge = bridge - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" self._smartbridge.add_subscriber(self._device_id, self.async_schedule_update_ha_state) diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index a7093579805..ffcb0f6ab95 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2018.09.18'] +REQUIREMENTS = ['youtube_dl==2018.09.26'] _LOGGER = logging.getLogger(__name__) @@ -148,7 +148,7 @@ class MediaExtractor: if entity_id: data[ATTR_ENTITY_ID] = entity_id - self.hass.async_add_job( + self.hass.async_create_task( self.hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, data) ) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 831009ed8bf..8530a01d3e6 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -28,7 +28,6 @@ from homeassistant.const import ( SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, STATE_IDLE, STATE_OFF, STATE_PLAYING, STATE_UNKNOWN) -from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa @@ -192,168 +191,6 @@ def is_on(hass, entity_id=None): for entity_id in entity_ids) -@bind_hass -def turn_on(hass, entity_id=None): - """Turn on specified media player or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TURN_ON, data) - - -@bind_hass -def turn_off(hass, entity_id=None): - """Turn off specified media player or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) - - -@bind_hass -def toggle(hass, entity_id=None): - """Toggle specified media player or all.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TOGGLE, data) - - -@bind_hass -def volume_up(hass, entity_id=None): - """Send the media player the command for volume up.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_VOLUME_UP, data) - - -@bind_hass -def volume_down(hass, entity_id=None): - """Send the media player the command for volume down.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN, data) - - -@bind_hass -def mute_volume(hass, mute, entity_id=None): - """Send the media player the command for muting the volume.""" - data = {ATTR_MEDIA_VOLUME_MUTED: mute} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE, data) - - -@bind_hass -def set_volume_level(hass, volume, entity_id=None): - """Send the media player the command for setting the volume.""" - data = {ATTR_MEDIA_VOLUME_LEVEL: volume} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_VOLUME_SET, data) - - -@bind_hass -def media_play_pause(hass, entity_id=None): - """Send the media player the command for play/pause.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, data) - - -@bind_hass -def media_play(hass, entity_id=None): - """Send the media player the command for play/pause.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY, data) - - -@bind_hass -def media_pause(hass, entity_id=None): - """Send the media player the command for pause.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_PAUSE, data) - - -@bind_hass -def media_stop(hass, entity_id=None): - """Send the media player the stop command.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_STOP, data) - - -@bind_hass -def media_next_track(hass, entity_id=None): - """Send the media player the command for next track.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, data) - - -@bind_hass -def media_previous_track(hass, entity_id=None): - """Send the media player the command for prev track.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data) - - -@bind_hass -def media_seek(hass, position, entity_id=None): - """Send the media player the command to seek in current playing media.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - data[ATTR_MEDIA_SEEK_POSITION] = position - hass.services.call(DOMAIN, SERVICE_MEDIA_SEEK, data) - - -@bind_hass -def play_media(hass, media_type, media_id, entity_id=None, enqueue=None): - """Send the media player the command for playing media.""" - data = {ATTR_MEDIA_CONTENT_TYPE: media_type, - ATTR_MEDIA_CONTENT_ID: media_id} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - if enqueue: - data[ATTR_MEDIA_ENQUEUE] = enqueue - - hass.services.call(DOMAIN, SERVICE_PLAY_MEDIA, data) - - -@bind_hass -def select_source(hass, source, entity_id=None): - """Send the media player the command to select input source.""" - data = {ATTR_INPUT_SOURCE: source} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - 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.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_CLEAR_PLAYLIST, data) - - -@bind_hass -def set_shuffle(hass, shuffle, entity_id=None): - """Send the media player the command to enable/disable shuffle mode.""" - data = {ATTR_MEDIA_SHUFFLE: shuffle} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_SHUFFLE_SET, data) - - WS_TYPE_MEDIA_PLAYER_THUMBNAIL = 'media_player_thumbnail' SCHEMA_WEBSOCKET_GET_THUMBNAIL = \ websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ @@ -1027,8 +864,8 @@ class MediaPlayerImageView(HomeAssistantView): body=data, content_type=content_type, headers=headers) -@callback -def websocket_handle_thumbnail(hass, connection, msg): +@websocket_api.async_response +async def websocket_handle_thumbnail(hass, connection, msg): """Handle get media player cover command. Async friendly. @@ -1037,24 +874,20 @@ def websocket_handle_thumbnail(hass, connection, msg): player = component.get_entity(msg['entity_id']) if player is None: - connection.send_message_outside(websocket_api.error_message( + connection.send_message(websocket_api.error_message( msg['id'], 'entity_not_found', 'Entity not found')) return - async def send_image(): - """Send image.""" - data, content_type = await player.async_get_media_image() + data, content_type = await player.async_get_media_image() - if data is None: - connection.send_message_outside(websocket_api.error_message( - msg['id'], 'thumbnail_fetch_failed', - 'Failed to fetch thumbnail')) - return + if data is None: + connection.send_message(websocket_api.error_message( + msg['id'], 'thumbnail_fetch_failed', + 'Failed to fetch thumbnail')) + return - connection.send_message_outside(websocket_api.result_message( - msg['id'], { - 'content_type': content_type, - 'content': base64.b64encode(data).decode('utf-8') - })) - - hass.async_add_job(send_image()) + connection.send_message(websocket_api.result_message( + msg['id'], { + 'content_type': content_type, + 'content': base64.b64encode(data).decode('utf-8') + })) diff --git a/homeassistant/components/media_player/anthemav.py b/homeassistant/components/media_player/anthemav.py index 33b6e28a890..f1954a1d37e 100644 --- a/homeassistant/components/media_player/anthemav.py +++ b/homeassistant/components/media_player/anthemav.py @@ -4,7 +4,6 @@ Support for Anthem Network Receivers and Processors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.anthemav/ """ -import asyncio import logging import voluptuous as vol @@ -35,9 +34,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up our socket to the AVR.""" import anthemav @@ -51,9 +49,9 @@ def async_setup_platform(hass, config, async_add_entities, def async_anthemav_update_callback(message): """Receive notification from transport that new data exists.""" _LOGGER.info("Received update callback from AVR: %s", message) - hass.async_add_job(device.async_update_ha_state()) + hass.async_create_task(device.async_update_ha_state()) - avr = yield from anthemav.Connection.create( + avr = await anthemav.Connection.create( host=host, port=port, loop=hass.loop, update_callback=async_anthemav_update_callback) @@ -136,28 +134,23 @@ class AnthemAVR(MediaPlayerDevice): """Return all active, configured inputs.""" return self._lookup('input_list', ["Unknown"]) - @asyncio.coroutine - def async_select_source(self, source): + async def async_select_source(self, source): """Change AVR to the designated source (by name).""" self._update_avr('input_name', source) - @asyncio.coroutine - def async_turn_off(self): + async def async_turn_off(self): """Turn AVR power off.""" self._update_avr('power', False) - @asyncio.coroutine - def async_turn_on(self): + async def async_turn_on(self): """Turn AVR power on.""" self._update_avr('power', True) - @asyncio.coroutine - def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume): """Set AVR volume (0 to 1).""" self._update_avr('volume_as_percentage', volume) - @asyncio.coroutine - def async_mute_volume(self, mute): + async def async_mute_volume(self, mute): """Engage AVR mute.""" self._update_avr('mute', mute) diff --git a/homeassistant/components/media_player/apple_tv.py b/homeassistant/components/media_player/apple_tv.py index 399e59ae9f5..bff8834639d 100644 --- a/homeassistant/components/media_player/apple_tv.py +++ b/homeassistant/components/media_player/apple_tv.py @@ -4,7 +4,6 @@ Support for Apple TV. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.apple_tv/ """ -import asyncio import logging from homeassistant.components.apple_tv import ( @@ -29,8 +28,7 @@ SUPPORT_APPLE_TV = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PLAY_MEDIA | \ SUPPORT_STOP | SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK -@asyncio.coroutine -def async_setup_platform( +async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): """Set up the Apple TV platform.""" if not discovery_info: @@ -71,8 +69,7 @@ class AppleTvDevice(MediaPlayerDevice): self._power.listeners.append(self) self.atv.push_updater.listener = self - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Handle when an entity is about to be added to Home Assistant.""" self._power.init() @@ -164,10 +161,9 @@ class AppleTvDevice(MediaPlayerDevice): if state in (STATE_PLAYING, STATE_PAUSED): return dt_util.utcnow() - @asyncio.coroutine - def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media(self, media_type, media_id, **kwargs): """Send the play_media command to the media player.""" - yield from self.atv.airplay.play_url(media_id) + await self.atv.airplay.play_url(media_id) @property def media_image_hash(self): @@ -176,12 +172,11 @@ class AppleTvDevice(MediaPlayerDevice): if self._playing and state not in [STATE_OFF, STATE_IDLE]: return self._playing.hash - @asyncio.coroutine - def async_get_media_image(self): + async def async_get_media_image(self): """Fetch media image of current playing image.""" state = self.state if self._playing and state not in [STATE_OFF, STATE_IDLE]: - return (yield from self.atv.metadata.artwork()), 'image/png' + return (await self.atv.metadata.artwork()), 'image/png' return None, None @@ -201,13 +196,11 @@ class AppleTvDevice(MediaPlayerDevice): """Flag media player features that are supported.""" return SUPPORT_APPLE_TV - @asyncio.coroutine - def async_turn_on(self): + async def async_turn_on(self): """Turn the media player on.""" self._power.set_power_on(True) - @asyncio.coroutine - def async_turn_off(self): + async def async_turn_off(self): """Turn the media player off.""" self._playing = None self._power.set_power_on(False) diff --git a/homeassistant/components/media_player/bluesound.py b/homeassistant/components/media_player/bluesound.py index ab012402636..f4ed62b15cd 100644 --- a/homeassistant/components/media_player/bluesound.py +++ b/homeassistant/components/media_player/bluesound.py @@ -96,7 +96,7 @@ def _add_player(hass, async_add_entities, host, port=None, name=None): @callback def _init_player(event=None): """Start polling.""" - hass.async_add_job(player.async_init()) + hass.async_create_task(player.async_init()) @callback def _start_polling(event=None): @@ -272,7 +272,7 @@ class BluesoundPlayer(MediaPlayerDevice): def start_polling(self): """Start the polling task.""" - self._polling_task = self._hass.async_add_job( + self._polling_task = self._hass.async_create_task( self._start_poll_command()) def stop_polling(self): diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 67d8ea0b419..d6515b9476d 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -343,7 +343,7 @@ class CastDevice(MediaPlayerDevice): # Discovered is not our device. return _LOGGER.debug("Discovered chromecast with same UUID: %s", discover) - self.hass.async_add_job(self.async_set_cast_info(discover)) + self.hass.async_create_task(self.async_set_cast_info(discover)) async def async_stop(event): """Disconnect socket on Home Assistant stop.""" @@ -352,7 +352,7 @@ class CastDevice(MediaPlayerDevice): async_dispatcher_connect(self.hass, SIGNAL_CAST_DISCOVERED, async_cast_discovered) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop) - self.hass.async_add_job(self.async_set_cast_info(self._cast_info)) + self.hass.async_create_task(self.async_set_cast_info(self._cast_info)) async def async_will_remove_from_hass(self) -> None: """Disconnect Chromecast object when removed.""" diff --git a/homeassistant/components/media_player/clementine.py b/homeassistant/components/media_player/clementine.py index e38c44b8d27..2add2bd682a 100644 --- a/homeassistant/components/media_player/clementine.py +++ b/homeassistant/components/media_player/clementine.py @@ -4,7 +4,6 @@ Support for Clementine Music Player as media player. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.clementine/ """ -import asyncio from datetime import timedelta import logging import time @@ -169,8 +168,7 @@ class ClementineDevice(MediaPlayerDevice): return None - @asyncio.coroutine - def async_get_media_image(self): + async def async_get_media_image(self): """Fetch media image of current playing image.""" if self._client.current_track: image = bytes(self._client.current_track['art']) diff --git a/homeassistant/components/media_player/emby.py b/homeassistant/components/media_player/emby.py index 2bf3a1b803f..50d2426c315 100644 --- a/homeassistant/components/media_player/emby.py +++ b/homeassistant/components/media_player/emby.py @@ -4,7 +4,6 @@ Support to interface with the Emby API. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.emby/ """ -import asyncio import logging import voluptuous as vol @@ -50,9 +49,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Emby platform.""" from pyemby import EmbyServer @@ -113,10 +111,9 @@ def async_setup_platform(hass, config, async_add_entities, """Start Emby connection.""" emby.start() - @asyncio.coroutine - def stop_emby(event): + async def stop_emby(event): """Stop Emby connection.""" - yield from emby.stop() + await emby.stop() emby.add_new_devices_callback(device_update_callback) emby.add_stale_devices_callback(device_removal_callback) @@ -141,8 +138,7 @@ class EmbyDevice(MediaPlayerDevice): self.media_status_last_position = None self.media_status_received = None - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callback.""" self.emby.add_update_callback( self.async_update_callback, self.device_id) diff --git a/homeassistant/components/media_player/frontier_silicon.py b/homeassistant/components/media_player/frontier_silicon.py index aebdb676859..67c84bd7b1b 100644 --- a/homeassistant/components/media_player/frontier_silicon.py +++ b/homeassistant/components/media_player/frontier_silicon.py @@ -4,7 +4,6 @@ Support for Frontier Silicon Devices (Medion, Hama, Auna,...). For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.frontier_silicon/ """ -import asyncio import logging import voluptuous as vol @@ -41,8 +40,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform( +async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): """Set up the Frontier Silicon platform.""" import requests @@ -157,18 +155,17 @@ class AFSAPIDevice(MediaPlayerDevice): """Image url of current playing media.""" return self._media_image_url - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest date and update device state.""" fs_device = self.fs_device if not self._name: - self._name = yield from fs_device.get_friendly_name() + self._name = await fs_device.get_friendly_name() if not self._source_list: - self._source_list = yield from fs_device.get_mode_list() + self._source_list = await fs_device.get_mode_list() - status = yield from fs_device.get_play_status() + status = await fs_device.get_play_status() self._state = { 'playing': STATE_PLAYING, 'paused': STATE_PAUSED, @@ -178,16 +175,16 @@ class AFSAPIDevice(MediaPlayerDevice): }.get(status, STATE_UNKNOWN) if self._state != STATE_OFF: - info_name = yield from fs_device.get_play_name() - info_text = yield from fs_device.get_play_text() + info_name = await fs_device.get_play_name() + info_text = await fs_device.get_play_text() self._title = ' - '.join(filter(None, [info_name, info_text])) - self._artist = yield from fs_device.get_play_artist() - self._album_name = yield from fs_device.get_play_album() + self._artist = await fs_device.get_play_artist() + self._album_name = await fs_device.get_play_album() - self._source = yield from fs_device.get_mode() - self._mute = yield from fs_device.get_mute() - self._media_image_url = yield from fs_device.get_play_graphic() + self._source = await fs_device.get_mode() + self._mute = await fs_device.get_mute() + self._media_image_url = await fs_device.get_play_graphic() else: self._title = None self._artist = None @@ -199,48 +196,40 @@ class AFSAPIDevice(MediaPlayerDevice): # Management actions # power control - @asyncio.coroutine - def async_turn_on(self): + async def async_turn_on(self): """Turn on the device.""" - yield from self.fs_device.set_power(True) + await self.fs_device.set_power(True) - @asyncio.coroutine - def async_turn_off(self): + async def async_turn_off(self): """Turn off the device.""" - yield from self.fs_device.set_power(False) + await self.fs_device.set_power(False) - @asyncio.coroutine - def async_media_play(self): + async def async_media_play(self): """Send play command.""" - yield from self.fs_device.play() + await self.fs_device.play() - @asyncio.coroutine - def async_media_pause(self): + async def async_media_pause(self): """Send pause command.""" - yield from self.fs_device.pause() + await self.fs_device.pause() - @asyncio.coroutine - def async_media_play_pause(self): + async def async_media_play_pause(self): """Send play/pause command.""" if 'playing' in self._state: - yield from self.fs_device.pause() + await self.fs_device.pause() else: - yield from self.fs_device.play() + await self.fs_device.play() - @asyncio.coroutine - def async_media_stop(self): + async def async_media_stop(self): """Send play/pause command.""" - yield from self.fs_device.pause() + await self.fs_device.pause() - @asyncio.coroutine - def async_media_previous_track(self): + async def async_media_previous_track(self): """Send previous track command (results in rewind).""" - yield from self.fs_device.rewind() + await self.fs_device.rewind() - @asyncio.coroutine - def async_media_next_track(self): + async def async_media_next_track(self): """Send next track command (results in fast-forward).""" - yield from self.fs_device.forward() + await self.fs_device.forward() # mute @property @@ -248,30 +237,25 @@ class AFSAPIDevice(MediaPlayerDevice): """Boolean if volume is currently muted.""" return self._mute - @asyncio.coroutine - def async_mute_volume(self, mute): + async def async_mute_volume(self, mute): """Send mute command.""" - yield from self.fs_device.set_mute(mute) + await self.fs_device.set_mute(mute) # volume - @asyncio.coroutine - def async_volume_up(self): + async def async_volume_up(self): """Send volume up command.""" - volume = yield from self.fs_device.get_volume() - yield from self.fs_device.set_volume(volume+1) + volume = await self.fs_device.get_volume() + await self.fs_device.set_volume(volume+1) - @asyncio.coroutine - def async_volume_down(self): + async def async_volume_down(self): """Send volume down command.""" - volume = yield from self.fs_device.get_volume() - yield from self.fs_device.set_volume(volume-1) + volume = await self.fs_device.get_volume() + await self.fs_device.set_volume(volume-1) - @asyncio.coroutine - def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume): """Set volume command.""" - yield from self.fs_device.set_volume(volume) + await self.fs_device.set_volume(volume) - @asyncio.coroutine - def async_select_source(self, source): + async def async_select_source(self, source): """Select input source.""" - yield from self.fs_device.set_mode(source) + await self.fs_device.set_mode(source) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index c98dc5c56fe..01d8069dc3b 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -156,9 +156,8 @@ def _check_deprecated_turn_off(hass, turn_off_action): return turn_off_action -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Kodi platform.""" if DATA_KODI not in hass.data: hass.data[DATA_KODI] = dict() @@ -211,8 +210,7 @@ def async_setup_platform(hass, config, async_add_entities, hass.data[DATA_KODI][ip_addr] = entity async_add_entities([entity], update_before_add=True) - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Map services to methods on MediaPlayerDevice.""" method = SERVICE_TO_METHOD.get(service.service) if not method: @@ -230,7 +228,7 @@ def async_setup_platform(hass, config, async_add_entities, update_tasks = [] for player in target_players: - yield from getattr(player, method['method'])(**params) + await getattr(player, method['method'])(**params) for player in target_players: if player.should_poll: @@ -238,7 +236,7 @@ def async_setup_platform(hass, config, async_add_entities, update_tasks.append(update_coro) if update_tasks: - yield from asyncio.wait(update_tasks, loop=hass.loop) + await asyncio.wait(update_tasks, loop=hass.loop) if hass.services.has_service(DOMAIN, SERVICE_ADD_MEDIA): return @@ -253,12 +251,11 @@ def async_setup_platform(hass, config, async_add_entities, def cmd(func): """Catch command exceptions.""" @wraps(func) - @asyncio.coroutine - def wrapper(obj, *args, **kwargs): + async def wrapper(obj, *args, **kwargs): """Wrap all command methods.""" import jsonrpc_base try: - yield from func(obj, *args, **kwargs) + await func(obj, *args, **kwargs) except jsonrpc_base.jsonrpc.TransportError as exc: # If Kodi is off, we expect calls to fail. if obj.state == STATE_OFF: @@ -325,7 +322,7 @@ class KodiDevice(MediaPlayerDevice): def on_hass_stop(event): """Close websocket connection when hass stops.""" - self.hass.async_add_job(self._ws_server.close()) + self.hass.async_create_task(self._ws_server.close()) self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, on_hass_stop) @@ -390,14 +387,13 @@ class KodiDevice(MediaPlayerDevice): self._properties = {} self._item = {} self._app_properties = {} - self.hass.async_add_job(self._ws_server.close()) + self.hass.async_create_task(self._ws_server.close()) - @asyncio.coroutine - def _get_players(self): + async def _get_players(self): """Return the active player objects or None.""" import jsonrpc_base try: - return (yield from self.server.Player.GetActivePlayers()) + return await self.server.Player.GetActivePlayers() except jsonrpc_base.jsonrpc.TransportError: if self._players is not None: _LOGGER.info("Unable to fetch kodi data") @@ -423,23 +419,21 @@ class KodiDevice(MediaPlayerDevice): return STATE_PLAYING - @asyncio.coroutine - def async_ws_connect(self): + async def async_ws_connect(self): """Connect to Kodi via websocket protocol.""" import jsonrpc_base try: - ws_loop_future = yield from self._ws_server.ws_connect() + ws_loop_future = await self._ws_server.ws_connect() except jsonrpc_base.jsonrpc.TransportError: _LOGGER.info("Unable to connect to Kodi via websocket") _LOGGER.debug( "Unable to connect to Kodi via websocket", exc_info=True) return - @asyncio.coroutine - def ws_loop_wrapper(): + async def ws_loop_wrapper(): """Catch exceptions from the websocket loop task.""" try: - yield from ws_loop_future + await ws_loop_future except jsonrpc_base.TransportError: # Kodi abruptly ends ws connection when exiting. We will try # to reconnect on the next poll. @@ -451,10 +445,9 @@ class KodiDevice(MediaPlayerDevice): # run until the websocket connection is closed. self.hass.loop.create_task(ws_loop_wrapper()) - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Retrieve latest state.""" - self._players = yield from self._get_players() + self._players = await self._get_players() if self._players is None: self._properties = {} @@ -463,10 +456,10 @@ class KodiDevice(MediaPlayerDevice): return if self._enable_websocket and not self._ws_server.connected: - self.hass.async_add_job(self.async_ws_connect()) + self.hass.async_create_task(self.async_ws_connect()) self._app_properties = \ - yield from self.server.Application.GetProperties( + await self.server.Application.GetProperties( ['volume', 'muted'] ) @@ -475,12 +468,12 @@ class KodiDevice(MediaPlayerDevice): assert isinstance(player_id, int) - self._properties = yield from self.server.Player.GetProperties( + self._properties = await self.server.Player.GetProperties( player_id, ['time', 'totaltime', 'speed', 'live'] ) - self._item = (yield from self.server.Player.GetItem( + self._item = (await self.server.Player.GetItem( player_id, ['title', 'file', 'uniqueid', 'thumbnail', 'artist', 'albumartist', 'showtitle', 'album', 'season', 'episode'] @@ -622,38 +615,34 @@ class KodiDevice(MediaPlayerDevice): return supported_features @cmd - @asyncio.coroutine - def async_turn_on(self): + async def async_turn_on(self): """Execute turn_on_action to turn on media player.""" if self._turn_on_action is not None: - yield from self._turn_on_action.async_run( + await self._turn_on_action.async_run( variables={"entity_id": self.entity_id}) else: _LOGGER.warning("turn_on requested but turn_on_action is none") @cmd - @asyncio.coroutine - def async_turn_off(self): + async def async_turn_off(self): """Execute turn_off_action to turn off media player.""" if self._turn_off_action is not None: - yield from self._turn_off_action.async_run( + await self._turn_off_action.async_run( variables={"entity_id": self.entity_id}) else: _LOGGER.warning("turn_off requested but turn_off_action is none") @cmd - @asyncio.coroutine - def async_volume_up(self): + async def async_volume_up(self): """Volume up the media player.""" assert ( - yield from self.server.Input.ExecuteAction('volumeup')) == 'OK' + await self.server.Input.ExecuteAction('volumeup')) == 'OK' @cmd - @asyncio.coroutine - def async_volume_down(self): + async def async_volume_down(self): """Volume down the media player.""" assert ( - yield from self.server.Input.ExecuteAction('volumedown')) == 'OK' + await self.server.Input.ExecuteAction('volumedown')) == 'OK' @cmd def async_set_volume_level(self, volume): @@ -671,13 +660,12 @@ class KodiDevice(MediaPlayerDevice): """ return self.server.Application.SetMute(mute) - @asyncio.coroutine - def async_set_play_state(self, state): + async def async_set_play_state(self, state): """Handle play/pause/toggle.""" - players = yield from self._get_players() + players = await self._get_players() if players is not None and players: - yield from self.server.Player.PlayPause( + await self.server.Player.PlayPause( players[0]['playerid'], state) @cmd @@ -705,26 +693,24 @@ class KodiDevice(MediaPlayerDevice): return self.async_set_play_state(False) @cmd - @asyncio.coroutine - def async_media_stop(self): + async def async_media_stop(self): """Stop the media player.""" - players = yield from self._get_players() + players = await self._get_players() if players: - yield from self.server.Player.Stop(players[0]['playerid']) + await self.server.Player.Stop(players[0]['playerid']) - @asyncio.coroutine - def _goto(self, direction): + async def _goto(self, direction): """Handle for previous/next track.""" - players = yield from self._get_players() + players = await self._get_players() if players: if direction == 'previous': # First seek to position 0. Kodi goes to the beginning of the # current track if the current track is not at the beginning. - yield from self.server.Player.Seek(players[0]['playerid'], 0) + await self.server.Player.Seek(players[0]['playerid'], 0) - yield from self.server.Player.GoTo( + await self.server.Player.GoTo( players[0]['playerid'], direction) @cmd @@ -744,10 +730,9 @@ class KodiDevice(MediaPlayerDevice): return self._goto('previous') @cmd - @asyncio.coroutine - def async_media_seek(self, position): + async def async_media_seek(self, position): """Send seek command.""" - players = yield from self._get_players() + players = await self._get_players() time = {} @@ -763,7 +748,7 @@ class KodiDevice(MediaPlayerDevice): time['hours'] = int(position) if players: - yield from self.server.Player.Seek(players[0]['playerid'], time) + await self.server.Player.Seek(players[0]['playerid'], time) @cmd def async_play_media(self, media_type, media_id, **kwargs): @@ -781,22 +766,20 @@ class KodiDevice(MediaPlayerDevice): return self.server.Player.Open( {"item": {"file": str(media_id)}}) - @asyncio.coroutine - def async_set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle): """Set shuffle mode, for the first player.""" if not self._players: raise RuntimeError("Error: No active player.") - yield from self.server.Player.SetShuffle( + await self.server.Player.SetShuffle( {"playerid": self._players[0]['playerid'], "shuffle": shuffle}) - @asyncio.coroutine - def async_call_method(self, method, **kwargs): + async def async_call_method(self, method, **kwargs): """Run Kodi JSONRPC API method with params.""" import jsonrpc_base _LOGGER.debug("Run API method %s, kwargs=%s", method, kwargs) result_ok = False try: - result = yield from getattr(self.server, method)(**kwargs) + result = await getattr(self.server, method)(**kwargs) result_ok = True except jsonrpc_base.jsonrpc.ProtocolError as exc: result = exc.args[2]['error'] @@ -817,8 +800,7 @@ class KodiDevice(MediaPlayerDevice): event_data=event_data) return result - @asyncio.coroutine - def async_add_media_to_playlist( + async def async_add_media_to_playlist( self, media_type, media_id=None, media_name='ALL', artist_name=''): """Add a media to default playlist (i.e. playlistid=0). @@ -832,7 +814,7 @@ class KodiDevice(MediaPlayerDevice): params = {"playlistid": 0} if media_type == "SONG": if media_id is None: - media_id = yield from self.async_find_song( + media_id = await self.async_find_song( media_name, artist_name) if media_id: params["item"] = {"songid": int(media_id)} @@ -840,10 +822,10 @@ class KodiDevice(MediaPlayerDevice): elif media_type == "ALBUM": if media_id is None: if media_name == "ALL": - yield from self.async_add_all_albums(artist_name) + await self.async_add_all_albums(artist_name) return - media_id = yield from self.async_find_album( + media_id = await self.async_find_album( media_name, artist_name) if media_id: params["item"] = {"albumid": int(media_id)} @@ -853,7 +835,7 @@ class KodiDevice(MediaPlayerDevice): if media_id is not None: try: - yield from self.server.Playlist.Add(params) + await self.server.Playlist.Add(params) except jsonrpc_base.jsonrpc.ProtocolError as exc: result = exc.args[2]['error'] _LOGGER.error("Run API method %s.Playlist.Add(%s) error: %s", @@ -864,43 +846,38 @@ class KodiDevice(MediaPlayerDevice): else: _LOGGER.warning("No media detected for Playlist.Add") - @asyncio.coroutine - def async_add_all_albums(self, artist_name): + async def async_add_all_albums(self, artist_name): """Add all albums of an artist to default playlist (i.e. playlistid=0). The artist is specified in terms of name. """ - artist_id = yield from self.async_find_artist(artist_name) + artist_id = await self.async_find_artist(artist_name) - albums = yield from self.async_get_albums(artist_id) + albums = await self.async_get_albums(artist_id) for alb in albums['albums']: - yield from self.server.Playlist.Add( + await self.server.Playlist.Add( {"playlistid": 0, "item": {"albumid": int(alb['albumid'])}}) - @asyncio.coroutine - def async_clear_playlist(self): + async def async_clear_playlist(self): """Clear default playlist (i.e. playlistid=0).""" return self.server.Playlist.Clear({"playlistid": 0}) - @asyncio.coroutine - def async_get_artists(self): + async def async_get_artists(self): """Get artists list.""" - return (yield from self.server.AudioLibrary.GetArtists()) + return await self.server.AudioLibrary.GetArtists() - @asyncio.coroutine - def async_get_albums(self, artist_id=None): + async def async_get_albums(self, artist_id=None): """Get albums list.""" if artist_id is None: - return (yield from self.server.AudioLibrary.GetAlbums()) + return await self.server.AudioLibrary.GetAlbums() - return (yield from self.server.AudioLibrary.GetAlbums( + return (await self.server.AudioLibrary.GetAlbums( {"filter": {"artistid": int(artist_id)}})) - @asyncio.coroutine - def async_find_artist(self, artist_name): + async def async_find_artist(self, artist_name): """Find artist by name.""" - artists = yield from self.async_get_artists() + artists = await self.async_get_artists() try: out = self._find( artist_name, [a['artist'] for a in artists['artists']]) @@ -909,37 +886,34 @@ class KodiDevice(MediaPlayerDevice): _LOGGER.warning("No artists were found: %s", artist_name) return None - @asyncio.coroutine - def async_get_songs(self, artist_id=None): + async def async_get_songs(self, artist_id=None): """Get songs list.""" if artist_id is None: - return (yield from self.server.AudioLibrary.GetSongs()) + return await self.server.AudioLibrary.GetSongs() - return (yield from self.server.AudioLibrary.GetSongs( + return (await self.server.AudioLibrary.GetSongs( {"filter": {"artistid": int(artist_id)}})) - @asyncio.coroutine - def async_find_song(self, song_name, artist_name=''): + async def async_find_song(self, song_name, artist_name=''): """Find song by name and optionally artist name.""" artist_id = None if artist_name != '': - artist_id = yield from self.async_find_artist(artist_name) + artist_id = await self.async_find_artist(artist_name) - songs = yield from self.async_get_songs(artist_id) + songs = await self.async_get_songs(artist_id) if songs['limits']['total'] == 0: return None out = self._find(song_name, [a['label'] for a in songs['songs']]) return songs['songs'][out[0][0]]['songid'] - @asyncio.coroutine - def async_find_album(self, album_name, artist_name=''): + async def async_find_album(self, album_name, artist_name=''): """Find album by name and optionally artist name.""" artist_id = None if artist_name != '': - artist_id = yield from self.async_find_artist(artist_name) + artist_id = await self.async_find_artist(artist_name) - albums = yield from self.async_get_albums(artist_id) + albums = await self.async_get_albums(artist_id) try: out = self._find( album_name, [a['label'] for a in albums['albums']]) diff --git a/homeassistant/components/media_player/liveboxplaytv.py b/homeassistant/components/media_player/liveboxplaytv.py index 9a08ceeac93..3f8ea2cfd48 100644 --- a/homeassistant/components/media_player/liveboxplaytv.py +++ b/homeassistant/components/media_player/liveboxplaytv.py @@ -4,7 +4,6 @@ Support for interface with an Orange Livebox Play TV appliance. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.liveboxplaytv/ """ -import asyncio from datetime import timedelta import logging @@ -44,9 +43,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Orange Livebox Play TV platform.""" host = config.get(CONF_HOST) port = config.get(CONF_PORT) @@ -83,8 +81,7 @@ class LiveboxPlayTvDevice(MediaPlayerDevice): self._media_image_url = None self._media_last_updated = None - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Retrieve the latest data.""" import pyteleloisirs try: @@ -95,7 +92,7 @@ class LiveboxPlayTvDevice(MediaPlayerDevice): channel = self._client.channel if channel is not None: self._current_channel = channel - program = yield from \ + program = await \ self._client.async_get_current_program() if program and self._current_program != program.get('name'): self._current_program = program.get('name') @@ -109,7 +106,7 @@ class LiveboxPlayTvDevice(MediaPlayerDevice): # Set media image to current program if a thumbnail is # available. Otherwise we'll use the channel's image. img_size = 800 - prg_img_url = yield from \ + prg_img_url = await \ self._client.async_get_current_program_image(img_size) if prg_img_url: self._media_image_url = prg_img_url diff --git a/homeassistant/components/media_player/russound_rio.py b/homeassistant/components/media_player/russound_rio.py index 74f6bfb35ab..19cc2228d32 100644 --- a/homeassistant/components/media_player/russound_rio.py +++ b/homeassistant/components/media_player/russound_rio.py @@ -4,7 +4,6 @@ Support for Russound multizone controllers using RIO Protocol. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.russound_rio/ """ -import asyncio import logging import voluptuous as vol @@ -33,8 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform( +async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): """Set up the Russound RIO platform.""" from russound_rio import Russound @@ -44,15 +42,15 @@ def async_setup_platform( russ = Russound(hass.loop, host, port) - yield from russ.connect() + await russ.connect() # Discover sources and zones - sources = yield from russ.enumerate_sources() - valid_zones = yield from russ.enumerate_zones() + sources = await russ.enumerate_sources() + valid_zones = await russ.enumerate_zones() devices = [] for zone_id, name in valid_zones: - yield from russ.watch_zone(zone_id) + await russ.watch_zone(zone_id) dev = RussoundZoneDevice(russ, zone_id, name, sources) devices.append(dev) @@ -108,8 +106,7 @@ class RussoundZoneDevice(MediaPlayerDevice): if source_id == current: self.schedule_update_ha_state() - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callback handlers.""" self._russ.add_zone_callback(self._zone_callback_handler) self._russ.add_source_callback(self._source_callback_handler) diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index c0a5d617f19..3a66aa66dc0 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -30,7 +30,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Samsung TV Remote' DEFAULT_PORT = 55000 -DEFAULT_TIMEOUT = 0 +DEFAULT_TIMEOUT = 1 KEY_PRESS_TIMEOUT = 1.2 KNOWN_DEVICES_KEY = 'samsungtv_known_devices' @@ -125,10 +125,14 @@ class SamsungTVDevice(MediaPlayerDevice): def update(self): """Update state of device.""" if sys.platform == 'win32': - _ping_cmd = ['ping', '-n 1', '-w', '1000', self._config['host']] + timeout_arg = '-w {}000'.format(self._config['timeout']) + _ping_cmd = [ + 'ping', '-n 3', timeout_arg, self._config['host']] else: - _ping_cmd = ['ping', '-n', '-q', '-c1', '-W1', - self._config['host']] + timeout_arg = '-W{}'.format(self._config['timeout']) + _ping_cmd = [ + 'ping', '-n', '-q', + '-c3', timeout_arg, self._config['host']] ping = subprocess.Popen( _ping_cmd, diff --git a/homeassistant/components/media_player/snapcast.py b/homeassistant/components/media_player/snapcast.py index 84864caeed1..fca440df783 100644 --- a/homeassistant/components/media_player/snapcast.py +++ b/homeassistant/components/media_player/snapcast.py @@ -4,7 +4,6 @@ Support for interacting with Snapcast clients. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.snapcast/ """ -import asyncio import logging import socket @@ -46,17 +45,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Snapcast platform.""" import snapcast.control from snapcast.control.server import CONTROL_PORT host = config.get(CONF_HOST) port = config.get(CONF_PORT, CONTROL_PORT) - @asyncio.coroutine - def _handle_service(service): + async def _handle_service(service): """Handle services.""" entity_ids = service.data.get(ATTR_ENTITY_ID) devices = [device for device in hass.data[DATA_KEY] @@ -65,7 +62,7 @@ def async_setup_platform(hass, config, async_add_entities, if service.service == SERVICE_SNAPSHOT: device.snapshot() elif service.service == SERVICE_RESTORE: - yield from device.async_restore() + await device.async_restore() hass.services.async_register( DOMAIN, SERVICE_SNAPSHOT, _handle_service, schema=SERVICE_SCHEMA) @@ -73,7 +70,7 @@ def async_setup_platform(hass, config, async_add_entities, DOMAIN, SERVICE_RESTORE, _handle_service, schema=SERVICE_SCHEMA) try: - server = yield from snapcast.control.create_server( + server = await snapcast.control.create_server( hass.loop, host, port, reconnect=True) except socket.gaierror: _LOGGER.error("Could not connect to Snapcast server at %s:%d", @@ -157,34 +154,30 @@ class SnapcastGroupDevice(MediaPlayerDevice): """Do not poll for state.""" return False - @asyncio.coroutine - def async_select_source(self, source): + async def async_select_source(self, source): """Set input source.""" streams = self._group.streams_by_name() if source in streams: - yield from self._group.set_stream(streams[source].identifier) + await self._group.set_stream(streams[source].identifier) self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_mute_volume(self, mute): + async def async_mute_volume(self, mute): """Send the mute command.""" - yield from self._group.set_muted(mute) + await self._group.set_muted(mute) self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume): """Set the volume level.""" - yield from self._group.set_volume(round(volume * 100)) + await self._group.set_volume(round(volume * 100)) self.async_schedule_update_ha_state() def snapshot(self): """Snapshot the group state.""" self._group.snapshot() - @asyncio.coroutine - def async_restore(self): + async def async_restore(self): """Restore the group state.""" - yield from self._group.restore() + await self._group.restore() class SnapcastClientDevice(MediaPlayerDevice): @@ -246,23 +239,20 @@ class SnapcastClientDevice(MediaPlayerDevice): """Do not poll for state.""" return False - @asyncio.coroutine - def async_mute_volume(self, mute): + async def async_mute_volume(self, mute): """Send the mute command.""" - yield from self._client.set_muted(mute) + await self._client.set_muted(mute) self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume): """Set the volume level.""" - yield from self._client.set_volume(round(volume * 100)) + await self._client.set_volume(round(volume * 100)) self.async_schedule_update_ha_state() def snapshot(self): """Snapshot the client state.""" self._client.snapshot() - @asyncio.coroutine - def async_restore(self): + async def async_restore(self): """Restore the client state.""" - yield from self._client.restore() + await self._client.restore() diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index fd735a5b830..41ca1b4e85e 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -4,7 +4,6 @@ Support to interface with Sonos players. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.sonos/ """ -import asyncio import datetime import functools as ft import logging @@ -33,9 +32,7 @@ _LOGGER = logging.getLogger(__name__) # Quiet down pysonos logging to just actual problems. logging.getLogger('pysonos').setLevel(logging.WARNING) -logging.getLogger('pysonos.events').setLevel(logging.ERROR) logging.getLogger('pysonos.data_structures_entry').setLevel(logging.ERROR) -_SOCO_SERVICES_LOGGER = logging.getLogger('pysonos.services') SUPPORT_SONOS = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ SUPPORT_PLAY | SUPPORT_PAUSE | SUPPORT_STOP | SUPPORT_SELECT_SOURCE |\ @@ -72,7 +69,7 @@ ATTR_SPEECH_ENHANCE = 'speech_enhance' ATTR_SONOS_GROUP = 'sonos_group' -UPNP_ERRORS_TO_IGNORE = ['701', '711'] +UPNP_ERRORS_TO_IGNORE = ['701', '711', '712'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_ADVERTISE_ADDR): cv.string, @@ -136,9 +133,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Sync version of async add devices.""" hass.add_job(async_add_entities, devices, update_before_add) - hass.add_job(_setup_platform, hass, - hass.data[SONOS_DOMAIN].get('media_player', {}), - add_entities, None) + hass.async_add_executor_job( + _setup_platform, hass, hass.data[SONOS_DOMAIN].get('media_player', {}), + add_entities, None) def _setup_platform(hass, config, add_entities, discovery_info): @@ -289,10 +286,6 @@ def soco_error(errorcodes=None): """Wrap for all soco UPnP exception.""" from pysonos.exceptions import SoCoUPnPException, SoCoException - # Temporarily disable SoCo logging because it will log the - # UPnP exception otherwise - _SOCO_SERVICES_LOGGER.disabled = True - try: return funct(*args, **kwargs) except SoCoUPnPException as err: @@ -302,8 +295,6 @@ def soco_error(errorcodes=None): _LOGGER.error("Error on %s with %s", funct.__name__, err) except SoCoException as err: _LOGGER.error("Error on %s with %s", funct.__name__, err) - finally: - _SOCO_SERVICES_LOGGER.disabled = False return wrapper return decorator @@ -344,13 +335,13 @@ class SonosDevice(MediaPlayerDevice): def __init__(self, player): """Initialize the Sonos device.""" self._receives_events = False - self._volume_increment = 5 + self._volume_increment = 2 self._unique_id = player.uid self._player = player self._model = None self._player_volume = None self._player_muted = None - self._play_mode = None + self._shuffle = None self._name = None self._coordinator = None self._sonos_group = None @@ -372,11 +363,10 @@ class SonosDevice(MediaPlayerDevice): self._set_basic_information() - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe sonos events.""" self.hass.data[DATA_SONOS].devices.append(self) - self.hass.async_add_job(self._subscribe_to_player_events) + self.hass.async_add_executor_job(self._subscribe_to_player_events) @property def unique_id(self): @@ -447,7 +437,7 @@ class SonosDevice(MediaPlayerDevice): speaker_info = self.soco.get_speaker_info(True) self._name = speaker_info['zone_name'] self._model = speaker_info['model_name'] - self._play_mode = self.soco.play_mode + self._shuffle = self.soco.shuffle self.update_volume() @@ -540,7 +530,7 @@ class SonosDevice(MediaPlayerDevice): if new_status == 'TRANSITIONING': return - self._play_mode = self.soco.play_mode + self._shuffle = self.soco.shuffle if self.soco.is_playing_tv: self.update_media_linein(SOURCE_TV) @@ -765,7 +755,7 @@ class SonosDevice(MediaPlayerDevice): @soco_coordinator def shuffle(self): """Shuffling state.""" - return 'SHUFFLE' in self._play_mode + return self._shuffle @property def media_content_type(self): @@ -841,11 +831,11 @@ class SonosDevice(MediaPlayerDevice): """Set volume level, range 0..1.""" self.soco.volume = str(int(volume * 100)) - @soco_error() + @soco_error(UPNP_ERRORS_TO_IGNORE) @soco_coordinator def set_shuffle(self, shuffle): """Enable/Disable shuffle mode.""" - self.soco.play_mode = 'SHUFFLE_NOREPEAT' if shuffle else 'NORMAL' + self.soco.shuffle = shuffle @soco_error() def mute_volume(self, mute): diff --git a/homeassistant/components/media_player/soundtouch.py b/homeassistant/components/media_player/soundtouch.py index b8ade374a46..037a9b88fc6 100644 --- a/homeassistant/components/media_player/soundtouch.py +++ b/homeassistant/components/media_player/soundtouch.py @@ -297,7 +297,7 @@ class SoundTouchDevice(MediaPlayerDevice): def play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" _LOGGER.debug("Starting media with media_id: %s", media_id) - if re.match(r'https?://', str(media_id)): + if re.match(r'http?://', str(media_id)): # URL _LOGGER.debug("Playing URL %s", str(media_id)) self._device.play_url(str(media_id)) diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 2d6a849aecb..f8347830141 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -64,9 +64,8 @@ SERVICE_TO_METHOD = { } -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the squeezebox platform.""" import socket @@ -106,13 +105,12 @@ def async_setup_platform(hass, config, async_add_entities, _LOGGER.debug("Creating LMS object for %s", ipaddr) lms = LogitechMediaServer(hass, host, port, username, password) - players = yield from lms.create_players() + players = await lms.create_players() hass.data[DATA_SQUEEZEBOX].extend(players) async_add_entities(players) - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Map services to methods on MediaPlayerDevice.""" method = SERVICE_TO_METHOD.get(service.service) if not method: @@ -129,11 +127,11 @@ def async_setup_platform(hass, config, async_add_entities, update_tasks = [] for player in target_players: - yield from getattr(player, method['method'])(**params) + await getattr(player, method['method'])(**params) update_tasks.append(player.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) for service in SERVICE_TO_METHOD: schema = SERVICE_TO_METHOD[service]['schema'] @@ -155,22 +153,20 @@ class LogitechMediaServer: self._username = username self._password = password - @asyncio.coroutine - def create_players(self): + async def create_players(self): """Create a list of devices connected to LMS.""" result = [] - data = yield from self.async_query('players', 'status') + data = await self.async_query('players', 'status') if data is False: return result for players in data.get('players_loop', []): player = SqueezeBoxDevice( self, players['playerid'], players['name']) - yield from player.async_update() + await player.async_update() result.append(player) return result - @asyncio.coroutine - def async_query(self, *command, player=""): + async def async_query(self, *command, player=""): """Abstract out the JSON-RPC connection.""" auth = None if self._username is None else aiohttp.BasicAuth( self._username, self._password) @@ -187,7 +183,7 @@ class LogitechMediaServer: try: websession = async_get_clientsession(self.hass) with async_timeout.timeout(TIMEOUT, loop=self.hass.loop): - response = yield from websession.post( + response = await websession.post( url, data=data, auth=auth) @@ -198,7 +194,7 @@ class LogitechMediaServer: response.status, response) return False - data = yield from response.json() + data = await response.json() except (asyncio.TimeoutError, aiohttp.ClientError) as error: _LOGGER.error("Failed communicating with LMS: %s", type(error)) @@ -256,11 +252,10 @@ class SqueezeBoxDevice(MediaPlayerDevice): return self._lms.async_query( *parameters, player=self._id) - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Retrieve the current state of the player.""" tags = 'adKl' - response = yield from self.async_query( + response = await self.async_query( "status", "-", "1", "tags:{tags}" .format(tags=tags)) diff --git a/homeassistant/components/media_player/volumio.py b/homeassistant/components/media_player/volumio.py index 743f19cb259..de0f726c2ce 100644 --- a/homeassistant/components/media_player/volumio.py +++ b/homeassistant/components/media_player/volumio.py @@ -51,9 +51,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Volumio platform.""" if DATA_VOLUMIO not in hass.data: hass.data[DATA_VOLUMIO] = dict() @@ -96,8 +95,7 @@ class Volumio(MediaPlayerDevice): self._playlists = [] self._currentplaylist = None - @asyncio.coroutine - def send_volumio_msg(self, method, params=None): + async def send_volumio_msg(self, method, params=None): """Send message.""" url = "http://{}:{}/api/v1/{}/".format(self.host, self.port, method) @@ -105,9 +103,9 @@ class Volumio(MediaPlayerDevice): try: websession = async_get_clientsession(self.hass) - response = yield from websession.get(url, params=params) + response = await websession.get(url, params=params) if response.status == 200: - data = yield from response.json() + data = await response.json() else: _LOGGER.error( "Query failed, response code: %s Full message: %s", @@ -124,11 +122,10 @@ class Volumio(MediaPlayerDevice): _LOGGER.error("Received invalid response: %s", data) return False - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update state.""" - resp = yield from self.send_volumio_msg('getState') - yield from self._async_update_playlists() + resp = await self.send_volumio_msg('getState') + await self._async_update_playlists() if resp is False: return self._state = resp.copy() diff --git a/homeassistant/components/microsoft_face.py b/homeassistant/components/microsoft_face.py index e0e0e716d2e..9be2f8eadf5 100644 --- a/homeassistant/components/microsoft_face.py +++ b/homeassistant/components/microsoft_face.py @@ -69,45 +69,7 @@ SCHEMA_TRAIN_SERVICE = vol.Schema({ }) -def create_group(hass, name): - """Create a new person group.""" - data = {ATTR_NAME: name} - hass.services.call(DOMAIN, SERVICE_CREATE_GROUP, data) - - -def delete_group(hass, name): - """Delete a person group.""" - data = {ATTR_NAME: name} - hass.services.call(DOMAIN, SERVICE_DELETE_GROUP, data) - - -def train_group(hass, group): - """Train a person group.""" - data = {ATTR_GROUP: group} - hass.services.call(DOMAIN, SERVICE_TRAIN_GROUP, data) - - -def create_person(hass, group, name): - """Create a person in a group.""" - data = {ATTR_GROUP: group, ATTR_NAME: name} - hass.services.call(DOMAIN, SERVICE_CREATE_PERSON, data) - - -def delete_person(hass, group, name): - """Delete a person in a group.""" - data = {ATTR_GROUP: group, ATTR_NAME: name} - hass.services.call(DOMAIN, SERVICE_DELETE_PERSON, data) - - -def face_person(hass, group, person, camera_entity): - """Add a new face picture to a person.""" - data = {ATTR_GROUP: group, ATTR_PERSON: person, - ATTR_CAMERA_ENTITY: camera_entity} - hass.services.call(DOMAIN, SERVICE_FACE_PERSON, data) - - -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up Microsoft Face.""" entities = {} face = MicrosoftFace( @@ -120,26 +82,25 @@ def async_setup(hass, config): try: # read exists group/person from cloud and create entities - yield from face.update_store() + await face.update_store() except HomeAssistantError as err: _LOGGER.error("Can't load data from face api: %s", err) return False hass.data[DATA_MICROSOFT_FACE] = face - @asyncio.coroutine - def async_create_group(service): + async def async_create_group(service): """Create a new person group.""" name = service.data[ATTR_NAME] g_id = slugify(name) try: - yield from face.call_api( + await face.call_api( 'put', "persongroups/{0}".format(g_id), {'name': name}) face.store[g_id] = {} entities[g_id] = MicrosoftFaceGroupEntity(hass, face, g_id, name) - yield from entities[g_id].async_update_ha_state() + await entities[g_id].async_update_ha_state() except HomeAssistantError as err: _LOGGER.error("Can't create group '%s' with error: %s", g_id, err) @@ -147,13 +108,12 @@ def async_setup(hass, config): DOMAIN, SERVICE_CREATE_GROUP, async_create_group, schema=SCHEMA_GROUP_SERVICE) - @asyncio.coroutine - def async_delete_group(service): + async def async_delete_group(service): """Delete a person group.""" g_id = slugify(service.data[ATTR_NAME]) try: - yield from face.call_api('delete', "persongroups/{0}".format(g_id)) + await face.call_api('delete', "persongroups/{0}".format(g_id)) face.store.pop(g_id) entity = entities.pop(g_id) @@ -165,13 +125,12 @@ def async_setup(hass, config): DOMAIN, SERVICE_DELETE_GROUP, async_delete_group, schema=SCHEMA_GROUP_SERVICE) - @asyncio.coroutine - def async_train_group(service): + async def async_train_group(service): """Train a person group.""" g_id = service.data[ATTR_GROUP] try: - yield from face.call_api( + await face.call_api( 'post', "persongroups/{0}/train".format(g_id)) except HomeAssistantError as err: _LOGGER.error("Can't train group '%s' with error: %s", g_id, err) @@ -180,19 +139,18 @@ def async_setup(hass, config): DOMAIN, SERVICE_TRAIN_GROUP, async_train_group, schema=SCHEMA_TRAIN_SERVICE) - @asyncio.coroutine - def async_create_person(service): + async def async_create_person(service): """Create a person in a group.""" name = service.data[ATTR_NAME] g_id = service.data[ATTR_GROUP] try: - user_data = yield from face.call_api( + user_data = await face.call_api( 'post', "persongroups/{0}/persons".format(g_id), {'name': name} ) face.store[g_id][name] = user_data['personId'] - yield from entities[g_id].async_update_ha_state() + await entities[g_id].async_update_ha_state() except HomeAssistantError as err: _LOGGER.error("Can't create person '%s' with error: %s", name, err) @@ -200,19 +158,18 @@ def async_setup(hass, config): DOMAIN, SERVICE_CREATE_PERSON, async_create_person, schema=SCHEMA_PERSON_SERVICE) - @asyncio.coroutine - def async_delete_person(service): + async def async_delete_person(service): """Delete a person in a group.""" name = service.data[ATTR_NAME] g_id = service.data[ATTR_GROUP] p_id = face.store[g_id].get(name) try: - yield from face.call_api( + await face.call_api( 'delete', "persongroups/{0}/persons/{1}".format(g_id, p_id)) face.store[g_id].pop(name) - yield from entities[g_id].async_update_ha_state() + await entities[g_id].async_update_ha_state() except HomeAssistantError as err: _LOGGER.error("Can't delete person '%s' with error: %s", p_id, err) @@ -220,8 +177,7 @@ def async_setup(hass, config): DOMAIN, SERVICE_DELETE_PERSON, async_delete_person, schema=SCHEMA_PERSON_SERVICE) - @asyncio.coroutine - def async_face_person(service): + async def async_face_person(service): """Add a new face picture to a person.""" g_id = service.data[ATTR_GROUP] p_id = face.store[g_id].get(service.data[ATTR_PERSON]) @@ -230,9 +186,9 @@ def async_setup(hass, config): camera = hass.components.camera try: - image = yield from camera.async_get_image(hass, camera_entity) + image = await camera.async_get_image(hass, camera_entity) - yield from face.call_api( + await face.call_api( 'post', "persongroups/{0}/persons/{1}/persistedFaces".format( g_id, p_id), @@ -307,10 +263,9 @@ class MicrosoftFace: """Store group/person data and IDs.""" return self._store - @asyncio.coroutine - def update_store(self): + async def update_store(self): """Load all group/person data into local store.""" - groups = yield from self.call_api('get', 'persongroups') + groups = await self.call_api('get', 'persongroups') tasks = [] for group in groups: @@ -319,7 +274,7 @@ class MicrosoftFace: self._entities[g_id] = MicrosoftFaceGroupEntity( self.hass, self, g_id, group['name']) - persons = yield from self.call_api( + persons = await self.call_api( 'get', "persongroups/{0}/persons".format(g_id)) for person in persons: @@ -328,11 +283,10 @@ class MicrosoftFace: tasks.append(self._entities[g_id].async_update_ha_state()) if tasks: - yield from asyncio.wait(tasks, loop=self.hass.loop) + await asyncio.wait(tasks, loop=self.hass.loop) - @asyncio.coroutine - def call_api(self, method, function, data=None, binary=False, - params=None): + async def call_api(self, method, function, data=None, binary=False, + params=None): """Make an api call.""" headers = {"Ocp-Apim-Subscription-Key": self._api_key} url = self._server_url.format(function) @@ -350,10 +304,10 @@ class MicrosoftFace: try: with async_timeout.timeout(self.timeout, loop=self.hass.loop): - response = yield from getattr(self.websession, method)( + response = await getattr(self.websession, method)( url, data=payload, headers=headers, params=params) - answer = yield from response.json() + answer = await response.json() _LOGGER.debug("Read from microsoft face api: %s", answer) if response.status < 300: diff --git a/homeassistant/components/mqtt/.translations/ca.json b/homeassistant/components/mqtt/.translations/ca.json index b6c73f35f26..72a2636fb60 100644 --- a/homeassistant/components/mqtt/.translations/ca.json +++ b/homeassistant/components/mqtt/.translations/ca.json @@ -10,13 +10,20 @@ "broker": { "data": { "broker": "Broker", - "discovery": "Activar descobreix automaticament", + "discovery": "Habilita descobriment autom\u00e0tic", "password": "Contrasenya", "port": "Port", "username": "Nom d'usuari" }, "description": "Introdu\u00efu la informaci\u00f3 de connexi\u00f3 del vostre broker MQTT.", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Habilita descobriment autom\u00e0tic" + }, + "description": "Voleu configurar Home Assistant perqu\u00e8 es connecti amb el broker MQTT proporcionat pel complement de hass.io {addon}?", + "title": "Broker MQTT a trav\u00e9s del complement de Hass.io" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/de.json b/homeassistant/components/mqtt/.translations/de.json index 2a35e95f559..1c895136d9d 100644 --- a/homeassistant/components/mqtt/.translations/de.json +++ b/homeassistant/components/mqtt/.translations/de.json @@ -9,12 +9,21 @@ "step": { "broker": { "data": { + "broker": "Server", "discovery": "Suche aktivieren", "password": "Passwort", + "port": "Port", "username": "Benutzername" }, "description": "Bitte gib die Verbindungsinformationen deines MQTT-Brokers ein.", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Suche aktivieren" + }, + "description": "M\u00f6chten Sie den Home Assistant so konfigurieren, dass er eine Verbindung mit dem MQTT-Broker herstellt, der vom Add-on hass.io {addon} bereitgestellt wird?", + "title": "MQTT Broker per Hass.io add-on" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/en.json b/homeassistant/components/mqtt/.translations/en.json index c0b83a1323f..b0de6dcd782 100644 --- a/homeassistant/components/mqtt/.translations/en.json +++ b/homeassistant/components/mqtt/.translations/en.json @@ -17,6 +17,13 @@ }, "description": "Please enter the connection information of your MQTT broker.", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Enable discovery" + }, + "description": "Do you want to configure Home Assistant to connect to the MQTT broker provided by the hass.io add-on {addon}?", + "title": "MQTT Broker via Hass.io add-on" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/es-419.json b/homeassistant/components/mqtt/.translations/es-419.json new file mode 100644 index 00000000000..e9e869ae966 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/es-419.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "hassio_confirm": { + "title": "MQTT Broker a trav\u00e9s del complemento Hass.io" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/es.json b/homeassistant/components/mqtt/.translations/es.json new file mode 100644 index 00000000000..e9e869ae966 --- /dev/null +++ b/homeassistant/components/mqtt/.translations/es.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "hassio_confirm": { + "title": "MQTT Broker a trav\u00e9s del complemento Hass.io" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/fr.json b/homeassistant/components/mqtt/.translations/fr.json index 916b4fdaf39..648c2f972d7 100644 --- a/homeassistant/components/mqtt/.translations/fr.json +++ b/homeassistant/components/mqtt/.translations/fr.json @@ -10,13 +10,20 @@ "broker": { "data": { "broker": "Broker", - "discovery": "Activer la d\u00e9couverte automatique", + "discovery": "Activer la d\u00e9couverte", "password": "Mot de passe", "port": "Port", "username": "Nom d'utilisateur" }, "description": "Veuillez entrer les informations de connexion de votre broker MQTT.", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Activer la d\u00e9couverte" + }, + "description": "Vous voulez configurer Home Assistant pour vous connecter au broker MQTT fourni par l\u2019Add-on hass.io {addon} ?", + "title": "MQTT Broker via le module compl\u00e9mentaire Hass.io" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/hu.json b/homeassistant/components/mqtt/.translations/hu.json index d85814e917c..ba08d36d581 100644 --- a/homeassistant/components/mqtt/.translations/hu.json +++ b/homeassistant/components/mqtt/.translations/hu.json @@ -16,6 +16,12 @@ }, "description": "K\u00e9rlek, add meg az MQTT br\u00f3ker kapcsol\u00f3d\u00e1si adatait.", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Felfedez\u00e9s enged\u00e9lyez\u00e9se" + }, + "description": "Szeretn\u00e9d, hogy a Home Assistant csatlakozzon a hass.io addon {addon} \u00e1ltal biztos\u00edtott MQTT br\u00f3kerhez?" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/ko.json b/homeassistant/components/mqtt/.translations/ko.json index f20658d252c..ed552c0d994 100644 --- a/homeassistant/components/mqtt/.translations/ko.json +++ b/homeassistant/components/mqtt/.translations/ko.json @@ -15,8 +15,15 @@ "port": "\ud3ec\ud2b8", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, - "description": "MQTT \ube0c\ub85c\ucee4\uc640\uc758 \uc5f0\uacb0 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "description": "MQTT \ube0c\ub85c\ucee4\uc758 \uc5f0\uacb0 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "\uae30\uae30 \uac80\uc0c9 \ud65c\uc131\ud654" + }, + "description": "Hass.io \uc560\ub4dc\uc628 {addon} \uc5d0\uc11c \uc81c\uacf5\ud558\ub294 MQTT \ube0c\ub85c\ucee4\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant \ub97c \uad6c\uc131 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Hass.io \uc560\ub4dc\uc628\uc758 MQTT \ube0c\ub85c\ucee4" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/lb.json b/homeassistant/components/mqtt/.translations/lb.json index 166fce9fbfb..9dcd9c58a3a 100644 --- a/homeassistant/components/mqtt/.translations/lb.json +++ b/homeassistant/components/mqtt/.translations/lb.json @@ -17,6 +17,13 @@ }, "description": "Gitt Verbindungs Informatioune vun \u00e4rem MQTT Broker an.", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Entdeckung aktiv\u00e9ieren" + }, + "description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mam MQTT broker ze verbannen dee vum hass.io add-on {addon} bereet gestallt g\u00ebtt?", + "title": "MQTT Broker via Hass.io add-on" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/nl.json b/homeassistant/components/mqtt/.translations/nl.json index b375f353810..247755d8e89 100644 --- a/homeassistant/components/mqtt/.translations/nl.json +++ b/homeassistant/components/mqtt/.translations/nl.json @@ -1,15 +1,29 @@ { "config": { + "abort": { + "single_instance_allowed": "Slechts \u00e9\u00e9n configuratie van MQTT is toegestaan." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken met de broker." + }, "step": { "broker": { "data": { "broker": "Broker", + "discovery": "Detectie inschakelen", "password": "Wachtwoord", "port": "Poort", "username": "Gebruikersnaam" }, "description": "MQTT", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Detectie inschakelen" + }, + "description": "Wilt u Home Assistant configureren om verbinding te maken met de MQTT-broker die wordt aangeboden door de hass.io add-on {addon} ?", + "title": "MQTTT Broker via Hass.io add-on" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/no.json b/homeassistant/components/mqtt/.translations/no.json index 412efd3e107..b3f1e4740b9 100644 --- a/homeassistant/components/mqtt/.translations/no.json +++ b/homeassistant/components/mqtt/.translations/no.json @@ -17,6 +17,13 @@ }, "description": "Vennligst oppgi tilkoblingsinformasjonen for din MQTT megler.", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Aktiver oppdagelse" + }, + "description": "Vil du konfigurere Home Assistant til \u00e5 koble til MQTT megler gitt av hass.io tillegget {addon}?", + "title": "MQTT megler via Hass.io tillegg" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/pl.json b/homeassistant/components/mqtt/.translations/pl.json index e87e550b98d..33c33c5c095 100644 --- a/homeassistant/components/mqtt/.translations/pl.json +++ b/homeassistant/components/mqtt/.translations/pl.json @@ -17,6 +17,13 @@ }, "description": "Wprowad\u017a informacje o po\u0142\u0105czeniu po\u015brednika MQTT.", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "W\u0142\u0105cz wykrywanie" + }, + "description": "Czy chcesz skonfigurowa\u0107 Home Assistant'a, aby po\u0142\u0105czy\u0142 si\u0119 z po\u015brednikiem MQTT dostarczonym przez dodatek Hass.io {addon}?", + "title": "Po\u015brednik MQTT za po\u015brednictwem dodatku Hass.io" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/pt.json b/homeassistant/components/mqtt/.translations/pt.json index 42f7c7f5ad2..1b8c3946b7c 100644 --- a/homeassistant/components/mqtt/.translations/pt.json +++ b/homeassistant/components/mqtt/.translations/pt.json @@ -10,6 +10,7 @@ "broker": { "data": { "broker": "", + "discovery": "Ativar descoberta", "password": "Palavra-passe", "port": "Porto", "username": "Utilizador" diff --git a/homeassistant/components/mqtt/.translations/ru.json b/homeassistant/components/mqtt/.translations/ru.json index f1ff498dd72..8b0ed27f3ae 100644 --- a/homeassistant/components/mqtt/.translations/ru.json +++ b/homeassistant/components/mqtt/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u0414\u043e\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f MQTT." + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443." @@ -17,6 +17,13 @@ }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a \u0432\u0430\u0448\u0435\u043c\u0443 \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT.", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435" + }, + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Home Assistant \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT \u0447\u0435\u0440\u0435\u0437 \u0430\u0434\u0434\u043e\u043d Hass.io {addon}?", + "title": "\u0411\u0440\u043e\u043a\u0435\u0440 MQTT \u0447\u0435\u0440\u0435\u0437 \u0430\u0434\u0434\u043e\u043d Hass.io" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/sl.json b/homeassistant/components/mqtt/.translations/sl.json index a12498ac4c2..d8d331449a2 100644 --- a/homeassistant/components/mqtt/.translations/sl.json +++ b/homeassistant/components/mqtt/.translations/sl.json @@ -17,6 +17,13 @@ }, "description": "Prosimo vnesite informacije o povezavi va\u0161ega MQTT posrednika.", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Omogo\u010di odkrivanje" + }, + "description": "\u017delite konfigurirati Home Assistent-a za povezavo s posrednikom MQTT, ki ga ponuja hass.io add-on {addon} ?", + "title": "MQTT Broker prek dodatka Hass.io" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/sv.json b/homeassistant/components/mqtt/.translations/sv.json index 7cf6d75b9c1..70e3720038d 100644 --- a/homeassistant/components/mqtt/.translations/sv.json +++ b/homeassistant/components/mqtt/.translations/sv.json @@ -1,13 +1,31 @@ { "config": { + "abort": { + "single_instance_allowed": "Endast en enda konfiguration av MQTT \u00e4r till\u00e5ten." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta till broker." + }, "step": { "broker": { "data": { + "broker": "Broker", + "discovery": "Aktivera uppt\u00e4ckt", "password": "L\u00f6senord", "port": "Port", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "V\u00e4nligen ange anslutningsinformationen f\u00f6r din MQTT broker.", + "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "Aktivera uppt\u00e4ckt" + }, + "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till MQTT Broker som tillhandah\u00e5lls av hass.io-till\u00e4gget {addon} ?", + "title": "MQTT Broker via Hass.io till\u00e4gg" } - } + }, + "title": "MQTT" } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/zh-Hans.json b/homeassistant/components/mqtt/.translations/zh-Hans.json index 98a7d9eb4be..f30e1bf10b4 100644 --- a/homeassistant/components/mqtt/.translations/zh-Hans.json +++ b/homeassistant/components/mqtt/.translations/zh-Hans.json @@ -17,6 +17,13 @@ }, "description": "\u8bf7\u8f93\u5165\u60a8\u7684 MQTT \u670d\u52a1\u5668\u7684\u8fde\u63a5\u4fe1\u606f\u3002", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "\u542f\u7528\u53d1\u73b0" + }, + "description": "\u662f\u5426\u8981\u914d\u7f6e Home Assistant \u8fde\u63a5\u5230 Hass.io \u52a0\u8f7d\u9879 {addon} \u63d0\u4f9b\u7684 MQTT \u670d\u52a1\u5668\uff1f", + "title": "\u6765\u81ea Hass.io \u52a0\u8f7d\u9879\u7684 MQTT \u670d\u52a1\u5668" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/zh-Hant.json b/homeassistant/components/mqtt/.translations/zh-Hant.json index cf87ceb8f98..535ed848793 100644 --- a/homeassistant/components/mqtt/.translations/zh-Hant.json +++ b/homeassistant/components/mqtt/.translations/zh-Hant.json @@ -10,13 +10,20 @@ "broker": { "data": { "broker": "Broker", - "discovery": "\u958b\u555f\u63a2\u7d22", + "discovery": "\u958b\u555f\u641c\u5c0b", "password": "\u4f7f\u7528\u8005\u5bc6\u78bc", "port": "\u901a\u8a0a\u57e0", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, "description": "\u8acb\u8f38\u5165 MQTT Broker \u9023\u7dda\u8cc7\u8a0a\u3002", "title": "MQTT" + }, + "hassio_confirm": { + "data": { + "discovery": "\u958b\u555f\u641c\u5c0b" + }, + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 Hass.io \u9644\u52a0\u7d44\u4ef6 {addon} \u4e4b MQTT broker\uff1f", + "title": "\u4f7f\u7528 Hass.io \u9644\u52a0\u7d44\u4ef6 MQTT Broker" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 874f21e5168..3e25563e9ba 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -254,7 +254,8 @@ def async_publish(hass: HomeAssistantType, topic: Any, payload, qos=None, """Publish message to an MQTT topic.""" data = _build_publish_data(topic, qos, retain) data[ATTR_PAYLOAD] = payload - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_PUBLISH, data)) + hass.async_create_task( + hass.services.async_call(DOMAIN, SERVICE_PUBLISH, data)) @bind_hass @@ -321,7 +322,8 @@ async def _async_setup_server(hass: HomeAssistantType, config: ConfigType): async def _async_setup_discovery(hass: HomeAssistantType, conf: ConfigType, - hass_config: ConfigType) -> bool: + hass_config: ConfigType, + config_entry) -> bool: """Try to start the discovery of MQTT devices. This method is a coroutine. @@ -334,7 +336,8 @@ async def _async_setup_discovery(hass: HomeAssistantType, conf: ConfigType, return False success = await discovery.async_start( - hass, conf[CONF_DISCOVERY_PREFIX], hass_config) # type: bool + hass, conf[CONF_DISCOVERY_PREFIX], hass_config, + config_entry) # type: bool return success @@ -409,7 +412,7 @@ async def async_setup_entry(hass, entry): # If user didn't have configuration.yaml config, generate defaults if conf is None: conf = CONFIG_SCHEMA({ - DOMAIN: entry.data + DOMAIN: entry.data, })[DOMAIN] elif any(key in conf for key in entry.data): _LOGGER.warning( @@ -522,7 +525,7 @@ async def async_setup_entry(hass, entry): if conf.get(CONF_DISCOVERY): await _async_setup_discovery( - hass, conf, hass.data[DATA_MQTT_HASS_CONFIG]) + hass, conf, hass.data[DATA_MQTT_HASS_CONFIG], entry) return True @@ -668,7 +671,7 @@ class MQTT: if any(other.topic == topic for other in self.subscriptions): # Other subscriptions on topic remaining - don't unsubscribe. return - self.hass.async_add_job(self._async_unsubscribe(topic)) + self.hass.async_create_task(self._async_unsubscribe(topic)) return async_remove diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 22072857b03..e0d1e692c60 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -5,7 +5,8 @@ import queue import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.const import ( + CONF_PASSWORD, CONF_PORT, CONF_USERNAME, CONF_PROTOCOL) from .const import CONF_BROKER, CONF_DISCOVERY, DEFAULT_DISCOVERY @@ -17,6 +18,8 @@ class FlowHandler(config_entries.ConfigFlow): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + _hassio_discovery = None + async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" if self._async_current_entries(): @@ -60,11 +63,65 @@ class FlowHandler(config_entries.ConfigFlow): return self.async_create_entry(title='configuration.yaml', data={}) + async def async_step_hassio(self, user_input=None): + """Receive a Hass.io discovery.""" + if self._async_current_entries(): + return self.async_abort(reason='single_instance_allowed') -def try_connection(broker, port, username, password): + self._hassio_discovery = user_input + + return await self.async_step_hassio_confirm() + + async def async_step_hassio_confirm(self, user_input=None): + """Confirm a Hass.io discovery.""" + errors = {} + + if user_input is not None: + data = self._hassio_discovery + can_connect = await self.hass.async_add_executor_job( + try_connection, + data[CONF_BROKER], + data[CONF_PORT], + data.get(CONF_USERNAME), + data.get(CONF_PASSWORD), + data.get(CONF_PROTOCOL) + ) + + if can_connect: + return self.async_create_entry( + title=data['addon'], data={ + CONF_BROKER: data[CONF_BROKER], + CONF_PORT: data[CONF_PORT], + CONF_USERNAME: data.get(CONF_USERNAME), + CONF_PASSWORD: data.get(CONF_PASSWORD), + CONF_PROTOCOL: data.get(CONF_PROTOCOL), + CONF_DISCOVERY: user_input[CONF_DISCOVERY], + }) + + errors['base'] = 'cannot_connect' + + return self.async_show_form( + step_id='hassio_confirm', + description_placeholders={ + 'addon': self._hassio_discovery['addon'] + }, + data_schema=vol.Schema({ + vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): bool + }), + errors=errors, + ) + + +def try_connection(broker, port, username, password, protocol='3.1'): """Test if we can connect to an MQTT broker.""" import paho.mqtt.client as mqtt - client = mqtt.Client() + + if protocol == '3.1': + proto = mqtt.MQTTv31 + else: + proto = mqtt.MQTTv311 + + client = mqtt.Client(protocol=proto) if username and password: client.username_pw_set(username, password) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index f42c1ed58e9..a762978a330 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -4,6 +4,7 @@ Support for MQTT discovery. For more details about this component, please refer to the documentation at https://home-assistant.io/components/mqtt/#discovery """ +import asyncio import json import logging import re @@ -13,6 +14,7 @@ from homeassistant.components.mqtt import CONF_STATE_TOPIC, ATTR_DISCOVERY_HASH from homeassistant.const import CONF_PLATFORM from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) @@ -38,11 +40,26 @@ ALLOWED_PLATFORMS = { 'alarm_control_panel': ['mqtt'], } +CONFIG_ENTRY_PLATFORMS = { + 'binary_sensor': ['mqtt'], + 'camera': ['mqtt'], + 'cover': ['mqtt'], + 'light': ['mqtt'], + 'sensor': ['mqtt'], + 'switch': ['mqtt'], + 'climate': ['mqtt'], + 'alarm_control_panel': ['mqtt'], +} + ALREADY_DISCOVERED = 'mqtt_discovered_components' +DATA_CONFIG_ENTRY_LOCK = 'mqtt_config_entry_lock' +CONFIG_ENTRY_IS_SETUP = 'mqtt_config_entry_is_setup' MQTT_DISCOVERY_UPDATED = 'mqtt_discovery_updated_{}' +MQTT_DISCOVERY_NEW = 'mqtt_discovery_new_{}_{}' -async def async_start(hass, discovery_topic, hass_config): +async def async_start(hass: HomeAssistantType, discovery_topic, hass_config, + config_entry=None) -> bool: """Initialize of MQTT Discovery.""" async def async_device_message_received(topic, payload, qos): """Process the received message.""" @@ -98,8 +115,23 @@ async def async_start(hass, discovery_topic, hass_config): _LOGGER.info("Found new component: %s %s", component, discovery_id) - await async_load_platform( - hass, component, platform, payload, hass_config) + if platform not in CONFIG_ENTRY_PLATFORMS.get(component, []): + await async_load_platform( + hass, component, platform, payload, hass_config) + return + + config_entries_key = '{}.{}'.format(component, platform) + async with hass.data[DATA_CONFIG_ENTRY_LOCK]: + if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]: + await hass.config_entries.async_forward_entry_setup( + config_entry, component) + hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key) + + async_dispatcher_send(hass, MQTT_DISCOVERY_NEW.format( + component, platform), payload) + + hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() + hass.data[CONFIG_ENTRY_IS_SETUP] = set() await mqtt.async_subscribe( hass, discovery_topic + '/#', async_device_message_received, 0) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 0a2cb255cc4..40a68195f26 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -12,6 +12,13 @@ "password": "Password", "discovery": "Enable discovery" } + }, + "hassio_confirm": { + "title": "MQTT Broker via Hass.io add-on", + "description": "Do you want to configure Home Assistant to connect to the MQTT broker provided by the hass.io add-on {addon}?", + "data": { + "discovery": "Enable discovery" + } } }, "abort": { diff --git a/homeassistant/components/mqtt_statestream.py b/homeassistant/components/mqtt_statestream.py index 592e31cbff1..3a0e5d39ff0 100644 --- a/homeassistant/components/mqtt_statestream.py +++ b/homeassistant/components/mqtt_statestream.py @@ -4,7 +4,6 @@ Publish simple item state changes via MQTT. For more details about this component, please refer to the documentation at https://home-assistant.io/components/mqtt_statestream/ """ -import asyncio import json import voluptuous as vol @@ -43,8 +42,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the MQTT state feed.""" conf = config.get(DOMAIN, {}) base_topic = conf.get(CONF_BASE_TOPIC) diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index 725494cd197..4f00247495a 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -111,7 +111,7 @@ async def async_setup(hass, config): hass.data[MYSENSORS_GATEWAYS] = gateways - hass.async_add_job(finish_setup(hass, gateways)) + hass.async_create_task(finish_setup(hass, gateways)) return True diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 88725e67940..558e944f727 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -106,7 +106,7 @@ async def _get_gateway(hass, config, gateway_conf, persistence_file): """Call callback.""" sub_cb(*args) - hass.async_add_job( + hass.async_create_task( mqtt.async_subscribe(topic, internal_callback, qos)) gateway = mysensors.AsyncMQTTGateway( @@ -192,7 +192,7 @@ async def _gw_start(hass, gateway): @callback def gw_stop(event): """Trigger to stop the gateway.""" - hass.async_add_job(gateway.stop()) + hass.async_create_task(gateway.stop()) if not connect_task.done(): connect_task.cancel() diff --git a/homeassistant/components/namecheapdns.py b/homeassistant/components/namecheapdns.py index dcca8829535..32a5c318852 100644 --- a/homeassistant/components/namecheapdns.py +++ b/homeassistant/components/namecheapdns.py @@ -4,7 +4,6 @@ Integrate with namecheap DNS services. For more details about this component, please refer to the documentation at https://home-assistant.io/components/namecheapdns/ """ -import asyncio import logging from datetime import timedelta @@ -32,8 +31,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Initialize the namecheap DNS component.""" host = config[DOMAIN][CONF_HOST] domain = config[DOMAIN][CONF_DOMAIN] @@ -41,23 +39,21 @@ def async_setup(hass, config): session = async_get_clientsession(hass) - result = yield from _update_namecheapdns(session, host, domain, password) + result = await _update_namecheapdns(session, host, domain, password) if not result: return False - @asyncio.coroutine - def update_domain_interval(now): + async def update_domain_interval(now): """Update the namecheap DNS entry.""" - yield from _update_namecheapdns(session, host, domain, password) + await _update_namecheapdns(session, host, domain, password) async_track_time_interval(hass, update_domain_interval, INTERVAL) return result -@asyncio.coroutine -def _update_namecheapdns(session, host, domain, password): +async def _update_namecheapdns(session, host, domain, password): """Update namecheap DNS entry.""" import xml.etree.ElementTree as ET @@ -67,8 +63,8 @@ def _update_namecheapdns(session, host, domain, password): 'password': password, } - resp = yield from session.get(UPDATE_URL, params=params) - xml_string = yield from resp.text() + resp = await session.get(UPDATE_URL, params=params) + xml_string = await resp.text() root = ET.fromstring(xml_string) err_count = root.find('ErrCount').text diff --git a/homeassistant/components/nest/.translations/hu.json b/homeassistant/components/nest/.translations/hu.json index abf8f79599f..142747a016f 100644 --- a/homeassistant/components/nest/.translations/hu.json +++ b/homeassistant/components/nest/.translations/hu.json @@ -1,13 +1,19 @@ { "config": { + "abort": { + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n." + }, "error": { - "invalid_code": "\u00c9rv\u00e9nytelen k\u00f3d" + "invalid_code": "\u00c9rv\u00e9nytelen k\u00f3d", + "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a k\u00f3d \u00e9rv\u00e9nyes\u00edt\u00e9se sor\u00e1n.", + "unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt a k\u00f3d \u00e9rv\u00e9nyes\u00edt\u00e9se sor\u00e1n" }, "step": { "init": { "data": { "flow_impl": "Szolg\u00e1ltat\u00f3" - } + }, + "title": "Hiteles\u00edt\u00e9si Szolg\u00e1ltat\u00f3" }, "link": { "data": { diff --git a/homeassistant/components/nest/.translations/ru.json b/homeassistant/components/nest/.translations/ru.json index 0f7b9b8dd71..6a73bd47203 100644 --- a/homeassistant/components/nest/.translations/ru.json +++ b/homeassistant/components/nest/.translations/ru.json @@ -1,7 +1,7 @@ { "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.", + "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "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/)." diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index f609c774b12..c66abf1a8bd 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -105,7 +105,7 @@ async def async_setup(hass, config): 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( + hass.async_create_task(hass.config_entries.flow.async_init( DOMAIN, context={'source': config_entries.SOURCE_IMPORT}, data={ 'nest_conf_path': access_token_cache_file, diff --git a/homeassistant/components/netatmo.py b/homeassistant/components/netatmo.py index 59b0a64f6e9..d8924c6c301 100644 --- a/homeassistant/components/netatmo.py +++ b/homeassistant/components/netatmo.py @@ -16,7 +16,7 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -REQUIREMENTS = ['pyatmo==1.1.1'] +REQUIREMENTS = ['pyatmo==1.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/no_ip.py b/homeassistant/components/no_ip.py index 6051fa85f55..beb11ed738f 100644 --- a/homeassistant/components/no_ip.py +++ b/homeassistant/components/no_ip.py @@ -53,8 +53,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Initialize the NO-IP component.""" domain = config[DOMAIN].get(CONF_DOMAIN) user = config[DOMAIN].get(CONF_USERNAME) @@ -65,16 +64,15 @@ def async_setup(hass, config): session = hass.helpers.aiohttp_client.async_get_clientsession() - result = yield from _update_no_ip( + result = await _update_no_ip( hass, session, domain, auth_str, timeout) if not result: return False - @asyncio.coroutine - def update_domain_interval(now): + async def update_domain_interval(now): """Update the NO-IP entry.""" - yield from _update_no_ip(hass, session, domain, auth_str, timeout) + await _update_no_ip(hass, session, domain, auth_str, timeout) hass.helpers.event.async_track_time_interval( update_domain_interval, INTERVAL) @@ -82,8 +80,7 @@ def async_setup(hass, config): return True -@asyncio.coroutine -def _update_no_ip(hass, session, domain, auth_str, timeout): +async def _update_no_ip(hass, session, domain, auth_str, timeout): """Update NO-IP.""" url = UPDATE_URL @@ -98,8 +95,8 @@ def _update_no_ip(hass, session, domain, auth_str, timeout): try: with async_timeout.timeout(timeout, loop=hass.loop): - resp = yield from session.get(url, params=params, headers=headers) - body = yield from resp.text() + resp = await session.get(url, params=params, headers=headers) + body = await resp.text() if body.startswith('good') or body.startswith('nochg'): return True diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 4de35d3f850..f0320617e19 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -12,7 +12,6 @@ import voluptuous as vol from homeassistant.setup import async_prepare_setup_platform from homeassistant.exceptions import HomeAssistantError -from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME, CONF_PLATFORM from homeassistant.helpers import config_per_platform, discovery @@ -50,34 +49,16 @@ NOTIFY_SERVICE_SCHEMA = vol.Schema({ }) -@bind_hass -def send_message(hass, message, title=None, data=None): - """Send a notification message.""" - info = { - ATTR_MESSAGE: message - } - - if title is not None: - info[ATTR_TITLE] = title - - if data is not None: - info[ATTR_DATA] = data - - hass.services.call(DOMAIN, SERVICE_NOTIFY, info) - - -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the notify services.""" targets = {} - @asyncio.coroutine - def async_setup_platform(p_type, p_config=None, discovery_info=None): + async def async_setup_platform(p_type, p_config=None, discovery_info=None): """Set up a notify platform.""" if p_config is None: p_config = {} - platform = yield from async_prepare_setup_platform( + platform = await async_prepare_setup_platform( hass, config, DOMAIN, p_type) if platform is None: @@ -88,10 +69,10 @@ def async_setup(hass, config): notify_service = None try: if hasattr(platform, 'async_get_service'): - notify_service = yield from \ + notify_service = await \ platform.async_get_service(hass, p_config, discovery_info) elif hasattr(platform, 'get_service'): - notify_service = yield from hass.async_add_job( + notify_service = await hass.async_add_job( platform.get_service, hass, p_config, discovery_info) else: raise HomeAssistantError("Invalid notify platform.") @@ -114,8 +95,7 @@ def async_setup(hass, config): if discovery_info is None: discovery_info = {} - @asyncio.coroutine - def async_notify_message(service): + async def async_notify_message(service): """Handle sending notification message service calls.""" kwargs = {} message = service.data[ATTR_MESSAGE] @@ -134,7 +114,7 @@ def async_setup(hass, config): kwargs[ATTR_MESSAGE] = message.async_render() kwargs[ATTR_DATA] = service.data.get(ATTR_DATA) - yield from notify_service.async_send_message(**kwargs) + await notify_service.async_send_message(**kwargs) if hasattr(notify_service, 'targets'): platform_name = ( @@ -164,12 +144,11 @@ def async_setup(hass, config): in config_per_platform(config, DOMAIN)] if setup_tasks: - yield from asyncio.wait(setup_tasks, loop=hass.loop) + await asyncio.wait(setup_tasks, loop=hass.loop) - @asyncio.coroutine - def async_platform_discovered(platform, info): + async def async_platform_discovered(platform, info): """Handle for discovered platform.""" - yield from async_setup_platform(platform, discovery_info=info) + await async_setup_platform(platform, discovery_info=info) discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered) diff --git a/homeassistant/components/notify/discord.py b/homeassistant/components/notify/discord.py index 0cf4bced360..8bd4e27155d 100644 --- a/homeassistant/components/notify/discord.py +++ b/homeassistant/components/notify/discord.py @@ -4,7 +4,6 @@ Discord platform for notify component. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.discord/ """ -import asyncio import logging import voluptuous as vol @@ -39,8 +38,7 @@ class DiscordNotificationService(BaseNotificationService): self.token = token self.hass = hass - @asyncio.coroutine - def async_send_message(self, message, **kwargs): + async def async_send_message(self, message, **kwargs): """Login to Discord, send message to channel(s) and log out.""" import discord discord_bot = discord.Client(loop=self.hass.loop) @@ -51,8 +49,7 @@ class DiscordNotificationService(BaseNotificationService): # pylint: disable=unused-variable @discord_bot.event - @asyncio.coroutine - def on_ready(): + async def on_ready(): """Send the messages when the bot is ready.""" try: data = kwargs.get(ATTR_DATA) @@ -60,14 +57,14 @@ class DiscordNotificationService(BaseNotificationService): images = data.get(ATTR_IMAGES) for channelid in kwargs[ATTR_TARGET]: channel = discord.Object(id=channelid) - yield from discord_bot.send_message(channel, message) + await discord_bot.send_message(channel, message) if images: for anum, f_name in enumerate(images): - yield from discord_bot.send_file(channel, f_name) + await discord_bot.send_file(channel, f_name) except (discord.errors.HTTPException, discord.errors.NotFound) as error: _LOGGER.warning("Communication error: %s", error) - yield from discord_bot.logout() - yield from discord_bot.close() + await discord_bot.logout() + await discord_bot.close() - yield from discord_bot.start(self.token) + await discord_bot.start(self.token) diff --git a/homeassistant/components/notify/group.py b/homeassistant/components/notify/group.py index 94856c730b1..5d25c2d815e 100644 --- a/homeassistant/components/notify/group.py +++ b/homeassistant/components/notify/group.py @@ -41,8 +41,7 @@ def update(input_dict, update_source): return input_dict -@asyncio.coroutine -def async_get_service(hass, config, discovery_info=None): +async def async_get_service(hass, config, discovery_info=None): """Get the Group notification service.""" return GroupNotifyPlatform(hass, config.get(CONF_SERVICES)) @@ -55,8 +54,7 @@ class GroupNotifyPlatform(BaseNotificationService): self.hass = hass self.entities = entities - @asyncio.coroutine - def async_send_message(self, message="", **kwargs): + async def async_send_message(self, message="", **kwargs): """Send message to all entities in the group.""" payload = {ATTR_MESSAGE: message} payload.update({key: val for key, val in kwargs.items() if val}) @@ -70,4 +68,4 @@ class GroupNotifyPlatform(BaseNotificationService): DOMAIN, entity.get(ATTR_SERVICE), sending_payload)) if tasks: - yield from asyncio.wait(tasks, loop=self.hass.loop) + await asyncio.wait(tasks, loop=self.hass.loop) diff --git a/homeassistant/components/notify/kodi.py b/homeassistant/components/notify/kodi.py index 3eb492f7fa6..74bfe61d3f2 100644 --- a/homeassistant/components/notify/kodi.py +++ b/homeassistant/components/notify/kodi.py @@ -4,7 +4,6 @@ Kodi notification service. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.kodi/ """ -import asyncio import logging import aiohttp @@ -38,8 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ ATTR_DISPLAYTIME = 'displaytime' -@asyncio.coroutine -def async_get_service(hass, config, discovery_info=None): +async def async_get_service(hass, config, discovery_info=None): """Return the notify service.""" url = '{}:{}'.format(config.get(CONF_HOST), config.get(CONF_PORT)) @@ -86,8 +84,7 @@ class KodiNotificationService(BaseNotificationService): self._server = jsonrpc_async.Server(self._url, **kwargs) - @asyncio.coroutine - def async_send_message(self, message="", **kwargs): + async def async_send_message(self, message="", **kwargs): """Send a message to Kodi.""" import jsonrpc_async try: @@ -96,7 +93,7 @@ class KodiNotificationService(BaseNotificationService): displaytime = data.get(ATTR_DISPLAYTIME, 10000) icon = data.get(ATTR_ICON, "info") title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) - yield from self._server.GUI.ShowNotification( + await self._server.GUI.ShowNotification( title, message, icon, displaytime) except jsonrpc_async.TransportError: diff --git a/homeassistant/components/notify/prowl.py b/homeassistant/components/notify/prowl.py index 3928fa81167..f0741766a70 100644 --- a/homeassistant/components/notify/prowl.py +++ b/homeassistant/components/notify/prowl.py @@ -25,8 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_get_service(hass, config, discovery_info=None): +async def async_get_service(hass, config, discovery_info=None): """Get the Prowl notification service.""" return ProwlNotificationService(hass, config[CONF_API_KEY]) @@ -39,8 +38,7 @@ class ProwlNotificationService(BaseNotificationService): self._hass = hass self._api_key = api_key - @asyncio.coroutine - def async_send_message(self, message, **kwargs): + async def async_send_message(self, message, **kwargs): """Send the message to the user.""" response = None session = None @@ -59,8 +57,8 @@ class ProwlNotificationService(BaseNotificationService): try: with async_timeout.timeout(10, loop=self._hass.loop): - response = yield from session.post(url, data=payload) - result = yield from response.text() + response = await session.post(url, data=payload) + result = await response.text() if response.status != 200 or 'error' in result: _LOGGER.error("Prowl service returned http " diff --git a/homeassistant/components/notify/telegram.py b/homeassistant/components/notify/telegram.py index b012506acd9..1dff82fa2cd 100644 --- a/homeassistant/components/notify/telegram.py +++ b/homeassistant/components/notify/telegram.py @@ -47,7 +47,7 @@ class TelegramNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" - service_data = dict(target=kwargs.get(ATTR_TARGET, self._chat_id)) + service_data = {ATTR_TARGET: kwargs.get(ATTR_TARGET, self._chat_id)} if ATTR_TITLE in kwargs: service_data.update({ATTR_TITLE: kwargs.get(ATTR_TITLE)}) if message: diff --git a/homeassistant/components/notify/tibber.py b/homeassistant/components/notify/tibber.py new file mode 100644 index 00000000000..ddbcb3f6c65 --- /dev/null +++ b/homeassistant/components/notify/tibber.py @@ -0,0 +1,37 @@ +""" +Tibber platform for notify component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.tibber/ +""" +import asyncio +import logging + +from homeassistant.components.notify import ( + ATTR_TITLE, ATTR_TITLE_DEFAULT, BaseNotificationService) +from homeassistant.components.tibber import DOMAIN as TIBBER_DOMAIN + + +_LOGGER = logging.getLogger(__name__) + + +async def async_get_service(hass, config, discovery_info=None): + """Get the Tibber notification service.""" + tibber_connection = hass.data[TIBBER_DOMAIN] + return TibberNotificationService(tibber_connection.send_notification) + + +class TibberNotificationService(BaseNotificationService): + """Implement the notification service for Tibber.""" + + def __init__(self, notify): + """Initialize the service.""" + self._notify = notify + + async def async_send_message(self, message=None, **kwargs): + """Send a message to Tibber devices.""" + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) + try: + await self._notify(title=title, message=message) + except asyncio.TimeoutError: + _LOGGER.error("Timeout sending message with Tibber") diff --git a/homeassistant/components/notify/yessssms.py b/homeassistant/components/notify/yessssms.py index 37a6a90a62e..e16e384ca25 100644 --- a/homeassistant/components/notify/yessssms.py +++ b/homeassistant/components/notify/yessssms.py @@ -13,7 +13,8 @@ from homeassistant.components.notify import ( from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_RECIPIENT import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['YesssSMS==0.1.1b3'] + +REQUIREMENTS = ['YesssSMS==0.2.3'] _LOGGER = logging.getLogger(__name__) @@ -38,14 +39,38 @@ class YesssSMSNotificationService(BaseNotificationService): from YesssSMS import YesssSMS self.yesss = YesssSMS(username, password) self._recipient = recipient + _LOGGER.debug( + "initialized; library version: %s", self.yesss.version()) def send_message(self, message="", **kwargs): """Send a SMS message via Yesss.at's website.""" + if self.yesss.account_is_suspended(): + # only retry to login after HASS was restarted with (hopefully) + # new login data. + _LOGGER.error( + "Account is suspended, cannot send SMS. " + "Check your login data and restart Home Assistant") + return try: self.yesss.send(self._recipient, message) - except ValueError as ex: - if str(ex).startswith("YesssSMS:"): - _LOGGER.error(str(ex)) - except RuntimeError as ex: - if str(ex).startswith("YesssSMS:"): - _LOGGER.error(str(ex)) + except self.yesss.NoRecipientError as ex: + _LOGGER.error( + "You need to provide a recipient for SMS notification: %s", + ex) + except self.yesss.EmptyMessageError as ex: + _LOGGER.error( + "Cannot send empty SMS message: %s", ex) + except self.yesss.SMSSendingError as ex: + _LOGGER.error(str(ex), exc_info=ex) + except ConnectionError as ex: + _LOGGER.error( + "YesssSMS: unable to connect to yesss.at server.", + exc_info=ex) + except self.yesss.AccountSuspendedError as ex: + _LOGGER.error( + "Wrong login credentials!! Verify correct credentials and " + "restart Home Assistant: %s", ex) + except self.yesss.LoginError as ex: + _LOGGER.error("Wrong login credentials: %s", ex) + else: + _LOGGER.info("SMS sent") diff --git a/homeassistant/components/onboarding/__init__.py b/homeassistant/components/onboarding/__init__.py index 52d18b9a870..376575e3440 100644 --- a/homeassistant/components/onboarding/__init__.py +++ b/homeassistant/components/onboarding/__init__.py @@ -23,7 +23,8 @@ def async_is_onboarded(hass): async def async_setup(hass, config): """Set up the onboarding component.""" - store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY, + private=True) data = await store.async_load() if data is None: diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 35ab16b4d1f..8485e1e3201 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -212,8 +212,15 @@ class OpenUV: async def async_update(self): """Update sensor/binary sensor data.""" if TYPE_PROTECTION_WINDOW in self.binary_sensor_conditions: - data = await self.client.uv_protection_window() - self.data[DATA_PROTECTION_WINDOW] = data + resp = await self.client.uv_protection_window() + data = resp['result'] + + if data.get('from_time') and data.get('to_time'): + self.data[DATA_PROTECTION_WINDOW] = data + else: + _LOGGER.error( + 'No valid protection window data for this location') + self.data[DATA_PROTECTION_WINDOW] = {} if any(c in self.sensor_conditions for c in SENSORS): data = await self.client.uv_index() diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 6b8fd68bc26..d38501b9b07 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -4,7 +4,6 @@ A component which is collecting configuration errors. For more details about this component, please refer to the documentation at https://home-assistant.io/components/persistent_notification/ """ -import asyncio import logging from collections import OrderedDict from typing import Awaitable @@ -18,7 +17,9 @@ from homeassistant.loader import bind_hass from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.util import slugify +import homeassistant.util.dt as dt_util +ATTR_CREATED_AT = 'created_at' ATTR_MESSAGE = 'message' ATTR_NOTIFICATION_ID = 'notification_id' ATTR_TITLE = 'title' @@ -96,11 +97,11 @@ def async_dismiss(hass: HomeAssistant, notification_id: str) -> None: """Remove a notification.""" data = {ATTR_NOTIFICATION_ID: notification_id} - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_DISMISS, data)) + hass.async_create_task( + hass.services.async_call(DOMAIN, SERVICE_DISMISS, data)) -@asyncio.coroutine -def async_setup(hass: HomeAssistant, config: dict) -> Awaitable[bool]: +async def async_setup(hass: HomeAssistant, config: dict) -> Awaitable[bool]: """Set up the persistent notification component.""" persistent_notifications = OrderedDict() hass.data[DOMAIN] = {'notifications': persistent_notifications} @@ -148,6 +149,7 @@ def async_setup(hass: HomeAssistant, config: dict) -> Awaitable[bool]: ATTR_NOTIFICATION_ID: notification_id, ATTR_STATUS: STATUS_UNREAD, ATTR_TITLE: title, + ATTR_CREATED_AT: dt_util.utcnow(), } hass.bus.async_fire(EVENT_PERSISTENT_NOTIFICATIONS_UPDATED) @@ -201,11 +203,12 @@ def async_setup(hass: HomeAssistant, config: dict) -> Awaitable[bool]: def websocket_get_notifications( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg): """Return a list of persistent_notifications.""" - connection.to_write.put_nowait( + connection.send_message( websocket_api.result_message(msg['id'], [ { - key: data[key] for key in (ATTR_NOTIFICATION_ID, ATTR_MESSAGE, - ATTR_STATUS, ATTR_TITLE) + key: data[key] for key in (ATTR_NOTIFICATION_ID, + ATTR_MESSAGE, ATTR_STATUS, + ATTR_TITLE, ATTR_CREATED_AT) } for data in hass.data[DOMAIN]['notifications'].values() ]) diff --git a/homeassistant/components/plant.py b/homeassistant/components/plant.py index 84dc8402742..9659fd4f7e1 100644 --- a/homeassistant/components/plant.py +++ b/homeassistant/components/plant.py @@ -4,7 +4,6 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/plant/ """ import logging -import asyncio from datetime import datetime, timedelta from collections import deque import voluptuous as vol @@ -97,8 +96,7 @@ CONFIG_SCHEMA = vol.Schema({ ENABLE_LOAD_HISTORY = False -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the Plant component.""" component = EntityComponent(_LOGGER, DOMAIN, hass, group_name=GROUP_NAME_ALL_PLANTS) @@ -112,7 +110,7 @@ def async_setup(hass, config): async_track_state_change(hass, sensor_entity_ids, entity.state_changed) entities.append(entity) - yield from component.async_add_entities(entities) + await component.async_add_entities(entities) return True @@ -246,15 +244,13 @@ class Plant(Entity): return '{} high'.format(sensor_name) return None - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """After being added to hass, load from history.""" if ENABLE_LOAD_HISTORY and 'recorder' in self.hass.config.components: # only use the database if it's configured self.hass.async_add_job(self._load_history_from_db) - @asyncio.coroutine - def _load_history_from_db(self): + async def _load_history_from_db(self): """Load the history of the brightness values from the database. This only needs to be done once during startup. diff --git a/homeassistant/components/prometheus.py b/homeassistant/components/prometheus.py index 5fa768b6983..ee4b88d4d9b 100644 --- a/homeassistant/components/prometheus.py +++ b/homeassistant/components/prometheus.py @@ -4,7 +4,6 @@ Support for Prometheus metrics export. For more details about this component, please refer to the documentation at https://home-assistant.io/components/prometheus/ """ -import asyncio import logging import voluptuous as vol @@ -265,8 +264,7 @@ class PrometheusView(HomeAssistantView): """Initialize Prometheus view.""" self.prometheus_client = prometheus_client - @asyncio.coroutine - def get(self, request): + async def get(self, request): """Handle request for Prometheus metrics.""" _LOGGER.debug("Received Prometheus metrics request") diff --git a/homeassistant/components/rachio.py b/homeassistant/components/rachio.py index cd80b7bec9b..27827da0182 100644 --- a/homeassistant/components/rachio.py +++ b/homeassistant/components/rachio.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.auth.util import generate_secret from homeassistant.components.http import HomeAssistantView from homeassistant.const import CONF_API_KEY, EVENT_HOMEASSISTANT_STOP, URL_API -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import discovery, config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send REQUIREMENTS = ['rachiopy==0.1.3'] @@ -22,11 +22,19 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = 'rachio' +SUPPORTED_DOMAINS = ['switch', 'binary_sensor'] + +# Manual run length +CONF_MANUAL_RUN_MINS = 'manual_run_mins' +DEFAULT_MANUAL_RUN_MINS = 10 CONF_CUSTOM_URL = 'hass_url_override' + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_CUSTOM_URL): cv.string, + vol.Optional(CONF_MANUAL_RUN_MINS, default=DEFAULT_MANUAL_RUN_MINS): + cv.positive_int, }) }, extra=vol.ALLOW_EXTRA) @@ -112,7 +120,7 @@ def setup(hass, config) -> bool: # Get the API user try: - person = RachioPerson(hass, rachio) + person = RachioPerson(hass, rachio, config[DOMAIN]) except AssertionError as error: _LOGGER.error("Could not reach the Rachio API: %s", error) return False @@ -126,17 +134,23 @@ def setup(hass, config) -> bool: # Enable component hass.data[DOMAIN] = person + + # Load platforms + for component in SUPPORTED_DOMAINS: + discovery.load_platform(hass, component, DOMAIN, {}, config) + return True class RachioPerson: """Represent a Rachio user.""" - def __init__(self, hass, rachio): + def __init__(self, hass, rachio, config): """Create an object from the provided API instance.""" # Use API token to get user ID self._hass = hass self.rachio = rachio + self.config = config response = rachio.person.getInfo() assert int(response[0][KEY_STATUS]) == 200, "API key error" diff --git a/homeassistant/components/raincloud.py b/homeassistant/components/raincloud.py index 53cd8e79d7e..47f6176d5f8 100644 --- a/homeassistant/components/raincloud.py +++ b/homeassistant/components/raincloud.py @@ -4,7 +4,6 @@ Support for Melnor RainCloud sprinkler water timer. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/raincloud/ """ -import asyncio from datetime import timedelta import logging @@ -148,8 +147,7 @@ class RainCloudEntity(Entity): """Return the name of the sensor.""" return self._name - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" async_dispatcher_connect( self.hass, SIGNAL_UPDATE_RAINCLOUD, self._update_callback) diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 11ecb20f7aa..4fc491e57e8 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -4,7 +4,6 @@ Component to interface with universal remote control devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/remote/ """ -import asyncio from datetime import timedelta import functools as ft import logging @@ -70,69 +69,11 @@ def is_on(hass, entity_id=None): return hass.states.is_state(entity_id, STATE_ON) -@bind_hass -def turn_on(hass, activity=None, entity_id=None): - """Turn all or specified remote on.""" - data = { - key: value for key, value in [ - (ATTR_ACTIVITY, activity), - (ATTR_ENTITY_ID, entity_id), - ] if value is not None} - hass.services.call(DOMAIN, SERVICE_TURN_ON, data) - - -@bind_hass -def turn_off(hass, activity=None, entity_id=None): - """Turn all or specified remote off.""" - data = {} - if activity: - data[ATTR_ACTIVITY] = activity - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) - - -@bind_hass -def toggle(hass, activity=None, entity_id=None): - """Toggle all or specified remote.""" - data = {} - if activity: - data[ATTR_ACTIVITY] = activity - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_TOGGLE, data) - - -@bind_hass -def send_command(hass, command, entity_id=None, device=None, - num_repeats=None, delay_secs=None): - """Send a command to a device.""" - data = {ATTR_COMMAND: command} - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - if device: - data[ATTR_DEVICE] = device - - if num_repeats: - data[ATTR_NUM_REPEATS] = num_repeats - - if delay_secs: - data[ATTR_DELAY_SECS] = delay_secs - - hass.services.call(DOMAIN, SERVICE_SEND_COMMAND, data) - - -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Track states and offer events for remotes.""" component = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_REMOTES) - yield from component.async_setup(config) + await component.async_setup(config) component.async_register_entity_service( SERVICE_TURN_OFF, REMOTE_SERVICE_ACTIVITY_SCHEMA, diff --git a/homeassistant/components/remote/apple_tv.py b/homeassistant/components/remote/apple_tv.py index d8eac11372c..72696143bfe 100644 --- a/homeassistant/components/remote/apple_tv.py +++ b/homeassistant/components/remote/apple_tv.py @@ -4,7 +4,6 @@ Remote control support for Apple TV. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/remote.apple_tv/ """ -import asyncio from homeassistant.components.apple_tv import ( ATTR_ATV, ATTR_POWER, DATA_APPLE_TV) @@ -15,9 +14,8 @@ from homeassistant.const import (CONF_NAME, CONF_HOST) DEPENDENCIES = ['apple_tv'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Apple TV remote platform.""" if not discovery_info: return @@ -59,16 +57,14 @@ class AppleTVRemote(remote.RemoteDevice): """No polling needed for Apple TV.""" return False - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on. This method is a coroutine. """ self._power.set_power_on(True) - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the device off. This method is a coroutine. @@ -81,12 +77,11 @@ class AppleTVRemote(remote.RemoteDevice): This method must be run in the event loop and returns a coroutine. """ # Send commands in specified order but schedule only one coroutine - @asyncio.coroutine - def _send_commands(): + async def _send_commands(): for single_command in command: if not hasattr(self._atv.remote_control, single_command): continue - yield from getattr(self._atv.remote_control, single_command)() + await getattr(self._atv.remote_control, single_command)() return _send_commands() diff --git a/homeassistant/components/remote/harmony.py b/homeassistant/components/remote/harmony.py index 5b7d0d1df78..14008d49760 100644 --- a/homeassistant/components/remote/harmony.py +++ b/homeassistant/components/remote/harmony.py @@ -4,7 +4,6 @@ Support for Harmony Hub devices. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/remote.harmony/ """ -import asyncio import logging import time @@ -152,8 +151,7 @@ class HarmonyRemote(remote.RemoteDevice): pyharmony.ha_write_config_file(self._config, self._config_path) self._delay_secs = delay_secs - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Complete the initialization.""" self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index 723f575ba34..7cd588683de 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -61,9 +61,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Xiaomi IR Remote (Chuangmi IR) platform.""" from miio import ChuangmiIr, DeviceException @@ -109,8 +108,7 @@ def async_setup_platform(hass, config, async_add_entities, async_add_entities([xiaomi_miio_remote]) - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Handle a learn command.""" if service.service != SERVICE_LEARN: _LOGGER.error("We should not handle service: %s", service.service) @@ -130,14 +128,14 @@ def async_setup_platform(hass, config, async_add_entities, slot = service.data.get(CONF_SLOT, entity.slot) - yield from hass.async_add_job(device.learn, slot) + await hass.async_add_job(device.learn, slot) timeout = service.data.get(CONF_TIMEOUT, entity.timeout) _LOGGER.info("Press the key you want Home Assistant to learn") start_time = utcnow() while (utcnow() - start_time) < timedelta(seconds=timeout): - message = yield from hass.async_add_job( + message = await hass.async_add_job( device.read, slot) _LOGGER.debug("Message received from device: '%s'", message) @@ -150,9 +148,9 @@ def async_setup_platform(hass, config, async_add_entities, if ('error' in message and message['error']['message'] == "learn timeout"): - yield from hass.async_add_job(device.learn, slot) + await hass.async_add_job(device.learn, slot) - yield from asyncio.sleep(1, loop=hass.loop) + await asyncio.sleep(1, loop=hass.loop) _LOGGER.error("Timeout. No infrared command captured") hass.components.persistent_notification.async_create( @@ -230,14 +228,12 @@ class XiaomiMiioRemote(RemoteDevice): return {'hidden': 'true'} return - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on.""" _LOGGER.error("Device does not support turn_on, " "please use 'remote.send_command' to send commands.") - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the device off.""" _LOGGER.error("Device does not support turn_off, " "please use 'remote.send_command' to send commands.") diff --git a/homeassistant/components/rest_command.py b/homeassistant/components/rest_command.py index 4632315b757..3f9b258634d 100644 --- a/homeassistant/components/rest_command.py +++ b/homeassistant/components/rest_command.py @@ -53,8 +53,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the REST command component.""" websession = async_get_clientsession(hass) @@ -87,8 +86,7 @@ def async_setup(hass, config): headers = {} headers[hdrs.CONTENT_TYPE] = content_type - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Execute a shell command service.""" payload = None if template_payload: @@ -98,7 +96,7 @@ def async_setup(hass, config): try: with async_timeout.timeout(timeout, loop=hass.loop): - request = yield from getattr(websession, method)( + request = await getattr(websession, method)( template_url.async_render(variables=service.data), data=payload, auth=auth, diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py index b8af971b3ff..a8aeca273d6 100644 --- a/homeassistant/components/rflink.py +++ b/homeassistant/components/rflink.py @@ -105,8 +105,7 @@ def identify_event_type(event): return 'unknown' -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the Rflink component.""" from rflink.protocol import create_rflink_connection import serial @@ -125,11 +124,10 @@ def async_setup(hass, config): # Allow platform to specify function to register new unknown devices hass.data[DATA_DEVICE_REGISTER] = {} - @asyncio.coroutine - def async_send_command(call): + async def async_send_command(call): """Send Rflink command.""" _LOGGER.debug('Rflink command for %s', str(call.data)) - if not (yield from RflinkCommand.send_command( + if not (await RflinkCommand.send_command( call.data.get(CONF_DEVICE_ID), call.data.get(CONF_COMMAND))): _LOGGER.error('Failed Rflink command for %s', str(call.data)) @@ -196,8 +194,7 @@ def async_setup(hass, config): _LOGGER.warning('disconnected from Rflink, reconnecting') hass.async_add_job(connect) - @asyncio.coroutine - def connect(): + async def connect(): """Set up connection and hook it into HA for reconnect/shutdown.""" _LOGGER.info('Initiating Rflink connection') @@ -217,7 +214,7 @@ def async_setup(hass, config): try: with async_timeout.timeout(CONNECTION_TIMEOUT, loop=hass.loop): - transport, protocol = yield from connection + transport, protocol = await connection except (serial.serialutil.SerialException, ConnectionRefusedError, TimeoutError, OSError, asyncio.TimeoutError) as exc: @@ -330,8 +327,7 @@ class RflinkDevice(Entity): self._available = availability self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register update callback.""" async_dispatcher_connect(self.hass, SIGNAL_AVAILABILITY, self.set_availability) @@ -367,13 +363,11 @@ class RflinkCommand(RflinkDevice): return bool(cls._protocol) @classmethod - @asyncio.coroutine - def send_command(cls, device_id, action): + async def send_command(cls, device_id, action): """Send device command to Rflink and wait for acknowledgement.""" - return (yield from cls._protocol.send_command_ack(device_id, action)) + return await cls._protocol.send_command_ack(device_id, action) - @asyncio.coroutine - def _async_handle_command(self, command, *args): + async def _async_handle_command(self, command, *args): """Do bookkeeping for command, send it to rflink and update state.""" self.cancel_queued_send_commands() @@ -412,10 +406,10 @@ class RflinkCommand(RflinkDevice): # Send initial command and queue repetitions. # This allows the entity state to be updated quickly and not having to # wait for all repetitions to be sent - yield from self._async_send_command(cmd, self._signal_repetitions) + await self._async_send_command(cmd, self._signal_repetitions) # Update state of entity - yield from self.async_update_ha_state() + await self.async_update_ha_state() def cancel_queued_send_commands(self): """Cancel queued signal repetition commands. @@ -428,8 +422,7 @@ class RflinkCommand(RflinkDevice): if self._repetition_task: self._repetition_task.cancel() - @asyncio.coroutine - def _async_send_command(self, cmd, repetitions): + async def _async_send_command(self, cmd, repetitions): """Send a command for device to Rflink gateway.""" _LOGGER.debug( "Sending command: %s to Rflink device: %s", cmd, self._device_id) @@ -440,7 +433,7 @@ class RflinkCommand(RflinkDevice): if self._wait_ack: # Puts command on outgoing buffer then waits for Rflink to confirm # the command has been send out in the ether. - yield from self._protocol.send_command_ack(self._device_id, cmd) + await self._protocol.send_command_ack(self._device_id, cmd) else: # Puts command on outgoing buffer and returns straight away. # Rflink protocol/transport handles asynchronous writing of buffer @@ -450,7 +443,7 @@ class RflinkCommand(RflinkDevice): self._protocol.send_command, self._device_id, cmd)) if repetitions > 1: - self._repetition_task = self.hass.async_add_job( + self._repetition_task = self.hass.async_create_task( self._async_send_command(cmd, repetitions - 1)) diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index b5bc97b7ffa..f2c82842bc1 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -4,7 +4,6 @@ Support for RFXtrx components. For more details about this component, please refer to the documentation at https://home-assistant.io/components/rfxtrx/ """ -import asyncio from collections import OrderedDict import logging @@ -316,8 +315,7 @@ class RfxtrxDevice(Entity): self._brightness = 0 self.added_to_hass = False - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe RFXtrx events.""" self.added_to_hass = True diff --git a/homeassistant/components/rss_feed_template.py b/homeassistant/components/rss_feed_template.py index 1441a98c0a8..34bee1ec5fc 100644 --- a/homeassistant/components/rss_feed_template.py +++ b/homeassistant/components/rss_feed_template.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/rss_feed_template/ """ -import asyncio from html import escape from aiohttp import web @@ -76,8 +75,7 @@ class RssView(HomeAssistantView): self._title = title self._items = items - @asyncio.coroutine - def get(self, request, entity_id=None): + async def get(self, request, entity_id=None): """Generate the RSS view XML.""" response = '\n\n' diff --git a/homeassistant/components/satel_integra.py b/homeassistant/components/satel_integra.py index 128377d19f7..8d7d1e619db 100644 --- a/homeassistant/components/satel_integra.py +++ b/homeassistant/components/satel_integra.py @@ -67,8 +67,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the Satel Integra component.""" conf = config.get(DOMAIN) @@ -83,13 +82,12 @@ def async_setup(hass, config): hass.data[DATA_SATEL] = controller - result = yield from controller.connect() + result = await controller.connect() if not result: return False - @asyncio.coroutine - def _close(): + async def _close(): controller.close() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close()) @@ -105,7 +103,7 @@ def async_setup(hass, config): async_load_platform(hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config)) - yield from asyncio.wait([task_control_panel, task_zones], loop=hass.loop) + await asyncio.wait([task_control_panel, task_zones], loop=hass.loop) @callback def alarm_status_update_callback(status): diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 8771a84c1d6..2bcb1c8e16d 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -12,7 +12,6 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, CONF_PLATFORM, SERVICE_TURN_ON) -from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -61,17 +60,6 @@ SCENE_SERVICE_SCHEMA = vol.Schema({ }) -@bind_hass -def activate(hass, entity_id=None): - """Activate a scene.""" - data = {} - - if entity_id: - data[ATTR_ENTITY_ID] = entity_id - - hass.services.call(DOMAIN, SERVICE_TURN_ON, data) - - async def async_setup(hass, config): """Set up the scenes.""" logger = logging.getLogger(__name__) diff --git a/homeassistant/components/scene/homeassistant.py b/homeassistant/components/scene/homeassistant.py index 7e1d670ca69..5812512ccef 100644 --- a/homeassistant/components/scene/homeassistant.py +++ b/homeassistant/components/scene/homeassistant.py @@ -4,7 +4,6 @@ Allow users to set and activate scenes. For more details about this component, please refer to the documentation at https://home-assistant.io/components/scene/ """ -import asyncio from collections import namedtuple import voluptuous as vol @@ -35,9 +34,8 @@ PLATFORM_SCHEMA = vol.Schema({ SCENECONFIG = namedtuple('SceneConfig', [CONF_NAME, STATES]) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up home assistant scene entries.""" scene_config = config.get(STATES) @@ -97,8 +95,7 @@ class HomeAssistantScene(Scene): ATTR_ENTITY_ID: list(self.scene_config.states.keys()), } - @asyncio.coroutine - def async_activate(self): + async def async_activate(self): """Activate scene. Try to get entities into requested state.""" - yield from async_reproduce_state( + await async_reproduce_state( self.hass, self.scene_config.states.values(), True) diff --git a/homeassistant/components/scene/hunterdouglas_powerview.py b/homeassistant/components/scene/hunterdouglas_powerview.py index 40534b68635..8c1ffa17e90 100644 --- a/homeassistant/components/scene/hunterdouglas_powerview.py +++ b/homeassistant/components/scene/hunterdouglas_powerview.py @@ -4,7 +4,6 @@ Support for Powerview scenes from a Powerview hub. For more details about this component, please refer to the documentation at https://home-assistant.io/components/scene.hunterdouglas_powerview/ """ -import asyncio import logging import voluptuous as vol @@ -36,9 +35,8 @@ ROOM_ID_IN_SCENE = 'roomId' STATE_ATTRIBUTE_ROOM_NAME = 'roomName' -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up home assistant scene entries.""" # from aiopvapi.hub import Hub from aiopvapi.scenes import Scenes @@ -48,9 +46,9 @@ def async_setup_platform(hass, config, async_add_entities, hub_address = config.get(HUB_ADDRESS) websession = async_get_clientsession(hass) - _scenes = yield from Scenes( + _scenes = await Scenes( hub_address, hass.loop, websession).get_resources() - _rooms = yield from Rooms( + _rooms = await Rooms( hub_address, hass.loop, websession).get_resources() if not _scenes or not _rooms: diff --git a/homeassistant/components/scene/lifx_cloud.py b/homeassistant/components/scene/lifx_cloud.py index 3169acb3a31..c1dda86343d 100644 --- a/homeassistant/components/scene/lifx_cloud.py +++ b/homeassistant/components/scene/lifx_cloud.py @@ -29,9 +29,8 @@ PLATFORM_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the scenes stored in the LIFX Cloud.""" token = config.get(CONF_TOKEN) timeout = config.get(CONF_TIMEOUT) @@ -45,7 +44,7 @@ def async_setup_platform(hass, config, async_add_entities, try: httpsession = async_get_clientsession(hass) with async_timeout.timeout(timeout, loop=hass.loop): - scenes_resp = yield from httpsession.get(url, headers=headers) + scenes_resp = await httpsession.get(url, headers=headers) except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.exception("Error on %s", url) @@ -53,7 +52,7 @@ def async_setup_platform(hass, config, async_add_entities, status = scenes_resp.status if status == 200: - data = yield from scenes_resp.json() + data = await scenes_resp.json() devices = [] for scene in data: devices.append(LifxCloudScene(hass, headers, timeout, scene)) @@ -83,15 +82,14 @@ class LifxCloudScene(Scene): """Return the name of the scene.""" return self._name - @asyncio.coroutine - def async_activate(self): + async def async_activate(self): """Activate the scene.""" url = LIFX_API_URL.format('scenes/scene_id:%s/activate' % self._uuid) try: httpsession = async_get_clientsession(self.hass) with async_timeout.timeout(self._timeout, loop=self.hass.loop): - yield from httpsession.put(url, headers=self._headers) + await httpsession.put(url, headers=self._headers) except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.exception("Error on %s", url) diff --git a/homeassistant/components/scene/lutron_caseta.py b/homeassistant/components/scene/lutron_caseta.py index 0f9173663a9..0ef974e2778 100644 --- a/homeassistant/components/scene/lutron_caseta.py +++ b/homeassistant/components/scene/lutron_caseta.py @@ -4,7 +4,6 @@ Support for Lutron Caseta scenes. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/scene.lutron_caseta/ """ -import asyncio import logging from homeassistant.components.lutron_caseta import LUTRON_CASETA_SMARTBRIDGE @@ -15,9 +14,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['lutron_caseta'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Lutron Caseta lights.""" devs = [] bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] @@ -43,7 +41,6 @@ class LutronCasetaScene(Scene): """Return the name of the scene.""" return self._scene_name - @asyncio.coroutine - def async_activate(self): + async def async_activate(self): """Activate the scene.""" self._bridge.activate_scene(self._scene_id) diff --git a/homeassistant/components/scene/wink.py b/homeassistant/components/scene/wink.py index 62da668694b..35db96c3b8b 100644 --- a/homeassistant/components/scene/wink.py +++ b/homeassistant/components/scene/wink.py @@ -4,7 +4,6 @@ Support for Wink scenes. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/scene.wink/ """ -import asyncio import logging from homeassistant.components.scene import Scene @@ -33,8 +32,7 @@ class WinkScene(WinkDevice, Scene): super().__init__(wink, hass) hass.data[DOMAIN]['entities']['scene'].append(self) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['scene'].append(self) diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index 247ac07283e..16c9f65420c 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -15,7 +15,6 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_TOGGLE, SERVICE_RELOAD, STATE_ON, CONF_ALIAS) -from homeassistant.core import split_entity_id from homeassistant.loader import bind_hass from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent @@ -62,41 +61,6 @@ def is_on(hass, entity_id): return hass.states.is_state(entity_id, STATE_ON) -@bind_hass -def turn_on(hass, entity_id, variables=None, context=None): - """Turn script on.""" - _, object_id = split_entity_id(entity_id) - - hass.services.call(DOMAIN, object_id, variables, context=context) - - -@bind_hass -def turn_off(hass, entity_id): - """Turn script on.""" - hass.services.call(DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}) - - -@bind_hass -def toggle(hass, entity_id): - """Toggle the script.""" - hass.services.call(DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}) - - -@bind_hass -def reload(hass): - """Reload script component.""" - hass.services.call(DOMAIN, SERVICE_RELOAD) - - -@bind_hass -def async_reload(hass): - """Reload the scripts from config. - - Returns a coroutine object. - """ - return hass.services.async_call(DOMAIN, SERVICE_RELOAD) - - async def async_setup(hass, config): """Load the scripts from the configuration.""" component = EntityComponent( diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 948f844cfd4..be599cc295a 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -14,7 +14,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_TEMPERATURE) + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_PRESSURE) _LOGGER = logging.getLogger(__name__) @@ -28,6 +28,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_HUMIDITY, # % of humidity in the air DEVICE_CLASS_ILLUMINANCE, # current light level (lx/lm) DEVICE_CLASS_TEMPERATURE, # temperature (C/F) + DEVICE_CLASS_PRESSURE, # pressure (hPa/mbar) ] DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) diff --git a/homeassistant/components/sensor/ads.py b/homeassistant/components/sensor/ads.py index 5d5cbb379bf..24515357f5e 100644 --- a/homeassistant/components/sensor/ads.py +++ b/homeassistant/components/sensor/ads.py @@ -4,7 +4,6 @@ Support for ADS sensors. For more details about this platform, please refer to the documentation. https://home-assistant.io/components/sensor.ads/ """ -import asyncio import logging import voluptuous as vol @@ -62,8 +61,7 @@ class AdsSensor(Entity): self.ads_type = ads_type self.factor = factor - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register device notification.""" def update(name, value): """Handle device notifications.""" diff --git a/homeassistant/components/sensor/alarmdecoder.py b/homeassistant/components/sensor/alarmdecoder.py index 51e166bfce6..546d09299dc 100644 --- a/homeassistant/components/sensor/alarmdecoder.py +++ b/homeassistant/components/sensor/alarmdecoder.py @@ -4,7 +4,6 @@ Support for AlarmDecoder Sensors (Shows Panel Display). For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.alarmdecoder/ """ -import asyncio import logging from homeassistant.helpers.entity import Entity @@ -34,8 +33,7 @@ class AlarmDecoderSensor(Entity): self._icon = 'mdi:alarm-check' self._name = 'Alarm Panel Display' - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( SIGNAL_PANEL_MESSAGE, self._message_callback) diff --git a/homeassistant/components/sensor/amcrest.py b/homeassistant/components/sensor/amcrest.py index 53a8c663f21..50d6e9b7fa9 100644 --- a/homeassistant/components/sensor/amcrest.py +++ b/homeassistant/components/sensor/amcrest.py @@ -4,7 +4,6 @@ This component provides HA sensor support for Amcrest IP cameras. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.amcrest/ """ -import asyncio from datetime import timedelta import logging @@ -19,9 +18,8 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=10) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up a sensor for an Amcrest IP Camera.""" if discovery_info is None: return diff --git a/homeassistant/components/sensor/android_ip_webcam.py b/homeassistant/components/sensor/android_ip_webcam.py index 333bf12ec21..0f795f85dcd 100644 --- a/homeassistant/components/sensor/android_ip_webcam.py +++ b/homeassistant/components/sensor/android_ip_webcam.py @@ -4,7 +4,6 @@ Support for IP Webcam sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.android_ip_webcam/ """ -import asyncio from homeassistant.components.android_ip_webcam import ( KEY_MAP, ICON_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, @@ -14,9 +13,8 @@ from homeassistant.helpers.icon import icon_for_battery_level DEPENDENCIES = ['android_ip_webcam'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the IP Webcam Sensor.""" if discovery_info is None: return @@ -62,8 +60,7 @@ class IPWebcamSensor(AndroidIPCamEntity): """Return the state of the sensor.""" return self._state - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Retrieve latest state.""" if self._sensor in ('audio_connections', 'video_connections'): if not self._ipcam.status_data: diff --git a/homeassistant/components/sensor/api_streams.py b/homeassistant/components/sensor/api_streams.py index 1ecae1e753e..ac9b15754d0 100644 --- a/homeassistant/components/sensor/api_streams.py +++ b/homeassistant/components/sensor/api_streams.py @@ -4,7 +4,6 @@ Entity to track connections to stream API. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.api_stream/ """ -import asyncio import logging from homeassistant.const import EVENT_HOMEASSISTANT_STOP @@ -48,9 +47,8 @@ class StreamHandler(logging.Handler): self.entity.schedule_update_ha_state() -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the API stream platform.""" entity = APICount() handler = StreamHandler(entity) diff --git a/homeassistant/components/sensor/aqualogic.py b/homeassistant/components/sensor/aqualogic.py new file mode 100644 index 00000000000..f10fd05b83f --- /dev/null +++ b/homeassistant/components/sensor/aqualogic.py @@ -0,0 +1,111 @@ +""" +Support for AquaLogic sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.aqualogic/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_MONITORED_CONDITIONS, + TEMP_CELSIUS, TEMP_FAHRENHEIT) +from homeassistant.core import callback +from homeassistant.helpers.entity import Entity +import homeassistant.components.aqualogic as aq +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['aqualogic'] + +TEMP_UNITS = [TEMP_CELSIUS, TEMP_FAHRENHEIT] +PERCENT_UNITS = ['%', '%'] +SALT_UNITS = ['g/L', 'PPM'] +WATT_UNITS = ['W', 'W'] +NO_UNITS = [None, None] + +# sensor_type [ description, unit, icon ] +# sensor_type corresponds to property names in aqualogic.core.AquaLogic +SENSOR_TYPES = { + 'air_temp': ['Air Temperature', TEMP_UNITS, 'mdi:thermometer'], + 'pool_temp': ['Pool Temperature', TEMP_UNITS, 'mdi:oil-temperature'], + 'spa_temp': ['Spa Temperature', TEMP_UNITS, 'mdi:oil-temperature'], + 'pool_chlorinator': ['Pool Chlorinator', PERCENT_UNITS, 'mdi:gauge'], + 'spa_chlorinator': ['Spa Chlorinator', PERCENT_UNITS, 'mdi:gauge'], + 'salt_level': ['Salt Level', SALT_UNITS, 'mdi:gauge'], + 'pump_speed': ['Pump Speed', PERCENT_UNITS, 'mdi:speedometer'], + 'pump_power': ['Pump Power', WATT_UNITS, 'mdi:gauge'], + 'status': ['Status', NO_UNITS, 'mdi:alert'] +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]) +}) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the sensor platform.""" + sensors = [] + + processor = hass.data[aq.DOMAIN] + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + sensors.append(AquaLogicSensor(processor, sensor_type)) + + async_add_entities(sensors) + + +class AquaLogicSensor(Entity): + """Sensor implementation for the AquaLogic component.""" + + def __init__(self, processor, sensor_type): + """Initialize sensor.""" + self._processor = processor + self._type = sensor_type + self._state = None + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def name(self): + """Return the name of the sensor.""" + return "AquaLogic {}".format(SENSOR_TYPES[self._type][0]) + + @property + def unit_of_measurement(self): + """Return the unit of measurement the value is expressed in.""" + panel = self._processor.panel + if panel is None: + return None + if panel.is_metric: + return SENSOR_TYPES[self._type][1][0] + return SENSOR_TYPES[self._type][1][1] + + @property + def should_poll(self): + """Return the polling state.""" + return False + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return SENSOR_TYPES[self._type][2] + + async def async_added_to_hass(self): + """Register callbacks.""" + self.hass.helpers.dispatcher.async_dispatcher_connect( + aq.UPDATE_TOPIC, self.async_update_callback) + + @callback + def async_update_callback(self): + """Update callback.""" + panel = self._processor.panel + if panel is not None: + self._state = getattr(panel, self._type) + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/sensor/arwn.py b/homeassistant/components/sensor/arwn.py index 580701490a6..2b79e4c3a9a 100644 --- a/homeassistant/components/sensor/arwn.py +++ b/homeassistant/components/sensor/arwn.py @@ -4,7 +4,6 @@ Support for collecting data from the ARWN project. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.arwn/ """ -import asyncio import json import logging @@ -58,9 +57,8 @@ def _slug(name): return 'sensor.arwn_{}'.format(slugify(name)) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the ARWN platform.""" @callback def async_sensor_event_received(topic, payload, qos): @@ -102,7 +100,7 @@ def async_setup_platform(hass, config, async_add_entities, else: store[sensor.name].set_event(event) - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( hass, TOPIC, async_sensor_event_received, 0) return True diff --git a/homeassistant/components/sensor/bh1750.py b/homeassistant/components/sensor/bh1750.py index 6230ae8a74d..fb0a0116818 100644 --- a/homeassistant/components/sensor/bh1750.py +++ b/homeassistant/components/sensor/bh1750.py @@ -4,7 +4,6 @@ Support for BH1750 light sensor. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.bh1750/ """ -import asyncio from functools import partial import logging @@ -66,9 +65,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=import-error -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the BH1750 sensor.""" import smbus from i2csense.bh1750 import BH1750 @@ -80,7 +78,7 @@ def async_setup_platform(hass, config, async_add_entities, bus = smbus.SMBus(bus_number) - sensor = yield from hass.async_add_job( + sensor = await hass.async_add_job( partial(BH1750, bus, i2c_address, operation_mode=operation_mode, measurement_delay=config.get(CONF_DELAY), @@ -133,10 +131,9 @@ class BH1750Sensor(Entity): """Return the class of this device, from component DEVICE_CLASSES.""" return DEVICE_CLASS_ILLUMINANCE - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data from the BH1750 and update the states.""" - yield from self.hass.async_add_job(self.bh1750_sensor.update) + await self.hass.async_add_job(self.bh1750_sensor.update) if self.bh1750_sensor.sample_ok \ and self.bh1750_sensor.light_level >= 0: self._state = int(round(self.bh1750_sensor.light_level diff --git a/homeassistant/components/sensor/blink.py b/homeassistant/components/sensor/blink.py index 97356b6fc61..885bb939edf 100644 --- a/homeassistant/components/sensor/blink.py +++ b/homeassistant/components/sensor/blink.py @@ -6,34 +6,24 @@ https://home-assistant.io/components/sensor.blink/ """ import logging -from homeassistant.components.blink import DOMAIN -from homeassistant.const import TEMP_FAHRENHEIT +from homeassistant.components.blink import BLINK_DATA, SENSORS from homeassistant.helpers.entity import Entity +from homeassistant.const import CONF_MONITORED_CONDITIONS _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['blink'] -SENSOR_TYPES = { - 'temperature': ['Temperature', TEMP_FAHRENHEIT], - 'battery': ['Battery', ''], - 'notifications': ['Notifications', ''] -} - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up a Blink sensor.""" if discovery_info is None: return - - data = hass.data[DOMAIN].blink - devs = list() - index = 0 - for name in data.cameras: - devs.append(BlinkSensor(name, 'temperature', index, data)) - devs.append(BlinkSensor(name, 'battery', index, data)) - devs.append(BlinkSensor(name, 'notifications', index, data)) - index += 1 + data = hass.data[BLINK_DATA] + devs = [] + for camera in data.sync.cameras: + for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: + devs.append(BlinkSensor(data, camera, sensor_type)) add_entities(devs, True) @@ -41,21 +31,29 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BlinkSensor(Entity): """A Blink camera sensor.""" - def __init__(self, name, sensor_type, index, data): + def __init__(self, data, camera, sensor_type): """Initialize sensors from Blink camera.""" - self._name = 'blink_' + name + '_' + SENSOR_TYPES[sensor_type][0] + name, units, icon = SENSORS[sensor_type] + self._name = "{} {} {}".format( + BLINK_DATA, camera, name) self._camera_name = name self._type = sensor_type self.data = data - self.index = index + self._camera = data.sync.cameras[camera] self._state = None - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._unit_of_measurement = units + self._icon = icon @property def name(self): """Return the name of the camera.""" return self._name + @property + def icon(self): + """Return the icon of the sensor.""" + return self._icon + @property def state(self): """Return the camera's current state.""" @@ -68,13 +66,11 @@ class BlinkSensor(Entity): def update(self): """Retrieve sensor data from the camera.""" - camera = self.data.cameras[self._camera_name] - if self._type == 'temperature': - self._state = camera.temperature - elif self._type == 'battery': - self._state = camera.battery_string - elif self._type == 'notifications': - self._state = camera.notifications - else: + self.data.refresh() + try: + self._state = self._camera.attributes[self._type] + except KeyError: self._state = None - _LOGGER.warning("Could not retrieve state from %s", self.name) + _LOGGER.error( + "%s not a valid camera attribute. Did the API change?", + self._type) diff --git a/homeassistant/components/sensor/bme280.py b/homeassistant/components/sensor/bme280.py index 676800c1069..f67dace817e 100644 --- a/homeassistant/components/sensor/bme280.py +++ b/homeassistant/components/sensor/bme280.py @@ -4,7 +4,6 @@ Support for BME280 temperature, humidity and pressure sensor. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.bme280/ """ -import asyncio from datetime import timedelta from functools import partial import logging @@ -81,9 +80,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=import-error -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the BME280 sensor.""" import smbus from i2csense.bme280 import BME280 @@ -93,7 +91,7 @@ def async_setup_platform(hass, config, async_add_entities, i2c_address = config.get(CONF_I2C_ADDRESS) bus = smbus.SMBus(config.get(CONF_I2C_BUS)) - sensor = yield from hass.async_add_job( + sensor = await hass.async_add_job( partial(BME280, bus, i2c_address, osrs_t=config.get(CONF_OVERSAMPLING_TEMP), osrs_p=config.get(CONF_OVERSAMPLING_PRES), @@ -108,7 +106,7 @@ def async_setup_platform(hass, config, async_add_entities, _LOGGER.error("BME280 sensor not detected at %s", i2c_address) return False - sensor_handler = yield from hass.async_add_job(BME280Handler, sensor) + sensor_handler = await hass.async_add_job(BME280Handler, sensor) dev = [] try: @@ -163,10 +161,9 @@ class BME280Sensor(Entity): """Return the unit of measurement of the sensor.""" return self._unit_of_measurement - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data from the BME280 and update the states.""" - yield from self.hass.async_add_job(self.bme280_client.update) + await self.hass.async_add_job(self.bme280_client.update) if self.bme280_client.sensor.sample_ok: if self.type == SENSOR_TEMP: temperature = round(self.bme280_client.sensor.temperature, 1) diff --git a/homeassistant/components/sensor/bme680.py b/homeassistant/components/sensor/bme680.py index 65d486ada36..c4e8baf6c05 100644 --- a/homeassistant/components/sensor/bme680.py +++ b/homeassistant/components/sensor/bme680.py @@ -7,7 +7,6 @@ Air Quality calculation based on humidity and volatile gas. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.bme680/ """ -import asyncio import logging from time import time, sleep @@ -97,14 +96,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the BME680 sensor.""" SENSOR_TYPES[SENSOR_TEMP][1] = hass.config.units.temperature_unit name = config.get(CONF_NAME) - sensor_handler = yield from hass.async_add_job(_setup_bme680, config) + sensor_handler = await hass.async_add_job(_setup_bme680, config) if sensor_handler is None: return @@ -351,10 +349,9 @@ class BME680Sensor(Entity): """Return the unit of measurement of the sensor.""" return self._unit_of_measurement - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data from the BME680 and update the states.""" - yield from self.hass.async_add_job(self.bme680_client.update) + await self.hass.async_add_job(self.bme680_client.update) if self.type == SENSOR_TEMP: temperature = round(self.bme680_client.sensor_data.temperature, 1) if self.temp_unit == TEMP_FAHRENHEIT: diff --git a/homeassistant/components/sensor/bmw_connected_drive.py b/homeassistant/components/sensor/bmw_connected_drive.py index ff80100e21d..964a8a4cb16 100644 --- a/homeassistant/components/sensor/bmw_connected_drive.py +++ b/homeassistant/components/sensor/bmw_connected_drive.py @@ -4,7 +4,6 @@ Reads vehicle status from BMW connected drive portal. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.bmw_connected_drive/ """ -import asyncio import logging from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN @@ -124,8 +123,7 @@ class BMWConnectedDriveSensor(Entity): """Schedule a state update.""" self.schedule_update_ha_state(True) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Add callback after being added to hass. Show latest data after startup. diff --git a/homeassistant/components/sensor/buienradar.py b/homeassistant/components/sensor/buienradar.py index c7ca0c097ff..36585b8e103 100644 --- a/homeassistant/components/sensor/buienradar.py +++ b/homeassistant/components/sensor/buienradar.py @@ -140,9 +140,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Create the buienradar sensor.""" from homeassistant.components.weather.buienradar import DEFAULT_TIMEFRAME @@ -168,7 +167,7 @@ def async_setup_platform(hass, config, async_add_entities, data = BrData(hass, coordinates, timeframe, dev) # schedule the first update in 1 minute from now: - yield from data.schedule_update(1) + await data.schedule_update(1) class BrSensor(Entity): @@ -386,8 +385,7 @@ class BrData: self.coordinates = coordinates self.timeframe = timeframe - @asyncio.coroutine - def update_devices(self): + async def update_devices(self): """Update all devices/sensors.""" if self.devices: tasks = [] @@ -397,18 +395,16 @@ class BrData: tasks.append(dev.async_update_ha_state()) if tasks: - yield from asyncio.wait(tasks, loop=self.hass.loop) + await asyncio.wait(tasks, loop=self.hass.loop) - @asyncio.coroutine - def schedule_update(self, minute=1): + async def schedule_update(self, minute=1): """Schedule an update after minute minutes.""" _LOGGER.debug("Scheduling next update in %s minutes.", minute) nxt = dt_util.utcnow() + timedelta(minutes=minute) async_track_point_in_utc_time(self.hass, self.async_update, nxt) - @asyncio.coroutine - def get_data(self, url): + async def get_data(self, url): """Load data from specified url.""" from buienradar.buienradar import (CONTENT, MESSAGE, STATUS_CODE, SUCCESS) @@ -419,10 +415,10 @@ class BrData: try: websession = async_get_clientsession(self.hass) with async_timeout.timeout(10, loop=self.hass.loop): - resp = yield from websession.get(url) + resp = await websession.get(url) result[STATUS_CODE] = resp.status - result[CONTENT] = yield from resp.text() + result[CONTENT] = await resp.text() if resp.status == 200: result[SUCCESS] = True else: @@ -434,17 +430,16 @@ class BrData: return result finally: if resp is not None: - yield from resp.release() + await resp.release() - @asyncio.coroutine - def async_update(self, *_): + async def async_update(self, *_): """Update the data from buienradar.""" from buienradar.buienradar import (parse_data, CONTENT, DATA, MESSAGE, STATUS_CODE, SUCCESS) - content = yield from self.get_data('http://xml.buienradar.nl') + content = await self.get_data('http://xml.buienradar.nl') if not content.get(SUCCESS, False): - content = yield from self.get_data('http://api.buienradar.nl') + content = await self.get_data('http://api.buienradar.nl') if content.get(SUCCESS) is not True: # unable to get the data @@ -453,7 +448,7 @@ class BrData: content.get(MESSAGE), content.get(STATUS_CODE),) # schedule new call - yield from self.schedule_update(SCHEDULE_NOK) + await self.schedule_update(SCHEDULE_NOK) return # rounding coordinates prevents unnecessary redirects/calls @@ -462,7 +457,7 @@ class BrData: round(self.coordinates[CONF_LATITUDE], 2), round(self.coordinates[CONF_LONGITUDE], 2) ) - raincontent = yield from self.get_data(rainurl) + raincontent = await self.get_data(rainurl) if raincontent.get(SUCCESS) is not True: # unable to get the data @@ -471,7 +466,7 @@ class BrData: raincontent.get(MESSAGE), raincontent.get(STATUS_CODE),) # schedule new call - yield from self.schedule_update(SCHEDULE_NOK) + await self.schedule_update(SCHEDULE_NOK) return result = parse_data(content.get(CONTENT), @@ -486,12 +481,12 @@ class BrData: _LOGGER.warning("Unable to parse data from Buienradar." "(Msg: %s)", result.get(MESSAGE),) - yield from self.schedule_update(SCHEDULE_NOK) + await self.schedule_update(SCHEDULE_NOK) return self.data = result.get(DATA) - yield from self.update_devices() - yield from self.schedule_update(SCHEDULE_OK) + await self.update_devices() + await self.schedule_update(SCHEDULE_OK) @property def attribution(self): diff --git a/homeassistant/components/sensor/citybikes.py b/homeassistant/components/sensor/citybikes.py index 8003a77a452..12c475e62ff 100644 --- a/homeassistant/components/sensor/citybikes.py +++ b/homeassistant/components/sensor/citybikes.py @@ -104,16 +104,15 @@ class CityBikesRequestError(Exception): pass -@asyncio.coroutine -def async_citybikes_request(hass, uri, schema): +async def async_citybikes_request(hass, uri, schema): """Perform a request to CityBikes API endpoint, and parse the response.""" try: session = async_get_clientsession(hass) with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): - req = yield from session.get(DEFAULT_ENDPOINT.format(uri=uri)) + req = await session.get(DEFAULT_ENDPOINT.format(uri=uri)) - json_response = yield from req.json() + json_response = await req.json() return schema(json_response) except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Could not connect to CityBikes API endpoint") @@ -125,9 +124,8 @@ def async_citybikes_request(hass, uri, schema): raise CityBikesRequestError -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the CityBikes platform.""" if PLATFORM not in hass.data: hass.data[PLATFORM] = {MONITORED_NETWORKS: {}} @@ -142,7 +140,7 @@ def async_setup_platform(hass, config, async_add_entities, radius = distance.convert(radius, LENGTH_FEET, LENGTH_METERS) if not network_id: - network_id = yield from CityBikesNetwork.get_closest_network_id( + network_id = await CityBikesNetwork.get_closest_network_id( hass, latitude, longitude) if network_id not in hass.data[PLATFORM][MONITORED_NETWORKS]: @@ -153,7 +151,7 @@ def async_setup_platform(hass, config, async_add_entities, else: network = hass.data[PLATFORM][MONITORED_NETWORKS][network_id] - yield from network.ready.wait() + await network.ready.wait() devices = [] for station in network.stations: @@ -177,13 +175,12 @@ class CityBikesNetwork: NETWORKS_LIST_LOADING = asyncio.Condition() @classmethod - @asyncio.coroutine - def get_closest_network_id(cls, hass, latitude, longitude): + async def get_closest_network_id(cls, hass, latitude, longitude): """Return the id of the network closest to provided location.""" try: - yield from cls.NETWORKS_LIST_LOADING.acquire() + await cls.NETWORKS_LIST_LOADING.acquire() if cls.NETWORKS_LIST is None: - networks = yield from async_citybikes_request( + networks = await async_citybikes_request( hass, NETWORKS_URI, NETWORKS_RESPONSE_SCHEMA) cls.NETWORKS_LIST = networks[ATTR_NETWORKS_LIST] result = None @@ -210,11 +207,10 @@ class CityBikesNetwork: self.stations = [] self.ready = asyncio.Event() - @asyncio.coroutine - def async_refresh(self, now=None): + async def async_refresh(self, now=None): """Refresh the state of the network.""" try: - network = yield from async_citybikes_request( + network = await async_citybikes_request( self.hass, STATIONS_URI.format(uid=self.network_id), STATIONS_RESPONSE_SCHEMA) self.stations = network[ATTR_NETWORK][ATTR_STATIONS_LIST] @@ -253,8 +249,7 @@ class CityBikesStation(Entity): return self._station_data[ATTR_NAME] return None - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update station state.""" if self._network.ready.is_set(): for station in self._network.stations: diff --git a/homeassistant/components/sensor/comed_hourly_pricing.py b/homeassistant/components/sensor/comed_hourly_pricing.py index 3595bcaa227..b5d230d8517 100644 --- a/homeassistant/components/sensor/comed_hourly_pricing.py +++ b/homeassistant/components/sensor/comed_hourly_pricing.py @@ -49,9 +49,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the ComEd Hourly Pricing sensor.""" websession = async_get_clientsession(hass) dev = [] @@ -101,8 +100,7 @@ class ComedHourlyPricingSensor(Entity): attrs = {ATTR_ATTRIBUTION: CONF_ATTRIBUTION} return attrs - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the ComEd Hourly Pricing data from the web service.""" try: if self.type == CONF_FIVE_MINUTE or \ @@ -114,9 +112,9 @@ class ComedHourlyPricingSensor(Entity): url_string += '?type=currenthouraverage' with async_timeout.timeout(60, loop=self.loop): - response = yield from self.websession.get(url_string) + response = await self.websession.get(url_string) # The API responds with MIME type 'text/html' - text = yield from response.text() + text = await response.text() data = json.loads(text) self._state = round( float(data[0]['price']) + self.offset, 2) diff --git a/homeassistant/components/sensor/discogs.py b/homeassistant/components/sensor/discogs.py index 6e85c41ac6e..70d7155fec7 100644 --- a/homeassistant/components/sensor/discogs.py +++ b/homeassistant/components/sensor/discogs.py @@ -4,7 +4,6 @@ Show the amount of records in a user's Discogs collection. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.discogs/ """ -import asyncio from datetime import timedelta import logging @@ -36,9 +35,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Discogs sensor.""" import discogs_client @@ -92,7 +90,6 @@ class DiscogsSensor(Entity): ATTR_IDENTITY: self._identity.name, } - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Set state to the amount of records in user's collection.""" self._state = self._identity.num_collection diff --git a/homeassistant/components/sensor/dnsip.py b/homeassistant/components/sensor/dnsip.py index 3027b6f8ca6..7b0d54cd934 100644 --- a/homeassistant/components/sensor/dnsip.py +++ b/homeassistant/components/sensor/dnsip.py @@ -4,7 +4,6 @@ Get your own public IP address or that of any host. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.dnsip/ """ -import asyncio import logging from datetime import timedelta @@ -42,8 +41,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@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 DNS IP sensor.""" hostname = config.get(CONF_HOSTNAME) name = config.get(CONF_NAME) @@ -86,11 +85,10 @@ class WanIpSensor(Entity): """Return the current DNS IP address for hostname.""" return self._state - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the current DNS IP address for hostname.""" - response = yield from self.resolver.query(self.hostname, - self.querytype) + response = await self.resolver.query(self.hostname, + self.querytype) if response: self._state = response[0].host else: diff --git a/homeassistant/components/sensor/dsmr.py b/homeassistant/components/sensor/dsmr.py index 13b13114150..d54959813f8 100644 --- a/homeassistant/components/sensor/dsmr.py +++ b/homeassistant/components/sensor/dsmr.py @@ -48,9 +48,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the DSMR sensor.""" # Suppress logging logging.getLogger('dsmr_parser').setLevel(logging.ERROR) @@ -168,7 +167,7 @@ def async_setup_platform(hass, config, async_add_entities, # Make all device entities aware of new telegram for device in devices: device.telegram = telegram - hass.async_add_job(device.async_update_ha_state()) + hass.async_create_task(device.async_update_ha_state()) # Creates an asyncio.Protocol factory for reading DSMR telegrams from # serial and calls update_entities_telegram to update entities on arrival @@ -182,13 +181,12 @@ def async_setup_platform(hass, config, async_add_entities, create_dsmr_reader, config[CONF_PORT], config[CONF_DSMR_VERSION], update_entities_telegram, loop=hass.loop) - @asyncio.coroutine - def connect_and_reconnect(): + async def connect_and_reconnect(): """Connect to DSMR and keep reconnecting until Home Assistant stops.""" while hass.state != CoreState.stopping: # Start DSMR asyncio.Protocol reader try: - transport, protocol = yield from hass.loop.create_task( + transport, protocol = await hass.loop.create_task( reader_factory()) except (serial.serialutil.SerialException, ConnectionRefusedError, TimeoutError): @@ -203,7 +201,7 @@ def async_setup_platform(hass, config, async_add_entities, EVENT_HOMEASSISTANT_STOP, transport.close) # Wait for reader to close - yield from protocol.wait_closed() + await protocol.wait_closed() if hass.state != CoreState.stopping: # Unexpected disconnect @@ -216,8 +214,8 @@ def async_setup_platform(hass, config, async_add_entities, update_entities_telegram({}) # throttle reconnect attempts - yield from asyncio.sleep(config[CONF_RECONNECT_INTERVAL], - loop=hass.loop) + await asyncio.sleep(config[CONF_RECONNECT_INTERVAL], + loop=hass.loop) # Can't be hass.async_add_job because job runs forever hass.loop.create_task(connect_and_reconnect()) @@ -309,8 +307,7 @@ class DerivativeDSMREntity(DSMREntity): """Return the calculated current hourly rate.""" return self._state - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Recalculate hourly rate if timestamp has changed. DSMR updates gas meter reading every hour. Along with the new diff --git a/homeassistant/components/sensor/dyson.py b/homeassistant/components/sensor/dyson.py index 4097bff32bf..53913d47b72 100644 --- a/homeassistant/components/sensor/dyson.py +++ b/homeassistant/components/sensor/dyson.py @@ -4,7 +4,6 @@ Support for Dyson Pure Cool Link Sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.dyson/ """ -import asyncio import logging from homeassistant.components.dyson import DYSON_DEVICES @@ -58,8 +57,7 @@ class DysonSensor(Entity): self._name = None self._sensor_type = sensor_type - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.async_add_job( self._device.add_message_listener, self.on_message) diff --git a/homeassistant/components/sensor/enphase_envoy.py b/homeassistant/components/sensor/enphase_envoy.py index 7f8cff0f885..4bbf7eec01b 100644 --- a/homeassistant/components/sensor/enphase_envoy.py +++ b/homeassistant/components/sensor/enphase_envoy.py @@ -14,7 +14,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import (CONF_IP_ADDRESS, CONF_MONITORED_CONDITIONS) -REQUIREMENTS = ['envoy_reader==0.2'] +REQUIREMENTS = ['envoy_reader==0.3'] _LOGGER = logging.getLogger(__name__) SENSORS = { diff --git a/homeassistant/components/sensor/envisalink.py b/homeassistant/components/sensor/envisalink.py index 91f99e31b48..aed2b056d2f 100644 --- a/homeassistant/components/sensor/envisalink.py +++ b/homeassistant/components/sensor/envisalink.py @@ -4,7 +4,6 @@ Support for Envisalink sensors (shows panel info). For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.envisalink/ """ -import asyncio import logging from homeassistant.core import callback @@ -19,9 +18,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['envisalink'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Perform the setup for Envisalink sensor devices.""" configured_partitions = discovery_info['partitions'] @@ -51,8 +49,7 @@ class EnvisalinkSensor(EnvisalinkDevice, Entity): _LOGGER.debug("Setting up sensor for partition: %s", partition_name) super().__init__(partition_name + ' Keypad', info, controller) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" async_dispatcher_connect( self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback) diff --git a/homeassistant/components/sensor/fail2ban.py b/homeassistant/components/sensor/fail2ban.py index 0f018af819d..f152a43e241 100644 --- a/homeassistant/components/sensor/fail2ban.py +++ b/homeassistant/components/sensor/fail2ban.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.fail2ban/ """ import os -import asyncio import logging from datetime import timedelta @@ -39,9 +38,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the fail2ban sensor.""" name = config.get(CONF_NAME) jails = config.get(CONF_JAILS) diff --git a/homeassistant/components/sensor/fastdotcom.py b/homeassistant/components/sensor/fastdotcom.py index 6624265f60c..c6a56701f7c 100644 --- a/homeassistant/components/sensor/fastdotcom.py +++ b/homeassistant/components/sensor/fastdotcom.py @@ -4,7 +4,6 @@ Support for Fast.com internet speed testing sensor. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.fastdotcom/ """ -import asyncio import logging import voluptuous as vol @@ -88,10 +87,9 @@ class SpeedtestSensor(Entity): self._state = data['download'] - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Handle entity which will be added.""" - state = yield from async_get_last_state(self.hass, self.entity_id) + state = await async_get_last_state(self.hass, self.entity_id) if not state: return self._state = state.state diff --git a/homeassistant/components/sensor/fido.py b/homeassistant/components/sensor/fido.py index 4c027b906a2..00754c5ba68 100644 --- a/homeassistant/components/sensor/fido.py +++ b/homeassistant/components/sensor/fido.py @@ -7,7 +7,6 @@ https://www.fido.ca/pages/#/my-account/wireless For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.fido/ """ -import asyncio import logging from datetime import timedelta @@ -70,16 +69,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Fido sensor.""" username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) httpsession = hass.helpers.aiohttp_client.async_get_clientsession() fido_data = FidoData(username, password, httpsession) - ret = yield from fido_data.async_update() + ret = await fido_data.async_update() if ret is False: return @@ -134,10 +132,9 @@ class FidoSensor(Entity): 'number': self._number, } - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data from Fido and update the state.""" - yield from self.fido_data.async_update() + await self.fido_data.async_update() if self.type == 'balance': if self.fido_data.data.get(self.type) is not None: self._state = round(self.fido_data.data[self.type], 2) diff --git a/homeassistant/components/sensor/file.py b/homeassistant/components/sensor/file.py index 1839b3566ee..3e2a5c21be8 100644 --- a/homeassistant/components/sensor/file.py +++ b/homeassistant/components/sensor/file.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.file/ """ import os -import asyncio import logging import voluptuous as vol @@ -32,9 +31,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the file sensor.""" file_path = config.get(CONF_FILE_PATH) name = config.get(CONF_NAME) diff --git a/homeassistant/components/sensor/geo_rss_events.py b/homeassistant/components/sensor/geo_rss_events.py index 1ba0ce2e065..5085e113e92 100644 --- a/homeassistant/components/sensor/geo_rss_events.py +++ b/homeassistant/components/sensor/geo_rss_events.py @@ -9,7 +9,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.geo_rss_events/ """ import logging -from collections import namedtuple from datetime import timedelta import voluptuous as vol @@ -19,9 +18,8 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_RADIUS, CONF_URL) from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle -REQUIREMENTS = ['feedparser==5.2.1', 'haversine==0.4.5'] +REQUIREMENTS = ['georss_client==0.3'] _LOGGER = logging.getLogger(__name__) @@ -38,9 +36,6 @@ DEFAULT_UNIT_OF_MEASUREMENT = 'Events' DOMAIN = 'geo_rss_events' -# Minimum time between updates from the source. -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) - SCAN_INTERVAL = timedelta(minutes=5) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -67,18 +62,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.debug("latitude=%s, longitude=%s, url=%s, radius=%s", home_latitude, home_longitude, url, radius_in_km) - # Initialise update service. - data = GeoRssServiceData(home_latitude, home_longitude, url, radius_in_km) - data.update() - # Create all sensors based on categories. devices = [] if not categories: - device = GeoRssServiceSensor(None, data, name, unit_of_measurement) + device = GeoRssServiceSensor((home_latitude, home_longitude), url, + radius_in_km, None, name, + unit_of_measurement) devices.append(device) else: for category in categories: - device = GeoRssServiceSensor(category, data, name, + device = GeoRssServiceSensor((home_latitude, home_longitude), url, + radius_in_km, category, name, unit_of_measurement) devices.append(device) add_entities(devices, True) @@ -87,14 +81,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class GeoRssServiceSensor(Entity): """Representation of a Sensor.""" - def __init__(self, category, data, service_name, unit_of_measurement): + def __init__(self, home_coordinates, url, radius, category, service_name, + unit_of_measurement): """Initialize the sensor.""" self._category = category - self._data = data self._service_name = service_name self._state = STATE_UNKNOWN self._state_attributes = None self._unit_of_measurement = unit_of_measurement + from georss_client.generic_feed import GenericFeed + self._feed = GenericFeed(home_coordinates, url, filter_radius=radius, + filter_categories=None if not category + else [category]) @property def name(self): @@ -125,115 +123,25 @@ class GeoRssServiceSensor(Entity): def update(self): """Update this sensor from the GeoRSS service.""" - _LOGGER.debug("About to update sensor %s", self.entity_id) - self._data.update() - # If no events were found due to an error then just set state to zero. - if self._data.events is None: - self._state = 0 - else: - if self._category is None: - # Add all events regardless of category. - my_events = self._data.events - else: - # Only keep events that belong to sensor's category. - my_events = [event for event in self._data.events if - event[ATTR_CATEGORY] == self._category] + import georss_client + status, feed_entries = self._feed.update() + if status == georss_client.UPDATE_OK: _LOGGER.debug("Adding events to sensor %s: %s", self.entity_id, - my_events) - self._state = len(my_events) + feed_entries) + self._state = len(feed_entries) # And now compute the attributes from the filtered events. matrix = {} - for event in my_events: - matrix[event[ATTR_TITLE]] = '{:.0f}km'.format( - event[ATTR_DISTANCE]) + for entry in feed_entries: + matrix[entry.title] = '{:.0f}km'.format( + entry.distance_to_home) self._state_attributes = matrix - - -class GeoRssServiceData: - """Provide access to GeoRSS feed and stores the latest data.""" - - def __init__(self, home_latitude, home_longitude, url, radius_in_km): - """Initialize the update service.""" - self._home_coordinates = [home_latitude, home_longitude] - self._url = url - self._radius_in_km = radius_in_km - self.events = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Retrieve data from GeoRSS feed and store events.""" - import feedparser - feed_data = feedparser.parse(self._url) - if not feed_data: - _LOGGER.error("Error fetching feed data from %s", self._url) + elif status == georss_client.UPDATE_OK_NO_DATA: + _LOGGER.debug("Update successful, but no data received from %s", + self._feed) + # Don't change the state or state attributes. else: - events = self.filter_entries(feed_data) - self.events = events - - def filter_entries(self, feed_data): - """Filter entries by distance from home coordinates.""" - events = [] - _LOGGER.debug("%s entri(es) available in feed %s", - len(feed_data.entries), self._url) - for entry in feed_data.entries: - geometry = None - if hasattr(entry, 'where'): - geometry = entry.where - elif hasattr(entry, 'geo_lat') and hasattr(entry, 'geo_long'): - coordinates = (float(entry.geo_long), float(entry.geo_lat)) - point = namedtuple('Point', ['type', 'coordinates']) - geometry = point('Point', coordinates) - if geometry: - distance = self.calculate_distance_to_geometry(geometry) - if distance <= self._radius_in_km: - event = { - ATTR_CATEGORY: None if not hasattr( - entry, 'category') else entry.category, - ATTR_TITLE: None if not hasattr( - entry, 'title') else entry.title, - ATTR_DISTANCE: distance - } - events.append(event) - _LOGGER.debug("%s events found nearby", len(events)) - return events - - def calculate_distance_to_geometry(self, geometry): - """Calculate the distance between HA and provided geometry.""" - distance = float("inf") - if geometry.type == 'Point': - distance = self.calculate_distance_to_point(geometry) - elif geometry.type == 'Polygon': - distance = self.calculate_distance_to_polygon( - geometry.coordinates[0]) - else: - _LOGGER.warning("Not yet implemented: %s", geometry.type) - return distance - - def calculate_distance_to_point(self, point): - """Calculate the distance between HA and the provided point.""" - # Swap coordinates to match: (lat, lon). - coordinates = (point.coordinates[1], point.coordinates[0]) - return self.calculate_distance_to_coords(coordinates) - - def calculate_distance_to_coords(self, coordinates): - """Calculate the distance between HA and the provided coordinates.""" - # Expecting coordinates in format: (lat, lon). - from haversine import haversine - distance = haversine(coordinates, self._home_coordinates) - _LOGGER.debug("Distance from %s to %s: %s km", self._home_coordinates, - coordinates, distance) - return distance - - def calculate_distance_to_polygon(self, polygon): - """Calculate the distance between HA and the provided polygon.""" - distance = float("inf") - # Calculate distance from polygon by calculating the distance - # to each point of the polygon but not to each edge of the - # polygon; should be good enough - for polygon_point in polygon: - coordinates = (polygon_point[1], polygon_point[0]) - distance = min(distance, - self.calculate_distance_to_coords(coordinates)) - _LOGGER.debug("Distance from %s to %s: %s km", self._home_coordinates, - polygon, distance) - return distance + _LOGGER.warning("Update not successful, no data received from %s", + self._feed) + # If no events were found due to an error then just set state to + # zero. + self._state = 0 diff --git a/homeassistant/components/sensor/gitlab_ci.py b/homeassistant/components/sensor/gitlab_ci.py new file mode 100644 index 00000000000..ceb5f75cace --- /dev/null +++ b/homeassistant/components/sensor/gitlab_ci.py @@ -0,0 +1,174 @@ +"""Module for retrieving latest GitLab CI job information.""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_NAME, CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_URL) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +CONF_GITLAB_ID = 'gitlab_id' +CONF_ATTRIBUTION = "Information provided by https://gitlab.com/" + +ICON_HAPPY = 'mdi:emoticon-happy' +ICON_SAD = 'mdi:emoticon-happy' +ICON_OTHER = 'mdi:git' + +ATTR_BUILD_ID = 'build id' +ATTR_BUILD_STATUS = 'build_status' +ATTR_BUILD_STARTED = 'build_started' +ATTR_BUILD_FINISHED = 'build_finished' +ATTR_BUILD_DURATION = 'build_duration' +ATTR_BUILD_COMMIT_ID = 'commit id' +ATTR_BUILD_COMMIT_DATE = 'commit date' +ATTR_BUILD_BRANCH = 'build branch' + +SCAN_INTERVAL = timedelta(seconds=300) + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_TOKEN): cv.string, + vol.Required(CONF_GITLAB_ID): cv.string, + vol.Optional(CONF_NAME, default='GitLab CI Status'): cv.string, + vol.Optional(CONF_URL, default='https://gitlab.com'): cv.string +}) + +REQUIREMENTS = ['python-gitlab==1.6.0'] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Sensor platform setup.""" + _name = config.get(CONF_NAME) + _interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + _url = config.get(CONF_URL) + + _gitlab_data = GitLabData( + priv_token=config[CONF_TOKEN], + gitlab_id=config[CONF_GITLAB_ID], + interval=_interval, + url=_url + ) + + add_entities([GitLabSensor(_gitlab_data, _name)], True) + + +class GitLabSensor(Entity): + """Representation of a Sensor.""" + + def __init__(self, gitlab_data, name): + """Initialize the sensor.""" + self._available = False + self._state = None + self._started_at = None + self._finished_at = None + self._duration = None + self._commit_id = None + self._commit_date = None + self._build_id = None + self._branch = None + self._gitlab_data = gitlab_data + self._name = name + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def available(self): + """Return True if entity is available.""" + return self._available + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_BUILD_STATUS: self._state, + ATTR_BUILD_STARTED: self._started_at, + ATTR_BUILD_FINISHED: self._finished_at, + ATTR_BUILD_DURATION: self._duration, + ATTR_BUILD_COMMIT_ID: self._commit_id, + ATTR_BUILD_COMMIT_DATE: self._commit_date, + ATTR_BUILD_ID: self._build_id, + ATTR_BUILD_BRANCH: self._branch + } + + @property + def icon(self): + """Return the icon to use in the frontend.""" + if self._state == 'success': + return ICON_HAPPY + if self._state == 'failed': + return ICON_SAD + return ICON_OTHER + + def update(self): + """Collect updated data from GitLab API.""" + self._gitlab_data.update() + + self._state = self._gitlab_data.status + self._started_at = self._gitlab_data.started_at + self._finished_at = self._gitlab_data.finished_at + self._duration = self._gitlab_data.duration + self._commit_id = self._gitlab_data.commit_id + self._commit_date = self._gitlab_data.commit_date + self._build_id = self._gitlab_data.build_id + self._branch = self._gitlab_data.branch + self._available = self._gitlab_data.available + + +class GitLabData(): + """GitLab Data object.""" + + def __init__(self, gitlab_id, priv_token, interval, url): + """Fetch data from GitLab API for most recent CI job.""" + import gitlab + self._gitlab_id = gitlab_id + self._gitlab = gitlab.Gitlab( + url, private_token=priv_token, per_page=1) + self._gitlab.auth() + self._gitlab_exceptions = gitlab.exceptions + self.update = Throttle(interval)(self._update) + + self.available = False + self.status = None + self.started_at = None + self.finished_at = None + self.duration = None + self.commit_id = None + self.commit_date = None + self.build_id = None + self.branch = None + + def _update(self): + try: + _projects = self._gitlab.projects.get(self._gitlab_id) + _last_pipeline = _projects.pipelines.list(page=1)[0] + _last_job = _last_pipeline.jobs.list(page=1)[0] + self.status = _last_pipeline.attributes.get('status') + self.started_at = _last_job.attributes.get('started_at') + self.finished_at = _last_job.attributes.get('finished_at') + self.duration = _last_job.attributes.get('duration') + _commit = _last_job.attributes.get('commit') + self.commit_id = _commit.get('id') + self.commit_date = _commit.get('committed_date') + self.build_id = _last_job.attributes.get('id') + self.branch = _last_job.attributes.get('ref') + self.available = True + except self._gitlab_exceptions.GitlabAuthenticationError as erra: + _LOGGER.error("Authentication Error: %s", erra) + self.available = False + except self._gitlab_exceptions.GitlabGetError as errg: + _LOGGER.error("Project Not Found: %s", errg) + self.available = False diff --git a/homeassistant/components/sensor/google_travel_time.py b/homeassistant/components/sensor/google_travel_time.py index a69b865f30b..6c197475653 100644 --- a/homeassistant/components/sensor/google_travel_time.py +++ b/homeassistant/components/sensor/google_travel_time.py @@ -10,7 +10,7 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import DOMAIN, PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity from homeassistant.const import ( CONF_API_KEY, CONF_NAME, EVENT_HOMEASSISTANT_START, ATTR_LATITUDE, @@ -68,6 +68,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) TRACKABLE_DOMAINS = ['device_tracker', 'sensor', 'zone'] +DATA_KEY = 'google_travel_time' def convert_time_to_utc(timestr): @@ -90,6 +91,10 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): if options.get('units') is None: options['units'] = hass.config.units.name + if DATA_KEY not in hass.data: + hass.data[DATA_KEY] = [] + hass.services.register( + DOMAIN, 'google_travel_sensor_update', update) travel_mode = config.get(CONF_TRAVEL_MODE) mode = options.get(CONF_MODE) @@ -110,10 +115,19 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): sensor = GoogleTravelTimeSensor( hass, name, api_key, origin, destination, options) + hass.data[DATA_KEY].append(sensor) if sensor.valid_api_connection: add_entities_callback([sensor]) + def update(service): + """Update service for manual updates.""" + entity_id = service.data.get('entity_id') + for sensor in hass.data[DATA_KEY]: + if sensor.entity_id == entity_id: + sensor.update(no_throttle=True) + sensor.schedule_update_ha_state() + # Wait until start event is sent to load this component. hass.bus.listen_once(EVENT_HOMEASSISTANT_START, run_setup) diff --git a/homeassistant/components/sensor/htu21d.py b/homeassistant/components/sensor/htu21d.py index 28ab933ff6c..ae2555f57f9 100644 --- a/homeassistant/components/sensor/htu21d.py +++ b/homeassistant/components/sensor/htu21d.py @@ -4,7 +4,6 @@ Support for HTU21D temperature and humidity sensor. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.htu21d/ """ -import asyncio from datetime import timedelta from functools import partial import logging @@ -40,9 +39,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=import-error -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the HTU21D sensor.""" import smbus from i2csense.htu21d import HTU21D @@ -52,14 +50,14 @@ def async_setup_platform(hass, config, async_add_entities, temp_unit = hass.config.units.temperature_unit bus = smbus.SMBus(config.get(CONF_I2C_BUS)) - sensor = yield from hass.async_add_job( + sensor = await hass.async_add_job( partial(HTU21D, bus, logger=_LOGGER) ) if not sensor.sample_ok: _LOGGER.error("HTU21D sensor not detected in bus %s", bus_number) return False - sensor_handler = yield from hass.async_add_job(HTU21DHandler, sensor) + sensor_handler = await hass.async_add_job(HTU21DHandler, sensor) dev = [HTU21DSensor(sensor_handler, name, SENSOR_TEMPERATURE, temp_unit), HTU21DSensor(sensor_handler, name, SENSOR_HUMIDITY, '%')] @@ -107,10 +105,9 @@ class HTU21DSensor(Entity): """Return the unit of measurement of the sensor.""" return self._unit_of_measurement - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data from the HTU21D sensor and update the state.""" - yield from self.hass.async_add_job(self._client.update) + await self.hass.async_add_job(self._client.update) if self._client.sensor.sample_ok: if self._variable == SENSOR_TEMPERATURE: value = round(self._client.sensor.temperature, 1) diff --git a/homeassistant/components/sensor/hydroquebec.py b/homeassistant/components/sensor/hydroquebec.py index 44b96bab1e9..cb75e69b919 100644 --- a/homeassistant/components/sensor/hydroquebec.py +++ b/homeassistant/components/sensor/hydroquebec.py @@ -7,7 +7,6 @@ https://www.hydroquebec.com/portail/en/group/clientele/portrait-de-consommation For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.hydroquebec/ """ -import asyncio import logging from datetime import timedelta @@ -93,9 +92,8 @@ DAILY_MAP = (('yesterday_total_consumption', 'consoTotalQuot'), ('yesterday_higher_price_consumption', 'consoHautQuot')) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the HydroQuebec sensor.""" # Create a data fetcher to support all of the configured sensors. Then make # the first call to init the data. @@ -107,7 +105,7 @@ def async_setup_platform(hass, config, async_add_entities, httpsession = hass.helpers.aiohttp_client.async_get_clientsession() hydroquebec_data = HydroquebecData(username, password, httpsession, contract) - contracts = yield from hydroquebec_data.get_contract_list() + contracts = await hydroquebec_data.get_contract_list() if not contracts: return _LOGGER.info("Contract list: %s", @@ -155,10 +153,9 @@ class HydroQuebecSensor(Entity): """Icon to use in the frontend, if any.""" return self._icon - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data from Hydroquebec and update the state.""" - yield from self.hydroquebec_data.async_update() + await self.hydroquebec_data.async_update() if self.hydroquebec_data.data.get(self.type) is not None: self._state = round(self.hydroquebec_data.data[self.type], 2) @@ -174,11 +171,10 @@ class HydroquebecData: self._contract = contract self.data = {} - @asyncio.coroutine - def get_contract_list(self): + async def get_contract_list(self): """Return the contract list.""" # Fetch data - ret = yield from self._fetch_data() + ret = await self._fetch_data() if ret: return self.client.get_contracts() return [] @@ -194,8 +190,7 @@ class HydroquebecData: return False return True - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Return the latest collected data from HydroQuebec.""" - yield from self._fetch_data() + await self._fetch_data() self.data = self.client.get_data(self._contract)[self._contract] diff --git a/homeassistant/components/sensor/insteon.py b/homeassistant/components/sensor/insteon.py index 5b8a6b9a977..7854967395b 100644 --- a/homeassistant/components/sensor/insteon.py +++ b/homeassistant/components/sensor/insteon.py @@ -4,7 +4,6 @@ Support for INSTEON dimmers via PowerLinc Modem. For more details about this component, please refer to the documentation at https://home-assistant.io/components/sensor.insteon/ """ -import asyncio import logging from homeassistant.components.insteon import InsteonEntity @@ -15,9 +14,8 @@ DEPENDENCIES = ['insteon'] _LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the INSTEON device class for the hass platform.""" insteon_modem = hass.data['insteon'].get('modem') diff --git a/homeassistant/components/sensor/jewish_calendar.py b/homeassistant/components/sensor/jewish_calendar.py index e5838fa8543..1de45d6145e 100644 --- a/homeassistant/components/sensor/jewish_calendar.py +++ b/homeassistant/components/sensor/jewish_calendar.py @@ -64,7 +64,8 @@ async def async_setup_platform( dev = [] for sensor_type in config[CONF_SENSORS]: dev.append(JewishCalSensor( - name, language, sensor_type, latitude, longitude, diaspora)) + name, language, sensor_type, latitude, longitude, + hass.config.time_zone, diaspora)) async_add_entities(dev, True) @@ -72,7 +73,8 @@ class JewishCalSensor(Entity): """Representation of an Jewish calendar sensor.""" def __init__( - self, name, language, sensor_type, latitude, longitude, diaspora): + self, name, language, sensor_type, latitude, longitude, timezone, + diaspora): """Initialize the Jewish calendar sensor.""" self.client_name = name self._name = SENSOR_TYPES[sensor_type][0] @@ -81,6 +83,7 @@ class JewishCalSensor(Entity): self._state = None self.latitude = latitude self.longitude = longitude + self.timezone = timezone self.diaspora = diaspora _LOGGER.debug("Sensor %s initialized", self.type) @@ -116,10 +119,14 @@ class JewishCalSensor(Entity): date.get_reading(self.diaspora), hebrew=self._hebrew) elif self.type == 'holiday_name': try: - self._state = next( - x.description[self._hebrew].long + description = next( + x.description[self._hebrew] for x in hdate.htables.HOLIDAYS if x.index == date.get_holyday()) + if not self._hebrew: + self._state = description + else: + self._state = description.long except StopIteration: self._state = None elif self.type == 'holyness': @@ -127,7 +134,7 @@ class JewishCalSensor(Entity): else: times = hdate.Zmanim( date=today, latitude=self.latitude, longitude=self.longitude, - hebrew=self._hebrew).zmanim + timezone=self.timezone, hebrew=self._hebrew).zmanim self._state = times[self.type].time() _LOGGER.debug("New value: %s", self._state) diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py index 6f0fb3aba30..74bb8261609 100644 --- a/homeassistant/components/sensor/miflora.py +++ b/homeassistant/components/sensor/miflora.py @@ -4,20 +4,17 @@ Support for Xiaomi Mi Flora BLE plant sensor. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.miflora/ """ -import asyncio from datetime import timedelta import logging import voluptuous as vol -import async_timeout from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -from homeassistant.exceptions import PlatformNotReady from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC, - CONF_SCAN_INTERVAL) - + CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START) +from homeassistant.core import callback REQUIREMENTS = ['miflora==0.4.0'] @@ -75,13 +72,6 @@ async def async_setup_platform(hass, config, async_add_entities, devs = [] - try: - with async_timeout.timeout(9): - await hass.async_add_executor_job(poller.fill_cache) - except asyncio.TimeoutError: - _LOGGER.error('Unable to connect to %s', config.get(CONF_MAC)) - raise PlatformNotReady - for parameter in config[CONF_MONITORED_CONDITIONS]: name = SENSOR_TYPES[parameter][0] unit = SENSOR_TYPES[parameter][1] @@ -93,7 +83,7 @@ async def async_setup_platform(hass, config, async_add_entities, devs.append(MiFloraSensor( poller, parameter, name, unit, force_update, median)) - async_add_entities(devs, update_before_add=True) + async_add_entities(devs) class MiFloraSensor(Entity): @@ -113,6 +103,14 @@ class MiFloraSensor(Entity): # Use median_count = 1 if no filtering is required. self.median_count = median + async def async_added_to_hass(self): + """Set initial state.""" + @callback + def on_startup(_): + self.async_schedule_update_ha_state(True) + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, on_startup) + @property def name(self): """Return the name of the sensor.""" diff --git a/homeassistant/components/sensor/min_max.py b/homeassistant/components/sensor/min_max.py index 7956dd97b5e..7d9e91a1bf1 100644 --- a/homeassistant/components/sensor/min_max.py +++ b/homeassistant/components/sensor/min_max.py @@ -4,7 +4,6 @@ Support for displaying the minimal and the maximal value. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.min_max/ """ -import asyncio import logging import voluptuous as vol @@ -54,9 +53,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the min/max/mean sensor.""" entity_ids = config.get(CONF_ENTITY_IDS) name = config.get(CONF_NAME) @@ -194,8 +192,7 @@ class MinMaxSensor(Entity): """Return the icon to use in the frontend, if any.""" return ICON - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data and updates the states.""" sensor_values = [self.states[k] for k in self._entity_ids if k in self.states] diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index 6cf2d55755d..fe0b77b2024 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -12,9 +12,12 @@ from typing import Optional import voluptuous as vol from homeassistant.core import callback +from homeassistant.components import sensor from homeassistant.components.mqtt import ( - CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_PAYLOAD_AVAILABLE, - CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability) + ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, + CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, + MqttAvailability, MqttDiscoveryUpdate) +from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, @@ -23,6 +26,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.components import mqtt import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType, ConfigType +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util import dt as dt_util @@ -52,10 +56,26 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None): - """Set up MQTT Sensor.""" - if discovery_info is not None: - config = PLATFORM_SCHEMA(discovery_info) + """Set up MQTT sensors through configuration.yaml.""" + await _async_setup_entity(hass, config, async_add_entities) + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT sensors dynamically through MQTT discovery.""" + async def async_discover_sensor(discovery_payload): + """Discover and add a discovered MQTT sensor.""" + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(hass, config, async_add_entities, + discovery_payload[ATTR_DISCOVERY_HASH]) + + async_dispatcher_connect(hass, + MQTT_DISCOVERY_NEW.format(sensor.DOMAIN, 'mqtt'), + async_discover_sensor) + + +async def _async_setup_entity(hass: HomeAssistantType, config: ConfigType, + async_add_entities, discovery_hash=None): + """Set up MQTT sensor.""" value_template = config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = hass @@ -75,20 +95,22 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE), + discovery_hash, )]) -class MqttSensor(MqttAvailability, Entity): +class MqttSensor(MqttAvailability, MqttDiscoveryUpdate, Entity): """Representation of a sensor that can be updated using MQTT.""" def __init__(self, name, state_topic, qos, unit_of_measurement, force_update, expire_after, icon, device_class: Optional[str], value_template, json_attributes, unique_id: Optional[str], - availability_topic, payload_available, - payload_not_available): + availability_topic, payload_available, payload_not_available, + discovery_hash): """Initialize the sensor.""" - super().__init__(availability_topic, qos, payload_available, - payload_not_available) + MqttAvailability.__init__(self, availability_topic, qos, + payload_available, payload_not_available) + MqttDiscoveryUpdate.__init__(self, discovery_hash) self._state = STATE_UNKNOWN self._name = name self._state_topic = state_topic @@ -103,10 +125,12 @@ class MqttSensor(MqttAvailability, Entity): self._json_attributes = set(json_attributes) self._unique_id = unique_id self._attributes = None + self._discovery_hash = discovery_hash async def async_added_to_hass(self): """Subscribe to MQTT events.""" - await super().async_added_to_hass() + await MqttAvailability.async_added_to_hass(self) + await MqttDiscoveryUpdate.async_added_to_hass(self) @callback def message_received(topic, payload, qos): diff --git a/homeassistant/components/sensor/mqtt_room.py b/homeassistant/components/sensor/mqtt_room.py index e12e8e033ac..39c202ef01c 100644 --- a/homeassistant/components/sensor/mqtt_room.py +++ b/homeassistant/components/sensor/mqtt_room.py @@ -4,7 +4,6 @@ Support for MQTT room presence detection. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.mqtt_room/ """ -import asyncio import logging import json from datetime import timedelta @@ -52,9 +51,8 @@ MQTT_PAYLOAD = vol.Schema(vol.All(json.loads, vol.Schema({ }, extra=vol.ALLOW_EXTRA))) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up MQTT room Sensor.""" async_add_entities([MQTTRoomSensor( config.get(CONF_NAME), @@ -81,8 +79,7 @@ class MQTTRoomSensor(Entity): self._distance = None self._updated = None - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to MQTT events.""" @callback def update_state(device_id, room, distance): @@ -118,7 +115,7 @@ class MQTTRoomSensor(Entity): or timediff.seconds >= self._timeout: update_state(**device) - return mqtt.async_subscribe( + return await mqtt.async_subscribe( self.hass, self._state_topic, message_received, 1) @property diff --git a/homeassistant/components/sensor/mychevy.py b/homeassistant/components/sensor/mychevy.py index fa3343d7791..06d4dade6aa 100644 --- a/homeassistant/components/sensor/mychevy.py +++ b/homeassistant/components/sensor/mychevy.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.mychevy/ """ -import asyncio import logging from homeassistant.components.mychevy import ( @@ -55,8 +54,7 @@ class MyChevyStatus(Entity): """Initialize sensor with car connection.""" self._state = None - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( UPDATE_TOPIC, self.success) @@ -129,8 +127,7 @@ class EVSensor(Entity): slugify(self._car.name), slugify(self._name))) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( UPDATE_TOPIC, self.async_update_callback) diff --git a/homeassistant/components/sensor/netatmo_public.py b/homeassistant/components/sensor/netatmo_public.py index d1c6e03d1b0..7a500b66183 100644 --- a/homeassistant/components/sensor/netatmo_public.py +++ b/homeassistant/components/sensor/netatmo_public.py @@ -10,7 +10,9 @@ import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_NAME, CONF_TYPE) +from homeassistant.const import ( + CONF_NAME, CONF_MODE, CONF_MONITORED_CONDITIONS, TEMP_CELSIUS, + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -26,13 +28,22 @@ CONF_LAT_SW = 'lat_sw' CONF_LON_SW = 'lon_sw' DEFAULT_NAME = 'Netatmo Public Data' -DEFAULT_TYPE = 'max' -SENSOR_TYPES = {'max', 'avg'} +DEFAULT_MODE = 'avg' +MODE_TYPES = {'max', 'avg'} + +SENSOR_TYPES = { + 'temperature': ['Temperature', TEMP_CELSIUS, 'mdi:thermometer', + DEVICE_CLASS_TEMPERATURE], + 'pressure': ['Pressure', 'mbar', 'mdi:gauge', None], + 'humidity': ['Humidity', '%', 'mdi:water-percent', DEVICE_CLASS_HUMIDITY], + 'rain': ['Rain', 'mm', 'mdi:weather-rainy', None], + 'windstrength': ['Wind Strength', 'km/h', 'mdi:weather-windy', None], + 'guststrength': ['Gust Strength', 'km/h', 'mdi:weather-windy', None], +} # NetAtmo Data is uploaded to server every 10 minutes MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=600) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_AREAS): vol.All(cv.ensure_list, [ { @@ -40,9 +51,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_LAT_SW): cv.latitude, vol.Required(CONF_LON_NE): cv.longitude, vol.Required(CONF_LON_SW): cv.longitude, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_TYPE, default=DEFAULT_TYPE): - vol.In(SENSOR_TYPES) + vol.Required(CONF_MONITORED_CONDITIONS): [vol.In(SENSOR_TYPES)], + vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.In(MODE_TYPES), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string } ]), }) @@ -59,20 +70,29 @@ def setup_platform(hass, config, add_entities, discovery_info=None): lat_ne=area_conf.get(CONF_LAT_NE), lon_ne=area_conf.get(CONF_LON_NE), lat_sw=area_conf.get(CONF_LAT_SW), - lon_sw=area_conf.get(CONF_LON_SW), - calculation=area_conf.get(CONF_TYPE)) - sensors.append(NetatmoPublicSensor(area_conf.get(CONF_NAME), data)) - add_entities(sensors) + lon_sw=area_conf.get(CONF_LON_SW)) + for sensor_type in area_conf.get(CONF_MONITORED_CONDITIONS): + sensors.append(NetatmoPublicSensor(area_conf.get(CONF_NAME), + data, sensor_type, + area_conf.get(CONF_MODE))) + add_entities(sensors, True) class NetatmoPublicSensor(Entity): """Represent a single sensor in a Netatmo.""" - def __init__(self, name, data): + def __init__(self, area_name, data, sensor_type, mode): """Initialize the sensor.""" self.netatmo_data = data - self._name = name + self.type = sensor_type + self._mode = mode + self._name = '{} {}'.format(area_name, + SENSOR_TYPES[self.type][0]) + self._area_name = area_name self._state = None + self._device_class = SENSOR_TYPES[self.type][3] + self._icon = SENSOR_TYPES[self.type][2] + self._unit_of_measurement = SENSOR_TYPES[self.type][1] @property def name(self): @@ -82,33 +102,63 @@ class NetatmoPublicSensor(Entity): @property def icon(self): """Icon to use in the frontend.""" - return 'mdi:weather-rainy' + return self._icon @property def device_class(self): """Return the device class of the sensor.""" - return None + return self._device_class @property def state(self): - """Return true if binary sensor is on.""" + """Return the state of the device.""" return self._state @property def unit_of_measurement(self): """Return the unit of measurement of this entity.""" - return 'mm' + return self._unit_of_measurement def update(self): """Get the latest data from NetAtmo API and updates the states.""" self.netatmo_data.update() - self._state = self.netatmo_data.data + + if self.netatmo_data.data is None: + _LOGGER.warning("No data found for %s", self._name) + self._state = None + return + + data = None + + if self.type == 'temperature': + data = self.netatmo_data.data.getLatestTemperatures() + elif self.type == 'pressure': + data = self.netatmo_data.data.getLatestPressures() + elif self.type == 'humidity': + data = self.netatmo_data.data.getLatestHumidities() + elif self.type == 'rain': + data = self.netatmo_data.data.getLatestRain() + elif self.type == 'windstrength': + data = self.netatmo_data.data.getLatestWindStrengths() + elif self.type == 'guststrength': + data = self.netatmo_data.data.getLatestGustStrengths() + + if not data: + _LOGGER.warning("No station provides %s data in the area %s", + self.type, self._area_name) + self._state = None + return + + if self._mode == 'avg': + self._state = round(sum(data.values()) / len(data), 1) + elif self._mode == 'max': + self._state = max(data.values()) class NetatmoPublicData: """Get the latest data from NetAtmo.""" - def __init__(self, auth, lat_ne, lon_ne, lat_sw, lon_sw, calculation): + def __init__(self, auth, lat_ne, lon_ne, lat_sw, lon_sw): """Initialize the data object.""" self.auth = auth self.data = None @@ -116,26 +166,20 @@ class NetatmoPublicData: self.lon_ne = lon_ne self.lat_sw = lat_sw self.lon_sw = lon_sw - self.calculation = calculation @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Request an update from the Netatmo API.""" import pyatmo - raindata = pyatmo.PublicData(self.auth, - LAT_NE=self.lat_ne, - LON_NE=self.lon_ne, - LAT_SW=self.lat_sw, - LON_SW=self.lon_sw, - required_data_type="rain") + data = pyatmo.PublicData(self.auth, + LAT_NE=self.lat_ne, + LON_NE=self.lon_ne, + LAT_SW=self.lat_sw, + LON_SW=self.lon_sw, + filtering=True) - if raindata.CountStationInArea() == 0: - _LOGGER.warning('No Rain Station available in this area.') + if data.CountStationInArea() == 0: + _LOGGER.warning('No Stations available in this area.') return - raindata_live = raindata.getLive() - - if self.calculation == 'avg': - self.data = sum(raindata_live.values()) / len(raindata_live) - else: - self.data = max(raindata_live.values()) + self.data = data diff --git a/homeassistant/components/sensor/openweathermap.py b/homeassistant/components/sensor/openweathermap.py index 2dbbb581741..08426ed3eb8 100644 --- a/homeassistant/components/sensor/openweathermap.py +++ b/homeassistant/components/sensor/openweathermap.py @@ -39,6 +39,7 @@ SENSOR_TYPES = { 'clouds': ['Cloud coverage', '%'], 'rain': ['Rain', 'mm'], 'snow': ['Snow', 'mm'], + 'weather_code': ['Weather code', None], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -177,6 +178,8 @@ class OpenWeatherMapSensor(Entity): if fc_data is None: return self._state = fc_data.get_weathers()[0].get_detailed_status() + elif self.type == 'weather_code': + self._state = data.get_weather_code() class WeatherData: diff --git a/homeassistant/components/sensor/otp.py b/homeassistant/components/sensor/otp.py index 2e3a13928e1..5394b49c389 100644 --- a/homeassistant/components/sensor/otp.py +++ b/homeassistant/components/sensor/otp.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.otp/ """ import time -import asyncio import logging import voluptuous as vol @@ -32,9 +31,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the OTP sensor.""" name = config.get(CONF_NAME) token = config.get(CONF_TOKEN) @@ -55,8 +53,7 @@ class TOTPSensor(Entity): self._state = None self._next_expiration = None - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Handle when an entity is about to be added to Home Assistant.""" self._call_loop() diff --git a/homeassistant/components/sensor/rflink.py b/homeassistant/components/sensor/rflink.py index 3952c815dca..e01c441be84 100644 --- a/homeassistant/components/sensor/rflink.py +++ b/homeassistant/components/sensor/rflink.py @@ -4,7 +4,6 @@ Support for Rflink sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.rflink/ """ -import asyncio from functools import partial import logging @@ -74,14 +73,12 @@ def devices_from_config(domain_config, hass=None): return devices -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Rflink platform.""" async_add_entities(devices_from_config(config, hass)) - @asyncio.coroutine - def add_new_device(event): + async def add_new_device(event): """Check if device is known, otherwise create device entity.""" device_id = event[EVENT_KEY_ID] diff --git a/homeassistant/components/sensor/scrape.py b/homeassistant/components/sensor/scrape.py index 9a43c3ff295..0174407c7c3 100644 --- a/homeassistant/components/sensor/scrape.py +++ b/homeassistant/components/sensor/scrape.py @@ -13,7 +13,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.sensor.rest import RestData from homeassistant.const import ( CONF_NAME, CONF_RESOURCE, CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, - CONF_VALUE_TEMPLATE, CONF_VERIFY_SSL, CONF_USERNAME, + CONF_VALUE_TEMPLATE, CONF_VERIFY_SSL, CONF_USERNAME, CONF_HEADERS, CONF_PASSWORD, CONF_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION) from homeassistant.helpers.entity import Entity @@ -35,6 +35,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_ATTR): cv.string, vol.Optional(CONF_AUTHENTICATION): vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]), + vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.string}), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, @@ -49,7 +50,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): name = config.get(CONF_NAME) resource = config.get(CONF_RESOURCE) method = 'GET' - payload = headers = None + payload = None + headers = config.get(CONF_HEADERS) verify_ssl = config.get(CONF_VERIFY_SSL) select = config.get(CONF_SELECT) attr = config.get(CONF_ATTR) diff --git a/homeassistant/components/sensor/serial.py b/homeassistant/components/sensor/serial.py index 39b69e0a8c4..5d49b065558 100644 --- a/homeassistant/components/sensor/serial.py +++ b/homeassistant/components/sensor/serial.py @@ -4,7 +4,6 @@ Support for reading data from a serial port. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.serial/ """ -import asyncio import logging import json @@ -35,9 +34,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Serial sensor platform.""" name = config.get(CONF_NAME) port = config.get(CONF_SERIAL_PORT) @@ -67,20 +65,18 @@ class SerialSensor(Entity): self._template = value_template self._attributes = [] - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Handle when an entity is about to be added to Home Assistant.""" self._serial_loop_task = self.hass.loop.create_task( self.serial_read(self._port, self._baudrate)) - @asyncio.coroutine - def serial_read(self, device, rate, **kwargs): + async def serial_read(self, device, rate, **kwargs): """Read the data from the port.""" import serial_asyncio - reader, _ = yield from serial_asyncio.open_serial_connection( + reader, _ = await serial_asyncio.open_serial_connection( url=device, baudrate=rate, **kwargs) while True: - line = yield from reader.readline() + line = await reader.readline() line = line.decode('utf-8').strip() try: @@ -98,8 +94,7 @@ class SerialSensor(Entity): self._state = line self.async_schedule_update_ha_state() - @asyncio.coroutine - def stop_serial_read(self): + async def stop_serial_read(self): """Close resources.""" if self._serial_loop_task: self._serial_loop_task.cancel() diff --git a/homeassistant/components/sensor/sma.py b/homeassistant/components/sensor/sma.py index 945c3873bb6..dd5209a4e0a 100644 --- a/homeassistant/components/sensor/sma.py +++ b/homeassistant/components/sensor/sma.py @@ -63,9 +63,8 @@ PLATFORM_SCHEMA = vol.All(PLATFORM_SCHEMA.extend({ }, extra=vol.PREVENT_EXTRA), _check_sensor_schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up SMA WebConnect sensor.""" import pysma @@ -107,10 +106,9 @@ def async_setup_platform(hass, config, async_add_entities, sma = pysma.SMA(session, url, config[CONF_PASSWORD], group=grp) # Ensure we logout on shutdown - @asyncio.coroutine - def async_close_session(event): + async def async_close_session(event): """Close the session.""" - yield from sma.close_session() + await sma.close_session() hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, async_close_session) @@ -120,15 +118,14 @@ def async_setup_platform(hass, config, async_add_entities, backoff = 0 - @asyncio.coroutine - def async_sma(event): + async def async_sma(event): """Update all the SMA sensors.""" nonlocal backoff if backoff > 1: backoff -= 1 return - values = yield from sma.read(keys_to_query) + values = await sma.read(keys_to_query) if values is None: backoff = 3 return @@ -142,7 +139,7 @@ def async_setup_platform(hass, config, async_add_entities, if task: tasks.append(task) if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) interval = config.get(CONF_SCAN_INTERVAL) or timedelta(seconds=5) async_track_time_interval(hass, async_sma, interval) diff --git a/homeassistant/components/sensor/sochain.py b/homeassistant/components/sensor/sochain.py index 9f8982f871f..b582ba04567 100644 --- a/homeassistant/components/sensor/sochain.py +++ b/homeassistant/components/sensor/sochain.py @@ -4,7 +4,6 @@ Support for watching multiple cryptocurrencies. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.sochain/ """ -import asyncio import logging from datetime import timedelta @@ -35,9 +34,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the sochain sensors.""" from pysochain import ChainSo address = config.get(CONF_ADDRESS) @@ -82,7 +80,6 @@ class SochainSensor(Entity): ATTR_ATTRIBUTION: CONF_ATTRIBUTION, } - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest state of the sensor.""" - yield from self.chainso.async_get_data() + await self.chainso.async_get_data() diff --git a/homeassistant/components/sensor/speedtest.py b/homeassistant/components/sensor/speedtest.py index 8da7374f231..ee6cad61e20 100644 --- a/homeassistant/components/sensor/speedtest.py +++ b/homeassistant/components/sensor/speedtest.py @@ -4,7 +4,6 @@ Support for Speedtest.net. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.speedtest/ """ -import asyncio import logging import voluptuous as vol @@ -139,10 +138,9 @@ class SpeedtestSensor(Entity): elif self.type == 'upload': self._state = round(self._data['upload'] / 10**6, 2) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Handle all entity which are about to be added.""" - state = yield from async_get_last_state(self.hass, self.entity_id) + state = await async_get_last_state(self.hass, self.entity_id) if not state: return self._state = state.state diff --git a/homeassistant/components/sensor/startca.py b/homeassistant/components/sensor/startca.py index d9a52e4aa23..85939ea72ae 100644 --- a/homeassistant/components/sensor/startca.py +++ b/homeassistant/components/sensor/startca.py @@ -7,7 +7,6 @@ https://home-assistant.io/components/sensor.startca/ from datetime import timedelta from xml.parsers.expat import ExpatError import logging -import asyncio import async_timeout import voluptuous as vol @@ -57,16 +56,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the sensor platform.""" websession = async_get_clientsession(hass) apikey = config.get(CONF_API_KEY) bandwidthcap = config.get(CONF_TOTAL_BANDWIDTH) ts_data = StartcaData(hass.loop, websession, apikey, bandwidthcap) - ret = yield from ts_data.async_update() + ret = await ts_data.async_update() if ret is False: _LOGGER.error("Invalid Start.ca API key: %s", apikey) return @@ -111,10 +109,9 @@ class StartcaSensor(Entity): """Icon to use in the frontend, if any.""" return self._icon - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data from Start.ca and update the state.""" - yield from self.startcadata.async_update() + await self.startcadata.async_update() if self.type in self.startcadata.data: self._state = round(self.startcadata.data[self.type], 2) diff --git a/homeassistant/components/sensor/statistics.py b/homeassistant/components/sensor/statistics.py index e7692001ffa..453acb94b11 100644 --- a/homeassistant/components/sensor/statistics.py +++ b/homeassistant/components/sensor/statistics.py @@ -4,7 +4,6 @@ Support for statistics for sensor values. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.statistics/ """ -import asyncio import logging import statistics from collections import deque @@ -58,9 +57,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Statistics sensor.""" entity_id = config.get(CONF_ENTITY_ID) name = config.get(CONF_NAME) @@ -179,8 +177,7 @@ class StatisticsSensor(Entity): self.ages.popleft() self.states.popleft() - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data and updates the states.""" if self._max_age is not None: self._purge_old() @@ -236,8 +233,7 @@ class StatisticsSensor(Entity): self.change = self.average_change = STATE_UNKNOWN self.change_rate = STATE_UNKNOWN - @asyncio.coroutine - def _initialize_from_database(self): + async def _initialize_from_database(self): """Initialize the list of states from the database. The query will get the list of states in DESCENDING order so that we diff --git a/homeassistant/components/sensor/teksavvy.py b/homeassistant/components/sensor/teksavvy.py index 87b89074cfb..0be18cbd6b6 100644 --- a/homeassistant/components/sensor/teksavvy.py +++ b/homeassistant/components/sensor/teksavvy.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/sensor.teksavvy/ """ from datetime import timedelta import logging -import asyncio import async_timeout import voluptuous as vol @@ -58,16 +57,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the sensor platform.""" websession = async_get_clientsession(hass) apikey = config.get(CONF_API_KEY) bandwidthcap = config.get(CONF_TOTAL_BANDWIDTH) ts_data = TekSavvyData(hass.loop, websession, apikey, bandwidthcap) - ret = yield from ts_data.async_update() + ret = await ts_data.async_update() if ret is False: _LOGGER.error("Invalid Teksavvy API key: %s", apikey) return @@ -112,10 +110,9 @@ class TekSavvySensor(Entity): """Icon to use in the frontend, if any.""" return self._icon - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data from TekSavvy and update the state.""" - yield from self.teksavvydata.async_update() + await self.teksavvydata.async_update() if self.type in self.teksavvydata.data: self._state = round(self.teksavvydata.data[self.type], 2) diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index f64e8b122ca..77b3759d5fc 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -4,7 +4,6 @@ Allows the creation of a sensor that breaks out state_attributes. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.template/ """ -import asyncio import logging from typing import Optional @@ -41,9 +40,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the template sensors.""" sensors = [] @@ -123,8 +121,7 @@ class SensorTemplate(Entity): self._entities = entity_ids self._device_class = device_class - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" @callback def template_sensor_state_listener(entity, old_state, new_state): @@ -177,8 +174,7 @@ class SensorTemplate(Entity): """No polling needed.""" return False - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update the state from the template.""" try: self._state = self._template.async_render() diff --git a/homeassistant/components/sensor/thethingsnetwork.py b/homeassistant/components/sensor/thethingsnetwork.py index 02661f2211d..0f1220f9b07 100644 --- a/homeassistant/components/sensor/thethingsnetwork.py +++ b/homeassistant/components/sensor/thethingsnetwork.py @@ -38,9 +38,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up The Things Network Data storage sensors.""" ttn = hass.data.get(DATA_TTN) device_id = config.get(CONF_DEVICE_ID) @@ -50,7 +49,7 @@ def async_setup_platform(hass, config, async_add_entities, ttn_data_storage = TtnDataStorage( hass, app_id, device_id, access_key, values) - success = yield from ttn_data_storage.async_update() + success = await ttn_data_storage.async_update() if not success: return False @@ -104,10 +103,9 @@ class TtnDataSensor(Entity): ATTR_TIME: self._state['time'], } - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the current state.""" - yield from self._ttn_data_storage.async_update() + await self._ttn_data_storage.async_update() self._state = self._ttn_data_storage.data @@ -128,13 +126,12 @@ class TtnDataStorage: AUTHORIZATION: 'key {}'.format(access_key), } - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the current state from The Things Network Data Storage.""" try: session = async_get_clientsession(self._hass) with async_timeout.timeout(DEFAULT_TIMEOUT, loop=self._hass.loop): - req = yield from session.get(self._url, headers=self._headers) + req = await session.get(self._url, headers=self._headers) except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Error while accessing: %s", self._url) @@ -155,7 +152,7 @@ class TtnDataStorage: _LOGGER.error("Application ID is not available: %s", self._app_id) return False - data = yield from req.json() + data = await req.json() self.data = data[0] for value in self._values.items(): diff --git a/homeassistant/components/sensor/tibber.py b/homeassistant/components/sensor/tibber.py index dbea54ff353..1207c8dfe20 100644 --- a/homeassistant/components/sensor/tibber.py +++ b/homeassistant/components/sensor/tibber.py @@ -10,26 +10,17 @@ import logging from datetime import timedelta import aiohttp -import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.components.tibber import DOMAIN as TIBBER_DOMAIN from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import Entity from homeassistant.util import dt as dt_util from homeassistant.util import Throttle -REQUIREMENTS = ['pyTibber==0.5.1'] - _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ACCESS_TOKEN): cv.string -}) - ICON = 'mdi:currency-usd' +ICON_RT = 'mdi:power-plug' SCAN_INTERVAL = timedelta(minutes=1) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) @@ -37,24 +28,28 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Tibber sensor.""" - import tibber - tibber_connection = tibber.Tibber(config[CONF_ACCESS_TOKEN], - websession=async_get_clientsession(hass)) + if discovery_info is None: + _LOGGER.error("Tibber sensor configuration has changed." + " Check https://home-assistant.io/components/tibber/") + return + + tibber_connection = hass.data.get(TIBBER_DOMAIN) try: - await tibber_connection.update_info() dev = [] for home in tibber_connection.get_homes(): await home.update_info() - dev.append(TibberSensor(home)) + dev.append(TibberSensorElPrice(home)) + if home.has_real_time_consumption: + dev.append(TibberSensorRT(home)) except (asyncio.TimeoutError, aiohttp.ClientError): raise PlatformNotReady() async_add_entities(dev, True) -class TibberSensor(Entity): - """Representation of an Tibber sensor.""" +class TibberSensorElPrice(Entity): + """Representation of an Tibber sensor for el price.""" def __init__(self, tibber_home): """Initialize the sensor.""" @@ -155,3 +150,71 @@ class TibberSensor(Entity): self._device_state_attributes['max_price'] = max_price self._device_state_attributes['min_price'] = min_price return state is not None + + +class TibberSensorRT(Entity): + """Representation of an Tibber sensor for real time consumption.""" + + def __init__(self, tibber_home): + """Initialize the sensor.""" + self._tibber_home = tibber_home + self._state = None + self._device_state_attributes = {} + self._unit_of_measurement = 'W' + nickname = tibber_home.info['viewer']['home']['appNickname'] + self._name = 'Real time consumption {}'.format(nickname) + + async def async_added_to_hass(self): + """Start unavailability tracking.""" + await self._tibber_home.rt_subscribe(self.hass.loop, + self._async_callback) + + async def _async_callback(self, payload): + """Handle received data.""" + data = payload.get('data', {}) + live_measurement = data.get('liveMeasurement', {}) + self._state = live_measurement.pop('power', None) + self._device_state_attributes = live_measurement + self.async_schedule_update_ha_state() + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._device_state_attributes + + @property + def available(self): + """Return True if entity is available.""" + return self._tibber_home.rt_subscription_running + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def should_poll(self): + """Return the polling state.""" + return False + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return ICON_RT + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return self._unit_of_measurement + + @property + def unique_id(self): + """Return a unique ID.""" + home = self._tibber_home.info['viewer']['home'] + _id = home['meteringPointData']['consumptionEan'] + return'{}_rt_consumption'.format(_id) diff --git a/homeassistant/components/sensor/time_date.py b/homeassistant/components/sensor/time_date.py index e4c719acd0d..1b346d409c4 100644 --- a/homeassistant/components/sensor/time_date.py +++ b/homeassistant/components/sensor/time_date.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.time_date/ """ from datetime import timedelta -import asyncio import logging import voluptuous as vol @@ -37,9 +36,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Time and Date sensor.""" if hass.config.time_zone is None: _LOGGER.error("Timezone is not set in Home Assistant configuration") diff --git a/homeassistant/components/sensor/tradfri.py b/homeassistant/components/sensor/tradfri.py index 86d0c1abc19..45167874de2 100644 --- a/homeassistant/components/sensor/tradfri.py +++ b/homeassistant/components/sensor/tradfri.py @@ -26,7 +26,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): devices_commands = await api(gateway.get_devices()) all_devices = await api(devices_commands) - devices = (dev for dev in all_devices if not dev.has_light_control) + devices = (dev for dev in all_devices if not dev.has_light_control and + not dev.has_socket_control) async_add_entities(TradfriDevice(device, api) for device in devices) @@ -92,7 +93,7 @@ class TradfriDevice(Entity): cmd = self._device.observe(callback=self._observe_update, err_callback=self._async_start_observe, duration=0) - self.hass.async_add_job(self._api(cmd)) + self.hass.async_create_task(self._api(cmd)) except PytradfriError as err: _LOGGER.warning("Observation failed, trying again", exc_info=err) self._async_start_observe() @@ -106,4 +107,4 @@ class TradfriDevice(Entity): """Receive new state data for this device.""" self._refresh(tradfri_device) - self.hass.async_add_job(self.async_update_ha_state()) + self.hass.async_create_task(self.async_update_ha_state()) diff --git a/homeassistant/components/sensor/upnp.py b/homeassistant/components/sensor/upnp.py index d021312d15c..c05e2ce0ade 100644 --- a/homeassistant/components/sensor/upnp.py +++ b/homeassistant/components/sensor/upnp.py @@ -1,87 +1,268 @@ """ -Support for UPnP Sensors (IGD). +Support for UPnP/IGD Sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.upnp/ """ +from datetime import datetime import logging -from homeassistant.components.upnp import DATA_UPNP, UNITS, CIC_SERVICE +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity +from homeassistant.components.upnp.const import DOMAIN as DATA_UPNP +from homeassistant.components.upnp.const import SIGNAL_REMOVE_SENSOR + _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['upnp'] -BYTES_RECEIVED = 1 -BYTES_SENT = 2 -PACKETS_RECEIVED = 3 -PACKETS_SENT = 4 +BYTES_RECEIVED = 'bytes_received' +BYTES_SENT = 'bytes_sent' +PACKETS_RECEIVED = 'packets_received' +PACKETS_SENT = 'packets_sent' -# sensor_type: [friendly_name, convert_unit, icon] SENSOR_TYPES = { - BYTES_RECEIVED: ['received bytes', True, 'mdi:server-network'], - BYTES_SENT: ['sent bytes', True, 'mdi:server-network'], - PACKETS_RECEIVED: ['packets received', False, 'mdi:server-network'], - PACKETS_SENT: ['packets sent', False, 'mdi:server-network'], + BYTES_RECEIVED: { + 'name': 'bytes received', + 'unit': 'bytes', + }, + BYTES_SENT: { + 'name': 'bytes sent', + 'unit': 'bytes', + }, + PACKETS_RECEIVED: { + 'name': 'packets received', + 'unit': 'packets', + }, + PACKETS_SENT: { + 'name': 'packets sent', + 'unit': 'packets', + }, } +IN = 'received' +OUT = 'sent' +KBYTE = 1024 + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the IGD sensors.""" - if discovery_info is None: - return - - device = hass.data[DATA_UPNP] - service = device.find_first_service(CIC_SERVICE) - unit = discovery_info['unit'] - async_add_entities([ - IGDSensor(service, t, unit if SENSOR_TYPES[t][1] else '#') - for t in SENSOR_TYPES], True) + """Old way of setting up UPnP/IGD sensors.""" + _LOGGER.debug('async_setup_platform: config: %s, discovery: %s', + config, discovery_info) -class IGDSensor(Entity): - """Representation of a UPnP IGD sensor.""" +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the UPnP/IGD sensor.""" + @callback + def async_add_sensor(device): + """Add sensors from UPnP/IGD device.""" + # raw sensors + per-second sensors + sensors = [ + RawUPnPIGDSensor(device, name, sensor_type) + for name, sensor_type in SENSOR_TYPES.items() + ] + sensors += [ + KBytePerSecondUPnPIGDSensor(device, IN), + KBytePerSecondUPnPIGDSensor(device, OUT), + PacketsPerSecondUPnPIGDSensor(device, IN), + PacketsPerSecondUPnPIGDSensor(device, OUT), + ] + async_add_entities(sensors, True) - def __init__(self, service, sensor_type, unit=None): - """Initialize the IGD sensor.""" - self._service = service - self.type = sensor_type - self.unit = unit - self.unit_factor = UNITS[unit] if unit in UNITS else 1 - self._name = 'IGD {}'.format(SENSOR_TYPES[sensor_type][0]) + data = config_entry.data + udn = data['udn'] + device = hass.data[DATA_UPNP]['devices'][udn] + async_add_sensor(device) + + +class UpnpSensor(Entity): + """Base class for UPnP/IGD sensors.""" + + def __init__(self, device): + """Initialize the base sensor.""" + self._device = device + + async def async_added_to_hass(self): + """Subscribe to sensors events.""" + async_dispatcher_connect(self.hass, + SIGNAL_REMOVE_SENSOR, + self._upnp_remove_sensor) + + @callback + def _upnp_remove_sensor(self, device): + """Remove sensor.""" + if self._device != device: + # not for us + return + + self.hass.async_create_task(self.async_remove()) + + +class RawUPnPIGDSensor(UpnpSensor): + """Representation of a UPnP/IGD sensor.""" + + def __init__(self, device, sensor_type_name, sensor_type): + """Initialize the UPnP/IGD sensor.""" + super().__init__(device) + self._type_name = sensor_type_name + self._type = sensor_type + self._name = '{} {}'.format(device.name, sensor_type['name']) self._state = None @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return self._name @property - def state(self): + def unique_id(self) -> str: + """Return an unique ID.""" + return '{}_{}'.format(self._device.udn, self._type_name) + + @property + def state(self) -> str: """Return the state of the device.""" - if self._state: - return format(float(self._state) / self.unit_factor, '.1f') - return self._state + return format(self._state, 'd') @property - def icon(self): + def icon(self) -> str: """Icon to use in the frontend, if any.""" - return SENSOR_TYPES[self.type][2] + return 'mdi:server-network' @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit of measurement of this entity, if any.""" - return self.unit + return self._type['unit'] async def async_update(self): """Get the latest information from the IGD.""" - if self.type == BYTES_RECEIVED: - self._state = await self._service.get_total_bytes_received() - elif self.type == BYTES_SENT: - self._state = await self._service.get_total_bytes_sent() - elif self.type == PACKETS_RECEIVED: - self._state = await self._service.get_total_packets_received() - elif self.type == PACKETS_SENT: - self._state = await self._service.get_total_packets_sent() + if self._type_name == BYTES_RECEIVED: + self._state = await self._device.async_get_total_bytes_received() + elif self._type_name == BYTES_SENT: + self._state = await self._device.async_get_total_bytes_sent() + elif self._type_name == PACKETS_RECEIVED: + self._state = await self._device.async_get_total_packets_received() + elif self._type_name == PACKETS_SENT: + self._state = await self._device.async_get_total_packets_sent() + + +class PerSecondUPnPIGDSensor(UpnpSensor): + """Abstract representation of a X Sent/Received per second sensor.""" + + def __init__(self, device, direction): + """Initializer.""" + super().__init__(device) + self._direction = direction + + self._state = None + self._last_value = None + self._last_update_time = None + + @property + def unit(self) -> str: + """Get unit we are measuring in.""" + raise NotImplementedError() + + @property + def _async_fetch_value(self): + """Fetch a value from the IGD.""" + raise NotImplementedError() + + @property + def unique_id(self) -> str: + """Return an unique ID.""" + return '{}_{}/sec_{}'.format(self._device.udn, + self.unit, + self._direction) + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return '{} {}/sec {}'.format(self._device.name, + self.unit, + self._direction) + + @property + def icon(self) -> str: + """Icon to use in the frontend, if any.""" + return 'mdi:server-network' + + @property + def unit_of_measurement(self) -> str: + """Return the unit of measurement of this entity, if any.""" + return '{}/sec'.format(self.unit) + + def _is_overflowed(self, new_value) -> bool: + """Check if value has overflowed.""" + return new_value < self._last_value + + async def async_update(self): + """Get the latest information from the UPnP/IGD.""" + new_value = await self._async_fetch_value() + + if self._last_value is None: + self._last_value = new_value + self._last_update_time = datetime.now() + return + + now = datetime.now() + if self._is_overflowed(new_value): + self._state = None # temporarily report nothing + else: + delta_time = (now - self._last_update_time).seconds + delta_value = new_value - self._last_value + self._state = (delta_value / delta_time) + + self._last_value = new_value + self._last_update_time = now + + +class KBytePerSecondUPnPIGDSensor(PerSecondUPnPIGDSensor): + """Representation of a KBytes Sent/Received per second sensor.""" + + @property + def unit(self) -> str: + """Get unit we are measuring in.""" + return 'kbyte' + + async def _async_fetch_value(self) -> float: + """Fetch value from device.""" + if self._direction == IN: + return await self._device.async_get_total_bytes_received() + + return await self._device.async_get_total_bytes_sent() + + @property + def state(self) -> str: + """Return the state of the device.""" + if self._state is None: + return None + + return format(float(self._state / KBYTE), '.1f') + + +class PacketsPerSecondUPnPIGDSensor(PerSecondUPnPIGDSensor): + """Representation of a Packets Sent/Received per second sensor.""" + + @property + def unit(self) -> str: + """Get unit we are measuring in.""" + return 'packets' + + async def _async_fetch_value(self) -> float: + """Fetch value from device.""" + if self._direction == IN: + return await self._device.async_get_total_packets_received() + + return await self._device.async_get_total_packets_sent() + + @property + def state(self) -> str: + """Return the state of the device.""" + if self._state is None: + return None + + return format(float(self._state), '.1f') diff --git a/homeassistant/components/sensor/viaggiatreno.py b/homeassistant/components/sensor/viaggiatreno.py index 1dd8523eb4b..82068c456b6 100644 --- a/homeassistant/components/sensor/viaggiatreno.py +++ b/homeassistant/components/sensor/viaggiatreno.py @@ -56,9 +56,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the ViaggiaTreno platform.""" train_id = config.get(CONF_TRAIN_ID) station_id = config.get(CONF_STATION_ID) @@ -68,16 +67,15 @@ def async_setup_platform(hass, config, async_add_entities, async_add_entities([ViaggiaTrenoSensor(train_id, station_id, name)]) -@asyncio.coroutine -def async_http_request(hass, uri): +async def async_http_request(hass, uri): """Perform actual request.""" try: session = hass.helpers.aiohttp_client.async_get_clientsession(hass) with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): - req = yield from session.get(uri) + req = await session.get(uri) if req.status != 200: return {'error': req.status} - json_response = yield from req.json() + json_response = await req.json() return json_response except (asyncio.TimeoutError, aiohttp.ClientError) as exc: _LOGGER.error("Cannot connect to ViaggiaTreno API endpoint: %s", exc) @@ -152,11 +150,10 @@ class ViaggiaTrenoSensor(Entity): return True return False - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update state.""" uri = self.uri - res = yield from async_http_request(self.hass, uri) + res = await async_http_request(self.hass, uri) if res.get('error', ''): if res['error'] == 204: self._state = NO_INFORMATION_STRING diff --git a/homeassistant/components/sensor/waqi.py b/homeassistant/components/sensor/waqi.py index 9f90f465fb2..d6b8d278fb1 100644 --- a/homeassistant/components/sensor/waqi.py +++ b/homeassistant/components/sensor/waqi.py @@ -59,9 +59,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the requested World Air Quality Index locations.""" import waqiasync @@ -74,7 +73,7 @@ def async_setup_platform(hass, config, async_add_entities, dev = [] try: for location_name in locations: - stations = yield from client.search(location_name) + stations = await client.search(location_name) _LOGGER.debug("The following stations were returned: %s", stations) for station in stations: waqi_sensor = WaqiSensor(client, station) @@ -161,13 +160,12 @@ class WaqiSensor(Entity): except (IndexError, KeyError): return {ATTR_ATTRIBUTION: ATTRIBUTION} - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data and updates the states.""" if self.uid: - result = yield from self._client.get_station_by_number(self.uid) + result = await self._client.get_station_by_number(self.uid) elif self.url: - result = yield from self._client.get_station_by_name(self.url) + result = await self._client.get_station_by_name(self.url) else: result = None self._data = result diff --git a/homeassistant/components/sensor/waterfurnace.py b/homeassistant/components/sensor/waterfurnace.py index 806b40551df..60da761cf75 100644 --- a/homeassistant/components/sensor/waterfurnace.py +++ b/homeassistant/components/sensor/waterfurnace.py @@ -4,7 +4,6 @@ Support for Waterfurnace. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.waterfurnace/ """ -import asyncio from homeassistant.components.sensor import ENTITY_ID_FORMAT from homeassistant.components.waterfurnace import ( @@ -100,8 +99,7 @@ class WaterFurnaceSensor(Entity): """Return the polling state.""" return False - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( UPDATE_TOPIC, self.async_update_callback) diff --git a/homeassistant/components/sensor/wink.py b/homeassistant/components/sensor/wink.py index 8e11b054b24..8c2abc0f875 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/sensor/wink.py @@ -4,7 +4,6 @@ Support for Wink sensors. For more details about this platform, please refer to the documentation at at https://home-assistant.io/components/sensor.wink/ """ -import asyncio import logging from homeassistant.components.wink import DOMAIN, WinkDevice @@ -60,8 +59,7 @@ class WinkSensorDevice(WinkDevice, Entity): else: self._unit_of_measurement = self.wink.unit() - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['sensor'].append(self) diff --git a/homeassistant/components/sensor/worxlandroid.py b/homeassistant/components/sensor/worxlandroid.py index f6593f4b1c5..be5c8452d88 100644 --- a/homeassistant/components/sensor/worxlandroid.py +++ b/homeassistant/components/sensor/worxlandroid.py @@ -50,9 +50,8 @@ ERROR_STATE = [ ] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Worx Landroid sensors.""" for typ in ('battery', 'state'): async_add_entities([WorxLandroidSensor(typ, config)]) @@ -88,8 +87,7 @@ class WorxLandroidSensor(Entity): return '%' return None - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update the sensor data from the mower.""" connection_error = False @@ -97,7 +95,7 @@ class WorxLandroidSensor(Entity): session = async_get_clientsession(self.hass) with async_timeout.timeout(self.timeout, loop=self.hass.loop): auth = aiohttp.helpers.BasicAuth('admin', self.pin) - mower_response = yield from session.get(self.url, auth=auth) + mower_response = await session.get(self.url, auth=auth) except (asyncio.TimeoutError, aiohttp.ClientError): if self.allow_unreachable is False: _LOGGER.error("Error connecting to mower at %s", self.url) @@ -115,7 +113,7 @@ class WorxLandroidSensor(Entity): elif connection_error is False: # set the expected content type to be text/html # since the mover incorrectly returns it... - data = yield from mower_response.json(content_type='text/html') + data = await mower_response.json(content_type='text/html') # sensor battery if self.sensor == 'battery': diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index a14d4b94789..590a9d96b4d 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -741,10 +741,9 @@ class WUndergroundSensor(Entity): """Return the units of measurement.""" return self._unit_of_measurement - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update current conditions.""" - yield from self.rest.async_update() + await self.rest.async_update() if not self.rest.data: # no data, return diff --git a/homeassistant/components/sensor/xiaomi_aqara.py b/homeassistant/components/sensor/xiaomi_aqara.py index 8a3a11db051..31366fe0097 100644 --- a/homeassistant/components/sensor/xiaomi_aqara.py +++ b/homeassistant/components/sensor/xiaomi_aqara.py @@ -5,7 +5,7 @@ from homeassistant.components.xiaomi_aqara import (PY_XIAOMI_GATEWAY, XiaomiDevice) from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, - TEMP_CELSIUS) + TEMP_CELSIUS, DEVICE_CLASS_PRESSURE) _LOGGER = logging.getLogger(__name__) @@ -14,7 +14,7 @@ SENSOR_TYPES = { 'humidity': ['%', None, DEVICE_CLASS_HUMIDITY], 'illumination': ['lm', None, DEVICE_CLASS_ILLUMINANCE], 'lux': ['lx', None, DEVICE_CLASS_ILLUMINANCE], - 'pressure': ['hPa', 'mdi:gauge', None] + 'pressure': ['hPa', None, DEVICE_CLASS_PRESSURE] } diff --git a/homeassistant/components/shell_command.py b/homeassistant/components/shell_command.py index 10a6c350b7c..2a95dd5c144 100644 --- a/homeassistant/components/shell_command.py +++ b/homeassistant/components/shell_command.py @@ -27,15 +27,13 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Set up the shell_command component.""" conf = config.get(DOMAIN, {}) cache = {} - @asyncio.coroutine - def async_service_handler(service: ServiceCall) -> None: + async def async_service_handler(service: ServiceCall) -> None: """Execute a shell command service.""" cmd = conf[service.service] @@ -85,8 +83,8 @@ def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: stderr=asyncio.subprocess.PIPE, ) - process = yield from create_process - stdout_data, stderr_data = yield from process.communicate() + process = await create_process + stdout_data, stderr_data = await process.communicate() if stdout_data: _LOGGER.debug("Stdout of command: `%s`, return code: %s:\n%s", diff --git a/homeassistant/components/sisyphus.py b/homeassistant/components/sisyphus.py index dc9f9cc4c25..f875e3a91c7 100644 --- a/homeassistant/components/sisyphus.py +++ b/homeassistant/components/sisyphus.py @@ -54,12 +54,12 @@ async def async_setup(hass, config): tables[name] = table _LOGGER.debug("Connected to %s at %s", name, host) - hass.async_add_job(async_load_platform( + hass.async_create_task(async_load_platform( hass, 'light', DOMAIN, { CONF_NAME: name, }, config )) - hass.async_add_job(async_load_platform( + hass.async_create_task(async_load_platform( hass, 'media_player', DOMAIN, { CONF_NAME: name, CONF_HOST: host, diff --git a/homeassistant/components/smhi/.translations/ca.json b/homeassistant/components/smhi/.translations/ca.json new file mode 100644 index 00000000000..23b6a2934f0 --- /dev/null +++ b/homeassistant/components/smhi/.translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "El nom ja existeix", + "wrong_location": "Ubicaci\u00f3 nom\u00e9s a Su\u00e8cia" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nom" + }, + "title": "Ubicaci\u00f3 a Su\u00e8cia" + } + }, + "title": "Servei meteorol\u00f2gic suec (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/en.json b/homeassistant/components/smhi/.translations/en.json new file mode 100644 index 00000000000..6aa256d87d4 --- /dev/null +++ b/homeassistant/components/smhi/.translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Name already exists", + "wrong_location": "Location Sweden only" + }, + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name" + }, + "title": "Location in Sweden" + } + }, + "title": "Swedish weather service (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/ko.json b/homeassistant/components/smhi/.translations/ko.json new file mode 100644 index 00000000000..f307fa1ad23 --- /dev/null +++ b/homeassistant/components/smhi/.translations/ko.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4", + "wrong_location": "\uc2a4\uc6e8\ub374 \uc9c0\uc5ed \uc804\uc6a9\uc785\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4", + "name": "\uc774\ub984" + }, + "title": "\uc2a4\uc6e8\ub374 \uc9c0\uc5ed \uc704\uce58" + } + }, + "title": "\uc2a4\uc6e8\ub374 \uae30\uc0c1 \uc11c\ube44\uc2a4 (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/lb.json b/homeassistant/components/smhi/.translations/lb.json new file mode 100644 index 00000000000..46abfd2677f --- /dev/null +++ b/homeassistant/components/smhi/.translations/lb.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Numm g\u00ebtt et schonn", + "wrong_location": "N\u00ebmmen Uertschaften an Schweden" + }, + "step": { + "user": { + "data": { + "latitude": "Breedegrad", + "longitude": "L\u00e4ngegrad", + "name": "Numm" + }, + "title": "Uertschaft an Schweden" + } + }, + "title": "Schwedeschen Wieder D\u00e9ngscht (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/nl.json b/homeassistant/components/smhi/.translations/nl.json new file mode 100644 index 00000000000..88edc116e74 --- /dev/null +++ b/homeassistant/components/smhi/.translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Naam bestaat al", + "wrong_location": "Locatie alleen Zweden" + }, + "step": { + "user": { + "data": { + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "name": "Naam" + }, + "title": "Locatie in Zweden" + } + }, + "title": "Zweedse weerdienst (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/pl.json b/homeassistant/components/smhi/.translations/pl.json new file mode 100644 index 00000000000..21973cd54b6 --- /dev/null +++ b/homeassistant/components/smhi/.translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Nazwa ju\u017c istnieje", + "wrong_location": "Lokalizacja w Szwecji" + }, + "step": { + "user": { + "data": { + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "name": "Nazwa" + }, + "title": "Lokalizacja w Szwecji" + } + }, + "title": "Szwedzka us\u0142uga pogodowa (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/ru.json b/homeassistant/components/smhi/.translations/ru.json new file mode 100644 index 00000000000..012bb74c568 --- /dev/null +++ b/homeassistant/components/smhi/.translations/ru.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442", + "wrong_location": "\u0422\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0428\u0432\u0435\u0446\u0438\u0438" + }, + "step": { + "user": { + "data": { + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "title": "\u041c\u0435\u0441\u0442\u043e\u043d\u0430\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435 \u0432 \u0428\u0432\u0435\u0446\u0438\u0438" + } + }, + "title": "\u0428\u0432\u0435\u0434\u0441\u043a\u0430\u044f \u043c\u0435\u0442\u0435\u043e\u0440\u043e\u043b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0441\u043b\u0443\u0436\u0431\u0430 (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/sl.json b/homeassistant/components/smhi/.translations/sl.json new file mode 100644 index 00000000000..94c3750f06f --- /dev/null +++ b/homeassistant/components/smhi/.translations/sl.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Ime \u017ee obstaja", + "wrong_location": "Lokacija le na \u0160vedskem" + }, + "step": { + "user": { + "data": { + "latitude": "Zemljepisna \u0161irina", + "longitude": "Zemljepisna dol\u017eina", + "name": "Ime" + }, + "title": "Lokacija na \u0160vedskem" + } + }, + "title": "\u0160vedska vremenska slu\u017eba (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/sv.json b/homeassistant/components/smhi/.translations/sv.json new file mode 100644 index 00000000000..69073a0eb73 --- /dev/null +++ b/homeassistant/components/smhi/.translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "Namnet finns redan", + "wrong_location": "Plats i Sverige endast" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Namn" + }, + "title": "Plats i Sverige" + } + }, + "title": "Svensk v\u00e4derservice (SMHI)" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/zh-Hans.json b/homeassistant/components/smhi/.translations/zh-Hans.json new file mode 100644 index 00000000000..a70bb7a6722 --- /dev/null +++ b/homeassistant/components/smhi/.translations/zh-Hans.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "\u540d\u79f0\u5df2\u5b58\u5728", + "wrong_location": "\u4ec5\u9650\u745e\u5178\u7684\u4f4d\u7f6e" + }, + "step": { + "user": { + "data": { + "latitude": "\u7eac\u5ea6", + "longitude": "\u7ecf\u5ea6", + "name": "\u540d\u79f0" + }, + "title": "\u5728\u745e\u5178\u7684\u4f4d\u7f6e" + } + }, + "title": "\u745e\u5178\u6c14\u8c61\u670d\u52a1\uff08SMHI\uff09" + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/.translations/zh-Hant.json b/homeassistant/components/smhi/.translations/zh-Hant.json new file mode 100644 index 00000000000..b982baac2f8 --- /dev/null +++ b/homeassistant/components/smhi/.translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728", + "wrong_location": "\u50c5\u9650\u745e\u5178\u5ea7\u6a19" + }, + "step": { + "user": { + "data": { + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "name": "\u540d\u7a31" + }, + "title": "\u745e\u5178\u5ea7\u6a19" + } + }, + "title": "\u745e\u5178\u6c23\u8c61\u670d\u52d9\uff08SMHI\uff09" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/ko.json b/homeassistant/components/sonos/.translations/ko.json index 89933f57425..0b2e2a1875c 100644 --- a/homeassistant/components/sonos/.translations/ko.json +++ b/homeassistant/components/sonos/.translations/ko.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Sonos\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Sonos \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Sonos" } }, diff --git a/homeassistant/components/sonos/.translations/pl.json b/homeassistant/components/sonos/.translations/pl.json index 2a0c526b9a6..a45cb4e9824 100644 --- a/homeassistant/components/sonos/.translations/pl.json +++ b/homeassistant/components/sonos/.translations/pl.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Chcesz skonfigurowa\u0107 Sonos?", + "description": "Czy chcesz skonfigurowa\u0107 Sonos?", "title": "Sonos" } }, diff --git a/homeassistant/components/sonos/.translations/ru.json b/homeassistant/components/sonos/.translations/ru.json index 63b6bd87c20..1bff827d273 100644 --- a/homeassistant/components/sonos/.translations/ru.json +++ b/homeassistant/components/sonos/.translations/ru.json @@ -2,11 +2,11 @@ "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." + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "step": { "confirm": { - "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Sonos?", + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Sonos?", "title": "Sonos" } }, diff --git a/homeassistant/components/sonos/.translations/zh-Hant.json b/homeassistant/components/sonos/.translations/zh-Hant.json index c6fb13c3605..520a29b7602 100644 --- a/homeassistant/components/sonos/.translations/zh-Hant.json +++ b/homeassistant/components/sonos/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Sonos \u8a2d\u5099\u3002", + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Sonos \u88dd\u7f6e\u3002", "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 Sonos \u5373\u53ef\u3002" }, "step": { diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index b4565794844..b794fe607e6 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -4,7 +4,7 @@ from homeassistant.helpers import config_entry_flow DOMAIN = 'sonos' -REQUIREMENTS = ['pysonos==0.0.2'] +REQUIREMENTS = ['pysonos==0.0.3'] async def async_setup(hass, config): diff --git a/homeassistant/components/spc.py b/homeassistant/components/spc.py index b00a4aeed2c..5aa987bd0a8 100644 --- a/homeassistant/components/spc.py +++ b/homeassistant/components/spc.py @@ -16,9 +16,6 @@ REQUIREMENTS = ['pyspcwebgw==0.4.0'] _LOGGER = logging.getLogger(__name__) -ATTR_DISCOVER_DEVICES = 'devices' -ATTR_DISCOVER_AREAS = 'areas' - CONF_WS_URL = 'ws_url' CONF_API_URL = 'api_url' @@ -66,13 +63,11 @@ async def async_setup(hass, config): # add sensor devices for each zone (typically motion/fire/door sensors) hass.async_create_task(discovery.async_load_platform( - hass, 'binary_sensor', DOMAIN, - {ATTR_DISCOVER_DEVICES: spc.zones.values()}, config)) + hass, 'binary_sensor', DOMAIN, {})) # create a separate alarm panel for each area hass.async_create_task(discovery.async_load_platform( - hass, 'alarm_control_panel', DOMAIN, - {ATTR_DISCOVER_AREAS: spc.areas.values()}, config)) + hass, 'alarm_control_panel', DOMAIN, {})) # start listening for incoming events over websocket spc.start() diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index 90c7f69e64a..e2717047b0a 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -4,7 +4,6 @@ Support for functionality to keep track of the sun. For more details about this component, please refer to the documentation at https://home-assistant.io/components/sun/ """ -import asyncio import logging from datetime import timedelta @@ -36,8 +35,7 @@ STATE_ATTR_NEXT_RISING = 'next_rising' STATE_ATTR_NEXT_SETTING = 'next_setting' -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Track the state of the sun.""" if config.get(CONF_ELEVATION) is not None: _LOGGER.warning( diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index c95c752435a..1adabe4b57e 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -9,7 +9,6 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.loader import bind_hass from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import ToggleEntity @@ -56,42 +55,6 @@ def is_on(hass, entity_id=None): return hass.states.is_state(entity_id, STATE_ON) -@bind_hass -def turn_on(hass, entity_id=None): - """Turn all or specified switch on.""" - hass.add_job(async_turn_on, hass, entity_id) - - -@callback -@bind_hass -def async_turn_on(hass, entity_id=None): - """Turn all or specified switch on.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)) - - -@bind_hass -def turn_off(hass, entity_id=None): - """Turn all or specified switch off.""" - hass.add_job(async_turn_off, hass, entity_id) - - -@callback -@bind_hass -def async_turn_off(hass, entity_id=None): - """Turn all or specified switch off.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job( - hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data)) - - -@bind_hass -def toggle(hass, entity_id=None): - """Toggle all or specified switch.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_TOGGLE, data) - - async def async_setup(hass, config): """Track states and offer events for switches.""" component = hass.data[DOMAIN] = EntityComponent( diff --git a/homeassistant/components/switch/ads.py b/homeassistant/components/switch/ads.py index 8c13e9a8960..ecd1e7edc31 100644 --- a/homeassistant/components/switch/ads.py +++ b/homeassistant/components/switch/ads.py @@ -4,7 +4,6 @@ Support for ADS switch platform. For more details about this platform, please refer to the documentation. https://home-assistant.io/components/switch.ads/ """ -import asyncio import logging import voluptuous as vol @@ -47,8 +46,7 @@ class AdsSwitch(ToggleEntity): self._name = name self.ads_var = ads_var - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register device notification.""" def update(name, value): """Handle device notification.""" diff --git a/homeassistant/components/switch/amcrest.py b/homeassistant/components/switch/amcrest.py index 0805793fe95..4eb20308850 100644 --- a/homeassistant/components/switch/amcrest.py +++ b/homeassistant/components/switch/amcrest.py @@ -4,7 +4,6 @@ Support for toggling Amcrest IP camera settings. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.amcrest/ """ -import asyncio import logging from homeassistant.components.amcrest import DATA_AMCREST, SWITCHES @@ -17,9 +16,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['amcrest'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the IP Amcrest camera switch platform.""" if discovery_info is None: return diff --git a/homeassistant/components/switch/android_ip_webcam.py b/homeassistant/components/switch/android_ip_webcam.py index 92e52c21caa..f770b9d5ebf 100644 --- a/homeassistant/components/switch/android_ip_webcam.py +++ b/homeassistant/components/switch/android_ip_webcam.py @@ -4,7 +4,6 @@ Support for IP Webcam settings. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.android_ip_webcam/ """ -import asyncio from homeassistant.components.switch import SwitchDevice from homeassistant.components.android_ip_webcam import ( @@ -14,9 +13,8 @@ from homeassistant.components.android_ip_webcam import ( DEPENDENCIES = ['android_ip_webcam'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the IP Webcam switch platform.""" if discovery_info is None: return @@ -51,8 +49,7 @@ class IPWebcamSettingsSwitch(AndroidIPCamEntity, SwitchDevice): """Return the name of the node.""" return self._name - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the updated status of the switch.""" self._state = bool(self._ipcam.current_settings.get(self._setting)) @@ -61,31 +58,29 @@ class IPWebcamSettingsSwitch(AndroidIPCamEntity, SwitchDevice): """Return the boolean response if the node is on.""" return self._state - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn device on.""" if self._setting == 'torch': - yield from self._ipcam.torch(activate=True) + await self._ipcam.torch(activate=True) elif self._setting == 'focus': - yield from self._ipcam.focus(activate=True) + await self._ipcam.focus(activate=True) elif self._setting == 'video_recording': - yield from self._ipcam.record(record=True) + await self._ipcam.record(record=True) else: - yield from self._ipcam.change_setting(self._setting, True) + await self._ipcam.change_setting(self._setting, True) self._state = True self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn device off.""" if self._setting == 'torch': - yield from self._ipcam.torch(activate=False) + await self._ipcam.torch(activate=False) elif self._setting == 'focus': - yield from self._ipcam.focus(activate=False) + await self._ipcam.focus(activate=False) elif self._setting == 'video_recording': - yield from self._ipcam.record(record=False) + await self._ipcam.record(record=False) else: - yield from self._ipcam.change_setting(self._setting, False) + await self._ipcam.change_setting(self._setting, False) self._state = False self.async_schedule_update_ha_state() diff --git a/homeassistant/components/switch/aqualogic.py b/homeassistant/components/switch/aqualogic.py new file mode 100644 index 00000000000..48c4702aca0 --- /dev/null +++ b/homeassistant/components/switch/aqualogic.py @@ -0,0 +1,114 @@ +""" +Support for AquaLogic switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.aqualogic/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.core import callback +import homeassistant.components.aqualogic as aq +from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.const import (CONF_MONITORED_CONDITIONS) + +DEPENDENCIES = ['aqualogic'] + +_LOGGER = logging.getLogger(__name__) + +SWITCH_TYPES = { + 'lights': 'Lights', + 'filter': 'Filter', + 'filter_low_speed': 'Filter Low Speed', + 'aux_1': 'Aux 1', + 'aux_2': 'Aux 2', + 'aux_3': 'Aux 3', + 'aux_4': 'Aux 4', + 'aux_5': 'Aux 5', + 'aux_6': 'Aux 6', + 'aux_7': 'Aux 7', +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SWITCH_TYPES)): + vol.All(cv.ensure_list, [vol.In(SWITCH_TYPES)]), +}) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the switch platform.""" + switches = [] + + processor = hass.data[aq.DOMAIN] + for switch_type in config.get(CONF_MONITORED_CONDITIONS): + switches.append(AquaLogicSwitch(processor, switch_type)) + + async_add_entities(switches) + + +class AquaLogicSwitch(SwitchDevice): + """Switch implementation for the AquaLogic component.""" + + def __init__(self, processor, switch_type): + """Initialize switch.""" + from aqualogic.core import States + self._processor = processor + self._type = switch_type + self._state_name = { + 'lights': States.LIGHTS, + 'filter': States.FILTER, + 'filter_low_speed': States.FILTER_LOW_SPEED, + 'aux_1': States.AUX_1, + 'aux_2': States.AUX_2, + 'aux_3': States.AUX_3, + 'aux_4': States.AUX_4, + 'aux_5': States.AUX_5, + 'aux_6': States.AUX_6, + 'aux_7': States.AUX_7 + }[switch_type] + + @property + def name(self): + """Return the name of the switch.""" + return "AquaLogic {}".format(SWITCH_TYPES[self._type]) + + @property + def should_poll(self): + """Return the polling state.""" + return False + + @property + def is_on(self): + """Return true if device is on.""" + panel = self._processor.panel + if panel is None: + return False + state = panel.get_state(self._state_name) + return state + + def turn_on(self, **kwargs): + """Turn the device on.""" + panel = self._processor.panel + if panel is None: + return + panel.set_state(self._state_name, True) + + def turn_off(self, **kwargs): + """Turn the device off.""" + panel = self._processor.panel + if panel is None: + return + panel.set_state(self._state_name, False) + + async def async_added_to_hass(self): + """Register callbacks.""" + self.hass.helpers.dispatcher.async_dispatcher_connect( + aq.UPDATE_TOPIC, self.async_update_callback) + + @callback + def async_update_callback(self): + """Update callback.""" + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py index e6115872390..0562292acec 100644 --- a/homeassistant/components/switch/broadlink.py +++ b/homeassistant/components/switch/broadlink.py @@ -80,11 +80,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): config.get(CONF_MAC).encode().replace(b':', b'')) switch_type = config.get(CONF_TYPE) - @asyncio.coroutine - def _learn_command(call): + async def _learn_command(call): """Handle a learn command.""" try: - auth = yield from hass.async_add_job(broadlink_device.auth) + auth = await hass.async_add_job(broadlink_device.auth) except socket.timeout: _LOGGER.error("Failed to connect to device, timeout") return @@ -92,12 +91,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.error("Failed to connect to device") return - yield from hass.async_add_job(broadlink_device.enter_learning) + await hass.async_add_job(broadlink_device.enter_learning) _LOGGER.info("Press the key you want Home Assistant to learn") start_time = utcnow() while (utcnow() - start_time) < timedelta(seconds=20): - packet = yield from hass.async_add_job( + packet = await hass.async_add_job( broadlink_device.check_data) if packet: log_msg = "Received packet is: {}".\ @@ -106,13 +105,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hass.components.persistent_notification.async_create( log_msg, title='Broadlink switch') return - yield from asyncio.sleep(1, loop=hass.loop) + await asyncio.sleep(1, loop=hass.loop) _LOGGER.error("Did not received any signal") hass.components.persistent_notification.async_create( "Did not received any signal", title='Broadlink switch') - @asyncio.coroutine - def _send_packet(call): + async def _send_packet(call): """Send a packet.""" packets = call.data.get('packet', []) for packet in packets: @@ -122,12 +120,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if extra > 0: packet = packet + ('=' * (4 - extra)) payload = b64decode(packet) - yield from hass.async_add_job( + await hass.async_add_job( broadlink_device.send_data, payload) break except (socket.timeout, ValueError): try: - yield from hass.async_add_job( + await hass.async_add_job( broadlink_device.auth) except socket.timeout: if retry == DEFAULT_RETRY-1: diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index 15fdee59eaf..05e0497155a 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -13,10 +13,12 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - is_on, turn_on, VALID_TRANSITION, ATTR_TRANSITION) + is_on, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, + ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN, VALID_TRANSITION) from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.const import ( - CONF_NAME, CONF_PLATFORM, CONF_LIGHTS, CONF_MODE) + ATTR_ENTITY_ID, CONF_NAME, CONF_PLATFORM, CONF_LIGHTS, CONF_MODE, + SERVICE_TURN_ON) from homeassistant.helpers.event import track_time_change from homeassistant.helpers.sun import get_astral_event_date from homeassistant.util import slugify @@ -69,30 +71,44 @@ def set_lights_xy(hass, lights, x_val, y_val, brightness, transition): """Set color of array of lights.""" for light in lights: if is_on(hass, light): - turn_on(hass, light, - xy_color=[x_val, y_val], - brightness=brightness, - transition=transition, - white_value=brightness) + service_data = {ATTR_ENTITY_ID: light} + if x_val is not None and y_val is not None: + service_data[ATTR_XY_COLOR] = [x_val, y_val] + if brightness is not None: + service_data[ATTR_BRIGHTNESS] = brightness + service_data[ATTR_WHITE_VALUE] = brightness + if transition is not None: + service_data[ATTR_TRANSITION] = transition + hass.services.call( + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) def set_lights_temp(hass, lights, mired, brightness, transition): """Set color of array of lights.""" for light in lights: if is_on(hass, light): - turn_on(hass, light, - color_temp=int(mired), - brightness=brightness, - transition=transition) + service_data = {ATTR_ENTITY_ID: light} + if mired is not None: + service_data[ATTR_COLOR_TEMP] = int(mired) + if brightness is not None: + service_data[ATTR_BRIGHTNESS] = brightness + if transition is not None: + service_data[ATTR_TRANSITION] = transition + hass.services.call( + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) def set_lights_rgb(hass, lights, rgb, transition): """Set color of array of lights.""" for light in lights: if is_on(hass, light): - turn_on(hass, light, - rgb_color=rgb, - transition=transition) + service_data = {ATTR_ENTITY_ID: light} + if rgb is not None: + service_data[ATTR_RGB_COLOR] = rgb + if transition is not None: + service_data[ATTR_TRANSITION] = transition + hass.services.call( + LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) def setup_platform(hass, config, add_entities, discovery_info=None): diff --git a/homeassistant/components/switch/hook.py b/homeassistant/components/switch/hook.py index 5a86346aa76..fdc13f59d28 100644 --- a/homeassistant/components/switch/hook.py +++ b/homeassistant/components/switch/hook.py @@ -31,9 +31,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up Hook by getting the access token and list of actions.""" username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) @@ -43,14 +42,14 @@ def async_setup_platform(hass, config, async_add_entities, if username is not None and password is not None: try: with async_timeout.timeout(TIMEOUT, loop=hass.loop): - response = yield from websession.post( + response = await websession.post( '{}{}'.format(HOOK_ENDPOINT, 'user/login'), data={ 'username': username, 'password': password}) # The Hook API returns JSON but calls it 'text/html'. Setting # content_type=None disables aiohttp's content-type validation. - data = yield from response.json(content_type=None) + data = await response.json(content_type=None) except (asyncio.TimeoutError, aiohttp.ClientError) as error: _LOGGER.error("Failed authentication API call: %s", error) return False @@ -63,10 +62,10 @@ def async_setup_platform(hass, config, async_add_entities, try: with async_timeout.timeout(TIMEOUT, loop=hass.loop): - response = yield from websession.get( + response = await websession.get( '{}{}'.format(HOOK_ENDPOINT, 'device'), params={"token": token}) - data = yield from response.json(content_type=None) + data = await response.json(content_type=None) except (asyncio.TimeoutError, aiohttp.ClientError) as error: _LOGGER.error("Failed getting devices: %s", error) return False @@ -104,16 +103,15 @@ class HookSmartHome(SwitchDevice): """Return true if device is on.""" return self._state - @asyncio.coroutine - def _send(self, url): + async def _send(self, url): """Send the url to the Hook API.""" try: _LOGGER.debug("Sending: %s", url) websession = async_get_clientsession(self.hass) with async_timeout.timeout(TIMEOUT, loop=self.hass.loop): - response = yield from websession.get( + response = await websession.get( url, params={"token": self._token}) - data = yield from response.json(content_type=None) + data = await response.json(content_type=None) except (asyncio.TimeoutError, aiohttp.ClientError) as error: _LOGGER.error("Failed setting state: %s", error) @@ -122,21 +120,19 @@ class HookSmartHome(SwitchDevice): _LOGGER.debug("Got: %s", data) return data['return_value'] == '1' - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on asynchronously.""" _LOGGER.debug("Turning on: %s", self._name) url = '{}{}{}{}'.format( HOOK_ENDPOINT, 'device/trigger/', self._id, '/On') - success = yield from self._send(url) + success = await self._send(url) self._state = success - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the device off asynchronously.""" _LOGGER.debug("Turning off: %s", self._name) url = '{}{}{}{}'.format( HOOK_ENDPOINT, 'device/trigger/', self._id, '/Off') - success = yield from self._send(url) + success = await self._send(url) # If it wasn't successful, keep state as true self._state = not success diff --git a/homeassistant/components/switch/insteon.py b/homeassistant/components/switch/insteon.py index 744d278d394..454b3ef39cb 100644 --- a/homeassistant/components/switch/insteon.py +++ b/homeassistant/components/switch/insteon.py @@ -4,7 +4,6 @@ Support for INSTEON dimmers via PowerLinc Modem. For more details about this component, please refer to the documentation at https://home-assistant.io/components/switch.insteon/ """ -import asyncio import logging from homeassistant.components.insteon import InsteonEntity @@ -15,9 +14,8 @@ DEPENDENCIES = ['insteon'] _LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the INSTEON device class for the hass platform.""" insteon_modem = hass.data['insteon'].get('modem') @@ -48,13 +46,11 @@ class InsteonSwitchDevice(InsteonEntity, SwitchDevice): """Return the boolean response if the node is on.""" return bool(self._insteon_device_state.value) - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn device on.""" self._insteon_device_state.on() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn device off.""" self._insteon_device_state.off() @@ -67,12 +63,10 @@ class InsteonOpenClosedDevice(InsteonEntity, SwitchDevice): """Return the boolean response if the node is on.""" return bool(self._insteon_device_state.value) - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn device on.""" self._insteon_device_state.open() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn device off.""" self._insteon_device_state.close() diff --git a/homeassistant/components/switch/lutron_caseta.py b/homeassistant/components/switch/lutron_caseta.py index 8587c78a5d5..f983050cffa 100644 --- a/homeassistant/components/switch/lutron_caseta.py +++ b/homeassistant/components/switch/lutron_caseta.py @@ -4,7 +4,6 @@ Support for Lutron Caseta switches. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sitch.lutron_caseta/ """ -import asyncio import logging from homeassistant.components.lutron_caseta import ( @@ -16,9 +15,8 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['lutron_caseta'] -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up Lutron switch.""" devs = [] bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] @@ -35,13 +33,11 @@ def async_setup_platform(hass, config, async_add_entities, class LutronCasetaLight(LutronCasetaDevice, SwitchDevice): """Representation of a Lutron Caseta switch.""" - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the switch on.""" self._smartbridge.turn_on(self._device_id) - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the switch off.""" self._smartbridge.turn_off(self._device_id) @@ -50,8 +46,7 @@ class LutronCasetaLight(LutronCasetaDevice, SwitchDevice): """Return true if device is on.""" return self._state["current_state"] > 0 - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update when forcing a refresh of the device.""" self._state = self._smartbridge.get_device_by_id(self._device_id) _LOGGER.debug(self._state) diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index b79f8f12b87..bb57f179340 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -15,12 +15,15 @@ from homeassistant.components.mqtt import ( CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate) +from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW from homeassistant.components.switch import SwitchDevice from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_ICON, STATE_ON) -from homeassistant.components import mqtt +from homeassistant.components import mqtt, switch import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.helpers.restore_state import async_get_last_state _LOGGER = logging.getLogger(__name__) @@ -47,20 +50,33 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up the MQTT switch.""" - if discovery_info is not None: - config = PLATFORM_SCHEMA(discovery_info) +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_entities, discovery_info=None): + """Set up MQTT switch through configuration.yaml.""" + await _async_setup_entity(hass, config, async_add_entities, + discovery_info) + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT switch dynamically through MQTT discovery.""" + async def async_discover(discovery_payload): + """Discover and add a MQTT switch.""" + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(hass, config, async_add_entities, + discovery_payload[ATTR_DISCOVERY_HASH]) + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(switch.DOMAIN, 'mqtt'), + async_discover) + + +async def _async_setup_entity(hass, config, async_add_entities, + discovery_hash=None): + """Set up the MQTT switch.""" value_template = config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = hass - discovery_hash = None - if discovery_info is not None and ATTR_DISCOVERY_HASH in discovery_info: - discovery_hash = discovery_info[ATTR_DISCOVERY_HASH] - newswitch = MqttSwitch( config.get(CONF_NAME), config.get(CONF_ICON), diff --git a/homeassistant/components/switch/netio.py b/homeassistant/components/switch/netio.py index 2ccb4501d73..fc6086f9897 100644 --- a/homeassistant/components/switch/netio.py +++ b/homeassistant/components/switch/netio.py @@ -117,7 +117,7 @@ class NetioApiView(HomeAssistantView): ndev.start_dates = start_dates for dev in DEVICES[host].entities: - hass.async_add_job(dev.async_update_ha_state()) + hass.async_create_task(dev.async_update_ha_state()) return self.json(True) diff --git a/homeassistant/components/switch/pilight.py b/homeassistant/components/switch/pilight.py index 8ae8e64c2ff..16dfc075409 100644 --- a/homeassistant/components/switch/pilight.py +++ b/homeassistant/components/switch/pilight.py @@ -4,7 +4,6 @@ Support for switching devices via Pilight to on and off. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.pilight/ """ -import asyncio import logging import voluptuous as vol @@ -122,10 +121,9 @@ class PilightSwitch(SwitchDevice): if any(self._code_on_receive) or any(self._code_off_receive): hass.bus.listen(pilight.EVENT, self._handle_code) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity about to be added to hass.""" - state = yield from async_get_last_state(self._hass, self.entity_id) + state = await async_get_last_state(self._hass, self.entity_id) if state: self._state = state.state == STATE_ON diff --git a/homeassistant/components/switch/rachio.py b/homeassistant/components/switch/rachio.py index 956befeeb9f..4797aae9a8c 100644 --- a/homeassistant/components/switch/rachio.py +++ b/homeassistant/components/switch/rachio.py @@ -7,10 +7,10 @@ https://home-assistant.io/components/switch.rachio/ from abc import abstractmethod from datetime import timedelta import logging -import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice -from homeassistant.components.rachio import (DOMAIN as DOMAIN_RACHIO, +from homeassistant.components.switch import SwitchDevice +from homeassistant.components.rachio import (CONF_MANUAL_RUN_MINS, + DOMAIN as DOMAIN_RACHIO, KEY_DEVICE_ID, KEY_ENABLED, KEY_ID, @@ -27,29 +27,20 @@ from homeassistant.components.rachio import (DOMAIN as DOMAIN_RACHIO, SUBTYPE_ZONE_COMPLETED, SUBTYPE_SLEEP_MODE_ON, SUBTYPE_SLEEP_MODE_OFF) -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_connect DEPENDENCIES = ['rachio'] _LOGGER = logging.getLogger(__name__) -# Manual run length -CONF_MANUAL_RUN_MINS = 'manual_run_mins' -DEFAULT_MANUAL_RUN_MINS = 10 - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_MANUAL_RUN_MINS, default=DEFAULT_MANUAL_RUN_MINS): - cv.positive_int, -}) - ATTR_ZONE_SUMMARY = 'Summary' ATTR_ZONE_NUMBER = 'Zone number' def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Rachio switches.""" - manual_run_time = timedelta(minutes=config.get(CONF_MANUAL_RUN_MINS)) + manual_run_time = timedelta(minutes=hass.data[DOMAIN_RACHIO].config.get( + CONF_MANUAL_RUN_MINS)) _LOGGER.info("Rachio run time is %s", str(manual_run_time)) # Add all zones from all controllers as switches @@ -126,6 +117,11 @@ class RachioStandbySwitch(RachioSwitch): """Return the name of the standby switch.""" return "{} in standby mode".format(self._controller.name) + @property + def unique_id(self) -> str: + """Return a unique id by combinining controller id and purpose.""" + return "{}-standby".format(self._controller.controller_id) + @property def icon(self) -> str: """Return an icon for the standby switch.""" @@ -189,6 +185,12 @@ class RachioZone(RachioSwitch): """Return the friendly name of the zone.""" return self._zone_name + @property + def unique_id(self) -> str: + """Return a unique id by combinining controller id and zone number.""" + return "{}-zone-{}".format(self._controller.controller_id, + self.zone_id) + @property def icon(self) -> str: """Return the icon to display.""" diff --git a/homeassistant/components/switch/rest.py b/homeassistant/components/switch/rest.py index 0e00bfe7844..9b8f889a8ae 100644 --- a/homeassistant/components/switch/rest.py +++ b/homeassistant/components/switch/rest.py @@ -47,9 +47,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the RESTful switch.""" body_off = config.get(CONF_BODY_OFF) body_on = config.get(CONF_BODY_ON) @@ -77,7 +76,7 @@ def async_setup_platform(hass, config, async_add_entities, switch = RestSwitch(name, resource, method, headers, auth, body_on, body_off, is_on_template, timeout) - req = yield from switch.get_device_state(hass) + req = await switch.get_device_state(hass) if req.status >= 400: _LOGGER.error("Got non-ok response from resource: %s", req.status) else: @@ -116,13 +115,12 @@ class RestSwitch(SwitchDevice): """Return true if device is on.""" return self._state - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on.""" body_on_t = self._body_on.async_render() try: - req = yield from self.set_device_state(body_on_t) + req = await self.set_device_state(body_on_t) if req.status == 200: self._state = True @@ -131,15 +129,14 @@ class RestSwitch(SwitchDevice): "Can't turn on %s. Is resource/endpoint offline?", self._resource) except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.error("Error while turn on %s", self._resource) + _LOGGER.error("Error while switching on %s", self._resource) - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the device off.""" body_off_t = self._body_off.async_render() try: - req = yield from self.set_device_state(body_off_t) + req = await self.set_device_state(body_off_t) if req.status == 200: self._state = False else: @@ -147,35 +144,35 @@ class RestSwitch(SwitchDevice): "Can't turn off %s. Is resource/endpoint offline?", self._resource) except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.error("Error while turn off %s", self._resource) + _LOGGER.error("Error while switching off %s", self._resource) - @asyncio.coroutine - def set_device_state(self, body): + async def set_device_state(self, body): """Send a state update to the device.""" websession = async_get_clientsession(self.hass) with async_timeout.timeout(self._timeout, loop=self.hass.loop): - req = yield from getattr(websession, self._method)( + req = await getattr(websession, self._method)( self._resource, auth=self._auth, data=bytes(body, 'utf-8'), headers=self._headers) return req - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the current state, catching errors.""" try: - yield from self.get_device_state(self.hass) - except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.exception("Error while fetch data.") + await self.get_device_state(self.hass) + except asyncio.TimeoutError: + _LOGGER.exception("Timed out while fetching data") + except aiohttp.ClientError as err: + _LOGGER.exception("Error while fetching data: %s", err) - @asyncio.coroutine - def get_device_state(self, hass): + async def get_device_state(self, hass): """Get the latest data from REST API and update the state.""" websession = async_get_clientsession(hass) with async_timeout.timeout(self._timeout, loop=hass.loop): - req = yield from websession.get(self._resource, auth=self._auth) - text = yield from req.text() + req = await websession.get(self._resource, auth=self._auth, + headers=self._headers) + text = await req.text() if self._is_on_template is not None: text = self._is_on_template.async_render_with_possible_json_value( diff --git a/homeassistant/components/switch/rflink.py b/homeassistant/components/switch/rflink.py index 370436b3184..2bbe3e3f03d 100644 --- a/homeassistant/components/switch/rflink.py +++ b/homeassistant/components/switch/rflink.py @@ -4,7 +4,6 @@ Support for Rflink switches. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.rflink/ """ -import asyncio import logging from homeassistant.components.rflink import ( @@ -85,9 +84,8 @@ def devices_from_config(domain_config, hass=None): return devices -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Rflink platform.""" async_add_entities(devices_from_config(config, hass)) diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py index 7461aa2a720..724fcbf6075 100644 --- a/homeassistant/components/switch/template.py +++ b/homeassistant/components/switch/template.py @@ -4,7 +4,6 @@ Support for switches which integrates with other components. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.template/ """ -import asyncio import logging import voluptuous as vol @@ -43,9 +42,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Template switch.""" switches = [] @@ -103,8 +101,7 @@ class SwitchTemplate(SwitchDevice): self._entity_picture = None self._entities = entity_ids - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" @callback def template_switch_state_listener(entity, old_state, new_state): @@ -152,18 +149,15 @@ class SwitchTemplate(SwitchDevice): """Return the entity_picture to use in the frontend, if any.""" return self._entity_picture - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Fire the on action.""" - yield from self._on_script.async_run() + await self._on_script.async_run() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Fire the off action.""" - yield from self._off_script.async_run() + await self._off_script.async_run() - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update the state from the template.""" try: state = self._template.async_render().lower() diff --git a/homeassistant/components/switch/tradfri.py b/homeassistant/components/switch/tradfri.py new file mode 100644 index 00000000000..74997332b07 --- /dev/null +++ b/homeassistant/components/switch/tradfri.py @@ -0,0 +1,137 @@ +""" +Support for the IKEA Tradfri platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.tradfri/ +""" +import logging + +from homeassistant.core import callback +from homeassistant.components.switch import SwitchDevice +from homeassistant.components.tradfri import ( + KEY_GATEWAY, KEY_API, DOMAIN as TRADFRI_DOMAIN) +from homeassistant.components.tradfri.const import ( + CONF_GATEWAY_ID) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['tradfri'] +IKEA = 'IKEA of Sweden' +TRADFRI_SWITCH_MANAGER = 'Tradfri Switch Manager' + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Load Tradfri switches based on a config entry.""" + gateway_id = config_entry.data[CONF_GATEWAY_ID] + api = hass.data[KEY_API][config_entry.entry_id] + gateway = hass.data[KEY_GATEWAY][config_entry.entry_id] + + devices_commands = await api(gateway.get_devices()) + devices = await api(devices_commands) + switches = [dev for dev in devices if dev.has_socket_control] + if switches: + async_add_entities( + TradfriSwitch(switch, api, gateway_id) for switch in switches) + + +class TradfriSwitch(SwitchDevice): + """The platform class required by Home Assistant.""" + + def __init__(self, switch, api, gateway_id): + """Initialize a switch.""" + self._api = api + self._unique_id = "{}-{}".format(gateway_id, switch.id) + self._switch = None + self._socket_control = None + self._switch_data = None + self._name = None + self._available = True + self._gateway_id = gateway_id + + self._refresh(switch) + + @property + def unique_id(self): + """Return unique ID for switch.""" + return self._unique_id + + @property + def device_info(self): + """Return the device info.""" + info = self._switch.device_info + + return { + 'identifiers': { + (TRADFRI_DOMAIN, self._switch.id) + }, + 'name': self._name, + 'manufacturer': info.manufacturer, + 'model': info.model_number, + 'sw_version': info.firmware_version, + 'via_hub': (TRADFRI_DOMAIN, self._gateway_id), + } + + async def async_added_to_hass(self): + """Start thread when added to hass.""" + self._async_start_observe() + + @property + def available(self): + """Return True if entity is available.""" + return self._available + + @property + def should_poll(self): + """No polling needed for tradfri switch.""" + return False + + @property + def name(self): + """Return the display name of this switch.""" + return self._name + + @property + def is_on(self): + """Return true if switch is on.""" + return self._switch_data.state + + async def async_turn_off(self, **kwargs): + """Instruct the switch to turn off.""" + await self._api(self._socket_control.set_state(False)) + + async def async_turn_on(self, **kwargs): + """Instruct the switch to turn on.""" + await self._api(self._socket_control.set_state(True)) + + @callback + def _async_start_observe(self, exc=None): + """Start observation of switch.""" + from pytradfri.error import PytradfriError + if exc: + _LOGGER.warning("Observation failed for %s", self._name, + exc_info=exc) + + try: + cmd = self._switch.observe(callback=self._observe_update, + err_callback=self._async_start_observe, + duration=0) + self.hass.async_create_task(self._api(cmd)) + except PytradfriError as err: + _LOGGER.warning("Observation failed, trying again", exc_info=err) + self._async_start_observe() + + def _refresh(self, switch): + """Refresh the switch data.""" + self._switch = switch + + # Caching of switchControl and switch object + self._available = switch.reachable + self._socket_control = switch.socket_control + self._switch_data = switch.socket_control.sockets[0] + self._name = switch.name + + @callback + def _observe_update(self, tradfri_device): + """Receive new state data for this switch.""" + self._refresh(tradfri_device) + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/switch/volvooncall.py b/homeassistant/components/switch/volvooncall.py index 96091a725a1..42c753725ab 100644 --- a/homeassistant/components/switch/volvooncall.py +++ b/homeassistant/components/switch/volvooncall.py @@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Tellstick switches.""" + """Set up a Volvo switch.""" if discovery_info is None: return add_entities([VolvoSwitch(hass, *discovery_info)]) diff --git a/homeassistant/components/switch/wink.py b/homeassistant/components/switch/wink.py index 0df59d6b51c..9dea93488af 100644 --- a/homeassistant/components/switch/wink.py +++ b/homeassistant/components/switch/wink.py @@ -4,7 +4,6 @@ Support for Wink switches. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.wink/ """ -import asyncio import logging from homeassistant.components.wink import DOMAIN, WinkDevice @@ -40,8 +39,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class WinkToggleDevice(WinkDevice, ToggleEntity): """Representation of a Wink toggle device.""" - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['switch'].append(self) diff --git a/homeassistant/components/switch/xiaomi_aqara.py b/homeassistant/components/switch/xiaomi_aqara.py index a29a3c74a2e..17265d5dfa2 100644 --- a/homeassistant/components/switch/xiaomi_aqara.py +++ b/homeassistant/components/switch/xiaomi_aqara.py @@ -99,6 +99,11 @@ class XiaomiGenericSwitch(XiaomiDevice, SwitchDevice): attrs.update(super().device_state_attributes) return attrs + @property + def should_poll(self): + """Return the polling state. Polling needed for zigbee plug only.""" + return self._supports_power_consumption + def turn_on(self, **kwargs): """Turn the switch on.""" if self._write_to_hub(self._sid, **{self._data_key: 'on'}): @@ -131,3 +136,8 @@ class XiaomiGenericSwitch(XiaomiDevice, SwitchDevice): return False self._state = state return True + + def update(self): + """Get data from hub.""" + _LOGGER.debug("Update data from hub: %s", self._name) + self._get_from_hub(self._sid) diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 2a2a19aa2f5..8ab6bd752ef 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -4,7 +4,6 @@ Support for system log. For more details about this component, please refer to the documentation at https://home-assistant.io/components/system_log/ """ -import asyncio from collections import deque from io import StringIO import logging @@ -134,8 +133,7 @@ class LogErrorHandler(logging.Handler): self.hass.bus.fire(EVENT_SYSTEM_LOG, entry) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the logger component.""" conf = config.get(DOMAIN) if conf is None: @@ -147,8 +145,7 @@ def async_setup(hass, config): hass.http.register_view(AllErrorsView(handler)) - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Handle logger services.""" if service.service == 'clear': handler.records.clear() @@ -159,8 +156,7 @@ def async_setup(hass, config): level = service.data[CONF_LEVEL] getattr(logger, level)(service.data[CONF_MESSAGE]) - @asyncio.coroutine - def async_shutdown_handler(event): + async def async_shutdown_handler(event): """Remove logging handler when Home Assistant is shutdown.""" # This is needed as older logger instances will remain logging.getLogger().removeHandler(handler) @@ -188,8 +184,7 @@ class AllErrorsView(HomeAssistantView): """Initialize a new AllErrorsView.""" self.handler = handler - @asyncio.coroutine - def get(self, request): + async def get(self, request): """Get all errors and warnings.""" # deque is not serializable (it's just "list-like") so it must be # converted to a list before it can be serialized to json diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 8e24716ab57..40724a1ee86 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -4,7 +4,6 @@ Component to send and receive Telegram messages. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/telegram_bot/ """ -import asyncio import io from functools import partial import logging @@ -210,8 +209,7 @@ def load_data(hass, url=None, filepath=None, username=None, password=None, return None -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the Telegram bot component.""" if not config[DOMAIN]: return False @@ -220,7 +218,7 @@ def async_setup(hass, config): p_type = p_config.get(CONF_PLATFORM) - platform = yield from async_prepare_setup_platform( + platform = await async_prepare_setup_platform( hass, config, DOMAIN, p_type) if platform is None: @@ -228,7 +226,7 @@ def async_setup(hass, config): _LOGGER.info("Setting up %s.%s", DOMAIN, p_type) try: - receiver_service = yield from \ + receiver_service = await \ platform.async_setup_platform(hass, p_config) if receiver_service is False: _LOGGER.error( @@ -247,8 +245,7 @@ def async_setup(hass, config): p_config.get(ATTR_PARSER) ) - @asyncio.coroutine - def async_send_telegram_message(service): + async def async_send_telegram_message(service): """Handle sending Telegram Bot message service calls.""" def _render_template_attr(data, attribute): attribute_templ = data.get(attribute) @@ -274,23 +271,23 @@ def async_setup(hass, config): _LOGGER.debug("New telegram message %s: %s", msgtype, kwargs) if msgtype == SERVICE_SEND_MESSAGE: - yield from hass.async_add_job( + await hass.async_add_job( partial(notify_service.send_message, **kwargs)) elif msgtype in [SERVICE_SEND_PHOTO, SERVICE_SEND_STICKER, SERVICE_SEND_VIDEO, SERVICE_SEND_DOCUMENT]: - yield from hass.async_add_job( + await hass.async_add_job( partial(notify_service.send_file, msgtype, **kwargs)) elif msgtype == SERVICE_SEND_LOCATION: - yield from hass.async_add_job( + await hass.async_add_job( partial(notify_service.send_location, **kwargs)) elif msgtype == SERVICE_ANSWER_CALLBACK_QUERY: - yield from hass.async_add_job( + await hass.async_add_job( partial(notify_service.answer_callback_query, **kwargs)) elif msgtype == SERVICE_DELETE_MESSAGE: - yield from hass.async_add_job( + await hass.async_add_job( partial(notify_service.delete_message, **kwargs)) else: - yield from hass.async_add_job( + await hass.async_add_job( partial(notify_service.edit_message, msgtype, **kwargs)) # Register notification services @@ -311,10 +308,11 @@ def initialize_bot(p_config): proxy_url = p_config.get(CONF_PROXY_URL) proxy_params = p_config.get(CONF_PROXY_PARAMS) - request = None if proxy_url is not None: - request = Request(proxy_url=proxy_url, + request = Request(con_pool_size=4, proxy_url=proxy_url, urllib3_proxy_kwargs=proxy_params) + else: + request = Request(con_pool_size=4) return Bot(token=api_key, request=request) diff --git a/homeassistant/components/telegram_bot/broadcast.py b/homeassistant/components/telegram_bot/broadcast.py index 7e157fdb0c7..7cfcc272a33 100644 --- a/homeassistant/components/telegram_bot/broadcast.py +++ b/homeassistant/components/telegram_bot/broadcast.py @@ -4,7 +4,6 @@ Telegram bot implementation to send messages only. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/telegram_bot.broadcast/ """ -import asyncio import logging from homeassistant.components.telegram_bot import ( @@ -16,14 +15,13 @@ _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = TELEGRAM_PLATFORM_SCHEMA -@asyncio.coroutine -def async_setup_platform(hass, config): +async def async_setup_platform(hass, config): """Set up the Telegram broadcast platform.""" # Check the API key works bot = initialize_bot(config) - bot_config = yield from hass.async_add_job(bot.getMe) + bot_config = await hass.async_add_job(bot.getMe) _LOGGER.debug("Telegram broadcast platform setup with bot %s", bot_config['username']) return True diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index 6ee42b32504..d1dea051985 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -4,14 +4,8 @@ Telegram bot polling implementation. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/telegram_bot.polling/ """ -import asyncio -from asyncio.futures import CancelledError import logging -import async_timeout -from aiohttp.client_exceptions import ClientError -from aiohttp.hdrs import CONNECTION, KEEP_ALIVE - from homeassistant.components.telegram_bot import ( initialize_bot, CONF_ALLOWED_CHAT_IDS, BaseTelegramBotEntity, @@ -19,22 +13,13 @@ from homeassistant.components.telegram_bot import ( from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from homeassistant.core import callback -from homeassistant.helpers.aiohttp_client import async_get_clientsession _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = TELEGRAM_PLATFORM_SCHEMA -RETRY_SLEEP = 10 -class WrongHttpStatus(Exception): - """Thrown when a wrong http status is received.""" - - pass - - -@asyncio.coroutine -def async_setup_platform(hass, config): +async def async_setup_platform(hass, config): """Set up the Telegram polling platform.""" bot = initialize_bot(config) pol = TelegramPoll(bot, hass, config[CONF_ALLOWED_CHAT_IDS]) @@ -55,73 +40,67 @@ def async_setup_platform(hass, config): return True +def process_error(bot, update, error): + """Telegram bot error handler.""" + from telegram.error import ( + TelegramError, TimedOut, NetworkError, RetryAfter) + + try: + raise error + except (TimedOut, NetworkError, RetryAfter): + # Long polling timeout or connection problem. Nothing serious. + pass + except TelegramError: + _LOGGER.error('Update "%s" caused error "%s"', update, error) + + +def message_handler(handler): + """Create messages handler.""" + from telegram import Update + from telegram.ext import Handler + + class MessageHandler(Handler): + """Telegram bot message handler.""" + + def __init__(self): + """Initialize the messages handler instance.""" + super().__init__(handler) + + def check_update(self, update): + """Check is update valid.""" + return isinstance(update, Update) + + def handle_update(self, update, dispatcher): + """Handle update.""" + optional_args = self.collect_optional_args(dispatcher, update) + return self.callback(dispatcher.bot, update, **optional_args) + + return MessageHandler() + + class TelegramPoll(BaseTelegramBotEntity): """Asyncio telegram incoming message handler.""" def __init__(self, bot, hass, allowed_chat_ids): """Initialize the polling instance.""" + from telegram.ext import Updater + BaseTelegramBotEntity.__init__(self, hass, allowed_chat_ids) - self.update_id = 0 - self.websession = async_get_clientsession(hass) - self.update_url = '{0}/getUpdates'.format(bot.base_url) - self.polling_task = None # The actual polling task. - self.timeout = 15 # async post timeout - # Polling timeout should always be less than async post timeout. - self.post_data = {'timeout': self.timeout - 5} + + self.updater = Updater(bot=bot, workers=4) + self.dispatcher = self.updater.dispatcher + + self.dispatcher.add_handler(message_handler(self.process_update)) + self.dispatcher.add_error_handler(process_error) def start_polling(self): """Start the polling task.""" - self.polling_task = self.hass.async_add_job(self.check_incoming()) + self.updater.start_polling() def stop_polling(self): """Stop the polling task.""" - self.polling_task.cancel() + self.updater.stop() - @asyncio.coroutine - def get_updates(self, offset): - """Bypass the default long polling method to enable asyncio.""" - resp = None - if offset: - self.post_data['offset'] = offset - try: - with async_timeout.timeout(self.timeout, loop=self.hass.loop): - resp = yield from self.websession.post( - self.update_url, data=self.post_data, - headers={CONNECTION: KEEP_ALIVE} - ) - if resp.status == 200: - _json = yield from resp.json() - return _json - raise WrongHttpStatus('wrong status {}'.format(resp.status)) - finally: - if resp is not None: - yield from resp.release() - - @asyncio.coroutine - def check_incoming(self): - """Continuously check for incoming telegram messages.""" - try: - while True: - try: - _updates = yield from self.get_updates(self.update_id) - except (WrongHttpStatus, ClientError) as err: - # WrongHttpStatus: Non-200 status code. - # Occurs at times (mainly 502) and recovers - # automatically. Pause for a while before retrying. - _LOGGER.error(err) - yield from asyncio.sleep(RETRY_SLEEP) - except (asyncio.TimeoutError, ValueError): - # Long polling timeout. Nothing serious. - # Json error. Just retry for the next message. - pass - else: - # no exception raised. update received data. - _updates = _updates.get('result') - if _updates is None: - _LOGGER.error("Incorrect result received.") - else: - for update in _updates: - self.update_id = update['update_id'] + 1 - self.process_message(update) - except CancelledError: - _LOGGER.debug("Stopping Telegram polling bot") + def process_update(self, bot, update): + """Process incoming message.""" + self.process_message(update.to_dict()) diff --git a/homeassistant/components/telegram_bot/webhooks.py b/homeassistant/components/telegram_bot/webhooks.py index b7dd7ab8269..72e8c557fe5 100644 --- a/homeassistant/components/telegram_bot/webhooks.py +++ b/homeassistant/components/telegram_bot/webhooks.py @@ -4,7 +4,6 @@ Allows utilizing telegram webhooks. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/telegram_bot.webhooks/ """ -import asyncio import datetime as dt from ipaddress import ip_network import logging @@ -47,13 +46,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config): +async def async_setup_platform(hass, config): """Set up the Telegram webhooks platform.""" import telegram bot = initialize_bot(config) - current_status = yield from hass.async_add_job(bot.getWebhookInfo) + current_status = await hass.async_add_job(bot.getWebhookInfo) base_url = config.get(CONF_URL, hass.config.api.base_url) # Some logging of Bot current status: @@ -81,7 +79,7 @@ def async_setup_platform(hass, config): retry_num) if current_status and current_status['url'] != handler_url: - result = yield from hass.async_add_job(_try_to_set_webhook) + result = await hass.async_add_job(_try_to_set_webhook) if result: _LOGGER.info("Set new telegram webhook %s", handler_url) else: @@ -108,8 +106,7 @@ class BotPushReceiver(HomeAssistantView, BaseTelegramBotEntity): BaseTelegramBotEntity.__init__(self, hass, allowed_chat_ids) self.trusted_networks = trusted_networks - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Accept the POST from telegram.""" real_ip = request[KEY_REAL_IP] if not any(real_ip in net for net in self.trusted_networks): @@ -117,7 +114,7 @@ class BotPushReceiver(HomeAssistantView, BaseTelegramBotEntity): return self.json_message('Access denied', HTTP_UNAUTHORIZED) try: - data = yield from request.json() + data = await request.json() except ValueError: return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) diff --git a/homeassistant/components/tellstick.py b/homeassistant/components/tellstick.py index 0eef2c4ece1..8f1c45d7312 100644 --- a/homeassistant/components/tellstick.py +++ b/homeassistant/components/tellstick.py @@ -4,7 +4,6 @@ Tellstick Component. For more details about this component, please refer to the documentation at https://home-assistant.io/components/tellstick/ """ -import asyncio import logging import threading @@ -158,8 +157,7 @@ class TellstickDevice(Entity): self._tellcore_device = tellcore_device self._name = tellcore_device.name - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( SIGNAL_TELLCORE_CALLBACK, diff --git a/homeassistant/components/thethingsnetwork.py b/homeassistant/components/thethingsnetwork.py index 08715c74d1f..61f9843be45 100644 --- a/homeassistant/components/thethingsnetwork.py +++ b/homeassistant/components/thethingsnetwork.py @@ -4,7 +4,6 @@ Support for The Things network. For more details about this component, please refer to the documentation at https://home-assistant.io/components/thethingsnetwork/ """ -import asyncio import logging import voluptuous as vol @@ -32,8 +31,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Initialize of The Things Network component.""" conf = config[DOMAIN] app_id = conf.get(CONF_APP_ID) diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py new file mode 100644 index 00000000000..8022902c580 --- /dev/null +++ b/homeassistant/components/tibber/__init__.py @@ -0,0 +1,55 @@ +""" +Support for Tibber. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/tibber/ +""" +import asyncio +import logging + +import aiohttp +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, CONF_ACCESS_TOKEN, + CONF_NAME) +from homeassistant.helpers import discovery +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +REQUIREMENTS = ['pyTibber==0.7.2'] + +DOMAIN = 'tibber' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_ACCESS_TOKEN): cv.string, + }) +}, extra=vol.ALLOW_EXTRA) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass, config): + """Set up the Tibber component.""" + conf = config.get(DOMAIN) + + import tibber + tibber_connection = tibber.Tibber(conf[CONF_ACCESS_TOKEN], + websession=async_get_clientsession(hass)) + hass.data[DOMAIN] = tibber_connection + + async def _close(event): + await tibber_connection.rt_disconnect() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _close) + + try: + await tibber_connection.update_info() + except (asyncio.TimeoutError, aiohttp.ClientError): + return False + + for component in ['sensor', 'notify']: + discovery.load_platform(hass, component, DOMAIN, + {CONF_NAME: DOMAIN}, config) + + return True diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 8406b3ff5ec..c29df9db858 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -12,12 +12,10 @@ import voluptuous as vol import homeassistant.util.dt as dt_util import homeassistant.helpers.config_validation as cv from homeassistant.const import (ATTR_ENTITY_ID, CONF_ICON, CONF_NAME) -from homeassistant.core import callback from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) @@ -63,64 +61,6 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@bind_hass -def start(hass, entity_id, duration): - """Start a timer.""" - hass.add_job(async_start, hass, entity_id, {ATTR_ENTITY_ID: entity_id, - ATTR_DURATION: duration}) - - -@callback -@bind_hass -def async_start(hass, entity_id, duration): - """Start a timer.""" - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_START, {ATTR_ENTITY_ID: entity_id, - ATTR_DURATION: duration})) - - -@bind_hass -def pause(hass, entity_id): - """Pause a timer.""" - hass.add_job(async_pause, hass, entity_id) - - -@callback -@bind_hass -def async_pause(hass, entity_id): - """Pause a timer.""" - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_PAUSE, {ATTR_ENTITY_ID: entity_id})) - - -@bind_hass -def cancel(hass, entity_id): - """Cancel a timer.""" - hass.add_job(async_cancel, hass, entity_id) - - -@callback -@bind_hass -def async_cancel(hass, entity_id): - """Cancel a timer.""" - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_CANCEL, {ATTR_ENTITY_ID: entity_id})) - - -@bind_hass -def finish(hass, entity_id): - """Finish a timer.""" - hass.add_job(async_cancel, hass, entity_id) - - -@callback -@bind_hass -def async_finish(hass, entity_id): - """Finish a timer.""" - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_FINISH, {ATTR_ENTITY_ID: entity_id})) - - async def async_setup(hass, config): """Set up a timer.""" component = EntityComponent(_LOGGER, DOMAIN, hass) diff --git a/homeassistant/components/tradfri/.translations/de.json b/homeassistant/components/tradfri/.translations/de.json index 5284ae18b6d..4a19972774d 100644 --- a/homeassistant/components/tradfri/.translations/de.json +++ b/homeassistant/components/tradfri/.translations/de.json @@ -11,6 +11,7 @@ "step": { "auth": { "data": { + "host": "Host", "security_code": "Sicherheitscode" }, "description": "Du findest den Sicherheitscode auf der R\u00fcckseite deines Gateways.", diff --git a/homeassistant/components/tradfri/.translations/hu.json b/homeassistant/components/tradfri/.translations/hu.json index 0844e6d7095..dc7c033d41d 100644 --- a/homeassistant/components/tradfri/.translations/hu.json +++ b/homeassistant/components/tradfri/.translations/hu.json @@ -1,11 +1,20 @@ { "config": { + "abort": { + "already_configured": "A bridge m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni a gatewayhez.", + "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a k\u00f3d \u00e9rv\u00e9nyes\u00edt\u00e9se sor\u00e1n." + }, "step": { "auth": { "data": { "host": "Hoszt", "security_code": "Biztons\u00e1gi K\u00f3d" - } + }, + "description": "A biztons\u00e1gi k\u00f3dot a Gatewayed h\u00e1toldal\u00e1n tal\u00e1lod.", + "title": "Add meg a biztons\u00e1gi k\u00f3dot" } }, "title": "IKEA TR\u00c5DFRI" diff --git a/homeassistant/components/tradfri/.translations/pl.json b/homeassistant/components/tradfri/.translations/pl.json index ec253447ef4..4fd71567afe 100644 --- a/homeassistant/components/tradfri/.translations/pl.json +++ b/homeassistant/components/tradfri/.translations/pl.json @@ -17,6 +17,7 @@ "description": "Mo\u017cesz znale\u017a\u0107 kod bezpiecze\u0144stwa z ty\u0142u bramy.", "title": "Wprowad\u017a kod bezpiecze\u0144stwa" } - } + }, + "title": "IKEA TR\u00c5DFRI" } } \ No newline at end of file diff --git a/homeassistant/components/tradfri/.translations/sv.json b/homeassistant/components/tradfri/.translations/sv.json index ffe8bff22b4..34799050539 100644 --- a/homeassistant/components/tradfri/.translations/sv.json +++ b/homeassistant/components/tradfri/.translations/sv.json @@ -4,11 +4,14 @@ "already_configured": "Bryggan \u00e4r redan konfigurerad" }, "error": { - "cannot_connect": "Det gick inte att ansluta till gatewayen." + "cannot_connect": "Det gick inte att ansluta till gatewayen.", + "invalid_key": "Misslyckades med att registrera den angivna nyckeln. Om det h\u00e4r h\u00e4nder, f\u00f6rs\u00f6k starta om gatewayen igen.", + "timeout": "Timeout vid valididering av kod" }, "step": { "auth": { "data": { + "host": "V\u00e4rd", "security_code": "S\u00e4kerhetskod" }, "description": "Du kan hitta s\u00e4kerhetskoden p\u00e5 baksidan av din gateway.", diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index 6e91ab338a3..ba13b8d511a 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -9,6 +9,7 @@ import logging import voluptuous as vol from homeassistant import config_entries +from homeassistant.const import EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json @@ -17,18 +18,18 @@ from .const import ( from . import config_flow # noqa pylint_disable=unused-import -REQUIREMENTS = ['pytradfri[async]==5.5.1'] +REQUIREMENTS = ['pytradfri[async]==6.0.1'] DOMAIN = 'tradfri' CONFIG_FILE = '.tradfri_psk.conf' KEY_GATEWAY = 'tradfri_gateway' KEY_API = 'tradfri_api' CONF_ALLOW_TRADFRI_GROUPS = 'allow_tradfri_groups' -DEFAULT_ALLOW_TRADFRI_GROUPS = True +DEFAULT_ALLOW_TRADFRI_GROUPS = False CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Inclusive(CONF_HOST, 'gateway'): cv.string, + vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_ALLOW_TRADFRI_GROUPS, default=DEFAULT_ALLOW_TRADFRI_GROUPS): cv.boolean, }) @@ -63,13 +64,14 @@ async def async_setup(hass, config): )) host = conf.get(CONF_HOST) + import_groups = conf[CONF_ALLOW_TRADFRI_GROUPS] if host is None or host in configured_hosts or host in legacy_hosts: return True hass.async_create_task(hass.config_entries.flow.async_init( DOMAIN, context={'source': config_entries.SOURCE_IMPORT}, - data={'host': host} + data={CONF_HOST: host, CONF_IMPORT_GROUPS: import_groups} )) return True @@ -87,6 +89,13 @@ async def async_setup_entry(hass, entry): psk=entry.data[CONF_KEY], loop=hass.loop ) + + async def on_hass_stop(event): + """Close connection when hass stops.""" + await factory.shutdown() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + api = factory.request gateway = Gateway() @@ -119,5 +128,8 @@ async def async_setup_entry(hass, entry): hass.async_create_task(hass.config_entries.async_forward_entry_setup( entry, 'sensor' )) + hass.async_create_task(hass.config_entries.async_forward_entry_setup( + entry, 'switch' + )) return True diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index 29aa768dbb5..2e24fde8294 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -34,6 +34,7 @@ class FlowHandler(config_entries.ConfigFlow): def __init__(self): """Initialize flow.""" self._host = None + self._import_groups = False async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" @@ -52,7 +53,8 @@ class FlowHandler(config_entries.ConfigFlow): # We don't ask for import group anymore as group state # is not reliable, don't want to show that to the user. - auth[CONF_IMPORT_GROUPS] = False + # But we still allow specifying import group via config yaml. + auth[CONF_IMPORT_GROUPS] = self._import_groups return await self._entry_from_data(auth) @@ -97,6 +99,7 @@ class FlowHandler(config_entries.ConfigFlow): # Happens if user has host directly in configuration.yaml if 'key' not in user_input: self._host = user_input['host'] + self._import_groups = user_input[CONF_IMPORT_GROUPS] return await self.async_step_auth() try: @@ -166,10 +169,15 @@ async def get_gateway_info(hass, host, identity, key): psk=key, loop=hass.loop ) + api = factory.request gateway = Gateway() gateway_info_result = await api(gateway.get_gateway_info()) - except RequestError: + + await factory.shutdown() + except (OSError, RequestError): + # We're also catching OSError as PyTradfri doesn't catch that one yet + # Upstream PR: https://github.com/ggravlingen/pytradfri/pull/189 raise AuthError('cannot_connect') return { diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index e5eef0e6135..86667782d09 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -299,7 +299,7 @@ class SpeechManager: # Is file store in file cache elif use_cache and key in self.file_cache: filename = self.file_cache[key] - self.hass.async_add_job(self.async_file_to_mem(key)) + self.hass.async_create_task(self.async_file_to_mem(key)) # Load speech from provider into memory else: filename = await self.async_get_tts_audio( @@ -331,7 +331,7 @@ class SpeechManager: self._async_store_to_memcache(key, filename, data) if cache: - self.hass.async_add_job( + self.hass.async_create_task( self.async_save_tts_audio(key, filename, data)) return filename diff --git a/homeassistant/components/tts/marytts.py b/homeassistant/components/tts/marytts.py index 072ea0e76e7..61f01a9b292 100644 --- a/homeassistant/components/tts/marytts.py +++ b/homeassistant/components/tts/marytts.py @@ -45,8 +45,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_get_engine(hass, config): +async def async_get_engine(hass, config): """Set up MaryTTS speech component.""" return MaryTTSProvider(hass, config) @@ -74,8 +73,7 @@ class MaryTTSProvider(Provider): """Return list of supported languages.""" return SUPPORT_LANGUAGES - @asyncio.coroutine - def async_get_tts_audio(self, message, language, options=None): + async def async_get_tts_audio(self, message, language, options=None): """Load TTS from MaryTTS.""" websession = async_get_clientsession(self.hass) @@ -98,13 +96,13 @@ class MaryTTSProvider(Provider): 'LOCALE': actual_language } - request = yield from websession.get(url, params=url_param) + request = await websession.get(url, params=url_param) if request.status != 200: _LOGGER.error("Error %d on load url %s", request.status, request.url) return (None, None) - data = yield from request.read() + data = await request.read() except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Timeout for MaryTTS API") diff --git a/homeassistant/components/tts/voicerss.py b/homeassistant/components/tts/voicerss.py index 38f6e2290b5..22eba69e510 100644 --- a/homeassistant/components/tts/voicerss.py +++ b/homeassistant/components/tts/voicerss.py @@ -80,8 +80,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_get_engine(hass, config): +async def async_get_engine(hass, config): """Set up VoiceRSS TTS component.""" return VoiceRSSProvider(hass, config) @@ -113,8 +112,7 @@ class VoiceRSSProvider(Provider): """Return list of supported languages.""" return SUPPORT_LANGUAGES - @asyncio.coroutine - def async_get_tts_audio(self, message, language, options=None): + async def async_get_tts_audio(self, message, language, options=None): """Load TTS from VoiceRSS.""" websession = async_get_clientsession(self.hass) form_data = self._form_data.copy() @@ -124,7 +122,7 @@ class VoiceRSSProvider(Provider): try: with async_timeout.timeout(10, loop=self.hass.loop): - request = yield from websession.post( + request = await websession.post( VOICERSS_API_URL, data=form_data ) @@ -132,7 +130,7 @@ class VoiceRSSProvider(Provider): _LOGGER.error("Error %d on load url %s.", request.status, request.url) return (None, None) - data = yield from request.read() + data = await request.read() if data in ERROR_MSG: _LOGGER.error( diff --git a/homeassistant/components/tts/yandextts.py b/homeassistant/components/tts/yandextts.py index b5e965a5b50..d0ec8c74a96 100644 --- a/homeassistant/components/tts/yandextts.py +++ b/homeassistant/components/tts/yandextts.py @@ -71,8 +71,7 @@ SUPPORTED_OPTIONS = [ ] -@asyncio.coroutine -def async_get_engine(hass, config): +async def async_get_engine(hass, config): """Set up VoiceRSS speech component.""" return YandexSpeechKitProvider(hass, config) @@ -106,8 +105,7 @@ class YandexSpeechKitProvider(Provider): """Return list of supported options.""" return SUPPORTED_OPTIONS - @asyncio.coroutine - def async_get_tts_audio(self, message, language, options=None): + async def async_get_tts_audio(self, message, language, options=None): """Load TTS from yandex.""" websession = async_get_clientsession(self.hass) actual_language = language @@ -125,14 +123,14 @@ class YandexSpeechKitProvider(Provider): 'speed': options.get(CONF_SPEED, self._speed) } - request = yield from websession.get( + request = await websession.get( YANDEX_API_URL, params=url_param) if request.status != 200: _LOGGER.error("Error %d on load URL %s", request.status, request.url) return (None, None) - data = yield from request.read() + data = await request.read() except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Timeout for yandex speech kit API") diff --git a/homeassistant/components/tuya.py b/homeassistant/components/tuya.py index 33f34164b02..22a82dec8e2 100644 --- a/homeassistant/components/tuya.py +++ b/homeassistant/components/tuya.py @@ -159,7 +159,7 @@ class TuyaDevice(Entity): def _delete_callback(self, dev_id): """Remove this entity.""" if dev_id == self.object_id: - self.hass.async_add_job(self.async_remove()) + self.hass.async_create_task(self.async_remove()) @callback def _update_callback(self): diff --git a/homeassistant/components/upcloud.py b/homeassistant/components/upcloud.py index 0f503dcdc39..a0b61f86e56 100644 --- a/homeassistant/components/upcloud.py +++ b/homeassistant/components/upcloud.py @@ -4,7 +4,6 @@ Support for UpCloud. For more details about this component, please refer to the documentation at https://home-assistant.io/components/upcloud/ """ -import asyncio import logging from datetime import timedelta @@ -129,8 +128,7 @@ class UpCloudServerEntity(Entity): except (AttributeError, KeyError, TypeError): return DEFAULT_COMPONENT_NAME.format(self.uuid) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" async_dispatcher_connect( self.hass, SIGNAL_UPDATE_UPCLOUD, self._update_callback) diff --git a/homeassistant/components/upnp.py b/homeassistant/components/upnp.py deleted file mode 100644 index 2bf0572d498..00000000000 --- a/homeassistant/components/upnp.py +++ /dev/null @@ -1,144 +0,0 @@ -""" -Will open a port in your router for Home Assistant and provide statistics. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/upnp/ -""" -from ipaddress import ip_address -import logging -import asyncio - -import voluptuous as vol - -from homeassistant.const import (EVENT_HOMEASSISTANT_STOP) -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery -from homeassistant.util import get_local_ip - -REQUIREMENTS = ['pyupnp-async==0.1.1.1'] -DEPENDENCIES = ['http'] - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['api'] -DOMAIN = 'upnp' - -DATA_UPNP = 'upnp_device' - -CONF_LOCAL_IP = 'local_ip' -CONF_ENABLE_PORT_MAPPING = 'port_mapping' -CONF_PORTS = 'ports' -CONF_UNITS = 'unit' -CONF_HASS = 'hass' - -NOTIFICATION_ID = 'upnp_notification' -NOTIFICATION_TITLE = 'UPnP Setup' - -IGD_DEVICE = 'urn:schemas-upnp-org:device:InternetGatewayDevice:1' -PPP_SERVICE = 'urn:schemas-upnp-org:service:WANPPPConnection:1' -IP_SERVICE = 'urn:schemas-upnp-org:service:WANIPConnection:1' -IP_SERVICE2 = 'urn:schemas-upnp-org:service:WANIPConnection:2' -CIC_SERVICE = 'urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1' - -UNITS = { - "Bytes": 1, - "KBytes": 1024, - "MBytes": 1024**2, - "GBytes": 1024**3, -} - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_ENABLE_PORT_MAPPING, default=False): cv.boolean, - vol.Optional(CONF_UNITS, default="MBytes"): vol.In(UNITS), - vol.Optional(CONF_LOCAL_IP): vol.All(ip_address, cv.string), - vol.Optional(CONF_PORTS): - vol.Schema({vol.Any(CONF_HASS, cv.positive_int): cv.positive_int}) - }), -}, extra=vol.ALLOW_EXTRA) - - -async def async_setup(hass, config): - """Register a port mapping for Home Assistant via UPnP.""" - config = config[DOMAIN] - host = config.get(CONF_LOCAL_IP) - - if host is None: - host = get_local_ip() - - if host == '127.0.0.1': - _LOGGER.error( - 'Unable to determine local IP. Add it to your configuration.') - return False - - import pyupnp_async - from pyupnp_async.error import UpnpSoapError - - service = None - resp = await pyupnp_async.msearch_first(search_target=IGD_DEVICE) - if not resp: - return False - - try: - device = await resp.get_device() - hass.data[DATA_UPNP] = device - for _service in device.services: - if _service['serviceType'] == PPP_SERVICE: - service = device.find_first_service(PPP_SERVICE) - if _service['serviceType'] == IP_SERVICE: - service = device.find_first_service(IP_SERVICE) - if _service['serviceType'] == IP_SERVICE2: - service = device.find_first_service(IP_SERVICE2) - if _service['serviceType'] == CIC_SERVICE: - unit = config[CONF_UNITS] - hass.async_create_task(discovery.async_load_platform( - hass, 'sensor', DOMAIN, {'unit': unit}, config)) - except UpnpSoapError as error: - _LOGGER.error(error) - return False - - if not service: - _LOGGER.warning("Could not find any UPnP IGD") - return False - - port_mapping = config[CONF_ENABLE_PORT_MAPPING] - if not port_mapping: - return True - - internal_port = hass.http.server_port - - ports = config.get(CONF_PORTS) - if ports is None: - ports = {CONF_HASS: internal_port} - - registered = [] - for internal, external in ports.items(): - if internal == CONF_HASS: - internal = internal_port - try: - await service.add_port_mapping(internal, external, host, 'TCP', - desc='Home Assistant') - registered.append(external) - _LOGGER.debug("Mapping external TCP port %s -> %s @ %s", - external, internal, host) - except UpnpSoapError as error: - _LOGGER.error(error) - hass.components.persistent_notification.create( - 'ERROR: tcp port {} is already mapped in your router.' - '
Please disable port_mapping in the upnp ' - 'configuration section.
' - 'You will need to restart hass after fixing.' - ''.format(external), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - - async def deregister_port(event): - """De-register the UPnP port mapping.""" - tasks = [service.delete_port_mapping(external, 'TCP') - for external in registered] - if tasks: - await asyncio.wait(tasks) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deregister_port) - - return True diff --git a/homeassistant/components/upnp/.translations/ca.json b/homeassistant/components/upnp/.translations/ca.json new file mode 100644 index 00000000000..5dba9d1e16e --- /dev/null +++ b/homeassistant/components/upnp/.translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD ja est\u00e0 configurat", + "no_devices_discovered": "No s'ha trobat cap UPnP/IGD", + "no_sensors_or_port_mapping": "Activa, com a m\u00ednim, els sensors o l'assignaci\u00f3 de ports" + }, + "step": { + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Activa l'assignaci\u00f3 de ports per a Home Assistant", + "enable_sensors": "Afegiu sensors de tr\u00e0nsit", + "igd": "UPnP/IGD" + }, + "title": "Opcions de configuraci\u00f3 per a UPnP/IGD" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/de.json b/homeassistant/components/upnp/.translations/de.json new file mode 100644 index 00000000000..675d1eb7d0c --- /dev/null +++ b/homeassistant/components/upnp/.translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD ist bereits konfiguriert", + "no_devices_discovered": "Keine UPnP/IGDs entdeckt", + "no_sensors_or_port_mapping": "Aktiviere mindestens Sensoren oder Port-Mapping" + }, + "step": { + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Aktiviere Port-Mapping f\u00fcr Home Assistant", + "enable_sensors": "Verkehrssensoren hinzuf\u00fcgen", + "igd": "UPnP/IGD" + }, + "title": "Konfigurationsoptionen f\u00fcr UPnP/IGD" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/en.json b/homeassistant/components/upnp/.translations/en.json new file mode 100644 index 00000000000..93e1db62f8e --- /dev/null +++ b/homeassistant/components/upnp/.translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD is already configured", + "no_devices_discovered": "No UPnP/IGDs discovered", + "no_sensors_or_port_mapping": "Enable at least sensors or port mapping" + }, + "step": { + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Enable port mapping for Home Assistant", + "enable_sensors": "Add traffic sensors", + "igd": "UPnP/IGD" + }, + "title": "Configuration options for the UPnP/IGD" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/fr.json b/homeassistant/components/upnp/.translations/fr.json new file mode 100644 index 00000000000..3eac9577890 --- /dev/null +++ b/homeassistant/components/upnp/.translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP / IGD est d\u00e9j\u00e0 configur\u00e9", + "no_devices_discovered": "Aucun UPnP / IGD d\u00e9couvert", + "no_sensors_or_port_mapping": "Activer au moins les capteurs ou la cartographie des ports" + }, + "step": { + "init": { + "title": "UPnP / IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Activer le mappage de port pour Home Assistant", + "enable_sensors": "Ajouter des capteurs de trafic", + "igd": "UPnP / IGD" + }, + "title": "Options de configuration pour UPnP / IGD" + } + }, + "title": "UPnP / IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/ko.json b/homeassistant/components/upnp/.translations/ko.json new file mode 100644 index 00000000000..0dd7a16de0b --- /dev/null +++ b/homeassistant/components/upnp/.translations/ko.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD \uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", + "no_devices_discovered": "\ubc1c\uacac\ub41c UPnP/IGD \uac00 \uc5c6\uc2b5\ub2c8\ub2e4", + "no_sensors_or_port_mapping": "\ucd5c\uc18c\ud55c \uc13c\uc11c \ud639\uc740 \ud3ec\ud2b8 \ub9e4\ud551\uc744 \ud65c\uc131\ud654 \ud574\uc57c \ud569\ub2c8\ub2e4" + }, + "error": { + "other": "\ub2e4\ub978" + }, + "step": { + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Home Assistant \ud3ec\ud2b8 \ub9e4\ud551 \ud65c\uc131\ud654", + "enable_sensors": "\ud2b8\ub798\ud53d \uc13c\uc11c \ucd94\uac00", + "igd": "UPnP/IGD" + }, + "title": "UPnP/IGD \uc758 \uad6c\uc131 \uc635\uc158" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/lb.json b/homeassistant/components/upnp/.translations/lb.json new file mode 100644 index 00000000000..1d13492a487 --- /dev/null +++ b/homeassistant/components/upnp/.translations/lb.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD ass scho konfigur\u00e9iert", + "no_devices_discovered": "Keng UPnP/IGDs entdeckt", + "no_sensors_or_port_mapping": "Aktiv\u00e9ier op mannst Sensoren oder Port Mapping" + }, + "error": { + "one": "Een", + "other": "Aaner" + }, + "step": { + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Port Mapping fir Home Assistant aktiv\u00e9ieren", + "enable_sensors": "Trafic Sensoren dob\u00e4isetzen", + "igd": "UPnP/IGD" + }, + "title": "Konfiguratiouns Optiounen fir UPnP/IGD" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/nl.json b/homeassistant/components/upnp/.translations/nl.json new file mode 100644 index 00000000000..647eb647f24 --- /dev/null +++ b/homeassistant/components/upnp/.translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP / IGD is al geconfigureerd", + "no_devices_discovered": "Geen UPnP / IGD's ontdekt", + "no_sensors_or_port_mapping": "Schakel ten minste sensoren of poorttoewijzing in" + }, + "step": { + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Poorttoewijzing voor Home Assistant inschakelen", + "enable_sensors": "Voeg verkeerssensoren toe", + "igd": "UPnP/IGD" + }, + "title": "Configuratiemogelijkheden voor de UPnP/IGD" + } + }, + "title": "UPnP / IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/no.json b/homeassistant/components/upnp/.translations/no.json new file mode 100644 index 00000000000..fbb1b4afc75 --- /dev/null +++ b/homeassistant/components/upnp/.translations/no.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP / IGD er allerede konfigurert", + "no_devices_discovered": "Ingen UPnP / IGDs oppdaget" + }, + "error": { + "few": "f\u00e5", + "many": "mange", + "one": "en", + "other": "andre", + "two": "to", + "zero": "ingen" + }, + "step": { + "init": { + "title": "UPnP / IGD" + }, + "user": { + "data": { + "enable_sensors": "Legg til trafikk sensorer", + "igd": "UPnP / IGD" + }, + "title": "Konfigurasjonsalternativer for UPnP / IGD" + } + }, + "title": "UPnP / IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/pl.json b/homeassistant/components/upnp/.translations/pl.json new file mode 100644 index 00000000000..e47a25b9d93 --- /dev/null +++ b/homeassistant/components/upnp/.translations/pl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD jest ju\u017c skonfigurowane", + "no_devices_discovered": "Nie wykryto urz\u0105dze\u0144 UPnP/IGD", + "no_sensors_or_port_mapping": "W\u0142\u0105cz przynajmniej sensory lub mapowanie port\u00f3w" + }, + "error": { + "few": "kilka", + "many": "wiele", + "one": "jeden", + "other": "inne" + }, + "step": { + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "W\u0142\u0105cz mapowanie port\u00f3w dla Home Assistant'a", + "enable_sensors": "Dodaj sensor ruchu sieciowego", + "igd": "UPnP/IGD" + }, + "title": "Opcje konfiguracji dla UPnP/IGD" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/ru.json b/homeassistant/components/upnp/.translations/ru.json new file mode 100644 index 00000000000..9b7d358da0a --- /dev/null +++ b/homeassistant/components/upnp/.translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "no_devices_discovered": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e UPnP / IGD", + "no_sensors_or_port_mapping": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438\u043b\u0438 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043f\u043e\u0440\u0442\u043e\u0432 " + }, + "step": { + "init": { + "title": "UPnP / IGD" + }, + "user": { + "data": { + "enable_port_mapping": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043f\u043e\u0440\u0442\u043e\u0432 \u0434\u043b\u044f Home Assistant", + "enable_sensors": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0441\u0435\u0442\u0435\u0432\u043e\u0433\u043e \u0442\u0440\u0430\u0444\u0438\u043a\u0430", + "igd": "UPnP / IGD" + }, + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0434\u043b\u044f UPnP / IGD" + } + }, + "title": "UPnP / IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/sl.json b/homeassistant/components/upnp/.translations/sl.json new file mode 100644 index 00000000000..20debe7f09a --- /dev/null +++ b/homeassistant/components/upnp/.translations/sl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD je \u017ee konfiguriran", + "no_devices_discovered": "Ni odkritih UPnP/IGD naprav", + "no_sensors_or_port_mapping": "Omogo\u010dite vsaj senzorje ali preslikavo vrat (port mapping)" + }, + "error": { + "few": "nekaj", + "one": "ena", + "other": "ve\u010d", + "two": "dve" + }, + "step": { + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Omogo\u010dajo preslikavo vrat (port mapping) za Home Assistent-a", + "enable_sensors": "Dodaj prometne senzorje", + "igd": "UPnP/IGD" + }, + "title": "Mo\u017enosti konfiguracije za UPnP/IGD" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/sv.json b/homeassistant/components/upnp/.translations/sv.json new file mode 100644 index 00000000000..63c63781845 --- /dev/null +++ b/homeassistant/components/upnp/.translations/sv.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD \u00e4r redan konfigurerad", + "no_devices_discovered": "Inga UPnP/IGDs uppt\u00e4cktes", + "no_sensors_or_port_mapping": "Aktivera minst sensorer eller portmappning" + }, + "error": { + "one": "En", + "other": "Andra" + }, + "step": { + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "Aktivera portmappning f\u00f6r Home Assistant", + "enable_sensors": "L\u00e4gg till trafiksensorer", + "igd": "UPnP/IGD" + }, + "title": "Konfigurationsalternativ f\u00f6r UPnP/IGD" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/zh-Hans.json b/homeassistant/components/upnp/.translations/zh-Hans.json new file mode 100644 index 00000000000..c4962ba1c4b --- /dev/null +++ b/homeassistant/components/upnp/.translations/zh-Hans.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD \u5df2\u914d\u7f6e\u5b8c\u6210", + "no_devices_discovered": "\u672a\u53d1\u73b0 UPnP/IGD", + "no_sensors_or_port_mapping": "\u81f3\u5c11\u542f\u7528\u4f20\u611f\u5668\u6216\u7aef\u53e3\u6620\u5c04" + }, + "step": { + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "\u4e3a Home Assistant \u542f\u7528\u7aef\u53e3\u6620\u5c04", + "enable_sensors": "\u6dfb\u52a0\u6d41\u91cf\u4f20\u611f\u5668", + "igd": "UPnP/IGD" + }, + "title": "UPnP/IGD \u7684\u914d\u7f6e\u9009\u9879" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/zh-Hant.json b/homeassistant/components/upnp/.translations/zh-Hant.json new file mode 100644 index 00000000000..ca8171265ae --- /dev/null +++ b/homeassistant/components/upnp/.translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "UPnP/IGD \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "no_devices_discovered": "\u672a\u641c\u5c0b\u5230 UPnP/IGD", + "no_sensors_or_port_mapping": "\u81f3\u5c11\u958b\u555f\u611f\u61c9\u5668\u6216\u901a\u8a0a\u57e0\u8f49\u767c" + }, + "step": { + "init": { + "title": "UPnP/IGD" + }, + "user": { + "data": { + "enable_port_mapping": "\u958b\u555f Home Assistant \u901a\u8a0a\u57e0\u8f49\u767c", + "enable_sensors": "\u65b0\u589e\u4ea4\u901a\u611f\u61c9\u5668", + "igd": "UPnP/IGD" + }, + "title": "UPnP/IGD \u8a2d\u5b9a\u9078\u9805" + } + }, + "title": "UPnP/IGD" + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py new file mode 100644 index 00000000000..f70fbcc4d20 --- /dev/null +++ b/homeassistant/components/upnp/__init__.py @@ -0,0 +1,169 @@ +""" +Will open a port in your router for Home Assistant and provide statistics. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/upnp/ +""" +import asyncio +from ipaddress import ip_address + +import aiohttp +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import dispatcher +from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.components.discovery import DOMAIN as DISCOVERY_DOMAIN + +from .const import ( + CONF_ENABLE_PORT_MAPPING, CONF_ENABLE_SENSORS, + CONF_HASS, CONF_LOCAL_IP, CONF_PORTS, + CONF_UDN, CONF_SSDP_DESCRIPTION, + SIGNAL_REMOVE_SENSOR, +) +from .const import DOMAIN +from .const import LOGGER as _LOGGER +from .config_flow import ensure_domain_data +from .device import Device + + +REQUIREMENTS = ['async-upnp-client==0.12.4'] +DEPENDENCIES = ['http'] + +NOTIFICATION_ID = 'upnp_notification' +NOTIFICATION_TITLE = 'UPnP/IGD Setup' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_ENABLE_PORT_MAPPING, default=False): cv.boolean, + vol.Optional(CONF_ENABLE_SENSORS, default=True): cv.boolean, + vol.Optional(CONF_LOCAL_IP): vol.All(ip_address, cv.string), + vol.Optional(CONF_PORTS): + vol.Schema({ + vol.Any(CONF_HASS, cv.positive_int): + vol.Any(CONF_HASS, cv.positive_int) + }) + }), +}, extra=vol.ALLOW_EXTRA) + + +def _substitute_hass_ports(ports, hass_port): + """Substitute 'hass' for the hass_port.""" + ports = ports.copy() + + # substitute 'hass' for hass_port, both keys and values + if CONF_HASS in ports: + ports[hass_port] = ports[CONF_HASS] + del ports[CONF_HASS] + + for port in ports: + if ports[port] == CONF_HASS: + ports[port] = hass_port + + return ports + + +# config +async def async_setup(hass: HomeAssistantType, config: ConfigType): + """Register a port mapping for Home Assistant via UPnP.""" + ensure_domain_data(hass) + + # ensure sane config + if DOMAIN not in config: + return True + + if DISCOVERY_DOMAIN not in config: + _LOGGER.warning('UPNP needs discovery, please enable it') + return False + + # overridden local ip + upnp_config = config[DOMAIN] + if CONF_LOCAL_IP in upnp_config: + hass.data[DOMAIN]['local_ip'] = upnp_config[CONF_LOCAL_IP] + + # determine ports + ports = {CONF_HASS: CONF_HASS} # default, port_mapping disabled by default + if CONF_PORTS in upnp_config: + # copy from config + ports = upnp_config[CONF_PORTS] + + hass.data[DOMAIN]['auto_config'] = { + 'active': True, + 'enable_sensors': upnp_config[CONF_ENABLE_SENSORS], + 'enable_port_mapping': upnp_config[CONF_ENABLE_PORT_MAPPING], + 'ports': ports, + } + + return True + + +# config flow +async def async_setup_entry(hass: HomeAssistantType, + config_entry: ConfigEntry): + """Set up UPnP/IGD-device from a config entry.""" + ensure_domain_data(hass) + data = config_entry.data + + # build UPnP/IGD device + ssdp_description = data[CONF_SSDP_DESCRIPTION] + try: + device = await Device.async_create_device(hass, ssdp_description) + except (asyncio.TimeoutError, aiohttp.ClientError): + _LOGGER.error('Unable to create upnp-device') + return False + + hass.data[DOMAIN]['devices'][device.udn] = device + + # port mapping + if data.get(CONF_ENABLE_PORT_MAPPING): + local_ip = hass.data[DOMAIN].get('local_ip') + ports = hass.data[DOMAIN]['auto_config']['ports'] + _LOGGER.debug('Enabling port mappings: %s', ports) + + hass_port = hass.http.server_port + ports = _substitute_hass_ports(ports, hass_port) + await device.async_add_port_mappings(ports, local_ip=local_ip) + + # sensors + if data.get(CONF_ENABLE_SENSORS): + _LOGGER.debug('Enabling sensors') + + # register sensor setup handlers + hass.async_create_task(hass.config_entries.async_forward_entry_setup( + config_entry, 'sensor')) + + async def unload_entry(event): + """Unload entry on quit.""" + await async_unload_entry(hass, config_entry) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unload_entry) + + return True + + +async def async_unload_entry(hass: HomeAssistantType, + config_entry: ConfigEntry): + """Unload a config entry.""" + data = config_entry.data + udn = data[CONF_UDN] + + if udn not in hass.data[DOMAIN]['devices']: + return True + device = hass.data[DOMAIN]['devices'][udn] + + # port mapping + if data.get(CONF_ENABLE_PORT_MAPPING): + _LOGGER.debug('Deleting port mappings') + await device.async_delete_port_mappings() + + # sensors + if data.get(CONF_ENABLE_SENSORS): + _LOGGER.debug('Deleting sensors') + dispatcher.async_dispatcher_send(hass, SIGNAL_REMOVE_SENSOR, device) + + # clear stored device + del hass.data[DOMAIN]['devices'][udn] + + return True diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py new file mode 100644 index 00000000000..f695e3ada75 --- /dev/null +++ b/homeassistant/components/upnp/config_flow.py @@ -0,0 +1,160 @@ +"""Config flow for UPNP.""" +from collections import OrderedDict + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant import data_entry_flow + +from .const import ( + CONF_ENABLE_PORT_MAPPING, CONF_ENABLE_SENSORS, + CONF_SSDP_DESCRIPTION, CONF_UDN +) +from .const import DOMAIN + + +def ensure_domain_data(hass): + """Ensure hass.data is filled properly.""" + hass.data[DOMAIN] = hass.data.get(DOMAIN, {}) + hass.data[DOMAIN]['devices'] = hass.data[DOMAIN].get('devices', {}) + hass.data[DOMAIN]['discovered'] = hass.data[DOMAIN].get('discovered', {}) + hass.data[DOMAIN]['auto_config'] = hass.data[DOMAIN].get('auto_config', { + 'active': False, + 'enable_sensors': False, + 'enable_port_mapping': False, + 'ports': {'hass': 'hass'}, + }) + + +@config_entries.HANDLERS.register(DOMAIN) +class UpnpFlowHandler(data_entry_flow.FlowHandler): + """Handle a UPnP/IGD config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + @property + def _configured_upnp_igds(self): + """Get all configured IGDs.""" + return { + entry.data[CONF_UDN]: { + 'udn': entry.data[CONF_UDN], + } + for entry in self.hass.config_entries.async_entries(DOMAIN) + } + + @property + def _discovered_upnp_igds(self): + """Get all discovered entries.""" + return self.hass.data[DOMAIN]['discovered'] + + def _store_discovery_info(self, discovery_info): + """Add discovery info.""" + udn = discovery_info['udn'] + self.hass.data[DOMAIN]['discovered'][udn] = discovery_info + + def _auto_config_settings(self): + """Check if auto_config has been enabled.""" + return self.hass.data[DOMAIN]['auto_config'] + + async def async_step_discovery(self, discovery_info): + """ + Handle a discovered UPnP/IGD. + + This flow is triggered by the discovery component. It will check if the + host is already configured and delegate to the import step if not. + """ + ensure_domain_data(self.hass) + + # store discovered device + discovery_info['friendly_name'] = \ + '{} ({})'.format(discovery_info['host'], discovery_info['name']) + self._store_discovery_info(discovery_info) + + # ensure not already discovered/configured + udn = discovery_info['udn'] + if udn in self._configured_upnp_igds: + return self.async_abort(reason='already_configured') + + # auto config? + auto_config = self._auto_config_settings() + if auto_config['active']: + import_info = { + 'name': discovery_info['friendly_name'], + 'enable_sensors': auto_config['enable_sensors'], + 'enable_port_mapping': auto_config['enable_port_mapping'], + } + + return await self._async_save_entry(import_info) + + return await self.async_step_user() + + async def async_step_user(self, user_input=None): + """Manual set up.""" + ensure_domain_data(self.hass) + + # if user input given, handle it + user_input = user_input or {} + if 'name' in user_input: + if not user_input['enable_sensors'] and \ + not user_input['enable_port_mapping']: + return self.async_abort(reason='no_sensors_or_port_mapping') + + # ensure not already configured + configured_names = [ + entry['friendly_name'] + for udn, entry in self._discovered_upnp_igds.items() + if udn in self._configured_upnp_igds + ] + if user_input['name'] in configured_names: + return self.async_abort(reason='already_configured') + + return await self._async_save_entry(user_input) + + # let user choose from all discovered, non-configured, UPnP/IGDs + names = [ + entry['friendly_name'] + for udn, entry in self._discovered_upnp_igds.items() + if udn not in self._configured_upnp_igds + ] + if not names: + return self.async_abort(reason='no_devices_discovered') + + return self.async_show_form( + step_id='user', + data_schema=vol.Schema( + OrderedDict([ + (vol.Required('name'), vol.In(names)), + (vol.Optional('enable_sensors', default=False), bool), + (vol.Optional('enable_port_mapping', default=False), bool), + ]) + )) + + async def async_step_import(self, import_info): + """Import a new UPnP/IGD as a config entry.""" + ensure_domain_data(self.hass) + + return await self._async_save_entry(import_info) + + async def _async_save_entry(self, import_info): + """Store UPNP/IGD as new entry.""" + ensure_domain_data(self.hass) + + # ensure we know the host + name = import_info['name'] + discovery_infos = [info + for info in self._discovered_upnp_igds.values() + if info['friendly_name'] == name] + if not discovery_infos: + return self.async_abort(reason='host_not_found') + discovery_info = discovery_infos[0] + + return self.async_create_entry( + title=discovery_info['name'], + data={ + CONF_SSDP_DESCRIPTION: discovery_info['ssdp_description'], + CONF_UDN: discovery_info['udn'], + CONF_ENABLE_SENSORS: import_info['enable_sensors'], + CONF_ENABLE_PORT_MAPPING: import_info['enable_port_mapping'], + }, + ) diff --git a/homeassistant/components/upnp/const.py b/homeassistant/components/upnp/const.py new file mode 100644 index 00000000000..7a906ae02be --- /dev/null +++ b/homeassistant/components/upnp/const.py @@ -0,0 +1,14 @@ +"""Constants for the IGD component.""" +import logging + + +CONF_ENABLE_PORT_MAPPING = 'port_mapping' +CONF_ENABLE_SENSORS = 'sensors' +CONF_HASS = 'hass' +CONF_LOCAL_IP = 'local_ip' +CONF_PORTS = 'ports' +CONF_SSDP_DESCRIPTION = 'ssdp_description' +CONF_UDN = 'udn' +DOMAIN = 'upnp' +LOGGER = logging.getLogger('homeassistant.components.upnp') +SIGNAL_REMOVE_SENSOR = 'upnp_remove_sensor' diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py new file mode 100644 index 00000000000..4a444aa3087 --- /dev/null +++ b/homeassistant/components/upnp/device.py @@ -0,0 +1,131 @@ +"""Hass representation of an UPnP/IGD.""" +import asyncio +from ipaddress import IPv4Address + +import aiohttp + +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import get_local_ip + +from .const import LOGGER as _LOGGER + + +class Device: + """Hass representation of an UPnP/IGD.""" + + def __init__(self, igd_device): + """Initializer.""" + self._igd_device = igd_device + self._mapped_ports = [] + + @classmethod + async def async_create_device(cls, + hass: HomeAssistantType, + ssdp_description: str): + """Create UPnP/IGD device.""" + # build async_upnp_client requester + from async_upnp_client.aiohttp import AiohttpSessionRequester + session = async_get_clientsession(hass) + requester = AiohttpSessionRequester(session, True) + + # create async_upnp_client device + from async_upnp_client import UpnpFactory + factory = UpnpFactory(requester, + disable_state_variable_validation=True) + upnp_device = await factory.async_create_device(ssdp_description) + + # wrap with async_upnp_client IgdDevice + from async_upnp_client.igd import IgdDevice + igd_device = IgdDevice(upnp_device, None) + + return cls(igd_device) + + @property + def udn(self): + """Get the UDN.""" + return self._igd_device.udn + + @property + def name(self): + """Get the name.""" + return self._igd_device.name + + async def async_add_port_mappings(self, ports, local_ip=None): + """Add port mappings.""" + # determine local ip, ensure sane IP + if local_ip is None: + local_ip = get_local_ip() + + if local_ip == '127.0.0.1': + _LOGGER.error( + 'Could not create port mapping, our IP is 127.0.0.1') + local_ip = IPv4Address(local_ip) + + # create port mappings + for external_port, internal_port in ports.items(): + await self._async_add_port_mapping(external_port, + local_ip, + internal_port) + self._mapped_ports.append(external_port) + + async def _async_add_port_mapping(self, + external_port, + local_ip, + internal_port): + """Add a port mapping.""" + # create port mapping + from async_upnp_client import UpnpError + _LOGGER.info('Creating port mapping %s:%s:%s (TCP)', + external_port, local_ip, internal_port) + try: + await self._igd_device.async_add_port_mapping( + remote_host=None, + external_port=external_port, + protocol='TCP', + internal_port=internal_port, + internal_client=local_ip, + enabled=True, + description="Home Assistant", + lease_duration=None) + + self._mapped_ports.append(external_port) + except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError): + _LOGGER.error('Could not add port mapping: %s:%s:%s', + external_port, local_ip, internal_port) + + async def async_delete_port_mappings(self): + """Remove a port mapping.""" + for port in self._mapped_ports: + await self._async_delete_port_mapping(port) + + async def _async_delete_port_mapping(self, external_port): + """Remove a port mapping.""" + from async_upnp_client import UpnpError + _LOGGER.info('Deleting port mapping %s (TCP)', external_port) + try: + await self._igd_device.async_delete_port_mapping( + remote_host=None, + external_port=external_port, + protocol='TCP') + + self._mapped_ports.remove(external_port) + except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError): + _LOGGER.error('Could not delete port mapping') + + async def async_get_total_bytes_received(self): + """Get total bytes received.""" + return await self._igd_device.async_get_total_bytes_received() + + async def async_get_total_bytes_sent(self): + """Get total bytes sent.""" + return await self._igd_device.async_get_total_bytes_sent() + + async def async_get_total_packets_received(self): + """Get total packets received.""" + # pylint: disable=invalid-name + return await self._igd_device.async_get_total_packets_received() + + async def async_get_total_packets_sent(self): + """Get total packets sent.""" + return await self._igd_device.async_get_total_packets_sent() diff --git a/homeassistant/components/upnp/strings.json b/homeassistant/components/upnp/strings.json new file mode 100644 index 00000000000..9dd4c3f5ad0 --- /dev/null +++ b/homeassistant/components/upnp/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "title": "UPnP/IGD", + "step": { + "init": { + "title": "UPnP/IGD" + }, + "user": { + "title": "Configuration options for the UPnP/IGD", + "data":{ + "igd": "UPnP/IGD", + "enable_sensors": "Add traffic sensors", + "enable_port_mapping": "Enable port mapping for Home Assistant" + } + } + }, + "abort": { + "no_devices_discovered": "No UPnP/IGDs discovered", + "already_configured": "UPnP/IGD is already configured", + "no_sensors_or_port_mapping": "Enable at least sensors or port mapping" + } + } +} diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 1808737d281..212e6bd648f 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -4,7 +4,6 @@ Support for vacuum cleaner robots (botvacs). For more details about this platform, please refer to the documentation https://home-assistant.io/components/vacuum/ """ -import asyncio from datetime import timedelta from functools import partial import logging @@ -94,101 +93,12 @@ def is_on(hass, entity_id=None): return hass.states.is_state(entity_id, STATE_ON) -@bind_hass -def turn_on(hass, entity_id=None): - """Turn all or specified vacuum on.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_TURN_ON, data) - - -@bind_hass -def turn_off(hass, entity_id=None): - """Turn all or specified vacuum off.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) - - -@bind_hass -def toggle(hass, entity_id=None): - """Toggle all or specified vacuum.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_TOGGLE, data) - - -@bind_hass -def locate(hass, entity_id=None): - """Locate all or specified vacuum.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_LOCATE, data) - - -@bind_hass -def clean_spot(hass, entity_id=None): - """Tell all or specified vacuum to perform a spot clean-up.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_CLEAN_SPOT, data) - - -@bind_hass -def return_to_base(hass, entity_id=None): - """Tell all or specified vacuum to return to base.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_RETURN_TO_BASE, data) - - -@bind_hass -def start_pause(hass, entity_id=None): - """Tell all or specified vacuum to start or pause the current task.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_START_PAUSE, data) - - -@bind_hass -def start(hass, entity_id=None): - """Tell all or specified vacuum to start or resume the current task.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_START, data) - - -@bind_hass -def pause(hass, entity_id=None): - """Tell all or the specified vacuum to pause the current task.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_PAUSE, data) - - -@bind_hass -def stop(hass, entity_id=None): - """Stop all or specified vacuum.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_STOP, data) - - -@bind_hass -def set_fan_speed(hass, fan_speed, entity_id=None): - """Set fan speed for all or specified vacuum.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - data[ATTR_FAN_SPEED] = fan_speed - hass.services.call(DOMAIN, SERVICE_SET_FAN_SPEED, data) - - -@bind_hass -def send_command(hass, command, params=None, entity_id=None): - """Send command to all or specified vacuum.""" - data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - data[ATTR_COMMAND] = command - if params is not None: - data[ATTR_PARAMS] = params - hass.services.call(DOMAIN, SERVICE_SEND_COMMAND, data) - - -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the vacuum component.""" component = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_VACUUMS) - yield from component.async_setup(config) + await component.async_setup(config) component.async_register_entity_service( SERVICE_TURN_ON, VACUUM_SERVICE_SCHEMA, diff --git a/homeassistant/components/vacuum/dyson.py b/homeassistant/components/vacuum/dyson.py index 943b97f6360..3d6e23c20c8 100644 --- a/homeassistant/components/vacuum/dyson.py +++ b/homeassistant/components/vacuum/dyson.py @@ -4,7 +4,6 @@ Support for the Dyson 360 eye vacuum cleaner robot. For more details about this platform, please refer to the documentation https://home-assistant.io/components/vacuum.dyson/ """ -import asyncio import logging from homeassistant.components.dyson import DYSON_DEVICES @@ -55,8 +54,7 @@ class Dyson360EyeDevice(VacuumDevice): _LOGGER.debug("Creating device %s", device.name) self._device = device - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.async_add_job( self._device.add_message_listener, self.on_message) diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/vacuum/mqtt.py index 47617277773..fcb77e10732 100644 --- a/homeassistant/components/vacuum/mqtt.py +++ b/homeassistant/components/vacuum/mqtt.py @@ -4,7 +4,6 @@ Support for a generic MQTT vacuum. For more details about this platform, please refer to the documentation https://home-assistant.io/components/vacuum.mqtt/ """ -import asyncio import logging import voluptuous as vol @@ -139,9 +138,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the vacuum.""" name = config.get(CONF_NAME) supported_feature_strings = config.get(CONF_SUPPORTED_FEATURES) @@ -265,10 +263,9 @@ class MqttVacuum(MqttAvailability, VacuumDevice): self._battery_level = 0 self._fan_speed = 'unknown' - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe MQTT events.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() @callback def message_received(topic, payload, qos): @@ -332,7 +329,7 @@ class MqttVacuum(MqttAvailability, VacuumDevice): self._docked_topic, self._fan_speed_topic) if topic] for topic in set(topics_list): - yield from self.hass.components.mqtt.async_subscribe( + await self.hass.components.mqtt.async_subscribe( topic, message_received, self._qos) @property @@ -395,8 +392,7 @@ class MqttVacuum(MqttAvailability, VacuumDevice): """Flag supported features.""" return self._supported_features - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the vacuum on.""" if self.supported_features & SUPPORT_TURN_ON == 0: return @@ -406,8 +402,7 @@ class MqttVacuum(MqttAvailability, VacuumDevice): self._status = 'Cleaning' self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the vacuum off.""" if self.supported_features & SUPPORT_TURN_OFF == 0: return @@ -417,8 +412,7 @@ class MqttVacuum(MqttAvailability, VacuumDevice): self._status = 'Turning Off' self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_stop(self, **kwargs): + async def async_stop(self, **kwargs): """Stop the vacuum.""" if self.supported_features & SUPPORT_STOP == 0: return @@ -428,8 +422,7 @@ class MqttVacuum(MqttAvailability, VacuumDevice): self._status = 'Stopping the current task' self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_clean_spot(self, **kwargs): + async def async_clean_spot(self, **kwargs): """Perform a spot clean-up.""" if self.supported_features & SUPPORT_CLEAN_SPOT == 0: return @@ -439,8 +432,7 @@ class MqttVacuum(MqttAvailability, VacuumDevice): self._status = "Cleaning spot" self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_locate(self, **kwargs): + async def async_locate(self, **kwargs): """Locate the vacuum (usually by playing a song).""" if self.supported_features & SUPPORT_LOCATE == 0: return @@ -450,8 +442,7 @@ class MqttVacuum(MqttAvailability, VacuumDevice): self._status = "Hi, I'm over here!" self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_start_pause(self, **kwargs): + async def async_start_pause(self, **kwargs): """Start, pause or resume the cleaning task.""" if self.supported_features & SUPPORT_PAUSE == 0: return @@ -461,8 +452,7 @@ class MqttVacuum(MqttAvailability, VacuumDevice): self._status = 'Pausing/Resuming cleaning...' self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_return_to_base(self, **kwargs): + async def async_return_to_base(self, **kwargs): """Tell the vacuum to return to its dock.""" if self.supported_features & SUPPORT_RETURN_HOME == 0: return @@ -473,8 +463,7 @@ class MqttVacuum(MqttAvailability, VacuumDevice): self._status = 'Returning home...' self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_set_fan_speed(self, fan_speed, **kwargs): + async def async_set_fan_speed(self, fan_speed, **kwargs): """Set fan speed.""" if self.supported_features & SUPPORT_FAN_SPEED == 0: return @@ -487,8 +476,7 @@ class MqttVacuum(MqttAvailability, VacuumDevice): self._status = "Setting fan to {}...".format(fan_speed) self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_send_command(self, command, params=None, **kwargs): + async def async_send_command(self, command, params=None, **kwargs): """Send a command to a vacuum cleaner.""" if self.supported_features & SUPPORT_SEND_COMMAND == 0: return diff --git a/homeassistant/components/vacuum/roomba.py b/homeassistant/components/vacuum/roomba.py index 487fd573f37..72d564909a8 100644 --- a/homeassistant/components/vacuum/roomba.py +++ b/homeassistant/components/vacuum/roomba.py @@ -68,9 +68,8 @@ SUPPORT_ROOMBA = SUPPORT_BATTERY | SUPPORT_PAUSE | SUPPORT_RETURN_HOME | \ SUPPORT_ROOMBA_CARPET_BOOST = SUPPORT_ROOMBA | SUPPORT_FAN_SPEED -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the iRobot Roomba vacuum cleaner platform.""" from roomba import Roomba if PLATFORM not in hass.data: @@ -96,7 +95,7 @@ def async_setup_platform(hass, config, async_add_entities, try: with async_timeout.timeout(9): - yield from hass.async_add_job(roomba.connect) + await hass.async_add_job(roomba.connect) except asyncio.TimeoutError: raise PlatformNotReady @@ -170,54 +169,46 @@ class RoombaVacuum(VacuumDevice): """Return the state attributes of the device.""" return self._state_attrs - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the vacuum on.""" - yield from self.hass.async_add_job(self.vacuum.send_command, 'start') + await self.hass.async_add_job(self.vacuum.send_command, 'start') self._is_on = True - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the vacuum off and return to home.""" - yield from self.async_stop() - yield from self.async_return_to_base() + await self.async_stop() + await self.async_return_to_base() - @asyncio.coroutine - def async_stop(self, **kwargs): + async def async_stop(self, **kwargs): """Stop the vacuum cleaner.""" - yield from self.hass.async_add_job(self.vacuum.send_command, 'stop') + await self.hass.async_add_job(self.vacuum.send_command, 'stop') self._is_on = False - @asyncio.coroutine - def async_resume(self, **kwargs): + async def async_resume(self, **kwargs): """Resume the cleaning cycle.""" - yield from self.hass.async_add_job(self.vacuum.send_command, 'resume') + await self.hass.async_add_job(self.vacuum.send_command, 'resume') self._is_on = True - @asyncio.coroutine - def async_pause(self, **kwargs): + async def async_pause(self, **kwargs): """Pause the cleaning cycle.""" - yield from self.hass.async_add_job(self.vacuum.send_command, 'pause') + await self.hass.async_add_job(self.vacuum.send_command, 'pause') self._is_on = False - @asyncio.coroutine - def async_start_pause(self, **kwargs): + async def async_start_pause(self, **kwargs): """Pause the cleaning task or resume it.""" if self.vacuum_state and self.is_on: # vacuum is running - yield from self.async_pause() + await self.async_pause() elif self._status == 'Stopped': # vacuum is stopped - yield from self.async_resume() + await self.async_resume() else: # vacuum is off - yield from self.async_turn_on() + await self.async_turn_on() - @asyncio.coroutine - def async_return_to_base(self, **kwargs): + async def async_return_to_base(self, **kwargs): """Set the vacuum cleaner to return to the dock.""" - yield from self.hass.async_add_job(self.vacuum.send_command, 'dock') + await self.hass.async_add_job(self.vacuum.send_command, 'dock') self._is_on = False - @asyncio.coroutine - def async_set_fan_speed(self, fan_speed, **kwargs): + async def async_set_fan_speed(self, fan_speed, **kwargs): """Set fan speed.""" if fan_speed.capitalize() in FAN_SPEEDS: fan_speed = fan_speed.capitalize() @@ -240,22 +231,20 @@ class RoombaVacuum(VacuumDevice): _LOGGER.error("No such fan speed available: %s", fan_speed) return # The set_preference method does only accept string values - yield from self.hass.async_add_job( + await self.hass.async_add_job( self.vacuum.set_preference, 'carpetBoost', str(carpet_boost)) - yield from self.hass.async_add_job( + await self.hass.async_add_job( self.vacuum.set_preference, 'vacHigh', str(high_perf)) - @asyncio.coroutine - def async_send_command(self, command, params=None, **kwargs): + async def async_send_command(self, command, params=None, **kwargs): """Send raw command.""" _LOGGER.debug("async_send_command %s (%s), %s", command, params, kwargs) - yield from self.hass.async_add_job( + await self.hass.async_add_job( self.vacuum.send_command, command, params) return True - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Fetch state from the device.""" # No data, no update if not self.vacuum.master_state: diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py index 290c3417149..d2da4f3b6ac 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/vacuum/xiaomi_miio.py @@ -92,6 +92,7 @@ STATE_CODE_TO_STATE = { 3: STATE_IDLE, 5: STATE_CLEANING, 6: STATE_RETURNING, + 7: STATE_CLEANING, 8: STATE_DOCKED, 9: STATE_ERROR, 10: STATE_PAUSED, @@ -103,9 +104,8 @@ STATE_CODE_TO_STATE = { } -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the Xiaomi vacuum cleaner robot platform.""" from miio import Vacuum if DATA_KEY not in hass.data: @@ -124,8 +124,7 @@ def async_setup_platform(hass, config, async_add_entities, async_add_entities([mirobo], update_before_add=True) - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Map services to methods on MiroboVacuum.""" method = SERVICE_TO_METHOD.get(service.service) params = {key: value for key, value in service.data.items() @@ -139,14 +138,14 @@ def async_setup_platform(hass, config, async_add_entities, update_tasks = [] for vacuum in target_vacuums: - yield from getattr(vacuum, method['method'])(**params) + await getattr(vacuum, method['method'])(**params) for vacuum in target_vacuums: update_coro = vacuum.async_update_ha_state(True) update_tasks.append(update_coro) if update_tasks: - yield from asyncio.wait(update_tasks, loop=hass.loop) + await asyncio.wait(update_tasks, loop=hass.loop) for vacuum_service in SERVICE_TO_METHOD: schema = SERVICE_TO_METHOD[vacuum_service].get( @@ -259,12 +258,11 @@ class MiroboVacuum(StateVacuumDevice): """Flag vacuum cleaner robot features that are supported.""" return SUPPORT_XIAOMI - @asyncio.coroutine - def _try_command(self, mask_error, func, *args, **kwargs): + async def _try_command(self, mask_error, func, *args, **kwargs): """Call a vacuum command handling error messages.""" from miio import DeviceException try: - yield from self.hass.async_add_job(partial(func, *args, **kwargs)) + await self.hass.async_add_job(partial(func, *args, **kwargs)) return True except DeviceException as exc: _LOGGER.error(mask_error, exc) @@ -281,14 +279,12 @@ class MiroboVacuum(StateVacuumDevice): await self._try_command( "Unable to set start/pause: %s", self._vacuum.pause) - @asyncio.coroutine - def async_stop(self, **kwargs): + async def async_stop(self, **kwargs): """Stop the vacuum cleaner.""" - yield from self._try_command( + await self._try_command( "Unable to stop: %s", self._vacuum.stop) - @asyncio.coroutine - def async_set_fan_speed(self, fan_speed, **kwargs): + async def async_set_fan_speed(self, fan_speed, **kwargs): """Set fan speed.""" if fan_speed.capitalize() in FAN_SPEEDS: fan_speed = FAN_SPEEDS[fan_speed.capitalize()] @@ -300,68 +296,60 @@ class MiroboVacuum(StateVacuumDevice): "Valid speeds are: %s", exc, self.fan_speed_list) return - yield from self._try_command( + await self._try_command( "Unable to set fan speed: %s", self._vacuum.set_fan_speed, fan_speed) - @asyncio.coroutine - def async_return_to_base(self, **kwargs): + async def async_return_to_base(self, **kwargs): """Set the vacuum cleaner to return to the dock.""" - yield from self._try_command( + await self._try_command( "Unable to return home: %s", self._vacuum.home) - @asyncio.coroutine - def async_clean_spot(self, **kwargs): + async def async_clean_spot(self, **kwargs): """Perform a spot clean-up.""" - yield from self._try_command( + await self._try_command( "Unable to start the vacuum for a spot clean-up: %s", self._vacuum.spot) - @asyncio.coroutine - def async_locate(self, **kwargs): + async def async_locate(self, **kwargs): """Locate the vacuum cleaner.""" - yield from self._try_command( + await self._try_command( "Unable to locate the botvac: %s", self._vacuum.find) - @asyncio.coroutine - def async_send_command(self, command, params=None, **kwargs): + async def async_send_command(self, command, params=None, **kwargs): """Send raw command.""" - yield from self._try_command( + await self._try_command( "Unable to send command to the vacuum: %s", self._vacuum.raw_command, command, params) - @asyncio.coroutine - def async_remote_control_start(self): + async def async_remote_control_start(self): """Start remote control mode.""" - yield from self._try_command( + await self._try_command( "Unable to start remote control the vacuum: %s", self._vacuum.manual_start) - @asyncio.coroutine - def async_remote_control_stop(self): + async def async_remote_control_stop(self): """Stop remote control mode.""" - yield from self._try_command( + await self._try_command( "Unable to stop remote control the vacuum: %s", self._vacuum.manual_stop) - @asyncio.coroutine - def async_remote_control_move(self, - rotation: int = 0, - velocity: float = 0.3, - duration: int = 1500): + async def async_remote_control_move(self, + rotation: int = 0, + velocity: float = 0.3, + duration: int = 1500): """Move vacuum with remote control mode.""" - yield from self._try_command( + await self._try_command( "Unable to move with remote control the vacuum: %s", self._vacuum.manual_control, velocity=velocity, rotation=rotation, duration=duration) - @asyncio.coroutine - def async_remote_control_move_step(self, - rotation: int = 0, - velocity: float = 0.2, - duration: int = 1500): + async def async_remote_control_move_step(self, + rotation: int = 0, + velocity: float = 0.2, + duration: int = 1500): """Move vacuum one step with remote control mode.""" - yield from self._try_command( + await self._try_command( "Unable to remote control the vacuum: %s", self._vacuum.manual_control_once, velocity=velocity, rotation=rotation, duration=duration) diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index 1f26ab639d6..016547697b9 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -10,8 +10,8 @@ from datetime import timedelta import voluptuous as vol -from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP) +from homeassistant.const import (CONF_PASSWORD, CONF_SCAN_INTERVAL, + CONF_USERNAME, EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers import discovery from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -35,6 +35,9 @@ CONF_SMARTCAM = 'smartcam' DOMAIN = 'verisure' +MIN_SCAN_INTERVAL = timedelta(minutes=1) +DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) + SERVICE_CAPTURE_SMARTCAM = 'capture_smartcam' HUB = None @@ -53,6 +56,8 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_SMARTPLUGS, default=True): cv.boolean, vol.Optional(CONF_THERMOMETERS, default=True): cv.boolean, vol.Optional(CONF_SMARTCAM, default=True): cv.boolean, + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): ( + vol.All(cv.time_period, vol.Clamp(min=MIN_SCAN_INTERVAL))), }), }, extra=vol.ALLOW_EXTRA) @@ -66,6 +71,8 @@ def setup(hass, config): import verisure global HUB HUB = VerisureHub(config[DOMAIN], verisure) + HUB.update_overview = Throttle( + config[DOMAIN][CONF_SCAN_INTERVAL])(HUB.update_overview) if not HUB.login(): return False hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, @@ -140,7 +147,6 @@ class VerisureHub: return False return True - @Throttle(timedelta(seconds=60)) def update_overview(self): """Update the overview.""" try: diff --git a/homeassistant/components/wake_on_lan.py b/homeassistant/components/wake_on_lan.py index 5bcb0d4dd79..dba99bf7e3d 100644 --- a/homeassistant/components/wake_on_lan.py +++ b/homeassistant/components/wake_on_lan.py @@ -4,7 +4,6 @@ Component to wake up devices sending Wake-On-LAN magic packets. For more details about this component, please refer to the documentation at https://home-assistant.io/components/wake_on_lan/ """ -import asyncio from functools import partial import logging @@ -29,24 +28,22 @@ WAKE_ON_LAN_SEND_MAGIC_PACKET_SCHEMA = vol.Schema({ }) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the wake on LAN component.""" import wakeonlan - @asyncio.coroutine - def send_magic_packet(call): + async def send_magic_packet(call): """Send magic packet to wake up a device.""" mac_address = call.data.get(CONF_MAC) broadcast_address = call.data.get(CONF_BROADCAST_ADDRESS) _LOGGER.info("Send magic packet to mac %s (broadcast: %s)", mac_address, broadcast_address) if broadcast_address is not None: - yield from hass.async_add_job( + await hass.async_add_job( partial(wakeonlan.send_magic_packet, mac_address, ip_address=broadcast_address)) else: - yield from hass.async_add_job( + await hass.async_add_job( partial(wakeonlan.send_magic_packet, mac_address)) hass.services.async_register( diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index a43999f2276..f9a8f1fbbe4 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -4,7 +4,6 @@ Weather component that handles meteorological data for your location. For more details about this component, please refer to the documentation at https://home-assistant.io/components/weather/ """ -import asyncio import logging from homeassistant.helpers.entity_component import EntityComponent @@ -27,6 +26,8 @@ ATTR_FORECAST_PRECIPITATION = 'precipitation' ATTR_FORECAST_TEMP = 'temperature' ATTR_FORECAST_TEMP_LOW = 'templow' ATTR_FORECAST_TIME = 'datetime' +ATTR_FORECAST_WIND_SPEED = 'wind_speed' +ATTR_FORECAST_WIND_BEARING = 'wind_bearing' ATTR_WEATHER_ATTRIBUTION = 'attribution' ATTR_WEATHER_HUMIDITY = 'humidity' ATTR_WEATHER_OZONE = 'ozone' @@ -37,12 +38,11 @@ ATTR_WEATHER_WIND_BEARING = 'wind_bearing' ATTR_WEATHER_WIND_SPEED = 'wind_speed' -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the weather component.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - yield from component.async_setup(config) + await component.async_setup(config) return True diff --git a/homeassistant/components/weather/buienradar.py b/homeassistant/components/weather/buienradar.py index 6b92eb97c9e..1ec3fc513e9 100644 --- a/homeassistant/components/weather/buienradar.py +++ b/homeassistant/components/weather/buienradar.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/weather.buienradar/ """ import logging -import asyncio import voluptuous as vol @@ -55,9 +54,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): """Set up the buienradar platform.""" latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) @@ -86,7 +84,7 @@ def async_setup_platform(hass, config, async_add_entities, async_add_entities([BrWeather(data, config)]) # schedule the first update in 1 minute from now: - yield from data.schedule_update(1) + await data.schedule_update(1) class BrWeather(WeatherEntity): diff --git a/homeassistant/components/weather/darksky.py b/homeassistant/components/weather/darksky.py index 34a6fd3d6f6..c753c0249ca 100644 --- a/homeassistant/components/weather/darksky.py +++ b/homeassistant/components/weather/darksky.py @@ -13,10 +13,12 @@ import voluptuous as vol from homeassistant.components.weather import ( ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_WIND_SPEED, ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_PRECIPITATION, PLATFORM_SCHEMA, WeatherEntity) from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS, - TEMP_FAHRENHEIT) + CONF_MODE, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -26,6 +28,8 @@ _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Powered by Dark Sky" +FORECAST_MODE = ['hourly', 'daily'] + MAP_CONDITION = { 'clear-day': 'sunny', 'clear-night': 'clear-night', @@ -50,6 +54,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 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_UNITS): vol.In(['auto', 'si', 'us', 'ca', 'uk', 'uk2']), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) @@ -62,6 +67,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) name = config.get(CONF_NAME) + mode = config.get(CONF_MODE) units = config.get(CONF_UNITS) if not units: @@ -70,16 +76,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): dark_sky = DarkSkyData( config.get(CONF_API_KEY), latitude, longitude, units) - add_entities([DarkSkyWeather(name, dark_sky)], True) + add_entities([DarkSkyWeather(name, dark_sky, mode)], True) class DarkSkyWeather(WeatherEntity): """Representation of a weather condition.""" - def __init__(self, name, dark_sky): + def __init__(self, name, dark_sky, mode): """Initialize Dark Sky weather.""" self._name = name self._dark_sky = dark_sky + self._mode = mode self._ds_data = None self._ds_currently = None @@ -117,11 +124,26 @@ class DarkSkyWeather(WeatherEntity): """Return the wind speed.""" return self._ds_currently.get('windSpeed') + @property + def wind_bearing(self): + """Return the wind bearing.""" + return self._ds_currently.get('windBearing') + + @property + def ozone(self): + """Return the ozone level.""" + return self._ds_currently.get('ozone') + @property def pressure(self): """Return the pressure.""" return self._ds_currently.get('pressure') + @property + def visibility(self): + """Return the visibility.""" + return self._ds_currently.get('visibility') + @property def condition(self): """Return the weather condition.""" @@ -130,14 +152,47 @@ class DarkSkyWeather(WeatherEntity): @property def forecast(self): """Return the forecast array.""" - return [{ - ATTR_FORECAST_TIME: - datetime.fromtimestamp(entry.d.get('time')).isoformat(), - ATTR_FORECAST_TEMP: - entry.d.get('temperature'), - ATTR_FORECAST_CONDITION: - MAP_CONDITION.get(entry.d.get('icon')) - } for entry in self._ds_hourly.data] + # Per conversation with Joshua Reyes of Dark Sky, to get the total + # forecasted precipitation, you have to multiple the intensity by + # the hours for the forecast interval + def calc_precipitation(intensity, hours): + amount = None + if intensity is not None: + amount = round((intensity * hours), 1) + return amount if amount > 0 else None + + data = None + + if self._mode == 'daily': + data = [{ + ATTR_FORECAST_TIME: + datetime.fromtimestamp(entry.d.get('time')).isoformat(), + ATTR_FORECAST_TEMP: + entry.d.get('temperatureHigh'), + ATTR_FORECAST_TEMP_LOW: + entry.d.get('temperatureLow'), + ATTR_FORECAST_PRECIPITATION: + calc_precipitation(entry.d.get('precipIntensity'), 24), + ATTR_FORECAST_WIND_SPEED: + entry.d.get('windSpeed'), + ATTR_FORECAST_WIND_BEARING: + entry.d.get('windBearing'), + ATTR_FORECAST_CONDITION: + MAP_CONDITION.get(entry.d.get('icon')) + } for entry in self._ds_daily.data] + else: + data = [{ + ATTR_FORECAST_TIME: + datetime.fromtimestamp(entry.d.get('time')).isoformat(), + ATTR_FORECAST_TEMP: + entry.d.get('temperature'), + ATTR_FORECAST_PRECIPITATION: + calc_precipitation(entry.d.get('precipIntensity'), 1), + ATTR_FORECAST_CONDITION: + MAP_CONDITION.get(entry.d.get('icon')) + } for entry in self._ds_hourly.data] + + return data def update(self): """Get the latest data from Dark Sky.""" diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index b300fcbcbec..b70413b9565 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -11,7 +11,9 @@ import voluptuous as vol from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity) + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_SPEED, + ATTR_FORECAST_WIND_BEARING, + PLATFORM_SCHEMA, WeatherEntity) from homeassistant.const import ( CONF_API_KEY, TEMP_CELSIUS, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME, STATE_UNKNOWN) @@ -22,9 +24,6 @@ REQUIREMENTS = ['pyowm==2.9.0'] _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'] @@ -131,6 +130,9 @@ class OpenWeatherMapWeather(WeatherEntity): @property def wind_speed(self): """Return the wind speed.""" + if self.hass.config.units.name == 'imperial': + return round(self.data.get_wind().get('speed') * 2.24, 2) + return round(self.data.get_wind().get('speed') * 3.6, 2) @property diff --git a/homeassistant/components/webhook.py b/homeassistant/components/webhook.py new file mode 100644 index 00000000000..2a4c3f973f2 --- /dev/null +++ b/homeassistant/components/webhook.py @@ -0,0 +1,85 @@ +"""Webhooks for Home Assistant. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/webhook/ +""" +import logging + +from aiohttp.web import Response + +from homeassistant.core import callback +from homeassistant.loader import bind_hass +from homeassistant.auth.util import generate_secret +from homeassistant.components.http.view import HomeAssistantView + +DOMAIN = 'webhook' +DEPENDENCIES = ['http'] +_LOGGER = logging.getLogger(__name__) + + +@callback +@bind_hass +def async_register(hass, webhook_id, handler): + """Register a webhook.""" + handlers = hass.data.setdefault(DOMAIN, {}) + + if webhook_id in handlers: + raise ValueError('Handler is already defined!') + + handlers[webhook_id] = handler + + +@callback +@bind_hass +def async_unregister(hass, webhook_id): + """Remove a webhook.""" + handlers = hass.data.setdefault(DOMAIN, {}) + handlers.pop(webhook_id, None) + + +@callback +def async_generate_id(): + """Generate a webhook_id.""" + return generate_secret(entropy=32) + + +@callback +@bind_hass +def async_generate_url(hass, webhook_id): + """Generate a webhook_id.""" + return "{}/api/webhook/{}".format(hass.config.api.base_url, webhook_id) + + +async def async_setup(hass, config): + """Initialize the webhook component.""" + hass.http.register_view(WebhookView) + return True + + +class WebhookView(HomeAssistantView): + """Handle incoming webhook requests.""" + + url = "/api/webhook/{webhook_id}" + name = "api:webhook" + requires_auth = False + + async def post(self, request, webhook_id): + """Handle webhook call.""" + hass = request.app['hass'] + handlers = hass.data.setdefault(DOMAIN, {}) + handler = handlers.get(webhook_id) + + # Always respond successfully to not give away if a hook exists or not. + if handler is None: + _LOGGER.warning( + 'Received message for unregistered webhook %s', webhook_id) + return Response(status=200) + + try: + response = await handler(hass, webhook_id, request) + if response is None: + response = Response(status=200) + return response + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error processing webhook %s", webhook_id) + return Response(status=200) diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py deleted file mode 100644 index 4e7c186facc..00000000000 --- a/homeassistant/components/websocket_api.py +++ /dev/null @@ -1,654 +0,0 @@ -""" -Websocket based API for Home Assistant. - -For more details about this component, please refer to the documentation at -https://developers.home-assistant.io/docs/external_api_websocket.html -""" -import asyncio -from concurrent import futures -from contextlib import suppress -from functools import partial, wraps -import json -import logging - -from aiohttp import web -import voluptuous as vol -from voluptuous.humanize import humanize_error - -from homeassistant.const import ( - MATCH_ALL, EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, - __version__) -from homeassistant.core import Context, callback, HomeAssistant -from homeassistant.loader import bind_hass -from homeassistant.helpers.json import JSONEncoder -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.service import async_get_all_descriptions -from homeassistant.components.http import HomeAssistantView -from homeassistant.components.http.auth import validate_password -from homeassistant.components.http.const import KEY_AUTHENTICATED -from homeassistant.components.http.ban import process_wrong_login, \ - process_success_login - -DOMAIN = 'websocket_api' - -URL = '/api/websocket' -DEPENDENCIES = ('http',) - -MAX_PENDING_MSG = 512 - -ERR_ID_REUSE = 1 -ERR_INVALID_FORMAT = 2 -ERR_NOT_FOUND = 3 -ERR_UNKNOWN_COMMAND = 4 -ERR_UNKNOWN_ERROR = 5 - -TYPE_AUTH = 'auth' -TYPE_AUTH_INVALID = 'auth_invalid' -TYPE_AUTH_OK = 'auth_ok' -TYPE_AUTH_REQUIRED = 'auth_required' -TYPE_CALL_SERVICE = 'call_service' -TYPE_EVENT = 'event' -TYPE_GET_CONFIG = 'get_config' -TYPE_GET_SERVICES = 'get_services' -TYPE_GET_STATES = 'get_states' -TYPE_PING = 'ping' -TYPE_PONG = 'pong' -TYPE_RESULT = 'result' -TYPE_SUBSCRIBE_EVENTS = 'subscribe_events' -TYPE_UNSUBSCRIBE_EVENTS = 'unsubscribe_events' - -_LOGGER = logging.getLogger(__name__) - -JSON_DUMP = partial(json.dumps, cls=JSONEncoder) - -AUTH_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('type'): TYPE_AUTH, - vol.Exclusive('api_password', 'auth'): str, - vol.Exclusive('access_token', 'auth'): str, -}) - -# Minimal requirements of a message -MINIMAL_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, - vol.Required('type'): cv.string, -}, extra=vol.ALLOW_EXTRA) -# Base schema to extend by message handlers -BASE_COMMAND_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, -}) - - -SCHEMA_SUBSCRIBE_EVENTS = BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): TYPE_SUBSCRIBE_EVENTS, - vol.Optional('event_type', default=MATCH_ALL): str, -}) - - -SCHEMA_UNSUBSCRIBE_EVENTS = BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): TYPE_UNSUBSCRIBE_EVENTS, - vol.Required('subscription'): cv.positive_int, -}) - - -SCHEMA_CALL_SERVICE = BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): TYPE_CALL_SERVICE, - vol.Required('domain'): str, - vol.Required('service'): str, - vol.Optional('service_data'): dict -}) - - -SCHEMA_GET_STATES = BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): TYPE_GET_STATES, -}) - - -SCHEMA_GET_SERVICES = BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): TYPE_GET_SERVICES, -}) - - -SCHEMA_GET_CONFIG = BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): TYPE_GET_CONFIG, -}) - - -SCHEMA_PING = BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): TYPE_PING, -}) - - -# Define the possible errors that occur when connections are cancelled. -# Originally, this was just asyncio.CancelledError, but issue #9546 showed -# that futures.CancelledErrors can also occur in some situations. -CANCELLATION_ERRORS = (asyncio.CancelledError, futures.CancelledError) - - -def auth_ok_message(): - """Return an auth_ok message.""" - return { - 'type': TYPE_AUTH_OK, - 'ha_version': __version__, - } - - -def auth_required_message(): - """Return an auth_required message.""" - return { - 'type': TYPE_AUTH_REQUIRED, - 'ha_version': __version__, - } - - -def auth_invalid_message(message): - """Return an auth_invalid message.""" - return { - 'type': TYPE_AUTH_INVALID, - 'message': message, - } - - -def event_message(iden, event): - """Return an event message.""" - return { - 'id': iden, - 'type': TYPE_EVENT, - 'event': event.as_dict(), - } - - -def error_message(iden, code, message): - """Return an error result message.""" - return { - 'id': iden, - 'type': TYPE_RESULT, - 'success': False, - 'error': { - 'code': code, - 'message': message, - }, - } - - -def pong_message(iden): - """Return a pong message.""" - return { - 'id': iden, - 'type': TYPE_PONG, - } - - -def result_message(iden, result=None): - """Return a success result message.""" - return { - 'id': iden, - 'type': TYPE_RESULT, - 'success': True, - 'result': result, - } - - -@bind_hass -@callback -def async_register_command(hass, command, handler, schema): - """Register a websocket command.""" - handlers = hass.data.get(DOMAIN) - if handlers is None: - handlers = hass.data[DOMAIN] = {} - handlers[command] = (handler, schema) - - -def require_owner(func): - """Websocket decorator to require user to be an owner.""" - @wraps(func) - def with_owner(hass, connection, msg): - """Check owner and call function.""" - user = connection.request.get('hass_user') - - if user is None or not user.is_owner: - connection.to_write.put_nowait(error_message( - msg['id'], 'unauthorized', 'This command is for owners only.')) - return - - func(hass, connection, msg) - - return with_owner - - -async def async_setup(hass, config): - """Initialize the websocket API.""" - hass.http.register_view(WebsocketAPIView) - - async_register_command(hass, TYPE_SUBSCRIBE_EVENTS, - handle_subscribe_events, SCHEMA_SUBSCRIBE_EVENTS) - async_register_command(hass, TYPE_UNSUBSCRIBE_EVENTS, - handle_unsubscribe_events, - SCHEMA_UNSUBSCRIBE_EVENTS) - async_register_command(hass, TYPE_CALL_SERVICE, - handle_call_service, SCHEMA_CALL_SERVICE) - async_register_command(hass, TYPE_GET_STATES, - handle_get_states, SCHEMA_GET_STATES) - async_register_command(hass, TYPE_GET_SERVICES, - handle_get_services, SCHEMA_GET_SERVICES) - async_register_command(hass, TYPE_GET_CONFIG, - handle_get_config, SCHEMA_GET_CONFIG) - async_register_command(hass, TYPE_PING, - handle_ping, SCHEMA_PING) - - return True - - -class WebsocketAPIView(HomeAssistantView): - """View to serve a websockets endpoint.""" - - name = "websocketapi" - url = URL - requires_auth = False - - async def get(self, request): - """Handle an incoming websocket connection.""" - return await ActiveConnection(request.app['hass'], request).handle() - - -class ActiveConnection: - """Handle an active websocket client connection.""" - - def __init__(self, hass, request): - """Initialize an active connection.""" - self.hass = hass - self.request = request - self.wsock = None - self.event_listeners = {} - self.to_write = asyncio.Queue(maxsize=MAX_PENDING_MSG, loop=hass.loop) - self._handle_task = None - self._writer_task = None - - @property - def user(self): - """Return the user associated with the connection.""" - return self.request.get('hass_user') - - def context(self, msg): - """Return a context.""" - user = self.user - if user is None: - return Context() - return Context(user_id=user.id) - - def debug(self, message1, message2=''): - """Print a debug message.""" - _LOGGER.debug("WS %s: %s %s", id(self.wsock), message1, message2) - - def log_error(self, message1, message2=''): - """Print an error message.""" - _LOGGER.error("WS %s: %s %s", id(self.wsock), message1, message2) - - async def _writer(self): - """Write outgoing messages.""" - # Exceptions if Socket disconnected or cancelled by connection handler - with suppress(RuntimeError, *CANCELLATION_ERRORS): - while not self.wsock.closed: - message = await self.to_write.get() - if message is None: - break - self.debug("Sending", message) - try: - await self.wsock.send_json(message, dumps=JSON_DUMP) - except TypeError as err: - _LOGGER.error('Unable to serialize to JSON: %s\n%s', - err, message) - - @callback - def send_message_outside(self, message): - """Send a message to the client. - - Closes connection if the client is not reading the messages. - - Async friendly. - """ - try: - self.to_write.put_nowait(message) - except asyncio.QueueFull: - self.log_error("Client exceeded max pending messages [2]:", - MAX_PENDING_MSG) - self.cancel() - - @callback - def cancel(self): - """Cancel the connection.""" - self._handle_task.cancel() - self._writer_task.cancel() - - async def handle(self): - """Handle the websocket connection.""" - request = self.request - wsock = self.wsock = web.WebSocketResponse(heartbeat=55) - await wsock.prepare(request) - self.debug("Connected") - - self._handle_task = asyncio.Task.current_task(loop=self.hass.loop) - - @callback - def handle_hass_stop(event): - """Cancel this connection.""" - self.cancel() - - unsub_stop = self.hass.bus.async_listen( - EVENT_HOMEASSISTANT_STOP, handle_hass_stop) - self._writer_task = self.hass.async_add_job(self._writer()) - final_message = None - msg = None - authenticated = False - - try: - if request[KEY_AUTHENTICATED]: - authenticated = True - - # always request auth when auth is active - # even request passed pre-authentication (trusted networks) - # or when using legacy api_password - if self.hass.auth.active or not authenticated: - self.debug("Request auth") - await self.wsock.send_json(auth_required_message()) - msg = await wsock.receive_json() - msg = AUTH_MESSAGE_SCHEMA(msg) - - if self.hass.auth.active and 'access_token' in msg: - self.debug("Received access_token") - refresh_token = \ - await self.hass.auth.async_validate_access_token( - msg['access_token']) - authenticated = refresh_token is not None - if authenticated: - request['hass_user'] = refresh_token.user - request['refresh_token_id'] = refresh_token.id - - elif ((not self.hass.auth.active or - self.hass.auth.support_legacy) and - 'api_password' in msg): - self.debug("Received api_password") - authenticated = validate_password( - request, msg['api_password']) - - if not authenticated: - self.debug("Authorization failed") - await self.wsock.send_json( - auth_invalid_message('Invalid access token or password')) - await process_wrong_login(request) - return wsock - - self.debug("Auth OK") - await process_success_login(request) - await self.wsock.send_json(auth_ok_message()) - - # ---------- AUTH PHASE OVER ---------- - - msg = await wsock.receive_json() - last_id = 0 - handlers = self.hass.data[DOMAIN] - - while msg: - self.debug("Received", msg) - msg = MINIMAL_MESSAGE_SCHEMA(msg) - cur_id = msg['id'] - - if cur_id <= last_id: - self.to_write.put_nowait(error_message( - cur_id, ERR_ID_REUSE, - 'Identifier values have to increase.')) - - elif msg['type'] not in handlers: - 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']] - try: - handler(self.hass, self, schema(msg)) - except Exception: # pylint: disable=broad-except - _LOGGER.exception('Error handling message: %s', msg) - self.to_write.put_nowait(error_message( - cur_id, ERR_UNKNOWN_ERROR, - 'Unknown error.')) - - last_id = cur_id - msg = await wsock.receive_json() - - except vol.Invalid as err: - error_msg = "Message incorrectly formatted: " - if msg: - error_msg += humanize_error(msg, err) - else: - error_msg += str(err) - - self.log_error(error_msg) - - if not authenticated: - final_message = auth_invalid_message(error_msg) - - else: - if isinstance(msg, dict): - iden = msg.get('id') - else: - iden = None - - final_message = error_message( - iden, ERR_INVALID_FORMAT, error_msg) - - except TypeError as err: - if wsock.closed: - self.debug("Connection closed by client") - else: - _LOGGER.exception("Unexpected TypeError: %s", err) - - except ValueError as err: - msg = "Received invalid JSON" - value = getattr(err, 'doc', None) # Py3.5+ only - if value: - msg += ': {}'.format(value) - self.log_error(msg) - self._writer_task.cancel() - - except CANCELLATION_ERRORS: - self.debug("Connection cancelled") - - except asyncio.QueueFull: - self.log_error("Client exceeded max pending messages [1]:", - MAX_PENDING_MSG) - self._writer_task.cancel() - - except Exception: # pylint: disable=broad-except - error = "Unexpected error inside websocket API. " - if msg is not None: - error += str(msg) - _LOGGER.exception(error) - - finally: - unsub_stop() - - for unsub in self.event_listeners.values(): - unsub() - - try: - if final_message is not None: - self.to_write.put_nowait(final_message) - self.to_write.put_nowait(None) - # Make sure all error messages are written before closing - await self._writer_task - except asyncio.QueueFull: - self._writer_task.cancel() - - await wsock.close() - self.debug("Closed connection") - - return wsock - - -def async_response(func): - """Decorate an async function to handle WebSocket API messages.""" - async def handle_msg_response(hass, connection, msg): - """Create a response and handle exception.""" - try: - await func(hass, connection, msg) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - connection.send_message_outside(error_message( - msg['id'], 'unknown', 'Unexpected error occurred')) - - @callback - @wraps(func) - def schedule_handler(hass, connection, msg): - """Schedule the handler.""" - hass.async_create_task(handle_msg_response(hass, connection, msg)) - - return schedule_handler - - -@callback -def handle_subscribe_events(hass, connection, msg): - """Handle subscribe events command. - - Async friendly. - """ - async def forward_events(event): - """Forward events to websocket.""" - if event.event_type == EVENT_TIME_CHANGED: - return - - connection.send_message_outside(event_message(msg['id'], event)) - - connection.event_listeners[msg['id']] = hass.bus.async_listen( - msg['event_type'], forward_events) - - connection.to_write.put_nowait(result_message(msg['id'])) - - -@callback -def handle_unsubscribe_events(hass, connection, msg): - """Handle unsubscribe events command. - - Async friendly. - """ - subscription = msg['subscription'] - - if subscription in connection.event_listeners: - connection.event_listeners.pop(subscription)() - connection.to_write.put_nowait(result_message(msg['id'])) - else: - connection.to_write.put_nowait(error_message( - msg['id'], ERR_NOT_FOUND, 'Subscription not found.')) - - -@async_response -async def handle_call_service(hass, connection, msg): - """Handle call service command. - - Async friendly. - """ - blocking = True - if (msg['domain'] == 'homeassistant' and - msg['service'] in ['restart', 'stop']): - blocking = False - await hass.services.async_call( - msg['domain'], msg['service'], msg.get('service_data'), blocking, - connection.context(msg)) - connection.send_message_outside(result_message(msg['id'])) - - -@callback -def handle_get_states(hass, connection, msg): - """Handle get states command. - - Async friendly. - """ - connection.to_write.put_nowait(result_message( - msg['id'], hass.states.async_all())) - - -@async_response -async def handle_get_services(hass, connection, msg): - """Handle get services command. - - Async friendly. - """ - descriptions = await async_get_all_descriptions(hass) - connection.send_message_outside( - result_message(msg['id'], descriptions)) - - -@callback -def handle_get_config(hass, connection, msg): - """Handle get config command. - - Async friendly. - """ - connection.to_write.put_nowait(result_message( - msg['id'], hass.config.as_dict())) - - -@callback -def handle_ping(hass, connection, msg): - """Handle ping command. - - Async friendly. - """ - connection.to_write.put_nowait(pong_message(msg['id'])) - - -def ws_require_user( - only_owner=False, only_system_user=False, allow_system_user=True, - only_active_user=True, only_inactive_user=False): - """Decorate function validating login user exist in current WS connection. - - Will write out error message if not authenticated. - """ - def validator(func): - """Decorate func.""" - @wraps(func) - def check_current_user(hass: HomeAssistant, - connection: ActiveConnection, - msg): - """Check current user.""" - def output_error(message_id, message): - """Output error message.""" - connection.send_message_outside(error_message( - msg['id'], message_id, message)) - - if connection.user is None: - output_error('no_user', 'Not authenticated as a user') - return - - if only_owner and not connection.user.is_owner: - output_error('only_owner', 'Only allowed as owner') - return - - if (only_system_user and - not connection.user.system_generated): - output_error('only_system_user', - 'Only allowed as system user') - return - - if (not allow_system_user - and connection.user.system_generated): - output_error('not_system_user', 'Not allowed as system user') - return - - if (only_active_user and - not connection.user.is_active): - output_error('only_active_user', - 'Only allowed as active user') - return - - if only_inactive_user and connection.user.is_active: - output_error('only_inactive_user', - 'Not allowed as active user') - return - - return func(hass, connection, msg) - - return check_current_user - - return validator diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py new file mode 100644 index 00000000000..90c802423ce --- /dev/null +++ b/homeassistant/components/websocket_api/__init__.py @@ -0,0 +1,42 @@ +""" +Websocket based API for Home Assistant. + +For more details about this component, please refer to the documentation at +https://developers.home-assistant.io/docs/external_api_websocket.html +""" +from homeassistant.core import callback +from homeassistant.loader import bind_hass + +from . import commands, connection, const, decorators, http, messages + +DOMAIN = const.DOMAIN + +DEPENDENCIES = ('http',) + +# Backwards compat / Make it easier to integrate +# pylint: disable=invalid-name +ActiveConnection = connection.ActiveConnection +BASE_COMMAND_MESSAGE_SCHEMA = messages.BASE_COMMAND_MESSAGE_SCHEMA +error_message = messages.error_message +result_message = messages.result_message +async_response = decorators.async_response +require_owner = decorators.require_owner +ws_require_user = decorators.ws_require_user +# pylint: enable=invalid-name + + +@bind_hass +@callback +def async_register_command(hass, command, handler, schema): + """Register a websocket command.""" + handlers = hass.data.get(DOMAIN) + if handlers is None: + handlers = hass.data[DOMAIN] = {} + handlers[command] = (handler, schema) + + +async def async_setup(hass, config): + """Initialize the websocket API.""" + hass.http.register_view(http.WebsocketAPIView) + commands.async_register_commands(hass) + return True diff --git a/homeassistant/components/websocket_api/auth.py b/homeassistant/components/websocket_api/auth.py new file mode 100644 index 00000000000..db41f3df06d --- /dev/null +++ b/homeassistant/components/websocket_api/auth.py @@ -0,0 +1,99 @@ +"""Handle the auth of a connection.""" +import voluptuous as vol +from voluptuous.humanize import humanize_error + +from homeassistant.const import __version__ +from homeassistant.components.http.auth import validate_password +from homeassistant.components.http.ban import process_wrong_login, \ + process_success_login + +from .connection import ActiveConnection +from .error import Disconnect + +TYPE_AUTH = 'auth' +TYPE_AUTH_INVALID = 'auth_invalid' +TYPE_AUTH_OK = 'auth_ok' +TYPE_AUTH_REQUIRED = 'auth_required' + +AUTH_MESSAGE_SCHEMA = vol.Schema({ + vol.Required('type'): TYPE_AUTH, + vol.Exclusive('api_password', 'auth'): str, + vol.Exclusive('access_token', 'auth'): str, +}) + + +def auth_ok_message(): + """Return an auth_ok message.""" + return { + 'type': TYPE_AUTH_OK, + 'ha_version': __version__, + } + + +def auth_required_message(): + """Return an auth_required message.""" + return { + 'type': TYPE_AUTH_REQUIRED, + 'ha_version': __version__, + } + + +def auth_invalid_message(message): + """Return an auth_invalid message.""" + return { + 'type': TYPE_AUTH_INVALID, + 'message': message, + } + + +class AuthPhase: + """Connection that requires client to authenticate first.""" + + def __init__(self, logger, hass, send_message, request): + """Initialize the authentiated connection.""" + self._hass = hass + self._send_message = send_message + self._logger = logger + self._request = request + self._authenticated = False + self._connection = None + + async def async_handle(self, msg): + """Handle authentication.""" + try: + msg = AUTH_MESSAGE_SCHEMA(msg) + except vol.Invalid as err: + error_msg = 'Auth message incorrectly formatted: {}'.format( + humanize_error(msg, err)) + self._logger.warning(error_msg) + self._send_message(auth_invalid_message(error_msg)) + raise Disconnect + + if self._hass.auth.active and 'access_token' in msg: + self._logger.debug("Received access_token") + refresh_token = \ + await self._hass.auth.async_validate_access_token( + msg['access_token']) + if refresh_token is not None: + return await self._async_finish_auth( + refresh_token.user, refresh_token) + + elif ((not self._hass.auth.active or self._hass.auth.support_legacy) + and 'api_password' in msg): + self._logger.debug("Received api_password") + if validate_password(self._request, msg['api_password']): + return await self._async_finish_auth(None, None) + + self._send_message(auth_invalid_message( + 'Invalid access token or password')) + await process_wrong_login(self._request) + raise Disconnect + + async def _async_finish_auth(self, user, refresh_token) \ + -> ActiveConnection: + """Create an active connection.""" + self._logger.debug("Auth OK") + await process_success_login(self._request) + self._send_message(auth_ok_message()) + return ActiveConnection( + self._logger, self._hass, self._send_message, user, refresh_token) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py new file mode 100644 index 00000000000..8e1dac4af8e --- /dev/null +++ b/homeassistant/components/websocket_api/commands.py @@ -0,0 +1,183 @@ +"""Commands part of Websocket API.""" +import voluptuous as vol + +from homeassistant.const import MATCH_ALL, EVENT_TIME_CHANGED +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.service import async_get_all_descriptions + +from . import const, decorators, messages + + +TYPE_CALL_SERVICE = 'call_service' +TYPE_EVENT = 'event' +TYPE_GET_CONFIG = 'get_config' +TYPE_GET_SERVICES = 'get_services' +TYPE_GET_STATES = 'get_states' +TYPE_PING = 'ping' +TYPE_PONG = 'pong' +TYPE_SUBSCRIBE_EVENTS = 'subscribe_events' +TYPE_UNSUBSCRIBE_EVENTS = 'unsubscribe_events' + + +@callback +def async_register_commands(hass): + """Register commands.""" + async_reg = hass.components.websocket_api.async_register_command + async_reg(TYPE_SUBSCRIBE_EVENTS, handle_subscribe_events, + SCHEMA_SUBSCRIBE_EVENTS) + async_reg(TYPE_UNSUBSCRIBE_EVENTS, handle_unsubscribe_events, + SCHEMA_UNSUBSCRIBE_EVENTS) + async_reg(TYPE_CALL_SERVICE, handle_call_service, SCHEMA_CALL_SERVICE) + async_reg(TYPE_GET_STATES, handle_get_states, SCHEMA_GET_STATES) + async_reg(TYPE_GET_SERVICES, handle_get_services, SCHEMA_GET_SERVICES) + async_reg(TYPE_GET_CONFIG, handle_get_config, SCHEMA_GET_CONFIG) + async_reg(TYPE_PING, handle_ping, SCHEMA_PING) + + +SCHEMA_SUBSCRIBE_EVENTS = messages.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): TYPE_SUBSCRIBE_EVENTS, + vol.Optional('event_type', default=MATCH_ALL): str, +}) + + +SCHEMA_UNSUBSCRIBE_EVENTS = messages.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): TYPE_UNSUBSCRIBE_EVENTS, + vol.Required('subscription'): cv.positive_int, +}) + + +SCHEMA_CALL_SERVICE = messages.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): TYPE_CALL_SERVICE, + vol.Required('domain'): str, + vol.Required('service'): str, + vol.Optional('service_data'): dict +}) + + +SCHEMA_GET_STATES = messages.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): TYPE_GET_STATES, +}) + + +SCHEMA_GET_SERVICES = messages.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): TYPE_GET_SERVICES, +}) + + +SCHEMA_GET_CONFIG = messages.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): TYPE_GET_CONFIG, +}) + + +SCHEMA_PING = messages.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): TYPE_PING, +}) + + +def event_message(iden, event): + """Return an event message.""" + return { + 'id': iden, + 'type': TYPE_EVENT, + 'event': event.as_dict(), + } + + +def pong_message(iden): + """Return a pong message.""" + return { + 'id': iden, + 'type': TYPE_PONG, + } + + +@callback +def handle_subscribe_events(hass, connection, msg): + """Handle subscribe events command. + + Async friendly. + """ + async def forward_events(event): + """Forward events to websocket.""" + if event.event_type == EVENT_TIME_CHANGED: + return + + connection.send_message(event_message(msg['id'], event)) + + connection.event_listeners[msg['id']] = hass.bus.async_listen( + msg['event_type'], forward_events) + + connection.send_message(messages.result_message(msg['id'])) + + +@callback +def handle_unsubscribe_events(hass, connection, msg): + """Handle unsubscribe events command. + + Async friendly. + """ + subscription = msg['subscription'] + + if subscription in connection.event_listeners: + connection.event_listeners.pop(subscription)() + connection.send_message(messages.result_message(msg['id'])) + else: + connection.send_message(messages.error_message( + msg['id'], const.ERR_NOT_FOUND, 'Subscription not found.')) + + +@decorators.async_response +async def handle_call_service(hass, connection, msg): + """Handle call service command. + + Async friendly. + """ + blocking = True + if (msg['domain'] == 'homeassistant' and + msg['service'] in ['restart', 'stop']): + blocking = False + await hass.services.async_call( + msg['domain'], msg['service'], msg.get('service_data'), blocking, + connection.context(msg)) + connection.send_message(messages.result_message(msg['id'])) + + +@callback +def handle_get_states(hass, connection, msg): + """Handle get states command. + + Async friendly. + """ + connection.send_message(messages.result_message( + msg['id'], hass.states.async_all())) + + +@decorators.async_response +async def handle_get_services(hass, connection, msg): + """Handle get services command. + + Async friendly. + """ + descriptions = await async_get_all_descriptions(hass) + connection.send_message( + messages.result_message(msg['id'], descriptions)) + + +@callback +def handle_get_config(hass, connection, msg): + """Handle get config command. + + Async friendly. + """ + connection.send_message(messages.result_message( + msg['id'], hass.config.as_dict())) + + +@callback +def handle_ping(hass, connection, msg): + """Handle ping command. + + Async friendly. + """ + connection.send_message(pong_message(msg['id'])) diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py new file mode 100644 index 00000000000..1cb58591a0a --- /dev/null +++ b/homeassistant/components/websocket_api/connection.py @@ -0,0 +1,78 @@ +"""Connection session.""" +import voluptuous as vol + +from homeassistant.core import callback, Context + +from . import const, messages + + +class ActiveConnection: + """Handle an active websocket client connection.""" + + def __init__(self, logger, hass, send_message, user, refresh_token): + """Initialize an active connection.""" + self.logger = logger + self.hass = hass + self.send_message = send_message + self.user = user + if refresh_token: + self.refresh_token_id = refresh_token.id + else: + self.refresh_token_id = None + + self.event_listeners = {} + self.last_id = 0 + + def context(self, msg): + """Return a context.""" + user = self.user + if user is None: + return Context() + return Context(user_id=user.id) + + @callback + def async_handle(self, msg): + """Handle a single incoming message.""" + handlers = self.hass.data[const.DOMAIN] + + try: + msg = messages.MINIMAL_MESSAGE_SCHEMA(msg) + cur_id = msg['id'] + except vol.Invalid: + self.logger.error('Received invalid command', msg) + self.send_message(messages.error_message( + msg.get('id'), const.ERR_INVALID_FORMAT, + 'Message incorrectly formatted.')) + return + + if cur_id <= self.last_id: + self.send_message(messages.error_message( + cur_id, const.ERR_ID_REUSE, + 'Identifier values have to increase.')) + return + + if msg['type'] not in handlers: + self.logger.error( + 'Received invalid command: {}'.format(msg['type'])) + self.send_message(messages.error_message( + cur_id, const.ERR_UNKNOWN_COMMAND, + 'Unknown command.')) + return + + handler, schema = handlers[msg['type']] + + try: + handler(self.hass, self, schema(msg)) + except Exception: # pylint: disable=broad-except + self.logger.exception('Error handling message: %s', msg) + self.send_message(messages.error_message( + cur_id, const.ERR_UNKNOWN_ERROR, + 'Unknown error.')) + + self.last_id = cur_id + + @callback + def async_close(self): + """Close down connection.""" + for unsub in self.event_listeners.values(): + unsub() diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py new file mode 100644 index 00000000000..8d452959ca5 --- /dev/null +++ b/homeassistant/components/websocket_api/const.py @@ -0,0 +1,20 @@ +"""Websocket constants.""" +import asyncio +from concurrent import futures + +DOMAIN = 'websocket_api' +URL = '/api/websocket' +MAX_PENDING_MSG = 512 + +ERR_ID_REUSE = 1 +ERR_INVALID_FORMAT = 2 +ERR_NOT_FOUND = 3 +ERR_UNKNOWN_COMMAND = 4 +ERR_UNKNOWN_ERROR = 5 + +TYPE_RESULT = 'result' + +# Define the possible errors that occur when connections are cancelled. +# Originally, this was just asyncio.CancelledError, but issue #9546 showed +# that futures.CancelledErrors can also occur in some situations. +CANCELLATION_ERRORS = (asyncio.CancelledError, futures.CancelledError) diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py new file mode 100644 index 00000000000..5f78790f5db --- /dev/null +++ b/homeassistant/components/websocket_api/decorators.py @@ -0,0 +1,103 @@ +"""Decorators for the Websocket API.""" +from functools import wraps +import logging + +from homeassistant.core import callback + +from . import messages + + +_LOGGER = logging.getLogger(__name__) + + +async def _handle_async_response(func, hass, connection, msg): + """Create a response and handle exception.""" + try: + await func(hass, connection, msg) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + connection.send_message(messages.error_message( + msg['id'], 'unknown', 'Unexpected error occurred')) + + +def async_response(func): + """Decorate an async function to handle WebSocket API messages.""" + @callback + @wraps(func) + def schedule_handler(hass, connection, msg): + """Schedule the handler.""" + hass.async_create_task( + _handle_async_response(func, hass, connection, msg)) + + return schedule_handler + + +def require_owner(func): + """Websocket decorator to require user to be an owner.""" + @wraps(func) + def with_owner(hass, connection, msg): + """Check owner and call function.""" + user = connection.user + + if user is None or not user.is_owner: + connection.send_message(messages.error_message( + msg['id'], 'unauthorized', 'This command is for owners only.')) + return + + func(hass, connection, msg) + + return with_owner + + +def ws_require_user( + only_owner=False, only_system_user=False, allow_system_user=True, + only_active_user=True, only_inactive_user=False): + """Decorate function validating login user exist in current WS connection. + + Will write out error message if not authenticated. + """ + def validator(func): + """Decorate func.""" + @wraps(func) + def check_current_user(hass, connection, msg): + """Check current user.""" + def output_error(message_id, message): + """Output error message.""" + connection.send_message(messages.error_message( + msg['id'], message_id, message)) + + if connection.user is None: + output_error('no_user', 'Not authenticated as a user') + return + + if only_owner and not connection.user.is_owner: + output_error('only_owner', 'Only allowed as owner') + return + + if (only_system_user and + not connection.user.system_generated): + output_error('only_system_user', + 'Only allowed as system user') + return + + if (not allow_system_user + and connection.user.system_generated): + output_error('not_system_user', 'Not allowed as system user') + return + + if (only_active_user and + not connection.user.is_active): + output_error('only_active_user', + 'Only allowed as active user') + return + + if only_inactive_user and connection.user.is_active: + output_error('only_inactive_user', + 'Not allowed as active user') + return + + return func(hass, connection, msg) + + return check_current_user + + return validator diff --git a/homeassistant/components/websocket_api/error.py b/homeassistant/components/websocket_api/error.py new file mode 100644 index 00000000000..c0b7ea04554 --- /dev/null +++ b/homeassistant/components/websocket_api/error.py @@ -0,0 +1,8 @@ +"""WebSocket API related errors.""" +from homeassistant.exceptions import HomeAssistantError + + +class Disconnect(HomeAssistantError): + """Disconnect the current session.""" + + pass diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py new file mode 100644 index 00000000000..87f25c9b3ef --- /dev/null +++ b/homeassistant/components/websocket_api/http.py @@ -0,0 +1,189 @@ +"""View to accept incoming websocket connection.""" +import asyncio +from contextlib import suppress +from functools import partial +import json +import logging + +from aiohttp import web, WSMsgType +import async_timeout + +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import callback +from homeassistant.components.http import HomeAssistantView +from homeassistant.helpers.json import JSONEncoder + +from .const import MAX_PENDING_MSG, CANCELLATION_ERRORS, URL +from .auth import AuthPhase, auth_required_message +from .error import Disconnect + +JSON_DUMP = partial(json.dumps, cls=JSONEncoder) + + +class WebsocketAPIView(HomeAssistantView): + """View to serve a websockets endpoint.""" + + name = "websocketapi" + url = URL + requires_auth = False + + async def get(self, request): + """Handle an incoming websocket connection.""" + return await WebSocketHandler( + request.app['hass'], request).async_handle() + + +class WebSocketHandler: + """Handle an active websocket client connection.""" + + def __init__(self, hass, request): + """Initialize an active connection.""" + self.hass = hass + self.request = request + self.wsock = None + self._to_write = asyncio.Queue(maxsize=MAX_PENDING_MSG, loop=hass.loop) + self._handle_task = None + self._writer_task = None + self._logger = logging.getLogger( + "{}.connection.{}".format(__name__, id(self))) + + async def _writer(self): + """Write outgoing messages.""" + # Exceptions if Socket disconnected or cancelled by connection handler + with suppress(RuntimeError, *CANCELLATION_ERRORS): + while not self.wsock.closed: + message = await self._to_write.get() + if message is None: + break + self._logger.debug("Sending %s", message) + try: + await self.wsock.send_json(message, dumps=JSON_DUMP) + except TypeError as err: + self._logger.error('Unable to serialize to JSON: %s\n%s', + err, message) + + @callback + def _send_message(self, message): + """Send a message to the client. + + Closes connection if the client is not reading the messages. + + Async friendly. + """ + try: + self._to_write.put_nowait(message) + except asyncio.QueueFull: + self._logger.error("Client exceeded max pending messages [2]: %s", + MAX_PENDING_MSG) + self._cancel() + + @callback + def _cancel(self): + """Cancel the connection.""" + self._handle_task.cancel() + self._writer_task.cancel() + + async def async_handle(self): + """Handle a websocket response.""" + request = self.request + wsock = self.wsock = web.WebSocketResponse(heartbeat=55) + await wsock.prepare(request) + self._logger.debug("Connected") + + # Py3.7+ + if hasattr(asyncio, 'current_task'): + # pylint: disable=no-member + self._handle_task = asyncio.current_task() + else: + self._handle_task = asyncio.Task.current_task(loop=self.hass.loop) + + @callback + def handle_hass_stop(event): + """Cancel this connection.""" + self._cancel() + + unsub_stop = self.hass.bus.async_listen( + EVENT_HOMEASSISTANT_STOP, handle_hass_stop) + + self._writer_task = self.hass.async_create_task(self._writer()) + + auth = AuthPhase(self._logger, self.hass, self._send_message, request) + connection = None + disconnect_warn = None + + try: + self._send_message(auth_required_message()) + + # Auth Phase + try: + with async_timeout.timeout(10): + msg = await wsock.receive() + except asyncio.TimeoutError: + disconnect_warn = \ + 'Did not receive auth message within 10 seconds' + raise Disconnect + + if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING): + raise Disconnect + + elif msg.type != WSMsgType.TEXT: + disconnect_warn = 'Received non-Text message.' + raise Disconnect + + try: + msg = msg.json() + except ValueError: + disconnect_warn = 'Received invalid JSON.' + raise Disconnect + + self._logger.debug("Received %s", msg) + connection = await auth.async_handle(msg) + + # Command phase + while not wsock.closed: + msg = await wsock.receive() + + if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING): + break + + elif msg.type != WSMsgType.TEXT: + disconnect_warn = 'Received non-Text message.' + break + + try: + msg = msg.json() + except ValueError: + disconnect_warn = 'Received invalid JSON.' + break + + self._logger.debug("Received %s", msg) + connection.async_handle(msg) + + except asyncio.CancelledError: + self._logger.info("Connection closed by client") + + except Disconnect: + pass + + except Exception: # pylint: disable=broad-except + self._logger.exception("Unexpected error inside websocket API") + + finally: + unsub_stop() + + if connection is not None: + connection.async_close() + + try: + self._to_write.put_nowait(None) + # Make sure all error messages are written before closing + await self._writer_task + except asyncio.QueueFull: + self._writer_task.cancel() + + await wsock.close() + + if disconnect_warn is None: + self._logger.debug("Disconnected") + else: + self._logger.warning("Disconnected: %s", disconnect_warn) diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py new file mode 100644 index 00000000000..d616b6ad670 --- /dev/null +++ b/homeassistant/components/websocket_api/messages.py @@ -0,0 +1,42 @@ +"""Message templates for websocket commands.""" + +import voluptuous as vol + +from homeassistant.helpers import config_validation as cv + +from . import const + + +# Minimal requirements of a message +MINIMAL_MESSAGE_SCHEMA = vol.Schema({ + vol.Required('id'): cv.positive_int, + vol.Required('type'): cv.string, +}, extra=vol.ALLOW_EXTRA) + +# Base schema to extend by message handlers +BASE_COMMAND_MESSAGE_SCHEMA = vol.Schema({ + vol.Required('id'): cv.positive_int, +}) + + +def result_message(iden, result=None): + """Return a success result message.""" + return { + 'id': iden, + 'type': const.TYPE_RESULT, + 'success': True, + 'result': result, + } + + +def error_message(iden, code, message): + """Return an error result message.""" + return { + 'id': iden, + 'type': const.TYPE_RESULT, + 'success': False, + 'error': { + 'code': code, + 'message': message, + }, + } diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index 0399b25b278..d21ccc18c93 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -4,7 +4,6 @@ Support for Wink hubs. For more details about this component, please refer to the documentation at https://home-assistant.io/components/wink/ """ -import asyncio from datetime import timedelta import json import logging @@ -763,8 +762,7 @@ class WinkDevice(Entity): class WinkSirenDevice(WinkDevice): """Representation of a Wink siren device.""" - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['switch'].append(self) @@ -824,8 +822,7 @@ class WinkNimbusDialDevice(WinkDevice): super().__init__(dial, hass) self.parent = nimbus - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is added to hass.""" self.hass.data[DOMAIN]['entities']['sensor'].append(self) diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py index f2d51d2fc2e..27414a64150 100644 --- a/homeassistant/components/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara.py @@ -4,7 +4,6 @@ Support for Xiaomi Gateways. For more details about this component, please refer to the documentation at https://home-assistant.io/components/xiaomi_aqara/ """ -import asyncio import logging from datetime import timedelta @@ -23,7 +22,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow from homeassistant.util import slugify -REQUIREMENTS = ['PyXiaomiGateway==0.10.0'] +REQUIREMENTS = ['PyXiaomiGateway==0.11.0'] _LOGGER = logging.getLogger(__name__) @@ -113,8 +112,7 @@ def setup(hass, config): interface = config[DOMAIN][CONF_INTERFACE] discovery_retry = config[DOMAIN][CONF_DISCOVERY_RETRY] - @asyncio.coroutine - def xiaomi_gw_discovered(service, discovery_info): + async def xiaomi_gw_discovered(service, discovery_info): """Perform action when Xiaomi Gateway device(s) has been found.""" # We don't need to do anything here, the purpose of Home Assistant's # discovery service is to just trigger loading of this @@ -218,7 +216,7 @@ class XiaomiDevice(Entity): self._get_from_hub = xiaomi_hub.get_from_hub self._device_state_attributes = {} self._remove_unavailability_tracker = None - xiaomi_hub.callbacks[self._sid].append(self._add_push_data_job) + self._xiaomi_hub = xiaomi_hub self.parse_data(device['data'], device['raw_data']) self.parse_voltage(device['data']) @@ -233,9 +231,9 @@ class XiaomiDevice(Entity): def _add_push_data_job(self, *args): self.hass.add_job(self.push_data, *args) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Start unavailability tracking.""" + self._xiaomi_hub.callbacks[self._sid].append(self._add_push_data_job) self._async_track_unavailable() @property diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 7c48a577850..2e74e079d5f 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -109,7 +109,8 @@ async def async_setup(hass, config): await APPLICATION_CONTROLLER.startup(auto_form=True) for device in APPLICATION_CONTROLLER.devices.values(): - hass.async_add_job(listener.async_device_initialized(device, False)) + hass.async_create_task( + listener.async_device_initialized(device, False)) async def permit(service): """Allow devices to join this network.""" @@ -161,7 +162,8 @@ class ApplicationListener: def device_initialized(self, device): """Handle device joined and basic information discovered.""" - self._hass.async_add_job(self.async_device_initialized(device, True)) + self._hass.async_create_task( + self.async_device_initialized(device, True)) def device_left(self, device): """Handle device leaving the network.""" @@ -170,7 +172,7 @@ class ApplicationListener: def device_removed(self, device): """Handle device being removed from the network.""" for device_entity in self._device_registry[device.ieee]: - self._hass.async_add_job(device_entity.async_remove()) + self._hass.async_create_task(device_entity.async_remove()) async def async_device_initialized(self, device, join): """Handle device joined and basic information discovered (async).""" diff --git a/homeassistant/components/zigbee.py b/homeassistant/components/zigbee.py index 67bdf744251..4c294e51231 100644 --- a/homeassistant/components/zigbee.py +++ b/homeassistant/components/zigbee.py @@ -4,7 +4,6 @@ Support for ZigBee devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/zigbee/ """ -import asyncio import logging from binascii import hexlify, unhexlify @@ -277,8 +276,7 @@ class ZigBeeDigitalIn(Entity): self._config = config self._state = False - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" def handle_frame(frame): """Handle an incoming frame. @@ -403,8 +401,7 @@ class ZigBeeAnalogIn(Entity): self._config = config self._value = None - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register callbacks.""" def handle_frame(frame): """Handle an incoming frame. diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 3754bf5edbc..370b52d1360 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -55,7 +55,7 @@ async def async_setup(hass, config): entry.get(CONF_ICON), entry.get(CONF_PASSIVE)) zone.entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, entry[CONF_NAME], entities) - hass.async_add_job(zone.async_update_ha_state()) + hass.async_create_task(zone.async_update_ha_state()) entities.add(zone.entity_id) if ENTITY_ID_HOME not in entities and HOME_ZONE not in zone_entries: @@ -63,7 +63,7 @@ async def async_setup(hass, config): hass.config.latitude, hass.config.longitude, DEFAULT_RADIUS, ICON_HOME, False) zone.entity_id = ENTITY_ID_HOME - hass.async_add_job(zone.async_update_ha_state()) + hass.async_create_task(zone.async_update_ha_state()) return True @@ -77,7 +77,7 @@ async def async_setup_entry(hass, config_entry): 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()) + hass.async_create_task(zone.async_update_ha_state()) hass.data[DOMAIN][slugify(name)] = zone return True diff --git a/homeassistant/components/zoneminder.py b/homeassistant/components/zoneminder.py index b7f43177200..3f6d8ba7fcf 100644 --- a/homeassistant/components/zoneminder.py +++ b/homeassistant/components/zoneminder.py @@ -15,7 +15,7 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['zm-py==0.0.4'] +REQUIREMENTS = ['zm-py==0.0.5'] CONF_PATH_ZMS = 'path_zms' diff --git a/homeassistant/components/zwave/.translations/ca.json b/homeassistant/components/zwave/.translations/ca.json new file mode 100644 index 00000000000..b617a902374 --- /dev/null +++ b/homeassistant/components/zwave/.translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave ja est\u00e0 configurat", + "one_instance_only": "El component nom\u00e9s admet una inst\u00e0ncia de Z-Wave" + }, + "error": { + "option_error": "Ha fallat la validaci\u00f3 de Z-Wave. \u00c9s correcta la ruta al port on hi ha la mem\u00f2ria USB?" + }, + "step": { + "user": { + "data": { + "network_key": "Clau de xarxa (deixeu-ho en blanc per generar-la autom\u00e0ticament)", + "usb_path": "Ruta del port USB" + }, + "description": "Consulteu https://www.home-assistant.io/docs/z-wave/installation/ per obtenir informaci\u00f3 sobre les variables de configuraci\u00f3", + "title": "Configureu Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/en.json b/homeassistant/components/zwave/.translations/en.json new file mode 100644 index 00000000000..081d5c858cb --- /dev/null +++ b/homeassistant/components/zwave/.translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave is already configured", + "one_instance_only": "Component only supports one Z-Wave instance" + }, + "error": { + "option_error": "Z-Wave validation failed. Is the path to the USB stick correct?" + }, + "step": { + "user": { + "data": { + "network_key": "Network Key (leave blank to auto-generate)", + "usb_path": "USB Path" + }, + "description": "See https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables", + "title": "Set up Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/ko.json b/homeassistant/components/zwave/.translations/ko.json new file mode 100644 index 00000000000..43103de3d51 --- /dev/null +++ b/homeassistant/components/zwave/.translations/ko.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "one_instance_only": "\uad6c\uc131\uc694\uc18c\ub294 \ud558\ub098\uc758 Z-Wave \uc778\uc2a4\ud134\uc2a4\ub9cc \uc9c0\uc6d0\ud569\ub2c8\ub2e4" + }, + "error": { + "option_error": "Z-Wave \uc720\ud6a8\uc131 \uac80\uc0ac\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. USB \uc2a4\ud2f1\uc758 \uacbd\ub85c\uac00 \uc815\ud655\ud569\ub2c8\uae4c?" + }, + "step": { + "user": { + "data": { + "network_key": "\ub124\ud2b8\uc6cc\ud06c \ud0a4 (\uacf5\ub780\uc73c\ub85c \ube44\uc6cc\ub450\uba74 \uc790\ub3d9 \uc0dd\uc131\ud569\ub2c8\ub2e4)", + "usb_path": "USB \uacbd\ub85c" + }, + "description": "\uad6c\uc131 \ubcc0\uc218\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/docs/z-wave/installation/ \uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694", + "title": "Z-Wave \uc124\uc815" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/lb.json b/homeassistant/components/zwave/.translations/lb.json new file mode 100644 index 00000000000..84b6d8aa67d --- /dev/null +++ b/homeassistant/components/zwave/.translations/lb.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave ass scho konfigur\u00e9iert", + "one_instance_only": "Komponent \u00ebnnerst\u00ebtzt n\u00ebmmen eng Z-Wave Instanz" + }, + "error": { + "option_error": "Z-Wave Validatioun net g\u00eblteg. Ass de Pad zum USB Stick richteg?" + }, + "step": { + "user": { + "data": { + "network_key": "Netzwierk Schl\u00ebssel (eidel loossen fir een automatesch z'erstellen)", + "usb_path": "USB Pad" + }, + "description": "Lies op https://www.home-assistant.io/docs/z-wave/installation/ fir weider Informatiounen iwwert d'Konfiguratioun vun den Variabelen", + "title": "Z-Wave konfigur\u00e9ieren" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/nl.json b/homeassistant/components/zwave/.translations/nl.json new file mode 100644 index 00000000000..0b700b895fd --- /dev/null +++ b/homeassistant/components/zwave/.translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave is al geconfigureerd", + "one_instance_only": "Component ondersteunt slechts \u00e9\u00e9n Z-Wave-instantie" + }, + "error": { + "option_error": "Z-Wave-validatie mislukt. Is het pad naar de USB-stick correct?" + }, + "step": { + "user": { + "data": { + "network_key": "Netwerksleutel (laat leeg om automatisch te genereren)", + "usb_path": "USB-pad" + }, + "description": "Zie https://www.home-assistant.io/docs/z-wave/installation/ voor informatie over de configuratievariabelen", + "title": "Stel Z-Wave in" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/ru.json b/homeassistant/components/zwave/.translations/ru.json new file mode 100644 index 00000000000..457bfd3baa8 --- /dev/null +++ b/homeassistant/components/zwave/.translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 Z-Wave" + }, + "error": { + "option_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 Z-Wave. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0443\u0442\u044c \u043a USB-\u043d\u0430\u043a\u043e\u043f\u0438\u0442\u0435\u043b\u044e." + }, + "step": { + "user": { + "data": { + "network_key": "\u041a\u043b\u044e\u0447 \u0441\u0435\u0442\u0438 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438)", + "usb_path": "\u041f\u0443\u0442\u044c \u043a USB" + }, + "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0445 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/sl.json b/homeassistant/components/zwave/.translations/sl.json new file mode 100644 index 00000000000..fa799d1ed36 --- /dev/null +++ b/homeassistant/components/zwave/.translations/sl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave je \u017ee konfiguriran", + "one_instance_only": "Komponente podpirajo le eno Z-Wave instanco" + }, + "error": { + "option_error": "Potrjevanje Z-Wave ni uspelo. Ali je pot do USB klju\u010da pravilna?" + }, + "step": { + "user": { + "data": { + "network_key": "Omre\u017eni klju\u010d (pustite prazno za samodejno generiranje)", + "usb_path": "USB Pot" + }, + "description": "Za informacije o konfiguracijskih spremenljivka si oglejte https://www.home-assistant.io/docs/z-wave/installation/", + "title": "Nastavite Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/sv.json b/homeassistant/components/zwave/.translations/sv.json new file mode 100644 index 00000000000..508652a1784 --- /dev/null +++ b/homeassistant/components/zwave/.translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave \u00e4r redan konfigurerat", + "one_instance_only": "Komponenten st\u00f6der endast en Z-Wave-instans" + }, + "error": { + "option_error": "Z-Wave-valideringen misslyckades. \u00c4r s\u00f6kv\u00e4gen till USB-minnet korrekt?" + }, + "step": { + "user": { + "data": { + "network_key": "N\u00e4tverksnyckel (l\u00e4mna blank f\u00f6r automatisk generering)", + "usb_path": "USB-s\u00f6kv\u00e4g" + }, + "description": "Se https://www.home-assistant.io/docs/z-wave/installation/ f\u00f6r information om konfigurationsvariabler", + "title": "St\u00e4lla in Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/zh-Hans.json b/homeassistant/components/zwave/.translations/zh-Hans.json new file mode 100644 index 00000000000..2c72ce72c60 --- /dev/null +++ b/homeassistant/components/zwave/.translations/zh-Hans.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave \u5df2\u914d\u7f6e\u5b8c\u6210", + "one_instance_only": "\u7ec4\u4ef6\u53ea\u652f\u6301\u4e00\u4e2a Z-Wave \u5b9e\u4f8b" + }, + "error": { + "option_error": "Z-Wave \u9a8c\u8bc1\u5931\u8d25\u3002 USB \u68d2\u7684\u8def\u5f84\u662f\u5426\u6b63\u786e\uff1f" + }, + "step": { + "user": { + "data": { + "network_key": "\u7f51\u7edc\u5bc6\u94a5\uff08\u7559\u7a7a\u5c06\u81ea\u52a8\u751f\u6210\uff09", + "usb_path": "USB \u8def\u5f84" + }, + "description": "\u6709\u5173\u914d\u7f6e\u7684\u4fe1\u606f\uff0c\u8bf7\u53c2\u9605 https://www.home-assistant.io/docs/z-wave/installation/", + "title": "\u8bbe\u7f6e Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/zh-Hant.json b/homeassistant/components/zwave/.translations/zh-Hant.json new file mode 100644 index 00000000000..2a84e8b3fd6 --- /dev/null +++ b/homeassistant/components/zwave/.translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Z-Wave \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "one_instance_only": "\u7d44\u4ef6\u50c5\u652f\u63f4\u4e00\u7d44 Z-Wave \u7269\u4ef6" + }, + "error": { + "option_error": "Z-Wave \u9a57\u8b49\u5931\u6557\uff0c\u8acb\u78ba\u5b9a USB \u96a8\u8eab\u789f\u8def\u5f91\u6b63\u78ba\uff1f" + }, + "step": { + "user": { + "data": { + "network_key": "\u7db2\u8def\u5bc6\u9470\uff08\u4fdd\u7559\u7a7a\u767d\u5c07\u6703\u81ea\u52d5\u7522\u751f\uff09", + "usb_path": "USB \u8def\u5f91" + }, + "description": "\u95dc\u65bc\u8a2d\u5b9a\u8b8a\u6578\u8cc7\u8a0a\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/docs/z-wave/installation/", + "title": "\u8a2d\u5b9a Z-Wave" + } + }, + "title": "Z-Wave" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 4cb2f6b0f7b..fa78f719557 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -84,6 +84,17 @@ SET_CONFIG_PARAMETER_SCHEMA = vol.Schema({ vol.Optional(const.ATTR_CONFIG_SIZE, default=2): vol.Coerce(int) }) +SET_NODE_VALUE_SCHEMA = vol.Schema({ + vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), + vol.Required(const.ATTR_VALUE_ID): vol.Coerce(int), + vol.Required(const.ATTR_CONFIG_VALUE): vol.Coerce(int) +}) + +REFRESH_NODE_VALUE_SCHEMA = vol.Schema({ + vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), + vol.Required(const.ATTR_VALUE_ID): vol.Coerce(int) +}) + SET_POLL_INTENSITY_SCHEMA = vol.Schema({ vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), vol.Required(const.ATTR_VALUE_ID): vol.Coerce(int), @@ -264,7 +275,9 @@ async def async_setup(hass, config): ZWaveNetwork.SIGNAL_SCENE_EVENT, ZWaveNetwork.SIGNAL_NODE_EVENT, ZWaveNetwork.SIGNAL_AWAKE_NODES_QUERIED, - ZWaveNetwork.SIGNAL_ALL_NODES_QUERIED): + ZWaveNetwork.SIGNAL_ALL_NODES_QUERIED, + ZWaveNetwork + .SIGNAL_ALL_NODES_QUERIED_SOME_DEAD): pprint(_obj_to_dict(value)) print("") @@ -345,6 +358,12 @@ async def async_setup(hass, config): "have been queried") hass.bus.fire(const.EVENT_NETWORK_COMPLETE) + def network_complete_some_dead(): + """Handle the querying of all nodes on network.""" + _LOGGER.info("Z-Wave network is complete. All nodes on the network " + "have been queried, but some node are marked dead") + hass.bus.fire(const.EVENT_NETWORK_COMPLETE_SOME_DEAD) + dispatcher.connect( value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED, weak=False) dispatcher.connect( @@ -353,6 +372,9 @@ async def async_setup(hass, config): network_ready, ZWaveNetwork.SIGNAL_AWAKE_NODES_QUERIED, weak=False) dispatcher.connect( network_complete, ZWaveNetwork.SIGNAL_ALL_NODES_QUERIED, weak=False) + dispatcher.connect( + network_complete_some_dead, + ZWaveNetwork.SIGNAL_ALL_NODES_QUERIED_SOME_DEAD, weak=False) def add_node(service): """Switch into inclusion mode.""" @@ -492,6 +514,23 @@ async def async_setup(hass, config): "with selection %s", param, node_id, selection) + def refresh_node_value(service): + """Refresh the specified value from a node.""" + node_id = service.data.get(const.ATTR_NODE_ID) + value_id = service.data.get(const.ATTR_VALUE_ID) + node = network.nodes[node_id] + node.values[value_id].refresh() + _LOGGER.info("Node %s value %s refreshed", node_id, value_id) + + def set_node_value(service): + """Set the specified value on a node.""" + node_id = service.data.get(const.ATTR_NODE_ID) + value_id = service.data.get(const.ATTR_VALUE_ID) + value = service.data.get(const.ATTR_CONFIG_VALUE) + node = network.nodes[node_id] + node.values[value_id].data = value + _LOGGER.info("Node %s value %s set to %s", node_id, value_id, value) + def print_config_parameter(service): """Print a config parameter from a node.""" node_id = service.data.get(const.ATTR_NODE_ID) @@ -662,6 +701,12 @@ async def async_setup(hass, config): hass.services.register(DOMAIN, const.SERVICE_SET_CONFIG_PARAMETER, set_config_parameter, schema=SET_CONFIG_PARAMETER_SCHEMA) + hass.services.register(DOMAIN, const.SERVICE_SET_NODE_VALUE, + set_node_value, + schema=SET_NODE_VALUE_SCHEMA) + hass.services.register(DOMAIN, const.SERVICE_REFRESH_NODE_VALUE, + refresh_node_value, + schema=REFRESH_NODE_VALUE_SCHEMA) hass.services.register(DOMAIN, const.SERVICE_PRINT_CONFIG_PARAMETER, print_config_parameter, schema=PRINT_CONFIG_PARAMETER_SCHEMA) diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index 0228e64cf6e..b84f0287349 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -39,6 +39,8 @@ SERVICE_SOFT_RESET = "soft_reset" SERVICE_TEST_NODE = "test_node" SERVICE_TEST_NETWORK = "test_network" SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter" +SERVICE_SET_NODE_VALUE = "set_node_value" +SERVICE_REFRESH_NODE_VALUE = "refresh_node_value" SERVICE_PRINT_CONFIG_PARAMETER = "print_config_parameter" SERVICE_PRINT_NODE = "print_node" SERVICE_REMOVE_FAILED_NODE = "remove_failed_node" @@ -58,6 +60,7 @@ EVENT_SCENE_ACTIVATED = "zwave.scene_activated" EVENT_NODE_EVENT = "zwave.node_event" EVENT_NETWORK_READY = "zwave.network_ready" EVENT_NETWORK_COMPLETE = "zwave.network_complete" +EVENT_NETWORK_COMPLETE_SOME_DEAD = "zwave.network_complete_some_dead" EVENT_NETWORK_START = "zwave.network_start" EVENT_NETWORK_STOP = "zwave.network_stop" diff --git a/homeassistant/components/zwave/discovery_schemas.py b/homeassistant/components/zwave/discovery_schemas.py index 2a4e42ab92c..56d63d658a9 100644 --- a/homeassistant/components/zwave/discovery_schemas.py +++ b/homeassistant/components/zwave/discovery_schemas.py @@ -211,7 +211,8 @@ DISCOVERY_SCHEMAS = [ const.COMMAND_CLASS_SENSOR_MULTILEVEL, const.COMMAND_CLASS_METER, const.COMMAND_CLASS_ALARM, - const.COMMAND_CLASS_SENSOR_ALARM], + const.COMMAND_CLASS_SENSOR_ALARM, + const.COMMAND_CLASS_INDICATOR], const.DISC_GENRE: const.GENRE_USER, }})}, {const.DISC_COMPONENT: 'switch', diff --git a/homeassistant/components/zwave/services.yaml b/homeassistant/components/zwave/services.yaml index 1762c33237d..7c926a5a879 100644 --- a/homeassistant/components/zwave/services.yaml +++ b/homeassistant/components/zwave/services.yaml @@ -18,40 +18,40 @@ change_association: description: (Optional) Instance of multichannel association. Defaults to 0. add_node: - description: Add a new (unsecure) node to the Z-Wave network. Refer to OZW.log for progress. + description: Add a new (unsecure) node to the Z-Wave network. Refer to OZW_Log.txt for progress. add_node_secure: - description: Add a new node to the Z-Wave network with secure communications. Secure network key must be set, this process will fallback to add_node (unsecure) for unsupported devices. Note that unsecure devices can't directly talk to secure devices. Refer to OZW.log for progress. + description: Add a new node to the Z-Wave network with secure communications. Secure network key must be set, this process will fallback to add_node (unsecure) for unsupported devices. Note that unsecure devices can't directly talk to secure devices. Refer to OZW_Log.txt for progress. cancel_command: description: Cancel a running Z-Wave controller command. Use this to exit add_node, if you weren't going to use it but activated it. heal_network: - description: Start a Z-Wave network heal. This might take a while and will slow down the Z-Wave network greatly while it is being processed. Refer to OZW.log for progress. + description: Start a Z-Wave network heal. This might take a while and will slow down the Z-Wave network greatly while it is being processed. Refer to OZW_Log.txt for progress. fields: return_routes: description: Whether or not to update the return routes from the nodes to the controller. Defaults to False. example: True heal_node: - description: Start a Z-Wave node heal. Refer to OZW.log for progress. + description: Start a Z-Wave node heal. Refer to OZW_Log.txt for progress. fields: return_routes: description: Whether or not to update the return routes from the node to the controller. Defaults to False. example: True remove_node: - description: Remove a node from the Z-Wave network. Refer to OZW.log for progress. + description: Remove a node from the Z-Wave network. Refer to OZW_Log.txt for progress. remove_failed_node: - description: This command will remove a failed node from the network. The node should be on the controllers failed nodes list, otherwise this command will fail. Refer to OZW.log for progress. + description: This command will remove a failed node from the network. The node should be on the controller's failed nodes list, otherwise this command will fail. Refer to OZW_Log.txt for progress. fields: node_id: description: Node id of the device to remove (integer). example: 10 replace_failed_node: - description: Replace a failed node with another. If the node is not in the controller's failed nodes list, or the node responds, this command will fail. Refer to OZW.log for progress. + description: Replace a failed node with another. If the node is not in the controller's failed nodes list, or the node responds, this command will fail. Refer to OZW_Log.txt for progress. fields: node_id: description: Node id of the device to replace (integer). @@ -69,6 +69,24 @@ set_config_parameter: size: description: (Optional) Set the size of the parameter value. Only needed if no parameters are available. +set_node_value: + description: Set the value for a given value_id on a Z-Wave device. + fields: + node_id: + description: Node id of the device to set the value on (integer). + value_id: + description: Value id of the value to set (integer). + value: + description: Value to set (integer). + +refresh_node_value: + description: Refresh the value for a given value_id on a Z-Wave device. + fields: + node_id: + description: Node id of the device to refresh value from (integer). + value_id: + description: Value id of the value to refresh. + set_poll_intensity: description: Set the polling interval to a nodes value fields: @@ -129,10 +147,10 @@ stop_network: description: Stop the Z-Wave network, all updates into HASS will stop. soft_reset: - description: This will reset the controller without removing its data. Use carefully because not all controllers support this. Refer to controllers manual. + description: This will reset the controller without removing its data. Use carefully because not all controllers support this. Refer to your controller's manual. test_network: - description: This will send test to nodes in the Z-Wave network. This will greatly slow down the Z-Wave network while it is being processed. Refer to OZW.log for progress. + description: This will send test to nodes in the Z-Wave network. This will greatly slow down the Z-Wave network while it is being processed. Refer to OZW_Log.txt for progress. test_node: description: This will send test messages to a node in the Z-Wave network. This could bring back dead nodes. diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 7763594e0e1..1cc2e1362af 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -115,14 +115,13 @@ should follow the same return values as a normal step. If the result of the step is to show a form, the user will be able to continue the flow from the config panel. """ - import logging import uuid from typing import Set, Optional, List, Dict # noqa pylint: disable=unused-import from homeassistant import data_entry_flow from homeassistant.core import callback, HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ConfigEntryNotReady from homeassistant.setup import async_setup_component, async_process_deps_reqs from homeassistant.util.decorator import Registry @@ -141,6 +140,7 @@ FLOWS = [ 'deconz', 'homematicip_cloud', 'hue', + 'ifttt', 'ios', 'mqtt', 'nest', @@ -148,6 +148,7 @@ FLOWS = [ 'sonos', 'tradfri', 'zone', + 'upnp', ] @@ -159,9 +160,15 @@ PATH_CONFIG = '.config_entries.json' SAVE_DELAY = 1 +# The config entry has been set up successfully ENTRY_STATE_LOADED = 'loaded' +# There was an error while trying to set up this config entry ENTRY_STATE_SETUP_ERROR = 'setup_error' +# The config entry was not ready to be set up yet, but might be later +ENTRY_STATE_SETUP_RETRY = 'setup_retry' +# The config entry has not been loaded ENTRY_STATE_NOT_LOADED = 'not_loaded' +# An error occurred when trying to unload the entry ENTRY_STATE_FAILED_UNLOAD = 'failed_unload' DISCOVERY_NOTIFICATION_ID = 'config_entry_discovery' @@ -184,7 +191,8 @@ class ConfigEntry: """Hold a configuration entry.""" __slots__ = ('entry_id', 'version', 'domain', 'title', 'data', 'source', - 'connection_class', 'state') + 'connection_class', 'state', '_setup_lock', + '_async_cancel_retry_setup') def __init__(self, version: str, domain: str, title: str, data: dict, source: str, connection_class: str, @@ -215,8 +223,11 @@ class ConfigEntry: # State of the entry (LOADED, NOT_LOADED) self.state = state + # Function to cancel a scheduled retry + self._async_cancel_retry_setup = None + async def async_setup( - self, hass: HomeAssistant, *, component=None) -> None: + self, hass: HomeAssistant, *, component=None, tries=0) -> None: """Set up an entry.""" if component is None: component = getattr(hass.components, self.domain) @@ -228,6 +239,22 @@ class ConfigEntry: _LOGGER.error('%s.async_config_entry did not return boolean', component.DOMAIN) result = False + except ConfigEntryNotReady: + self.state = ENTRY_STATE_SETUP_RETRY + wait_time = 2**min(tries, 4) * 5 + tries += 1 + _LOGGER.warning( + 'Config entry for %s not ready yet. Retrying in %d seconds.', + self.domain, wait_time) + + async def setup_again(now): + """Run setup again.""" + self._async_cancel_retry_setup = None + await self.async_setup(hass, component=component, tries=tries) + + self._async_cancel_retry_setup = \ + hass.helpers.event.async_call_later(wait_time, setup_again) + return except Exception: # pylint: disable=broad-except _LOGGER.exception('Error setting up entry %s for %s', self.title, component.DOMAIN) @@ -250,6 +277,15 @@ class ConfigEntry: if component is None: component = getattr(hass.components, self.domain) + if component.DOMAIN == self.domain: + if self._async_cancel_retry_setup is not None: + self._async_cancel_retry_setup() + self.state = ENTRY_STATE_NOT_LOADED + return True + + if self.state != ENTRY_STATE_LOADED: + return True + supports_unload = hasattr(component, 'async_unload_entry') if not supports_unload: @@ -258,16 +294,18 @@ class ConfigEntry: try: result = await component.async_unload_entry(hass, self) - if not isinstance(result, bool): - _LOGGER.error('%s.async_unload_entry did not return boolean', - component.DOMAIN) - result = False + assert isinstance(result, bool) + + # Only adjust state if we unloaded the component + if result and component.DOMAIN == self.domain: + self.state = ENTRY_STATE_NOT_LOADED return result except Exception: # pylint: disable=broad-except _LOGGER.exception('Error unloading entry %s for %s', self.title, component.DOMAIN) - self.state = ENTRY_STATE_FAILED_UNLOAD + if component.DOMAIN == self.domain: + self.state = ENTRY_STATE_FAILED_UNLOAD return False def as_dict(self): @@ -379,6 +417,12 @@ class ConfigEntries: CONN_CLASS_UNKNOWN)) for entry in config['entries']] + @callback + def async_update_entry(self, entry, *, data): + """Update a config entry.""" + entry.data = data + self._async_schedule_save() + async def async_forward_entry_setup(self, entry, component): """Forward the setup of an entry to a different component. diff --git a/homeassistant/const.py b/homeassistant/const.py index 59d19d2b29a..d023591c828 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,8 +1,8 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 79 -PATCH_VERSION = '3' +MINOR_VERSION = 80 +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) @@ -172,6 +172,7 @@ DEVICE_CLASS_BATTERY = 'battery' DEVICE_CLASS_HUMIDITY = 'humidity' DEVICE_CLASS_ILLUMINANCE = 'illuminance' DEVICE_CLASS_TEMPERATURE = 'temperature' +DEVICE_CLASS_PRESSURE = 'pressure' # #### STATES #### STATE_ON = 'on' @@ -410,7 +411,9 @@ HTTP_UNAUTHORIZED = 401 HTTP_NOT_FOUND = 404 HTTP_METHOD_NOT_ALLOWED = 405 HTTP_UNPROCESSABLE_ENTITY = 422 +HTTP_TOO_MANY_REQUESTS = 429 HTTP_INTERNAL_SERVER_ERROR = 500 +HTTP_SERVICE_UNAVAILABLE = 503 HTTP_BASIC_AUTHENTICATION = 'basic' HTTP_DIGEST_AUTHENTICATION = 'digest' diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index ecf9850a67c..57265cf696d 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -153,7 +153,10 @@ class FlowHandler: } @callback - def async_create_entry(self, *, title: str, data: Dict) -> Dict: + def async_create_entry(self, *, title: str, data: Dict, + description: Optional[str] = None, + description_placeholders: Optional[Dict] = None) \ + -> Dict: """Finish config flow and create a config entry.""" return { 'version': self.VERSION, @@ -162,6 +165,8 @@ class FlowHandler: 'handler': self.handler, 'title': title, 'data': data, + 'description': description, + 'description_placeholders': description_placeholders, } @callback diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 73bd2377950..11aa1848529 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -35,6 +35,12 @@ class PlatformNotReady(HomeAssistantError): pass +class ConfigEntryNotReady(HomeAssistantError): + """Error to indicate that config entry is not ready.""" + + pass + + class InvalidStateError(HomeAssistantError): """When an invalid state is encountered.""" diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 8d4cd0a5bbf..478b29c75b2 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -26,11 +26,12 @@ CONNECTION_ZIGBEE = 'zigbee' class DeviceEntry: """Device Registry Entry.""" - config_entries = attr.ib(type=set, converter=set) - connections = attr.ib(type=set, converter=set) - identifiers = attr.ib(type=set, converter=set) - manufacturer = attr.ib(type=str) - model = attr.ib(type=str) + config_entries = attr.ib(type=set, converter=set, + default=attr.Factory(set)) + connections = attr.ib(type=set, converter=set, default=attr.Factory(set)) + identifiers = attr.ib(type=set, converter=set, default=attr.Factory(set)) + manufacturer = attr.ib(type=str, default=None) + model = attr.ib(type=str, default=None) name = attr.ib(type=str, default=None) sw_version = attr.ib(type=str, default=None) hub_device_id = attr.ib(type=str, default=None) @@ -56,46 +57,53 @@ class DeviceRegistry: return None @callback - def async_get_or_create(self, *, config_entry_id, connections, identifiers, - manufacturer, model, name=None, sw_version=None, + def async_get_or_create(self, *, config_entry_id, connections=None, + identifiers=None, manufacturer=_UNDEF, + model=_UNDEF, name=_UNDEF, sw_version=_UNDEF, via_hub=None): """Get device. Create if it doesn't exist.""" if not identifiers and not connections: return None + if identifiers is None: + identifiers = set() + + if connections is None: + connections = set() + device = self.async_get_device(identifiers, connections) + if device is None: + device = DeviceEntry() + self.devices[device.id] = device + if via_hub is not None: hub_device = self.async_get_device({via_hub}, set()) - hub_device_id = hub_device.id if hub_device else None + hub_device_id = hub_device.id if hub_device else _UNDEF else: - hub_device_id = None + hub_device_id = _UNDEF - if device is not None: - return self._async_update_device( - device.id, config_entry_id=config_entry_id, - hub_device_id=hub_device_id - ) - - device = DeviceEntry( - config_entries={config_entry_id}, - connections=connections, - identifiers=identifiers, + return self._async_update_device( + device.id, + add_config_entry_id=config_entry_id, + hub_device_id=hub_device_id, + merge_connections=connections, + merge_identifiers=identifiers, manufacturer=manufacturer, model=model, name=name, sw_version=sw_version, - hub_device_id=hub_device_id ) - self.devices[device.id] = device - - self.async_schedule_save() - - return device @callback - def _async_update_device(self, device_id, *, config_entry_id=_UNDEF, + def _async_update_device(self, device_id, *, add_config_entry_id=_UNDEF, remove_config_entry_id=_UNDEF, + merge_connections=_UNDEF, + merge_identifiers=_UNDEF, + manufacturer=_UNDEF, + model=_UNDEF, + name=_UNDEF, + sw_version=_UNDEF, hub_device_id=_UNDEF): """Update device attributes.""" old = self.devices[device_id] @@ -104,21 +112,34 @@ class DeviceRegistry: config_entries = old.config_entries - if (config_entry_id is not _UNDEF and - config_entry_id not in old.config_entries): - config_entries = old.config_entries | {config_entry_id} + if (add_config_entry_id is not _UNDEF and + add_config_entry_id not in old.config_entries): + config_entries = old.config_entries | {add_config_entry_id} if (remove_config_entry_id is not _UNDEF and remove_config_entry_id in config_entries): - config_entries = set(config_entries) - config_entries.remove(remove_config_entry_id) + config_entries = config_entries - {remove_config_entry_id} if config_entries is not old.config_entries: changes['config_entries'] = config_entries - if (hub_device_id is not _UNDEF and - hub_device_id != old.hub_device_id): - changes['hub_device_id'] = hub_device_id + for attr_name, value in ( + ('connections', merge_connections), + ('identifiers', merge_identifiers), + ): + old_value = getattr(old, attr_name) + if value is not _UNDEF and value != old_value: + changes[attr_name] = old_value | value + + for attr_name, value in ( + ('manufacturer', manufacturer), + ('model', model), + ('name', name), + ('sw_version', sw_version), + ('hub_device_id', hub_device_id), + ): + if value is not _UNDEF and value != getattr(old, attr_name): + changes[attr_name] = value if not changes: return old diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index e48af6a3365..60fd661a765 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -1,5 +1,4 @@ """An abstract class for entities.""" -import asyncio from datetime import timedelta import logging import functools as ft @@ -202,8 +201,7 @@ class Entity: self._context = context self._context_set = dt_util.utcnow() - @asyncio.coroutine - def async_update_ha_state(self, force_refresh=False): + async def async_update_ha_state(self, force_refresh=False): """Update Home Assistant with current state of entity. If force_refresh == True will update entity before setting state. @@ -220,7 +218,7 @@ class Entity: # update entity data if force_refresh: try: - yield from self.async_device_update() + await self.async_device_update() except Exception: # pylint: disable=broad-except _LOGGER.exception("Update for %s fails", self.entity_id) return @@ -323,8 +321,7 @@ class Entity: """Schedule an update ha state change task.""" self.hass.async_add_job(self.async_update_ha_state(force_refresh)) - @asyncio.coroutine - def async_device_update(self, warning=True): + async def async_device_update(self, warning=True): """Process 'update' or 'async_update' from entity. This method is a coroutine. @@ -335,7 +332,7 @@ class Entity: # Process update sequential if self.parallel_updates: - yield from self.parallel_updates.acquire() + await self.parallel_updates.acquire() if warning: update_warn = self.hass.loop.call_later( @@ -347,9 +344,9 @@ class Entity: try: # pylint: disable=no-member if hasattr(self, 'async_update'): - yield from self.async_update() + await self.async_update() elif hasattr(self, 'update'): - yield from self.hass.async_add_job(self.update) + await self.hass.async_add_job(self.update) finally: self._update_staged = False if warning: diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 09f8838b160..c2ab8722c97 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -190,10 +190,13 @@ class EntityComponent: sorted(self.entities, key=lambda entity: entity.name or entity.entity_id)] - self.hass.components.group.async_set_group( - slugify(self.group_name), name=self.group_name, - visible=False, entity_ids=ids - ) + self.hass.async_create_task( + self.hass.services.async_call( + 'group', 'set', dict( + object_id=slugify(self.group_name), + name=self.group_name, + visible=False, + entities=ids))) async def _async_reset(self): """Remove entities and reset the entity component to initial values. @@ -212,7 +215,9 @@ class EntityComponent: self.config = None if self.group_name is not None: - self.hass.components.group.async_remove(slugify(self.group_name)) + await self.hass.services.async_call( + 'group', 'remove', dict( + object_id=slugify(self.group_name))) async def async_remove_entity(self, entity_id): """Remove an entity managed by one of the platforms.""" diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index f2913e37339..3ab45577236 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -273,21 +273,28 @@ class EntityPlatform: config_entry_id = None device_info = entity.device_info + device_id = None if config_entry_id is not None and device_info is not None: + processed_dev_info = { + 'config_entry_id': config_entry_id + } + for key in ( + 'connections', + 'identifiers', + 'manufacturer', + 'model', + 'name', + 'sw_version', + 'via_hub', + ): + if key in device_info: + processed_dev_info[key] = device_info[key] + device = device_registry.async_get_or_create( - config_entry_id=config_entry_id, - connections=device_info.get('connections') or set(), - identifiers=device_info.get('identifiers') or set(), - manufacturer=device_info.get('manufacturer'), - model=device_info.get('model'), - name=device_info.get('name'), - sw_version=device_info.get('sw_version'), - via_hub=device_info.get('via_hub')) + **processed_dev_info) if device: device_id = device.id - else: - device_id = None entry = entity_registry.async_get_or_create( self.domain, self.platform_name, entity.unique_id, diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index c8488fa3334..05555e8b5c6 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -227,6 +227,10 @@ def async_call_later(hass, delay, action): hass, action, dt_util.utcnow() + timedelta(seconds=delay)) +call_later = threaded_listener_factory( + async_call_later) + + @callback @bind_hass def async_track_time_interval(hass, action, interval): diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 96f9b2d5069..5e660ba7b7f 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1,6 +1,7 @@ """Helpers to execute scripts.""" import logging +from contextlib import suppress from itertools import islice from typing import Optional, Sequence @@ -95,7 +96,9 @@ class Script(): def async_script_delay(now): """Handle delay.""" # pylint: disable=cell-var-from-loop - self._async_listener.remove(unsub) + with suppress(ValueError): + self._async_listener.remove(unsub) + self.hass.async_create_task( self.async_run(variables, context)) @@ -240,7 +243,8 @@ class Script(): @callback def async_script_timeout(now): """Call after timeout is retrieve.""" - self._async_listener.remove(unsub) + with suppress(ValueError): + self._async_listener.remove(unsub) # Check if we want to continue to execute # the script after the timeout diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 95e6925b2a4..cfe73d6d147 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -2,7 +2,7 @@ import asyncio import logging import os -from typing import Dict, Optional, Callable +from typing import Dict, Optional, Callable, Any from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback @@ -46,11 +46,12 @@ async def async_migrator(hass, old_path, store, *, class Store: """Class to help storing data.""" - def __init__(self, hass, version: int, key: str): + def __init__(self, hass, version: int, key: str, private: bool = False): """Initialize storage class.""" self.version = version self.key = key self.hass = hass + self._private = private self._data = None self._unsub_delay_listener = None self._unsub_stop_listener = None @@ -62,7 +63,7 @@ class Store: """Return the config path.""" return self.hass.config.path(STORAGE_DIR, self.key) - async def async_load(self): + async def async_load(self) -> Optional[Dict[str, Any]]: """Load data. If the expected version does not match the given version, the migrate @@ -186,7 +187,7 @@ class Store: os.makedirs(os.path.dirname(path)) _LOGGER.debug('Writing data for %s', self.key) - json.save_json(path, data) + json.save_json(path, data, self._private) async def _async_migrate_func(self, old_version, old_data): """Migrate to the new version.""" diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index d0d3fb457b1..c68aa311998 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -580,6 +580,16 @@ def regex_findall_index(value, find='', index=0, ignorecase=False): return re.findall(find, value, flags)[index] +def bitwise_and(first_value, second_value): + """Perform a bitwise and operation.""" + return first_value & second_value + + +def bitwise_or(first_value, second_value): + """Perform a bitwise or operation.""" + return first_value | second_value + + @contextfilter def random_every_time(context, values): """Choose a random value. @@ -617,6 +627,8 @@ ENV.filters['regex_match'] = regex_match ENV.filters['regex_replace'] = regex_replace ENV.filters['regex_search'] = regex_search ENV.filters['regex_findall_index'] = regex_findall_index +ENV.filters['bitwise_and'] = bitwise_and +ENV.filters['bitwise_or'] = bitwise_or ENV.globals['log'] = logarithm ENV.globals['sin'] = sine ENV.globals['cos'] = cosine diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index 98de59f2da1..f0df58a51f4 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -186,6 +186,6 @@ def _logbook_filtering(hass, last_changed, last_updated): # pylint: disable=protected-access events = logbook._exclude_events(events, {}) - list(logbook.humanify(events)) + list(logbook.humanify(None, events)) return timer() - start diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 8ecfebd5b33..0a2a2a1edf3 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -3,6 +3,8 @@ import logging from typing import Union, List, Dict import json +import os +from os import O_WRONLY, O_CREAT, O_TRUNC from homeassistant.exceptions import HomeAssistantError @@ -38,15 +40,20 @@ def load_json(filename: str, default: Union[List, Dict, None] = None) \ return {} if default is None else default -def save_json(filename: str, data: Union[List, Dict]) -> None: +def save_json(filename: str, data: Union[List, Dict], + private: bool = False) -> None: """Save JSON data to a file. Returns True on success. """ + tmp_filename = filename + "__TEMP__" try: json_data = json.dumps(data, sort_keys=True, indent=4) - with open(filename, 'w', encoding='utf-8') as fdesc: + mode = 0o600 if private else 0o644 + with open(os.open(tmp_filename, O_WRONLY | O_CREAT | O_TRUNC, mode), + 'w', encoding='utf-8') as fdesc: fdesc.write(json_data) + os.replace(tmp_filename, filename) except TypeError as error: _LOGGER.exception('Failed to serialize to JSON: %s', filename) @@ -55,3 +62,11 @@ def save_json(filename: str, data: Union[List, Dict]) -> None: _LOGGER.exception('Saving JSON file failed: %s', filename) raise WriteError(error) + finally: + if os.path.exists(tmp_filename): + try: + os.remove(tmp_filename) + except OSError as err: + # If we are cleaning up then something else went wrong, so + # we should suppress likely follow-on errors in the cleanup + _LOGGER.error("JSON replacement cleanup failed: %s", err) diff --git a/homeassistant/util/network.py b/homeassistant/util/network.py new file mode 100644 index 00000000000..48840f339c1 --- /dev/null +++ b/homeassistant/util/network.py @@ -0,0 +1,22 @@ +"""Network utilities.""" +from ipaddress import IPv4Address, IPv6Address, ip_address, ip_network +from typing import Union + +# IP addresses of loopback interfaces +LOCAL_IPS = ( + ip_address('127.0.0.1'), + ip_address('::1'), +) + +# RFC1918 - Address allocation for Private Internets +LOCAL_NETWORKS = ( + ip_network('10.0.0.0/8'), + ip_network('172.16.0.0/12'), + ip_network('192.168.0.0/16'), +) + + +def is_local(address: Union[IPv4Address, IPv6Address]) -> bool: + """Check if an address is local.""" + return address in LOCAL_IPS or \ + any(address in network for network in LOCAL_NETWORKS) diff --git a/requirements_all.txt b/requirements_all.txt index cdcab14d81c..c91b4127880 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -58,7 +58,7 @@ PyRMVtransport==0.1 PySwitchbot==0.3 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.10.0 +PyXiaomiGateway==0.11.0 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.1 @@ -76,7 +76,7 @@ TwitterAPI==2.5.4 WazeRouteCalculator==0.6 # homeassistant.components.notify.yessssms -YesssSMS==0.1.1b3 +YesssSMS==0.2.3 # homeassistant.components.abode abodepy==0.13.1 @@ -110,7 +110,7 @@ aioimaplib==0.7.13 aiolifx==0.6.3 # homeassistant.components.light.lifx -aiolifx_effects==0.1.2 +aiolifx_effects==0.2.1 # homeassistant.components.scene.hunterdouglas_powerview aiopvapi==1.5.4 @@ -139,9 +139,13 @@ apcaccess==0.0.13 # homeassistant.components.notify.apns apns2==0.3.0 +# homeassistant.components.aqualogic +aqualogic==1.0 + # homeassistant.components.asterisk_mbox asterisk_mbox==0.5.0 +# homeassistant.components.upnp # homeassistant.components.media_player.dlna_dmr async-upnp-client==0.12.4 @@ -172,10 +176,10 @@ beautifulsoup4==4.6.3 bellows==0.7.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.5.2 +bimmer_connected==0.5.3 # homeassistant.components.blink -blinkpy==0.6.0 +blinkpy==0.9.0 # homeassistant.components.light.blinksticklight blinkstick==1.1.8 @@ -324,7 +328,7 @@ enocean==0.40 # envirophat==0.0.6 # homeassistant.components.sensor.enphase_envoy -envoy_reader==0.2 +envoy_reader==0.3 # homeassistant.components.sensor.season ephem==3.7.6.0 @@ -338,8 +342,9 @@ eternalegypt==0.0.5 # homeassistant.components.keyboard_remote # evdev==0.6.1 +# homeassistant.components.evohome # homeassistant.components.climate.honeywell -evohomeclient==0.2.5 +evohomeclient==0.2.7 # homeassistant.components.image_processing.dlib_face_detect # homeassistant.components.image_processing.dlib_face_identify @@ -352,7 +357,6 @@ fastdotcom==0.0.3 fedexdeliverymanager==1.0.6 # homeassistant.components.feedreader -# homeassistant.components.sensor.geo_rss_events feedparser==5.2.1 # homeassistant.components.sensor.fints @@ -393,6 +397,9 @@ geizhals==0.0.7 # homeassistant.components.geo_location.geo_json_events geojson_client==0.1 +# homeassistant.components.sensor.geo_rss_events +georss_client==0.3 + # homeassistant.components.sensor.gitter gitterpy==0.1.7 @@ -429,9 +436,6 @@ habitipy==0.2.0 # homeassistant.components.hangouts hangups==0.4.5 -# homeassistant.components.sensor.geo_rss_events -haversine==0.4.5 - # homeassistant.components.mqtt.server hbmqtt==0.9.4 @@ -454,7 +458,7 @@ hole==0.3.0 holidays==0.9.7 # homeassistant.components.frontend -home-assistant-frontend==20180927.0 +home-assistant-frontend==20181012.0 # homeassistant.components.homekit_controller # homekit==0.10 @@ -467,7 +471,7 @@ homematicip==0.9.8 httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.0.12 +huawei-lte-api==1.0.16 # homeassistant.components.hydrawise hydrawiser==0.1.1 @@ -557,7 +561,7 @@ liveboxplaytv==2.0.2 lmnotify==0.0.4 # homeassistant.components.device_tracker.google_maps -locationsharinglib==2.0.11 +locationsharinglib==3.0.3 # homeassistant.components.logi_circle logi_circle==0.1.7 @@ -763,8 +767,8 @@ pyRFXtrx==0.23 # homeassistant.components.switch.switchmate pySwitchmate==0.4.1 -# homeassistant.components.sensor.tibber -pyTibber==0.5.1 +# homeassistant.components.tibber +pyTibber==0.7.2 # homeassistant.components.switch.dlink pyW215==0.6.0 @@ -794,7 +798,7 @@ pyasn1-modules==0.1.5 pyasn1==0.3.7 # homeassistant.components.netatmo -pyatmo==1.1.1 +pyatmo==1.2 # homeassistant.components.apple_tv pyatv==0.3.10 @@ -865,7 +869,7 @@ pyeight==0.0.9 pyemby==1.5 # homeassistant.components.envisalink -pyenvisalink==2.3 +pyenvisalink==3.7 # homeassistant.components.climate.ephember pyephember==0.2.0 @@ -883,7 +887,7 @@ pyflic-homeassistant==0.4.dev0 pyfnip==0.2 # homeassistant.components.fritzbox -pyfritzhome==0.3.7 +pyfritzhome==0.4.0 # homeassistant.components.ifttt pyfttt==0.3 @@ -908,7 +912,7 @@ pyhik==0.1.8 pyhiveapi==0.2.14 # homeassistant.components.homematic -pyhomematic==0.1.49 +pyhomematic==0.1.50 # homeassistant.components.sensor.hydroquebec pyhydroquebec==2.2.2 @@ -996,7 +1000,7 @@ pymysensors==0.17.0 pynello==1.5.1 # homeassistant.components.device_tracker.netgear -pynetgear==0.4.1 +pynetgear==0.4.2 # homeassistant.components.switch.netio pynetio==0.1.6 @@ -1014,6 +1018,9 @@ pynx584==0.4 # homeassistant.components.openuv pyopenuv==1.0.4 +# homeassistant.components.light.opple +pyoppleio==1.0.5 + # homeassistant.components.iota pyota==2.0.5 @@ -1068,7 +1075,7 @@ pysma==0.2 pysnmp==4.4.5 # homeassistant.components.sonos -pysonos==0.0.2 +pysonos==0.0.3 # homeassistant.components.spc pyspcwebgw==0.4.0 @@ -1114,6 +1121,9 @@ python-forecastio==1.4.0 # homeassistant.components.gc100 python-gc100==1.0.3a +# homeassistant.components.sensor.gitlab_ci +python-gitlab==1.6.0 + # homeassistant.components.sensor.hp_ilo python-hpilo==3.9 @@ -1207,7 +1217,7 @@ pytouchline==0.7 pytrackr==0.0.5 # homeassistant.components.tradfri -pytradfri[async]==5.5.1 +pytradfri[async]==6.0.1 # homeassistant.components.sensor.trafikverket_weatherstation pytrafikverket==0.1.5.8 @@ -1215,9 +1225,6 @@ pytrafikverket==0.1.5.8 # homeassistant.components.device_tracker.unifi pyunifi==2.13 -# homeassistant.components.upnp -pyupnp-async==0.1.1.1 - # homeassistant.components.binary_sensor.uptimerobot pyuptimerobot==0.0.5 @@ -1328,7 +1335,7 @@ shodan==1.10.2 simplepush==1.1.4 # homeassistant.components.alarm_control_panel.simplisafe -simplisafe-python==2.0.2 +simplisafe-python==3.1.2 # homeassistant.components.sisyphus sisyphus-control==2.1 @@ -1546,7 +1553,7 @@ yeelight==0.4.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.09.18 +youtube_dl==2018.09.26 # homeassistant.components.light.zengge zengge==0.2 @@ -1567,4 +1574,4 @@ zigpy-xbee==0.1.1 zigpy==0.2.0 # homeassistant.components.zoneminder -zm-py==0.0.4 +zm-py==0.0.5 diff --git a/requirements_test.txt b/requirements_test.txt index 15e06c4e53d..9f36d8f42ca 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,5 +13,5 @@ pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 pytest-timeout==1.3.2 -pytest==3.8.0 +pytest==3.8.2 requests_mock==1.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e67da9755cd..871714cc47d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -14,7 +14,7 @@ pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 pytest-timeout==1.3.2 -pytest==3.8.0 +pytest==3.8.2 requests_mock==1.5.2 @@ -24,6 +24,9 @@ HAP-python==2.2.2 # homeassistant.components.sensor.rmvtransport PyRMVtransport==0.1 +# homeassistant.components.notify.yessssms +YesssSMS==0.2.3 + # homeassistant.components.device_tracker.automatic aioautomatic==0.6.5 @@ -52,11 +55,11 @@ dsmr_parser==0.11 # homeassistant.components.sensor.season ephem==3.7.6.0 +# homeassistant.components.evohome # homeassistant.components.climate.honeywell -evohomeclient==0.2.5 +evohomeclient==0.2.7 # homeassistant.components.feedreader -# homeassistant.components.sensor.geo_rss_events feedparser==5.2.1 # homeassistant.components.sensor.foobot @@ -68,15 +71,15 @@ gTTS-token==1.1.2 # homeassistant.components.geo_location.geo_json_events geojson_client==0.1 +# homeassistant.components.sensor.geo_rss_events +georss_client==0.3 + # homeassistant.components.ffmpeg ha-ffmpeg==1.9 # homeassistant.components.hangouts hangups==0.4.5 -# homeassistant.components.sensor.geo_rss_events -haversine==0.4.5 - # homeassistant.components.mqtt.server hbmqtt==0.9.4 @@ -87,7 +90,7 @@ hdate==0.6.3 holidays==0.9.7 # homeassistant.components.frontend -home-assistant-frontend==20180927.0 +home-assistant-frontend==20181012.0 # homeassistant.components.homematicip_cloud homematicip==0.9.8 @@ -169,7 +172,7 @@ pyotp==2.2.6 pyqwikswitch==0.8 # homeassistant.components.sonos -pysonos==0.0.2 +pysonos==0.0.3 # homeassistant.components.spc pyspcwebgw==0.4.0 @@ -185,14 +188,11 @@ python-nest==4.0.3 pythonwhois==2.4.3 # homeassistant.components.tradfri -pytradfri[async]==5.5.1 +pytradfri[async]==6.0.1 # homeassistant.components.device_tracker.unifi pyunifi==2.13 -# homeassistant.components.upnp -pyupnp-async==0.1.1.1 - # homeassistant.components.notify.html5 pywebpush==1.6.0 diff --git a/script/check_dirty b/script/check_dirty new file mode 100755 index 00000000000..94db657a542 --- /dev/null +++ b/script/check_dirty @@ -0,0 +1,7 @@ +#!/bin/bash +[[ -z $(git ls-files --others --exclude-standard) ]] && exit 0 + +echo -e '\n***** ERROR\nTests are leaving files behind. Please update the tests to avoid writing any files:' +git ls-files --others --exclude-standard +echo +exit 1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 7493e523273..9c0323bf5ca 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -51,6 +51,7 @@ TEST_REQUIREMENTS = ( 'foobot_async', 'gTTS-token', 'geojson_client', + 'georss_client', 'hangups', 'HAP-python', 'ha-ffmpeg', @@ -103,7 +104,8 @@ TEST_REQUIREMENTS = ( 'yahoo-finance', 'pythonwhois', 'wakeonlan', - 'vultr' + 'vultr', + 'YesssSMS', ) IGNORE_PACKAGES = ( diff --git a/tests/common.py b/tests/common.py index 0cb15d683b5..ee181cfa2e9 100644 --- a/tests/common.py +++ b/tests/common.py @@ -392,7 +392,7 @@ def ensure_auth_manager_loaded(auth_mgr): """Ensure an auth manager is considered loaded.""" store = auth_mgr._store if store._users is None: - store._users = OrderedDict() + store._set_defaults() class MockModule: diff --git a/tests/components/alarm_control_panel/common.py b/tests/components/alarm_control_panel/common.py new file mode 100644 index 00000000000..cf2de857076 --- /dev/null +++ b/tests/components/alarm_control_panel/common.py @@ -0,0 +1,83 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.alarm_control_panel import DOMAIN +from homeassistant.const import ( + ATTR_CODE, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER, + SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS) +from homeassistant.loader import bind_hass + + +@bind_hass +def alarm_disarm(hass, code=None, entity_id=None): + """Send the alarm the command for disarm.""" + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_ALARM_DISARM, data) + + +@bind_hass +def alarm_arm_home(hass, code=None, entity_id=None): + """Send the alarm the command for arm home.""" + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_ALARM_ARM_HOME, data) + + +@bind_hass +def alarm_arm_away(hass, code=None, entity_id=None): + """Send the alarm the command for arm away.""" + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data) + + +@bind_hass +def alarm_arm_night(hass, code=None, entity_id=None): + """Send the alarm the command for arm night.""" + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_ALARM_ARM_NIGHT, data) + + +@bind_hass +def alarm_trigger(hass, code=None, entity_id=None): + """Send the alarm the command for trigger.""" + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data) + + +@bind_hass +def alarm_arm_custom_bypass(hass, code=None, entity_id=None): + """Send the alarm the command for arm custom bypass.""" + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_ALARM_ARM_CUSTOM_BYPASS, data) diff --git a/tests/components/alarm_control_panel/test_manual.py b/tests/components/alarm_control_panel/test_manual.py index 29f630093d9..02085a44b47 100644 --- a/tests/components/alarm_control_panel/test_manual.py +++ b/tests/components/alarm_control_panel/test_manual.py @@ -14,6 +14,7 @@ from homeassistant.components import alarm_control_panel import homeassistant.util.dt as dt_util from tests.common import fire_time_changed, get_test_home_assistant +from tests.components.alarm_control_panel import common CODE = 'HELLO_CODE' @@ -53,7 +54,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_home(self.hass, CODE) + common.alarm_arm_home(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_HOME, @@ -76,7 +77,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_home(self.hass, CODE, entity_id) + common.alarm_arm_home(self.hass, CODE, entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -111,7 +112,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_home(self.hass, CODE + '2') + common.alarm_arm_home(self.hass, CODE + '2') self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -134,7 +135,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE, entity_id) + common.alarm_arm_away(self.hass, CODE, entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, @@ -160,7 +161,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_home(self.hass, 'abc') + common.alarm_arm_home(self.hass, 'abc') self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -183,7 +184,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -218,7 +219,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE + '2') + common.alarm_arm_away(self.hass, CODE + '2') self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -241,7 +242,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_night(self.hass, CODE) + common.alarm_arm_night(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_NIGHT, @@ -264,7 +265,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_night(self.hass, CODE, entity_id) + common.alarm_arm_night(self.hass, CODE, entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -284,7 +285,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): assert state.state == STATE_ALARM_ARMED_NIGHT # Do not go to the pending state when updating to the same state - alarm_control_panel.alarm_arm_night(self.hass, CODE, entity_id) + common.alarm_arm_night(self.hass, CODE, entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_NIGHT, @@ -307,7 +308,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_night(self.hass, CODE + '2') + common.alarm_arm_night(self.hass, CODE + '2') self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -329,7 +330,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -362,13 +363,13 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -402,7 +403,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass) + common.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -425,7 +426,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass) + common.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -448,7 +449,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass) + common.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -496,13 +497,13 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -540,13 +541,13 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -584,13 +585,13 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -640,13 +641,13 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -687,7 +688,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): entity_id = 'alarm_control_panel.test' - alarm_control_panel.alarm_arm_home(self.hass) + common.alarm_arm_home(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -717,7 +718,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): entity_id = 'alarm_control_panel.test' - alarm_control_panel.alarm_arm_away(self.hass) + common.alarm_arm_away(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -747,7 +748,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): entity_id = 'alarm_control_panel.test' - alarm_control_panel.alarm_arm_night(self.hass) + common.alarm_arm_night(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -779,7 +780,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): entity_id = 'alarm_control_panel.test' - alarm_control_panel.alarm_trigger(self.hass) + common.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -820,7 +821,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_TRIGGERED, @@ -855,7 +856,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -881,7 +882,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_TRIGGERED, @@ -915,7 +916,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_TRIGGERED, @@ -947,13 +948,13 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE, entity_id) + common.alarm_arm_away(self.hass, CODE, entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_TRIGGERED, @@ -985,13 +986,13 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE, entity_id) + common.alarm_arm_away(self.hass, CODE, entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_TRIGGERED, @@ -1006,7 +1007,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_TRIGGERED, @@ -1037,13 +1038,13 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass) + common.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_disarm(self.hass, entity_id=entity_id) + common.alarm_disarm(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -1075,13 +1076,13 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass) + common.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_disarm(self.hass, entity_id=entity_id) + common.alarm_disarm(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -1117,19 +1118,19 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_home(self.hass, 'def') + common.alarm_arm_home(self.hass, 'def') self.hass.block_till_done() state = self.hass.states.get(entity_id) self.assertEqual(STATE_ALARM_ARMED_HOME, state.state) - alarm_control_panel.alarm_disarm(self.hass, 'def') + common.alarm_disarm(self.hass, 'def') self.hass.block_till_done() state = self.hass.states.get(entity_id) self.assertEqual(STATE_ALARM_ARMED_HOME, state.state) - alarm_control_panel.alarm_disarm(self.hass, 'abc') + common.alarm_disarm(self.hass, 'abc') self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -1152,7 +1153,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_custom_bypass(self.hass, CODE) + common.alarm_arm_custom_bypass(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_CUSTOM_BYPASS, @@ -1175,7 +1176,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_custom_bypass(self.hass, CODE, entity_id) + common.alarm_arm_custom_bypass(self.hass, CODE, entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -1211,7 +1212,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_custom_bypass(self.hass, CODE + '2') + common.alarm_arm_custom_bypass(self.hass, CODE + '2') self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -1232,7 +1233,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): entity_id = 'alarm_control_panel.test' - alarm_control_panel.alarm_arm_custom_bypass(self.hass) + common.alarm_arm_custom_bypass(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -1271,7 +1272,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -1281,7 +1282,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_ARMED_AWAY, state.attributes['post_pending_state']) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -1300,7 +1301,7 @@ class TestAlarmControlPanelManual(unittest.TestCase): state = self.hass.states.get(entity_id) self.assertEqual(STATE_ALARM_ARMED_AWAY, state.state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) diff --git a/tests/components/alarm_control_panel/test_manual_mqtt.py b/tests/components/alarm_control_panel/test_manual_mqtt.py index 5b601f089dd..4e2ec6a9489 100644 --- a/tests/components/alarm_control_panel/test_manual_mqtt.py +++ b/tests/components/alarm_control_panel/test_manual_mqtt.py @@ -1,7 +1,7 @@ """The tests for the manual_mqtt Alarm Control Panel component.""" from datetime import timedelta import unittest -from unittest.mock import patch +from unittest.mock import patch, Mock from homeassistant.setup import setup_component from homeassistant.const import ( @@ -13,6 +13,7 @@ import homeassistant.util.dt as dt_util from tests.common import ( fire_time_changed, get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, assert_setup_component) +from tests.components.alarm_control_panel import common CODE = 'HELLO_CODE' @@ -23,6 +24,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() + self.hass.config_entries._async_schedule_save = Mock() self.mock_publish = mock_mqtt_component(self.hass) def tearDown(self): # pylint: disable=invalid-name @@ -69,7 +71,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_home(self.hass, CODE) + common.alarm_arm_home(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_HOME, @@ -94,7 +96,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_home(self.hass, CODE, entity_id) + common.alarm_arm_home(self.hass, CODE, entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -131,7 +133,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_home(self.hass, CODE + '2') + common.alarm_arm_home(self.hass, CODE + '2') self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -156,7 +158,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE, entity_id) + common.alarm_arm_away(self.hass, CODE, entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, @@ -184,7 +186,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_home(self.hass, 'abc') + common.alarm_arm_home(self.hass, 'abc') self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -209,7 +211,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -246,7 +248,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE + '2') + common.alarm_arm_away(self.hass, CODE + '2') self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -271,7 +273,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_night(self.hass, CODE, entity_id) + common.alarm_arm_night(self.hass, CODE, entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_NIGHT, @@ -296,7 +298,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_night(self.hass, CODE) + common.alarm_arm_night(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -316,7 +318,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.hass.states.get(entity_id).state) # Do not go to the pending state when updating to the same state - alarm_control_panel.alarm_arm_night(self.hass, CODE, entity_id) + common.alarm_arm_night(self.hass, CODE, entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_NIGHT, @@ -341,7 +343,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_night(self.hass, CODE + '2') + common.alarm_arm_night(self.hass, CODE + '2') self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -365,7 +367,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -400,13 +402,13 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -442,7 +444,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass) + common.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -467,7 +469,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass) + common.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -492,7 +494,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass) + common.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -538,7 +540,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_TRIGGERED, @@ -575,7 +577,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -603,7 +605,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_TRIGGERED, @@ -639,7 +641,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_TRIGGERED, @@ -673,13 +675,13 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE, entity_id) + common.alarm_arm_away(self.hass, CODE, entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_TRIGGERED, @@ -694,7 +696,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_TRIGGERED, @@ -727,13 +729,13 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass) + common.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_disarm(self.hass, entity_id=entity_id) + common.alarm_disarm(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_DISARMED, @@ -767,13 +769,13 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass) + common.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_disarm(self.hass, entity_id=entity_id) + common.alarm_disarm(self.hass, entity_id=entity_id) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -811,13 +813,13 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -857,13 +859,13 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -903,13 +905,13 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -961,13 +963,13 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() self.assertEqual(STATE_ALARM_ARMED_AWAY, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -1010,7 +1012,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): entity_id = 'alarm_control_panel.test' - alarm_control_panel.alarm_arm_home(self.hass) + common.alarm_arm_home(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -1042,7 +1044,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): entity_id = 'alarm_control_panel.test' - alarm_control_panel.alarm_arm_away(self.hass) + common.alarm_arm_away(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -1074,7 +1076,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): entity_id = 'alarm_control_panel.test' - alarm_control_panel.alarm_arm_night(self.hass) + common.alarm_arm_night(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -1108,7 +1110,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): entity_id = 'alarm_control_panel.test' - alarm_control_panel.alarm_trigger(self.hass) + common.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -1158,7 +1160,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_away(self.hass, CODE) + common.alarm_arm_away(self.hass, CODE) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -1168,7 +1170,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_ARMED_AWAY, state.attributes['post_pending_state']) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -1187,7 +1189,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): state = self.hass.states.get(entity_id) self.assertEqual(STATE_ALARM_ARMED_AWAY, state.state) - alarm_control_panel.alarm_trigger(self.hass, entity_id=entity_id) + common.alarm_trigger(self.hass, entity_id=entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -1229,19 +1231,19 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_arm_home(self.hass, 'def') + common.alarm_arm_home(self.hass, 'def') self.hass.block_till_done() state = self.hass.states.get(entity_id) self.assertEqual(STATE_ALARM_ARMED_HOME, state.state) - alarm_control_panel.alarm_disarm(self.hass, 'def') + common.alarm_disarm(self.hass, 'def') self.hass.block_till_done() state = self.hass.states.get(entity_id) self.assertEqual(STATE_ALARM_ARMED_HOME, state.state) - alarm_control_panel.alarm_disarm(self.hass, 'abc') + common.alarm_disarm(self.hass, 'abc') self.hass.block_till_done() state = self.hass.states.get(entity_id) @@ -1367,7 +1369,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) - alarm_control_panel.alarm_trigger(self.hass) + common.alarm_trigger(self.hass) self.hass.block_till_done() self.assertEqual(STATE_ALARM_PENDING, @@ -1400,7 +1402,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.mock_publish.async_publish.reset_mock() # Arm in home mode - alarm_control_panel.alarm_arm_home(self.hass) + common.alarm_arm_home(self.hass) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'alarm/state', STATE_ALARM_PENDING, 0, True) @@ -1416,7 +1418,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.mock_publish.async_publish.reset_mock() # Arm in away mode - alarm_control_panel.alarm_arm_away(self.hass) + common.alarm_arm_away(self.hass) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'alarm/state', STATE_ALARM_PENDING, 0, True) @@ -1432,7 +1434,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.mock_publish.async_publish.reset_mock() # Arm in night mode - alarm_control_panel.alarm_arm_night(self.hass) + common.alarm_arm_night(self.hass) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'alarm/state', STATE_ALARM_PENDING, 0, True) @@ -1448,7 +1450,7 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): self.mock_publish.async_publish.reset_mock() # Disarm - alarm_control_panel.alarm_disarm(self.hass) + common.alarm_disarm(self.hass) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'alarm/state', STATE_ALARM_DISARMED, 0, True) diff --git a/tests/components/alarm_control_panel/test_mqtt.py b/tests/components/alarm_control_panel/test_mqtt.py index ce152a3d7c9..dd606bb53ec 100644 --- a/tests/components/alarm_control_panel/test_mqtt.py +++ b/tests/components/alarm_control_panel/test_mqtt.py @@ -6,11 +6,13 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNAVAILABLE, STATE_UNKNOWN) -from homeassistant.components import alarm_control_panel +from homeassistant.components import alarm_control_panel, mqtt +from homeassistant.components.mqtt.discovery import async_start from tests.common import ( - mock_mqtt_component, fire_mqtt_message, get_test_home_assistant, - assert_setup_component) + mock_mqtt_component, async_fire_mqtt_message, fire_mqtt_message, + get_test_home_assistant, assert_setup_component, MockConfigEntry) +from tests.components.alarm_control_panel import common CODE = 'HELLO_CODE' @@ -104,7 +106,7 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): } }) - alarm_control_panel.alarm_arm_home(self.hass) + common.alarm_arm_home(self.hass) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'alarm/command', 'ARM_HOME', 0, False) @@ -122,7 +124,7 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): }) call_count = self.mock_publish.call_count - alarm_control_panel.alarm_arm_home(self.hass, 'abcd') + common.alarm_arm_home(self.hass, 'abcd') self.hass.block_till_done() self.assertEqual(call_count, self.mock_publish.call_count) @@ -137,7 +139,7 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): } }) - alarm_control_panel.alarm_arm_away(self.hass) + common.alarm_arm_away(self.hass) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'alarm/command', 'ARM_AWAY', 0, False) @@ -155,7 +157,7 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): }) call_count = self.mock_publish.call_count - alarm_control_panel.alarm_arm_away(self.hass, 'abcd') + common.alarm_arm_away(self.hass, 'abcd') self.hass.block_till_done() self.assertEqual(call_count, self.mock_publish.call_count) @@ -170,7 +172,7 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): } }) - alarm_control_panel.alarm_disarm(self.hass) + common.alarm_disarm(self.hass) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'alarm/command', 'DISARM', 0, False) @@ -188,7 +190,7 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): }) call_count = self.mock_publish.call_count - alarm_control_panel.alarm_disarm(self.hass, 'abcd') + common.alarm_disarm(self.hass, 'abcd') self.hass.block_till_done() self.assertEqual(call_count, self.mock_publish.call_count) @@ -239,3 +241,33 @@ class TestAlarmControlPanelMQTT(unittest.TestCase): self.assertEqual(STATE_UNAVAILABLE, state.state) fire_mqtt_message(self.hass, 'availability-topic', 'good') + + +async def test_discovery_removal_alarm(hass, mqtt_mock, caplog): + """Test removal of discovered alarm_control_panel.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + + data = ( + '{ "name": "Beer",' + ' "status_topic": "test_topic",' + ' "command_topic": "test_topic" }' + ) + + async_fire_mqtt_message(hass, + 'homeassistant/alarm_control_panel/bla/config', + data) + await hass.async_block_till_done() + + state = hass.states.get('alarm_control_panel.beer') + assert state is not None + assert state.name == 'Beer' + + async_fire_mqtt_message(hass, + 'homeassistant/alarm_control_panel/bla/config', + '') + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('alarm_control_panel.beer') + assert state is None diff --git a/tests/components/alarm_control_panel/test_spc.py b/tests/components/alarm_control_panel/test_spc.py deleted file mode 100644 index b1078e1b14f..00000000000 --- a/tests/components/alarm_control_panel/test_spc.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Tests for Vanderbilt SPC alarm control panel platform.""" -from homeassistant.components.alarm_control_panel import spc -from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED) -from homeassistant.components.spc import (DATA_API) - - -async def test_setup_platform(hass): - """Test adding areas as separate alarm control panel devices.""" - added_entities = [] - - def add_entities(entities): - nonlocal added_entities - added_entities = list(entities) - - area_defs = [{ - 'id': '1', - 'name': 'House', - 'mode': '3', - 'last_set_time': '1485759851', - 'last_set_user_id': '1', - 'last_set_user_name': 'Pelle', - 'last_unset_time': '1485800564', - 'last_unset_user_id': '1', - 'last_unset_user_name': 'Lisa', - 'last_alarm': '1478174896' - }, { - 'id': '3', - 'name': 'Garage', - 'mode': '0', - 'last_set_time': '1483705803', - 'last_set_user_id': '9998', - 'last_set_user_name': 'Pelle', - 'last_unset_time': '1483705808', - 'last_unset_user_id': '9998', - 'last_unset_user_name': 'Lisa' - }] - - from pyspcwebgw import Area - - areas = [Area(gateway=None, spc_area=a) for a in area_defs] - - hass.data[DATA_API] = None - - await spc.async_setup_platform(hass=hass, - config={}, - async_add_entities=add_entities, - discovery_info={'areas': areas}) - - assert len(added_entities) == 2 - - assert added_entities[0].name == 'House' - assert added_entities[0].state == STATE_ALARM_ARMED_AWAY - assert added_entities[0].changed_by == 'Pelle' - - assert added_entities[1].name == 'Garage' - assert added_entities[1].state == STATE_ALARM_DISARMED - assert added_entities[1].changed_by == 'Lisa' diff --git a/tests/components/automation/common.py b/tests/components/automation/common.py new file mode 100644 index 00000000000..4c8f91849aa --- /dev/null +++ b/tests/components/automation/common.py @@ -0,0 +1,53 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.automation import DOMAIN, SERVICE_TRIGGER +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, + SERVICE_RELOAD) +from homeassistant.loader import bind_hass + + +@bind_hass +def turn_on(hass, entity_id=None): + """Turn on specified automation or all.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_TURN_ON, data) + + +@bind_hass +def turn_off(hass, entity_id=None): + """Turn off specified automation or all.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) + + +@bind_hass +def toggle(hass, entity_id=None): + """Toggle specified automation or all.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_TOGGLE, data) + + +@bind_hass +def trigger(hass, entity_id=None): + """Trigger specified automation or all.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_TRIGGER, data) + + +@bind_hass +def reload(hass): + """Reload the automation from config.""" + hass.services.call(DOMAIN, SERVICE_RELOAD) + + +@bind_hass +def async_reload(hass): + """Reload the automation from config. + + Returns a coroutine object. + """ + return hass.services.async_call(DOMAIN, SERVICE_RELOAD) diff --git a/tests/components/automation/test_event.py b/tests/components/automation/test_event.py index 6e16c03f2dc..09d237013b0 100644 --- a/tests/components/automation/test_event.py +++ b/tests/components/automation/test_event.py @@ -6,6 +6,7 @@ from homeassistant.setup import setup_component import homeassistant.components.automation as automation from tests.common import get_test_home_assistant, mock_component +from tests.components.automation import common # pylint: disable=invalid-name @@ -50,7 +51,7 @@ class TestAutomationEvent(unittest.TestCase): self.assertEqual(1, len(self.calls)) assert self.calls[0].context is context - automation.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() self.hass.bus.fire('test_event') @@ -75,7 +76,7 @@ class TestAutomationEvent(unittest.TestCase): self.hass.block_till_done() self.assertEqual(1, len(self.calls)) - automation.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() self.hass.bus.fire('test_event') diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index c3bd6c224af..3bcbc7da04f 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -15,6 +15,7 @@ import homeassistant.util.dt as dt_util from tests.common import ( assert_setup_component, get_test_home_assistant, fire_time_changed, mock_service, async_mock_service, mock_restore_cache) +from tests.components.automation import common # pylint: disable=invalid-name @@ -363,7 +364,7 @@ class TestAutomation(unittest.TestCase): self.hass.block_till_done() assert len(self.calls) == 1 - automation.turn_off(self.hass, entity_id) + common.turn_off(self.hass, entity_id) self.hass.block_till_done() assert not automation.is_on(self.hass, entity_id) @@ -371,7 +372,7 @@ class TestAutomation(unittest.TestCase): self.hass.block_till_done() assert len(self.calls) == 1 - automation.toggle(self.hass, entity_id) + common.toggle(self.hass, entity_id) self.hass.block_till_done() assert automation.is_on(self.hass, entity_id) @@ -379,17 +380,17 @@ class TestAutomation(unittest.TestCase): self.hass.block_till_done() assert len(self.calls) == 2 - automation.trigger(self.hass, entity_id) + common.trigger(self.hass, entity_id) self.hass.block_till_done() assert len(self.calls) == 3 - automation.turn_off(self.hass, entity_id) + common.turn_off(self.hass, entity_id) self.hass.block_till_done() - automation.trigger(self.hass, entity_id) + common.trigger(self.hass, entity_id) self.hass.block_till_done() assert len(self.calls) == 4 - automation.turn_on(self.hass, entity_id) + common.turn_on(self.hass, entity_id) self.hass.block_till_done() assert automation.is_on(self.hass, entity_id) @@ -439,7 +440,7 @@ class TestAutomation(unittest.TestCase): }}): with patch('homeassistant.config.find_config_file', return_value=''): - automation.reload(self.hass) + common.reload(self.hass) self.hass.block_till_done() # De-flake ?! self.hass.block_till_done() @@ -489,7 +490,7 @@ class TestAutomation(unittest.TestCase): return_value={automation.DOMAIN: 'not valid'}): with patch('homeassistant.config.find_config_file', return_value=''): - automation.reload(self.hass) + common.reload(self.hass) self.hass.block_till_done() assert self.hass.states.get('automation.hello') is None @@ -527,7 +528,7 @@ class TestAutomation(unittest.TestCase): side_effect=HomeAssistantError('bla')): with patch('homeassistant.config.find_config_file', return_value=''): - automation.reload(self.hass) + common.reload(self.hass) self.hass.block_till_done() assert self.hass.states.get('automation.hello') is not None diff --git a/tests/components/automation/test_litejet.py b/tests/components/automation/test_litejet.py index ca6f7796cfc..3d88174708b 100644 --- a/tests/components/automation/test_litejet.py +++ b/tests/components/automation/test_litejet.py @@ -7,9 +7,10 @@ from datetime import timedelta from homeassistant import setup import homeassistant.util.dt as dt_util from homeassistant.components import litejet -from tests.common import (fire_time_changed, get_test_home_assistant) import homeassistant.components.automation as automation +from tests.common import (fire_time_changed, get_test_home_assistant) + _LOGGER = logging.getLogger(__name__) ENTITY_SWITCH = 'switch.mock_switch_1' diff --git a/tests/components/automation/test_mqtt.py b/tests/components/automation/test_mqtt.py index 8ec5351af94..29a53467c4f 100644 --- a/tests/components/automation/test_mqtt.py +++ b/tests/components/automation/test_mqtt.py @@ -7,6 +7,7 @@ import homeassistant.components.automation as automation from tests.common import ( mock_mqtt_component, fire_mqtt_message, get_test_home_assistant, mock_component) +from tests.components.automation import common # pylint: disable=invalid-name @@ -56,7 +57,7 @@ class TestAutomationMQTT(unittest.TestCase): self.assertEqual('mqtt - test-topic - { "hello": "world" } - world', self.calls[0].data['some']) - automation.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() fire_mqtt_message(self.hass, 'test-topic', 'test_payload') self.hass.block_till_done() diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py index af95bc0ff02..183d1f4a5f9 100644 --- a/tests/components/automation/test_numeric_state.py +++ b/tests/components/automation/test_numeric_state.py @@ -11,6 +11,7 @@ import homeassistant.util.dt as dt_util from tests.common import ( get_test_home_assistant, mock_component, fire_time_changed, assert_setup_component) +from tests.components.automation import common # pylint: disable=invalid-name @@ -57,7 +58,7 @@ class TestAutomationNumericState(unittest.TestCase): # Set above 12 so the automation will fire again self.hass.states.set('test.entity', 12) - automation.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() self.hass.states.set('test.entity', 9) self.hass.block_till_done() @@ -775,7 +776,7 @@ class TestAutomationNumericState(unittest.TestCase): self.hass.states.set('test.entity_1', 9) self.hass.states.set('test.entity_2', 9) self.hass.block_till_done() - automation.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() fire_time_changed(self.hass, dt_util.utcnow() + timedelta(seconds=10)) diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index 274980fabc0..15c6353b234 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -12,6 +12,7 @@ import homeassistant.components.automation as automation from tests.common import ( fire_time_changed, get_test_home_assistant, assert_setup_component, mock_component) +from tests.components.automation import common # pylint: disable=invalid-name @@ -68,7 +69,7 @@ class TestAutomationState(unittest.TestCase): 'state - test.entity - hello - world - None', self.calls[0].data['some']) - automation.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() self.hass.states.set('test.entity', 'planet') self.hass.block_till_done() @@ -370,7 +371,7 @@ class TestAutomationState(unittest.TestCase): self.hass.states.set('test.entity_1', 'world') self.hass.states.set('test.entity_2', 'world') self.hass.block_till_done() - automation.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() fire_time_changed(self.hass, dt_util.utcnow() + timedelta(seconds=10)) diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index 4556b7cbe45..ad8709fdf36 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -12,6 +12,7 @@ import homeassistant.util.dt as dt_util from tests.common import ( fire_time_changed, get_test_home_assistant, mock_component) +from tests.components.automation import common # pylint: disable=invalid-name @@ -57,7 +58,7 @@ class TestAutomationSun(unittest.TestCase): } }) - automation.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() fire_time_changed(self.hass, trigger_time) @@ -66,7 +67,7 @@ class TestAutomationSun(unittest.TestCase): with patch('homeassistant.util.dt.utcnow', return_value=now): - automation.turn_on(self.hass) + common.turn_on(self.hass) self.hass.block_till_done() fire_time_changed(self.hass, trigger_time) diff --git a/tests/components/automation/test_template.py b/tests/components/automation/test_template.py index e9c763ccc73..4fec0e707a9 100644 --- a/tests/components/automation/test_template.py +++ b/tests/components/automation/test_template.py @@ -7,6 +7,7 @@ import homeassistant.components.automation as automation from tests.common import ( get_test_home_assistant, assert_setup_component, mock_component) +from tests.components.automation import common # pylint: disable=invalid-name @@ -49,7 +50,7 @@ class TestAutomationTemplate(unittest.TestCase): self.hass.block_till_done() self.assertEqual(1, len(self.calls)) - automation.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() self.hass.states.set('test.entity', 'planet') diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index 5f928cf92a0..dcb723d725e 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -11,6 +11,7 @@ import homeassistant.components.automation as automation from tests.common import ( fire_time_changed, get_test_home_assistant, assert_setup_component, mock_component) +from tests.components.automation import common # pylint: disable=invalid-name @@ -52,7 +53,7 @@ class TestAutomationTime(unittest.TestCase): self.hass.block_till_done() self.assertEqual(1, len(self.calls)) - automation.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() fire_time_changed(self.hass, dt_util.utcnow().replace(hour=0)) diff --git a/tests/components/automation/test_webhook.py b/tests/components/automation/test_webhook.py new file mode 100644 index 00000000000..a6cde395583 --- /dev/null +++ b/tests/components/automation/test_webhook.py @@ -0,0 +1,75 @@ +"""The tests for the webhook automation trigger.""" +from homeassistant.core import callback +from homeassistant.setup import async_setup_component + + +async def test_webhook_json(hass, aiohttp_client): + """Test triggering with a JSON webhook.""" + events = [] + + @callback + def store_event(event): + """Helepr to store events.""" + events.append(event) + + hass.bus.async_listen('test_success', store_event) + + assert await async_setup_component(hass, 'automation', { + 'automation': { + 'trigger': { + 'platform': 'webhook', + 'webhook_id': 'json_webhook' + }, + 'action': { + 'event': 'test_success', + 'event_data_template': { + 'hello': 'yo {{ trigger.json.hello }}', + } + } + } + }) + + client = await aiohttp_client(hass.http.app) + + await client.post('/api/webhook/json_webhook', json={ + 'hello': 'world' + }) + + assert len(events) == 1 + assert events[0].data['hello'] == 'yo world' + + +async def test_webhook_post(hass, aiohttp_client): + """Test triggering with a POST webhook.""" + events = [] + + @callback + def store_event(event): + """Helepr to store events.""" + events.append(event) + + hass.bus.async_listen('test_success', store_event) + + assert await async_setup_component(hass, 'automation', { + 'automation': { + 'trigger': { + 'platform': 'webhook', + 'webhook_id': 'post_webhook' + }, + 'action': { + 'event': 'test_success', + 'event_data_template': { + 'hello': 'yo {{ trigger.data.hello }}', + } + } + } + }) + + client = await aiohttp_client(hass.http.app) + + await client.post('/api/webhook/post_webhook', data={ + 'hello': 'world' + }) + + assert len(events) == 1 + assert events[0].data['hello'] == 'yo world' diff --git a/tests/components/automation/test_zone.py b/tests/components/automation/test_zone.py index d146278a997..795f55a3e0b 100644 --- a/tests/components/automation/test_zone.py +++ b/tests/components/automation/test_zone.py @@ -6,6 +6,7 @@ from homeassistant.setup import setup_component from homeassistant.components import automation, zone from tests.common import get_test_home_assistant, mock_component +from tests.components.automation import common # pylint: disable=invalid-name @@ -87,7 +88,7 @@ class TestAutomationZone(unittest.TestCase): }) self.hass.block_till_done() - automation.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() self.hass.states.set('test.entity', 'hello', { diff --git a/tests/components/binary_sensor/test_mqtt.py b/tests/components/binary_sensor/test_mqtt.py index 57050c2cbf5..84619ce4ee6 100644 --- a/tests/components/binary_sensor/test_mqtt.py +++ b/tests/components/binary_sensor/test_mqtt.py @@ -2,14 +2,17 @@ import unittest import homeassistant.core as ha -from homeassistant.setup import setup_component -import homeassistant.components.binary_sensor as binary_sensor +from homeassistant.setup import setup_component, async_setup_component +from homeassistant.components import binary_sensor, mqtt +from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE -from tests.common import get_test_home_assistant, fire_mqtt_message -from tests.common import mock_component, mock_mqtt_component +from tests.common import ( + get_test_home_assistant, fire_mqtt_message, async_fire_mqtt_message, + mock_component, mock_mqtt_component, async_mock_mqtt_component, + MockConfigEntry) class TestSensorMQTT(unittest.TestCase): @@ -77,25 +80,6 @@ class TestSensorMQTT(unittest.TestCase): 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, { @@ -223,3 +207,46 @@ class TestSensorMQTT(unittest.TestCase): fire_mqtt_message(self.hass, 'test-topic', 'ON') self.hass.block_till_done() self.assertEqual(2, len(events)) + + +async def test_unique_id(hass): + """Test unique id option only creates one sensor per unique_id.""" + await async_mock_mqtt_component(hass) + assert await async_setup_component(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' + }] + }) + async_fire_mqtt_message(hass, 'test-topic', 'payload') + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + +async def test_discovery_removal_binary_sensor(hass, mqtt_mock, caplog): + """Test removal of discovered binary_sensor.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + data = ( + '{ "name": "Beer",' + ' "status_topic": "test_topic" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', + data) + await hass.async_block_till_done() + state = hass.states.get('binary_sensor.beer') + assert state is not None + assert state.name == 'Beer' + async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', + '') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('binary_sensor.beer') + assert state is None diff --git a/tests/components/binary_sensor/test_spc.py b/tests/components/binary_sensor/test_spc.py deleted file mode 100644 index ec0886aeed8..00000000000 --- a/tests/components/binary_sensor/test_spc.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Tests for Vanderbilt SPC binary sensor platform.""" -from homeassistant.components.binary_sensor import spc - - -async def test_setup_platform(hass): - """Test autodiscovery of supported device types.""" - added_entities = [] - - zone_defs = [{ - 'id': '1', - 'type': '3', - 'zone_name': 'Kitchen smoke', - 'area': '1', - 'area_name': 'House', - 'input': '0', - 'status': '0', - }, { - 'id': '3', - 'type': '0', - 'zone_name': 'Hallway PIR', - 'area': '1', - 'area_name': 'House', - 'input': '0', - 'status': '0', - }, { - 'id': '5', - 'type': '1', - 'zone_name': 'Front door', - 'area': '1', - 'area_name': 'House', - 'input': '1', - 'status': '0', - }] - - def add_entities(entities): - nonlocal added_entities - added_entities = list(entities) - - from pyspcwebgw import Zone - - zones = [Zone(area=None, spc_zone=z) for z in zone_defs] - - await spc.async_setup_platform(hass=hass, - config={}, - async_add_entities=add_entities, - discovery_info={'devices': zones}) - - assert len(added_entities) == 3 - assert added_entities[0].device_class == 'smoke' - assert added_entities[0].state == 'off' - assert added_entities[1].device_class == 'motion' - assert added_entities[1].state == 'off' - assert added_entities[2].device_class == 'opening' - assert added_entities[2].state == 'on' - assert all(d.hidden for d in added_entities) diff --git a/tests/components/camera/common.py b/tests/components/camera/common.py new file mode 100644 index 00000000000..21f7244bd29 --- /dev/null +++ b/tests/components/camera/common.py @@ -0,0 +1,47 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.camera import ( + ATTR_FILENAME, DOMAIN, SERVICE_ENABLE_MOTION, SERVICE_SNAPSHOT) +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, \ + SERVICE_TURN_ON +from homeassistant.core import callback +from homeassistant.loader import bind_hass + + +@bind_hass +async def async_turn_off(hass, entity_id=None): + """Turn off camera.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data) + + +@bind_hass +async def async_turn_on(hass, entity_id=None): + """Turn on camera, and set operation mode.""" + data = {} + if entity_id is not None: + data[ATTR_ENTITY_ID] = entity_id + + await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data) + + +@bind_hass +def enable_motion_detection(hass, entity_id=None): + """Enable Motion Detection.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_ENABLE_MOTION, data)) + + +@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 {} + data[ATTR_FILENAME] = filename + + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_SNAPSHOT, data)) diff --git a/tests/components/camera/test_demo.py b/tests/components/camera/test_demo.py index 63c70ddc6ca..f6e2513380c 100644 --- a/tests/components/camera/test_demo.py +++ b/tests/components/camera/test_demo.py @@ -8,6 +8,8 @@ from homeassistant.components.camera import STATE_STREAMING, STATE_IDLE from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component +from tests.components.camera import common + @pytest.fixture def demo_camera(hass): @@ -37,12 +39,12 @@ async def test_init_state_is_streaming(hass, demo_camera): async def test_turn_on_state_back_to_streaming(hass, demo_camera): """After turn on state back to streaming.""" assert demo_camera.state == STATE_STREAMING - await camera.async_turn_off(hass, demo_camera.entity_id) + await common.async_turn_off(hass, demo_camera.entity_id) await hass.async_block_till_done() assert demo_camera.state == STATE_IDLE - await camera.async_turn_on(hass, demo_camera.entity_id) + await common.async_turn_on(hass, demo_camera.entity_id) await hass.async_block_till_done() assert demo_camera.state == STATE_STREAMING @@ -50,7 +52,7 @@ async def test_turn_on_state_back_to_streaming(hass, demo_camera): async def test_turn_off_image(hass, demo_camera): """After turn off, Demo camera raise error.""" - await camera.async_turn_off(hass, demo_camera.entity_id) + await common.async_turn_off(hass, demo_camera.entity_id) await hass.async_block_till_done() with pytest.raises(HomeAssistantError) as error: @@ -61,7 +63,7 @@ async def test_turn_off_image(hass, demo_camera): async def test_turn_off_invalid_camera(hass, demo_camera): """Turn off non-exist camera should quietly fail.""" assert demo_camera.state == STATE_STREAMING - await camera.async_turn_off(hass, 'camera.invalid_camera') + await common.async_turn_off(hass, 'camera.invalid_camera') await hass.async_block_till_done() assert demo_camera.state == STATE_STREAMING @@ -81,7 +83,7 @@ async def test_motion_detection(hass): assert not state.attributes.get('motion_detection') # Call service to turn on motion detection - camera.enable_motion_detection(hass, 'camera.demo_camera') + common.enable_motion_detection(hass, 'camera.demo_camera') await hass.async_block_till_done() # Check if state has been updated. diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 053fa6d29dc..6b98f378ef0 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -7,13 +7,15 @@ import pytest from homeassistant.setup import setup_component, async_setup_component from homeassistant.const import ATTR_ENTITY_PICTURE -from homeassistant.components import camera, http, websocket_api +from homeassistant.components import camera, http +from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.exceptions import HomeAssistantError from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import ( get_test_home_assistant, get_test_instance_port, assert_setup_component, mock_coro) +from tests.components.camera import common @pytest.fixture @@ -126,7 +128,7 @@ def test_snapshot_service(hass, mock_camera): with patch('homeassistant.components.camera.open', mopen, create=True), \ patch.object(hass.config, 'is_allowed_path', return_value=True): - hass.components.camera.async_snapshot('/tmp/bla') + common.async_snapshot(hass, '/tmp/bla') yield from hass.async_block_till_done() mock_write = mopen().write @@ -149,7 +151,7 @@ async def test_webocket_camera_thumbnail(hass, hass_ws_client, mock_camera): msg = await client.receive_json() assert msg['id'] == 5 - assert msg['type'] == websocket_api.TYPE_RESULT + assert msg['type'] == TYPE_RESULT assert msg['success'] assert msg['result']['content_type'] == 'image/jpeg' assert msg['result']['content'] == \ diff --git a/tests/components/climate/common.py b/tests/components/climate/common.py new file mode 100644 index 00000000000..4ac6f553091 --- /dev/null +++ b/tests/components/climate/common.py @@ -0,0 +1,115 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.climate import ( + _LOGGER, ATTR_AUX_HEAT, ATTR_AWAY_MODE, ATTR_FAN_MODE, ATTR_HOLD_MODE, + ATTR_HUMIDITY, ATTR_OPERATION_MODE, ATTR_SWING_MODE, ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, DOMAIN, SERVICE_SET_AWAY_MODE, SERVICE_SET_HOLD_MODE, + SERVICE_SET_AUX_HEAT, SERVICE_SET_TEMPERATURE, SERVICE_SET_HUMIDITY, + SERVICE_SET_FAN_MODE, SERVICE_SET_OPERATION_MODE, SERVICE_SET_SWING_MODE) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_TEMPERATURE) +from homeassistant.loader import bind_hass + + +@bind_hass +def set_away_mode(hass, away_mode, entity_id=None): + """Turn all or specified climate devices away mode on.""" + data = { + ATTR_AWAY_MODE: away_mode + } + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SET_AWAY_MODE, data) + + +@bind_hass +def set_hold_mode(hass, hold_mode, entity_id=None): + """Set new hold mode.""" + data = { + ATTR_HOLD_MODE: hold_mode + } + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SET_HOLD_MODE, data) + + +@bind_hass +def set_aux_heat(hass, aux_heat, entity_id=None): + """Turn all or specified climate devices auxiliary heater on.""" + data = { + ATTR_AUX_HEAT: aux_heat + } + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data) + + +@bind_hass +def set_temperature(hass, temperature=None, entity_id=None, + target_temp_high=None, target_temp_low=None, + operation_mode=None): + """Set new target temperature.""" + kwargs = { + key: value for key, value in [ + (ATTR_TEMPERATURE, temperature), + (ATTR_TARGET_TEMP_HIGH, target_temp_high), + (ATTR_TARGET_TEMP_LOW, target_temp_low), + (ATTR_ENTITY_ID, entity_id), + (ATTR_OPERATION_MODE, operation_mode) + ] if value is not None + } + _LOGGER.debug("set_temperature start data=%s", kwargs) + hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, kwargs) + + +@bind_hass +def set_humidity(hass, humidity, entity_id=None): + """Set new target humidity.""" + data = {ATTR_HUMIDITY: humidity} + + if entity_id is not None: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SET_HUMIDITY, data) + + +@bind_hass +def set_fan_mode(hass, fan, entity_id=None): + """Set all or specified climate devices fan mode on.""" + data = {ATTR_FAN_MODE: fan} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SET_FAN_MODE, data) + + +@bind_hass +def set_operation_mode(hass, operation_mode, entity_id=None): + """Set new target operation mode.""" + data = {ATTR_OPERATION_MODE: operation_mode} + + if entity_id is not None: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, data) + + +@bind_hass +def set_swing_mode(hass, swing_mode, entity_id=None): + """Set new target swing mode.""" + data = {ATTR_SWING_MODE: swing_mode} + + if entity_id is not None: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data) diff --git a/tests/components/climate/test_demo.py b/tests/components/climate/test_demo.py index 0cd6d288536..4990a8a6998 100644 --- a/tests/components/climate/test_demo.py +++ b/tests/components/climate/test_demo.py @@ -8,6 +8,7 @@ from homeassistant.setup import setup_component from homeassistant.components import climate from tests.common import get_test_home_assistant +from tests.components.climate import common ENTITY_CLIMATE = 'climate.hvac' @@ -56,7 +57,7 @@ class TestDemoClimate(unittest.TestCase): """Test setting the target temperature without required attribute.""" state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(21, state.attributes.get('temperature')) - climate.set_temperature(self.hass, None, ENTITY_CLIMATE) + common.set_temperature(self.hass, None, ENTITY_CLIMATE) self.hass.block_till_done() self.assertEqual(21, state.attributes.get('temperature')) @@ -64,7 +65,7 @@ class TestDemoClimate(unittest.TestCase): """Test the setting of the target temperature.""" state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(21, state.attributes.get('temperature')) - climate.set_temperature(self.hass, 30, ENTITY_CLIMATE) + common.set_temperature(self.hass, 30, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(30.0, state.attributes.get('temperature')) @@ -73,7 +74,7 @@ class TestDemoClimate(unittest.TestCase): """Test the setting of the target temperature.""" state = self.hass.states.get(ENTITY_HEATPUMP) self.assertEqual(20, state.attributes.get('temperature')) - climate.set_temperature(self.hass, 21, ENTITY_HEATPUMP) + common.set_temperature(self.hass, 21, ENTITY_HEATPUMP) self.hass.block_till_done() state = self.hass.states.get(ENTITY_HEATPUMP) self.assertEqual(21.0, state.attributes.get('temperature')) @@ -84,8 +85,8 @@ class TestDemoClimate(unittest.TestCase): self.assertEqual(None, state.attributes.get('temperature')) self.assertEqual(21.0, state.attributes.get('target_temp_low')) self.assertEqual(24.0, state.attributes.get('target_temp_high')) - climate.set_temperature(self.hass, target_temp_high=25, - target_temp_low=20, entity_id=ENTITY_ECOBEE) + common.set_temperature(self.hass, target_temp_high=25, + target_temp_low=20, entity_id=ENTITY_ECOBEE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_ECOBEE) self.assertEqual(None, state.attributes.get('temperature')) @@ -98,9 +99,9 @@ class TestDemoClimate(unittest.TestCase): self.assertEqual(None, state.attributes.get('temperature')) self.assertEqual(21.0, state.attributes.get('target_temp_low')) self.assertEqual(24.0, state.attributes.get('target_temp_high')) - climate.set_temperature(self.hass, temperature=None, - entity_id=ENTITY_ECOBEE, target_temp_low=None, - target_temp_high=None) + common.set_temperature(self.hass, temperature=None, + entity_id=ENTITY_ECOBEE, target_temp_low=None, + target_temp_high=None) self.hass.block_till_done() state = self.hass.states.get(ENTITY_ECOBEE) self.assertEqual(None, state.attributes.get('temperature')) @@ -111,7 +112,7 @@ class TestDemoClimate(unittest.TestCase): """Test setting the target humidity without required attribute.""" state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(67, state.attributes.get('humidity')) - climate.set_humidity(self.hass, None, ENTITY_CLIMATE) + common.set_humidity(self.hass, None, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(67, state.attributes.get('humidity')) @@ -120,7 +121,7 @@ class TestDemoClimate(unittest.TestCase): """Test the setting of the target humidity.""" state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(67, state.attributes.get('humidity')) - climate.set_humidity(self.hass, 64, ENTITY_CLIMATE) + common.set_humidity(self.hass, 64, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(64.0, state.attributes.get('humidity')) @@ -129,7 +130,7 @@ class TestDemoClimate(unittest.TestCase): """Test setting fan mode without required attribute.""" state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("On High", state.attributes.get('fan_mode')) - climate.set_fan_mode(self.hass, None, ENTITY_CLIMATE) + common.set_fan_mode(self.hass, None, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("On High", state.attributes.get('fan_mode')) @@ -138,7 +139,7 @@ class TestDemoClimate(unittest.TestCase): """Test setting of new fan mode.""" state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("On High", state.attributes.get('fan_mode')) - climate.set_fan_mode(self.hass, "On Low", ENTITY_CLIMATE) + common.set_fan_mode(self.hass, "On Low", ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("On Low", state.attributes.get('fan_mode')) @@ -147,7 +148,7 @@ class TestDemoClimate(unittest.TestCase): """Test setting swing mode without required attribute.""" state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("Off", state.attributes.get('swing_mode')) - climate.set_swing_mode(self.hass, None, ENTITY_CLIMATE) + common.set_swing_mode(self.hass, None, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("Off", state.attributes.get('swing_mode')) @@ -156,7 +157,7 @@ class TestDemoClimate(unittest.TestCase): """Test setting of new swing mode.""" state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("Off", state.attributes.get('swing_mode')) - climate.set_swing_mode(self.hass, "Auto", ENTITY_CLIMATE) + common.set_swing_mode(self.hass, "Auto", ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("Auto", state.attributes.get('swing_mode')) @@ -169,7 +170,7 @@ class TestDemoClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("cool", state.attributes.get('operation_mode')) self.assertEqual("cool", state.state) - climate.set_operation_mode(self.hass, None, ENTITY_CLIMATE) + common.set_operation_mode(self.hass, None, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("cool", state.attributes.get('operation_mode')) @@ -180,7 +181,7 @@ class TestDemoClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("cool", state.attributes.get('operation_mode')) self.assertEqual("cool", state.state) - climate.set_operation_mode(self.hass, "heat", ENTITY_CLIMATE) + common.set_operation_mode(self.hass, "heat", ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("heat", state.attributes.get('operation_mode')) @@ -190,41 +191,41 @@ class TestDemoClimate(unittest.TestCase): """Test setting the away mode without required attribute.""" state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('on', state.attributes.get('away_mode')) - climate.set_away_mode(self.hass, None, ENTITY_CLIMATE) + common.set_away_mode(self.hass, None, ENTITY_CLIMATE) self.hass.block_till_done() self.assertEqual('on', state.attributes.get('away_mode')) def test_set_away_mode_on(self): """Test setting the away mode on/true.""" - climate.set_away_mode(self.hass, True, ENTITY_CLIMATE) + common.set_away_mode(self.hass, True, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('on', state.attributes.get('away_mode')) def test_set_away_mode_off(self): """Test setting the away mode off/false.""" - climate.set_away_mode(self.hass, False, ENTITY_CLIMATE) + common.set_away_mode(self.hass, False, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('off', state.attributes.get('away_mode')) def test_set_hold_mode_home(self): """Test setting the hold mode home.""" - climate.set_hold_mode(self.hass, 'home', ENTITY_ECOBEE) + common.set_hold_mode(self.hass, 'home', ENTITY_ECOBEE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_ECOBEE) self.assertEqual('home', state.attributes.get('hold_mode')) def test_set_hold_mode_away(self): """Test setting the hold mode away.""" - climate.set_hold_mode(self.hass, 'away', ENTITY_ECOBEE) + common.set_hold_mode(self.hass, 'away', ENTITY_ECOBEE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_ECOBEE) self.assertEqual('away', state.attributes.get('hold_mode')) def test_set_hold_mode_none(self): """Test setting the hold mode off/false.""" - climate.set_hold_mode(self.hass, 'off', ENTITY_ECOBEE) + common.set_hold_mode(self.hass, 'off', ENTITY_ECOBEE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_ECOBEE) self.assertEqual('off', state.attributes.get('hold_mode')) @@ -233,20 +234,20 @@ class TestDemoClimate(unittest.TestCase): """Test setting the auxiliary heater without required attribute.""" state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('off', state.attributes.get('aux_heat')) - climate.set_aux_heat(self.hass, None, ENTITY_CLIMATE) + common.set_aux_heat(self.hass, None, ENTITY_CLIMATE) self.hass.block_till_done() self.assertEqual('off', state.attributes.get('aux_heat')) def test_set_aux_heat_on(self): """Test setting the axillary heater on/true.""" - climate.set_aux_heat(self.hass, True, ENTITY_CLIMATE) + common.set_aux_heat(self.hass, True, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('on', state.attributes.get('aux_heat')) def test_set_aux_heat_off(self): """Test setting the auxiliary heater off/false.""" - climate.set_aux_heat(self.hass, False, ENTITY_CLIMATE) + common.set_aux_heat(self.hass, False, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('off', state.attributes.get('aux_heat')) diff --git a/tests/components/climate/test_generic_thermostat.py b/tests/components/climate/test_generic_thermostat.py index c4e07705230..47ec621aeb5 100644 --- a/tests/components/climate/test_generic_thermostat.py +++ b/tests/components/climate/test_generic_thermostat.py @@ -25,6 +25,7 @@ from homeassistant.components.climate import STATE_HEAT, STATE_COOL import homeassistant.components as comps from tests.common import (assert_setup_component, get_test_home_assistant, mock_restore_cache) +from tests.components.climate import common ENTITY = 'climate.test' @@ -108,7 +109,7 @@ class TestGenericThermostatHeaterSwitching(unittest.TestCase): self._setup_sensor(18) self.hass.block_till_done() - climate.set_temperature(self.hass, 23) + common.set_temperature(self.hass, 23) self.hass.block_till_done() self.assertEqual(STATE_ON, @@ -135,7 +136,7 @@ class TestGenericThermostatHeaterSwitching(unittest.TestCase): self._setup_sensor(18) self.hass.block_till_done() - climate.set_temperature(self.hass, 23) + common.set_temperature(self.hass, 23) self.hass.block_till_done() self.assertEqual(STATE_ON, @@ -186,20 +187,20 @@ class TestClimateGenericThermostat(unittest.TestCase): def test_set_target_temp(self): """Test the setting of the target temperature.""" - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() state = self.hass.states.get(ENTITY) self.assertEqual(30.0, state.attributes.get('temperature')) - climate.set_temperature(self.hass, None) + common.set_temperature(self.hass, None) self.hass.block_till_done() state = self.hass.states.get(ENTITY) self.assertEqual(30.0, state.attributes.get('temperature')) def test_set_away_mode(self): """Test the setting away mode.""" - climate.set_temperature(self.hass, 23) + common.set_temperature(self.hass, 23) self.hass.block_till_done() - climate.set_away_mode(self.hass, True) + common.set_away_mode(self.hass, True) self.hass.block_till_done() state = self.hass.states.get(ENTITY) self.assertEqual(16, state.attributes.get('temperature')) @@ -209,13 +210,13 @@ class TestClimateGenericThermostat(unittest.TestCase): Verify original temperature is restored. """ - climate.set_temperature(self.hass, 23) + common.set_temperature(self.hass, 23) self.hass.block_till_done() - climate.set_away_mode(self.hass, True) + common.set_away_mode(self.hass, True) self.hass.block_till_done() state = self.hass.states.get(ENTITY) self.assertEqual(16, state.attributes.get('temperature')) - climate.set_away_mode(self.hass, False) + common.set_away_mode(self.hass, False) self.hass.block_till_done() state = self.hass.states.get(ENTITY) self.assertEqual(23, state.attributes.get('temperature')) @@ -236,7 +237,7 @@ class TestClimateGenericThermostat(unittest.TestCase): self._setup_switch(False) self._setup_sensor(25) self.hass.block_till_done() - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() self.assertEqual(1, len(self.calls)) call = self.calls[0] @@ -249,7 +250,7 @@ class TestClimateGenericThermostat(unittest.TestCase): self._setup_switch(True) self._setup_sensor(30) self.hass.block_till_done() - climate.set_temperature(self.hass, 25) + common.set_temperature(self.hass, 25) self.hass.block_till_done() self.assertEqual(2, len(self.calls)) call = self.calls[0] @@ -260,7 +261,7 @@ class TestClimateGenericThermostat(unittest.TestCase): def test_temp_change_heater_on_within_tolerance(self): """Test if temperature change doesn't turn on within tolerance.""" self._setup_switch(False) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() self._setup_sensor(29) self.hass.block_till_done() @@ -269,7 +270,7 @@ class TestClimateGenericThermostat(unittest.TestCase): def test_temp_change_heater_on_outside_tolerance(self): """Test if temperature change turn heater on outside cold tolerance.""" self._setup_switch(False) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() self._setup_sensor(27) self.hass.block_till_done() @@ -282,7 +283,7 @@ class TestClimateGenericThermostat(unittest.TestCase): def test_temp_change_heater_off_within_tolerance(self): """Test if temperature change doesn't turn off within tolerance.""" self._setup_switch(True) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() self._setup_sensor(33) self.hass.block_till_done() @@ -291,7 +292,7 @@ class TestClimateGenericThermostat(unittest.TestCase): def test_temp_change_heater_off_outside_tolerance(self): """Test if temperature change turn heater off outside hot tolerance.""" self._setup_switch(True) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() self._setup_sensor(35) self.hass.block_till_done() @@ -304,9 +305,9 @@ class TestClimateGenericThermostat(unittest.TestCase): def test_running_when_operating_mode_is_off(self): """Test that the switch turns off when enabled is set False.""" self._setup_switch(True) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() - climate.set_operation_mode(self.hass, STATE_OFF) + common.set_operation_mode(self.hass, STATE_OFF) self.hass.block_till_done() self.assertEqual(1, len(self.calls)) call = self.calls[0] @@ -317,9 +318,9 @@ class TestClimateGenericThermostat(unittest.TestCase): def test_no_state_change_when_operation_mode_off(self): """Test that the switch doesn't turn on when enabled is False.""" self._setup_switch(False) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() - climate.set_operation_mode(self.hass, STATE_OFF) + common.set_operation_mode(self.hass, STATE_OFF) self.hass.block_till_done() self._setup_sensor(25) self.hass.block_till_done() @@ -328,7 +329,7 @@ class TestClimateGenericThermostat(unittest.TestCase): @mock.patch('logging.Logger.error') def test_invalid_operating_mode(self, log_mock): """Test error handling for invalid operation mode.""" - climate.set_operation_mode(self.hass, 'invalid mode') + common.set_operation_mode(self.hass, 'invalid mode') self.hass.block_till_done() self.assertEqual(log_mock.call_count, 1) @@ -337,12 +338,12 @@ class TestClimateGenericThermostat(unittest.TestCase): Switch turns on when temp below setpoint and mode changes. """ - climate.set_operation_mode(self.hass, STATE_OFF) - climate.set_temperature(self.hass, 30) + common.set_operation_mode(self.hass, STATE_OFF) + common.set_temperature(self.hass, 30) self._setup_sensor(25) self.hass.block_till_done() self._setup_switch(False) - climate.set_operation_mode(self.hass, climate.STATE_HEAT) + common.set_operation_mode(self.hass, climate.STATE_HEAT) self.hass.block_till_done() self.assertEqual(1, len(self.calls)) call = self.calls[0] @@ -395,7 +396,7 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): self._setup_switch(True) self._setup_sensor(25) self.hass.block_till_done() - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() self.assertEqual(2, len(self.calls)) call = self.calls[0] @@ -407,9 +408,9 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): """Test the setting away mode when cooling.""" self._setup_sensor(25) self.hass.block_till_done() - climate.set_temperature(self.hass, 19) + common.set_temperature(self.hass, 19) self.hass.block_till_done() - climate.set_away_mode(self.hass, True) + common.set_away_mode(self.hass, True) self.hass.block_till_done() state = self.hass.states.get(ENTITY) self.assertEqual(30, state.attributes.get('temperature')) @@ -419,12 +420,12 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): Switch turns on when temp below setpoint and mode changes. """ - climate.set_operation_mode(self.hass, STATE_OFF) - climate.set_temperature(self.hass, 25) + common.set_operation_mode(self.hass, STATE_OFF) + common.set_temperature(self.hass, 25) self._setup_sensor(30) self.hass.block_till_done() self._setup_switch(False) - climate.set_operation_mode(self.hass, climate.STATE_COOL) + common.set_operation_mode(self.hass, climate.STATE_COOL) self.hass.block_till_done() self.assertEqual(1, len(self.calls)) call = self.calls[0] @@ -437,7 +438,7 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): self._setup_switch(False) self._setup_sensor(30) self.hass.block_till_done() - climate.set_temperature(self.hass, 25) + common.set_temperature(self.hass, 25) self.hass.block_till_done() self.assertEqual(1, len(self.calls)) call = self.calls[0] @@ -448,7 +449,7 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): def test_temp_change_ac_off_within_tolerance(self): """Test if temperature change doesn't turn ac off within tolerance.""" self._setup_switch(True) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() self._setup_sensor(29.8) self.hass.block_till_done() @@ -457,7 +458,7 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): def test_set_temp_change_ac_off_outside_tolerance(self): """Test if temperature change turn ac off.""" self._setup_switch(True) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() self._setup_sensor(27) self.hass.block_till_done() @@ -470,7 +471,7 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): def test_temp_change_ac_on_within_tolerance(self): """Test if temperature change doesn't turn ac on within tolerance.""" self._setup_switch(False) - climate.set_temperature(self.hass, 25) + common.set_temperature(self.hass, 25) self.hass.block_till_done() self._setup_sensor(25.2) self.hass.block_till_done() @@ -479,7 +480,7 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): def test_temp_change_ac_on_outside_tolerance(self): """Test if temperature change turn ac on.""" self._setup_switch(False) - climate.set_temperature(self.hass, 25) + common.set_temperature(self.hass, 25) self.hass.block_till_done() self._setup_sensor(30) self.hass.block_till_done() @@ -492,9 +493,9 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): def test_running_when_operating_mode_is_off(self): """Test that the switch turns off when enabled is set False.""" self._setup_switch(True) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() - climate.set_operation_mode(self.hass, STATE_OFF) + common.set_operation_mode(self.hass, STATE_OFF) self.hass.block_till_done() self.assertEqual(1, len(self.calls)) call = self.calls[0] @@ -505,9 +506,9 @@ class TestClimateGenericThermostatACMode(unittest.TestCase): def test_no_state_change_when_operation_mode_off(self): """Test that the switch doesn't turn on when enabled is False.""" self._setup_switch(False) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() - climate.set_operation_mode(self.hass, STATE_OFF) + common.set_operation_mode(self.hass, STATE_OFF) self.hass.block_till_done() self._setup_sensor(35) self.hass.block_till_done() @@ -556,7 +557,7 @@ class TestClimateGenericThermostatACModeMinCycle(unittest.TestCase): def test_temp_change_ac_trigger_on_not_long_enough(self): """Test if temperature change turn ac on.""" self._setup_switch(False) - climate.set_temperature(self.hass, 25) + common.set_temperature(self.hass, 25) self.hass.block_till_done() self._setup_sensor(30) self.hass.block_till_done() @@ -569,7 +570,7 @@ class TestClimateGenericThermostatACModeMinCycle(unittest.TestCase): with mock.patch('homeassistant.helpers.condition.dt_util.utcnow', return_value=fake_changed): self._setup_switch(False) - climate.set_temperature(self.hass, 25) + common.set_temperature(self.hass, 25) self.hass.block_till_done() self._setup_sensor(30) self.hass.block_till_done() @@ -582,7 +583,7 @@ class TestClimateGenericThermostatACModeMinCycle(unittest.TestCase): def test_temp_change_ac_trigger_off_not_long_enough(self): """Test if temperature change turn ac on.""" self._setup_switch(True) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() self._setup_sensor(25) self.hass.block_till_done() @@ -595,7 +596,7 @@ class TestClimateGenericThermostatACModeMinCycle(unittest.TestCase): with mock.patch('homeassistant.helpers.condition.dt_util.utcnow', return_value=fake_changed): self._setup_switch(True) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() self._setup_sensor(25) self.hass.block_till_done() @@ -647,7 +648,7 @@ class TestClimateGenericThermostatMinCycle(unittest.TestCase): def test_temp_change_heater_trigger_off_not_long_enough(self): """Test if temp change doesn't turn heater off because of time.""" self._setup_switch(True) - climate.set_temperature(self.hass, 25) + common.set_temperature(self.hass, 25) self.hass.block_till_done() self._setup_sensor(30) self.hass.block_till_done() @@ -656,7 +657,7 @@ class TestClimateGenericThermostatMinCycle(unittest.TestCase): def test_temp_change_heater_trigger_on_not_long_enough(self): """Test if temp change doesn't turn heater on because of time.""" self._setup_switch(False) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() self._setup_sensor(25) self.hass.block_till_done() @@ -669,7 +670,7 @@ class TestClimateGenericThermostatMinCycle(unittest.TestCase): with mock.patch('homeassistant.helpers.condition.dt_util.utcnow', return_value=fake_changed): self._setup_switch(False) - climate.set_temperature(self.hass, 30) + common.set_temperature(self.hass, 30) self.hass.block_till_done() self._setup_sensor(25) self.hass.block_till_done() @@ -686,7 +687,7 @@ class TestClimateGenericThermostatMinCycle(unittest.TestCase): with mock.patch('homeassistant.helpers.condition.dt_util.utcnow', return_value=fake_changed): self._setup_switch(True) - climate.set_temperature(self.hass, 25) + common.set_temperature(self.hass, 25) self.hass.block_till_done() self._setup_sensor(30) self.hass.block_till_done() @@ -743,7 +744,7 @@ class TestClimateGenericThermostatACKeepAlive(unittest.TestCase): self.hass.block_till_done() self._setup_sensor(30) self.hass.block_till_done() - climate.set_temperature(self.hass, 25) + common.set_temperature(self.hass, 25) self.hass.block_till_done() test_time = datetime.datetime.now(pytz.UTC) self._send_time_changed(test_time) @@ -766,7 +767,7 @@ class TestClimateGenericThermostatACKeepAlive(unittest.TestCase): self.hass.block_till_done() self._setup_sensor(20) self.hass.block_till_done() - climate.set_temperature(self.hass, 25) + common.set_temperature(self.hass, 25) self.hass.block_till_done() test_time = datetime.datetime.now(pytz.UTC) self._send_time_changed(test_time) @@ -833,7 +834,7 @@ class TestClimateGenericThermostatKeepAlive(unittest.TestCase): self.hass.block_till_done() self._setup_sensor(20) self.hass.block_till_done() - climate.set_temperature(self.hass, 25) + common.set_temperature(self.hass, 25) self.hass.block_till_done() test_time = datetime.datetime.now(pytz.UTC) self._send_time_changed(test_time) @@ -856,7 +857,7 @@ class TestClimateGenericThermostatKeepAlive(unittest.TestCase): self.hass.block_till_done() self._setup_sensor(30) self.hass.block_till_done() - climate.set_temperature(self.hass, 25) + common.set_temperature(self.hass, 25) self.hass.block_till_done() test_time = datetime.datetime.now(pytz.UTC) self._send_time_changed(test_time) @@ -926,7 +927,7 @@ class TestClimateGenericThermostatTurnOnOff(unittest.TestCase): def test_turn_on_when_off(self): """Test if climate.turn_on turns on a turned off device.""" - climate.set_operation_mode(self.hass, STATE_OFF) + common.set_operation_mode(self.hass, STATE_OFF) self.hass.block_till_done() self.hass.services.call('climate', SERVICE_TURN_ON) self.hass.block_till_done() @@ -939,8 +940,8 @@ class TestClimateGenericThermostatTurnOnOff(unittest.TestCase): def test_turn_on_when_on(self): """Test if climate.turn_on does nothing to a turned on device.""" - climate.set_operation_mode(self.hass, STATE_HEAT, self.HEAT_ENTITY) - climate.set_operation_mode(self.hass, STATE_COOL, self.COOL_ENTITY) + common.set_operation_mode(self.hass, STATE_HEAT, self.HEAT_ENTITY) + common.set_operation_mode(self.hass, STATE_COOL, self.COOL_ENTITY) self.hass.block_till_done() self.hass.services.call('climate', SERVICE_TURN_ON) self.hass.block_till_done() @@ -953,8 +954,8 @@ class TestClimateGenericThermostatTurnOnOff(unittest.TestCase): def test_turn_off_when_on(self): """Test if climate.turn_off turns off a turned on device.""" - climate.set_operation_mode(self.hass, STATE_HEAT, self.HEAT_ENTITY) - climate.set_operation_mode(self.hass, STATE_COOL, self.COOL_ENTITY) + common.set_operation_mode(self.hass, STATE_HEAT, self.HEAT_ENTITY) + common.set_operation_mode(self.hass, STATE_COOL, self.COOL_ENTITY) self.hass.block_till_done() self.hass.services.call('climate', SERVICE_TURN_OFF) self.hass.block_till_done() @@ -967,7 +968,7 @@ class TestClimateGenericThermostatTurnOnOff(unittest.TestCase): def test_turn_off_when_off(self): """Test if climate.turn_off does nothing to a turned off device.""" - climate.set_operation_mode(self.hass, STATE_OFF) + common.set_operation_mode(self.hass, STATE_OFF) self.hass.block_till_done() self.hass.services.call('climate', SERVICE_TURN_OFF) self.hass.block_till_done() diff --git a/tests/components/climate/test_mqtt.py b/tests/components/climate/test_mqtt.py index f46a23e4f97..c63dbf26690 100644 --- a/tests/components/climate/test_mqtt.py +++ b/tests/components/climate/test_mqtt.py @@ -6,14 +6,17 @@ from homeassistant.util.unit_system import ( METRIC_SYSTEM ) from homeassistant.setup import setup_component -from homeassistant.components import climate +from homeassistant.components import climate, mqtt 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, DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP) +from homeassistant.components.mqtt.discovery import async_start from tests.common import (get_test_home_assistant, mock_mqtt_component, - fire_mqtt_message, mock_component) + async_fire_mqtt_message, fire_mqtt_message, + mock_component, MockConfigEntry) +from tests.components.climate import common ENTITY_CLIMATE = 'climate.test' @@ -88,7 +91,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("off", state.attributes.get('operation_mode')) self.assertEqual("off", state.state) - climate.set_operation_mode(self.hass, None, ENTITY_CLIMATE) + common.set_operation_mode(self.hass, None, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("off", state.attributes.get('operation_mode')) @@ -101,7 +104,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("off", state.attributes.get('operation_mode')) self.assertEqual("off", state.state) - climate.set_operation_mode(self.hass, "cool", ENTITY_CLIMATE) + common.set_operation_mode(self.hass, "cool", ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("cool", state.attributes.get('operation_mode')) @@ -119,7 +122,7 @@ class TestMQTTClimate(unittest.TestCase): self.assertEqual("off", state.attributes.get('operation_mode')) self.assertEqual("off", state.state) - climate.set_operation_mode(self.hass, "cool", ENTITY_CLIMATE) + common.set_operation_mode(self.hass, "cool", ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("off", state.attributes.get('operation_mode')) @@ -146,7 +149,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("off", state.attributes.get('operation_mode')) self.assertEqual("off", state.state) - climate.set_operation_mode(self.hass, "on", ENTITY_CLIMATE) + common.set_operation_mode(self.hass, "on", ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("on", state.attributes.get('operation_mode')) @@ -157,7 +160,7 @@ class TestMQTTClimate(unittest.TestCase): ]) self.mock_publish.async_publish.reset_mock() - climate.set_operation_mode(self.hass, "off", ENTITY_CLIMATE) + common.set_operation_mode(self.hass, "off", ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("off", state.attributes.get('operation_mode')) @@ -174,7 +177,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("low", state.attributes.get('fan_mode')) - climate.set_fan_mode(self.hass, None, ENTITY_CLIMATE) + common.set_fan_mode(self.hass, None, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("low", state.attributes.get('fan_mode')) @@ -188,7 +191,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("low", state.attributes.get('fan_mode')) - climate.set_fan_mode(self.hass, 'high', ENTITY_CLIMATE) + common.set_fan_mode(self.hass, 'high', ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("low", state.attributes.get('fan_mode')) @@ -209,7 +212,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("low", state.attributes.get('fan_mode')) - climate.set_fan_mode(self.hass, 'high', ENTITY_CLIMATE) + common.set_fan_mode(self.hass, 'high', ENTITY_CLIMATE) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'fan-mode-topic', 'high', 0, False) @@ -222,7 +225,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("off", state.attributes.get('swing_mode')) - climate.set_swing_mode(self.hass, None, ENTITY_CLIMATE) + common.set_swing_mode(self.hass, None, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("off", state.attributes.get('swing_mode')) @@ -236,7 +239,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("off", state.attributes.get('swing_mode')) - climate.set_swing_mode(self.hass, 'on', ENTITY_CLIMATE) + common.set_swing_mode(self.hass, 'on', ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("off", state.attributes.get('swing_mode')) @@ -257,7 +260,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual("off", state.attributes.get('swing_mode')) - climate.set_swing_mode(self.hass, 'on', ENTITY_CLIMATE) + common.set_swing_mode(self.hass, 'on', ENTITY_CLIMATE) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'swing-mode-topic', 'on', 0, False) @@ -270,15 +273,15 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(21, state.attributes.get('temperature')) - climate.set_operation_mode(self.hass, 'heat', ENTITY_CLIMATE) + common.set_operation_mode(self.hass, 'heat', ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('heat', state.attributes.get('operation_mode')) self.mock_publish.async_publish.assert_called_once_with( 'mode-topic', 'heat', 0, False) self.mock_publish.async_publish.reset_mock() - climate.set_temperature(self.hass, temperature=47, - entity_id=ENTITY_CLIMATE) + common.set_temperature(self.hass, temperature=47, + entity_id=ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(47, state.attributes.get('temperature')) @@ -287,9 +290,9 @@ class TestMQTTClimate(unittest.TestCase): # also test directly supplying the operation mode to set_temperature self.mock_publish.async_publish.reset_mock() - climate.set_temperature(self.hass, temperature=21, - operation_mode="cool", - entity_id=ENTITY_CLIMATE) + common.set_temperature(self.hass, temperature=21, + operation_mode="cool", + entity_id=ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('cool', state.attributes.get('operation_mode')) @@ -308,10 +311,10 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(21, state.attributes.get('temperature')) - climate.set_operation_mode(self.hass, 'heat', ENTITY_CLIMATE) + common.set_operation_mode(self.hass, 'heat', ENTITY_CLIMATE) self.hass.block_till_done() - climate.set_temperature(self.hass, temperature=47, - entity_id=ENTITY_CLIMATE) + common.set_temperature(self.hass, temperature=47, + entity_id=ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(21, state.attributes.get('temperature')) @@ -347,7 +350,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('off', state.attributes.get('away_mode')) - climate.set_away_mode(self.hass, True, ENTITY_CLIMATE) + common.set_away_mode(self.hass, True, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('off', state.attributes.get('away_mode')) @@ -377,7 +380,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('off', state.attributes.get('away_mode')) - climate.set_away_mode(self.hass, True, ENTITY_CLIMATE) + common.set_away_mode(self.hass, True, ENTITY_CLIMATE) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'away-mode-topic', 'AN', 0, False) @@ -385,7 +388,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('on', state.attributes.get('away_mode')) - climate.set_away_mode(self.hass, False, ENTITY_CLIMATE) + common.set_away_mode(self.hass, False, ENTITY_CLIMATE) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'away-mode-topic', 'AUS', 0, False) @@ -401,7 +404,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(None, state.attributes.get('hold_mode')) - climate.set_hold_mode(self.hass, 'on', ENTITY_CLIMATE) + common.set_hold_mode(self.hass, 'on', ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(None, state.attributes.get('hold_mode')) @@ -422,7 +425,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual(None, state.attributes.get('hold_mode')) - climate.set_hold_mode(self.hass, 'on', ENTITY_CLIMATE) + common.set_hold_mode(self.hass, 'on', ENTITY_CLIMATE) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'hold-topic', 'on', 0, False) @@ -430,7 +433,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('on', state.attributes.get('hold_mode')) - climate.set_hold_mode(self.hass, 'off', ENTITY_CLIMATE) + common.set_hold_mode(self.hass, 'off', ENTITY_CLIMATE) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'hold-topic', 'off', 0, False) @@ -446,7 +449,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('off', state.attributes.get('aux_heat')) - climate.set_aux_heat(self.hass, True, ENTITY_CLIMATE) + common.set_aux_heat(self.hass, True, ENTITY_CLIMATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('off', state.attributes.get('aux_heat')) @@ -472,7 +475,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('off', state.attributes.get('aux_heat')) - climate.set_aux_heat(self.hass, True, ENTITY_CLIMATE) + common.set_aux_heat(self.hass, True, ENTITY_CLIMATE) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'aux-topic', 'ON', 0, False) @@ -480,7 +483,7 @@ class TestMQTTClimate(unittest.TestCase): state = self.hass.states.get(ENTITY_CLIMATE) self.assertEqual('on', state.attributes.get('aux_heat')) - climate.set_aux_heat(self.hass, False, ENTITY_CLIMATE) + common.set_aux_heat(self.hass, False, ENTITY_CLIMATE) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'aux-topic', 'OFF', 0, False) @@ -649,3 +652,24 @@ class TestMQTTClimate(unittest.TestCase): self.assertIsInstance(max_temp, float) self.assertEqual(60, max_temp) + + +async def test_discovery_removal_climate(hass, mqtt_mock, caplog): + """Test removal of discovered climate.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + data = ( + '{ "name": "Beer" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', + data) + await hass.async_block_till_done() + state = hass.states.get('climate.beer') + assert state is not None + assert state.name == 'Beer' + async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', + '') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('climate.beer') + assert state is None diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 66d29aac757..67d7eebbfec 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -72,7 +72,7 @@ def test_get_entries(hass, client): @asyncio.coroutine def test_remove_entry(hass, client): """Test removing an entry via the API.""" - entry = MockConfigEntry(domain='demo') + entry = MockConfigEntry(domain='demo', state=core_ce.ENTRY_STATE_LOADED) entry.add_to_hass(hass) resp = yield from client.delete( '/api/config/config_entries/entry/{}'.format(entry.entry_id)) @@ -206,6 +206,8 @@ def test_create_account(hass, client): 'title': 'Test Entry', 'type': 'create_entry', 'version': 1, + 'description': None, + 'description_placeholders': None, } @@ -266,6 +268,8 @@ def test_two_step_flow(hass, client): 'type': 'create_entry', 'title': 'user-title', 'version': 1, + 'description': None, + 'description_placeholders': None, } diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index f8ea51cfdc8..87eb0fb2d6f 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -27,7 +27,6 @@ async def test_list_devices(hass, client, registry): manufacturer='manufacturer', model='model') registry.async_get_or_create( config_entry_id='1234', - connections={}, identifiers={('bridgeid', '1234')}, manufacturer='manufacturer', model='model', via_hub=('bridgeid', '0123')) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 232405a632c..252d0b1d872 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -4,7 +4,9 @@ from unittest.mock import patch import pytest from homeassistant.setup import async_setup_component -from homeassistant.components import websocket_api +from homeassistant.components.websocket_api.http import URL +from homeassistant.components.websocket_api.auth import ( + TYPE_AUTH, TYPE_AUTH_OK, TYPE_AUTH_REQUIRED) from tests.common import MockUser, CLIENT_ID @@ -14,41 +16,52 @@ def hass_ws_client(aiohttp_client): """Websocket client fixture connected to websocket server.""" async def create_client(hass, access_token=None): """Create a websocket client.""" - wapi = hass.components.websocket_api assert await async_setup_component(hass, 'websocket_api') client = await aiohttp_client(hass.http.app) - patching = None + patches = [] - if access_token is not None: - patching = patch('homeassistant.auth.AuthManager.active', - return_value=True) - patching.start() + if access_token is None: + patches.append(patch( + 'homeassistant.auth.AuthManager.active', return_value=False)) + patches.append(patch( + 'homeassistant.auth.AuthManager.support_legacy', + return_value=True)) + patches.append(patch( + 'homeassistant.components.websocket_api.auth.' + 'validate_password', return_value=True)) + else: + patches.append(patch( + 'homeassistant.auth.AuthManager.active', return_value=True)) + patches.append(patch( + 'homeassistant.components.http.auth.setup_auth')) + + for p in patches: + p.start() try: - websocket = await client.ws_connect(wapi.URL) + websocket = await client.ws_connect(URL) auth_resp = await websocket.receive_json() + assert auth_resp['type'] == TYPE_AUTH_REQUIRED - if auth_resp['type'] == wapi.TYPE_AUTH_OK: - assert access_token is None, \ - 'Access token given but no auth required' - return websocket - - assert access_token is not None, \ - 'Access token required for fixture' - - await websocket.send_json({ - 'type': websocket_api.TYPE_AUTH, - 'access_token': access_token - }) + if access_token is None: + await websocket.send_json({ + 'type': TYPE_AUTH, + 'api_password': 'bla' + }) + else: + await websocket.send_json({ + 'type': TYPE_AUTH, + 'access_token': access_token + }) auth_ok = await websocket.receive_json() - assert auth_ok['type'] == wapi.TYPE_AUTH_OK + assert auth_ok['type'] == TYPE_AUTH_OK finally: - if patching is not None: - patching.stop() + for p in patches: + p.stop() # wrap in client websocket.client = client diff --git a/tests/components/counter/common.py b/tests/components/counter/common.py new file mode 100644 index 00000000000..36d09979d0d --- /dev/null +++ b/tests/components/counter/common.py @@ -0,0 +1,52 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.components.counter import ( + DOMAIN, SERVICE_DECREMENT, SERVICE_INCREMENT, SERVICE_RESET) +from homeassistant.core import callback +from homeassistant.loader import bind_hass + + +@bind_hass +def increment(hass, entity_id): + """Increment a counter.""" + hass.add_job(async_increment, hass, entity_id) + + +@callback +@bind_hass +def async_increment(hass, entity_id): + """Increment a counter.""" + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_INCREMENT, {ATTR_ENTITY_ID: entity_id})) + + +@bind_hass +def decrement(hass, entity_id): + """Decrement a counter.""" + hass.add_job(async_decrement, hass, entity_id) + + +@callback +@bind_hass +def async_decrement(hass, entity_id): + """Decrement a counter.""" + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_DECREMENT, {ATTR_ENTITY_ID: entity_id})) + + +@bind_hass +def reset(hass, entity_id): + """Reset a counter.""" + hass.add_job(async_reset, hass, entity_id) + + +@callback +@bind_hass +def async_reset(hass, entity_id): + """Reset a counter.""" + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_RESET, {ATTR_ENTITY_ID: entity_id})) diff --git a/tests/components/counter/test_init.py b/tests/components/counter/test_init.py index af36c1c8f95..929d96d4650 100644 --- a/tests/components/counter/test_init.py +++ b/tests/components/counter/test_init.py @@ -7,11 +7,11 @@ import logging from homeassistant.core import CoreState, State, Context from homeassistant.setup import setup_component, async_setup_component from homeassistant.components.counter import ( - DOMAIN, decrement, increment, reset, CONF_INITIAL, CONF_STEP, CONF_NAME, - CONF_ICON) + DOMAIN, CONF_INITIAL, CONF_RESTORE, CONF_STEP, CONF_NAME, CONF_ICON) from homeassistant.const import (ATTR_ICON, ATTR_FRIENDLY_NAME) from tests.common import (get_test_home_assistant, mock_restore_cache) +from tests.components.counter.common import decrement, increment, reset _LOGGER = logging.getLogger(__name__) @@ -55,6 +55,7 @@ class TestCounter(unittest.TestCase): CONF_NAME: 'Hello World', CONF_ICON: 'mdi:work', CONF_INITIAL: 10, + CONF_RESTORE: False, CONF_STEP: 5, } } @@ -172,9 +173,12 @@ def test_initial_state_overrules_restore_state(hass): yield from async_setup_component(hass, DOMAIN, { DOMAIN: { - 'test1': {}, + 'test1': { + CONF_RESTORE: False, + }, 'test2': { CONF_INITIAL: 10, + CONF_RESTORE: False, }, }}) @@ -187,6 +191,33 @@ def test_initial_state_overrules_restore_state(hass): assert int(state.state) == 10 +@asyncio.coroutine +def test_restore_state_overrules_initial_state(hass): + """Ensure states are restored on startup.""" + mock_restore_cache(hass, ( + State('counter.test1', '11'), + State('counter.test2', '-22'), + )) + + hass.state = CoreState.starting + + yield from async_setup_component(hass, DOMAIN, { + DOMAIN: { + 'test1': {}, + 'test2': { + CONF_INITIAL: 10, + }, + }}) + + state = hass.states.get('counter.test1') + assert state + assert int(state.state) == 11 + + state = hass.states.get('counter.test2') + assert state + assert int(state.state) == -22 + + @asyncio.coroutine def test_no_initial_state_and_no_restore_state(hass): """Ensure that entity is create without initial and restore feature.""" diff --git a/tests/components/cover/__init__.py b/tests/components/cover/__init__.py new file mode 100644 index 00000000000..aaaf6b237cd --- /dev/null +++ b/tests/components/cover/__init__.py @@ -0,0 +1 @@ +"""Tests for the cover component.""" diff --git a/tests/components/cover/test_command_line.py b/tests/components/cover/test_command_line.py index 346c3f94683..0e03539d58c 100644 --- a/tests/components/cover/test_command_line.py +++ b/tests/components/cover/test_command_line.py @@ -1,87 +1,75 @@ """The tests the cover command line platform.""" - import os import tempfile -import unittest from unittest import mock -from homeassistant.setup import setup_component -import homeassistant.components.cover as cover -from homeassistant.components.cover import ( - command_line as cmd_rs) +import pytest -from tests.common import get_test_home_assistant +from homeassistant.components.cover import DOMAIN +import homeassistant.components.cover.command_line as cmd_rs +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, + SERVICE_STOP_COVER) +from homeassistant.setup import async_setup_component -class TestCommandCover(unittest.TestCase): - """Test the cover command line platform.""" +@pytest.fixture +def rs(hass): + """Return CommandCover instance.""" + return cmd_rs.CommandCover(hass, 'foo', 'command_open', 'command_close', + 'command_stop', 'command_state', None) - def setup_method(self, method): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.rs = cmd_rs.CommandCover(self.hass, 'foo', - 'command_open', 'command_close', - 'command_stop', 'command_state', - None) - def teardown_method(self, method): - """Stop down everything that was started.""" - self.hass.stop() +def test_should_poll_new(rs): + """Test the setting of polling.""" + assert rs.should_poll is True + rs._command_state = None + assert rs.should_poll is False - def test_should_poll(self): - """Test the setting of polling.""" - self.assertTrue(self.rs.should_poll) - self.rs._command_state = None - self.assertFalse(self.rs.should_poll) - def test_query_state_value(self): - """Test with state value.""" - with mock.patch('subprocess.check_output') as mock_run: - mock_run.return_value = b' foo bar ' - result = self.rs._query_state_value('runme') - self.assertEqual('foo bar', result) - self.assertEqual(mock_run.call_count, 1) - self.assertEqual( - mock_run.call_args, mock.call('runme', shell=True) - ) +def test_query_state_value(rs): + """Test with state value.""" + with mock.patch('subprocess.check_output') as mock_run: + mock_run.return_value = b' foo bar ' + result = rs._query_state_value('runme') + assert 'foo bar' == result + assert mock_run.call_count == 1 + assert mock_run.call_args == mock.call('runme', shell=True) - def test_state_value(self): - """Test with state value.""" - with tempfile.TemporaryDirectory() as tempdirname: - path = os.path.join(tempdirname, 'cover_status') - test_cover = { - 'command_state': 'cat {}'.format(path), - 'command_open': 'echo 1 > {}'.format(path), - 'command_close': 'echo 1 > {}'.format(path), - 'command_stop': 'echo 0 > {}'.format(path), - 'value_template': '{{ value }}' - } - self.assertTrue(setup_component(self.hass, cover.DOMAIN, { - 'cover': { - 'platform': 'command_line', - 'covers': { - 'test': test_cover - } + +async def test_state_value(hass): + """Test with state value.""" + with tempfile.TemporaryDirectory() as tempdirname: + path = os.path.join(tempdirname, 'cover_status') + test_cover = { + 'command_state': 'cat {}'.format(path), + 'command_open': 'echo 1 > {}'.format(path), + 'command_close': 'echo 1 > {}'.format(path), + 'command_stop': 'echo 0 > {}'.format(path), + 'value_template': '{{ value }}' + } + assert await async_setup_component(hass, DOMAIN, { + 'cover': { + 'platform': 'command_line', + 'covers': { + 'test': test_cover } - })) + } + }) is True - state = self.hass.states.get('cover.test') - self.assertEqual('unknown', state.state) + assert 'unknown' == hass.states.get('cover.test').state - cover.open_cover(self.hass, 'cover.test') - self.hass.block_till_done() + await hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) + assert 'open' == hass.states.get('cover.test').state - state = self.hass.states.get('cover.test') - self.assertEqual('open', state.state) + await hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) + assert 'open' == hass.states.get('cover.test').state - cover.close_cover(self.hass, 'cover.test') - self.hass.block_till_done() - - state = self.hass.states.get('cover.test') - self.assertEqual('open', state.state) - - cover.stop_cover(self.hass, 'cover.test') - self.hass.block_till_done() - - state = self.hass.states.get('cover.test') - self.assertEqual('closed', state.state) + await hass.services.async_call( + DOMAIN, SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) + assert 'closed' == hass.states.get('cover.test').state diff --git a/tests/components/cover/test_demo.py b/tests/components/cover/test_demo.py index 65aa9a9b9ef..011928f851a 100644 --- a/tests/components/cover/test_demo.py +++ b/tests/components/cover/test_demo.py @@ -1,158 +1,181 @@ """The tests for the Demo cover platform.""" -import unittest from datetime import timedelta + +import pytest + +from homeassistant.components.cover import ( + ATTR_POSITION, ATTR_TILT_POSITION, DOMAIN) +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_CLOSE_COVER, SERVICE_CLOSE_COVER_TILT, + SERVICE_OPEN_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, + SERVICE_STOP_COVER_TILT) +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from homeassistant.setup import setup_component -from homeassistant.components import cover -from tests.common import get_test_home_assistant, fire_time_changed +from tests.common import assert_setup_component, async_fire_time_changed +CONFIG = {'cover': {'platform': 'demo'}} ENTITY_COVER = 'cover.living_room_window' -class TestCoverDemo(unittest.TestCase): - """Test the Demo cover.""" +@pytest.fixture +async def setup_comp(hass): + """Set up demo cover component.""" + with assert_setup_component(1, DOMAIN): + await async_setup_component(hass, DOMAIN, CONFIG) - def setUp(self): # pylint: disable=invalid-name - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.assertTrue(setup_component(self.hass, cover.DOMAIN, {'cover': { - 'platform': 'demo', - }})) - def tearDown(self): # pylint: disable=invalid-name - """Stop down everything that was started.""" - self.hass.stop() +async def test_supported_features(hass, setup_comp): + """Test cover supported features.""" + state = hass.states.get('cover.garage_door') + assert 3 == state.attributes.get('supported_features') + state = hass.states.get('cover.kitchen_window') + assert 11 == state.attributes.get('supported_features') + state = hass.states.get('cover.hall_window') + assert 15 == state.attributes.get('supported_features') + state = hass.states.get('cover.living_room_window') + assert 255 == state.attributes.get('supported_features') - def test_supported_features(self): - """Test cover supported features.""" - state = self.hass.states.get('cover.garage_door') - self.assertEqual(3, state.attributes.get('supported_features')) - state = self.hass.states.get('cover.kitchen_window') - self.assertEqual(11, state.attributes.get('supported_features')) - state = self.hass.states.get('cover.hall_window') - self.assertEqual(15, state.attributes.get('supported_features')) - state = self.hass.states.get('cover.living_room_window') - self.assertEqual(255, state.attributes.get('supported_features')) - def test_close_cover(self): - """Test closing the cover.""" - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(state.state, 'open') - self.assertEqual(70, state.attributes.get('current_position')) - cover.close_cover(self.hass, ENTITY_COVER) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(state.state, 'closing') - for _ in range(7): - future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() +async def test_close_cover(hass, setup_comp): + """Test closing the cover.""" + state = hass.states.get(ENTITY_COVER) + assert state.state == 'open' + assert 70 == state.attributes.get('current_position') - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(state.state, 'closed') - self.assertEqual(0, state.attributes.get('current_position')) - - def test_open_cover(self): - """Test opening the cover.""" - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(state.state, 'open') - self.assertEqual(70, state.attributes.get('current_position')) - cover.open_cover(self.hass, ENTITY_COVER) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(state.state, 'opening') - for _ in range(7): - future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(state.state, 'open') - self.assertEqual(100, state.attributes.get('current_position')) - - def test_set_cover_position(self): - """Test moving the cover to a specific position.""" - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(70, state.attributes.get('current_position')) - cover.set_cover_position(self.hass, 10, ENTITY_COVER) - self.hass.block_till_done() - for _ in range(6): - future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(10, state.attributes.get('current_position')) - - def test_stop_cover(self): - """Test stopping the cover.""" - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(70, state.attributes.get('current_position')) - cover.open_cover(self.hass, ENTITY_COVER) - self.hass.block_till_done() + await hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) + state = hass.states.get(ENTITY_COVER) + assert state.state == 'closing' + for _ in range(7): future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - cover.stop_cover(self.hass, ENTITY_COVER) - self.hass.block_till_done() - fire_time_changed(self.hass, future) - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(80, state.attributes.get('current_position')) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - def test_close_cover_tilt(self): - """Test closing the cover tilt.""" - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(50, state.attributes.get('current_tilt_position')) - cover.close_cover_tilt(self.hass, ENTITY_COVER) - self.hass.block_till_done() - for _ in range(7): - future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() + state = hass.states.get(ENTITY_COVER) + assert state.state == 'closed' + assert 0 == state.attributes.get('current_position') - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(0, state.attributes.get('current_tilt_position')) - def test_open_cover_tilt(self): - """Test opening the cover tilt.""" - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(50, state.attributes.get('current_tilt_position')) - cover.open_cover_tilt(self.hass, ENTITY_COVER) - self.hass.block_till_done() - for _ in range(7): - future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(100, state.attributes.get('current_tilt_position')) - - def test_set_cover_tilt_position(self): - """Test moving the cover til to a specific position.""" - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(50, state.attributes.get('current_tilt_position')) - cover.set_cover_tilt_position(self.hass, 90, ENTITY_COVER) - self.hass.block_till_done() - for _ in range(7): - future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(90, state.attributes.get('current_tilt_position')) - - def test_stop_cover_tilt(self): - """Test stopping the cover tilt.""" - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(50, state.attributes.get('current_tilt_position')) - cover.close_cover_tilt(self.hass, ENTITY_COVER) - self.hass.block_till_done() +async def test_open_cover(hass, setup_comp): + """Test opening the cover.""" + state = hass.states.get(ENTITY_COVER) + assert state.state == 'open' + assert 70 == state.attributes.get('current_position') + await hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) + state = hass.states.get(ENTITY_COVER) + assert state.state == 'opening' + for _ in range(7): future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - cover.stop_cover_tilt(self.hass, ENTITY_COVER) - self.hass.block_till_done() - fire_time_changed(self.hass, future) - state = self.hass.states.get(ENTITY_COVER) - self.assertEqual(40, state.attributes.get('current_tilt_position')) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_COVER) + assert state.state == 'open' + assert 100 == state.attributes.get('current_position') + + +async def test_set_cover_position(hass, setup_comp): + """Test moving the cover to a specific position.""" + state = hass.states.get(ENTITY_COVER) + assert 70 == state.attributes.get('current_position') + await hass.services.async_call( + DOMAIN, SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: ENTITY_COVER, ATTR_POSITION: 10}, blocking=True) + for _ in range(6): + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_COVER) + assert 10 == state.attributes.get('current_position') + + +async def test_stop_cover(hass, setup_comp): + """Test stopping the cover.""" + state = hass.states.get(ENTITY_COVER) + assert 70 == state.attributes.get('current_position') + await hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_COVER) + assert 80 == state.attributes.get('current_position') + + +async def test_close_cover_tilt(hass, setup_comp): + """Test closing the cover tilt.""" + state = hass.states.get(ENTITY_COVER) + assert 50 == state.attributes.get('current_tilt_position') + await hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER_TILT, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) + for _ in range(7): + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_COVER) + assert 0 == state.attributes.get('current_tilt_position') + + +async def test_open_cover_tilt(hass, setup_comp): + """Test opening the cover tilt.""" + state = hass.states.get(ENTITY_COVER) + assert 50 == state.attributes.get('current_tilt_position') + await hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) + for _ in range(7): + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_COVER) + assert 100 == state.attributes.get('current_tilt_position') + + +async def test_set_cover_tilt_position(hass, setup_comp): + """Test moving the cover til to a specific position.""" + state = hass.states.get(ENTITY_COVER) + assert 50 == state.attributes.get('current_tilt_position') + await hass.services.async_call( + DOMAIN, SERVICE_SET_COVER_TILT_POSITION, + {ATTR_ENTITY_ID: ENTITY_COVER, ATTR_TILT_POSITION: 90}, blocking=True) + for _ in range(7): + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_COVER) + assert 90 == state.attributes.get('current_tilt_position') + + +async def test_stop_cover_tilt(hass, setup_comp): + """Test stopping the cover tilt.""" + state = hass.states.get(ENTITY_COVER) + assert 50 == state.attributes.get('current_tilt_position') + await hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER_TILT, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, SERVICE_STOP_COVER_TILT, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_COVER) + assert 40 == state.attributes.get('current_tilt_position') diff --git a/tests/components/cover/test_group.py b/tests/components/cover/test_group.py index 028845983a0..2211c8c77bc 100644 --- a/tests/components/cover/test_group.py +++ b/tests/components/cover/test_group.py @@ -1,19 +1,23 @@ """The tests for the group cover platform.""" - -import unittest from datetime import timedelta -import homeassistant.util.dt as dt_util -from homeassistant import setup -from homeassistant.components import cover +import pytest + from homeassistant.components.cover import ( - ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, DOMAIN) + ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, ATTR_POSITION, + ATTR_TILT_POSITION, DOMAIN) from homeassistant.components.cover.group import DEFAULT_NAME from homeassistant.const import ( - ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES, - CONF_ENTITIES, STATE_OPEN, STATE_CLOSED) -from tests.common import ( - assert_setup_component, get_test_home_assistant, fire_time_changed) + ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, + ATTR_SUPPORTED_FEATURES, CONF_ENTITIES, + SERVICE_CLOSE_COVER, SERVICE_CLOSE_COVER_TILT, + SERVICE_OPEN_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, + SERVICE_STOP_COVER_TILT, STATE_OPEN, STATE_CLOSED) +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.common import assert_setup_component, async_fire_time_changed COVER_GROUP = 'cover.cover_group' DEMO_COVER = 'cover.kitchen_window' @@ -31,320 +35,301 @@ CONFIG = { } -class TestMultiCover(unittest.TestCase): - """Test the group cover platform.""" +@pytest.fixture +async def setup_comp(hass): + """Set up group cover component.""" + with assert_setup_component(2, DOMAIN): + await async_setup_component(hass, DOMAIN, CONFIG) - def setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() +async def test_attributes(hass): + """Test handling of state attributes.""" + config = {DOMAIN: {'platform': 'group', CONF_ENTITIES: [ + DEMO_COVER, DEMO_COVER_POS, DEMO_COVER_TILT, DEMO_TILT]}} - def test_attributes(self): - """Test handling of state attributes.""" - config = {DOMAIN: {'platform': 'group', CONF_ENTITIES: [ - DEMO_COVER, DEMO_COVER_POS, DEMO_COVER_TILT, DEMO_TILT]}} + with assert_setup_component(1, DOMAIN): + await async_setup_component(hass, DOMAIN, config) - with assert_setup_component(1, DOMAIN): - assert setup.setup_component(self.hass, DOMAIN, config) + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_CLOSED + assert state.attributes.get(ATTR_FRIENDLY_NAME) == DEFAULT_NAME + assert state.attributes.get(ATTR_ASSUMED_STATE) is None + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0 + assert state.attributes.get(ATTR_CURRENT_POSITION) is None + assert state.attributes.get(ATTR_CURRENT_TILT_POSITION) is None - state = self.hass.states.get(COVER_GROUP) - attr = state.attributes - self.assertEqual(state.state, STATE_CLOSED) - self.assertEqual(attr.get(ATTR_FRIENDLY_NAME), DEFAULT_NAME) - self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) - self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 0) - self.assertEqual(attr.get(ATTR_CURRENT_POSITION), None) - self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), None) + # Add Entity that supports open / close / stop + hass.states.async_set( + DEMO_COVER, STATE_OPEN, {ATTR_SUPPORTED_FEATURES: 11}) + await hass.async_block_till_done() - # Add Entity that supports open / close / stop - self.hass.states.set( - DEMO_COVER, STATE_OPEN, {ATTR_SUPPORTED_FEATURES: 11}) - self.hass.block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_ASSUMED_STATE) is None + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 11 + assert state.attributes.get(ATTR_CURRENT_POSITION) is None + assert state.attributes.get(ATTR_CURRENT_TILT_POSITION) is None - state = self.hass.states.get(COVER_GROUP) - attr = state.attributes - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) - self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 11) - self.assertEqual(attr.get(ATTR_CURRENT_POSITION), None) - self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), None) + # Add Entity that supports set_cover_position + hass.states.async_set( + DEMO_COVER_POS, STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: 4, ATTR_CURRENT_POSITION: 70}) + await hass.async_block_till_done() - # Add Entity that supports set_cover_position - self.hass.states.set( - DEMO_COVER_POS, STATE_OPEN, - {ATTR_SUPPORTED_FEATURES: 4, ATTR_CURRENT_POSITION: 70}) - self.hass.block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_ASSUMED_STATE) is None + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 15 + assert state.attributes.get(ATTR_CURRENT_POSITION) == 70 + assert state.attributes.get(ATTR_CURRENT_TILT_POSITION) is None - state = self.hass.states.get(COVER_GROUP) - attr = state.attributes - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) - self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 15) - self.assertEqual(attr.get(ATTR_CURRENT_POSITION), 70) - self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), None) + # Add Entity that supports open tilt / close tilt / stop tilt + hass.states.async_set( + DEMO_TILT, STATE_OPEN, {ATTR_SUPPORTED_FEATURES: 112}) + await hass.async_block_till_done() - # Add Entity that supports open tilt / close tilt / stop tilt - self.hass.states.set( - DEMO_TILT, STATE_OPEN, {ATTR_SUPPORTED_FEATURES: 112}) - self.hass.block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_ASSUMED_STATE) is None + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 127 + assert state.attributes.get(ATTR_CURRENT_POSITION) == 70 + assert state.attributes.get(ATTR_CURRENT_TILT_POSITION) is None - state = self.hass.states.get(COVER_GROUP) - attr = state.attributes - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) - self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 127) - self.assertEqual(attr.get(ATTR_CURRENT_POSITION), 70) - self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), None) + # Add Entity that supports set_tilt_position + hass.states.async_set( + DEMO_COVER_TILT, STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: 128, ATTR_CURRENT_TILT_POSITION: 60}) + await hass.async_block_till_done() - # Add Entity that supports set_tilt_position - self.hass.states.set( - DEMO_COVER_TILT, STATE_OPEN, - {ATTR_SUPPORTED_FEATURES: 128, ATTR_CURRENT_TILT_POSITION: 60}) - self.hass.block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_ASSUMED_STATE) is None + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 255 + assert state.attributes.get(ATTR_CURRENT_POSITION) == 70 + assert state.attributes.get(ATTR_CURRENT_TILT_POSITION) == 60 - state = self.hass.states.get(COVER_GROUP) - attr = state.attributes - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) - self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 255) - self.assertEqual(attr.get(ATTR_CURRENT_POSITION), 70) - self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), 60) + # ### Test assumed state ### + # ########################## - # ### Test assumed state ### - # ########################## + # For covers + hass.states.async_set( + DEMO_COVER, STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: 4, ATTR_CURRENT_POSITION: 100}) + await hass.async_block_till_done() - # For covers - self.hass.states.set( - DEMO_COVER, STATE_OPEN, - {ATTR_SUPPORTED_FEATURES: 4, ATTR_CURRENT_POSITION: 100}) - self.hass.block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_ASSUMED_STATE) is True + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 244 + assert state.attributes.get(ATTR_CURRENT_POSITION) == 100 + assert state.attributes.get(ATTR_CURRENT_TILT_POSITION) == 60 - state = self.hass.states.get(COVER_GROUP) - attr = state.attributes - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(attr.get(ATTR_ASSUMED_STATE), True) - self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 244) - self.assertEqual(attr.get(ATTR_CURRENT_POSITION), 100) - self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), 60) + hass.states.async_remove(DEMO_COVER) + hass.states.async_remove(DEMO_COVER_POS) + await hass.async_block_till_done() - self.hass.states.remove(DEMO_COVER) - self.hass.block_till_done() - self.hass.states.remove(DEMO_COVER_POS) - self.hass.block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_ASSUMED_STATE) is None + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 240 + assert state.attributes.get(ATTR_CURRENT_POSITION) is None + assert state.attributes.get(ATTR_CURRENT_TILT_POSITION) == 60 - state = self.hass.states.get(COVER_GROUP) - attr = state.attributes - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) - self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 240) - self.assertEqual(attr.get(ATTR_CURRENT_POSITION), None) - self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), 60) + # For tilts + hass.states.async_set( + DEMO_TILT, STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: 128, ATTR_CURRENT_TILT_POSITION: 100}) + await hass.async_block_till_done() - # For tilts - self.hass.states.set( - DEMO_TILT, STATE_OPEN, - {ATTR_SUPPORTED_FEATURES: 128, ATTR_CURRENT_TILT_POSITION: 100}) - self.hass.block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_ASSUMED_STATE) is True + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 128 + assert state.attributes.get(ATTR_CURRENT_POSITION) is None + assert state.attributes.get(ATTR_CURRENT_TILT_POSITION) == 100 - state = self.hass.states.get(COVER_GROUP) - attr = state.attributes - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(attr.get(ATTR_ASSUMED_STATE), True) - self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 128) - self.assertEqual(attr.get(ATTR_CURRENT_POSITION), None) - self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), 100) + hass.states.async_remove(DEMO_COVER_TILT) + hass.states.async_set(DEMO_TILT, STATE_CLOSED) + await hass.async_block_till_done() - self.hass.states.remove(DEMO_COVER_TILT) - self.hass.states.set(DEMO_TILT, STATE_CLOSED) - self.hass.block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_CLOSED + assert state.attributes.get(ATTR_ASSUMED_STATE) is None + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0 + assert state.attributes.get(ATTR_CURRENT_POSITION) is None + assert state.attributes.get(ATTR_CURRENT_TILT_POSITION) is None - state = self.hass.states.get(COVER_GROUP) - attr = state.attributes - self.assertEqual(state.state, STATE_CLOSED) - self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) - self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 0) - self.assertEqual(attr.get(ATTR_CURRENT_POSITION), None) - self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), None) + hass.states.async_set( + DEMO_TILT, STATE_CLOSED, {ATTR_ASSUMED_STATE: True}) + await hass.async_block_till_done() - self.hass.states.set( - DEMO_TILT, STATE_CLOSED, {ATTR_ASSUMED_STATE: True}) - self.hass.block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.attributes.get(ATTR_ASSUMED_STATE) is True - state = self.hass.states.get(COVER_GROUP) - attr = state.attributes - self.assertEqual(attr.get(ATTR_ASSUMED_STATE), True) - def test_open_covers(self): - """Test open cover function.""" - with assert_setup_component(2, DOMAIN): - assert setup.setup_component(self.hass, DOMAIN, CONFIG) - - cover.open_cover(self.hass, COVER_GROUP) - self.hass.block_till_done() - for _ in range(10): - future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - - state = self.hass.states.get(COVER_GROUP) - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(state.attributes.get(ATTR_CURRENT_POSITION), 100) - - self.assertEqual(self.hass.states.get(DEMO_COVER).state, STATE_OPEN) - self.assertEqual(self.hass.states.get(DEMO_COVER_POS) - .attributes.get(ATTR_CURRENT_POSITION), 100) - self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) - .attributes.get(ATTR_CURRENT_POSITION), 100) - - def test_close_covers(self): - """Test close cover function.""" - with assert_setup_component(2, DOMAIN): - assert setup.setup_component(self.hass, DOMAIN, CONFIG) - - cover.close_cover(self.hass, COVER_GROUP) - self.hass.block_till_done() - for _ in range(10): - future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - - state = self.hass.states.get(COVER_GROUP) - self.assertEqual(state.state, STATE_CLOSED) - self.assertEqual(state.attributes.get(ATTR_CURRENT_POSITION), 0) - - self.assertEqual(self.hass.states.get(DEMO_COVER).state, STATE_CLOSED) - self.assertEqual(self.hass.states.get(DEMO_COVER_POS) - .attributes.get(ATTR_CURRENT_POSITION), 0) - self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) - .attributes.get(ATTR_CURRENT_POSITION), 0) - - def test_stop_covers(self): - """Test stop cover function.""" - with assert_setup_component(2, DOMAIN): - assert setup.setup_component(self.hass, DOMAIN, CONFIG) - - cover.open_cover(self.hass, COVER_GROUP) - self.hass.block_till_done() +async def test_open_covers(hass, setup_comp): + """Test open cover function.""" + await hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True) + for _ in range(10): future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - cover.stop_cover(self.hass, COVER_GROUP) - self.hass.block_till_done() + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_CURRENT_POSITION) == 100 + + assert hass.states.get(DEMO_COVER).state == STATE_OPEN + assert hass.states.get(DEMO_COVER_POS) \ + .attributes.get(ATTR_CURRENT_POSITION) == 100 + assert hass.states.get(DEMO_COVER_TILT) \ + .attributes.get(ATTR_CURRENT_POSITION) == 100 + + +async def test_close_covers(hass, setup_comp): + """Test close cover function.""" + await hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True) + for _ in range(10): future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - state = self.hass.states.get(COVER_GROUP) - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(state.attributes.get(ATTR_CURRENT_POSITION), 100) + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_CLOSED + assert state.attributes.get(ATTR_CURRENT_POSITION) == 0 - self.assertEqual(self.hass.states.get(DEMO_COVER).state, STATE_OPEN) - self.assertEqual(self.hass.states.get(DEMO_COVER_POS) - .attributes.get(ATTR_CURRENT_POSITION), 20) - self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) - .attributes.get(ATTR_CURRENT_POSITION), 80) + assert hass.states.get(DEMO_COVER).state == STATE_CLOSED + assert hass.states.get(DEMO_COVER_POS) \ + .attributes.get(ATTR_CURRENT_POSITION) == 0 + assert hass.states.get(DEMO_COVER_TILT) \ + .attributes.get(ATTR_CURRENT_POSITION) == 0 - def test_set_cover_position(self): - """Test set cover position function.""" - with assert_setup_component(2, DOMAIN): - assert setup.setup_component(self.hass, DOMAIN, CONFIG) - cover.set_cover_position(self.hass, 50, COVER_GROUP) - self.hass.block_till_done() - for _ in range(4): - future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() +async def test_stop_covers(hass, setup_comp): + """Test stop cover function.""" + await hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True) + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - state = self.hass.states.get(COVER_GROUP) - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(state.attributes.get(ATTR_CURRENT_POSITION), 50) + await hass.services.async_call( + DOMAIN, SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True) + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - self.assertEqual(self.hass.states.get(DEMO_COVER).state, STATE_CLOSED) - self.assertEqual(self.hass.states.get(DEMO_COVER_POS) - .attributes.get(ATTR_CURRENT_POSITION), 50) - self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) - .attributes.get(ATTR_CURRENT_POSITION), 50) + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_CURRENT_POSITION) == 100 - def test_open_tilts(self): - """Test open tilt function.""" - with assert_setup_component(2, DOMAIN): - assert setup.setup_component(self.hass, DOMAIN, CONFIG) + assert hass.states.get(DEMO_COVER).state == STATE_OPEN + assert hass.states.get(DEMO_COVER_POS) \ + .attributes.get(ATTR_CURRENT_POSITION) == 20 + assert hass.states.get(DEMO_COVER_TILT) \ + .attributes.get(ATTR_CURRENT_POSITION) == 80 - cover.open_cover_tilt(self.hass, COVER_GROUP) - self.hass.block_till_done() - for _ in range(5): - future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - state = self.hass.states.get(COVER_GROUP) - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(state.attributes.get(ATTR_CURRENT_TILT_POSITION), 100) - - self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) - .attributes.get(ATTR_CURRENT_TILT_POSITION), 100) - - def test_close_tilts(self): - """Test close tilt function.""" - with assert_setup_component(2, DOMAIN): - assert setup.setup_component(self.hass, DOMAIN, CONFIG) - - cover.close_cover_tilt(self.hass, COVER_GROUP) - self.hass.block_till_done() - for _ in range(5): - future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - - state = self.hass.states.get(COVER_GROUP) - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(state.attributes.get(ATTR_CURRENT_TILT_POSITION), 0) - - self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) - .attributes.get(ATTR_CURRENT_TILT_POSITION), 0) - - def test_stop_tilts(self): - """Test stop tilts function.""" - with assert_setup_component(2, DOMAIN): - assert setup.setup_component(self.hass, DOMAIN, CONFIG) - - cover.open_cover_tilt(self.hass, COVER_GROUP) - self.hass.block_till_done() +async def test_set_cover_position(hass, setup_comp): + """Test set cover position function.""" + await hass.services.async_call( + DOMAIN, SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: COVER_GROUP, ATTR_POSITION: 50}, blocking=True) + for _ in range(4): future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - cover.stop_cover_tilt(self.hass, COVER_GROUP) - self.hass.block_till_done() + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_CURRENT_POSITION) == 50 + + assert hass.states.get(DEMO_COVER).state == STATE_CLOSED + assert hass.states.get(DEMO_COVER_POS) \ + .attributes.get(ATTR_CURRENT_POSITION) == 50 + assert hass.states.get(DEMO_COVER_TILT) \ + .attributes.get(ATTR_CURRENT_POSITION) == 50 + + +async def test_open_tilts(hass, setup_comp): + """Test open tilt function.""" + await hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True) + for _ in range(5): future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - state = self.hass.states.get(COVER_GROUP) - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(state.attributes.get(ATTR_CURRENT_TILT_POSITION), 60) + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_CURRENT_TILT_POSITION) == 100 - self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) - .attributes.get(ATTR_CURRENT_TILT_POSITION), 60) + assert hass.states.get(DEMO_COVER_TILT) \ + .attributes.get(ATTR_CURRENT_TILT_POSITION) == 100 - def test_set_tilt_positions(self): - """Test set tilt position function.""" - with assert_setup_component(2, DOMAIN): - assert setup.setup_component(self.hass, DOMAIN, CONFIG) - cover.set_cover_tilt_position(self.hass, 80, COVER_GROUP) - self.hass.block_till_done() - for _ in range(3): - future = dt_util.utcnow() + timedelta(seconds=1) - fire_time_changed(self.hass, future) - self.hass.block_till_done() +async def test_close_tilts(hass, setup_comp): + """Test close tilt function.""" + await hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER_TILT, + {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True) + for _ in range(5): + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - state = self.hass.states.get(COVER_GROUP) - self.assertEqual(state.state, STATE_OPEN) - self.assertEqual(state.attributes.get(ATTR_CURRENT_TILT_POSITION), 80) + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_CURRENT_TILT_POSITION) == 0 - self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) - .attributes.get(ATTR_CURRENT_TILT_POSITION), 80) + assert hass.states.get(DEMO_COVER_TILT) \ + .attributes.get(ATTR_CURRENT_TILT_POSITION) == 0 + + +async def test_stop_tilts(hass, setup_comp): + """Test stop tilts function.""" + await hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True) + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + await hass.services.async_call( + DOMAIN, SERVICE_STOP_COVER_TILT, + {ATTR_ENTITY_ID: COVER_GROUP}, blocking=True) + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_CURRENT_TILT_POSITION) == 60 + + assert hass.states.get(DEMO_COVER_TILT) \ + .attributes.get(ATTR_CURRENT_TILT_POSITION) == 60 + + +async def test_set_tilt_positions(hass, setup_comp): + """Test set tilt position function.""" + await hass.services.async_call( + DOMAIN, SERVICE_SET_COVER_TILT_POSITION, + {ATTR_ENTITY_ID: COVER_GROUP, ATTR_TILT_POSITION: 80}, blocking=True) + for _ in range(3): + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPEN + assert state.attributes.get(ATTR_CURRENT_TILT_POSITION) == 80 + + assert hass.states.get(DEMO_COVER_TILT) \ + .attributes.get(ATTR_CURRENT_TILT_POSITION) == 80 diff --git a/tests/components/cover/test_mqtt.py b/tests/components/cover/test_mqtt.py index ad68c2416ca..355f620520a 100644 --- a/tests/components/cover/test_mqtt.py +++ b/tests/components/cover/test_mqtt.py @@ -1,14 +1,21 @@ """The tests for the MQTT cover platform.""" import unittest -from homeassistant.setup import setup_component -from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN, \ - STATE_UNAVAILABLE, ATTR_ASSUMED_STATE -import homeassistant.components.cover as cover +from homeassistant.components import cover, mqtt +from homeassistant.components.cover import (ATTR_POSITION, ATTR_TILT_POSITION) from homeassistant.components.cover.mqtt import MqttCover +from homeassistant.components.mqtt.discovery import async_start +from homeassistant.const import ( + ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, + SERVICE_CLOSE_COVER, SERVICE_CLOSE_COVER_TILT, SERVICE_OPEN_COVER, + SERVICE_OPEN_COVER_TILT, SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, + STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE, STATE_UNKNOWN) +from homeassistant.setup import setup_component, async_setup_component from tests.common import ( - get_test_home_assistant, mock_mqtt_component, fire_mqtt_message) + get_test_home_assistant, mock_mqtt_component, async_fire_mqtt_message, + fire_mqtt_message, MockConfigEntry, async_mock_mqtt_component) class TestCoverMQTT(unittest.TestCase): @@ -115,7 +122,9 @@ class TestCoverMQTT(unittest.TestCase): self.assertEqual(STATE_UNKNOWN, state.state) self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) - cover.open_cover(self.hass, 'cover.test') + self.hass.services.call( + cover.DOMAIN, SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -124,7 +133,9 @@ class TestCoverMQTT(unittest.TestCase): state = self.hass.states.get('cover.test') self.assertEqual(STATE_OPEN, state.state) - cover.close_cover(self.hass, 'cover.test') + self.hass.services.call( + cover.DOMAIN, SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -147,7 +158,9 @@ class TestCoverMQTT(unittest.TestCase): state = self.hass.states.get('cover.test') self.assertEqual(STATE_UNKNOWN, state.state) - cover.open_cover(self.hass, 'cover.test') + self.hass.services.call( + cover.DOMAIN, SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -170,7 +183,9 @@ class TestCoverMQTT(unittest.TestCase): state = self.hass.states.get('cover.test') self.assertEqual(STATE_UNKNOWN, state.state) - cover.close_cover(self.hass, 'cover.test') + self.hass.services.call( + cover.DOMAIN, SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -193,7 +208,9 @@ class TestCoverMQTT(unittest.TestCase): state = self.hass.states.get('cover.test') self.assertEqual(STATE_UNKNOWN, state.state) - cover.stop_cover(self.hass, 'cover.test') + self.hass.services.call( + cover.DOMAIN, SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -295,7 +312,9 @@ class TestCoverMQTT(unittest.TestCase): } })) - cover.set_cover_position(self.hass, 100, 'cover.test') + self.hass.services.call( + cover.DOMAIN, SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: 'cover.test', ATTR_POSITION: 100}, blocking=True) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -316,7 +335,9 @@ class TestCoverMQTT(unittest.TestCase): } })) - cover.set_cover_position(self.hass, 62, 'cover.test') + self.hass.services.call( + cover.DOMAIN, SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: 'cover.test', ATTR_POSITION: 62}, blocking=True) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -401,14 +422,18 @@ class TestCoverMQTT(unittest.TestCase): } })) - cover.open_cover_tilt(self.hass, 'cover.test') + self.hass.services.call( + cover.DOMAIN, SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'tilt-command-topic', 100, 0, False) self.mock_publish.async_publish.reset_mock() - cover.close_cover_tilt(self.hass, 'cover.test') + self.hass.services.call( + cover.DOMAIN, SERVICE_CLOSE_COVER_TILT, + {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -433,14 +458,18 @@ class TestCoverMQTT(unittest.TestCase): } })) - cover.open_cover_tilt(self.hass, 'cover.test') + self.hass.services.call( + cover.DOMAIN, SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'tilt-command-topic', 400, 0, False) self.mock_publish.async_publish.reset_mock() - cover.close_cover_tilt(self.hass, 'cover.test') + self.hass.services.call( + cover.DOMAIN, SERVICE_CLOSE_COVER_TILT, + {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -540,7 +569,10 @@ class TestCoverMQTT(unittest.TestCase): } })) - cover.set_cover_tilt_position(self.hass, 50, 'cover.test') + self.hass.services.call( + cover.DOMAIN, SERVICE_SET_COVER_TILT_POSITION, + {ATTR_ENTITY_ID: 'cover.test', ATTR_TILT_POSITION: 50}, + blocking=True) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -567,7 +599,10 @@ class TestCoverMQTT(unittest.TestCase): } })) - cover.set_cover_tilt_position(self.hass, 50, 'cover.test') + self.hass.services.call( + cover.DOMAIN, SERVICE_SET_COVER_TILT_POSITION, + {ATTR_ENTITY_ID: 'cover.test', ATTR_TILT_POSITION: 50}, + blocking=True) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -579,7 +614,8 @@ class TestCoverMQTT(unittest.TestCase): 'cover.test', 'state-topic', 'command-topic', None, 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, - False, None, 100, 0, 0, 100, False, False, None, None) + False, None, 100, 0, 0, 100, False, False, None, None, None, + None) self.assertEqual(44, mqtt_cover.find_percentage_in_range(44)) @@ -589,7 +625,8 @@ class TestCoverMQTT(unittest.TestCase): 'cover.test', 'state-topic', 'command-topic', None, 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, - False, None, 180, 80, 80, 180, False, False, None, None) + False, None, 180, 80, 80, 180, False, False, None, None, None, + None) self.assertEqual(40, mqtt_cover.find_percentage_in_range(120)) @@ -599,7 +636,8 @@ class TestCoverMQTT(unittest.TestCase): 'cover.test', 'state-topic', 'command-topic', None, 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, - False, None, 100, 0, 0, 100, False, True, None, None) + False, None, 100, 0, 0, 100, False, True, None, None, None, + None) self.assertEqual(56, mqtt_cover.find_percentage_in_range(44)) @@ -609,7 +647,8 @@ class TestCoverMQTT(unittest.TestCase): 'cover.test', 'state-topic', 'command-topic', None, 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, - False, None, 180, 80, 80, 180, False, True, None, None) + False, None, 180, 80, 80, 180, False, True, None, None, None, + None) self.assertEqual(60, mqtt_cover.find_percentage_in_range(120)) @@ -619,7 +658,8 @@ class TestCoverMQTT(unittest.TestCase): 'cover.test', 'state-topic', 'command-topic', None, 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, - False, None, 100, 0, 0, 100, False, False, None, None) + False, None, 100, 0, 0, 100, False, False, None, None, None, + None) self.assertEqual(44, mqtt_cover.find_in_range_from_percent(44)) @@ -629,7 +669,8 @@ class TestCoverMQTT(unittest.TestCase): 'cover.test', 'state-topic', 'command-topic', None, 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, - False, None, 180, 80, 80, 180, False, False, None, None) + False, None, 180, 80, 80, 180, False, False, None, None, None, + None) self.assertEqual(120, mqtt_cover.find_in_range_from_percent(40)) @@ -639,7 +680,8 @@ class TestCoverMQTT(unittest.TestCase): 'cover.test', 'state-topic', 'command-topic', None, 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, - False, None, 100, 0, 0, 100, False, True, None, None) + False, None, 100, 0, 0, 100, False, True, None, None, None, + None) self.assertEqual(44, mqtt_cover.find_in_range_from_percent(56)) @@ -649,7 +691,8 @@ class TestCoverMQTT(unittest.TestCase): 'cover.test', 'state-topic', 'command-topic', None, 'tilt-command-topic', 'tilt-status-topic', 0, False, 'OPEN', 'CLOSE', 'OPEN', 'CLOSE', 'STOP', None, None, - False, None, 180, 80, 80, 180, False, True, None, None) + False, None, 180, 80, 80, 180, False, True, None, None, None, + None) self.assertEqual(120, mqtt_cover.find_in_range_from_percent(60)) @@ -722,3 +765,48 @@ class TestCoverMQTT(unittest.TestCase): state = self.hass.states.get('cover.test') self.assertEqual(STATE_UNAVAILABLE, state.state) + + +async def test_discovery_removal_cover(hass, mqtt_mock, caplog): + """Test removal of discovered cover.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + data = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/cover/bla/config', + data) + await hass.async_block_till_done() + state = hass.states.get('cover.beer') + assert state is not None + assert state.name == 'Beer' + async_fire_mqtt_message(hass, 'homeassistant/cover/bla/config', + '') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('cover.beer') + assert state is None + + +async def test_unique_id(hass): + """Test unique_id option only creates one cover per id.""" + await async_mock_mqtt_component(hass) + assert await async_setup_component(hass, cover.DOMAIN, { + cover.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' + }] + }) + + async_fire_mqtt_message(hass, 'test-topic', 'payload') + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids(cover.DOMAIN)) == 1 diff --git a/tests/components/cover/test_template.py b/tests/components/cover/test_template.py index b786b463dad..3c820f1a0ac 100644 --- a/tests/components/cover/test_template.py +++ b/tests/components/cover/test_template.py @@ -1,17 +1,24 @@ """The tests the cover command line platform.""" - import logging import unittest -from homeassistant.core import callback from homeassistant import setup -import homeassistant.components.cover as cover -from homeassistant.const import STATE_OPEN, STATE_CLOSED +from homeassistant.core import callback +from homeassistant.components.cover import ( + ATTR_POSITION, ATTR_TILT_POSITION, DOMAIN) +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_CLOSE_COVER, SERVICE_CLOSE_COVER_TILT, + SERVICE_OPEN_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, + STATE_CLOSED, STATE_OPEN) from tests.common import ( get_test_home_assistant, assert_setup_component) + _LOGGER = logging.getLogger(__name__) +ENTITY_COVER = 'cover.test_template_cover' + class TestTemplateCover(unittest.TestCase): """Test the cover command line platform.""" @@ -362,7 +369,9 @@ class TestTemplateCover(unittest.TestCase): state = self.hass.states.get('cover.test_template_cover') assert state.state == STATE_CLOSED - cover.open_cover(self.hass, 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) self.hass.block_till_done() assert len(self.calls) == 1 @@ -398,10 +407,14 @@ class TestTemplateCover(unittest.TestCase): state = self.hass.states.get('cover.test_template_cover') assert state.state == STATE_OPEN - cover.close_cover(self.hass, 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) self.hass.block_till_done() - cover.stop_cover(self.hass, 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) self.hass.block_till_done() assert len(self.calls) == 2 @@ -445,18 +458,23 @@ class TestTemplateCover(unittest.TestCase): state = self.hass.states.get('cover.test_template_cover') assert state.state == STATE_OPEN - cover.open_cover(self.hass, 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) self.hass.block_till_done() state = self.hass.states.get('cover.test_template_cover') assert state.attributes.get('current_position') == 100.0 - cover.close_cover(self.hass, 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) self.hass.block_till_done() state = self.hass.states.get('cover.test_template_cover') assert state.attributes.get('current_position') == 0.0 - cover.set_cover_position(self.hass, 25, - 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: ENTITY_COVER, ATTR_POSITION: 25}, blocking=True) self.hass.block_till_done() state = self.hass.states.get('cover.test_template_cover') assert state.attributes.get('current_position') == 25.0 @@ -490,8 +508,10 @@ class TestTemplateCover(unittest.TestCase): self.hass.start() self.hass.block_till_done() - cover.set_cover_tilt_position(self.hass, 42, - 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_SET_COVER_TILT_POSITION, + {ATTR_ENTITY_ID: ENTITY_COVER, ATTR_TILT_POSITION: 42}, + blocking=True) self.hass.block_till_done() assert len(self.calls) == 1 @@ -525,7 +545,9 @@ class TestTemplateCover(unittest.TestCase): self.hass.start() self.hass.block_till_done() - cover.open_cover_tilt(self.hass, 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) self.hass.block_till_done() assert len(self.calls) == 1 @@ -559,7 +581,9 @@ class TestTemplateCover(unittest.TestCase): self.hass.start() self.hass.block_till_done() - cover.close_cover_tilt(self.hass, 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_CLOSE_COVER_TILT, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) self.hass.block_till_done() assert len(self.calls) == 1 @@ -585,18 +609,23 @@ class TestTemplateCover(unittest.TestCase): state = self.hass.states.get('cover.test_template_cover') assert state.attributes.get('current_position') is None - cover.set_cover_position(self.hass, 42, - 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: ENTITY_COVER, ATTR_POSITION: 42}, blocking=True) self.hass.block_till_done() state = self.hass.states.get('cover.test_template_cover') assert state.attributes.get('current_position') == 42.0 - cover.close_cover(self.hass, 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) self.hass.block_till_done() state = self.hass.states.get('cover.test_template_cover') assert state.state == STATE_CLOSED - cover.open_cover(self.hass, 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) self.hass.block_till_done() state = self.hass.states.get('cover.test_template_cover') assert state.state == STATE_OPEN @@ -627,18 +656,24 @@ class TestTemplateCover(unittest.TestCase): state = self.hass.states.get('cover.test_template_cover') assert state.attributes.get('current_tilt_position') is None - cover.set_cover_tilt_position(self.hass, 42, - 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_SET_COVER_TILT_POSITION, + {ATTR_ENTITY_ID: ENTITY_COVER, ATTR_TILT_POSITION: 42}, + blocking=True) self.hass.block_till_done() state = self.hass.states.get('cover.test_template_cover') assert state.attributes.get('current_tilt_position') == 42.0 - cover.close_cover_tilt(self.hass, 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_CLOSE_COVER_TILT, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) self.hass.block_till_done() state = self.hass.states.get('cover.test_template_cover') assert state.attributes.get('current_tilt_position') == 0.0 - cover.open_cover_tilt(self.hass, 'cover.test_template_cover') + self.hass.services.call( + DOMAIN, SERVICE_OPEN_COVER_TILT, + {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True) self.hass.block_till_done() state = self.hass.states.get('cover.test_template_cover') assert state.attributes.get('current_tilt_position') == 100.0 diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 3920a45ddf6..8582f5b38cf 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -1,6 +1,7 @@ """The tests for the emulated Hue component.""" import asyncio import json +from ipaddress import ip_address from unittest.mock import patch from aiohttp.hdrs import CONTENT_TYPE @@ -484,3 +485,12 @@ def perform_put_light_state(hass_hue, client, entity_id, is_on, yield from hass_hue.async_block_till_done() return result + + +async def test_external_ip_blocked(hue_client): + """Test external IP blocked.""" + with patch('homeassistant.components.http.real_ip.ip_address', + return_value=ip_address('45.45.45.45')): + result = await hue_client.get('/api/username/lights') + + assert result.status == 400 diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index 2f443eb5d6e..9b0a5cd9052 100644 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -1,14 +1,16 @@ """Test the Emulated Hue component.""" import json -from unittest.mock import patch, Mock, mock_open +from unittest.mock import patch, Mock, mock_open, MagicMock from homeassistant.components.emulated_hue import Config def test_config_google_home_entity_id_to_number(): """Test config adheres to the type.""" - conf = Config(Mock(), { + mock_hass = Mock() + mock_hass.config.path = MagicMock("path", return_value="test_path") + conf = Config(mock_hass, { 'type': 'google_home' }) @@ -16,29 +18,33 @@ def test_config_google_home_entity_id_to_number(): handle = mop() with patch('homeassistant.util.json.open', mop, create=True): - number = conf.entity_id_to_number('light.test') - assert number == '2' - assert handle.write.call_count == 1 - assert json.loads(handle.write.mock_calls[0][1][0]) == { - '1': 'light.test2', - '2': 'light.test', - } + with patch('homeassistant.util.json.os.open', return_value=0): + with patch('homeassistant.util.json.os.replace'): + number = conf.entity_id_to_number('light.test') + assert number == '2' + assert handle.write.call_count == 1 + assert json.loads(handle.write.mock_calls[0][1][0]) == { + '1': 'light.test2', + '2': 'light.test', + } - number = conf.entity_id_to_number('light.test') - assert number == '2' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test') + assert number == '2' + assert handle.write.call_count == 1 - number = conf.entity_id_to_number('light.test2') - assert number == '1' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test2') + assert number == '1' + assert handle.write.call_count == 1 - entity_id = conf.number_to_entity_id('1') - assert entity_id == 'light.test2' + entity_id = conf.number_to_entity_id('1') + assert entity_id == 'light.test2' def test_config_google_home_entity_id_to_number_altered(): """Test config adheres to the type.""" - conf = Config(Mock(), { + mock_hass = Mock() + mock_hass.config.path = MagicMock("path", return_value="test_path") + conf = Config(mock_hass, { 'type': 'google_home' }) @@ -46,29 +52,33 @@ def test_config_google_home_entity_id_to_number_altered(): handle = mop() with patch('homeassistant.util.json.open', mop, create=True): - number = conf.entity_id_to_number('light.test') - assert number == '22' - assert handle.write.call_count == 1 - assert json.loads(handle.write.mock_calls[0][1][0]) == { - '21': 'light.test2', - '22': 'light.test', - } + with patch('homeassistant.util.json.os.open', return_value=0): + with patch('homeassistant.util.json.os.replace'): + number = conf.entity_id_to_number('light.test') + assert number == '22' + assert handle.write.call_count == 1 + assert json.loads(handle.write.mock_calls[0][1][0]) == { + '21': 'light.test2', + '22': 'light.test', + } - number = conf.entity_id_to_number('light.test') - assert number == '22' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test') + assert number == '22' + assert handle.write.call_count == 1 - number = conf.entity_id_to_number('light.test2') - assert number == '21' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test2') + assert number == '21' + assert handle.write.call_count == 1 - entity_id = conf.number_to_entity_id('21') - assert entity_id == 'light.test2' + entity_id = conf.number_to_entity_id('21') + assert entity_id == 'light.test2' def test_config_google_home_entity_id_to_number_empty(): """Test config adheres to the type.""" - conf = Config(Mock(), { + mock_hass = Mock() + mock_hass.config.path = MagicMock("path", return_value="test_path") + conf = Config(mock_hass, { 'type': 'google_home' }) @@ -76,23 +86,25 @@ def test_config_google_home_entity_id_to_number_empty(): handle = mop() with patch('homeassistant.util.json.open', mop, create=True): - number = conf.entity_id_to_number('light.test') - assert number == '1' - assert handle.write.call_count == 1 - assert json.loads(handle.write.mock_calls[0][1][0]) == { - '1': 'light.test', - } + with patch('homeassistant.util.json.os.open', return_value=0): + with patch('homeassistant.util.json.os.replace'): + number = conf.entity_id_to_number('light.test') + assert number == '1' + assert handle.write.call_count == 1 + assert json.loads(handle.write.mock_calls[0][1][0]) == { + '1': 'light.test', + } - number = conf.entity_id_to_number('light.test') - assert number == '1' - assert handle.write.call_count == 1 + number = conf.entity_id_to_number('light.test') + assert number == '1' + assert handle.write.call_count == 1 - number = conf.entity_id_to_number('light.test2') - assert number == '2' - assert handle.write.call_count == 2 + number = conf.entity_id_to_number('light.test2') + assert number == '2' + assert handle.write.call_count == 2 - entity_id = conf.number_to_entity_id('2') - assert entity_id == 'light.test2' + entity_id = conf.number_to_entity_id('2') + assert entity_id == 'light.test2' def test_config_alexa_entity_id_to_number(): diff --git a/tests/components/fan/__init__.py b/tests/components/fan/__init__.py index 28ae7f4e249..e7cc83217ef 100644 --- a/tests/components/fan/__init__.py +++ b/tests/components/fan/__init__.py @@ -1,39 +1 @@ """Tests for fan platforms.""" - -import unittest - -from homeassistant.components.fan import FanEntity - - -class BaseFan(FanEntity): - """Implementation of the abstract FanEntity.""" - - def __init__(self): - """Initialize the fan.""" - pass - - -class TestFanEntity(unittest.TestCase): - """Test coverage for base fan entity class.""" - - def setUp(self): - """Set up test data.""" - self.fan = BaseFan() - - def tearDown(self): - """Tear down unit test data.""" - self.fan = None - - def test_fanentity(self): - """Test fan entity methods.""" - self.assertIsNone(self.fan.state) - self.assertEqual(0, len(self.fan.speed_list)) - self.assertEqual(0, self.fan.supported_features) - self.assertEqual({}, self.fan.state_attributes) - # Test set_speed not required - self.fan.set_speed() - self.fan.oscillate() - with self.assertRaises(NotImplementedError): - self.fan.turn_on() - with self.assertRaises(NotImplementedError): - self.fan.turn_off() diff --git a/tests/components/fan/common.py b/tests/components/fan/common.py new file mode 100644 index 00000000000..60e1cab1ac0 --- /dev/null +++ b/tests/components/fan/common.py @@ -0,0 +1,72 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.fan import ( + ATTR_DIRECTION, ATTR_SPEED, ATTR_OSCILLATING, DOMAIN, + SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, SERVICE_SET_SPEED) +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF) +from homeassistant.loader import bind_hass + + +@bind_hass +def turn_on(hass, entity_id: str = None, speed: str = None) -> None: + """Turn all or specified fan on.""" + data = { + key: value for key, value in [ + (ATTR_ENTITY_ID, entity_id), + (ATTR_SPEED, speed), + ] if value is not None + } + + hass.services.call(DOMAIN, SERVICE_TURN_ON, data) + + +@bind_hass +def turn_off(hass, entity_id: str = None) -> None: + """Turn all or specified fan off.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + + hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) + + +@bind_hass +def oscillate(hass, entity_id: str = None, + should_oscillate: bool = True) -> None: + """Set oscillation on all or specified fan.""" + data = { + key: value for key, value in [ + (ATTR_ENTITY_ID, entity_id), + (ATTR_OSCILLATING, should_oscillate), + ] if value is not None + } + + hass.services.call(DOMAIN, SERVICE_OSCILLATE, data) + + +@bind_hass +def set_speed(hass, entity_id: str = None, speed: str = None) -> None: + """Set speed for all or specified fan.""" + data = { + key: value for key, value in [ + (ATTR_ENTITY_ID, entity_id), + (ATTR_SPEED, speed), + ] if value is not None + } + + hass.services.call(DOMAIN, SERVICE_SET_SPEED, data) + + +@bind_hass +def set_direction(hass, entity_id: str = None, direction: str = None) -> None: + """Set direction for all or specified fan.""" + data = { + key: value for key, value in [ + (ATTR_ENTITY_ID, entity_id), + (ATTR_DIRECTION, direction), + ] if value is not None + } + + hass.services.call(DOMAIN, SERVICE_SET_DIRECTION, data) diff --git a/tests/components/fan/test_demo.py b/tests/components/fan/test_demo.py index 69680fb1cfd..48704ca4464 100644 --- a/tests/components/fan/test_demo.py +++ b/tests/components/fan/test_demo.py @@ -7,6 +7,7 @@ from homeassistant.components import fan from homeassistant.const import STATE_OFF, STATE_ON from tests.common import get_test_home_assistant +from tests.components.fan import common FAN_ENTITY_ID = 'fan.living_room_fan' @@ -34,11 +35,11 @@ class TestDemoFan(unittest.TestCase): """Test turning on the device.""" self.assertEqual(STATE_OFF, self.get_entity().state) - fan.turn_on(self.hass, FAN_ENTITY_ID) + common.turn_on(self.hass, FAN_ENTITY_ID) self.hass.block_till_done() self.assertNotEqual(STATE_OFF, self.get_entity().state) - fan.turn_on(self.hass, FAN_ENTITY_ID, fan.SPEED_HIGH) + common.turn_on(self.hass, FAN_ENTITY_ID, fan.SPEED_HIGH) self.hass.block_till_done() self.assertEqual(STATE_ON, self.get_entity().state) self.assertEqual(fan.SPEED_HIGH, @@ -48,11 +49,11 @@ class TestDemoFan(unittest.TestCase): """Test turning off the device.""" self.assertEqual(STATE_OFF, self.get_entity().state) - fan.turn_on(self.hass, FAN_ENTITY_ID) + common.turn_on(self.hass, FAN_ENTITY_ID) self.hass.block_till_done() self.assertNotEqual(STATE_OFF, self.get_entity().state) - fan.turn_off(self.hass, FAN_ENTITY_ID) + common.turn_off(self.hass, FAN_ENTITY_ID) self.hass.block_till_done() self.assertEqual(STATE_OFF, self.get_entity().state) @@ -60,11 +61,11 @@ class TestDemoFan(unittest.TestCase): """Test turning off all fans.""" self.assertEqual(STATE_OFF, self.get_entity().state) - fan.turn_on(self.hass, FAN_ENTITY_ID) + common.turn_on(self.hass, FAN_ENTITY_ID) self.hass.block_till_done() self.assertNotEqual(STATE_OFF, self.get_entity().state) - fan.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() self.assertEqual(STATE_OFF, self.get_entity().state) @@ -72,7 +73,7 @@ class TestDemoFan(unittest.TestCase): """Test setting the direction of the device.""" self.assertEqual(STATE_OFF, self.get_entity().state) - fan.set_direction(self.hass, FAN_ENTITY_ID, fan.DIRECTION_REVERSE) + common.set_direction(self.hass, FAN_ENTITY_ID, fan.DIRECTION_REVERSE) self.hass.block_till_done() self.assertEqual(fan.DIRECTION_REVERSE, self.get_entity().attributes.get('direction')) @@ -81,7 +82,7 @@ class TestDemoFan(unittest.TestCase): """Test setting the speed of the device.""" self.assertEqual(STATE_OFF, self.get_entity().state) - fan.set_speed(self.hass, FAN_ENTITY_ID, fan.SPEED_LOW) + common.set_speed(self.hass, FAN_ENTITY_ID, fan.SPEED_LOW) self.hass.block_till_done() self.assertEqual(fan.SPEED_LOW, self.get_entity().attributes.get('speed')) @@ -90,11 +91,11 @@ class TestDemoFan(unittest.TestCase): """Test oscillating the fan.""" self.assertFalse(self.get_entity().attributes.get('oscillating')) - fan.oscillate(self.hass, FAN_ENTITY_ID, True) + common.oscillate(self.hass, FAN_ENTITY_ID, True) self.hass.block_till_done() self.assertTrue(self.get_entity().attributes.get('oscillating')) - fan.oscillate(self.hass, FAN_ENTITY_ID, False) + common.oscillate(self.hass, FAN_ENTITY_ID, False) self.hass.block_till_done() self.assertFalse(self.get_entity().attributes.get('oscillating')) @@ -102,6 +103,6 @@ class TestDemoFan(unittest.TestCase): """Test is on service call.""" self.assertFalse(fan.is_on(self.hass, FAN_ENTITY_ID)) - fan.turn_on(self.hass, FAN_ENTITY_ID) + common.turn_on(self.hass, FAN_ENTITY_ID) self.hass.block_till_done() self.assertTrue(fan.is_on(self.hass, FAN_ENTITY_ID)) diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py new file mode 100644 index 00000000000..15f9a79d2d2 --- /dev/null +++ b/tests/components/fan/test_init.py @@ -0,0 +1,40 @@ +"""Tests for fan platforms.""" + +import unittest + +from homeassistant.components.fan import FanEntity + + +class BaseFan(FanEntity): + """Implementation of the abstract FanEntity.""" + + def __init__(self): + """Initialize the fan.""" + pass + + +class TestFanEntity(unittest.TestCase): + """Test coverage for base fan entity class.""" + + def setUp(self): + """Set up test data.""" + self.fan = BaseFan() + + def tearDown(self): + """Tear down unit test data.""" + self.fan = None + + def test_fanentity(self): + """Test fan entity methods.""" + self.assertEqual('on', self.fan.state) + self.assertEqual(0, len(self.fan.speed_list)) + self.assertEqual(0, self.fan.supported_features) + self.assertEqual({'speed_list': []}, self.fan.state_attributes) + # Test set_speed not required + self.fan.oscillate(True) + with self.assertRaises(NotImplementedError): + self.fan.set_speed('slow') + with self.assertRaises(NotImplementedError): + self.fan.turn_on() + with self.assertRaises(NotImplementedError): + self.fan.turn_off() diff --git a/tests/components/fan/test_mqtt.py b/tests/components/fan/test_mqtt.py index 7f69e56218b..7434e5aa1c9 100644 --- a/tests/components/fan/test_mqtt.py +++ b/tests/components/fan/test_mqtt.py @@ -1,12 +1,14 @@ """Test MQTT fans.""" import unittest -from homeassistant.setup import setup_component +from homeassistant.setup import setup_component, async_setup_component from homeassistant.components import fan +from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNAVAILABLE from tests.common import ( - mock_mqtt_component, fire_mqtt_message, get_test_home_assistant) + mock_mqtt_component, async_fire_mqtt_message, fire_mqtt_message, + get_test_home_assistant, async_mock_mqtt_component) class TestMqttFan(unittest.TestCase): @@ -102,3 +104,49 @@ class TestMqttFan(unittest.TestCase): state = self.hass.states.get('fan.test') self.assertNotEqual(STATE_UNAVAILABLE, state.state) + + +async def test_discovery_removal_fan(hass, mqtt_mock, caplog): + """Test removal of discovered fan.""" + await async_start(hass, 'homeassistant', {}) + data = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/fan/bla/config', + data) + await hass.async_block_till_done() + state = hass.states.get('fan.beer') + assert state is not None + assert state.name == 'Beer' + async_fire_mqtt_message(hass, 'homeassistant/fan/bla/config', + '') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.beer') + assert state is None + + +async def test_unique_id(hass): + """Test unique_id option only creates one fan per id.""" + await async_mock_mqtt_component(hass) + assert await async_setup_component(hass, fan.DOMAIN, { + fan.DOMAIN: [{ + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'command_topic': 'test-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }, { + 'platform': 'mqtt', + 'name': 'Test 2', + 'state_topic': 'test-topic', + 'command_topic': 'test-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }] + }) + + async_fire_mqtt_message(hass, 'test-topic', 'payload') + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids(fan.DOMAIN)) == 1 diff --git a/tests/components/fan/test_template.py b/tests/components/fan/test_template.py index e229083069d..09d3603e004 100644 --- a/tests/components/fan/test_template.py +++ b/tests/components/fan/test_template.py @@ -3,7 +3,6 @@ import logging from homeassistant.core import callback from homeassistant import setup -import homeassistant.components as components from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.components.fan import ( ATTR_SPEED, ATTR_OSCILLATING, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, @@ -11,6 +10,8 @@ from homeassistant.components.fan import ( from tests.common import ( get_test_home_assistant, assert_setup_component) +from tests.components.fan import common + _LOGGER = logging.getLogger(__name__) @@ -288,7 +289,7 @@ class TestTemplateFan: self._register_components() # Turn on fan - components.fan.turn_on(self.hass, _TEST_FAN) + common.turn_on(self.hass, _TEST_FAN) self.hass.block_till_done() # verify @@ -296,7 +297,7 @@ class TestTemplateFan: self._verify(STATE_ON, None, None, None) # Turn off fan - components.fan.turn_off(self.hass, _TEST_FAN) + common.turn_off(self.hass, _TEST_FAN) self.hass.block_till_done() # verify @@ -308,7 +309,7 @@ class TestTemplateFan: self._register_components() # Turn on fan with high speed - components.fan.turn_on(self.hass, _TEST_FAN, SPEED_HIGH) + common.turn_on(self.hass, _TEST_FAN, SPEED_HIGH) self.hass.block_till_done() # verify @@ -321,11 +322,11 @@ class TestTemplateFan: self._register_components() # Turn on fan - components.fan.turn_on(self.hass, _TEST_FAN) + common.turn_on(self.hass, _TEST_FAN) self.hass.block_till_done() # Set fan's speed to high - components.fan.set_speed(self.hass, _TEST_FAN, SPEED_HIGH) + common.set_speed(self.hass, _TEST_FAN, SPEED_HIGH) self.hass.block_till_done() # verify @@ -333,7 +334,7 @@ class TestTemplateFan: self._verify(STATE_ON, SPEED_HIGH, None, None) # Set fan's speed to medium - components.fan.set_speed(self.hass, _TEST_FAN, SPEED_MEDIUM) + common.set_speed(self.hass, _TEST_FAN, SPEED_MEDIUM) self.hass.block_till_done() # verify @@ -345,11 +346,11 @@ class TestTemplateFan: self._register_components() # Turn on fan - components.fan.turn_on(self.hass, _TEST_FAN) + common.turn_on(self.hass, _TEST_FAN) self.hass.block_till_done() # Set fan's speed to 'invalid' - components.fan.set_speed(self.hass, _TEST_FAN, 'invalid') + common.set_speed(self.hass, _TEST_FAN, 'invalid') self.hass.block_till_done() # verify speed is unchanged @@ -361,11 +362,11 @@ class TestTemplateFan: self._register_components() # Turn on fan - components.fan.turn_on(self.hass, _TEST_FAN) + common.turn_on(self.hass, _TEST_FAN) self.hass.block_till_done() # Set fan's speed to high - components.fan.set_speed(self.hass, _TEST_FAN, SPEED_HIGH) + common.set_speed(self.hass, _TEST_FAN, SPEED_HIGH) self.hass.block_till_done() # verify @@ -373,7 +374,7 @@ class TestTemplateFan: self._verify(STATE_ON, SPEED_HIGH, None, None) # Set fan's speed to 'invalid' - components.fan.set_speed(self.hass, _TEST_FAN, 'invalid') + common.set_speed(self.hass, _TEST_FAN, 'invalid') self.hass.block_till_done() # verify speed is unchanged @@ -385,11 +386,11 @@ class TestTemplateFan: self._register_components(['1', '2', '3']) # Turn on fan - components.fan.turn_on(self.hass, _TEST_FAN) + common.turn_on(self.hass, _TEST_FAN) self.hass.block_till_done() # Set fan's speed to '1' - components.fan.set_speed(self.hass, _TEST_FAN, '1') + common.set_speed(self.hass, _TEST_FAN, '1') self.hass.block_till_done() # verify @@ -397,7 +398,7 @@ class TestTemplateFan: self._verify(STATE_ON, '1', None, None) # Set fan's speed to 'medium' which is invalid - components.fan.set_speed(self.hass, _TEST_FAN, SPEED_MEDIUM) + common.set_speed(self.hass, _TEST_FAN, SPEED_MEDIUM) self.hass.block_till_done() # verify that speed is unchanged @@ -409,11 +410,11 @@ class TestTemplateFan: self._register_components() # Turn on fan - components.fan.turn_on(self.hass, _TEST_FAN) + common.turn_on(self.hass, _TEST_FAN) self.hass.block_till_done() # Set fan's osc to True - components.fan.oscillate(self.hass, _TEST_FAN, True) + common.oscillate(self.hass, _TEST_FAN, True) self.hass.block_till_done() # verify @@ -421,7 +422,7 @@ class TestTemplateFan: self._verify(STATE_ON, None, True, None) # Set fan's osc to False - components.fan.oscillate(self.hass, _TEST_FAN, False) + common.oscillate(self.hass, _TEST_FAN, False) self.hass.block_till_done() # verify @@ -433,11 +434,11 @@ class TestTemplateFan: self._register_components() # Turn on fan - components.fan.turn_on(self.hass, _TEST_FAN) + common.turn_on(self.hass, _TEST_FAN) self.hass.block_till_done() # Set fan's osc to 'invalid' - components.fan.oscillate(self.hass, _TEST_FAN, 'invalid') + common.oscillate(self.hass, _TEST_FAN, 'invalid') self.hass.block_till_done() # verify @@ -449,11 +450,11 @@ class TestTemplateFan: self._register_components() # Turn on fan - components.fan.turn_on(self.hass, _TEST_FAN) + common.turn_on(self.hass, _TEST_FAN) self.hass.block_till_done() # Set fan's osc to True - components.fan.oscillate(self.hass, _TEST_FAN, True) + common.oscillate(self.hass, _TEST_FAN, True) self.hass.block_till_done() # verify @@ -461,7 +462,7 @@ class TestTemplateFan: self._verify(STATE_ON, None, True, None) # Set fan's osc to False - components.fan.oscillate(self.hass, _TEST_FAN, None) + common.oscillate(self.hass, _TEST_FAN, None) self.hass.block_till_done() # verify osc is unchanged @@ -473,11 +474,11 @@ class TestTemplateFan: self._register_components() # Turn on fan - components.fan.turn_on(self.hass, _TEST_FAN) + common.turn_on(self.hass, _TEST_FAN) self.hass.block_till_done() # Set fan's direction to forward - components.fan.set_direction(self.hass, _TEST_FAN, DIRECTION_FORWARD) + common.set_direction(self.hass, _TEST_FAN, DIRECTION_FORWARD) self.hass.block_till_done() # verify @@ -486,7 +487,7 @@ class TestTemplateFan: self._verify(STATE_ON, None, None, DIRECTION_FORWARD) # Set fan's direction to reverse - components.fan.set_direction(self.hass, _TEST_FAN, DIRECTION_REVERSE) + common.set_direction(self.hass, _TEST_FAN, DIRECTION_REVERSE) self.hass.block_till_done() # verify @@ -499,11 +500,11 @@ class TestTemplateFan: self._register_components() # Turn on fan - components.fan.turn_on(self.hass, _TEST_FAN) + common.turn_on(self.hass, _TEST_FAN) self.hass.block_till_done() # Set fan's direction to 'invalid' - components.fan.set_direction(self.hass, _TEST_FAN, 'invalid') + common.set_direction(self.hass, _TEST_FAN, 'invalid') self.hass.block_till_done() # verify direction is unchanged @@ -515,11 +516,11 @@ class TestTemplateFan: self._register_components() # Turn on fan - components.fan.turn_on(self.hass, _TEST_FAN) + common.turn_on(self.hass, _TEST_FAN) self.hass.block_till_done() # Set fan's direction to forward - components.fan.set_direction(self.hass, _TEST_FAN, DIRECTION_FORWARD) + common.set_direction(self.hass, _TEST_FAN, DIRECTION_FORWARD) self.hass.block_till_done() # verify @@ -528,7 +529,7 @@ class TestTemplateFan: self._verify(STATE_ON, None, None, DIRECTION_FORWARD) # Set fan's direction to 'invalid' - components.fan.set_direction(self.hass, _TEST_FAN, 'invalid') + common.set_direction(self.hass, _TEST_FAN, 'invalid') self.hass.block_till_done() # verify direction is unchanged diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 17bf3d953ef..2e78e0441a3 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -5,12 +5,11 @@ 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 homeassistant.components.websocket_api.const import TYPE_RESULT from tests.common import mock_coro @@ -214,7 +213,7 @@ async def test_missing_themes(hass, hass_ws_client): msg = await client.receive_json() assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT + assert msg['type'] == TYPE_RESULT assert msg['success'] assert msg['result']['default_theme'] == 'default' assert msg['result']['themes'] == {} @@ -253,7 +252,7 @@ async def test_get_panels(hass, hass_ws_client): msg = await client.receive_json() assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT + assert msg['type'] == TYPE_RESULT assert msg['success'] assert msg['result']['map']['component_name'] == 'map' assert msg['result']['map']['url_path'] == 'map' @@ -276,68 +275,11 @@ async def test_get_translations(hass, hass_ws_client): msg = await client.receive_json() assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT + assert msg['type'] == 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' - - async def test_auth_load(mock_http_client): """Test auth component loaded by default.""" resp = await mock_http_client.get('/auth/providers') @@ -352,10 +294,18 @@ async def test_onboarding_load(mock_http_client): async def test_auth_authorize(mock_http_client): """Test the authorize endpoint works.""" - resp = await mock_http_client.get('/auth/authorize?hello=world') - assert resp.url.query_string == 'hello=world' - assert resp.url.path == '/frontend_es5/authorize.html' + resp = await mock_http_client.get( + '/auth/authorize?response_type=code&client_id=https://localhost/&' + 'redirect_uri=https://localhost/&state=123%23456') - resp = await mock_http_client.get('/auth/authorize?latest&hello=world') - assert resp.url.query_string == 'latest&hello=world' - assert resp.url.path == '/frontend_latest/authorize.html' + assert str(resp.url.relative()) == ( + '/frontend_es5/authorize.html?response_type=code&client_id=' + 'https://localhost/&redirect_uri=https://localhost/&state=123%23456') + + resp = await mock_http_client.get( + '/auth/authorize?latest&response_type=code&client_id=' + 'https://localhost/&redirect_uri=https://localhost/&state=123%23456') + + assert str(resp.url.relative()) == ( + '/frontend_latest/authorize.html?latest&response_type=code&client_id=' + 'https://localhost/&redirect_uri=https://localhost/&state=123%23456') diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index d9682940bdc..2ebfa5cc9ed 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -23,7 +23,12 @@ HA_HEADERS = { PROJECT_ID = 'hasstest-1234' CLIENT_ID = 'helloworld' ACCESS_TOKEN = 'superdoublesecret' -AUTH_HEADER = {AUTHORIZATION: 'Bearer {}'.format(ACCESS_TOKEN)} + + +@pytest.fixture +def auth_header(hass_access_token): + """Generate an HTTP header with bearer token authorization.""" + return {AUTHORIZATION: 'Bearer {}'.format(hass_access_token)} @pytest.fixture @@ -33,8 +38,6 @@ def assistant_client(loop, hass, aiohttp_client): setup.async_setup_component(hass, 'google_assistant', { 'google_assistant': { 'project_id': PROJECT_ID, - 'client_id': CLIENT_ID, - 'access_token': ACCESS_TOKEN, 'entity_config': { 'light.ceiling_lights': { 'aliases': ['top lights', 'ceiling lights'], @@ -97,31 +100,14 @@ def hass_fixture(loop, hass): @asyncio.coroutine -def test_auth(assistant_client): - """Test the auth process.""" - result = yield from assistant_client.get( - ga.const.GOOGLE_ASSISTANT_API_ENDPOINT + '/auth', - params={ - 'redirect_uri': - 'http://testurl/r/{}'.format(PROJECT_ID), - 'client_id': CLIENT_ID, - 'state': 'random1234', - }, - allow_redirects=False) - assert result.status == 301 - loc = result.headers.get('Location') - assert ACCESS_TOKEN in loc - - -@asyncio.coroutine -def test_sync_request(hass_fixture, assistant_client): +def test_sync_request(hass_fixture, assistant_client, auth_header): """Test a sync request.""" reqid = '5711642932632160983' data = {'requestId': reqid, 'inputs': [{'intent': 'action.devices.SYNC'}]} result = yield from assistant_client.post( ga.const.GOOGLE_ASSISTANT_API_ENDPOINT, data=json.dumps(data), - headers=AUTH_HEADER) + headers=auth_header) assert result.status == 200 body = yield from result.json() assert body.get('requestId') == reqid @@ -141,7 +127,7 @@ def test_sync_request(hass_fixture, assistant_client): @asyncio.coroutine -def test_query_request(hass_fixture, assistant_client): +def test_query_request(hass_fixture, assistant_client, auth_header): """Test a query request.""" reqid = '5711642932632160984' data = { @@ -165,7 +151,7 @@ def test_query_request(hass_fixture, assistant_client): result = yield from assistant_client.post( ga.const.GOOGLE_ASSISTANT_API_ENDPOINT, data=json.dumps(data), - headers=AUTH_HEADER) + headers=auth_header) assert result.status == 200 body = yield from result.json() assert body.get('requestId') == reqid @@ -180,7 +166,7 @@ def test_query_request(hass_fixture, assistant_client): @asyncio.coroutine -def test_query_climate_request(hass_fixture, assistant_client): +def test_query_climate_request(hass_fixture, assistant_client, auth_header): """Test a query request.""" reqid = '5711642932632160984' data = { @@ -200,7 +186,7 @@ def test_query_climate_request(hass_fixture, assistant_client): result = yield from assistant_client.post( ga.const.GOOGLE_ASSISTANT_API_ENDPOINT, data=json.dumps(data), - headers=AUTH_HEADER) + headers=auth_header) assert result.status == 200 body = yield from result.json() assert body.get('requestId') == reqid @@ -229,7 +215,7 @@ def test_query_climate_request(hass_fixture, assistant_client): @asyncio.coroutine -def test_query_climate_request_f(hass_fixture, assistant_client): +def test_query_climate_request_f(hass_fixture, assistant_client, auth_header): """Test a query request.""" # Mock demo devices as fahrenheit to see if we convert to celsius hass_fixture.config.units.temperature_unit = const.TEMP_FAHRENHEIT @@ -256,7 +242,7 @@ def test_query_climate_request_f(hass_fixture, assistant_client): result = yield from assistant_client.post( ga.const.GOOGLE_ASSISTANT_API_ENDPOINT, data=json.dumps(data), - headers=AUTH_HEADER) + headers=auth_header) assert result.status == 200 body = yield from result.json() assert body.get('requestId') == reqid @@ -286,7 +272,7 @@ def test_query_climate_request_f(hass_fixture, assistant_client): @asyncio.coroutine -def test_execute_request(hass_fixture, assistant_client): +def test_execute_request(hass_fixture, assistant_client, auth_header): """Test an execute request.""" reqid = '5711642932632160985' data = { @@ -358,7 +344,7 @@ def test_execute_request(hass_fixture, assistant_client): result = yield from assistant_client.post( ga.const.GOOGLE_ASSISTANT_API_ENDPOINT, data=json.dumps(data), - headers=AUTH_HEADER) + headers=auth_header) assert result.status == 200 body = yield from result.json() assert body.get('requestId') == reqid diff --git a/tests/components/google_assistant/test_init.py b/tests/components/google_assistant/test_init.py index 9ced9fc329d..3f6a799b423 100644 --- a/tests/components/google_assistant/test_init.py +++ b/tests/components/google_assistant/test_init.py @@ -5,7 +5,6 @@ from homeassistant.setup import async_setup_component from homeassistant.components import google_assistant as ga GA_API_KEY = "Agdgjsj399sdfkosd932ksd" -GA_AGENT_USER_ID = "testid" @asyncio.coroutine @@ -17,9 +16,6 @@ def test_request_sync_service(aioclient_mock, hass): yield from async_setup_component(hass, 'google_assistant', { 'google_assistant': { 'project_id': 'test_project', - 'client_id': 'r7328kwdsdfsdf03223409', - 'access_token': '8wdsfjsf932492342349234', - 'agent_user_id': GA_AGENT_USER_ID, 'api_key': GA_API_KEY }}) diff --git a/tests/components/group/common.py b/tests/components/group/common.py new file mode 100644 index 00000000000..380586e3854 --- /dev/null +++ b/tests/components/group/common.py @@ -0,0 +1,70 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.group import ( + ATTR_ADD_ENTITIES, ATTR_CONTROL, ATTR_ENTITIES, ATTR_OBJECT_ID, ATTR_VIEW, + ATTR_VISIBLE, DOMAIN, SERVICE_REMOVE, SERVICE_SET, SERVICE_SET_VISIBILITY) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_ICON, ATTR_NAME, SERVICE_RELOAD) +from homeassistant.core import callback +from homeassistant.loader import bind_hass + + +@bind_hass +def reload(hass): + """Reload the automation from config.""" + hass.add_job(async_reload, hass) + + +@callback +@bind_hass +def async_reload(hass): + """Reload the automation from config.""" + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_RELOAD)) + + +@bind_hass +def set_group(hass, object_id, name=None, entity_ids=None, visible=None, + icon=None, view=None, control=None, add=None): + """Create/Update a group.""" + hass.add_job( + async_set_group, hass, object_id, name, entity_ids, visible, icon, + view, control, add) + + +@callback +@bind_hass +def async_set_group(hass, object_id, name=None, entity_ids=None, visible=None, + icon=None, view=None, control=None, add=None): + """Create/Update a group.""" + data = { + key: value for key, value in [ + (ATTR_OBJECT_ID, object_id), + (ATTR_NAME, name), + (ATTR_ENTITIES, entity_ids), + (ATTR_VISIBLE, visible), + (ATTR_ICON, icon), + (ATTR_VIEW, view), + (ATTR_CONTROL, control), + (ATTR_ADD_ENTITIES, add), + ] if value is not None + } + + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_SET, data)) + + +@callback +@bind_hass +def async_remove(hass, object_id): + """Remove a user group.""" + data = {ATTR_OBJECT_ID: object_id} + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_REMOVE, data)) + + +@bind_hass +def set_visibility(hass, entity_id=None, visible=True): + """Hide or shows a group.""" + data = {ATTR_ENTITY_ID: entity_id, ATTR_VISIBLE: visible} + hass.services.call(DOMAIN, SERVICE_SET_VISIBILITY, data) diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 47101dd415a..55c8a7778cb 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -12,6 +12,7 @@ from homeassistant.const import ( import homeassistant.components.group as group from tests.common import get_test_home_assistant, assert_setup_component +from tests.components.group import common class TestComponentsGroup(unittest.TestCase): @@ -367,7 +368,7 @@ class TestComponentsGroup(unittest.TestCase): }}}): with patch('homeassistant.config.find_config_file', return_value=''): - group.reload(self.hass) + common.reload(self.hass) self.hass.block_till_done() assert sorted(self.hass.states.entity_ids()) == \ @@ -385,13 +386,13 @@ class TestComponentsGroup(unittest.TestCase): group_entity_id = group.ENTITY_ID_FORMAT.format('test_group') # Hide the group - group.set_visibility(self.hass, group_entity_id, False) + common.set_visibility(self.hass, group_entity_id, False) self.hass.block_till_done() group_state = self.hass.states.get(group_entity_id) self.assertTrue(group_state.attributes.get(ATTR_HIDDEN)) # Show it again - group.set_visibility(self.hass, group_entity_id, True) + common.set_visibility(self.hass, group_entity_id, True) self.hass.block_till_done() group_state = self.hass.states.get(group_entity_id) self.assertIsNone(group_state.attributes.get(ATTR_HIDDEN)) @@ -408,7 +409,7 @@ class TestComponentsGroup(unittest.TestCase): # The old way would create a new group modify_group1 because # internally it didn't know anything about those created in the config - group.set_group(self.hass, 'modify_group', icon="mdi:play") + common.set_group(self.hass, 'modify_group', icon="mdi:play") self.hass.block_till_done() group_state = self.hass.states.get( @@ -441,7 +442,7 @@ def test_service_group_set_group_remove_group(hass): 'group': {} }) - group.async_set_group(hass, 'user_test_group', name="Test") + common.async_set_group(hass, 'user_test_group', name="Test") yield from hass.async_block_till_done() group_state = hass.states.get('group.user_test_group') @@ -449,7 +450,7 @@ def test_service_group_set_group_remove_group(hass): assert group_state.attributes[group.ATTR_AUTO] assert group_state.attributes['friendly_name'] == "Test" - group.async_set_group( + common.async_set_group( hass, 'user_test_group', view=True, visible=False, entity_ids=['test.entity_bla1']) yield from hass.async_block_till_done() @@ -462,7 +463,7 @@ def test_service_group_set_group_remove_group(hass): assert group_state.attributes['friendly_name'] == "Test" assert list(group_state.attributes['entity_id']) == ['test.entity_bla1'] - group.async_set_group( + common.async_set_group( hass, 'user_test_group', icon="mdi:camera", name="Test2", control="hidden", add=['test.entity_id2']) yield from hass.async_block_till_done() @@ -478,7 +479,7 @@ def test_service_group_set_group_remove_group(hass): assert sorted(list(group_state.attributes['entity_id'])) == sorted([ 'test.entity_bla1', 'test.entity_id2']) - group.async_remove(hass, 'user_test_group') + common.async_remove(hass, 'user_test_group') yield from hass.async_block_till_done() group_state = hass.states.get('group.user_test_group') diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index 9f20efc08a5..f9ad1c578de 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -4,8 +4,9 @@ from unittest.mock import patch, Mock import pytest +from homeassistant.core import CoreState from homeassistant.setup import async_setup_component -from homeassistant.components.hassio.handler import HassIO +from homeassistant.components.hassio.handler import HassIO, HassioAPIError from tests.common import mock_coro from . import API_PASSWORD, HASSIO_TOKEN @@ -21,7 +22,7 @@ def hassio_env(): patch.dict(os.environ, {'HASSIO_TOKEN': "123456"}), \ patch('homeassistant.components.hassio.HassIO.' 'get_homeassistant_info', - Mock(return_value=mock_coro(None))): + Mock(side_effect=HassioAPIError())): yield @@ -32,7 +33,8 @@ def hassio_client(hassio_env, hass, aiohttp_client): Mock(return_value=mock_coro({"result": "ok"}))), \ patch('homeassistant.components.hassio.HassIO.' 'get_homeassistant_info', - Mock(return_value=mock_coro(None))): + Mock(side_effect=HassioAPIError())): + hass.state = CoreState.starting hass.loop.run_until_complete(async_setup_component(hass, 'hassio', { 'http': { 'api_password': API_PASSWORD diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py new file mode 100644 index 00000000000..fdf3230dedc --- /dev/null +++ b/tests/components/hassio/test_auth.py @@ -0,0 +1,123 @@ +"""The tests for the hassio component.""" +from unittest.mock import patch, Mock + +from homeassistant.const import HTTP_HEADER_HA_AUTH +from homeassistant.exceptions import HomeAssistantError + +from tests.common import mock_coro, register_auth_provider +from . import API_PASSWORD + + +async def test_login_success(hass, hassio_client): + """Test no auth needed for .""" + await register_auth_provider(hass, {'type': 'homeassistant'}) + + with patch('homeassistant.auth.providers.homeassistant.' + 'HassAuthProvider.async_validate_login', + Mock(return_value=mock_coro())) as mock_login: + resp = await hassio_client.post( + '/api/hassio_auth', + json={ + "username": "test", + "password": "123456", + "addon": "samba", + }, + headers={ + HTTP_HEADER_HA_AUTH: API_PASSWORD + } + ) + + # Check we got right response + assert resp.status == 200 + mock_login.assert_called_with("test", "123456") + + +async def test_login_error(hass, hassio_client): + """Test no auth needed for error.""" + await register_auth_provider(hass, {'type': 'homeassistant'}) + + with patch('homeassistant.auth.providers.homeassistant.' + 'HassAuthProvider.async_validate_login', + Mock(side_effect=HomeAssistantError())) as mock_login: + resp = await hassio_client.post( + '/api/hassio_auth', + json={ + "username": "test", + "password": "123456", + "addon": "samba", + }, + headers={ + HTTP_HEADER_HA_AUTH: API_PASSWORD + } + ) + + # Check we got right response + assert resp.status == 403 + mock_login.assert_called_with("test", "123456") + + +async def test_login_no_data(hass, hassio_client): + """Test auth with no data -> error.""" + await register_auth_provider(hass, {'type': 'homeassistant'}) + + with patch('homeassistant.auth.providers.homeassistant.' + 'HassAuthProvider.async_validate_login', + Mock(side_effect=HomeAssistantError())) as mock_login: + resp = await hassio_client.post( + '/api/hassio_auth', + headers={ + HTTP_HEADER_HA_AUTH: API_PASSWORD + } + ) + + # Check we got right response + assert resp.status == 400 + assert not mock_login.called + + +async def test_login_no_username(hass, hassio_client): + """Test auth with no username in data -> error.""" + await register_auth_provider(hass, {'type': 'homeassistant'}) + + with patch('homeassistant.auth.providers.homeassistant.' + 'HassAuthProvider.async_validate_login', + Mock(side_effect=HomeAssistantError())) as mock_login: + resp = await hassio_client.post( + '/api/hassio_auth', + json={ + "password": "123456", + "addon": "samba", + }, + headers={ + HTTP_HEADER_HA_AUTH: API_PASSWORD + } + ) + + # Check we got right response + assert resp.status == 400 + assert not mock_login.called + + +async def test_login_success_extra(hass, hassio_client): + """Test auth with extra data.""" + await register_auth_provider(hass, {'type': 'homeassistant'}) + + with patch('homeassistant.auth.providers.homeassistant.' + 'HassAuthProvider.async_validate_login', + Mock(return_value=mock_coro())) as mock_login: + resp = await hassio_client.post( + '/api/hassio_auth', + json={ + "username": "test", + "password": "123456", + "addon": "samba", + "path": "/share", + }, + headers={ + HTTP_HEADER_HA_AUTH: API_PASSWORD + } + ) + + # Check we got right response + assert resp.status == 200 + mock_login.assert_called_with("test", "123456") diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py new file mode 100644 index 00000000000..c8926a1cd18 --- /dev/null +++ b/tests/components/hassio/test_discovery.py @@ -0,0 +1,141 @@ +"""Test config flow.""" +from unittest.mock import patch, Mock + +from homeassistant.setup import async_setup_component +from homeassistant.components.hassio.handler import HassioAPIError +from homeassistant.const import EVENT_HOMEASSISTANT_START, HTTP_HEADER_HA_AUTH + +from tests.common import mock_coro +from . import API_PASSWORD + + +async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client): + """Test startup and discovery after event.""" + aioclient_mock.get( + "http://127.0.0.1/discovery", json={ + 'result': 'ok', 'data': {'discovery': [ + { + "service": "mqtt", "uuid": "test", + "addon": "mosquitto", "config": + { + 'broker': 'mock-broker', + 'port': 1883, + 'username': 'mock-user', + 'password': 'mock-pass', + 'protocol': '3.1.1' + } + } + ]}}) + aioclient_mock.get( + "http://127.0.0.1/addons/mosquitto/info", json={ + 'result': 'ok', 'data': {'name': "Mosquitto Test"} + }) + + assert aioclient_mock.call_count == 0 + + with patch('homeassistant.components.mqtt.' + 'config_flow.FlowHandler.async_step_hassio', + Mock(return_value=mock_coro({"type": "abort"}))) as mock_mqtt: + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + assert aioclient_mock.call_count == 2 + assert mock_mqtt.called + mock_mqtt.assert_called_with({ + 'broker': 'mock-broker', 'port': 1883, 'username': 'mock-user', + 'password': 'mock-pass', 'protocol': '3.1.1', + 'addon': 'Mosquitto Test', + }) + + +async def test_hassio_discovery_startup_done(hass, aioclient_mock, + hassio_client): + """Test startup and discovery with hass discovery.""" + aioclient_mock.get( + "http://127.0.0.1/discovery", json={ + 'result': 'ok', 'data': {'discovery': [ + { + "service": "mqtt", "uuid": "test", + "addon": "mosquitto", "config": + { + 'broker': 'mock-broker', + 'port': 1883, + 'username': 'mock-user', + 'password': 'mock-pass', + 'protocol': '3.1.1' + } + } + ]}}) + aioclient_mock.get( + "http://127.0.0.1/addons/mosquitto/info", json={ + 'result': 'ok', 'data': {'name': "Mosquitto Test"} + }) + + with patch('homeassistant.components.hassio.HassIO.update_hass_api', + Mock(return_value=mock_coro({"result": "ok"}))), \ + patch('homeassistant.components.hassio.HassIO.' + 'get_homeassistant_info', + Mock(side_effect=HassioAPIError())), \ + patch('homeassistant.components.mqtt.' + 'config_flow.FlowHandler.async_step_hassio', + Mock(return_value=mock_coro({"type": "abort"})) + ) as mock_mqtt: + await hass.async_start() + await async_setup_component(hass, 'hassio', { + 'http': { + 'api_password': API_PASSWORD + } + }) + await hass.async_block_till_done() + + assert aioclient_mock.call_count == 2 + assert mock_mqtt.called + mock_mqtt.assert_called_with({ + 'broker': 'mock-broker', 'port': 1883, 'username': 'mock-user', + 'password': 'mock-pass', 'protocol': '3.1.1', + 'addon': 'Mosquitto Test', + }) + + +async def test_hassio_discovery_webhook(hass, aioclient_mock, hassio_client): + """Test discovery webhook.""" + aioclient_mock.get( + "http://127.0.0.1/discovery/testuuid", json={ + 'result': 'ok', 'data': + { + "service": "mqtt", "uuid": "test", + "addon": "mosquitto", "config": + { + 'broker': 'mock-broker', + 'port': 1883, + 'username': 'mock-user', + 'password': 'mock-pass', + 'protocol': '3.1.1' + } + } + }) + aioclient_mock.get( + "http://127.0.0.1/addons/mosquitto/info", json={ + 'result': 'ok', 'data': {'name': "Mosquitto Test"} + }) + + with patch('homeassistant.components.mqtt.' + 'config_flow.FlowHandler.async_step_hassio', + Mock(return_value=mock_coro({"type": "abort"}))) as mock_mqtt: + resp = await hassio_client.post( + '/api/hassio_push/discovery/testuuid', headers={ + HTTP_HEADER_HA_AUTH: API_PASSWORD + }, json={ + "addon": "mosquitto", "service": "mqtt", "uuid": "testuuid" + } + ) + await hass.async_block_till_done() + + assert resp.status == 200 + assert aioclient_mock.call_count == 2 + assert mock_mqtt.called + mock_mqtt.assert_called_with({ + 'broker': 'mock-broker', 'port': 1883, 'username': 'mock-user', + 'password': 'mock-pass', 'protocol': '3.1.1', + 'addon': 'Mosquitto Test', + }) diff --git a/tests/components/hassio/test_handler.py b/tests/components/hassio/test_handler.py index 78745489a78..db3917a2201 100644 --- a/tests/components/hassio/test_handler.py +++ b/tests/components/hassio/test_handler.py @@ -1,90 +1,118 @@ """The tests for the hassio component.""" -import asyncio import aiohttp +import pytest + +from homeassistant.components.hassio.handler import HassioAPIError -@asyncio.coroutine -def test_api_ping(hassio_handler, aioclient_mock): +async def test_api_ping(hassio_handler, aioclient_mock): """Test setup with API ping.""" aioclient_mock.get( "http://127.0.0.1/supervisor/ping", json={'result': 'ok'}) - assert (yield from hassio_handler.is_connected()) + assert (await hassio_handler.is_connected()) assert aioclient_mock.call_count == 1 -@asyncio.coroutine -def test_api_ping_error(hassio_handler, aioclient_mock): +async def test_api_ping_error(hassio_handler, aioclient_mock): """Test setup with API ping error.""" aioclient_mock.get( "http://127.0.0.1/supervisor/ping", json={'result': 'error'}) - assert not (yield from hassio_handler.is_connected()) + assert not (await hassio_handler.is_connected()) assert aioclient_mock.call_count == 1 -@asyncio.coroutine -def test_api_ping_exeption(hassio_handler, aioclient_mock): +async def test_api_ping_exeption(hassio_handler, aioclient_mock): """Test setup with API ping exception.""" aioclient_mock.get( "http://127.0.0.1/supervisor/ping", exc=aiohttp.ClientError()) - assert not (yield from hassio_handler.is_connected()) + assert not (await hassio_handler.is_connected()) assert aioclient_mock.call_count == 1 -@asyncio.coroutine -def test_api_homeassistant_info(hassio_handler, aioclient_mock): +async def test_api_homeassistant_info(hassio_handler, aioclient_mock): """Test setup with API homeassistant info.""" aioclient_mock.get( "http://127.0.0.1/homeassistant/info", json={ 'result': 'ok', 'data': {'last_version': '10.0'}}) - data = yield from hassio_handler.get_homeassistant_info() + data = await hassio_handler.get_homeassistant_info() assert aioclient_mock.call_count == 1 assert data['last_version'] == "10.0" -@asyncio.coroutine -def test_api_homeassistant_info_error(hassio_handler, aioclient_mock): +async def test_api_homeassistant_info_error(hassio_handler, aioclient_mock): """Test setup with API homeassistant info error.""" aioclient_mock.get( "http://127.0.0.1/homeassistant/info", json={ 'result': 'error', 'message': None}) - data = yield from hassio_handler.get_homeassistant_info() + with pytest.raises(HassioAPIError): + await hassio_handler.get_homeassistant_info() + assert aioclient_mock.call_count == 1 - assert data is None -@asyncio.coroutine -def test_api_homeassistant_stop(hassio_handler, aioclient_mock): +async def test_api_homeassistant_stop(hassio_handler, aioclient_mock): """Test setup with API HomeAssistant stop.""" aioclient_mock.post( "http://127.0.0.1/homeassistant/stop", json={'result': 'ok'}) - assert (yield from hassio_handler.stop_homeassistant()) + assert (await hassio_handler.stop_homeassistant()) assert aioclient_mock.call_count == 1 -@asyncio.coroutine -def test_api_homeassistant_restart(hassio_handler, aioclient_mock): +async def test_api_homeassistant_restart(hassio_handler, aioclient_mock): """Test setup with API HomeAssistant restart.""" aioclient_mock.post( "http://127.0.0.1/homeassistant/restart", json={'result': 'ok'}) - assert (yield from hassio_handler.restart_homeassistant()) + assert (await hassio_handler.restart_homeassistant()) assert aioclient_mock.call_count == 1 -@asyncio.coroutine -def test_api_homeassistant_config(hassio_handler, aioclient_mock): - """Test setup with API HomeAssistant restart.""" +async def test_api_homeassistant_config(hassio_handler, aioclient_mock): + """Test setup with API HomeAssistant config.""" aioclient_mock.post( "http://127.0.0.1/homeassistant/check", json={ 'result': 'ok', 'data': {'test': 'bla'}}) - data = yield from hassio_handler.check_homeassistant_config() + data = await hassio_handler.check_homeassistant_config() assert data['data']['test'] == 'bla' assert aioclient_mock.call_count == 1 + + +async def test_api_addon_info(hassio_handler, aioclient_mock): + """Test setup with API Add-on info.""" + aioclient_mock.get( + "http://127.0.0.1/addons/test/info", json={ + 'result': 'ok', 'data': {'name': 'bla'}}) + + data = await hassio_handler.get_addon_info("test") + assert data['name'] == 'bla' + assert aioclient_mock.call_count == 1 + + +async def test_api_discovery_message(hassio_handler, aioclient_mock): + """Test setup with API discovery message.""" + aioclient_mock.get( + "http://127.0.0.1/discovery/test", json={ + 'result': 'ok', 'data': {"service": "mqtt"}}) + + data = await hassio_handler.get_discovery_message("test") + assert data['service'] == "mqtt" + assert aioclient_mock.call_count == 1 + + +async def test_api_retrieve_discovery(hassio_handler, aioclient_mock): + """Test setup with API discovery message.""" + aioclient_mock.get( + "http://127.0.0.1/discovery", json={ + 'result': 'ok', 'data': {'discovery': [{"service": "mqtt"}]}}) + + data = await hassio_handler.retrieve_discovery_messages() + assert data['discovery'][-1]['service'] == "mqtt" + assert aioclient_mock.call_count == 1 diff --git a/tests/components/homekit/__init__.py b/tests/components/homekit/__init__.py new file mode 100644 index 00000000000..1734367bcdb --- /dev/null +++ b/tests/components/homekit/__init__.py @@ -0,0 +1 @@ +"""Tests for the HomeKit component.""" diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 5b76618d460..d5552cce82c 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -9,7 +9,8 @@ import homeassistant.components.climate as climate import homeassistant.components.media_player as media_player from homeassistant.components.homekit import get_accessory, TYPES from homeassistant.components.homekit.const import ( - CONF_FEATURE_LIST, FEATURE_ON_OFF, TYPE_OUTLET, TYPE_SWITCH) + CONF_FEATURE_LIST, FEATURE_ON_OFF, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, + TYPE_SPRINKLER, TYPE_SWITCH, TYPE_VALVE) from homeassistant.const import ( ATTR_CODE, ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_TYPE, TEMP_CELSIUS, @@ -140,6 +141,10 @@ def test_type_sensors(type_name, entity_id, state, attrs): ('Switch', 'script.test', 'on', {}, {}), ('Switch', 'switch.test', 'on', {}, {}), ('Switch', 'switch.test', 'on', {}, {CONF_TYPE: TYPE_SWITCH}), + ('Valve', 'switch.test', 'on', {}, {CONF_TYPE: TYPE_FAUCET}), + ('Valve', 'switch.test', 'on', {}, {CONF_TYPE: TYPE_VALVE}), + ('Valve', 'switch.test', 'on', {}, {CONF_TYPE: TYPE_SHOWER}), + ('Valve', 'switch.test', 'on', {}, {CONF_TYPE: TYPE_SPRINKLER}), ]) def test_type_switches(type_name, entity_id, state, attrs, config): """Test if switch types are associated correctly.""" diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index f8afb4a49ab..a831a7e9e5d 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -5,8 +5,8 @@ import pytest from homeassistant import setup from homeassistant.components.homekit import ( - generate_aid, HomeKit, STATUS_READY, STATUS_RUNNING, - STATUS_STOPPED, STATUS_WAIT) + generate_aid, HomeKit, MAX_DEVICES, STATUS_READY, + STATUS_RUNNING, STATUS_STOPPED, STATUS_WAIT) from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( CONF_AUTO_START, BRIDGE_NAME, DEFAULT_PORT, DOMAIN, HOMEKIT_FILE, @@ -173,7 +173,8 @@ async def test_homekit_start(hass, hk_driver, debounce_patcher): """Test HomeKit start method.""" pin = b'123-45-678' homekit = HomeKit(hass, None, None, None, {}, {'cover.demo': {}}) - homekit.bridge = 'bridge' + homekit.bridge = Mock() + homekit.bridge.accessories = [] homekit.driver = hk_driver hass.states.async_set('light.demo', 'on') @@ -190,7 +191,7 @@ async def test_homekit_start(hass, hk_driver, debounce_patcher): mock_add_acc.assert_called_with(state) mock_setup_msg.assert_called_with(hass, pin) - hk_driver_add_acc.assert_called_with('bridge') + hk_driver_add_acc.assert_called_with(homekit.bridge) assert hk_driver_start.called assert homekit.status == STATUS_RUNNING @@ -217,3 +218,18 @@ async def test_homekit_stop(hass): homekit.status = STATUS_RUNNING await hass.async_add_job(homekit.stop) assert homekit.driver.stop.called is True + + +async def test_homekit_too_many_accessories(hass, hk_driver): + """Test adding too many accessories to HomeKit.""" + homekit = HomeKit(hass, None, None, None, None, None) + homekit.bridge = Mock() + homekit.bridge.accessories = range(MAX_DEVICES + 1) + homekit.driver = hk_driver + + with patch('pyhap.accessory_driver.AccessoryDriver.start'), \ + patch('pyhap.accessory_driver.AccessoryDriver.add_accessory'), \ + patch('homeassistant.components.homekit._LOGGER.warning') \ + as mock_warn: + await hass.async_add_job(homekit.start) + assert mock_warn.called is True diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index c2b80226508..bc44a93884a 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -1,8 +1,11 @@ """Test different accessory types: Switches.""" import pytest -from homeassistant.components.homekit.type_switches import Outlet, Switch -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.components.homekit.const import ( + TYPE_FAUCET, TYPE_SHOWER, TYPE_SPRINKLER, TYPE_VALVE) +from homeassistant.components.homekit.type_switches import ( + Outlet, Switch, Valve) +from homeassistant.const import ATTR_ENTITY_ID, CONF_TYPE, STATE_OFF, STATE_ON from homeassistant.core import split_entity_id from tests.common import async_mock_service @@ -90,3 +93,70 @@ async def test_switch_set_state(hass, hk_driver, entity_id): await hass.async_block_till_done() assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id + + +async def test_valve_set_state(hass, hk_driver): + """Test if Valve accessory and HA are updated accordingly.""" + entity_id = 'switch.valve_test' + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + + acc = Valve(hass, hk_driver, 'Valve', entity_id, 2, + {CONF_TYPE: TYPE_FAUCET}) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.category == 29 # Faucet + assert acc.char_valve_type.value == 3 # Water faucet + + acc = Valve(hass, hk_driver, 'Valve', entity_id, 2, + {CONF_TYPE: TYPE_SHOWER}) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.category == 30 # Shower + assert acc.char_valve_type.value == 2 # Shower head + + acc = Valve(hass, hk_driver, 'Valve', entity_id, 2, + {CONF_TYPE: TYPE_SPRINKLER}) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.category == 28 # Sprinkler + assert acc.char_valve_type.value == 1 # Irrigation + + acc = Valve(hass, hk_driver, 'Valve', entity_id, 2, + {CONF_TYPE: TYPE_VALVE}) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 29 # Faucet + + assert acc.char_active.value is False + assert acc.char_in_use.value is False + assert acc.char_valve_type.value == 0 # Generic Valve + + hass.states.async_set(entity_id, STATE_ON) + await hass.async_block_till_done() + assert acc.char_active.value is True + assert acc.char_in_use.value is True + + hass.states.async_set(entity_id, STATE_OFF) + await hass.async_block_till_done() + assert acc.char_active.value is False + assert acc.char_in_use.value is False + + # Set from HomeKit + call_turn_on = async_mock_service(hass, 'switch', 'turn_on') + call_turn_off = async_mock_service(hass, 'switch', 'turn_off') + + await hass.async_add_job(acc.char_active.client_update_value, True) + await hass.async_block_till_done() + assert acc.char_in_use.value is True + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + + await hass.async_add_job(acc.char_active.client_update_value, False) + await hass.async_block_till_done() + assert acc.char_in_use.value is False + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 9be92b817be..0368dfa642e 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -4,7 +4,8 @@ import voluptuous as vol from homeassistant.components.homekit.const import ( CONF_FEATURE, CONF_FEATURE_LIST, HOMEKIT_NOTIFY_ID, FEATURE_ON_OFF, - FEATURE_PLAY_PAUSE, TYPE_OUTLET) + FEATURE_PLAY_PAUSE, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER, + TYPE_SWITCH, TYPE_VALVE) from homeassistant.components.homekit.util import ( convert_to_float, density_to_air_quality, dismiss_setup_message, show_setup_message, temperature_to_homekit, temperature_to_states, @@ -23,7 +24,8 @@ from tests.common import async_mock_service def test_validate_entity_config(): """Test validate entities.""" - configs = [{'invalid_entity_id': {}}, {'demo.test': 1}, + configs = [None, [], 'string', 12345, + {'invalid_entity_id': {}}, {'demo.test': 1}, {'demo.test': 'test'}, {'demo.test': [1, 2]}, {'demo.test': None}, {'demo.test': {CONF_NAME: None}}, {'media_player.test': {CONF_FEATURE_LIST: [ @@ -57,8 +59,19 @@ def test_validate_entity_config(): assert vec({'media_player.demo': config}) == \ {'media_player.demo': {CONF_FEATURE_LIST: {FEATURE_ON_OFF: {}, FEATURE_PLAY_PAUSE: {}}}} + + assert vec({'switch.demo': {CONF_TYPE: TYPE_FAUCET}}) == \ + {'switch.demo': {CONF_TYPE: TYPE_FAUCET}} assert vec({'switch.demo': {CONF_TYPE: TYPE_OUTLET}}) == \ {'switch.demo': {CONF_TYPE: TYPE_OUTLET}} + assert vec({'switch.demo': {CONF_TYPE: TYPE_SHOWER}}) == \ + {'switch.demo': {CONF_TYPE: TYPE_SHOWER}} + assert vec({'switch.demo': {CONF_TYPE: TYPE_SPRINKLER}}) == \ + {'switch.demo': {CONF_TYPE: TYPE_SPRINKLER}} + assert vec({'switch.demo': {CONF_TYPE: TYPE_SWITCH}}) == \ + {'switch.demo': {CONF_TYPE: TYPE_SWITCH}} + assert vec({'switch.demo': {CONF_TYPE: TYPE_VALVE}}) == \ + {'switch.demo': {CONF_TYPE: TYPE_VALVE}} def test_validate_media_player_features(): diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index c20cee0d0e8..ceb30091970 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -34,7 +34,7 @@ async def test_bridge_setup_invalid_username(): side_effect=errors.AuthenticationRequired): assert await hue_bridge.async_setup() is False - assert len(hass.async_add_job.mock_calls) == 1 + assert len(hass.async_create_task.mock_calls) == 1 assert len(hass.config_entries.flow.async_init.mock_calls) == 1 assert hass.config_entries.flow.async_init.mock_calls[0][2]['data'] == { 'host': '1.2.3.4' @@ -87,7 +87,7 @@ async def test_reset_if_entry_had_wrong_auth(): side_effect=errors.AuthenticationRequired): assert await hue_bridge.async_setup() is False - assert len(hass.async_add_job.mock_calls) == 1 + assert len(hass.async_create_task.mock_calls) == 1 assert await hue_bridge.async_reset() diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index 5da6d5b709a..1fcc092dd30 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -20,62 +20,6 @@ async def test_setup_with_no_config(hass): assert hass.data[hue.DOMAIN] == {} -async def test_setup_with_discovery_no_known_auth(hass, aioclient_mock): - """Test discovering a bridge and not having known auth.""" - aioclient_mock.get(hue.API_NUPNP, json=[ - { - 'internalipaddress': '0.0.0.0', - 'id': 'abcd1234' - } - ]) - - with patch.object(hass, 'config_entries') as mock_config_entries, \ - patch.object(hue, 'configured_hosts', return_value=[]): - mock_config_entries.flow.async_init.return_value = mock_coro() - assert await async_setup_component(hass, hue.DOMAIN, { - hue.DOMAIN: {} - }) is True - - # Flow started for discovered bridge - assert len(mock_config_entries.flow.mock_calls) == 1 - assert mock_config_entries.flow.mock_calls[0][2]['data'] == { - 'host': '0.0.0.0', - 'path': '.hue_abcd1234.conf', - } - - # Config stored for domain. - assert hass.data[hue.DOMAIN] == { - '0.0.0.0': { - hue.CONF_HOST: '0.0.0.0', - hue.CONF_FILENAME: '.hue_abcd1234.conf', - hue.CONF_ALLOW_HUE_GROUPS: hue.DEFAULT_ALLOW_HUE_GROUPS, - hue.CONF_ALLOW_UNREACHABLE: hue.DEFAULT_ALLOW_UNREACHABLE, - } - } - - -async def test_setup_with_discovery_known_auth(hass, aioclient_mock): - """Test we don't do anything if we discover already configured hub.""" - aioclient_mock.get(hue.API_NUPNP, json=[ - { - 'internalipaddress': '0.0.0.0', - 'id': 'abcd1234' - } - ]) - - with patch.object(hass, 'config_entries') as mock_config_entries, \ - patch.object(hue, 'configured_hosts', return_value=['0.0.0.0']): - assert await async_setup_component(hass, hue.DOMAIN, { - hue.DOMAIN: {} - }) is True - - # Flow started for discovered bridge - assert len(mock_config_entries.flow.mock_calls) == 0 - - # Config stored for domain. - assert hass.data[hue.DOMAIN] == {} - - async def test_setup_defined_hosts_known_auth(hass): """Test we don't initiate a config entry if config bridge is known.""" with patch.object(hass, 'config_entries') as mock_config_entries, \ diff --git a/tests/components/ifttt/__init__.py b/tests/components/ifttt/__init__.py new file mode 100644 index 00000000000..2fe2f40276c --- /dev/null +++ b/tests/components/ifttt/__init__.py @@ -0,0 +1 @@ +"""Tests for the IFTTT component.""" diff --git a/tests/components/ifttt/test_init.py b/tests/components/ifttt/test_init.py new file mode 100644 index 00000000000..61d6654ba55 --- /dev/null +++ b/tests/components/ifttt/test_init.py @@ -0,0 +1,48 @@ +"""Test the init file of IFTTT.""" +from unittest.mock import Mock, patch + +from homeassistant import data_entry_flow +from homeassistant.core import callback +from homeassistant.components import ifttt + + +async def test_config_flow_registers_webhook(hass, aiohttp_client): + """Test setting up IFTTT and sending webhook.""" + with patch('homeassistant.util.get_local_ip', return_value='example.com'): + result = await hass.config_entries.flow.async_init('ifttt', context={ + 'source': 'user' + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM, result + + result = await hass.config_entries.flow.async_configure( + result['flow_id'], {}) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + webhook_id = result['result'].data['webhook_id'] + + ifttt_events = [] + + @callback + def handle_event(event): + """Handle IFTTT event.""" + ifttt_events.append(event) + + hass.bus.async_listen(ifttt.EVENT_RECEIVED, handle_event) + + client = await aiohttp_client(hass.http.app) + await client.post('/api/webhook/{}'.format(webhook_id), json={ + 'hello': 'ifttt' + }) + + assert len(ifttt_events) == 1 + assert ifttt_events[0].data['webhook_id'] == webhook_id + assert ifttt_events[0].data['hello'] == 'ifttt' + + +async def test_config_flow_aborts_external_url(hass, aiohttp_client): + """Test setting up IFTTT and sending webhook.""" + hass.config.api = Mock(base_url='http://192.168.1.10') + result = await hass.config_entries.flow.async_init('ifttt', context={ + 'source': 'user' + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT + assert result['reason'] == 'not_internet_accessible' diff --git a/tests/components/image_processing/common.py b/tests/components/image_processing/common.py new file mode 100644 index 00000000000..b767884503d --- /dev/null +++ b/tests/components/image_processing/common.py @@ -0,0 +1,23 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.image_processing import DOMAIN, SERVICE_SCAN +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import callback +from homeassistant.loader import bind_hass + + +@bind_hass +def scan(hass, entity_id=None): + """Force process of all cameras or given entity.""" + hass.add_job(async_scan, hass, entity_id) + + +@callback +@bind_hass +def async_scan(hass, entity_id=None): + """Force process of all cameras or given entity.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_SCAN, data)) diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index 4240e173b26..7a31b2ffadf 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -10,6 +10,7 @@ import homeassistant.components.image_processing as ip from tests.common import ( get_test_home_assistant, get_test_instance_port, assert_setup_component) +from tests.components.image_processing import common class TestSetupImageProcessing: @@ -85,7 +86,7 @@ class TestImageProcessing: """Grab an image from camera entity.""" self.hass.start() - ip.scan(self.hass, entity_id='image_processing.test') + common.scan(self.hass, entity_id='image_processing.test') self.hass.block_till_done() state = self.hass.states.get('image_processing.test') @@ -100,7 +101,7 @@ class TestImageProcessing: """Try to get image without exists camera.""" self.hass.states.remove('camera.demo_camera') - ip.scan(self.hass, entity_id='image_processing.test') + common.scan(self.hass, entity_id='image_processing.test') self.hass.block_till_done() state = self.hass.states.get('image_processing.test') @@ -152,7 +153,7 @@ class TestImageProcessingAlpr: """Set up and scan a picture and test plates from event.""" aioclient_mock.get(self.url, content=b'image') - ip.scan(self.hass, entity_id='image_processing.demo_alpr') + common.scan(self.hass, entity_id='image_processing.demo_alpr') self.hass.block_till_done() state = self.hass.states.get('image_processing.demo_alpr') @@ -171,8 +172,8 @@ class TestImageProcessingAlpr: """Set up and scan a picture and test plates from event.""" aioclient_mock.get(self.url, content=b'image') - ip.scan(self.hass, entity_id='image_processing.demo_alpr') - ip.scan(self.hass, entity_id='image_processing.demo_alpr') + common.scan(self.hass, entity_id='image_processing.demo_alpr') + common.scan(self.hass, entity_id='image_processing.demo_alpr') self.hass.block_till_done() state = self.hass.states.get('image_processing.demo_alpr') @@ -195,7 +196,7 @@ class TestImageProcessingAlpr: """Set up and scan a picture and test plates from event.""" aioclient_mock.get(self.url, content=b'image') - ip.scan(self.hass, entity_id='image_processing.demo_alpr') + common.scan(self.hass, entity_id='image_processing.demo_alpr') self.hass.block_till_done() state = self.hass.states.get('image_processing.demo_alpr') @@ -254,7 +255,7 @@ class TestImageProcessingFace: """Set up and scan a picture and test faces from event.""" aioclient_mock.get(self.url, content=b'image') - ip.scan(self.hass, entity_id='image_processing.demo_face') + common.scan(self.hass, entity_id='image_processing.demo_face') self.hass.block_till_done() state = self.hass.states.get('image_processing.demo_face') @@ -279,7 +280,7 @@ class TestImageProcessingFace: """Set up and scan a picture and test faces from event.""" aioclient_mock.get(self.url, content=b'image') - ip.scan(self.hass, entity_id='image_processing.demo_face') + common.scan(self.hass, entity_id='image_processing.demo_face') self.hass.block_till_done() state = self.hass.states.get('image_processing.demo_face') diff --git a/tests/components/image_processing/test_microsoft_face_detect.py b/tests/components/image_processing/test_microsoft_face_detect.py index 9047c5b8475..c7528c346ee 100644 --- a/tests/components/image_processing/test_microsoft_face_detect.py +++ b/tests/components/image_processing/test_microsoft_face_detect.py @@ -9,6 +9,7 @@ import homeassistant.components.microsoft_face as mf from tests.common import ( get_test_home_assistant, assert_setup_component, load_fixture, mock_coro) +from tests.components.image_processing import common class TestMicrosoftFaceDetectSetup: @@ -146,7 +147,7 @@ class TestMicrosoftFaceDetect: params={'returnFaceAttributes': "age,gender"} ) - ip.scan(self.hass, entity_id='image_processing.test_local') + common.scan(self.hass, entity_id='image_processing.test_local') self.hass.block_till_done() state = self.hass.states.get('image_processing.test_local') diff --git a/tests/components/image_processing/test_microsoft_face_identify.py b/tests/components/image_processing/test_microsoft_face_identify.py index 6d3eae38728..892326e5bff 100644 --- a/tests/components/image_processing/test_microsoft_face_identify.py +++ b/tests/components/image_processing/test_microsoft_face_identify.py @@ -9,6 +9,7 @@ import homeassistant.components.microsoft_face as mf from tests.common import ( get_test_home_assistant, assert_setup_component, load_fixture, mock_coro) +from tests.components.image_processing import common class TestMicrosoftFaceIdentifySetup: @@ -150,7 +151,7 @@ class TestMicrosoftFaceIdentify: text=load_fixture('microsoft_face_identify.json') ) - ip.scan(self.hass, entity_id='image_processing.test_local') + common.scan(self.hass, entity_id='image_processing.test_local') self.hass.block_till_done() state = self.hass.states.get('image_processing.test_local') diff --git a/tests/components/image_processing/test_openalpr_cloud.py b/tests/components/image_processing/test_openalpr_cloud.py index 2d6015e3fe7..8a71db7fb7b 100644 --- a/tests/components/image_processing/test_openalpr_cloud.py +++ b/tests/components/image_processing/test_openalpr_cloud.py @@ -10,6 +10,7 @@ from homeassistant.components.image_processing.openalpr_cloud import ( from tests.common import ( get_test_home_assistant, assert_setup_component, load_fixture, mock_coro) +from tests.components.image_processing import common class TestOpenAlprCloudSetup: @@ -160,7 +161,7 @@ class TestOpenAlprCloud: with patch('homeassistant.components.camera.async_get_image', return_value=mock_coro( camera.Image('image/jpeg', b'image'))): - ip.scan(self.hass, entity_id='image_processing.test_local') + common.scan(self.hass, entity_id='image_processing.test_local') self.hass.block_till_done() state = self.hass.states.get('image_processing.test_local') @@ -188,7 +189,7 @@ class TestOpenAlprCloud: with patch('homeassistant.components.camera.async_get_image', return_value=mock_coro( camera.Image('image/jpeg', b'image'))): - ip.scan(self.hass, entity_id='image_processing.test_local') + common.scan(self.hass, entity_id='image_processing.test_local') self.hass.block_till_done() assert len(aioclient_mock.mock_calls) == 1 @@ -204,7 +205,7 @@ class TestOpenAlprCloud: with patch('homeassistant.components.camera.async_get_image', return_value=mock_coro( camera.Image('image/jpeg', b'image'))): - ip.scan(self.hass, entity_id='image_processing.test_local') + common.scan(self.hass, entity_id='image_processing.test_local') self.hass.block_till_done() assert len(aioclient_mock.mock_calls) == 1 diff --git a/tests/components/image_processing/test_openalpr_local.py b/tests/components/image_processing/test_openalpr_local.py index 772d66670a0..6d860da3313 100644 --- a/tests/components/image_processing/test_openalpr_local.py +++ b/tests/components/image_processing/test_openalpr_local.py @@ -9,6 +9,7 @@ import homeassistant.components.image_processing as ip from tests.common import ( get_test_home_assistant, assert_setup_component, load_fixture) +from tests.components.image_processing import common @asyncio.coroutine @@ -146,7 +147,7 @@ class TestOpenAlprLocal: """Set up and scan a picture and test plates from event.""" aioclient_mock.get(self.url, content=b'image') - ip.scan(self.hass, entity_id='image_processing.test_local') + common.scan(self.hass, entity_id='image_processing.test_local') self.hass.block_till_done() state = self.hass.states.get('image_processing.test_local') diff --git a/tests/components/light/common.py b/tests/components/light/common.py new file mode 100644 index 00000000000..906e0458dba --- /dev/null +++ b/tests/components/light/common.py @@ -0,0 +1,97 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME, ATTR_COLOR_TEMP, + ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, ATTR_KELVIN, ATTR_PROFILE, + ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN) +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON) +from homeassistant.core import callback +from homeassistant.loader import bind_hass + + +@bind_hass +def turn_on(hass, entity_id=None, transition=None, brightness=None, + brightness_pct=None, rgb_color=None, xy_color=None, hs_color=None, + color_temp=None, kelvin=None, white_value=None, + profile=None, flash=None, effect=None, color_name=None): + """Turn all or specified light on.""" + hass.add_job( + async_turn_on, hass, entity_id, transition, brightness, brightness_pct, + rgb_color, xy_color, hs_color, color_temp, kelvin, white_value, + profile, flash, effect, color_name) + + +@callback +@bind_hass +def async_turn_on(hass, entity_id=None, transition=None, brightness=None, + brightness_pct=None, rgb_color=None, xy_color=None, + hs_color=None, color_temp=None, kelvin=None, + white_value=None, profile=None, flash=None, effect=None, + color_name=None): + """Turn all or specified light on.""" + data = { + key: value for key, value in [ + (ATTR_ENTITY_ID, entity_id), + (ATTR_PROFILE, profile), + (ATTR_TRANSITION, transition), + (ATTR_BRIGHTNESS, brightness), + (ATTR_BRIGHTNESS_PCT, brightness_pct), + (ATTR_RGB_COLOR, rgb_color), + (ATTR_XY_COLOR, xy_color), + (ATTR_HS_COLOR, hs_color), + (ATTR_COLOR_TEMP, color_temp), + (ATTR_KELVIN, kelvin), + (ATTR_WHITE_VALUE, white_value), + (ATTR_FLASH, flash), + (ATTR_EFFECT, effect), + (ATTR_COLOR_NAME, color_name), + ] if value is not None + } + + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)) + + +@bind_hass +def turn_off(hass, entity_id=None, transition=None): + """Turn all or specified light off.""" + hass.add_job(async_turn_off, hass, entity_id, transition) + + +@callback +@bind_hass +def async_turn_off(hass, entity_id=None, transition=None): + """Turn all or specified light off.""" + data = { + key: value for key, value in [ + (ATTR_ENTITY_ID, entity_id), + (ATTR_TRANSITION, transition), + ] if value is not None + } + + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, data)) + + +@bind_hass +def toggle(hass, entity_id=None, transition=None): + """Toggle all or specified light.""" + hass.add_job(async_toggle, hass, entity_id, transition) + + +@callback +@bind_hass +def async_toggle(hass, entity_id=None, transition=None): + """Toggle all or specified light.""" + data = { + key: value for key, value in [ + (ATTR_ENTITY_ID, entity_id), + (ATTR_TRANSITION, transition), + ] if value is not None + } + + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_TOGGLE, data)) diff --git a/tests/components/light/test_demo.py b/tests/components/light/test_demo.py index db575bba5ba..8711acaa318 100644 --- a/tests/components/light/test_demo.py +++ b/tests/components/light/test_demo.py @@ -1,81 +1,79 @@ """The tests for the demo light component.""" -# pylint: disable=protected-access -import unittest +import pytest -from homeassistant.setup import setup_component -import homeassistant.components.light as light +from homeassistant.setup import async_setup_component +from homeassistant.components import light -from tests.common import get_test_home_assistant +from tests.components.light import common ENTITY_LIGHT = 'light.bed_light' -class TestDemoLight(unittest.TestCase): - """Test the demo light.""" - - # pylint: disable=invalid-name - def setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.assertTrue(setup_component(self.hass, light.DOMAIN, {'light': { +@pytest.fixture(autouse=True) +def setup_comp(hass): + """Set up demo component.""" + hass.loop.run_until_complete(async_setup_component(hass, light.DOMAIN, { + 'light': { 'platform': 'demo', }})) - # pylint: disable=invalid-name - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() - def test_state_attributes(self): - """Test light state attributes.""" - light.turn_on( - self.hass, ENTITY_LIGHT, xy_color=(.4, .4), brightness=25) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_LIGHT) - self.assertTrue(light.is_on(self.hass, ENTITY_LIGHT)) - self.assertEqual((0.4, 0.4), state.attributes.get( - light.ATTR_XY_COLOR)) - self.assertEqual(25, state.attributes.get(light.ATTR_BRIGHTNESS)) - self.assertEqual( - (255, 234, 164), state.attributes.get(light.ATTR_RGB_COLOR)) - self.assertEqual('rainbow', state.attributes.get(light.ATTR_EFFECT)) - light.turn_on( - self.hass, ENTITY_LIGHT, rgb_color=(251, 253, 255), - white_value=254) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_LIGHT) - self.assertEqual(254, state.attributes.get(light.ATTR_WHITE_VALUE)) - self.assertEqual( - (250, 252, 255), state.attributes.get(light.ATTR_RGB_COLOR)) - self.assertEqual( - (0.319, 0.326), state.attributes.get(light.ATTR_XY_COLOR)) - light.turn_on(self.hass, ENTITY_LIGHT, color_temp=400, effect='none') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_LIGHT) - self.assertEqual(400, state.attributes.get(light.ATTR_COLOR_TEMP)) - self.assertEqual(153, state.attributes.get(light.ATTR_MIN_MIREDS)) - self.assertEqual(500, state.attributes.get(light.ATTR_MAX_MIREDS)) - self.assertEqual('none', state.attributes.get(light.ATTR_EFFECT)) - light.turn_on(self.hass, ENTITY_LIGHT, kelvin=3000, brightness_pct=50) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_LIGHT) - self.assertEqual(333, state.attributes.get(light.ATTR_COLOR_TEMP)) - self.assertEqual(127, state.attributes.get(light.ATTR_BRIGHTNESS)) +async def test_state_attributes(hass): + """Test light state attributes.""" + common.async_turn_on( + hass, ENTITY_LIGHT, xy_color=(.4, .4), brightness=25) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_LIGHT) + assert light.is_on(hass, ENTITY_LIGHT) + assert (0.4, 0.4) == state.attributes.get(light.ATTR_XY_COLOR) + assert 25 == state.attributes.get(light.ATTR_BRIGHTNESS) + assert (255, 234, 164) == state.attributes.get(light.ATTR_RGB_COLOR) + assert 'rainbow' == state.attributes.get(light.ATTR_EFFECT) + common.async_turn_on( + hass, ENTITY_LIGHT, rgb_color=(251, 253, 255), + white_value=254) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_LIGHT) + assert 254 == state.attributes.get(light.ATTR_WHITE_VALUE) + assert (250, 252, 255) == state.attributes.get(light.ATTR_RGB_COLOR) + assert (0.319, 0.326) == state.attributes.get(light.ATTR_XY_COLOR) + common.async_turn_on(hass, ENTITY_LIGHT, color_temp=400, effect='none') + await hass.async_block_till_done() + state = hass.states.get(ENTITY_LIGHT) + assert 400 == state.attributes.get(light.ATTR_COLOR_TEMP) + assert 153 == state.attributes.get(light.ATTR_MIN_MIREDS) + assert 500 == state.attributes.get(light.ATTR_MAX_MIREDS) + assert 'none' == state.attributes.get(light.ATTR_EFFECT) + common.async_turn_on(hass, ENTITY_LIGHT, kelvin=3000, brightness_pct=50) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_LIGHT) + assert 333 == state.attributes.get(light.ATTR_COLOR_TEMP) + assert 127 == state.attributes.get(light.ATTR_BRIGHTNESS) - def test_turn_off(self): - """Test light turn off method.""" - light.turn_on(self.hass, ENTITY_LIGHT) - self.hass.block_till_done() - self.assertTrue(light.is_on(self.hass, ENTITY_LIGHT)) - light.turn_off(self.hass, ENTITY_LIGHT) - self.hass.block_till_done() - self.assertFalse(light.is_on(self.hass, ENTITY_LIGHT)) - def test_turn_off_without_entity_id(self): - """Test light turn off all lights.""" - light.turn_on(self.hass, ENTITY_LIGHT) - self.hass.block_till_done() - self.assertTrue(light.is_on(self.hass, ENTITY_LIGHT)) - light.turn_off(self.hass) - self.hass.block_till_done() - self.assertFalse(light.is_on(self.hass, ENTITY_LIGHT)) +async def test_turn_off(hass): + """Test light turn off method.""" + await hass.services.async_call('light', 'turn_on', { + 'entity_id': ENTITY_LIGHT + }, blocking=True) + + assert light.is_on(hass, ENTITY_LIGHT) + + await hass.services.async_call('light', 'turn_off', { + 'entity_id': ENTITY_LIGHT + }, blocking=True) + + assert not light.is_on(hass, ENTITY_LIGHT) + + +async def test_turn_off_without_entity_id(hass): + """Test light turn off all lights.""" + await hass.services.async_call('light', 'turn_on', { + }, blocking=True) + + assert light.is_on(hass, ENTITY_LIGHT) + + await hass.services.async_call('light', 'turn_off', { + }, blocking=True) + + assert not light.is_on(hass, ENTITY_LIGHT) diff --git a/tests/components/light/test_group.py b/tests/components/light/test_group.py index 4619e9fb9bd..472bdf42385 100644 --- a/tests/components/light/test_group.py +++ b/tests/components/light/test_group.py @@ -3,10 +3,11 @@ from unittest.mock import MagicMock import asynctest -from homeassistant.components import light from homeassistant.components.light import group from homeassistant.setup import async_setup_component +from tests.components.light import common + async def test_default_state(hass): """Test light group default state.""" @@ -300,29 +301,29 @@ async def test_service_calls(hass): await hass.async_block_till_done() assert hass.states.get('light.light_group').state == 'on' - light.async_toggle(hass, 'light.light_group') + common.async_toggle(hass, 'light.light_group') await hass.async_block_till_done() assert hass.states.get('light.bed_light').state == 'off' assert hass.states.get('light.ceiling_lights').state == 'off' assert hass.states.get('light.kitchen_lights').state == 'off' - light.async_turn_on(hass, 'light.light_group') + common.async_turn_on(hass, 'light.light_group') await hass.async_block_till_done() assert hass.states.get('light.bed_light').state == 'on' assert hass.states.get('light.ceiling_lights').state == 'on' assert hass.states.get('light.kitchen_lights').state == 'on' - light.async_turn_off(hass, 'light.light_group') + common.async_turn_off(hass, 'light.light_group') await hass.async_block_till_done() assert hass.states.get('light.bed_light').state == 'off' assert hass.states.get('light.ceiling_lights').state == 'off' assert hass.states.get('light.kitchen_lights').state == 'off' - light.async_turn_on(hass, 'light.light_group', brightness=128, - effect='Random', rgb_color=(42, 255, 255)) + common.async_turn_on(hass, 'light.light_group', brightness=128, + effect='Random', rgb_color=(42, 255, 255)) await hass.async_block_till_done() state = hass.states.get('light.bed_light') diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 66dbadb5c38..6253de8cbae 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -15,6 +15,7 @@ from homeassistant.helpers.intent import IntentHandleError from tests.common import ( async_mock_service, mock_service, get_test_home_assistant, mock_storage) +from tests.components.light import common class TestLight(unittest.TestCase): @@ -54,7 +55,7 @@ class TestLight(unittest.TestCase): turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - light.turn_on( + common.turn_on( self.hass, entity_id='entity_id_val', transition='transition_val', @@ -88,7 +89,7 @@ class TestLight(unittest.TestCase): turn_off_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_OFF) - light.turn_off( + common.turn_off( self.hass, entity_id='entity_id_val', transition='transition_val') self.hass.block_till_done() @@ -105,7 +106,7 @@ class TestLight(unittest.TestCase): toggle_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TOGGLE) - light.toggle( + common.toggle( self.hass, entity_id='entity_id_val', transition='transition_val') self.hass.block_till_done() @@ -135,8 +136,8 @@ class TestLight(unittest.TestCase): self.assertFalse(light.is_on(self.hass, dev3.entity_id)) # Test basic turn_on, turn_off, toggle services - light.turn_off(self.hass, entity_id=dev1.entity_id) - light.turn_on(self.hass, entity_id=dev2.entity_id) + common.turn_off(self.hass, entity_id=dev1.entity_id) + common.turn_on(self.hass, entity_id=dev2.entity_id) self.hass.block_till_done() @@ -144,7 +145,7 @@ class TestLight(unittest.TestCase): self.assertTrue(light.is_on(self.hass, dev2.entity_id)) # turn on all lights - light.turn_on(self.hass) + common.turn_on(self.hass) self.hass.block_till_done() @@ -153,7 +154,7 @@ class TestLight(unittest.TestCase): self.assertTrue(light.is_on(self.hass, dev3.entity_id)) # turn off all lights - light.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() @@ -162,7 +163,7 @@ class TestLight(unittest.TestCase): self.assertFalse(light.is_on(self.hass, dev3.entity_id)) # toggle all lights - light.toggle(self.hass) + common.toggle(self.hass) self.hass.block_till_done() @@ -171,7 +172,7 @@ class TestLight(unittest.TestCase): self.assertTrue(light.is_on(self.hass, dev3.entity_id)) # toggle all lights - light.toggle(self.hass) + common.toggle(self.hass) self.hass.block_till_done() @@ -180,12 +181,12 @@ class TestLight(unittest.TestCase): self.assertFalse(light.is_on(self.hass, dev3.entity_id)) # Ensure all attributes process correctly - light.turn_on(self.hass, dev1.entity_id, - transition=10, brightness=20, color_name='blue') - light.turn_on( + common.turn_on(self.hass, dev1.entity_id, + transition=10, brightness=20, color_name='blue') + common.turn_on( self.hass, dev2.entity_id, rgb_color=(255, 255, 255), white_value=255) - light.turn_on(self.hass, dev3.entity_id, xy_color=(.4, .6)) + common.turn_on(self.hass, dev3.entity_id, xy_color=(.4, .6)) self.hass.block_till_done() @@ -211,9 +212,9 @@ class TestLight(unittest.TestCase): prof_name, prof_h, prof_s, prof_bri = 'relax', 35.932, 69.412, 144 # Test light profiles - light.turn_on(self.hass, dev1.entity_id, profile=prof_name) + common.turn_on(self.hass, dev1.entity_id, profile=prof_name) # Specify a profile and a brightness attribute to overwrite it - light.turn_on( + common.turn_on( self.hass, dev2.entity_id, profile=prof_name, brightness=100) @@ -232,10 +233,10 @@ class TestLight(unittest.TestCase): }, data) # Test bad data - light.turn_on(self.hass) - light.turn_on(self.hass, dev1.entity_id, profile="nonexisting") - light.turn_on(self.hass, dev2.entity_id, xy_color=["bla-di-bla", 5]) - light.turn_on(self.hass, dev3.entity_id, rgb_color=[255, None, 2]) + common.turn_on(self.hass) + common.turn_on(self.hass, dev1.entity_id, profile="nonexisting") + common.turn_on(self.hass, dev2.entity_id, xy_color=["bla-di-bla", 5]) + common.turn_on(self.hass, dev3.entity_id, rgb_color=[255, None, 2]) self.hass.block_till_done() @@ -249,13 +250,13 @@ class TestLight(unittest.TestCase): self.assertEqual({}, data) # faulty attributes will not trigger a service call - light.turn_on( + common.turn_on( self.hass, dev1.entity_id, profile=prof_name, brightness='bright') - light.turn_on( + common.turn_on( self.hass, dev1.entity_id, rgb_color='yellowish') - light.turn_on( + common.turn_on( self.hass, dev2.entity_id, white_value='high') @@ -299,7 +300,7 @@ class TestLight(unittest.TestCase): dev1, _, _ = platform.DEVICES - light.turn_on(self.hass, dev1.entity_id, profile='test') + common.turn_on(self.hass, dev1.entity_id, profile='test') self.hass.block_till_done() @@ -340,7 +341,7 @@ class TestLight(unittest.TestCase): )) dev, _, _ = platform.DEVICES - light.turn_on(self.hass, dev.entity_id) + common.turn_on(self.hass, dev.entity_id) self.hass.block_till_done() _, data = dev.last_call('turn_on') self.assertEqual({ @@ -380,7 +381,7 @@ class TestLight(unittest.TestCase): dev = next(filter(lambda x: x.entity_id == 'light.ceiling_2', platform.DEVICES)) - light.turn_on(self.hass, dev.entity_id) + common.turn_on(self.hass, dev.entity_id) self.hass.block_till_done() _, data = dev.last_call('turn_on') self.assertEqual({ diff --git a/tests/components/light/test_litejet.py b/tests/components/light/test_litejet.py index 3040c95e0ac..1d7b8ea97fa 100644 --- a/tests/components/light/test_litejet.py +++ b/tests/components/light/test_litejet.py @@ -5,9 +5,11 @@ from unittest import mock from homeassistant import setup from homeassistant.components import litejet -from tests.common import get_test_home_assistant import homeassistant.components.light as light +from tests.common import get_test_home_assistant +from tests.components.light import common + _LOGGER = logging.getLogger(__name__) ENTITY_LIGHT = 'light.mock_load_1' @@ -78,7 +80,7 @@ class TestLiteJetLight(unittest.TestCase): assert not light.is_on(self.hass, ENTITY_LIGHT) - light.turn_on(self.hass, ENTITY_LIGHT, brightness=102) + common.turn_on(self.hass, ENTITY_LIGHT, brightness=102) self.hass.block_till_done() self.mock_lj.activate_load_at.assert_called_with( ENTITY_LIGHT_NUMBER, 39, 0) @@ -90,11 +92,11 @@ class TestLiteJetLight(unittest.TestCase): assert not light.is_on(self.hass, ENTITY_LIGHT) - light.turn_on(self.hass, ENTITY_LIGHT) + common.turn_on(self.hass, ENTITY_LIGHT) self.hass.block_till_done() self.mock_lj.activate_load.assert_called_with(ENTITY_LIGHT_NUMBER) - light.turn_off(self.hass, ENTITY_LIGHT) + common.turn_off(self.hass, ENTITY_LIGHT) self.hass.block_till_done() self.mock_lj.deactivate_load.assert_called_with(ENTITY_LIGHT_NUMBER) diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index 1245411dcc4..118cdb3c995 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -145,11 +145,14 @@ from unittest.mock import patch from homeassistant.setup import setup_component from homeassistant.const import ( STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) -import homeassistant.components.light as light +from homeassistant.components import light, mqtt +from homeassistant.components.mqtt.discovery import async_start import homeassistant.core as ha + from tests.common import ( assert_setup_component, get_test_home_assistant, mock_mqtt_component, - fire_mqtt_message, mock_coro) + async_fire_mqtt_message, fire_mqtt_message, mock_coro, MockConfigEntry) +from tests.components.light import common class TestLightMQTT(unittest.TestCase): @@ -505,7 +508,7 @@ class TestLightMQTT(unittest.TestCase): self.assertEqual(50, state.attributes.get('white_value')) self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) - light.turn_on(self.hass, 'light.test') + common.turn_on(self.hass, 'light.test') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -514,7 +517,7 @@ class TestLightMQTT(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - light.turn_off(self.hass, 'light.test') + common.turn_off(self.hass, 'light.test') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -524,10 +527,10 @@ class TestLightMQTT(unittest.TestCase): self.assertEqual(STATE_OFF, state.state) 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=[255, 128, 0], - white_value=80) + common.turn_on(self.hass, 'light.test', + brightness=50, xy_color=[0.123, 0.123]) + common.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([ @@ -565,7 +568,7 @@ class TestLightMQTT(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_OFF, state.state) - light.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 64]) + common.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 64]) self.hass.block_till_done() self.mock_publish.async_publish.assert_has_calls([ @@ -713,7 +716,7 @@ class TestLightMQTT(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_OFF, state.state) - light.turn_on(self.hass, 'light.test', brightness=50) + common.turn_on(self.hass, 'light.test', brightness=50) self.hass.block_till_done() # Should get the following MQTT messages. @@ -725,7 +728,7 @@ class TestLightMQTT(unittest.TestCase): ], any_order=True) self.mock_publish.async_publish.reset_mock() - light.turn_off(self.hass, 'light.test') + common.turn_off(self.hass, 'light.test') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -746,7 +749,7 @@ class TestLightMQTT(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_OFF, state.state) - light.turn_on(self.hass, 'light.test', brightness=50) + common.turn_on(self.hass, 'light.test', brightness=50) self.hass.block_till_done() # Should get the following MQTT messages. @@ -758,7 +761,7 @@ class TestLightMQTT(unittest.TestCase): ], any_order=True) self.mock_publish.async_publish.reset_mock() - light.turn_off(self.hass, 'light.test') + common.turn_off(self.hass, 'light.test') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -782,7 +785,7 @@ class TestLightMQTT(unittest.TestCase): self.assertEqual(STATE_OFF, state.state) # Turn on w/ no brightness - should set to max - light.turn_on(self.hass, 'light.test') + common.turn_on(self.hass, 'light.test') self.hass.block_till_done() # Should get the following MQTT messages. @@ -791,7 +794,7 @@ class TestLightMQTT(unittest.TestCase): 'test_light/bright', 255, 0, False) self.mock_publish.async_publish.reset_mock() - light.turn_off(self.hass, 'light.test') + common.turn_off(self.hass, 'light.test') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -799,19 +802,19 @@ class TestLightMQTT(unittest.TestCase): self.mock_publish.async_publish.reset_mock() # Turn on w/ brightness - light.turn_on(self.hass, 'light.test', brightness=50) + common.turn_on(self.hass, 'light.test', brightness=50) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'test_light/bright', 50, 0, False) self.mock_publish.async_publish.reset_mock() - light.turn_off(self.hass, 'light.test') + common.turn_off(self.hass, 'light.test') self.hass.block_till_done() # Turn on w/ just a color to insure brightness gets # added and sent. - light.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 0]) + common.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 0]) self.hass.block_till_done() self.mock_publish.async_publish.assert_has_calls([ @@ -876,3 +879,31 @@ class TestLightMQTT(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_UNAVAILABLE, state.state) + + +async def test_discovery_removal_light(hass, mqtt_mock, caplog): + """Test removal of discovered light.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + + data = ( + '{ "name": "Beer",' + ' "status_topic": "test_topic",' + ' "command_topic": "test_topic" }' + ) + + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data) + await hass.async_block_till_done() + + state = hass.states.get('light.beer') + assert state is not None + assert state.name == 'Beer' + + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + '') + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('light.beer') + assert state is None diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index 90875285f17..46db2f61fb3 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -97,10 +97,13 @@ from homeassistant.const import ( STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE, ATTR_SUPPORTED_FEATURES) import homeassistant.components.light as light +from homeassistant.components.mqtt.discovery import async_start import homeassistant.core as ha + from tests.common import ( get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, - assert_setup_component, mock_coro) + assert_setup_component, mock_coro, async_fire_mqtt_message) +from tests.components.light import common class TestLightMQTTJSON(unittest.TestCase): @@ -315,7 +318,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual(191, state.attributes.get(ATTR_SUPPORTED_FEATURES)) self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) - light.turn_on(self.hass, 'light.test') + common.turn_on(self.hass, 'light.test') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -324,7 +327,7 @@ class TestLightMQTTJSON(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - light.turn_off(self.hass, 'light.test') + common.turn_off(self.hass, 'light.test') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -333,9 +336,9 @@ class TestLightMQTTJSON(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_OFF, state.state) - light.turn_on(self.hass, 'light.test', - brightness=50, color_temp=155, effect='colorloop', - white_value=170) + common.turn_on(self.hass, 'light.test', + brightness=50, color_temp=155, effect='colorloop', + white_value=170) self.hass.block_till_done() self.assertEqual('test_light_rgb/set', @@ -361,8 +364,8 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual(170, state.attributes['white_value']) # Test a color command - light.turn_on(self.hass, 'light.test', - brightness=50, hs_color=(125, 100)) + common.turn_on(self.hass, 'light.test', + brightness=50, hs_color=(125, 100)) self.hass.block_till_done() self.assertEqual('test_light_rgb/set', @@ -398,7 +401,7 @@ class TestLightMQTTJSON(unittest.TestCase): } }) - light.turn_on(self.hass, 'light.test', hs_color=(180.0, 50.0)) + common.turn_on(self.hass, 'light.test', hs_color=(180.0, 50.0)) self.hass.block_till_done() message_json = json.loads( @@ -427,7 +430,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual(STATE_OFF, state.state) self.assertEqual(40, state.attributes.get(ATTR_SUPPORTED_FEATURES)) - light.turn_on(self.hass, 'light.test', flash="short") + common.turn_on(self.hass, 'light.test', flash="short") self.hass.block_till_done() self.assertEqual('test_light_rgb/set', @@ -443,7 +446,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual("ON", message_json["state"]) self.mock_publish.async_publish.reset_mock() - light.turn_on(self.hass, 'light.test', flash="long") + common.turn_on(self.hass, 'light.test', flash="long") self.hass.block_till_done() self.assertEqual('test_light_rgb/set', @@ -474,7 +477,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual(STATE_OFF, state.state) self.assertEqual(40, state.attributes.get(ATTR_SUPPORTED_FEATURES)) - light.turn_on(self.hass, 'light.test', transition=10) + common.turn_on(self.hass, 'light.test', transition=10) self.hass.block_till_done() self.assertEqual('test_light_rgb/set', @@ -490,7 +493,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual("ON", message_json["state"]) # Transition back off - light.turn_off(self.hass, 'light.test', transition=10) + common.turn_off(self.hass, 'light.test', transition=10) self.hass.block_till_done() self.assertEqual('test_light_rgb/set', @@ -667,3 +670,25 @@ class TestLightMQTTJSON(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_UNAVAILABLE, state.state) + + +async def test_discovery_removal(hass, mqtt_mock, caplog): + """Test removal of discovered mqtt_json lights.""" + await async_start(hass, 'homeassistant', {}) + data = ( + '{ "name": "Beer",' + ' "platform": "mqtt_json",' + ' "command_topic": "test_topic" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + data) + await hass.async_block_till_done() + state = hass.states.get('light.beer') + assert state is not None + assert state.name == 'Beer' + async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', + '') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('light.beer') + assert state is None diff --git a/tests/components/light/test_mqtt_template.py b/tests/components/light/test_mqtt_template.py index 8f92d659b9b..731e7cd4e45 100644 --- a/tests/components/light/test_mqtt_template.py +++ b/tests/components/light/test_mqtt_template.py @@ -34,9 +34,11 @@ from homeassistant.const import ( STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) import homeassistant.components.light as light import homeassistant.core as ha + from tests.common import ( get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, assert_setup_component, mock_coro) +from tests.components.light import common class TestLightMQTTTemplate(unittest.TestCase): @@ -244,7 +246,7 @@ class TestLightMQTTTemplate(unittest.TestCase): self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) # turn on the light - light.turn_on(self.hass, 'light.test') + common.turn_on(self.hass, 'light.test') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -254,7 +256,7 @@ class TestLightMQTTTemplate(unittest.TestCase): self.assertEqual(STATE_ON, state.state) # turn the light off - light.turn_off(self.hass, 'light.test') + common.turn_off(self.hass, 'light.test') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -264,8 +266,8 @@ class TestLightMQTTTemplate(unittest.TestCase): self.assertEqual(STATE_OFF, state.state) # turn on the light with brightness, color - light.turn_on(self.hass, 'light.test', brightness=50, - rgb_color=[75, 75, 75]) + common.turn_on(self.hass, 'light.test', brightness=50, + rgb_color=[75, 75, 75]) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -273,7 +275,8 @@ class TestLightMQTTTemplate(unittest.TestCase): self.mock_publish.async_publish.reset_mock() # turn on the light with color temp and white val - light.turn_on(self.hass, 'light.test', color_temp=200, white_value=139) + common.turn_on(self.hass, 'light.test', + color_temp=200, white_value=139) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -305,7 +308,7 @@ class TestLightMQTTTemplate(unittest.TestCase): self.assertEqual(STATE_OFF, state.state) # short flash - light.turn_on(self.hass, 'light.test', flash='short') + common.turn_on(self.hass, 'light.test', flash='short') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -313,7 +316,7 @@ class TestLightMQTTTemplate(unittest.TestCase): self.mock_publish.async_publish.reset_mock() # long flash - light.turn_on(self.hass, 'light.test', flash='long') + common.turn_on(self.hass, 'light.test', flash='long') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -336,7 +339,7 @@ class TestLightMQTTTemplate(unittest.TestCase): self.assertEqual(STATE_OFF, state.state) # transition on - light.turn_on(self.hass, 'light.test', transition=10) + common.turn_on(self.hass, 'light.test', transition=10) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -344,7 +347,7 @@ class TestLightMQTTTemplate(unittest.TestCase): self.mock_publish.async_publish.reset_mock() # transition off - light.turn_off(self.hass, 'light.test', transition=4) + common.turn_off(self.hass, 'light.test', transition=4) self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( diff --git a/tests/components/light/test_template.py b/tests/components/light/test_template.py index cc481fabb5c..5e4dd8555ae 100644 --- a/tests/components/light/test_template.py +++ b/tests/components/light/test_template.py @@ -3,12 +3,13 @@ import logging from homeassistant.core import callback from homeassistant import setup -import homeassistant.components as core from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.const import STATE_ON, STATE_OFF from tests.common import ( get_test_home_assistant, assert_setup_component) +from tests.components.light import common + _LOGGER = logging.getLogger(__name__) @@ -378,7 +379,7 @@ class TestTemplateLight: state = self.hass.states.get('light.test_template_light') assert state.state == STATE_OFF - core.light.turn_on(self.hass, 'light.test_template_light') + common.turn_on(self.hass, 'light.test_template_light') self.hass.block_till_done() assert len(self.calls) == 1 @@ -418,7 +419,7 @@ class TestTemplateLight: state = self.hass.states.get('light.test_template_light') assert state.state == STATE_OFF - core.light.turn_on(self.hass, 'light.test_template_light') + common.turn_on(self.hass, 'light.test_template_light') self.hass.block_till_done() state = self.hass.states.get('light.test_template_light') @@ -461,7 +462,7 @@ class TestTemplateLight: state = self.hass.states.get('light.test_template_light') assert state.state == STATE_ON - core.light.turn_off(self.hass, 'light.test_template_light') + common.turn_off(self.hass, 'light.test_template_light') self.hass.block_till_done() assert len(self.calls) == 1 @@ -498,7 +499,7 @@ class TestTemplateLight: state = self.hass.states.get('light.test_template_light') assert state.state == STATE_OFF - core.light.turn_off(self.hass, 'light.test_template_light') + common.turn_off(self.hass, 'light.test_template_light') self.hass.block_till_done() assert len(self.calls) == 1 @@ -538,7 +539,7 @@ class TestTemplateLight: state = self.hass.states.get('light.test_template_light') assert state.attributes.get('brightness') is None - core.light.turn_on( + common.turn_on( self.hass, 'light.test_template_light', **{ATTR_BRIGHTNESS: 124}) self.hass.block_till_done() assert len(self.calls) == 1 diff --git a/tests/components/lock/common.py b/tests/components/lock/common.py new file mode 100644 index 00000000000..2150b3cb894 --- /dev/null +++ b/tests/components/lock/common.py @@ -0,0 +1,45 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.lock import DOMAIN +from homeassistant.const import ( + ATTR_CODE, ATTR_ENTITY_ID, SERVICE_LOCK, SERVICE_UNLOCK, SERVICE_OPEN) +from homeassistant.loader import bind_hass + + +@bind_hass +def lock(hass, entity_id=None, code=None): + """Lock all or specified locks.""" + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_LOCK, data) + + +@bind_hass +def unlock(hass, entity_id=None, code=None): + """Unlock all or specified locks.""" + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_UNLOCK, data) + + +@bind_hass +def open_lock(hass, entity_id=None, code=None): + """Open all or specified locks.""" + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_OPEN, data) diff --git a/tests/components/lock/test_demo.py b/tests/components/lock/test_demo.py index 500cc7f9a6a..255e5307f7a 100644 --- a/tests/components/lock/test_demo.py +++ b/tests/components/lock/test_demo.py @@ -5,6 +5,8 @@ from homeassistant.setup import setup_component from homeassistant.components import lock from tests.common import get_test_home_assistant, mock_service +from tests.components.lock import common + FRONT = 'lock.front_door' KITCHEN = 'lock.kitchen_door' OPENABLE_LOCK = 'lock.openable_lock' @@ -36,14 +38,14 @@ class TestLockDemo(unittest.TestCase): def test_locking(self): """Test the locking of a lock.""" - lock.lock(self.hass, KITCHEN) + common.lock(self.hass, KITCHEN) self.hass.block_till_done() self.assertTrue(lock.is_locked(self.hass, KITCHEN)) def test_unlocking(self): """Test the unlocking of a lock.""" - lock.unlock(self.hass, FRONT) + common.unlock(self.hass, FRONT) self.hass.block_till_done() self.assertFalse(lock.is_locked(self.hass, FRONT)) @@ -51,6 +53,6 @@ class TestLockDemo(unittest.TestCase): def test_opening(self): """Test the opening of a lock.""" calls = mock_service(self.hass, lock.DOMAIN, lock.SERVICE_OPEN) - lock.open_lock(self.hass, OPENABLE_LOCK) + common.open_lock(self.hass, OPENABLE_LOCK) self.hass.block_till_done() self.assertEqual(1, len(calls)) diff --git a/tests/components/lock/test_mqtt.py b/tests/components/lock/test_mqtt.py index 4ef8532f39e..4d2378ff9fa 100644 --- a/tests/components/lock/test_mqtt.py +++ b/tests/components/lock/test_mqtt.py @@ -5,8 +5,12 @@ from homeassistant.setup import setup_component from homeassistant.const import ( STATE_LOCKED, STATE_UNLOCKED, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) import homeassistant.components.lock as lock +from homeassistant.components.mqtt.discovery import async_start + from tests.common import ( - mock_mqtt_component, fire_mqtt_message, get_test_home_assistant) + mock_mqtt_component, async_fire_mqtt_message, fire_mqtt_message, + get_test_home_assistant) +from tests.components.lock import common class TestLockMQTT(unittest.TestCase): @@ -67,7 +71,7 @@ class TestLockMQTT(unittest.TestCase): self.assertEqual(STATE_UNLOCKED, state.state) self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) - lock.lock(self.hass, 'lock.test') + common.lock(self.hass, 'lock.test') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -76,7 +80,7 @@ class TestLockMQTT(unittest.TestCase): state = self.hass.states.get('lock.test') self.assertEqual(STATE_LOCKED, state.state) - lock.unlock(self.hass, 'lock.test') + common.unlock(self.hass, 'lock.test') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -172,3 +176,24 @@ class TestLockMQTT(unittest.TestCase): state = self.hass.states.get('lock.test') self.assertEqual(STATE_UNAVAILABLE, state.state) + + +async def test_discovery_removal_lock(hass, mqtt_mock, caplog): + """Test removal of discovered lock.""" + await async_start(hass, 'homeassistant', {}) + data = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/lock/bla/config', + data) + await hass.async_block_till_done() + state = hass.states.get('lock.beer') + assert state is not None + assert state.name == 'Beer' + async_fire_mqtt_message(hass, 'homeassistant/lock/bla/config', + '') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('lock.beer') + assert state is None diff --git a/tests/components/lovelace/__init__.py b/tests/components/lovelace/__init__.py new file mode 100644 index 00000000000..fea220146ca --- /dev/null +++ b/tests/components/lovelace/__init__.py @@ -0,0 +1 @@ +"""Tests for Lovelace.""" diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py new file mode 100644 index 00000000000..0fde6de902c --- /dev/null +++ b/tests/components/lovelace/test_init.py @@ -0,0 +1,120 @@ +"""Test the Lovelace initialization.""" +from unittest.mock import patch + +from homeassistant.exceptions import HomeAssistantError +from homeassistant.setup import async_setup_component +from homeassistant.components.websocket_api.const import TYPE_RESULT + + +async def test_deprecated_lovelace_ui(hass, hass_ws_client): + """Test lovelace_ui command.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.lovelace.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'] == TYPE_RESULT + assert msg['success'] + assert msg['result'] == {'hello': 'world'} + + +async def test_deprecated_lovelace_ui_not_found(hass, hass_ws_client): + """Test lovelace_ui command cannot find file.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.lovelace.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'] == TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'file_not_found' + + +async def test_deprecated_lovelace_ui_load_err(hass, hass_ws_client): + """Test lovelace_ui command cannot find file.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.lovelace.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'] == TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'load_error' + + +async def test_lovelace_ui(hass, hass_ws_client): + """Test lovelace_ui command.""" + await async_setup_component(hass, 'lovelace') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.lovelace.load_yaml', + return_value={'hello': 'world'}): + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == 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, 'lovelace') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.lovelace.load_yaml', + side_effect=FileNotFoundError): + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == 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, 'lovelace') + client = await hass_ws_client(hass) + + with patch('homeassistant.components.lovelace.load_yaml', + side_effect=HomeAssistantError): + await client.send_json({ + 'id': 5, + 'type': 'lovelace/config', + }) + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == TYPE_RESULT + assert msg['success'] is False + assert msg['error']['code'] == 'load_error' diff --git a/tests/components/media_player/common.py b/tests/components/media_player/common.py new file mode 100644 index 00000000000..3f4d4cb9f24 --- /dev/null +++ b/tests/components/media_player/common.py @@ -0,0 +1,150 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.media_player import ( + ATTR_INPUT_SOURCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, + ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_SEEK_POSITION, ATTR_MEDIA_VOLUME_LEVEL, + ATTR_MEDIA_VOLUME_MUTED, DOMAIN, SERVICE_CLEAR_PLAYLIST, + SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE) +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PREVIOUS_TRACK, + SERVICE_MEDIA_SEEK, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, + SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, + SERVICE_VOLUME_UP) +from homeassistant.loader import bind_hass + + +@bind_hass +def turn_on(hass, entity_id=None): + """Turn on specified media player or all.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_TURN_ON, data) + + +@bind_hass +def turn_off(hass, entity_id=None): + """Turn off specified media player or all.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) + + +@bind_hass +def toggle(hass, entity_id=None): + """Toggle specified media player or all.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_TOGGLE, data) + + +@bind_hass +def volume_up(hass, entity_id=None): + """Send the media player the command for volume up.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_VOLUME_UP, data) + + +@bind_hass +def volume_down(hass, entity_id=None): + """Send the media player the command for volume down.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN, data) + + +@bind_hass +def mute_volume(hass, mute, entity_id=None): + """Send the media player the command for muting the volume.""" + data = {ATTR_MEDIA_VOLUME_MUTED: mute} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE, data) + + +@bind_hass +def set_volume_level(hass, volume, entity_id=None): + """Send the media player the command for setting the volume.""" + data = {ATTR_MEDIA_VOLUME_LEVEL: volume} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_VOLUME_SET, data) + + +@bind_hass +def media_play_pause(hass, entity_id=None): + """Send the media player the command for play/pause.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, data) + + +@bind_hass +def media_play(hass, entity_id=None): + """Send the media player the command for play/pause.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY, data) + + +@bind_hass +def media_pause(hass, entity_id=None): + """Send the media player the command for pause.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_MEDIA_PAUSE, data) + + +@bind_hass +def media_next_track(hass, entity_id=None): + """Send the media player the command for next track.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, data) + + +@bind_hass +def media_previous_track(hass, entity_id=None): + """Send the media player the command for prev track.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data) + + +@bind_hass +def media_seek(hass, position, entity_id=None): + """Send the media player the command to seek in current playing media.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + data[ATTR_MEDIA_SEEK_POSITION] = position + hass.services.call(DOMAIN, SERVICE_MEDIA_SEEK, data) + + +@bind_hass +def play_media(hass, media_type, media_id, entity_id=None, enqueue=None): + """Send the media player the command for playing media.""" + data = {ATTR_MEDIA_CONTENT_TYPE: media_type, + ATTR_MEDIA_CONTENT_ID: media_id} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + if enqueue: + data[ATTR_MEDIA_ENQUEUE] = enqueue + + hass.services.call(DOMAIN, SERVICE_PLAY_MEDIA, data) + + +@bind_hass +def select_source(hass, source, entity_id=None): + """Send the media player the command to select input source.""" + data = {ATTR_INPUT_SOURCE: source} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SELECT_SOURCE, data) + + +@bind_hass +def clear_playlist(hass, entity_id=None): + """Send the media player the command for clear playlist.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_CLEAR_PLAYLIST, data) diff --git a/tests/components/media_player/test_demo.py b/tests/components/media_player/test_demo.py index 121018e7541..e986ac02065 100644 --- a/tests/components/media_player/test_demo.py +++ b/tests/components/media_player/test_demo.py @@ -12,6 +12,7 @@ from homeassistant.helpers.aiohttp_client import DATA_CLIENTSESSION import requests from tests.common import get_test_home_assistant, get_test_instance_port +from tests.components.media_player import common SERVER_PORT = get_test_instance_port() HTTP_BASE_URL = 'http://127.0.0.1:{}'.format(SERVER_PORT) @@ -42,12 +43,12 @@ class TestDemoMediaPlayer(unittest.TestCase): state = self.hass.states.get(entity_id) assert 'dvd' == state.attributes.get('source') - mp.select_source(self.hass, None, entity_id) + common.select_source(self.hass, None, entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) assert 'dvd' == state.attributes.get('source') - mp.select_source(self.hass, 'xbox', entity_id) + common.select_source(self.hass, 'xbox', entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) assert 'xbox' == state.attributes.get('source') @@ -59,7 +60,7 @@ class TestDemoMediaPlayer(unittest.TestCase): {'media_player': {'platform': 'demo'}}) assert self.hass.states.is_state(entity_id, 'playing') - mp.clear_playlist(self.hass, entity_id) + common.clear_playlist(self.hass, entity_id) self.hass.block_till_done() assert self.hass.states.is_state(entity_id, 'off') @@ -71,34 +72,34 @@ class TestDemoMediaPlayer(unittest.TestCase): state = self.hass.states.get(entity_id) assert 1.0 == state.attributes.get('volume_level') - mp.set_volume_level(self.hass, None, entity_id) + common.set_volume_level(self.hass, None, entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) assert 1.0 == state.attributes.get('volume_level') - mp.set_volume_level(self.hass, 0.5, entity_id) + common.set_volume_level(self.hass, 0.5, entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) assert 0.5 == state.attributes.get('volume_level') - mp.volume_down(self.hass, entity_id) + common.volume_down(self.hass, entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) assert 0.4 == state.attributes.get('volume_level') - mp.volume_up(self.hass, entity_id) + common.volume_up(self.hass, entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) assert 0.5 == state.attributes.get('volume_level') assert False is state.attributes.get('is_volume_muted') - mp.mute_volume(self.hass, None, entity_id) + common.mute_volume(self.hass, None, entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) assert False is state.attributes.get('is_volume_muted') - mp.mute_volume(self.hass, True, entity_id) + common.mute_volume(self.hass, True, entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) assert True is state.attributes.get('is_volume_muted') @@ -110,16 +111,16 @@ class TestDemoMediaPlayer(unittest.TestCase): {'media_player': {'platform': 'demo'}}) assert self.hass.states.is_state(entity_id, 'playing') - mp.turn_off(self.hass, entity_id) + common.turn_off(self.hass, entity_id) self.hass.block_till_done() assert self.hass.states.is_state(entity_id, 'off') assert not mp.is_on(self.hass, entity_id) - mp.turn_on(self.hass, entity_id) + common.turn_on(self.hass, entity_id) self.hass.block_till_done() assert self.hass.states.is_state(entity_id, 'playing') - mp.toggle(self.hass, entity_id) + common.toggle(self.hass, entity_id) self.hass.block_till_done() assert self.hass.states.is_state(entity_id, 'off') assert not mp.is_on(self.hass, entity_id) @@ -131,19 +132,19 @@ class TestDemoMediaPlayer(unittest.TestCase): {'media_player': {'platform': 'demo'}}) assert self.hass.states.is_state(entity_id, 'playing') - mp.media_pause(self.hass, entity_id) + common.media_pause(self.hass, entity_id) self.hass.block_till_done() assert self.hass.states.is_state(entity_id, 'paused') - mp.media_play_pause(self.hass, entity_id) + common.media_play_pause(self.hass, entity_id) self.hass.block_till_done() assert self.hass.states.is_state(entity_id, 'playing') - mp.media_play_pause(self.hass, entity_id) + common.media_play_pause(self.hass, entity_id) self.hass.block_till_done() assert self.hass.states.is_state(entity_id, 'paused') - mp.media_play(self.hass, entity_id) + common.media_play(self.hass, entity_id) self.hass.block_till_done() assert self.hass.states.is_state(entity_id, 'playing') @@ -155,17 +156,17 @@ class TestDemoMediaPlayer(unittest.TestCase): state = self.hass.states.get(entity_id) assert 1 == state.attributes.get('media_track') - mp.media_next_track(self.hass, entity_id) + common.media_next_track(self.hass, entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) assert 2 == state.attributes.get('media_track') - mp.media_next_track(self.hass, entity_id) + common.media_next_track(self.hass, entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) assert 3 == state.attributes.get('media_track') - mp.media_previous_track(self.hass, entity_id) + common.media_previous_track(self.hass, entity_id) self.hass.block_till_done() state = self.hass.states.get(entity_id) assert 2 == state.attributes.get('media_track') @@ -177,12 +178,12 @@ class TestDemoMediaPlayer(unittest.TestCase): state = self.hass.states.get(ent_id) assert 1 == state.attributes.get('media_episode') - mp.media_next_track(self.hass, ent_id) + common.media_next_track(self.hass, ent_id) self.hass.block_till_done() state = self.hass.states.get(ent_id) assert 2 == state.attributes.get('media_episode') - mp.media_previous_track(self.hass, ent_id) + common.media_previous_track(self.hass, ent_id) self.hass.block_till_done() state = self.hass.states.get(ent_id) assert 1 == state.attributes.get('media_episode') @@ -200,14 +201,14 @@ class TestDemoMediaPlayer(unittest.TestCase): state.attributes.get('supported_features')) assert state.attributes.get('media_content_id') is not None - mp.play_media(self.hass, None, 'some_id', ent_id) + common.play_media(self.hass, None, 'some_id', ent_id) self.hass.block_till_done() state = self.hass.states.get(ent_id) assert 0 < (mp.SUPPORT_PLAY_MEDIA & state.attributes.get('supported_features')) assert not 'some_id' == state.attributes.get('media_content_id') - mp.play_media(self.hass, 'youtube', 'some_id', ent_id) + common.play_media(self.hass, 'youtube', 'some_id', ent_id) self.hass.block_till_done() state = self.hass.states.get(ent_id) assert 0 < (mp.SUPPORT_PLAY_MEDIA & @@ -215,10 +216,10 @@ class TestDemoMediaPlayer(unittest.TestCase): assert 'some_id' == state.attributes.get('media_content_id') assert not mock_seek.called - mp.media_seek(self.hass, None, ent_id) + common.media_seek(self.hass, None, ent_id) self.hass.block_till_done() assert not mock_seek.called - mp.media_seek(self.hass, 100, ent_id) + common.media_seek(self.hass, 100, ent_id) self.hass.block_till_done() assert mock_seek.called diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index 5d632d4de0b..808c6e4f50f 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -3,7 +3,7 @@ import base64 from unittest.mock import patch from homeassistant.setup import async_setup_component -from homeassistant.components import websocket_api +from homeassistant.components.websocket_api.const import TYPE_RESULT from tests.common import mock_coro @@ -30,7 +30,7 @@ async def test_get_panels(hass, hass_ws_client): msg = await client.receive_json() assert msg['id'] == 5 - assert msg['type'] == websocket_api.TYPE_RESULT + assert msg['type'] == TYPE_RESULT assert msg['success'] assert msg['result']['content_type'] == 'image/jpeg' assert msg['result']['content'] == \ diff --git a/tests/components/media_player/test_samsungtv.py b/tests/components/media_player/test_samsungtv.py index 2fedfb6a65e..5551a86df05 100644 --- a/tests/components/media_player/test_samsungtv.py +++ b/tests/components/media_player/test_samsungtv.py @@ -128,6 +128,23 @@ class TestSamsungTv(unittest.TestCase): self.device.update() self.assertEqual(STATE_OFF, self.device._state) + @mock.patch( + 'homeassistant.components.media_player.samsungtv.subprocess.Popen' + ) + def test_timeout(self, mocked_popen): + """Test timeout use.""" + ping = mock.Mock() + mocked_popen.return_value = ping + ping.returncode = 0 + self.device.update() + expected_timeout = self.device._config['timeout'] + timeout_arg = '-W{}'.format(expected_timeout) + ping_command = [ + 'ping', '-n', '-q', '-c3', timeout_arg, 'fake'] + expected_call = call(ping_command, stderr=-3, stdout=-1) + self.assertEqual(mocked_popen.call_args, expected_call) + self.assertEqual(STATE_ON, self.device._state) + def test_send_key(self): """Test for send key.""" self.device.send_key('KEY_POWER') diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py index cb3da3ab899..cfe969a25c4 100644 --- a/tests/components/media_player/test_sonos.py +++ b/tests/components/media_player/test_sonos.py @@ -57,7 +57,7 @@ class SoCoMock(): self.is_visible = True self.volume = 50 self.mute = False - self.play_mode = 'NORMAL' + self.shuffle = False self.night_mode = False self.dialog_mode = False self.music_library = MusicLibraryMock() diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 9f6be60c68b..08bb4e54a39 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -5,7 +5,7 @@ import pytest from homeassistant.setup import async_setup_component -from tests.common import mock_coro +from tests.common import mock_coro, MockConfigEntry @pytest.fixture(autouse=True) @@ -88,3 +88,67 @@ async def test_manual_config_set(hass, mock_try_connection, result = await hass.config_entries.flow.async_init( 'mqtt', context={'source': 'user'}) assert result['type'] == 'abort' + + +async def test_user_single_instance(hass): + """Test we only allow a single config flow.""" + MockConfigEntry(domain='mqtt').add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + 'mqtt', context={'source': 'user'}) + assert result['type'] == 'abort' + assert result['reason'] == 'single_instance_allowed' + + +async def test_hassio_single_instance(hass): + """Test we only allow a single config flow.""" + MockConfigEntry(domain='mqtt').add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + 'mqtt', context={'source': 'hassio'}) + assert result['type'] == 'abort' + assert result['reason'] == 'single_instance_allowed' + + +async def test_hassio_confirm(hass, mock_try_connection, + mock_finish_setup): + """Test we can finish a config flow.""" + mock_try_connection.return_value = True + + result = await hass.config_entries.flow.async_init( + 'mqtt', + data={ + 'addon': 'Mock Addon', + 'broker': 'mock-broker', + 'port': 1883, + 'username': 'mock-user', + 'password': 'mock-pass', + 'protocol': '3.1.1' + }, + context={'source': 'hassio'} + ) + assert result['type'] == 'form' + assert result['step_id'] == 'hassio_confirm' + assert result['description_placeholders'] == { + 'addon': 'Mock Addon', + } + + result = await hass.config_entries.flow.async_configure( + result['flow_id'], { + 'discovery': True, + } + ) + + assert result['type'] == 'create_entry' + assert result['result'].data == { + 'broker': 'mock-broker', + 'port': 1883, + 'username': 'mock-user', + 'password': 'mock-pass', + 'protocol': '3.1.1', + 'discovery': True, + } + # Check we tried the connection + assert len(mock_try_connection.mock_calls) == 1 + # Check config entry got setup + assert len(mock_finish_setup.mock_calls) == 1 diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 6de277eb48d..36b022de7a6 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -2,18 +2,23 @@ import asyncio from unittest.mock import patch +from homeassistant.components import mqtt from homeassistant.components.mqtt.discovery import async_start, \ ALREADY_DISCOVERED -from tests.common import async_fire_mqtt_message, mock_coro +from tests.common import async_fire_mqtt_message, mock_coro, MockConfigEntry @asyncio.coroutine def test_subscribing_config_topic(hass, mqtt_mock): """Test setting up discovery.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN, data={ + mqtt.CONF_BROKER: 'test-broker' + }) + hass_config = {} discovery_topic = 'homeassistant' - yield from async_start(hass, discovery_topic, hass_config) + yield from async_start(hass, discovery_topic, hass_config, entry) assert mqtt_mock.async_subscribe.called call_args = mqtt_mock.async_subscribe.mock_calls[0][1] @@ -25,8 +30,12 @@ def test_subscribing_config_topic(hass, mqtt_mock): @asyncio.coroutine def test_invalid_topic(mock_load_platform, hass, mqtt_mock): """Test sending to invalid topic.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN, data={ + mqtt.CONF_BROKER: 'test-broker' + }) + mock_load_platform.return_value = mock_coro() - yield from async_start(hass, 'homeassistant', {}) + yield from async_start(hass, 'homeassistant', {}, entry) async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/not_config', '{}') @@ -38,8 +47,12 @@ def test_invalid_topic(mock_load_platform, hass, mqtt_mock): @asyncio.coroutine def test_invalid_json(mock_load_platform, hass, mqtt_mock, caplog): """Test sending in invalid JSON.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN, data={ + mqtt.CONF_BROKER: 'test-broker' + }) + mock_load_platform.return_value = mock_coro() - yield from async_start(hass, 'homeassistant', {}) + yield from async_start(hass, 'homeassistant', {}, entry) async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', 'not json') @@ -52,10 +65,12 @@ def test_invalid_json(mock_load_platform, hass, mqtt_mock, caplog): @asyncio.coroutine def test_only_valid_components(mock_load_platform, hass, mqtt_mock, caplog): """Test for a valid component.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + invalid_component = "timer" mock_load_platform.return_value = mock_coro() - yield from async_start(hass, 'homeassistant', {}) + yield from async_start(hass, 'homeassistant', {}, entry) async_fire_mqtt_message(hass, 'homeassistant/{}/bla/config'.format( invalid_component @@ -73,7 +88,9 @@ def test_only_valid_components(mock_load_platform, hass, mqtt_mock, caplog): @asyncio.coroutine def test_correct_config_discovery(hass, mqtt_mock, caplog): """Test sending in correct JSON.""" - yield from async_start(hass, 'homeassistant', {}) + entry = MockConfigEntry(domain=mqtt.DOMAIN) + + yield from async_start(hass, 'homeassistant', {}, entry) async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', '{ "name": "Beer" }') @@ -89,7 +106,9 @@ def test_correct_config_discovery(hass, mqtt_mock, caplog): @asyncio.coroutine def test_discover_fan(hass, mqtt_mock, caplog): """Test discovering an MQTT fan.""" - yield from async_start(hass, 'homeassistant', {}) + entry = MockConfigEntry(domain=mqtt.DOMAIN) + + yield from async_start(hass, 'homeassistant', {}, entry) async_fire_mqtt_message(hass, 'homeassistant/fan/bla/config', ('{ "name": "Beer",' @@ -106,7 +125,9 @@ def test_discover_fan(hass, mqtt_mock, caplog): @asyncio.coroutine def test_discover_climate(hass, mqtt_mock, caplog): """Test discovering an MQTT climate component.""" - yield from async_start(hass, 'homeassistant', {}) + entry = MockConfigEntry(domain=mqtt.DOMAIN) + + yield from async_start(hass, 'homeassistant', {}, entry) data = ( '{ "name": "ClimateTest",' @@ -127,7 +148,9 @@ def test_discover_climate(hass, mqtt_mock, caplog): @asyncio.coroutine def test_discover_alarm_control_panel(hass, mqtt_mock, caplog): """Test discovering an MQTT alarm control panel component.""" - yield from async_start(hass, 'homeassistant', {}) + entry = MockConfigEntry(domain=mqtt.DOMAIN) + + yield from async_start(hass, 'homeassistant', {}, entry) data = ( '{ "name": "AlarmControlPanelTest",' @@ -149,7 +172,9 @@ def test_discover_alarm_control_panel(hass, mqtt_mock, caplog): @asyncio.coroutine def test_discovery_incl_nodeid(hass, mqtt_mock, caplog): """Test sending in correct JSON with optional node_id included.""" - yield from async_start(hass, 'homeassistant', {}) + entry = MockConfigEntry(domain=mqtt.DOMAIN) + + yield from async_start(hass, 'homeassistant', {}, entry) async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/my_node_id/bla' '/config', '{ "name": "Beer" }') @@ -165,7 +190,9 @@ def test_discovery_incl_nodeid(hass, mqtt_mock, caplog): @asyncio.coroutine def test_non_duplicate_discovery(hass, mqtt_mock, caplog): """Test for a non duplicate component.""" - yield from async_start(hass, 'homeassistant', {}) + entry = MockConfigEntry(domain=mqtt.DOMAIN) + + yield from async_start(hass, 'homeassistant', {}, entry) async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', '{ "name": "Beer" }') @@ -181,31 +208,3 @@ def test_non_duplicate_discovery(hass, mqtt_mock, caplog): assert state_duplicate is None assert 'Component has already been discovered: ' \ 'binary_sensor bla' in caplog.text - - -@asyncio.coroutine -def test_discovery_removal(hass, mqtt_mock, caplog): - """Test expansion of abbreviated discovery payload.""" - yield from async_start(hass, 'homeassistant', {}) - - data = ( - '{ "name": "Beer",' - ' "status_topic": "test_topic",' - ' "command_topic": "test_topic" }' - ) - - async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config', - data) - yield from hass.async_block_till_done() - - state = hass.states.get('switch.beer') - assert state is not None - assert state.name == 'Beer' - - async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config', - '') - yield from hass.async_block_till_done() - yield from hass.async_block_till_done() - - state = hass.states.get('switch.beer') - assert state is None diff --git a/tests/components/notify/common.py b/tests/components/notify/common.py new file mode 100644 index 00000000000..42b4b35b63f --- /dev/null +++ b/tests/components/notify/common.py @@ -0,0 +1,24 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.notify import ( + ATTR_DATA, ATTR_MESSAGE, ATTR_TITLE, DOMAIN, SERVICE_NOTIFY) +from homeassistant.loader import bind_hass + + +@bind_hass +def send_message(hass, message, title=None, data=None): + """Send a notification message.""" + info = { + ATTR_MESSAGE: message + } + + if title is not None: + info[ATTR_TITLE] = title + + if data is not None: + info[ATTR_DATA] = data + + hass.services.call(DOMAIN, SERVICE_NOTIFY, info) diff --git a/tests/components/notify/test_demo.py b/tests/components/notify/test_demo.py index f9c107a447e..3be2ddcb86b 100644 --- a/tests/components/notify/test_demo.py +++ b/tests/components/notify/test_demo.py @@ -7,7 +7,9 @@ from homeassistant.setup import setup_component from homeassistant.components.notify import demo from homeassistant.core import callback from homeassistant.helpers import discovery, script + from tests.common import assert_setup_component, get_test_home_assistant +from tests.components.notify import common CONFIG = { notify.DOMAIN: { @@ -79,7 +81,7 @@ class TestNotifyDemo(unittest.TestCase): def test_sending_none_message(self): """Test send with None as message.""" self._setup_notify() - notify.send_message(self.hass, None) + common.send_message(self.hass, None) self.hass.block_till_done() self.assertTrue(len(self.events) == 0) @@ -87,7 +89,7 @@ class TestNotifyDemo(unittest.TestCase): """Send a templated message.""" self._setup_notify() self.hass.states.set('sensor.temperature', 10) - notify.send_message(self.hass, '{{ states.sensor.temperature.state }}', + common.send_message(self.hass, '{{ states.sensor.temperature.state }}', '{{ states.sensor.temperature.name }}') self.hass.block_till_done() last_event = self.events[-1] @@ -97,7 +99,7 @@ class TestNotifyDemo(unittest.TestCase): def test_method_forwards_correct_data(self): """Test that all data from the service gets forwarded to service.""" self._setup_notify() - notify.send_message(self.hass, 'my message', 'my title', + common.send_message(self.hass, 'my message', 'my title', {'hello': 'world'}) self.hass.block_till_done() self.assertTrue(len(self.events) == 1) diff --git a/tests/components/notify/test_yessssms.py b/tests/components/notify/test_yessssms.py new file mode 100644 index 00000000000..42dd7be1aca --- /dev/null +++ b/tests/components/notify/test_yessssms.py @@ -0,0 +1,208 @@ +"""The tests for the notify yessssms platform.""" +import unittest +import requests_mock +from homeassistant.components.notify import yessssms + + +class TestNotifyYesssSMS(unittest.TestCase): + """Test the yessssms notify.""" + + def setUp(self): # pylint: disable=invalid-name + """Set up things to be run when tests are started.""" + login = "06641234567" + passwd = "testpasswd" + recipient = "06501234567" + self.yessssms = yessssms.YesssSMSNotificationService( + login, passwd, recipient) + + @requests_mock.Mocker() + def test_login_error(self, mock): + """Test login that fails.""" + mock.register_uri( + requests_mock.POST, + # pylint: disable=protected-access + self.yessssms.yesss._login_url, + status_code=200, + text="BlaBlaBlaLogin nicht erfolgreichBlaBla" + ) + + message = "Testing YesssSMS platform :)" + + with self.assertLogs("homeassistant.components.notify", + level='ERROR'): + self.yessssms.send_message(message) + self.assertTrue(mock.called) + self.assertEqual(mock.call_count, 1) + + def test_empty_message_error(self): + """Test for an empty SMS message error.""" + message = "" + with self.assertLogs("homeassistant.components.notify", + level='ERROR'): + self.yessssms.send_message(message) + + @requests_mock.Mocker() + def test_error_account_suspended(self, mock): + """Test login that fails after multiple attempts.""" + mock.register_uri( + 'POST', + # pylint: disable=protected-access + self.yessssms.yesss._login_url, + status_code=200, + text="BlaBlaBlaLogin nicht erfolgreichBlaBla" + ) + + message = "Testing YesssSMS platform :)" + + with self.assertLogs("homeassistant.components.notify", + level='ERROR'): + self.yessssms.send_message(message) + self.assertTrue(mock.called) + self.assertEqual(mock.call_count, 1) + + mock.register_uri( + 'POST', + # pylint: disable=protected-access + self.yessssms.yesss._login_url, + status_code=200, + text="Wegen 3 ungültigen Login-Versuchen ist Ihr Account für " + "eine Stunde gesperrt." + ) + + message = "Testing YesssSMS platform :)" + + with self.assertLogs("homeassistant.components.notify", + level='ERROR'): + self.yessssms.send_message(message) + self.assertTrue(mock.called) + self.assertEqual(mock.call_count, 2) + + def test_error_account_suspended_2(self): + """Test login that fails after multiple attempts.""" + message = "Testing YesssSMS platform :)" + # pylint: disable=protected-access + self.yessssms.yesss._suspended = True + + with self.assertLogs("homeassistant.components.notify", + level='ERROR') as context: + self.yessssms.send_message(message) + self.assertIn("Account is suspended, cannot send SMS.", + context.output[0]) + + @requests_mock.Mocker() + def test_send_message(self, mock): + """Test send message.""" + message = "Testing YesssSMS platform :)" + mock.register_uri( + 'POST', + # pylint: disable=protected-access + self.yessssms.yesss._login_url, + status_code=302, + # pylint: disable=protected-access + headers={'location': self.yessssms.yesss._kontomanager} + ) + # pylint: disable=protected-access + login = self.yessssms.yesss._logindata['login_rufnummer'] + mock.register_uri( + 'GET', + # pylint: disable=protected-access + self.yessssms.yesss._kontomanager, + status_code=200, + text="test..." + login + "" + ) + mock.register_uri( + 'POST', + # pylint: disable=protected-access + self.yessssms.yesss._websms_url, + status_code=200, + text="

Ihre SMS wurde erfolgreich verschickt!

" + ) + mock.register_uri( + 'GET', + # pylint: disable=protected-access + self.yessssms.yesss._logout_url, + status_code=200, + ) + + with self.assertLogs("homeassistant.components.notify", + level='INFO') as context: + self.yessssms.send_message(message) + self.assertIn("SMS sent", context.output[0]) + self.assertTrue(mock.called) + self.assertEqual(mock.call_count, 4) + self.assertIn(mock.last_request.scheme + "://" + + mock.last_request.hostname + + mock.last_request.path + "?" + + mock.last_request.query, + # pylint: disable=protected-access + self.yessssms.yesss._logout_url) + + def test_no_recipient_error(self): + """Test for missing/empty recipient.""" + message = "Testing YesssSMS platform :)" + # pylint: disable=protected-access + self.yessssms._recipient = "" + + with self.assertLogs("homeassistant.components.notify", + level='ERROR') as context: + self.yessssms.send_message(message) + + self.assertIn("You need to provide a recipient for SMS notification", + context.output[0]) + + @requests_mock.Mocker() + def test_sms_sending_error(self, mock): + """Test sms sending error.""" + mock.register_uri( + 'POST', + # pylint: disable=protected-access + self.yessssms.yesss._login_url, + status_code=302, + # pylint: disable=protected-access + headers={'location': self.yessssms.yesss._kontomanager} + ) + # pylint: disable=protected-access + login = self.yessssms.yesss._logindata['login_rufnummer'] + mock.register_uri( + 'GET', + # pylint: disable=protected-access + self.yessssms.yesss._kontomanager, + status_code=200, + text="test..." + login + "" + ) + mock.register_uri( + 'POST', + # pylint: disable=protected-access + self.yessssms.yesss._websms_url, + status_code=500 + ) + + message = "Testing YesssSMS platform :)" + + with self.assertLogs("homeassistant.components.notify", + level='ERROR') as context: + self.yessssms.send_message(message) + + self.assertTrue(mock.called) + self.assertEqual(mock.call_count, 3) + self.assertIn("YesssSMS: error sending SMS", context.output[0]) + + @requests_mock.Mocker() + def test_connection_error(self, mock): + """Test connection error.""" + mock.register_uri( + 'POST', + # pylint: disable=protected-access + self.yessssms.yesss._login_url, + exc=ConnectionError + ) + + message = "Testing YesssSMS platform :)" + + with self.assertLogs("homeassistant.components.notify", + level='ERROR') as context: + self.yessssms.send_message(message) + + self.assertTrue(mock.called) + self.assertEqual(mock.call_count, 1) + self.assertIn("unable to connect", context.output[0]) diff --git a/tests/components/persistent_notification/test_init.py b/tests/components/persistent_notification/test_init.py index 6acc796a108..43b91e2622a 100644 --- a/tests/components/persistent_notification/test_init.py +++ b/tests/components/persistent_notification/test_init.py @@ -1,5 +1,5 @@ """The tests for the persistent notification component.""" -from homeassistant.components import websocket_api +from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.setup import setup_component, async_setup_component import homeassistant.components.persistent_notification as pn @@ -41,6 +41,7 @@ class TestPersistentNotification: assert notification['status'] == pn.STATUS_UNREAD assert notification['message'] == 'Hello World 2' assert notification['title'] == '2 beers' + assert notification['created_at'] is not None notifications.clear() def test_create_notification_id(self): @@ -151,7 +152,7 @@ async def test_ws_get_notifications(hass, hass_ws_client): }) msg = await client.receive_json() assert msg['id'] == 5 - assert msg['type'] == websocket_api.TYPE_RESULT + assert msg['type'] == TYPE_RESULT assert msg['success'] notifications = msg['result'] assert len(notifications) == 0 @@ -165,7 +166,7 @@ async def test_ws_get_notifications(hass, hass_ws_client): }) msg = await client.receive_json() assert msg['id'] == 6 - assert msg['type'] == websocket_api.TYPE_RESULT + assert msg['type'] == TYPE_RESULT assert msg['success'] notifications = msg['result'] assert len(notifications) == 1 @@ -174,6 +175,7 @@ async def test_ws_get_notifications(hass, hass_ws_client): assert notification['message'] == 'test' assert notification['title'] is None assert notification['status'] == pn.STATUS_UNREAD + assert notification['created_at'] is not None # Mark Read await hass.services.async_call(pn.DOMAIN, pn.SERVICE_MARK_READ, { diff --git a/tests/components/remote/common.py b/tests/components/remote/common.py new file mode 100644 index 00000000000..d03cf5d6d16 --- /dev/null +++ b/tests/components/remote/common.py @@ -0,0 +1,55 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.remote import ( + ATTR_ACTIVITY, ATTR_COMMAND, ATTR_DELAY_SECS, ATTR_DEVICE, + ATTR_NUM_REPEATS, DOMAIN, SERVICE_SEND_COMMAND) +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON) +from homeassistant.loader import bind_hass + + +@bind_hass +def turn_on(hass, activity=None, entity_id=None): + """Turn all or specified remote on.""" + data = { + key: value for key, value in [ + (ATTR_ACTIVITY, activity), + (ATTR_ENTITY_ID, entity_id), + ] if value is not None} + hass.services.call(DOMAIN, SERVICE_TURN_ON, data) + + +@bind_hass +def turn_off(hass, activity=None, entity_id=None): + """Turn all or specified remote off.""" + data = {} + if activity: + data[ATTR_ACTIVITY] = activity + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) + + +@bind_hass +def send_command(hass, command, entity_id=None, device=None, + num_repeats=None, delay_secs=None): + """Send a command to a device.""" + data = {ATTR_COMMAND: command} + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + if device: + data[ATTR_DEVICE] = device + + if num_repeats: + data[ATTR_NUM_REPEATS] = num_repeats + + if delay_secs: + data[ATTR_DELAY_SECS] = delay_secs + + hass.services.call(DOMAIN, SERVICE_SEND_COMMAND, data) diff --git a/tests/components/remote/test_demo.py b/tests/components/remote/test_demo.py index a0290987ff2..fbf7230c237 100644 --- a/tests/components/remote/test_demo.py +++ b/tests/components/remote/test_demo.py @@ -5,7 +5,9 @@ import unittest from homeassistant.setup import setup_component import homeassistant.components.remote as remote from homeassistant.const import STATE_ON, STATE_OFF + from tests.common import get_test_home_assistant +from tests.components.remote import common ENTITY_ID = 'remote.remote_one' @@ -28,22 +30,22 @@ class TestDemoRemote(unittest.TestCase): def test_methods(self): """Test if services call the entity methods as expected.""" - remote.turn_on(self.hass, entity_id=ENTITY_ID) + common.turn_on(self.hass, entity_id=ENTITY_ID) self.hass.block_till_done() state = self.hass.states.get(ENTITY_ID) self.assertEqual(state.state, STATE_ON) - remote.turn_off(self.hass, entity_id=ENTITY_ID) + common.turn_off(self.hass, entity_id=ENTITY_ID) self.hass.block_till_done() state = self.hass.states.get(ENTITY_ID) self.assertEqual(state.state, STATE_OFF) - remote.turn_on(self.hass, entity_id=ENTITY_ID) + common.turn_on(self.hass, entity_id=ENTITY_ID) self.hass.block_till_done() state = self.hass.states.get(ENTITY_ID) self.assertEqual(state.state, STATE_ON) - remote.send_command(self.hass, 'test', entity_id=ENTITY_ID) + common.send_command(self.hass, 'test', entity_id=ENTITY_ID) self.hass.block_till_done() state = self.hass.states.get(ENTITY_ID) self.assertEqual( diff --git a/tests/components/remote/test_init.py b/tests/components/remote/test_init.py index d98ec941f8b..21a083e3b9a 100644 --- a/tests/components/remote/test_init.py +++ b/tests/components/remote/test_init.py @@ -10,6 +10,8 @@ from homeassistant.const import ( import homeassistant.components.remote as remote from tests.common import mock_service, get_test_home_assistant +from tests.components.remote import common + TEST_PLATFORM = {remote.DOMAIN: {CONF_PLATFORM: 'test'}} SERVICE_SEND_COMMAND = 'send_command' @@ -46,7 +48,7 @@ class TestRemote(unittest.TestCase): turn_on_calls = mock_service( self.hass, remote.DOMAIN, SERVICE_TURN_ON) - remote.turn_on( + common.turn_on( self.hass, entity_id='entity_id_val') @@ -62,7 +64,7 @@ class TestRemote(unittest.TestCase): turn_off_calls = mock_service( self.hass, remote.DOMAIN, SERVICE_TURN_OFF) - remote.turn_off( + common.turn_off( self.hass, entity_id='entity_id_val') self.hass.block_till_done() @@ -79,7 +81,7 @@ class TestRemote(unittest.TestCase): send_command_calls = mock_service( self.hass, remote.DOMAIN, SERVICE_SEND_COMMAND) - remote.send_command( + common.send_command( self.hass, entity_id='entity_id_val', device='test_device', command=['test_command'], num_repeats='4', delay_secs='0.6') diff --git a/tests/components/scene/common.py b/tests/components/scene/common.py new file mode 100644 index 00000000000..4f8123ca638 --- /dev/null +++ b/tests/components/scene/common.py @@ -0,0 +1,19 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.scene import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON +from homeassistant.loader import bind_hass + + +@bind_hass +def activate(hass, entity_id=None): + """Activate a scene.""" + data = {} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_TURN_ON, data) diff --git a/tests/components/scene/test_init.py b/tests/components/scene/test_init.py index 3298d7648d9..81ab5f89c25 100644 --- a/tests/components/scene/test_init.py +++ b/tests/components/scene/test_init.py @@ -8,6 +8,8 @@ from homeassistant.components import light, scene from homeassistant.util import yaml from tests.common import get_test_home_assistant +from tests.components.light import common as common_light +from tests.components.scene import common class TestScene(unittest.TestCase): @@ -25,7 +27,7 @@ class TestScene(unittest.TestCase): self.light_1, self.light_2 = test_light.DEVICES[0:2] - light.turn_off( + common_light.turn_off( self.hass, [self.light_1.entity_id, self.light_2.entity_id]) self.hass.block_till_done() @@ -68,7 +70,7 @@ class TestScene(unittest.TestCase): }] })) - scene.activate(self.hass, 'scene.test') + common.activate(self.hass, 'scene.test') self.hass.block_till_done() self.assertTrue(self.light_1.is_on) @@ -94,7 +96,7 @@ class TestScene(unittest.TestCase): doc = yaml.yaml.safe_load(file) self.assertTrue(setup_component(self.hass, scene.DOMAIN, doc)) - scene.activate(self.hass, 'scene.test') + common.activate(self.hass, 'scene.test') self.hass.block_till_done() self.assertTrue(self.light_1.is_on) @@ -117,7 +119,7 @@ class TestScene(unittest.TestCase): }] })) - scene.activate(self.hass, 'scene.test') + common.activate(self.hass, 'scene.test') self.hass.block_till_done() self.assertTrue(self.light_1.is_on) diff --git a/tests/components/scene/test_litejet.py b/tests/components/scene/test_litejet.py index 864ffc41735..57d3c178dbd 100644 --- a/tests/components/scene/test_litejet.py +++ b/tests/components/scene/test_litejet.py @@ -5,8 +5,9 @@ from unittest import mock from homeassistant import setup from homeassistant.components import litejet + from tests.common import get_test_home_assistant -import homeassistant.components.scene as scene +from tests.components.scene import common _LOGGER = logging.getLogger(__name__) @@ -59,7 +60,7 @@ class TestLiteJetScene(unittest.TestCase): def test_activate(self): """Test activating the scene.""" - scene.activate(self.hass, ENTITY_SCENE) + common.activate(self.hass, ENTITY_SCENE) self.hass.block_till_done() self.mock_lj.activate_scene.assert_called_once_with( ENTITY_SCENE_NUMBER) diff --git a/tests/components/sensor/test_geo_rss_events.py b/tests/components/sensor/test_geo_rss_events.py index cc57c801430..21538d458bc 100644 --- a/tests/components/sensor/test_geo_rss_events.py +++ b/tests/components/sensor/test_geo_rss_events.py @@ -2,26 +2,38 @@ import unittest from unittest import mock import sys +from unittest.mock import MagicMock, patch -import feedparser import pytest +from homeassistant.components import sensor +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, ATTR_FRIENDLY_NAME, \ + EVENT_HOMEASSISTANT_START, ATTR_ICON from homeassistant.setup import setup_component -from tests.common import load_fixture, get_test_home_assistant +from tests.common import get_test_home_assistant, \ + assert_setup_component, fire_time_changed import homeassistant.components.sensor.geo_rss_events as geo_rss_events +import homeassistant.util.dt as dt_util URL = 'http://geo.rss.local/geo_rss_events.xml' VALID_CONFIG_WITH_CATEGORIES = { - 'platform': 'geo_rss_events', - geo_rss_events.CONF_URL: URL, - geo_rss_events.CONF_CATEGORIES: [ - 'Category 1', - 'Category 2' + sensor.DOMAIN: [ + { + 'platform': 'geo_rss_events', + geo_rss_events.CONF_URL: URL, + geo_rss_events.CONF_CATEGORIES: [ + 'Category 1' + ] + } ] } -VALID_CONFIG_WITHOUT_CATEGORIES = { - 'platform': 'geo_rss_events', - geo_rss_events.CONF_URL: URL +VALID_CONFIG = { + sensor.DOMAIN: [ + { + 'platform': 'geo_rss_events', + geo_rss_events.CONF_URL: URL + } + ] } @@ -34,119 +46,114 @@ class TestGeoRssServiceUpdater(unittest.TestCase): def setUp(self): """Initialize values for this testcase class.""" self.hass = get_test_home_assistant() - self.config = VALID_CONFIG_WITHOUT_CATEGORIES + # self.config = VALID_CONFIG_WITHOUT_CATEGORIES def tearDown(self): """Stop everything that was started.""" self.hass.stop() - @mock.patch('feedparser.parse', return_value=feedparser.parse("")) - def test_setup_with_categories(self, mock_parse): - """Test the general setup of this sensor.""" - self.config = VALID_CONFIG_WITH_CATEGORIES - self.assertTrue( - setup_component(self.hass, 'sensor', {'sensor': self.config})) - self.assertIsNotNone( - self.hass.states.get('sensor.event_service_category_1')) - self.assertIsNotNone( - self.hass.states.get('sensor.event_service_category_2')) + @staticmethod + def _generate_mock_feed_entry(external_id, title, distance_to_home, + coordinates, category): + """Construct a mock feed entry for testing purposes.""" + feed_entry = MagicMock() + feed_entry.external_id = external_id + feed_entry.title = title + feed_entry.distance_to_home = distance_to_home + feed_entry.coordinates = coordinates + feed_entry.category = category + return feed_entry - @mock.patch('feedparser.parse', return_value=feedparser.parse("")) - def test_setup_without_categories(self, mock_parse): - """Test the general setup of this sensor.""" - self.assertTrue( - setup_component(self.hass, 'sensor', {'sensor': self.config})) - self.assertIsNotNone(self.hass.states.get('sensor.event_service_any')) + @mock.patch('georss_client.generic_feed.GenericFeed') + def test_setup(self, mock_feed): + """Test the general setup of the platform.""" + # Set up some mock feed entries for this test. + mock_entry_1 = self._generate_mock_feed_entry('1234', 'Title 1', 15.5, + (-31.0, 150.0), + 'Category 1') + mock_entry_2 = self._generate_mock_feed_entry('2345', 'Title 2', 20.5, + (-31.1, 150.1), + 'Category 1') + mock_feed.return_value.update.return_value = 'OK', [mock_entry_1, + mock_entry_2] - def setup_data(self, url='url'): - """Set up data object for use by sensors.""" - home_latitude = -33.865 - home_longitude = 151.209444 - radius_in_km = 500 - data = geo_rss_events.GeoRssServiceData(home_latitude, - home_longitude, url, - radius_in_km) - return data + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch('homeassistant.util.dt.utcnow', return_value=utcnow): + with assert_setup_component(1, sensor.DOMAIN): + self.assertTrue(setup_component(self.hass, sensor.DOMAIN, + VALID_CONFIG)) + # Artificially trigger update. + self.hass.bus.fire(EVENT_HOMEASSISTANT_START) + # Collect events. + self.hass.block_till_done() - def test_update_sensor_with_category(self): - """Test updating sensor object.""" - raw_data = load_fixture('geo_rss_events.xml') - # Loading raw data from fixture and plug in to data object as URL - # works since the third-party feedparser library accepts a URL - # as well as the actual data. - data = self.setup_data(raw_data) - category = "Category 1" - name = "Name 1" - unit_of_measurement = "Unit 1" - sensor = geo_rss_events.GeoRssServiceSensor(category, - data, name, - unit_of_measurement) + all_states = self.hass.states.all() + assert len(all_states) == 1 - sensor.update() - assert sensor.name == "Name 1 Category 1" - assert sensor.unit_of_measurement == "Unit 1" - assert sensor.icon == "mdi:alert" - assert len(sensor._data.events) == 4 - assert sensor.state == 1 - assert sensor.device_state_attributes == {'Title 1': "117km"} - # Check entries of first hit - assert sensor._data.events[0][geo_rss_events.ATTR_TITLE] == "Title 1" - assert sensor._data.events[0][ - geo_rss_events.ATTR_CATEGORY] == "Category 1" - self.assertAlmostEqual(sensor._data.events[0][ - geo_rss_events.ATTR_DISTANCE], 116.586, 0) + state = self.hass.states.get("sensor.event_service_any") + self.assertIsNotNone(state) + assert state.name == "Event Service Any" + assert int(state.state) == 2 + assert state.attributes == { + ATTR_FRIENDLY_NAME: "Event Service Any", + ATTR_UNIT_OF_MEASUREMENT: "Events", + ATTR_ICON: "mdi:alert", + "Title 1": "16km", "Title 2": "20km"} - def test_update_sensor_without_category(self): - """Test updating sensor object.""" - raw_data = load_fixture('geo_rss_events.xml') - data = self.setup_data(raw_data) - category = None - name = "Name 2" - unit_of_measurement = "Unit 2" - sensor = geo_rss_events.GeoRssServiceSensor(category, - data, name, - unit_of_measurement) + # Simulate an update - empty data, but successful update, + # so no changes to entities. + mock_feed.return_value.update.return_value = 'OK_NO_DATA', None + fire_time_changed(self.hass, utcnow + + geo_rss_events.SCAN_INTERVAL) + self.hass.block_till_done() - sensor.update() - assert sensor.name == "Name 2 Any" - assert sensor.unit_of_measurement == "Unit 2" - assert sensor.icon == "mdi:alert" - assert len(sensor._data.events) == 4 - assert sensor.state == 4 - assert sensor.device_state_attributes == {'Title 1': "117km", - 'Title 2': "302km", - 'Title 3': "204km", - 'Title 6': "48km"} + all_states = self.hass.states.all() + assert len(all_states) == 1 + state = self.hass.states.get("sensor.event_service_any") + assert int(state.state) == 2 - def test_update_sensor_without_data(self): - """Test updating sensor object.""" - data = self.setup_data() - category = None - name = "Name 3" - unit_of_measurement = "Unit 3" - sensor = geo_rss_events.GeoRssServiceSensor(category, - data, name, - unit_of_measurement) + # Simulate an update - empty data, removes all entities + mock_feed.return_value.update.return_value = 'ERROR', None + fire_time_changed(self.hass, utcnow + + 2 * geo_rss_events.SCAN_INTERVAL) + self.hass.block_till_done() - sensor.update() - assert sensor.name == "Name 3 Any" - assert sensor.unit_of_measurement == "Unit 3" - assert sensor.icon == "mdi:alert" - assert len(sensor._data.events) == 0 - assert sensor.state == 0 + all_states = self.hass.states.all() + assert len(all_states) == 1 + state = self.hass.states.get("sensor.event_service_any") + assert int(state.state) == 0 - @mock.patch('feedparser.parse', return_value=None) - def test_update_sensor_with_none_result(self, parse_function): - """Test updating sensor object.""" - data = self.setup_data("http://invalid.url/") - category = None - name = "Name 4" - unit_of_measurement = "Unit 4" - sensor = geo_rss_events.GeoRssServiceSensor(category, - data, name, - unit_of_measurement) + @mock.patch('georss_client.generic_feed.GenericFeed') + def test_setup_with_categories(self, mock_feed): + """Test the general setup of the platform.""" + # Set up some mock feed entries for this test. + mock_entry_1 = self._generate_mock_feed_entry('1234', 'Title 1', 15.5, + (-31.0, 150.0), + 'Category 1') + mock_entry_2 = self._generate_mock_feed_entry('2345', 'Title 2', 20.5, + (-31.1, 150.1), + 'Category 1') + mock_feed.return_value.update.return_value = 'OK', [mock_entry_1, + mock_entry_2] - sensor.update() - assert sensor.name == "Name 4 Any" - assert sensor.unit_of_measurement == "Unit 4" - assert sensor.state == 0 + with assert_setup_component(1, sensor.DOMAIN): + self.assertTrue(setup_component(self.hass, sensor.DOMAIN, + VALID_CONFIG_WITH_CATEGORIES)) + # Artificially trigger update. + self.hass.bus.fire(EVENT_HOMEASSISTANT_START) + # Collect events. + self.hass.block_till_done() + + all_states = self.hass.states.all() + assert len(all_states) == 1 + + state = self.hass.states.get("sensor.event_service_category_1") + self.assertIsNotNone(state) + assert state.name == "Event Service Category 1" + assert int(state.state) == 2 + assert state.attributes == { + ATTR_FRIENDLY_NAME: "Event Service Category 1", + ATTR_UNIT_OF_MEASUREMENT: "Events", + ATTR_ICON: "mdi:alert", + "Title 1": "16km", "Title 2": "20km"} diff --git a/tests/components/sensor/test_jewish_calendar.py b/tests/components/sensor/test_jewish_calendar.py index 990f26d6ea7..b67e340a9aa 100644 --- a/tests/components/sensor/test_jewish_calendar.py +++ b/tests/components/sensor/test_jewish_calendar.py @@ -1,5 +1,6 @@ """The tests for the Jewish calendar sensor platform.""" import unittest +from datetime import time from datetime import datetime as dt from unittest.mock import patch @@ -64,7 +65,7 @@ class TestJewishCalenderSensor(unittest.TestCase): sensor = JewishCalSensor( name='test', language='english', sensor_type='date', latitude=self.TEST_LATITUDE, longitude=self.TEST_LONGITUDE, - diaspora=False) + timezone="UTC", diaspora=False) with patch('homeassistant.util.dt.now', return_value=test_time): run_coroutine_threadsafe( sensor.async_update(), @@ -77,7 +78,7 @@ class TestJewishCalenderSensor(unittest.TestCase): sensor = JewishCalSensor( name='test', language='hebrew', sensor_type='date', latitude=self.TEST_LATITUDE, longitude=self.TEST_LONGITUDE, - diaspora=False) + timezone="UTC", diaspora=False) with patch('homeassistant.util.dt.now', return_value=test_time): run_coroutine_threadsafe( sensor.async_update(), self.hass.loop).result() @@ -89,19 +90,31 @@ class TestJewishCalenderSensor(unittest.TestCase): sensor = JewishCalSensor( name='test', language='hebrew', sensor_type='holiday_name', latitude=self.TEST_LATITUDE, longitude=self.TEST_LONGITUDE, - diaspora=False) + timezone="UTC", diaspora=False) with patch('homeassistant.util.dt.now', return_value=test_time): run_coroutine_threadsafe( sensor.async_update(), self.hass.loop).result() self.assertEqual(sensor.state, "א\' ראש השנה") + def test_jewish_calendar_sensor_holiday_name_english(self): + """Test Jewish calendar sensor date output in hebrew.""" + test_time = dt(2018, 9, 10) + sensor = JewishCalSensor( + name='test', language='english', sensor_type='holiday_name', + latitude=self.TEST_LATITUDE, longitude=self.TEST_LONGITUDE, + timezone="UTC", diaspora=False) + with patch('homeassistant.util.dt.now', return_value=test_time): + run_coroutine_threadsafe( + sensor.async_update(), self.hass.loop).result() + self.assertEqual(sensor.state, "Rosh Hashana I") + def test_jewish_calendar_sensor_holyness(self): """Test Jewish calendar sensor date output in hebrew.""" test_time = dt(2018, 9, 10) sensor = JewishCalSensor( name='test', language='hebrew', sensor_type='holyness', latitude=self.TEST_LATITUDE, longitude=self.TEST_LONGITUDE, - diaspora=False) + timezone="UTC", diaspora=False) with patch('homeassistant.util.dt.now', return_value=test_time): run_coroutine_threadsafe( sensor.async_update(), self.hass.loop).result() @@ -113,8 +126,32 @@ class TestJewishCalenderSensor(unittest.TestCase): sensor = JewishCalSensor( name='test', language='hebrew', sensor_type='weekly_portion', latitude=self.TEST_LATITUDE, longitude=self.TEST_LONGITUDE, - diaspora=False) + timezone="UTC", diaspora=False) with patch('homeassistant.util.dt.now', return_value=test_time): run_coroutine_threadsafe( sensor.async_update(), self.hass.loop).result() self.assertEqual(sensor.state, "פרשת נצבים") + + def test_jewish_calendar_sensor_first_stars_ny(self): + """Test Jewish calendar sensor date output in hebrew.""" + test_time = dt(2018, 9, 8) + sensor = JewishCalSensor( + name='test', language='hebrew', sensor_type='first_stars', + latitude=40.7128, longitude=-74.0060, + timezone="America/New_York", diaspora=False) + with patch('homeassistant.util.dt.now', return_value=test_time): + run_coroutine_threadsafe( + sensor.async_update(), self.hass.loop).result() + self.assertEqual(sensor.state, time(19, 48)) + + def test_jewish_calendar_sensor_first_stars_jerusalem(self): + """Test Jewish calendar sensor date output in hebrew.""" + test_time = dt(2018, 9, 8) + sensor = JewishCalSensor( + name='test', language='hebrew', sensor_type='first_stars', + latitude=self.TEST_LATITUDE, longitude=self.TEST_LONGITUDE, + timezone="Asia/Jerusalem", diaspora=False) + with patch('homeassistant.util.dt.now', return_value=test_time): + run_coroutine_threadsafe( + sensor.async_update(), self.hass.loop).result() + self.assertEqual(sensor.state, time(19, 21)) diff --git a/tests/components/sensor/test_mqtt.py b/tests/components/sensor/test_mqtt.py index feef647b7b7..4f70c37e04f 100644 --- a/tests/components/sensor/test_mqtt.py +++ b/tests/components/sensor/test_mqtt.py @@ -6,13 +6,15 @@ from unittest.mock import patch import homeassistant.core as ha from homeassistant.setup import setup_component, async_setup_component +from homeassistant.components import mqtt +from homeassistant.components.mqtt.discovery import async_start import homeassistant.components.sensor as sensor from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE import homeassistant.util.dt as dt_util from tests.common import mock_mqtt_component, fire_mqtt_message, \ assert_setup_component, async_fire_mqtt_message, \ - async_mock_mqtt_component + async_mock_mqtt_component, MockConfigEntry from tests.common import get_test_home_assistant, mock_component @@ -387,3 +389,25 @@ async def test_unique_id(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 + + +async def test_discovery_removal_sensor(hass, mqtt_mock, caplog): + """Test removal of discovered sensor.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + data = ( + '{ "name": "Beer",' + ' "status_topic": "test_topic" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/config', + data) + await hass.async_block_till_done() + state = hass.states.get('sensor.beer') + assert state is not None + assert state.name == 'Beer' + async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/config', + '') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('sensor.beer') + assert state is None diff --git a/tests/components/switch/common.py b/tests/components/switch/common.py new file mode 100644 index 00000000000..8db8e425ddb --- /dev/null +++ b/tests/components/switch/common.py @@ -0,0 +1,39 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.switch import DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON) +from homeassistant.core import callback +from homeassistant.loader import bind_hass + + +@bind_hass +def turn_on(hass, entity_id=None): + """Turn all or specified switch on.""" + hass.add_job(async_turn_on, hass, entity_id) + + +@callback +@bind_hass +def async_turn_on(hass, entity_id=None): + """Turn all or specified switch on.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)) + + +@bind_hass +def turn_off(hass, entity_id=None): + """Turn all or specified switch off.""" + hass.add_job(async_turn_off, hass, entity_id) + + +@callback +@bind_hass +def async_turn_off(hass, entity_id=None): + """Turn all or specified switch off.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.async_add_job( + hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data)) diff --git a/tests/components/switch/test_command_line.py b/tests/components/switch/test_command_line.py index 9ec0507627d..a84281b4375 100644 --- a/tests/components/switch/test_command_line.py +++ b/tests/components/switch/test_command_line.py @@ -10,6 +10,7 @@ import homeassistant.components.switch as switch import homeassistant.components.switch.command_line as command_line from tests.common import get_test_home_assistant +from tests.components.switch import common # pylint: disable=invalid-name @@ -44,13 +45,13 @@ class TestCommandSwitch(unittest.TestCase): state = self.hass.states.get('switch.test') self.assertEqual(STATE_OFF, state.state) - switch.turn_on(self.hass, 'switch.test') + common.turn_on(self.hass, 'switch.test') self.hass.block_till_done() state = self.hass.states.get('switch.test') self.assertEqual(STATE_ON, state.state) - switch.turn_off(self.hass, 'switch.test') + common.turn_off(self.hass, 'switch.test') self.hass.block_till_done() state = self.hass.states.get('switch.test') @@ -78,13 +79,13 @@ class TestCommandSwitch(unittest.TestCase): state = self.hass.states.get('switch.test') self.assertEqual(STATE_OFF, state.state) - switch.turn_on(self.hass, 'switch.test') + common.turn_on(self.hass, 'switch.test') self.hass.block_till_done() state = self.hass.states.get('switch.test') self.assertEqual(STATE_ON, state.state) - switch.turn_off(self.hass, 'switch.test') + common.turn_off(self.hass, 'switch.test') self.hass.block_till_done() state = self.hass.states.get('switch.test') @@ -114,13 +115,13 @@ class TestCommandSwitch(unittest.TestCase): state = self.hass.states.get('switch.test') self.assertEqual(STATE_OFF, state.state) - switch.turn_on(self.hass, 'switch.test') + common.turn_on(self.hass, 'switch.test') self.hass.block_till_done() state = self.hass.states.get('switch.test') self.assertEqual(STATE_ON, state.state) - switch.turn_off(self.hass, 'switch.test') + common.turn_off(self.hass, 'switch.test') self.hass.block_till_done() state = self.hass.states.get('switch.test') @@ -147,13 +148,13 @@ class TestCommandSwitch(unittest.TestCase): state = self.hass.states.get('switch.test') self.assertEqual(STATE_OFF, state.state) - switch.turn_on(self.hass, 'switch.test') + common.turn_on(self.hass, 'switch.test') self.hass.block_till_done() state = self.hass.states.get('switch.test') self.assertEqual(STATE_ON, state.state) - switch.turn_off(self.hass, 'switch.test') + common.turn_off(self.hass, 'switch.test') self.hass.block_till_done() state = self.hass.states.get('switch.test') diff --git a/tests/components/switch/test_flux.py b/tests/components/switch/test_flux.py index f9ea88c5254..69e65e24659 100644 --- a/tests/components/switch/test_flux.py +++ b/tests/components/switch/test_flux.py @@ -11,6 +11,8 @@ import homeassistant.util.dt as dt_util from tests.common import ( assert_setup_component, get_test_home_assistant, fire_time_changed, mock_service) +from tests.components.light import common as common_light +from tests.components.switch import common class TestSwitchFlux(unittest.TestCase): @@ -147,7 +149,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -193,7 +195,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -240,7 +242,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -286,7 +288,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -334,7 +336,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -383,7 +385,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -434,7 +436,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -484,7 +486,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -534,7 +536,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -584,7 +586,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -633,7 +635,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -681,7 +683,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -698,9 +700,9 @@ class TestSwitchFlux(unittest.TestCase): {light.DOMAIN: {CONF_PLATFORM: 'test'}})) dev1, dev2, dev3 = platform.DEVICES - light.turn_on(self.hass, entity_id=dev2.entity_id) + common_light.turn_on(self.hass, entity_id=dev2.entity_id) self.hass.block_till_done() - light.turn_on(self.hass, entity_id=dev3.entity_id) + common_light.turn_on(self.hass, entity_id=dev3.entity_id) self.hass.block_till_done() state = self.hass.states.get(dev1.entity_id) @@ -743,7 +745,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -794,7 +796,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() @@ -838,7 +840,7 @@ class TestSwitchFlux(unittest.TestCase): }) turn_on_calls = mock_service( self.hass, light.DOMAIN, SERVICE_TURN_ON) - switch.turn_on(self.hass, 'switch.flux') + common.turn_on(self.hass, 'switch.flux') self.hass.block_till_done() fire_time_changed(self.hass, test_time) self.hass.block_till_done() diff --git a/tests/components/switch/test_init.py b/tests/components/switch/test_init.py index 579898437ca..a7462eecd42 100644 --- a/tests/components/switch/test_init.py +++ b/tests/components/switch/test_init.py @@ -8,6 +8,7 @@ from homeassistant.components import switch from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM from tests.common import get_test_home_assistant +from tests.components.switch import common class TestSwitch(unittest.TestCase): @@ -41,8 +42,8 @@ class TestSwitch(unittest.TestCase): self.assertFalse(switch.is_on(self.hass, self.switch_2.entity_id)) self.assertFalse(switch.is_on(self.hass, self.switch_3.entity_id)) - switch.turn_off(self.hass, self.switch_1.entity_id) - switch.turn_on(self.hass, self.switch_2.entity_id) + common.turn_off(self.hass, self.switch_1.entity_id) + common.turn_on(self.hass, self.switch_2.entity_id) self.hass.block_till_done() @@ -51,7 +52,7 @@ class TestSwitch(unittest.TestCase): self.assertTrue(switch.is_on(self.hass, self.switch_2.entity_id)) # Turn all off - switch.turn_off(self.hass) + common.turn_off(self.hass) self.hass.block_till_done() @@ -64,7 +65,7 @@ class TestSwitch(unittest.TestCase): self.assertFalse(switch.is_on(self.hass, self.switch_3.entity_id)) # Turn all on - switch.turn_on(self.hass) + common.turn_on(self.hass) self.hass.block_till_done() diff --git a/tests/components/switch/test_litejet.py b/tests/components/switch/test_litejet.py index 45e5509c169..f1d23f48b86 100644 --- a/tests/components/switch/test_litejet.py +++ b/tests/components/switch/test_litejet.py @@ -5,9 +5,11 @@ from unittest import mock from homeassistant import setup from homeassistant.components import litejet -from tests.common import get_test_home_assistant import homeassistant.components.switch as switch +from tests.common import get_test_home_assistant +from tests.components.switch import common + _LOGGER = logging.getLogger(__name__) ENTITY_SWITCH = 'switch.mock_switch_1' @@ -88,11 +90,11 @@ class TestLiteJetSwitch(unittest.TestCase): assert not switch.is_on(self.hass, ENTITY_SWITCH) - switch.turn_on(self.hass, ENTITY_SWITCH) + common.turn_on(self.hass, ENTITY_SWITCH) self.hass.block_till_done() self.mock_lj.press_switch.assert_called_with(ENTITY_SWITCH_NUMBER) - switch.turn_off(self.hass, ENTITY_SWITCH) + common.turn_off(self.hass, ENTITY_SWITCH) self.hass.block_till_done() self.mock_lj.release_switch.assert_called_with(ENTITY_SWITCH_NUMBER) diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py index c9bfd02156f..5ad233de284 100644 --- a/tests/components/switch/test_mqtt.py +++ b/tests/components/switch/test_mqtt.py @@ -2,13 +2,17 @@ import unittest from unittest.mock import patch -from homeassistant.setup import setup_component +from homeassistant.setup import setup_component, async_setup_component from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE,\ ATTR_ASSUMED_STATE import homeassistant.core as ha -import homeassistant.components.switch as switch +from homeassistant.components import switch, mqtt +from homeassistant.components.mqtt.discovery import async_start + from tests.common import ( - mock_mqtt_component, fire_mqtt_message, get_test_home_assistant, mock_coro) + mock_mqtt_component, fire_mqtt_message, get_test_home_assistant, mock_coro, + async_mock_mqtt_component, async_fire_mqtt_message, MockConfigEntry) +from tests.components.switch import common class TestSwitchMQTT(unittest.TestCase): @@ -73,7 +77,7 @@ class TestSwitchMQTT(unittest.TestCase): self.assertEqual(STATE_ON, state.state) self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) - switch.turn_on(self.hass, 'switch.test') + common.turn_on(self.hass, 'switch.test') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -82,7 +86,7 @@ class TestSwitchMQTT(unittest.TestCase): state = self.hass.states.get('switch.test') self.assertEqual(STATE_ON, state.state) - switch.turn_off(self.hass, 'switch.test') + common.turn_off(self.hass, 'switch.test') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( @@ -280,25 +284,56 @@ class TestSwitchMQTT(unittest.TestCase): state = self.hass.states.get('switch.test') self.assertEqual(STATE_OFF, state.state) - def test_unique_id(self): - """Test unique id option only creates one switch per unique_id.""" - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: [{ - 'platform': 'mqtt', - 'name': 'Test 1', - 'state_topic': 'test-topic', - 'command_topic': 'command-topic', - 'unique_id': 'TOTALLY_UNIQUE' - }, { - 'platform': 'mqtt', - 'name': 'Test 2', - 'state_topic': 'test-topic', - 'command_topic': 'command-topic', - 'unique_id': 'TOTALLY_UNIQUE' - }] - }) - fire_mqtt_message(self.hass, 'test-topic', 'payload') - self.hass.block_till_done() - assert len(self.hass.states.async_entity_ids()) == 2 - # all switches group is 1, unique id created is 1 +async def test_unique_id(hass): + """Test unique id option only creates one switch per unique_id.""" + await async_mock_mqtt_component(hass) + assert await async_setup_component(hass, switch.DOMAIN, { + switch.DOMAIN: [{ + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'command_topic': 'command-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }, { + 'platform': 'mqtt', + 'name': 'Test 2', + 'state_topic': 'test-topic', + 'command_topic': 'command-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }] + }) + + async_fire_mqtt_message(hass, 'test-topic', 'payload') + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids()) == 2 + # all switches group is 1, unique id created is 1 + + +async def test_discovery_removal_switch(hass, mqtt_mock, caplog): + """Test expansion of discovered switch.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + + data = ( + '{ "name": "Beer",' + ' "status_topic": "test_topic",' + ' "command_topic": "test_topic" }' + ) + + async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config', + data) + await hass.async_block_till_done() + + state = hass.states.get('switch.beer') + assert state is not None + assert state.name == 'Beer' + + async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config', + '') + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('switch.beer') + assert state is None diff --git a/tests/components/switch/test_template.py b/tests/components/switch/test_template.py index 47766e31f4d..1492aab250f 100644 --- a/tests/components/switch/test_template.py +++ b/tests/components/switch/test_template.py @@ -1,11 +1,11 @@ """The tests for the Template switch platform.""" from homeassistant.core import callback from homeassistant import setup -import homeassistant.components as core from homeassistant.const import STATE_ON, STATE_OFF from tests.common import ( get_test_home_assistant, assert_setup_component) +from tests.components.switch import common class TestTemplateSwitch: @@ -406,7 +406,7 @@ class TestTemplateSwitch: state = self.hass.states.get('switch.test_template_switch') assert state.state == STATE_OFF - core.switch.turn_on(self.hass, 'switch.test_template_switch') + common.turn_on(self.hass, 'switch.test_template_switch') self.hass.block_till_done() assert len(self.calls) == 1 @@ -442,7 +442,7 @@ class TestTemplateSwitch: state = self.hass.states.get('switch.test_template_switch') assert state.state == STATE_ON - core.switch.turn_off(self.hass, 'switch.test_template_switch') + common.turn_off(self.hass, 'switch.test_template_switch') self.hass.block_till_done() assert len(self.calls) == 1 diff --git a/tests/components/switch/test_wake_on_lan.py b/tests/components/switch/test_wake_on_lan.py index abe1532cec7..42ebd1ff231 100644 --- a/tests/components/switch/test_wake_on_lan.py +++ b/tests/components/switch/test_wake_on_lan.py @@ -7,6 +7,7 @@ from homeassistant.const import STATE_ON, STATE_OFF import homeassistant.components.switch as switch from tests.common import get_test_home_assistant, mock_service +from tests.components.switch import common TEST_STATE = None @@ -59,13 +60,13 @@ class TestWOLSwitch(unittest.TestCase): TEST_STATE = True - switch.turn_on(self.hass, 'switch.wake_on_lan') + common.turn_on(self.hass, 'switch.wake_on_lan') self.hass.block_till_done() state = self.hass.states.get('switch.wake_on_lan') self.assertEqual(STATE_ON, state.state) - switch.turn_off(self.hass, 'switch.wake_on_lan') + common.turn_off(self.hass, 'switch.wake_on_lan') self.hass.block_till_done() state = self.hass.states.get('switch.wake_on_lan') @@ -91,7 +92,7 @@ class TestWOLSwitch(unittest.TestCase): TEST_STATE = True - switch.turn_on(self.hass, 'switch.wake_on_lan') + common.turn_on(self.hass, 'switch.wake_on_lan') self.hass.block_till_done() state = self.hass.states.get('switch.wake_on_lan') @@ -123,7 +124,7 @@ class TestWOLSwitch(unittest.TestCase): state = self.hass.states.get('switch.wake_on_lan') self.assertEqual(STATE_OFF, state.state) - switch.turn_on(self.hass, 'switch.wake_on_lan') + common.turn_on(self.hass, 'switch.wake_on_lan') self.hass.block_till_done() @patch('wakeonlan.send_magic_packet', new=send_magic_packet) @@ -149,7 +150,7 @@ class TestWOLSwitch(unittest.TestCase): TEST_STATE = True - switch.turn_on(self.hass, 'switch.wake_on_lan') + common.turn_on(self.hass, 'switch.wake_on_lan') self.hass.block_till_done() state = self.hass.states.get('switch.wake_on_lan') @@ -158,7 +159,7 @@ class TestWOLSwitch(unittest.TestCase): TEST_STATE = False - switch.turn_off(self.hass, 'switch.wake_on_lan') + common.turn_off(self.hass, 'switch.wake_on_lan') self.hass.block_till_done() state = self.hass.states.get('switch.wake_on_lan') @@ -185,7 +186,7 @@ class TestWOLSwitch(unittest.TestCase): TEST_STATE = True - switch.turn_on(self.hass, 'switch.wake_on_lan') + common.turn_on(self.hass, 'switch.wake_on_lan') self.hass.block_till_done() state = self.hass.states.get('switch.wake_on_lan') diff --git a/tests/components/test_alert.py b/tests/components/test_alert.py index 90d732ac38e..44ece2fc38e 100644 --- a/tests/components/test_alert.py +++ b/tests/components/test_alert.py @@ -5,10 +5,12 @@ import unittest from homeassistant.setup import setup_component from homeassistant.core import callback +from homeassistant.components.alert import DOMAIN import homeassistant.components.alert as alert import homeassistant.components.notify as notify -from homeassistant.const import (CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, - CONF_STATE, STATE_ON, STATE_OFF) +from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, + SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON, STATE_OFF) from tests.common import get_test_home_assistant @@ -31,6 +33,63 @@ TEST_NOACK = [NAME, NAME, DONE_MESSAGE, "sensor.test", ENTITY_ID = alert.ENTITY_ID_FORMAT.format(NAME) +def turn_on(hass, entity_id): + """Reset the alert. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.add_job(async_turn_on, hass, entity_id) + + +@callback +def async_turn_on(hass, entity_id): + """Async reset the alert. + + This is a legacy helper method. Do not use it for new tests. + """ + data = {ATTR_ENTITY_ID: entity_id} + hass.async_create_task( + hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)) + + +def turn_off(hass, entity_id): + """Acknowledge alert. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.add_job(async_turn_off, hass, entity_id) + + +@callback +def async_turn_off(hass, entity_id): + """Async acknowledge the alert. + + This is a legacy helper method. Do not use it for new tests. + """ + data = {ATTR_ENTITY_ID: entity_id} + hass.async_create_task( + hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data)) + + +def toggle(hass, entity_id): + """Toggle acknowledgment of alert. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.add_job(async_toggle, hass, entity_id) + + +@callback +def async_toggle(hass, entity_id): + """Async toggle acknowledgment of alert. + + This is a legacy helper method. Do not use it for new tests. + """ + data = {ATTR_ENTITY_ID: entity_id} + hass.async_create_task( + hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data)) + + # pylint: disable=invalid-name class TestAlert(unittest.TestCase): """Test the alert module.""" @@ -69,7 +128,7 @@ class TestAlert(unittest.TestCase): assert setup_component(self.hass, alert.DOMAIN, TEST_CONFIG) self.hass.states.set("sensor.test", STATE_ON) self.hass.block_till_done() - alert.turn_off(self.hass, ENTITY_ID) + turn_off(self.hass, ENTITY_ID) self.hass.block_till_done() self.assertEqual(STATE_OFF, self.hass.states.get(ENTITY_ID).state) @@ -86,10 +145,10 @@ class TestAlert(unittest.TestCase): assert setup_component(self.hass, alert.DOMAIN, TEST_CONFIG) self.hass.states.set("sensor.test", STATE_ON) self.hass.block_till_done() - alert.turn_off(self.hass, ENTITY_ID) + turn_off(self.hass, ENTITY_ID) self.hass.block_till_done() self.assertEqual(STATE_OFF, self.hass.states.get(ENTITY_ID).state) - alert.turn_on(self.hass, ENTITY_ID) + turn_on(self.hass, ENTITY_ID) self.hass.block_till_done() self.assertEqual(STATE_ON, self.hass.states.get(ENTITY_ID).state) @@ -99,10 +158,10 @@ class TestAlert(unittest.TestCase): self.hass.states.set("sensor.test", STATE_ON) self.hass.block_till_done() self.assertEqual(STATE_ON, self.hass.states.get(ENTITY_ID).state) - alert.toggle(self.hass, ENTITY_ID) + toggle(self.hass, ENTITY_ID) self.hass.block_till_done() self.assertEqual(STATE_OFF, self.hass.states.get(ENTITY_ID).state) - alert.toggle(self.hass, ENTITY_ID) + toggle(self.hass, ENTITY_ID) self.hass.block_till_done() self.assertEqual(STATE_ON, self.hass.states.get(ENTITY_ID).state) @@ -117,7 +176,7 @@ class TestAlert(unittest.TestCase): hidden = self.hass.states.get(ENTITY_ID).attributes.get('hidden') self.assertFalse(hidden) - alert.turn_off(self.hass, ENTITY_ID) + turn_off(self.hass, ENTITY_ID) hidden = self.hass.states.get(ENTITY_ID).attributes.get('hidden') self.assertFalse(hidden) @@ -199,7 +258,7 @@ class TestAlert(unittest.TestCase): self.assertEqual(True, entity.hidden) def test_done_message_state_tracker_reset_on_cancel(self): - """Test that the done message is reset when cancelled.""" + """Test that the done message is reset when canceled.""" entity = alert.Alert(self.hass, *TEST_NOACK) entity._cancel = lambda *args: None assert entity._send_done_message is False diff --git a/tests/components/test_device_sun_light_trigger.py b/tests/components/test_device_sun_light_trigger.py index 35d53f9a5c8..7107eee74fe 100644 --- a/tests/components/test_device_sun_light_trigger.py +++ b/tests/components/test_device_sun_light_trigger.py @@ -12,6 +12,7 @@ from homeassistant.components import ( from homeassistant.util import dt as dt_util from tests.common import get_test_home_assistant, fire_time_changed +from tests.components.light import common as common_light class TestDeviceSunLightTrigger(unittest.TestCase): @@ -68,7 +69,7 @@ class TestDeviceSunLightTrigger(unittest.TestCase): self.hass, device_sun_light_trigger.DOMAIN, { device_sun_light_trigger.DOMAIN: {}})) - light.turn_off(self.hass) + common_light.turn_off(self.hass) self.hass.block_till_done() @@ -81,7 +82,7 @@ class TestDeviceSunLightTrigger(unittest.TestCase): def test_lights_turn_off_when_everyone_leaves(self): """Test lights turn off when everyone leaves the house.""" - light.turn_on(self.hass) + common_light.turn_on(self.hass) self.hass.block_till_done() @@ -100,7 +101,7 @@ class TestDeviceSunLightTrigger(unittest.TestCase): """Test lights turn on when coming home after sun set.""" test_time = datetime(2017, 4, 5, 3, 2, 3, tzinfo=dt_util.UTC) with patch('homeassistant.util.dt.utcnow', return_value=test_time): - light.turn_off(self.hass) + common_light.turn_off(self.hass) self.hass.block_till_done() self.assertTrue(setup_component( diff --git a/tests/components/test_duckdns.py b/tests/components/test_duckdns.py index d64ffbca81f..c3ece8a70fd 100644 --- a/tests/components/test_duckdns.py +++ b/tests/components/test_duckdns.py @@ -4,6 +4,7 @@ from datetime import timedelta import pytest +from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component from homeassistant.components import duckdns from homeassistant.util.dt import utcnow @@ -14,6 +15,19 @@ DOMAIN = 'bla' TOKEN = 'abcdefgh' +@bind_hass +@asyncio.coroutine +def async_set_txt(hass, txt): + """Set the txt record. Pass in None to remove it. + + This is a legacy helper method. Do not use it for new tests. + """ + yield from hass.services.async_call( + duckdns.DOMAIN, duckdns.SERVICE_SET_TXT, { + duckdns.ATTR_TXT: txt + }, blocking=True) + + @pytest.fixture def setup_duckdns(hass, aioclient_mock): """Fixture that sets up DuckDNS.""" @@ -84,7 +98,7 @@ def test_service_set_txt(hass, aioclient_mock, setup_duckdns): }, text='OK') assert aioclient_mock.call_count == 0 - yield from hass.components.duckdns.async_set_txt('some-txt') + yield from async_set_txt(hass, 'some-txt') assert aioclient_mock.call_count == 1 @@ -102,5 +116,5 @@ def test_service_clear_txt(hass, aioclient_mock, setup_duckdns): }, text='OK') assert aioclient_mock.call_count == 0 - yield from hass.components.duckdns.async_set_txt(None) + yield from async_set_txt(hass, None) assert aioclient_mock.call_count == 1 diff --git a/tests/components/test_ffmpeg.py b/tests/components/test_ffmpeg.py index 76b1300774b..774bb471f57 100644 --- a/tests/components/test_ffmpeg.py +++ b/tests/components/test_ffmpeg.py @@ -3,12 +3,46 @@ import asyncio from unittest.mock import patch, MagicMock import homeassistant.components.ffmpeg as ffmpeg +from homeassistant.components.ffmpeg import ( + DOMAIN, SERVICE_RESTART, SERVICE_START, SERVICE_STOP) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import callback from homeassistant.setup import setup_component, async_setup_component from tests.common import ( get_test_home_assistant, assert_setup_component, mock_coro) +@callback +def async_start(hass, entity_id=None): + """Start a FFmpeg process on entity. + + This is a legacy helper method. Do not use it for new tests. + """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_START, data)) + + +@callback +def async_stop(hass, entity_id=None): + """Stop a FFmpeg process on entity. + + This is a legacy helper method. Do not use it for new tests. + """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_STOP, data)) + + +@callback +def async_restart(hass, entity_id=None): + """Restart a FFmpeg process on entity. + + This is a legacy helper method. Do not use it for new tests. + """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_RESTART, data)) + + class MockFFmpegDev(ffmpeg.FFmpegBase): """FFmpeg device mock.""" @@ -106,7 +140,7 @@ def test_setup_component_test_service_start(hass): ffmpeg_dev = MockFFmpegDev(hass, False) yield from ffmpeg_dev.async_added_to_hass() - ffmpeg.async_start(hass) + async_start(hass) yield from hass.async_block_till_done() assert ffmpeg_dev.called_start @@ -122,7 +156,7 @@ def test_setup_component_test_service_stop(hass): ffmpeg_dev = MockFFmpegDev(hass, False) yield from ffmpeg_dev.async_added_to_hass() - ffmpeg.async_stop(hass) + async_stop(hass) yield from hass.async_block_till_done() assert ffmpeg_dev.called_stop @@ -138,7 +172,7 @@ def test_setup_component_test_service_restart(hass): ffmpeg_dev = MockFFmpegDev(hass, False) yield from ffmpeg_dev.async_added_to_hass() - ffmpeg.async_restart(hass) + async_restart(hass) yield from hass.async_block_till_done() assert ffmpeg_dev.called_stop @@ -155,7 +189,7 @@ def test_setup_component_test_service_start_with_entity(hass): ffmpeg_dev = MockFFmpegDev(hass, False) yield from ffmpeg_dev.async_added_to_hass() - ffmpeg.async_start(hass, 'test.ffmpeg_device') + async_start(hass, 'test.ffmpeg_device') yield from hass.async_block_till_done() assert ffmpeg_dev.called_start diff --git a/tests/components/test_init.py b/tests/components/test_init.py index 355f3dc0e96..68396f5abcb 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -8,8 +8,12 @@ import yaml import homeassistant.core as ha from homeassistant import config from homeassistant.const import ( - STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE) + ATTR_ENTITY_ID, STATE_ON, STATE_OFF, SERVICE_HOMEASSISTANT_RESTART, + SERVICE_HOMEASSISTANT_STOP, SERVICE_TURN_ON, SERVICE_TURN_OFF, + SERVICE_TOGGLE) import homeassistant.components as comps +from homeassistant.components import ( + SERVICE_CHECK_CONFIG, SERVICE_RELOAD_CORE_CONFIG) import homeassistant.helpers.intent as intent from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity @@ -20,6 +24,71 @@ from tests.common import ( async_mock_service) +def turn_on(hass, entity_id=None, **service_data): + """Turn specified entity on if possible. + + This is a legacy helper method. Do not use it for new tests. + """ + if entity_id is not None: + service_data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(ha.DOMAIN, SERVICE_TURN_ON, service_data) + + +def turn_off(hass, entity_id=None, **service_data): + """Turn specified entity off. + + This is a legacy helper method. Do not use it for new tests. + """ + if entity_id is not None: + service_data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(ha.DOMAIN, SERVICE_TURN_OFF, service_data) + + +def toggle(hass, entity_id=None, **service_data): + """Toggle specified entity. + + This is a legacy helper method. Do not use it for new tests. + """ + if entity_id is not None: + service_data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(ha.DOMAIN, SERVICE_TOGGLE, service_data) + + +def stop(hass): + """Stop Home Assistant. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP) + + +def restart(hass): + """Stop Home Assistant. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART) + + +def check_config(hass): + """Check the config files. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(ha.DOMAIN, SERVICE_CHECK_CONFIG) + + +def reload_core_config(hass): + """Reload the core config. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG) + + class TestComponentsCore(unittest.TestCase): """Test homeassistant.components module.""" @@ -49,28 +118,28 @@ class TestComponentsCore(unittest.TestCase): def test_turn_on_without_entities(self): """Test turn_on method without entities.""" calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) - comps.turn_on(self.hass) + turn_on(self.hass) self.hass.block_till_done() self.assertEqual(0, len(calls)) def test_turn_on(self): """Test turn_on method.""" calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) - comps.turn_on(self.hass, 'light.Ceiling') + turn_on(self.hass, 'light.Ceiling') self.hass.block_till_done() self.assertEqual(1, len(calls)) def test_turn_off(self): """Test turn_off method.""" calls = mock_service(self.hass, 'light', SERVICE_TURN_OFF) - comps.turn_off(self.hass, 'light.Bowl') + turn_off(self.hass, 'light.Bowl') self.hass.block_till_done() self.assertEqual(1, len(calls)) def test_toggle(self): """Test toggle method.""" calls = mock_service(self.hass, 'light', SERVICE_TOGGLE) - comps.toggle(self.hass, 'light.Bowl') + toggle(self.hass, 'light.Bowl') self.hass.block_till_done() self.assertEqual(1, len(calls)) @@ -102,7 +171,7 @@ class TestComponentsCore(unittest.TestCase): }) } with patch_yaml_files(files, True): - comps.reload_core_config(self.hass) + reload_core_config(self.hass) self.hass.block_till_done() assert self.hass.config.latitude == 10 @@ -125,7 +194,7 @@ class TestComponentsCore(unittest.TestCase): config.YAML_CONFIG_FILE: yaml.dump(['invalid', 'config']) } with patch_yaml_files(files, True): - comps.reload_core_config(self.hass) + reload_core_config(self.hass) self.hass.block_till_done() assert mock_error.called @@ -135,7 +204,7 @@ class TestComponentsCore(unittest.TestCase): return_value=mock_coro()) def test_stop_homeassistant(self, mock_stop): """Test stop service.""" - comps.stop(self.hass) + stop(self.hass) self.hass.block_till_done() assert mock_stop.called @@ -145,7 +214,7 @@ class TestComponentsCore(unittest.TestCase): return_value=mock_coro()) def test_restart_homeassistant(self, mock_check, mock_restart): """Test stop service.""" - comps.restart(self.hass) + restart(self.hass) self.hass.block_till_done() assert mock_restart.called assert mock_check.called @@ -156,7 +225,7 @@ class TestComponentsCore(unittest.TestCase): side_effect=HomeAssistantError("Test error")) def test_restart_homeassistant_wrong_conf(self, mock_check, mock_restart): """Test stop service.""" - comps.restart(self.hass) + restart(self.hass) self.hass.block_till_done() assert mock_check.called assert not mock_restart.called @@ -167,7 +236,7 @@ class TestComponentsCore(unittest.TestCase): return_value=mock_coro()) def test_check_config(self, mock_check, mock_stop): """Test stop service.""" - comps.check_config(self.hass) + check_config(self.hass) self.hass.block_till_done() assert mock_check.called assert not mock_stop.called diff --git a/tests/components/test_input_boolean.py b/tests/components/test_input_boolean.py index 999e7ac100f..b947155e6b2 100644 --- a/tests/components/test_input_boolean.py +++ b/tests/components/test_input_boolean.py @@ -7,9 +7,11 @@ import logging from homeassistant.core import CoreState, State, Context from homeassistant.setup import setup_component, async_setup_component from homeassistant.components.input_boolean import ( - DOMAIN, is_on, toggle, turn_off, turn_on, CONF_INITIAL) + is_on, CONF_INITIAL, DOMAIN) from homeassistant.const import ( - STATE_ON, STATE_OFF, ATTR_ICON, ATTR_FRIENDLY_NAME) + STATE_ON, STATE_OFF, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON, + SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON) +from homeassistant.loader import bind_hass from tests.common import ( get_test_home_assistant, mock_component, mock_restore_cache) @@ -17,6 +19,33 @@ from tests.common import ( _LOGGER = logging.getLogger(__name__) +@bind_hass +def toggle(hass, entity_id): + """Set input_boolean to False. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}) + + +@bind_hass +def turn_on(hass, entity_id): + """Set input_boolean to True. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}) + + +@bind_hass +def turn_off(hass, entity_id): + """Set input_boolean to False. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}) + + class TestInputBoolean(unittest.TestCase): """Test the input boolean module.""" diff --git a/tests/components/test_input_number.py b/tests/components/test_input_number.py index 659aaa524d9..a53704e1d10 100644 --- a/tests/components/test_input_number.py +++ b/tests/components/test_input_number.py @@ -4,13 +4,50 @@ import asyncio import unittest from homeassistant.core import CoreState, State, Context -from homeassistant.setup import setup_component, async_setup_component from homeassistant.components.input_number import ( - DOMAIN, set_value, increment, decrement) + ATTR_VALUE, DOMAIN, SERVICE_DECREMENT, SERVICE_INCREMENT, + SERVICE_SET_VALUE) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.loader import bind_hass +from homeassistant.setup import setup_component, async_setup_component from tests.common import get_test_home_assistant, mock_restore_cache +@bind_hass +def set_value(hass, entity_id, value): + """Set input_number to value. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_SET_VALUE, { + ATTR_ENTITY_ID: entity_id, + ATTR_VALUE: value, + }) + + +@bind_hass +def increment(hass, entity_id): + """Increment value of entity. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_INCREMENT, { + ATTR_ENTITY_ID: entity_id + }) + + +@bind_hass +def decrement(hass, entity_id): + """Decrement value of entity. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_DECREMENT, { + ATTR_ENTITY_ID: entity_id + }) + + class TestInputNumber(unittest.TestCase): """Test the input number component.""" diff --git a/tests/components/test_input_select.py b/tests/components/test_input_select.py index 1c73abfbb94..25f6d1c7673 100644 --- a/tests/components/test_input_select.py +++ b/tests/components/test_input_select.py @@ -3,15 +3,50 @@ import asyncio import unittest -from tests.common import get_test_home_assistant, mock_restore_cache - +from homeassistant.loader import bind_hass +from homeassistant.components.input_select import ( + ATTR_OPTION, ATTR_OPTIONS, DOMAIN, SERVICE_SET_OPTIONS, + SERVICE_SELECT_NEXT, SERVICE_SELECT_OPTION, SERVICE_SELECT_PREVIOUS) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON) from homeassistant.core import State, Context from homeassistant.setup import setup_component, async_setup_component -from homeassistant.components.input_select import ( - ATTR_OPTIONS, DOMAIN, SERVICE_SET_OPTIONS, - select_option, select_next, select_previous) -from homeassistant.const import ( - ATTR_ICON, ATTR_FRIENDLY_NAME) + +from tests.common import get_test_home_assistant, mock_restore_cache + + +@bind_hass +def select_option(hass, entity_id, option): + """Set value of input_select. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_SELECT_OPTION, { + ATTR_ENTITY_ID: entity_id, + ATTR_OPTION: option, + }) + + +@bind_hass +def select_next(hass, entity_id): + """Set next value of input_select. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_SELECT_NEXT, { + ATTR_ENTITY_ID: entity_id, + }) + + +@bind_hass +def select_previous(hass, entity_id): + """Set previous value of input_select. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_SELECT_PREVIOUS, { + ATTR_ENTITY_ID: entity_id, + }) class TestInputSelect(unittest.TestCase): diff --git a/tests/components/test_input_text.py b/tests/components/test_input_text.py index 7c8a0e65023..bea145390eb 100644 --- a/tests/components/test_input_text.py +++ b/tests/components/test_input_text.py @@ -3,13 +3,28 @@ import asyncio import unittest +from homeassistant.components.input_text import ( + ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE) +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import CoreState, State, Context +from homeassistant.loader import bind_hass from homeassistant.setup import setup_component, async_setup_component -from homeassistant.components.input_text import (DOMAIN, set_value) from tests.common import get_test_home_assistant, mock_restore_cache +@bind_hass +def set_value(hass, entity_id, value): + """Set input_text to value. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_SET_VALUE, { + ATTR_ENTITY_ID: entity_id, + ATTR_VALUE: value, + }) + + class TestInputText(unittest.TestCase): """Test the input slider component.""" diff --git a/tests/components/test_logbook.py b/tests/components/test_logbook.py index cf78fbec352..3bb3ae57c68 100644 --- a/tests/components/test_logbook.py +++ b/tests/components/test_logbook.py @@ -1,7 +1,7 @@ """The tests for the logbook component.""" # pylint: disable=protected-access,invalid-name import logging -from datetime import timedelta +from datetime import (timedelta, datetime) import unittest from homeassistant.components import sun @@ -11,6 +11,7 @@ from homeassistant.const import ( ATTR_HIDDEN, STATE_NOT_HOME, STATE_ON, STATE_OFF) import homeassistant.util.dt as dt_util from homeassistant.components import logbook, recorder +from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME from homeassistant.setup import setup_component, async_setup_component from tests.common import ( @@ -99,7 +100,7 @@ class TestComponentLogbook(unittest.TestCase): eventB = self.create_state_changed_event(pointB, entity_id, 20) eventC = self.create_state_changed_event(pointC, entity_id, 30) - entries = list(logbook.humanify((eventA, eventB, eventC))) + entries = list(logbook.humanify(self.hass, (eventA, eventB, eventC))) self.assertEqual(2, len(entries)) self.assert_entry( @@ -116,7 +117,7 @@ class TestComponentLogbook(unittest.TestCase): eventA = self.create_state_changed_event( pointA, entity_id, 10, attributes) - entries = list(logbook.humanify((eventA,))) + entries = list(logbook.humanify(self.hass, (eventA,))) self.assertEqual(0, len(entries)) @@ -133,7 +134,7 @@ class TestComponentLogbook(unittest.TestCase): events = logbook._exclude_events((ha.Event(EVENT_HOMEASSISTANT_STOP), eventA, eventB), {}) - entries = list(logbook.humanify(events)) + entries = list(logbook.humanify(self.hass, events)) self.assertEqual(2, len(entries)) self.assert_entry( @@ -155,7 +156,7 @@ class TestComponentLogbook(unittest.TestCase): events = logbook._exclude_events((ha.Event(EVENT_HOMEASSISTANT_STOP), eventA, eventB), {}) - entries = list(logbook.humanify(events)) + entries = list(logbook.humanify(self.hass, events)) self.assertEqual(2, len(entries)) self.assert_entry( @@ -177,7 +178,7 @@ class TestComponentLogbook(unittest.TestCase): events = logbook._exclude_events((ha.Event(EVENT_HOMEASSISTANT_STOP), eventA, eventB), {}) - entries = list(logbook.humanify(events)) + entries = list(logbook.humanify(self.hass, events)) self.assertEqual(2, len(entries)) self.assert_entry( @@ -203,7 +204,7 @@ class TestComponentLogbook(unittest.TestCase): events = logbook._exclude_events( (ha.Event(EVENT_HOMEASSISTANT_STOP), eventA, eventB), config[logbook.DOMAIN]) - entries = list(logbook.humanify(events)) + entries = list(logbook.humanify(self.hass, events)) self.assertEqual(2, len(entries)) self.assert_entry( @@ -229,7 +230,7 @@ class TestComponentLogbook(unittest.TestCase): events = logbook._exclude_events( (ha.Event(EVENT_HOMEASSISTANT_START), eventA, eventB), config[logbook.DOMAIN]) - entries = list(logbook.humanify(events)) + entries = list(logbook.humanify(self.hass, events)) self.assertEqual(2, len(entries)) self.assert_entry(entries[0], name='Home Assistant', message='started', @@ -266,7 +267,7 @@ class TestComponentLogbook(unittest.TestCase): events = logbook._exclude_events( (ha.Event(EVENT_HOMEASSISTANT_STOP), eventA, eventB), config[logbook.DOMAIN]) - entries = list(logbook.humanify(events)) + entries = list(logbook.humanify(self.hass, events)) self.assertEqual(2, len(entries)) self.assert_entry( @@ -292,7 +293,7 @@ class TestComponentLogbook(unittest.TestCase): events = logbook._exclude_events( (ha.Event(EVENT_HOMEASSISTANT_STOP), eventA, eventB), config[logbook.DOMAIN]) - entries = list(logbook.humanify(events)) + entries = list(logbook.humanify(self.hass, events)) self.assertEqual(2, len(entries)) self.assert_entry( @@ -318,7 +319,7 @@ class TestComponentLogbook(unittest.TestCase): events = logbook._exclude_events( (ha.Event(EVENT_HOMEASSISTANT_START), eventA, eventB), config[logbook.DOMAIN]) - entries = list(logbook.humanify(events)) + entries = list(logbook.humanify(self.hass, events)) self.assertEqual(2, len(entries)) self.assert_entry(entries[0], name='Home Assistant', message='started', @@ -352,7 +353,7 @@ class TestComponentLogbook(unittest.TestCase): events = logbook._exclude_events( (ha.Event(EVENT_HOMEASSISTANT_START), eventA1, eventA2, eventA3, eventB1, eventB2), config[logbook.DOMAIN]) - entries = list(logbook.humanify(events)) + entries = list(logbook.humanify(self.hass, events)) self.assertEqual(3, len(entries)) self.assert_entry(entries[0], name='Home Assistant', message='started', @@ -373,7 +374,7 @@ class TestComponentLogbook(unittest.TestCase): {'auto': True}) events = logbook._exclude_events((eventA, eventB), {}) - entries = list(logbook.humanify(events)) + entries = list(logbook.humanify(self.hass, events)) self.assertEqual(1, len(entries)) self.assert_entry(entries[0], pointA, 'bla', domain='switch', @@ -391,29 +392,18 @@ class TestComponentLogbook(unittest.TestCase): pointA, entity_id2, 20, last_changed=pointA, last_updated=pointB) events = logbook._exclude_events((eventA, eventB), {}) - entries = list(logbook.humanify(events)) + entries = list(logbook.humanify(self.hass, events)) self.assertEqual(1, len(entries)) self.assert_entry(entries[0], pointA, 'bla', domain='switch', entity_id=entity_id) - def test_entry_to_dict(self): - """Test conversion of entry to dict.""" - entry = logbook.Entry( - dt_util.utcnow(), 'Alarm', 'is triggered', 'switch', 'test_switch' - ) - data = entry.as_dict() - self.assertEqual('Alarm', data.get(logbook.ATTR_NAME)) - self.assertEqual('is triggered', data.get(logbook.ATTR_MESSAGE)) - self.assertEqual('switch', data.get(logbook.ATTR_DOMAIN)) - self.assertEqual('test_switch', data.get(logbook.ATTR_ENTITY_ID)) - def test_home_assistant_start_stop_grouped(self): """Test if HA start and stop events are grouped. Events that are occurring in the same minute. """ - entries = list(logbook.humanify(( + entries = list(logbook.humanify(self.hass, ( ha.Event(EVENT_HOMEASSISTANT_STOP), ha.Event(EVENT_HOMEASSISTANT_START), ))) @@ -428,7 +418,7 @@ class TestComponentLogbook(unittest.TestCase): entity_id = 'switch.bla' pointA = dt_util.utcnow() - entries = list(logbook.humanify(( + entries = list(logbook.humanify(self.hass, ( ha.Event(EVENT_HOMEASSISTANT_START), self.create_state_changed_event(pointA, entity_id, 10) ))) @@ -509,7 +499,7 @@ class TestComponentLogbook(unittest.TestCase): message = 'has a custom entry' entity_id = 'sun.sun' - entries = list(logbook.humanify(( + entries = list(logbook.humanify(self.hass, ( ha.Event(logbook.EVENT_LOGBOOK_ENTRY, { logbook.ATTR_NAME: name, logbook.ATTR_MESSAGE: message, @@ -526,19 +516,19 @@ class TestComponentLogbook(unittest.TestCase): domain=None, entity_id=None): """Assert an entry is what is expected.""" if when: - self.assertEqual(when, entry.when) + self.assertEqual(when, entry['when']) if name: - self.assertEqual(name, entry.name) + self.assertEqual(name, entry['name']) if message: - self.assertEqual(message, entry.message) + self.assertEqual(message, entry['message']) if domain: - self.assertEqual(domain, entry.domain) + self.assertEqual(domain, entry['domain']) if entity_id: - self.assertEqual(entity_id, entry.entity_id) + self.assertEqual(entity_id, entry['entity_id']) def create_state_changed_event(self, event_time_fired, entity_id, state, attributes=None, last_changed=None, @@ -566,3 +556,131 @@ async def test_logbook_view(hass, aiohttp_client): response = await client.get( '/api/logbook/{}'.format(dt_util.utcnow().isoformat())) assert response.status == 200 + + +async def test_logbook_view_period_entity(hass, aiohttp_client): + """Test the logbook view with period and entity.""" + await hass.async_add_job(init_recorder_component, hass) + await async_setup_component(hass, 'logbook', {}) + await hass.components.recorder.wait_connection_ready() + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + entity_id_test = 'switch.test' + hass.states.async_set(entity_id_test, STATE_OFF) + hass.states.async_set(entity_id_test, STATE_ON) + entity_id_second = 'switch.second' + hass.states.async_set(entity_id_second, STATE_OFF) + hass.states.async_set(entity_id_second, STATE_ON) + await hass.async_block_till_done() + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + client = await aiohttp_client(hass.http.app) + + # Today time 00:00:00 + start = dt_util.utcnow().date() + start_date = datetime(start.year, start.month, start.day) + + # Test today entries without filters + response = await client.get( + '/api/logbook/{}'.format(start_date.isoformat())) + assert response.status == 200 + json = await response.json() + assert len(json) == 2 + assert json[0]['entity_id'] == entity_id_test + assert json[1]['entity_id'] == entity_id_second + + # Test today entries with filter by period + response = await client.get( + '/api/logbook/{}?period=1'.format(start_date.isoformat())) + assert response.status == 200 + json = await response.json() + assert len(json) == 2 + assert json[0]['entity_id'] == entity_id_test + assert json[1]['entity_id'] == entity_id_second + + # Test today entries with filter by entity_id + response = await client.get( + '/api/logbook/{}?entity=switch.test'.format( + start_date.isoformat())) + assert response.status == 200 + json = await response.json() + assert len(json) == 1 + assert json[0]['entity_id'] == entity_id_test + + # Test entries for 3 days with filter by entity_id + response = await client.get( + '/api/logbook/{}?period=3&entity=switch.test'.format( + start_date.isoformat())) + assert response.status == 200 + json = await response.json() + assert len(json) == 1 + assert json[0]['entity_id'] == entity_id_test + + # Tomorrow time 00:00:00 + start = (dt_util.utcnow() + timedelta(days=1)).date() + start_date = datetime(start.year, start.month, start.day) + + # Test tomorrow entries without filters + response = await client.get( + '/api/logbook/{}'.format(start_date.isoformat())) + assert response.status == 200 + json = await response.json() + assert len(json) == 0 + + # Test tomorrow entries with filter by entity_id + response = await client.get( + '/api/logbook/{}?entity=switch.test'.format( + start_date.isoformat())) + assert response.status == 200 + json = await response.json() + assert len(json) == 0 + + # Test entries from tomorrow to 3 days ago with filter by entity_id + response = await client.get( + '/api/logbook/{}?period=3&entity=switch.test'.format( + start_date.isoformat())) + assert response.status == 200 + json = await response.json() + assert len(json) == 1 + assert json[0]['entity_id'] == entity_id_test + + +async def test_humanify_alexa_event(hass): + """Test humanifying Alexa event.""" + hass.states.async_set('light.kitchen', 'on', { + 'friendly_name': 'Kitchen Light' + }) + + results = list(logbook.humanify(hass, [ + ha.Event(EVENT_ALEXA_SMART_HOME, {'request': { + 'namespace': 'Alexa.Discovery', + 'name': 'Discover', + }}), + ha.Event(EVENT_ALEXA_SMART_HOME, {'request': { + 'namespace': 'Alexa.PowerController', + 'name': 'TurnOn', + 'entity_id': 'light.kitchen' + }}), + ha.Event(EVENT_ALEXA_SMART_HOME, {'request': { + 'namespace': 'Alexa.PowerController', + 'name': 'TurnOn', + 'entity_id': 'light.non_existing' + }}), + + ])) + + event1, event2, event3 = results + + assert event1['name'] == 'Amazon Alexa' + assert event1['message'] == 'send command Alexa.Discovery/Discover' + assert event1['entity_id'] is None + + assert event2['name'] == 'Amazon Alexa' + assert event2['message'] == \ + 'send command Alexa.PowerController/TurnOn for Kitchen Light' + assert event2['entity_id'] == 'light.kitchen' + + assert event3['name'] == 'Amazon Alexa' + assert event3['message'] == \ + 'send command Alexa.PowerController/TurnOn for light.non_existing' + assert event3['entity_id'] == 'light.non_existing' diff --git a/tests/components/test_microsoft_face.py b/tests/components/test_microsoft_face.py index 601d5e7ebcc..6c9c58da7df 100644 --- a/tests/components/test_microsoft_face.py +++ b/tests/components/test_microsoft_face.py @@ -3,12 +3,72 @@ import asyncio from unittest.mock import patch from homeassistant.components import camera, microsoft_face as mf +from homeassistant.components.microsoft_face import ( + ATTR_CAMERA_ENTITY, ATTR_GROUP, ATTR_PERSON, DOMAIN, SERVICE_CREATE_GROUP, + SERVICE_CREATE_PERSON, SERVICE_DELETE_GROUP, SERVICE_DELETE_PERSON, + SERVICE_FACE_PERSON, SERVICE_TRAIN_GROUP) +from homeassistant.const import ATTR_NAME from homeassistant.setup import setup_component from tests.common import ( get_test_home_assistant, assert_setup_component, mock_coro, load_fixture) +def create_group(hass, name): + """Create a new person group. + + This is a legacy helper method. Do not use it for new tests. + """ + data = {ATTR_NAME: name} + hass.services.call(DOMAIN, SERVICE_CREATE_GROUP, data) + + +def delete_group(hass, name): + """Delete a person group. + + This is a legacy helper method. Do not use it for new tests. + """ + data = {ATTR_NAME: name} + hass.services.call(DOMAIN, SERVICE_DELETE_GROUP, data) + + +def train_group(hass, group): + """Train a person group. + + This is a legacy helper method. Do not use it for new tests. + """ + data = {ATTR_GROUP: group} + hass.services.call(DOMAIN, SERVICE_TRAIN_GROUP, data) + + +def create_person(hass, group, name): + """Create a person in a group. + + This is a legacy helper method. Do not use it for new tests. + """ + data = {ATTR_GROUP: group, ATTR_NAME: name} + hass.services.call(DOMAIN, SERVICE_CREATE_PERSON, data) + + +def delete_person(hass, group, name): + """Delete a person in a group. + + This is a legacy helper method. Do not use it for new tests. + """ + data = {ATTR_GROUP: group, ATTR_NAME: name} + hass.services.call(DOMAIN, SERVICE_DELETE_PERSON, data) + + +def face_person(hass, group, person, camera_entity): + """Add a new face picture to a person. + + This is a legacy helper method. Do not use it for new tests. + """ + data = {ATTR_GROUP: group, ATTR_PERSON: person, + ATTR_CAMERA_ENTITY: camera_entity} + hass.services.call(DOMAIN, SERVICE_FACE_PERSON, data) + + class TestMicrosoftFaceSetup: """Test the microsoft face component.""" @@ -108,14 +168,14 @@ class TestMicrosoftFaceSetup: with assert_setup_component(3, mf.DOMAIN): setup_component(self.hass, mf.DOMAIN, self.config) - mf.create_group(self.hass, 'Service Group') + create_group(self.hass, 'Service Group') self.hass.block_till_done() entity = self.hass.states.get('microsoft_face.service_group') assert entity is not None assert len(aioclient_mock.mock_calls) == 1 - mf.delete_group(self.hass, 'Service Group') + delete_group(self.hass, 'Service Group') self.hass.block_till_done() entity = self.hass.states.get('microsoft_face.service_group') @@ -153,7 +213,7 @@ class TestMicrosoftFaceSetup: status=200, text="{}" ) - mf.create_person(self.hass, 'test group1', 'Hans') + create_person(self.hass, 'test group1', 'Hans') self.hass.block_till_done() entity_group1 = self.hass.states.get('microsoft_face.test_group1') @@ -163,7 +223,7 @@ class TestMicrosoftFaceSetup: assert entity_group1.attributes['Hans'] == \ '25985303-c537-4467-b41d-bdb45cd95ca1' - mf.delete_person(self.hass, 'test group1', 'Hans') + delete_person(self.hass, 'test group1', 'Hans') self.hass.block_till_done() entity_group1 = self.hass.states.get('microsoft_face.test_group1') @@ -184,7 +244,7 @@ class TestMicrosoftFaceSetup: status=200, text="{}" ) - mf.train_group(self.hass, 'Service Group') + train_group(self.hass, 'Service Group') self.hass.block_till_done() assert len(aioclient_mock.mock_calls) == 1 @@ -219,7 +279,7 @@ class TestMicrosoftFaceSetup: status=200, text="{}" ) - mf.face_person( + face_person( self.hass, 'test_group2', 'David', 'camera.demo_camera') self.hass.block_till_done() @@ -238,7 +298,7 @@ class TestMicrosoftFaceSetup: with assert_setup_component(3, mf.DOMAIN): setup_component(self.hass, mf.DOMAIN, self.config) - mf.create_group(self.hass, 'Service Group') + create_group(self.hass, 'Service Group') self.hass.block_till_done() entity = self.hass.states.get('microsoft_face.service_group') @@ -257,7 +317,7 @@ class TestMicrosoftFaceSetup: with assert_setup_component(3, mf.DOMAIN): setup_component(self.hass, mf.DOMAIN, self.config) - mf.create_group(self.hass, 'Service Group') + create_group(self.hass, 'Service Group') self.hass.block_till_done() entity = self.hass.states.get('microsoft_face.service_group') diff --git a/tests/components/test_panel_iframe.py b/tests/components/test_panel_iframe.py index 3ac06c09a26..cb868f64b58 100644 --- a/tests/components/test_panel_iframe.py +++ b/tests/components/test_panel_iframe.py @@ -62,7 +62,7 @@ class TestPanelIframe(unittest.TestCase): panels = self.hass.data[frontend.DATA_PANELS] - assert panels.get('router').to_response(self.hass, None) == { + assert panels.get('router').to_response() == { 'component_name': 'iframe', 'config': {'url': 'http://192.168.1.1'}, 'icon': 'mdi:network-wireless', @@ -70,7 +70,7 @@ class TestPanelIframe(unittest.TestCase): 'url_path': 'router' } - assert panels.get('weather').to_response(self.hass, None) == { + assert panels.get('weather').to_response() == { 'component_name': 'iframe', 'config': {'url': 'https://www.wunderground.com/us/ca/san-diego'}, 'icon': 'mdi:weather', @@ -78,7 +78,7 @@ class TestPanelIframe(unittest.TestCase): 'url_path': 'weather', } - assert panels.get('api').to_response(self.hass, None) == { + assert panels.get('api').to_response() == { 'component_name': 'iframe', 'config': {'url': '/api'}, 'icon': 'mdi:weather', @@ -86,7 +86,7 @@ class TestPanelIframe(unittest.TestCase): 'url_path': 'api', } - assert panels.get('ftp').to_response(self.hass, None) == { + assert panels.get('ftp').to_response() == { 'component_name': 'iframe', 'config': {'url': 'ftp://some/ftp'}, 'icon': 'mdi:weather', diff --git a/tests/components/test_script.py b/tests/components/test_script.py index b9aa921cb63..43727b6d559 100644 --- a/tests/components/test_script.py +++ b/tests/components/test_script.py @@ -3,9 +3,13 @@ import unittest from unittest.mock import patch -from homeassistant.core import Context, callback -from homeassistant.setup import setup_component from homeassistant.components import script +from homeassistant.components.script import DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_RELOAD, SERVICE_TOGGLE, SERVICE_TURN_OFF) +from homeassistant.core import Context, callback, split_entity_id +from homeassistant.loader import bind_hass +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant @@ -13,6 +17,44 @@ from tests.common import get_test_home_assistant ENTITY_ID = 'script.test' +@bind_hass +def turn_on(hass, entity_id, variables=None, context=None): + """Turn script on. + + This is a legacy helper method. Do not use it for new tests. + """ + _, object_id = split_entity_id(entity_id) + + hass.services.call(DOMAIN, object_id, variables, context=context) + + +@bind_hass +def turn_off(hass, entity_id): + """Turn script on. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}) + + +@bind_hass +def toggle(hass, entity_id): + """Toggle the script. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}) + + +@bind_hass +def reload(hass): + """Reload script component. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.services.call(DOMAIN, SERVICE_RELOAD) + + class TestScriptComponent(unittest.TestCase): """Test the Script component.""" @@ -76,17 +118,17 @@ class TestScriptComponent(unittest.TestCase): } }) - script.turn_on(self.hass, ENTITY_ID) + turn_on(self.hass, ENTITY_ID) self.hass.block_till_done() self.assertTrue(script.is_on(self.hass, ENTITY_ID)) self.assertEqual(0, len(events)) # Calling turn_on a second time should not advance the script - script.turn_on(self.hass, ENTITY_ID) + turn_on(self.hass, ENTITY_ID) self.hass.block_till_done() self.assertEqual(0, len(events)) - script.turn_off(self.hass, ENTITY_ID) + turn_off(self.hass, ENTITY_ID) self.hass.block_till_done() self.assertFalse(script.is_on(self.hass, ENTITY_ID)) self.assertEqual(0, len(events)) @@ -121,12 +163,12 @@ class TestScriptComponent(unittest.TestCase): } }) - script.toggle(self.hass, ENTITY_ID) + toggle(self.hass, ENTITY_ID) self.hass.block_till_done() self.assertTrue(script.is_on(self.hass, ENTITY_ID)) self.assertEqual(0, len(events)) - script.toggle(self.hass, ENTITY_ID) + toggle(self.hass, ENTITY_ID) self.hass.block_till_done() self.assertFalse(script.is_on(self.hass, ENTITY_ID)) self.assertEqual(0, len(events)) @@ -156,7 +198,7 @@ class TestScriptComponent(unittest.TestCase): }, }) - script.turn_on(self.hass, ENTITY_ID, { + turn_on(self.hass, ENTITY_ID, { 'greeting': 'world' }, context=context) @@ -204,7 +246,7 @@ class TestScriptComponent(unittest.TestCase): }}}): with patch('homeassistant.config.find_config_file', return_value=''): - script.reload(self.hass) + reload(self.hass) self.hass.block_till_done() assert self.hass.states.get(ENTITY_ID) is None diff --git a/tests/components/test_spc.py b/tests/components/test_spc.py index d4bedda4e96..bcbf970a48b 100644 --- a/tests/components/test_spc.py +++ b/tests/components/test_spc.py @@ -59,7 +59,8 @@ async def test_update_alarm_device(hass): return_value=mock_coro(True)): assert await async_setup_component(hass, 'spc', config) is True - await hass.async_block_till_done() + await hass.async_block_till_done() + entity_id = 'alarm_control_panel.house' assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY diff --git a/tests/components/test_upnp.py b/tests/components/test_upnp.py deleted file mode 100644 index 6089e6859f2..00000000000 --- a/tests/components/test_upnp.py +++ /dev/null @@ -1,183 +0,0 @@ -"""Test the UPNP component.""" -from collections import OrderedDict -from unittest.mock import patch, MagicMock - -import pytest - -from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.setup import async_setup_component -from homeassistant.components.upnp import IP_SERVICE, DATA_UPNP - - -class MockService(MagicMock): - """Mock upnp IP service.""" - - async def add_port_mapping(self, *args, **kwargs): - """Original function.""" - self.mock_add_port_mapping(*args, **kwargs) - - async def delete_port_mapping(self, *args, **kwargs): - """Original function.""" - self.mock_delete_port_mapping(*args, **kwargs) - - -class MockDevice(MagicMock): - """Mock upnp device.""" - - def find_first_service(self, *args, **kwargs): - """Original function.""" - self._service = MockService() - return self._service - - def peep_first_service(self): - """Access Mock first service.""" - return self._service - - -class MockResp(MagicMock): - """Mock upnp msearch response.""" - - async def get_device(self, *args, **kwargs): - """Original function.""" - device = MockDevice() - service = {'serviceType': IP_SERVICE} - device.services = [service] - return device - - -@pytest.fixture -def mock_msearch_first(*args, **kwargs): - """Wrap async mock msearch_first.""" - async def async_mock_msearch_first(*args, **kwargs): - """Mock msearch_first.""" - return MockResp(*args, **kwargs) - - with patch('pyupnp_async.msearch_first', new=async_mock_msearch_first): - yield - - -@pytest.fixture -def mock_async_exception(*args, **kwargs): - """Wrap async mock exception.""" - async def async_mock_exception(*args, **kwargs): - return Exception - - with patch('pyupnp_async.msearch_first', new=async_mock_exception): - yield - - -@pytest.fixture -def mock_local_ip(): - """Mock get_local_ip.""" - with patch('homeassistant.components.upnp.get_local_ip', - return_value='192.168.0.10'): - yield - - -async def test_setup_fail_if_no_ip(hass): - """Test setup fails if we can't find a local IP.""" - with patch('homeassistant.components.upnp.get_local_ip', - return_value='127.0.0.1'): - result = await async_setup_component(hass, 'upnp', { - 'upnp': {} - }) - - assert not result - - -async def test_setup_fail_if_cannot_select_igd(hass, - mock_local_ip, - mock_async_exception): - """Test setup fails if we can't find an UPnP IGD.""" - result = await async_setup_component(hass, 'upnp', { - 'upnp': {} - }) - - assert not result - - -async def test_setup_succeeds_if_specify_ip(hass, mock_msearch_first): - """Test setup succeeds if we specify IP and can't find a local IP.""" - with patch('homeassistant.components.upnp.get_local_ip', - return_value='127.0.0.1'): - result = await async_setup_component(hass, 'upnp', { - 'upnp': { - 'local_ip': '192.168.0.10', - 'port_mapping': 'True' - } - }) - - assert result - mock_service = hass.data[DATA_UPNP].peep_first_service() - assert len(mock_service.mock_add_port_mapping.mock_calls) == 1 - mock_service.mock_add_port_mapping.assert_called_once_with( - 8123, 8123, '192.168.0.10', 'TCP', desc='Home Assistant') - - -async def test_no_config_maps_hass_local_to_remote_port(hass, - mock_local_ip, - mock_msearch_first): - """Test by default we map local to remote port.""" - result = await async_setup_component(hass, 'upnp', { - 'upnp': { - 'port_mapping': 'True' - } - }) - - assert result - mock_service = hass.data[DATA_UPNP].peep_first_service() - assert len(mock_service.mock_add_port_mapping.mock_calls) == 1 - mock_service.mock_add_port_mapping.assert_called_once_with( - 8123, 8123, '192.168.0.10', 'TCP', desc='Home Assistant') - - -async def test_map_hass_to_remote_port(hass, - mock_local_ip, - mock_msearch_first): - """Test mapping hass to remote port.""" - result = await async_setup_component(hass, 'upnp', { - 'upnp': { - 'port_mapping': 'True', - 'ports': { - 'hass': 1000 - } - } - }) - - assert result - mock_service = hass.data[DATA_UPNP].peep_first_service() - assert len(mock_service.mock_add_port_mapping.mock_calls) == 1 - mock_service.mock_add_port_mapping.assert_called_once_with( - 8123, 1000, '192.168.0.10', 'TCP', desc='Home Assistant') - - -async def test_map_internal_to_remote_ports(hass, - mock_local_ip, - mock_msearch_first): - """Test mapping local to remote ports.""" - ports = OrderedDict() - ports['hass'] = 1000 - ports[1883] = 3883 - - result = await async_setup_component(hass, 'upnp', { - 'upnp': { - 'port_mapping': 'True', - 'ports': ports - } - }) - - assert result - mock_service = hass.data[DATA_UPNP].peep_first_service() - assert len(mock_service.mock_add_port_mapping.mock_calls) == 2 - - mock_service.mock_add_port_mapping.assert_any_call( - 8123, 1000, '192.168.0.10', 'TCP', desc='Home Assistant') - mock_service.mock_add_port_mapping.assert_any_call( - 1883, 3883, '192.168.0.10', 'TCP', desc='Home Assistant') - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - assert len(mock_service.mock_delete_port_mapping.mock_calls) == 2 - - mock_service.mock_delete_port_mapping.assert_any_call(1000, 'TCP') - mock_service.mock_delete_port_mapping.assert_any_call(3883, 'TCP') diff --git a/tests/components/test_webhook.py b/tests/components/test_webhook.py new file mode 100644 index 00000000000..9434c3d98d5 --- /dev/null +++ b/tests/components/test_webhook.py @@ -0,0 +1,96 @@ +"""Test the webhook component.""" +from unittest.mock import Mock + +import pytest + +from homeassistant.setup import async_setup_component + + +@pytest.fixture +def mock_client(hass, aiohttp_client): + """Create http client for webhooks.""" + hass.loop.run_until_complete(async_setup_component(hass, 'webhook', {})) + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) + + +async def test_unregistering_webhook(hass, mock_client): + """Test unregistering a webhook.""" + hooks = [] + webhook_id = hass.components.webhook.async_generate_id() + + async def handle(*args): + """Handle webhook.""" + hooks.append(args) + + hass.components.webhook.async_register(webhook_id, handle) + + resp = await mock_client.post('/api/webhook/{}'.format(webhook_id)) + assert resp.status == 200 + assert len(hooks) == 1 + + hass.components.webhook.async_unregister(webhook_id) + + resp = await mock_client.post('/api/webhook/{}'.format(webhook_id)) + assert resp.status == 200 + assert len(hooks) == 1 + + +async def test_generate_webhook_url(hass): + """Test we generate a webhook url correctly.""" + hass.config.api = Mock(base_url='https://example.com') + url = hass.components.webhook.async_generate_url('some_id') + + assert url == 'https://example.com/api/webhook/some_id' + + +async def test_posting_webhook_nonexisting(hass, mock_client): + """Test posting to a nonexisting webhook.""" + resp = await mock_client.post('/api/webhook/non-existing') + assert resp.status == 200 + + +async def test_posting_webhook_invalid_json(hass, mock_client): + """Test posting to a nonexisting webhook.""" + hass.components.webhook.async_register('hello', None) + resp = await mock_client.post('/api/webhook/hello', data='not-json') + assert resp.status == 200 + + +async def test_posting_webhook_json(hass, mock_client): + """Test posting a webhook with JSON data.""" + hooks = [] + webhook_id = hass.components.webhook.async_generate_id() + + async def handle(*args): + """Handle webhook.""" + hooks.append((args[0], args[1], await args[2].text())) + + hass.components.webhook.async_register(webhook_id, handle) + + resp = await mock_client.post('/api/webhook/{}'.format(webhook_id), json={ + 'data': True + }) + assert resp.status == 200 + assert len(hooks) == 1 + assert hooks[0][0] is hass + assert hooks[0][1] == webhook_id + assert hooks[0][2] == '{"data": true}' + + +async def test_posting_webhook_no_data(hass, mock_client): + """Test posting a webhook with no data.""" + hooks = [] + webhook_id = hass.components.webhook.async_generate_id() + + async def handle(*args): + """Handle webhook.""" + hooks.append(args) + + hass.components.webhook.async_register(webhook_id, handle) + + resp = await mock_client.post('/api/webhook/{}'.format(webhook_id)) + assert resp.status == 200 + assert len(hooks) == 1 + assert hooks[0][0] is hass + assert hooks[0][1] == webhook_id + assert await hooks[0][2].text() == '' diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py deleted file mode 100644 index cf74081adb1..00000000000 --- a/tests/components/test_websocket_api.py +++ /dev/null @@ -1,558 +0,0 @@ -"""Tests for the Home Assistant Websocket API.""" -import asyncio -from unittest.mock import patch, Mock - -from aiohttp import WSMsgType -from async_timeout import timeout -import pytest - -from homeassistant.core import callback -from homeassistant.components import websocket_api as wapi -from homeassistant.setup import async_setup_component - -from tests.common import mock_coro, async_mock_service - -API_PASSWORD = 'test1234' - - -@pytest.fixture -def websocket_client(hass, hass_ws_client): - """Create a websocket client.""" - return hass.loop.run_until_complete(hass_ws_client(hass)) - - -@pytest.fixture -def no_auth_websocket_client(hass, loop, aiohttp_client): - """Websocket connection that requires authentication.""" - assert loop.run_until_complete( - async_setup_component(hass, 'websocket_api', { - 'http': { - 'api_password': API_PASSWORD - } - })) - - client = loop.run_until_complete(aiohttp_client(hass.http.app)) - ws = loop.run_until_complete(client.ws_connect(wapi.URL)) - - auth_ok = loop.run_until_complete(ws.receive_json()) - assert auth_ok['type'] == wapi.TYPE_AUTH_REQUIRED - - yield ws - - if not ws.closed: - loop.run_until_complete(ws.close()) - - -@pytest.fixture -def mock_low_queue(): - """Mock a low queue.""" - with patch.object(wapi, 'MAX_PENDING_MSG', 5): - yield - - -@asyncio.coroutine -def test_auth_via_msg(no_auth_websocket_client): - """Test authenticating.""" - yield from no_auth_websocket_client.send_json({ - 'type': wapi.TYPE_AUTH, - 'api_password': API_PASSWORD - }) - - msg = yield from no_auth_websocket_client.receive_json() - - assert msg['type'] == wapi.TYPE_AUTH_OK - - -@asyncio.coroutine -def test_auth_via_msg_incorrect_pass(no_auth_websocket_client): - """Test authenticating.""" - with patch('homeassistant.components.websocket_api.process_wrong_login', - return_value=mock_coro()) as mock_process_wrong_login: - yield from no_auth_websocket_client.send_json({ - 'type': wapi.TYPE_AUTH, - 'api_password': API_PASSWORD + 'wrong' - }) - - msg = yield from no_auth_websocket_client.receive_json() - - assert mock_process_wrong_login.called - assert msg['type'] == wapi.TYPE_AUTH_INVALID - assert msg['message'] == 'Invalid access token or password' - - -@asyncio.coroutine -def test_pre_auth_only_auth_allowed(no_auth_websocket_client): - """Verify that before authentication, only auth messages are allowed.""" - yield from no_auth_websocket_client.send_json({ - 'type': wapi.TYPE_CALL_SERVICE, - 'domain': 'domain_test', - 'service': 'test_service', - 'service_data': { - 'hello': 'world' - } - }) - - msg = yield from no_auth_websocket_client.receive_json() - - assert msg['type'] == wapi.TYPE_AUTH_INVALID - assert msg['message'].startswith('Message incorrectly formatted') - - -@asyncio.coroutine -def test_invalid_message_format(websocket_client): - """Test sending invalid JSON.""" - yield from websocket_client.send_json({'type': 5}) - - msg = yield from websocket_client.receive_json() - - assert msg['type'] == wapi.TYPE_RESULT - error = msg['error'] - assert error['code'] == wapi.ERR_INVALID_FORMAT - assert error['message'].startswith('Message incorrectly formatted') - - -@asyncio.coroutine -def test_invalid_json(websocket_client): - """Test sending invalid JSON.""" - yield from websocket_client.send_str('this is not JSON') - - msg = yield from websocket_client.receive() - - assert msg.type == WSMsgType.close - - -@asyncio.coroutine -def test_quiting_hass(hass, websocket_client): - """Test sending invalid JSON.""" - with patch.object(hass.loop, 'stop'): - yield from hass.async_stop() - - msg = yield from websocket_client.receive() - - assert msg.type == WSMsgType.CLOSE - - -@asyncio.coroutine -def test_call_service(hass, websocket_client): - """Test call service command.""" - calls = [] - - @callback - def service_call(call): - calls.append(call) - - hass.services.async_register('domain_test', 'test_service', service_call) - - yield from websocket_client.send_json({ - 'id': 5, - 'type': wapi.TYPE_CALL_SERVICE, - 'domain': 'domain_test', - 'service': 'test_service', - 'service_data': { - 'hello': 'world' - } - }) - - msg = yield from websocket_client.receive_json() - assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT - assert msg['success'] - - assert len(calls) == 1 - call = calls[0] - - assert call.domain == 'domain_test' - assert call.service == 'test_service' - assert call.data == {'hello': 'world'} - - -@asyncio.coroutine -def test_subscribe_unsubscribe_events(hass, websocket_client): - """Test subscribe/unsubscribe events command.""" - init_count = sum(hass.bus.async_listeners().values()) - - yield from websocket_client.send_json({ - 'id': 5, - 'type': wapi.TYPE_SUBSCRIBE_EVENTS, - 'event_type': 'test_event' - }) - - msg = yield from websocket_client.receive_json() - assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT - assert msg['success'] - - # Verify we have a new listener - assert sum(hass.bus.async_listeners().values()) == init_count + 1 - - hass.bus.async_fire('ignore_event') - hass.bus.async_fire('test_event', {'hello': 'world'}) - hass.bus.async_fire('ignore_event') - - with timeout(3, loop=hass.loop): - msg = yield from websocket_client.receive_json() - - assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_EVENT - event = msg['event'] - - assert event['event_type'] == 'test_event' - assert event['data'] == {'hello': 'world'} - assert event['origin'] == 'LOCAL' - - yield from websocket_client.send_json({ - 'id': 6, - 'type': wapi.TYPE_UNSUBSCRIBE_EVENTS, - 'subscription': 5 - }) - - msg = yield from websocket_client.receive_json() - assert msg['id'] == 6 - assert msg['type'] == wapi.TYPE_RESULT - assert msg['success'] - - # Check our listener got unsubscribed - assert sum(hass.bus.async_listeners().values()) == init_count - - -@asyncio.coroutine -def test_get_states(hass, websocket_client): - """Test get_states command.""" - hass.states.async_set('greeting.hello', 'world') - hass.states.async_set('greeting.bye', 'universe') - - yield from websocket_client.send_json({ - 'id': 5, - 'type': wapi.TYPE_GET_STATES, - }) - - msg = yield from websocket_client.receive_json() - assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT - assert msg['success'] - - states = [] - for state in hass.states.async_all(): - state = state.as_dict() - state['last_changed'] = state['last_changed'].isoformat() - state['last_updated'] = state['last_updated'].isoformat() - states.append(state) - - assert msg['result'] == states - - -@asyncio.coroutine -def test_get_services(hass, websocket_client): - """Test get_services command.""" - yield from websocket_client.send_json({ - 'id': 5, - 'type': wapi.TYPE_GET_SERVICES, - }) - - msg = yield from websocket_client.receive_json() - assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT - assert msg['success'] - assert msg['result'] == hass.services.async_services() - - -@asyncio.coroutine -def test_get_config(hass, websocket_client): - """Test get_config command.""" - yield from websocket_client.send_json({ - 'id': 5, - 'type': wapi.TYPE_GET_CONFIG, - }) - - msg = yield from websocket_client.receive_json() - assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT - assert msg['success'] - - if 'components' in msg['result']: - msg['result']['components'] = set(msg['result']['components']) - if 'whitelist_external_dirs' in msg['result']: - msg['result']['whitelist_external_dirs'] = \ - set(msg['result']['whitelist_external_dirs']) - - assert msg['result'] == hass.config.as_dict() - - -@asyncio.coroutine -def test_ping(websocket_client): - """Test get_panels command.""" - yield from websocket_client.send_json({ - 'id': 5, - 'type': wapi.TYPE_PING, - }) - - msg = yield from websocket_client.receive_json() - assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_PONG - - -@asyncio.coroutine -def test_pending_msg_overflow(hass, mock_low_queue, websocket_client): - """Test get_panels command.""" - for idx in range(10): - yield from websocket_client.send_json({ - 'id': idx + 1, - 'type': wapi.TYPE_PING, - }) - msg = yield from websocket_client.receive() - assert msg.type == WSMsgType.close - - -@asyncio.coroutine -def test_unknown_command(websocket_client): - """Test get_panels command.""" - yield from websocket_client.send_json({ - 'id': 5, - 'type': 'unknown_command', - }) - - msg = yield from websocket_client.receive_json() - assert not msg['success'] - assert msg['error']['code'] == wapi.ERR_UNKNOWN_COMMAND - - -async def test_auth_active_with_token(hass, aiohttp_client, hass_access_token): - """Test authenticating with a token.""" - assert await async_setup_component(hass, 'websocket_api', { - 'http': { - 'api_password': API_PASSWORD - } - }) - - client = await aiohttp_client(hass.http.app) - - async with client.ws_connect(wapi.URL) as ws: - with patch('homeassistant.auth.AuthManager.active') as auth_active: - auth_active.return_value = True - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED - - await ws.send_json({ - 'type': wapi.TYPE_AUTH, - 'access_token': hass_access_token - }) - - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_OK - - -async def test_auth_active_user_inactive(hass, aiohttp_client, - hass_access_token): - """Test authenticating with a token.""" - refresh_token = await hass.auth.async_validate_access_token( - hass_access_token) - refresh_token.user.is_active = False - assert await async_setup_component(hass, 'websocket_api', { - 'http': { - 'api_password': API_PASSWORD - } - }) - - client = await aiohttp_client(hass.http.app) - - async with client.ws_connect(wapi.URL) as ws: - with patch('homeassistant.auth.AuthManager.active') as auth_active: - auth_active.return_value = True - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED - - await ws.send_json({ - 'type': wapi.TYPE_AUTH, - 'access_token': hass_access_token - }) - - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_INVALID - - -async def test_auth_active_with_password_not_allow(hass, aiohttp_client): - """Test authenticating with a token.""" - assert await async_setup_component(hass, 'websocket_api', { - 'http': { - 'api_password': API_PASSWORD - } - }) - - client = await aiohttp_client(hass.http.app) - - async with client.ws_connect(wapi.URL) as ws: - with patch('homeassistant.auth.AuthManager.active', - return_value=True): - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED - - await ws.send_json({ - 'type': wapi.TYPE_AUTH, - 'api_password': API_PASSWORD - }) - - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_INVALID - - -async def test_auth_legacy_support_with_password(hass, aiohttp_client): - """Test authenticating with a token.""" - assert await async_setup_component(hass, 'websocket_api', { - 'http': { - 'api_password': API_PASSWORD - } - }) - - client = await aiohttp_client(hass.http.app) - - async with client.ws_connect(wapi.URL) as ws: - with patch('homeassistant.auth.AuthManager.active', - return_value=True),\ - patch('homeassistant.auth.AuthManager.support_legacy', - return_value=True): - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED - - await ws.send_json({ - 'type': wapi.TYPE_AUTH, - 'api_password': API_PASSWORD - }) - - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_OK - - -async def test_auth_with_invalid_token(hass, aiohttp_client): - """Test authenticating with a token.""" - assert await async_setup_component(hass, 'websocket_api', { - 'http': { - 'api_password': API_PASSWORD - } - }) - - client = await aiohttp_client(hass.http.app) - - async with client.ws_connect(wapi.URL) as ws: - with patch('homeassistant.auth.AuthManager.active') as auth_active: - auth_active.return_value = True - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED - - await ws.send_json({ - 'type': wapi.TYPE_AUTH, - 'access_token': 'incorrect' - }) - - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_INVALID - - -async def test_call_service_context_with_user(hass, aiohttp_client, - hass_access_token): - """Test that the user is set in the service call context.""" - assert await async_setup_component(hass, 'websocket_api', { - 'http': { - 'api_password': API_PASSWORD - } - }) - - calls = async_mock_service(hass, 'domain_test', 'test_service') - client = await aiohttp_client(hass.http.app) - - async with client.ws_connect(wapi.URL) as ws: - with patch('homeassistant.auth.AuthManager.active') as auth_active: - auth_active.return_value = True - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED - - await ws.send_json({ - 'type': wapi.TYPE_AUTH, - 'access_token': hass_access_token - }) - - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_OK - - await ws.send_json({ - 'id': 5, - 'type': wapi.TYPE_CALL_SERVICE, - 'domain': 'domain_test', - 'service': 'test_service', - 'service_data': { - 'hello': 'world' - } - }) - - msg = await ws.receive_json() - assert msg['success'] - - refresh_token = await hass.auth.async_validate_access_token( - hass_access_token) - - assert len(calls) == 1 - call = calls[0] - assert call.domain == 'domain_test' - assert call.service == 'test_service' - assert call.data == {'hello': 'world'} - assert call.context.user_id == refresh_token.user.id - - -async def test_call_service_context_no_user(hass, aiohttp_client): - """Test that connection without user sets context.""" - assert await async_setup_component(hass, 'websocket_api', { - 'http': { - 'api_password': API_PASSWORD - } - }) - - calls = async_mock_service(hass, 'domain_test', 'test_service') - client = await aiohttp_client(hass.http.app) - - async with client.ws_connect(wapi.URL) as ws: - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED - - await ws.send_json({ - 'type': wapi.TYPE_AUTH, - 'api_password': API_PASSWORD - }) - - auth_msg = await ws.receive_json() - assert auth_msg['type'] == wapi.TYPE_AUTH_OK - - await ws.send_json({ - 'id': 5, - 'type': wapi.TYPE_CALL_SERVICE, - 'domain': 'domain_test', - 'service': 'test_service', - 'service_data': { - 'hello': 'world' - } - }) - - msg = await ws.receive_json() - assert msg['success'] - - assert len(calls) == 1 - call = calls[0] - assert call.domain == 'domain_test' - assert call.service == 'test_service' - assert call.data == {'hello': 'world'} - assert call.context.user_id is None - - -async def test_handler_failing(hass, websocket_client): - """Test a command that raises.""" - hass.components.websocket_api.async_register_command( - 'bla', Mock(side_effect=TypeError), - wapi.BASE_COMMAND_MESSAGE_SCHEMA.extend({'type': 'bla'})) - await websocket_client.send_json({ - 'id': 5, - 'type': 'bla', - }) - - msg = await websocket_client.receive_json() - assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT - assert not msg['success'] - assert msg['error']['code'] == wapi.ERR_UNKNOWN_ERROR diff --git a/tests/components/tradfri/conftest.py b/tests/components/tradfri/conftest.py new file mode 100644 index 00000000000..9a5745264b7 --- /dev/null +++ b/tests/components/tradfri/conftest.py @@ -0,0 +1,12 @@ +"""Common tradfri test fixtures.""" +from unittest.mock import patch + +import pytest + + +@pytest.fixture +def mock_gateway_info(): + """Mock get_gateway_info.""" + with patch('homeassistant.components.tradfri.config_flow.' + 'get_gateway_info') as mock_gateway: + yield mock_gateway diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index 99566356f61..6756a01bbc7 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -17,14 +17,6 @@ def mock_auth(): yield mock_auth -@pytest.fixture -def mock_gateway_info(): - """Mock get_gateway_info.""" - with patch('homeassistant.components.tradfri.config_flow.' - 'get_gateway_info') as mock_gateway: - yield mock_gateway - - @pytest.fixture def mock_entry_setup(): """Mock entry setup.""" @@ -125,34 +117,65 @@ async def test_discovery_connection(hass, mock_auth, mock_entry_setup): } -async def test_import_connection(hass, mock_gateway_info, mock_entry_setup): +async def test_import_connection(hass, mock_auth, mock_entry_setup): """Test a connection via import.""" - mock_gateway_info.side_effect = \ - lambda hass, host, identity, key: mock_coro({ - 'host': host, - 'identity': identity, - 'key': key, - 'gateway_id': 'mock-gateway' - }) + mock_auth.side_effect = lambda hass, host, code: mock_coro({ + 'host': host, + 'gateway_id': 'bla', + 'identity': 'mock-iden', + 'key': 'mock-key', + }) - result = await hass.config_entries.flow.async_init( + flow = await hass.config_entries.flow.async_init( 'tradfri', context={'source': 'import'}, data={ 'host': '123.123.123.123', - 'identity': 'mock-iden', - 'key': 'mock-key', 'import_groups': True }) + result = await hass.config_entries.flow.async_configure(flow['flow_id'], { + 'security_code': 'abcd', + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result['result'].data == { 'host': '123.123.123.123', - 'gateway_id': 'mock-gateway', + 'gateway_id': 'bla', 'identity': 'mock-iden', 'key': 'mock-key', 'import_groups': True } - assert len(mock_gateway_info.mock_calls) == 1 + assert len(mock_entry_setup.mock_calls) == 1 + + +async def test_import_connection_no_groups(hass, mock_auth, mock_entry_setup): + """Test a connection via import and no groups allowed.""" + mock_auth.side_effect = lambda hass, host, code: mock_coro({ + 'host': host, + 'gateway_id': 'bla', + 'identity': 'mock-iden', + 'key': 'mock-key', + }) + + flow = await hass.config_entries.flow.async_init( + 'tradfri', context={'source': 'import'}, data={ + 'host': '123.123.123.123', + 'import_groups': False + }) + + result = await hass.config_entries.flow.async_configure(flow['flow_id'], { + 'security_code': 'abcd', + }) + + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['result'].data == { + 'host': '123.123.123.123', + 'gateway_id': 'bla', + 'identity': 'mock-iden', + 'key': 'mock-key', + 'import_groups': False + } + assert len(mock_entry_setup.mock_calls) == 1 @@ -187,6 +210,37 @@ async def test_import_connection_legacy(hass, mock_gateway_info, assert len(mock_entry_setup.mock_calls) == 1 +async def test_import_connection_legacy_no_groups( + hass, mock_gateway_info, mock_entry_setup): + """Test a connection via legacy import and no groups allowed.""" + mock_gateway_info.side_effect = \ + lambda hass, host, identity, key: mock_coro({ + 'host': host, + 'identity': identity, + 'key': key, + 'gateway_id': 'mock-gateway' + }) + + result = await hass.config_entries.flow.async_init( + 'tradfri', context={'source': 'import'}, data={ + 'host': '123.123.123.123', + 'key': 'mock-key', + 'import_groups': False + }) + + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['result'].data == { + 'host': '123.123.123.123', + 'gateway_id': 'mock-gateway', + 'identity': 'homeassistant', + 'key': 'mock-key', + 'import_groups': False + } + + assert len(mock_gateway_info.mock_calls) == 1 + assert len(mock_entry_setup.mock_calls) == 1 + + async def test_discovery_duplicate_aborted(hass): """Test a duplicate discovery host is ignored.""" MockConfigEntry( diff --git a/tests/components/tradfri/test_init.py b/tests/components/tradfri/test_init.py index 4527e87f605..800c7b72ee6 100644 --- a/tests/components/tradfri/test_init.py +++ b/tests/components/tradfri/test_init.py @@ -58,7 +58,7 @@ async def test_config_json_host_not_imported(hass): assert len(mock_init.mock_calls) == 0 -async def test_config_json_host_imported(hass): +async def test_config_json_host_imported(hass, mock_gateway_info): """Test that we import a configured host.""" with patch('homeassistant.components.tradfri.load_json', return_value={'mock-host': {'key': 'some-info'}}): diff --git a/tests/components/upnp/__init__.py b/tests/components/upnp/__init__.py new file mode 100644 index 00000000000..4fcc4167e5b --- /dev/null +++ b/tests/components/upnp/__init__.py @@ -0,0 +1 @@ +"""Tests for the IGD component.""" diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py new file mode 100644 index 00000000000..3ff1316975f --- /dev/null +++ b/tests/components/upnp/test_config_flow.py @@ -0,0 +1,240 @@ +"""Tests for UPnP/IGD config flow.""" + +from homeassistant.components import upnp +from homeassistant.components.upnp import config_flow as upnp_config_flow + +from tests.common import MockConfigEntry + + +async def test_flow_none_discovered(hass): + """Test no device discovered flow.""" + flow = upnp_config_flow.UpnpFlowHandler() + flow.hass = hass + hass.data[upnp.DOMAIN] = { + 'discovered': {} + } + + result = await flow.async_step_user() + assert result['type'] == 'abort' + assert result['reason'] == 'no_devices_discovered' + + +async def test_flow_already_configured(hass): + """Test device already configured flow.""" + flow = upnp_config_flow.UpnpFlowHandler() + flow.hass = hass + + # discovered device + udn = 'uuid:device_1' + hass.data[upnp.DOMAIN] = { + 'discovered': { + udn: { + 'friendly_name': '192.168.1.1 (Test device)', + 'host': '192.168.1.1', + 'udn': udn, + }, + }, + } + + # configured entry + MockConfigEntry(domain=upnp.DOMAIN, data={ + 'udn': udn, + 'host': '192.168.1.1', + }).add_to_hass(hass) + + result = await flow.async_step_user({ + 'name': '192.168.1.1 (Test device)', + 'enable_sensors': True, + 'enable_port_mapping': False, + }) + assert result['type'] == 'abort' + assert result['reason'] == 'already_configured' + + +async def test_flow_no_sensors_no_port_mapping(hass): + """Test single device, no sensors, no port_mapping.""" + flow = upnp_config_flow.UpnpFlowHandler() + flow.hass = hass + + # discovered device + udn = 'uuid:device_1' + hass.data[upnp.DOMAIN] = { + 'discovered': { + udn: { + 'friendly_name': '192.168.1.1 (Test device)', + 'host': '192.168.1.1', + 'udn': udn, + }, + }, + } + + # configured entry + MockConfigEntry(domain=upnp.DOMAIN, data={ + 'udn': udn, + 'host': '192.168.1.1', + }).add_to_hass(hass) + + result = await flow.async_step_user({ + 'name': '192.168.1.1 (Test device)', + 'enable_sensors': False, + 'enable_port_mapping': False, + }) + assert result['type'] == 'abort' + assert result['reason'] == 'no_sensors_or_port_mapping' + + +async def test_flow_discovered_form(hass): + """Test single device discovered, show form flow.""" + flow = upnp_config_flow.UpnpFlowHandler() + flow.hass = hass + + # discovered device + udn = 'uuid:device_1' + hass.data[upnp.DOMAIN] = { + 'discovered': { + udn: { + 'friendly_name': '192.168.1.1 (Test device)', + 'host': '192.168.1.1', + 'udn': udn, + }, + }, + } + + result = await flow.async_step_user() + assert result['type'] == 'form' + assert result['step_id'] == 'user' + + +async def test_flow_two_discovered_form(hass): + """Test two devices discovered, show form flow with two devices.""" + flow = upnp_config_flow.UpnpFlowHandler() + flow.hass = hass + + # discovered device + udn_1 = 'uuid:device_1' + udn_2 = 'uuid:device_2' + hass.data[upnp.DOMAIN] = { + 'discovered': { + udn_1: { + 'friendly_name': '192.168.1.1 (Test device)', + 'host': '192.168.1.1', + 'udn': udn_1, + }, + udn_2: { + 'friendly_name': '192.168.2.1 (Test device)', + 'host': '192.168.2.1', + 'udn': udn_2, + }, + }, + } + + result = await flow.async_step_user() + assert result['type'] == 'form' + assert result['step_id'] == 'user' + assert result['data_schema']({ + 'name': '192.168.1.1 (Test device)', + 'enable_sensors': True, + 'enable_port_mapping': False, + }) + assert result['data_schema']({ + 'name': '192.168.2.1 (Test device)', + 'enable_sensors': True, + 'enable_port_mapping': False, + }) + + +async def test_config_entry_created(hass): + """Test config entry is created.""" + flow = upnp_config_flow.UpnpFlowHandler() + flow.hass = hass + + # discovered device + hass.data[upnp.DOMAIN] = { + 'discovered': { + 'uuid:device_1': { + 'friendly_name': '192.168.1.1 (Test device)', + 'name': 'Test device 1', + 'host': '192.168.1.1', + 'ssdp_description': 'http://192.168.1.1/desc.xml', + 'udn': 'uuid:device_1', + }, + }, + } + + result = await flow.async_step_user({ + 'name': '192.168.1.1 (Test device)', + 'enable_sensors': True, + 'enable_port_mapping': False, + }) + assert result['type'] == 'create_entry' + assert result['data'] == { + 'ssdp_description': 'http://192.168.1.1/desc.xml', + 'udn': 'uuid:device_1', + 'port_mapping': False, + 'sensors': True, + } + assert result['title'] == 'Test device 1' + + +async def test_flow_discovery_auto_config_sensors(hass): + """Test creation of device with auto_config.""" + flow = upnp_config_flow.UpnpFlowHandler() + flow.hass = hass + + # auto_config active + hass.data[upnp.DOMAIN] = { + 'auto_config': { + 'active': True, + 'enable_port_mapping': False, + 'enable_sensors': True, + }, + } + + # discovered device + result = await flow.async_step_discovery({ + 'name': 'Test device 1', + 'host': '192.168.1.1', + 'ssdp_description': 'http://192.168.1.1/desc.xml', + 'udn': 'uuid:device_1', + }) + + assert result['type'] == 'create_entry' + assert result['data'] == { + 'ssdp_description': 'http://192.168.1.1/desc.xml', + 'udn': 'uuid:device_1', + 'sensors': True, + 'port_mapping': False, + } + assert result['title'] == 'Test device 1' + + +async def test_flow_discovery_auto_config_sensors_port_mapping(hass): + """Test creation of device with auto_config, with port mapping.""" + flow = upnp_config_flow.UpnpFlowHandler() + flow.hass = hass + + # auto_config active, with port_mapping + hass.data[upnp.DOMAIN] = { + 'auto_config': { + 'active': True, + 'enable_port_mapping': True, + 'enable_sensors': True, + }, + } + + # discovered device + result = await flow.async_step_discovery({ + 'name': 'Test device 1', + 'host': '192.168.1.1', + 'ssdp_description': 'http://192.168.1.1/desc.xml', + 'udn': 'uuid:device_1', + }) + + assert result['type'] == 'create_entry' + assert result['data'] == { + 'udn': 'uuid:device_1', + 'ssdp_description': 'http://192.168.1.1/desc.xml', + 'sensors': True, + 'port_mapping': True, + } + assert result['title'] == 'Test device 1' diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py new file mode 100644 index 00000000000..ce4656032a6 --- /dev/null +++ b/tests/components/upnp/test_init.py @@ -0,0 +1,188 @@ +"""Test UPnP/IGD setup process.""" + +from ipaddress import ip_address +from unittest.mock import patch, MagicMock + +from homeassistant.setup import async_setup_component +from homeassistant.components import upnp +from homeassistant.components.upnp.device import Device +from homeassistant.const import EVENT_HOMEASSISTANT_STOP + +from tests.common import MockConfigEntry +from tests.common import mock_coro + + +class MockDevice(Device): + """Mock device for Device.""" + + def __init__(self, udn): + """Initializer.""" + super().__init__(None) + self._udn = udn + self.added_port_mappings = [] + self.removed_port_mappings = [] + + @classmethod + async def async_create_device(cls, hass, ssdp_description): + """Return self.""" + return cls() + + @property + def udn(self): + """Get the UDN.""" + return self._udn + + async def _async_add_port_mapping(self, + external_port, + local_ip, + internal_port): + """Add a port mapping.""" + entry = [external_port, local_ip, internal_port] + self.added_port_mappings.append(entry) + + async def _async_delete_port_mapping(self, external_port): + """Remove a port mapping.""" + entry = external_port + self.removed_port_mappings.append(entry) + + +async def test_async_setup_no_auto_config(hass): + """Test async_setup.""" + # setup component, enable auto_config + await async_setup_component(hass, 'upnp') + + assert hass.data[upnp.DOMAIN]['auto_config'] == { + 'active': False, + 'enable_sensors': False, + 'enable_port_mapping': False, + 'ports': {'hass': 'hass'}, + } + + +async def test_async_setup_auto_config(hass): + """Test async_setup.""" + # setup component, enable auto_config + await async_setup_component(hass, 'upnp', {'upnp': {}, 'discovery': {}}) + + assert hass.data[upnp.DOMAIN]['auto_config'] == { + 'active': True, + 'enable_sensors': True, + 'enable_port_mapping': False, + 'ports': {'hass': 'hass'}, + } + + +async def test_async_setup_auto_config_port_mapping(hass): + """Test async_setup.""" + # setup component, enable auto_config + await async_setup_component(hass, 'upnp', { + 'upnp': { + 'port_mapping': True, + 'ports': {'hass': 'hass'}, + }, + 'discovery': {}}) + + assert hass.data[upnp.DOMAIN]['auto_config'] == { + 'active': True, + 'enable_sensors': True, + 'enable_port_mapping': True, + 'ports': {'hass': 'hass'}, + } + + +async def test_async_setup_auto_config_no_sensors(hass): + """Test async_setup.""" + # setup component, enable auto_config + await async_setup_component(hass, 'upnp', { + 'upnp': {'sensors': False}, + 'discovery': {}}) + + assert hass.data[upnp.DOMAIN]['auto_config'] == { + 'active': True, + 'enable_sensors': False, + 'enable_port_mapping': False, + 'ports': {'hass': 'hass'}, + } + + +async def test_async_setup_entry_default(hass): + """Test async_setup_entry.""" + udn = 'uuid:device_1' + entry = MockConfigEntry(domain=upnp.DOMAIN, data={ + 'ssdp_description': 'http://192.168.1.1/desc.xml', + 'udn': udn, + 'sensors': True, + 'port_mapping': False, + }) + + # ensure hass.http is available + await async_setup_component(hass, 'upnp') + + # mock homeassistant.components.upnp.device.Device + mock_device = MagicMock() + mock_device.udn = udn + mock_device.async_add_port_mappings.return_value = mock_coro() + mock_device.async_delete_port_mappings.return_value = mock_coro() + with patch.object(Device, 'async_create_device') as mock_create_device: + mock_create_device.return_value = mock_coro( + return_value=mock_device) + with patch('homeassistant.components.upnp.device.get_local_ip', + return_value='192.168.1.10'): + assert await upnp.async_setup_entry(hass, entry) is True + + # ensure device is stored/used + assert hass.data[upnp.DOMAIN]['devices'][udn] == mock_device + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + # ensure cleaned up + assert udn not in hass.data[upnp.DOMAIN]['devices'] + + # ensure no port-mapping-methods called + assert len(mock_device.async_add_port_mappings.mock_calls) == 0 + assert len(mock_device.async_delete_port_mappings.mock_calls) == 0 + + +async def test_async_setup_entry_port_mapping(hass): + """Test async_setup_entry.""" + udn = 'uuid:device_1' + entry = MockConfigEntry(domain=upnp.DOMAIN, data={ + 'ssdp_description': 'http://192.168.1.1/desc.xml', + 'udn': udn, + 'sensors': False, + 'port_mapping': True, + }) + + # ensure hass.http is available + await async_setup_component(hass, 'upnp', { + 'upnp': { + 'port_mapping': True, + 'ports': {'hass': 'hass'}, + }, + 'discovery': {}, + }) + + mock_device = MockDevice(udn) + with patch.object(Device, 'async_create_device') as mock_create_device: + mock_create_device.return_value = mock_coro(return_value=mock_device) + with patch('homeassistant.components.upnp.device.get_local_ip', + return_value='192.168.1.10'): + assert await upnp.async_setup_entry(hass, entry) is True + + # ensure device is stored/used + assert hass.data[upnp.DOMAIN]['devices'][udn] == mock_device + + # ensure add-port-mapping-methods called + assert mock_device.added_port_mappings == [ + [8123, ip_address('192.168.1.10'), 8123] + ] + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + # ensure cleaned up + assert udn not in hass.data[upnp.DOMAIN]['devices'] + + # ensure delete-port-mapping-methods called + assert mock_device.removed_port_mappings == [8123] diff --git a/tests/components/vacuum/common.py b/tests/components/vacuum/common.py new file mode 100644 index 00000000000..436f23f5546 --- /dev/null +++ b/tests/components/vacuum/common.py @@ -0,0 +1,101 @@ +"""Collection of helper methods. + +All containing methods are legacy helpers that should not be used by new +components. Instead call the service directly. +""" +from homeassistant.components.vacuum import ( + ATTR_FAN_SPEED, ATTR_PARAMS, DOMAIN, SERVICE_CLEAN_SPOT, SERVICE_LOCATE, + SERVICE_PAUSE, SERVICE_SEND_COMMAND, SERVICE_SET_FAN_SPEED, SERVICE_START, + SERVICE_START_PAUSE, SERVICE_STOP, SERVICE_RETURN_TO_BASE) +from homeassistant.const import ( + ATTR_COMMAND, ATTR_ENTITY_ID, SERVICE_TOGGLE, + SERVICE_TURN_OFF, SERVICE_TURN_ON) +from homeassistant.loader import bind_hass + + +@bind_hass +def turn_on(hass, entity_id=None): + """Turn all or specified vacuum on.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_TURN_ON, data) + + +@bind_hass +def turn_off(hass, entity_id=None): + """Turn all or specified vacuum off.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) + + +@bind_hass +def toggle(hass, entity_id=None): + """Toggle all or specified vacuum.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_TOGGLE, data) + + +@bind_hass +def locate(hass, entity_id=None): + """Locate all or specified vacuum.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_LOCATE, data) + + +@bind_hass +def clean_spot(hass, entity_id=None): + """Tell all or specified vacuum to perform a spot clean-up.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_CLEAN_SPOT, data) + + +@bind_hass +def return_to_base(hass, entity_id=None): + """Tell all or specified vacuum to return to base.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_RETURN_TO_BASE, data) + + +@bind_hass +def start_pause(hass, entity_id=None): + """Tell all or specified vacuum to start or pause the current task.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_START_PAUSE, data) + + +@bind_hass +def start(hass, entity_id=None): + """Tell all or specified vacuum to start or resume the current task.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_START, data) + + +@bind_hass +def pause(hass, entity_id=None): + """Tell all or the specified vacuum to pause the current task.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_PAUSE, data) + + +@bind_hass +def stop(hass, entity_id=None): + """Stop all or specified vacuum.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_STOP, data) + + +@bind_hass +def set_fan_speed(hass, fan_speed, entity_id=None): + """Set fan speed for all or specified vacuum.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + data[ATTR_FAN_SPEED] = fan_speed + hass.services.call(DOMAIN, SERVICE_SET_FAN_SPEED, data) + + +@bind_hass +def send_command(hass, command, params=None, entity_id=None): + """Send command to all or specified vacuum.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + data[ATTR_COMMAND] = command + if params is not None: + data[ATTR_PARAMS] = params + hass.services.call(DOMAIN, SERVICE_SEND_COMMAND, data) diff --git a/tests/components/vacuum/test_demo.py b/tests/components/vacuum/test_demo.py index 1fc8f8cd5c1..f88908ecc41 100644 --- a/tests/components/vacuum/test_demo.py +++ b/tests/components/vacuum/test_demo.py @@ -15,7 +15,9 @@ from homeassistant.components.vacuum.demo import ( from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, CONF_PLATFORM, STATE_OFF, STATE_ON) from homeassistant.setup import setup_component + from tests.common import get_test_home_assistant, mock_service +from tests.components.vacuum import common ENTITY_VACUUM_BASIC = '{}.{}'.format(DOMAIN, DEMO_VACUUM_BASIC).lower() @@ -108,27 +110,27 @@ class TestVacuumDemo(unittest.TestCase): self.hass.block_till_done() self.assertFalse(vacuum.is_on(self.hass)) - vacuum.turn_on(self.hass, ENTITY_VACUUM_COMPLETE) + common.turn_on(self.hass, ENTITY_VACUUM_COMPLETE) self.hass.block_till_done() self.assertTrue(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE)) - vacuum.turn_off(self.hass, ENTITY_VACUUM_COMPLETE) + common.turn_off(self.hass, ENTITY_VACUUM_COMPLETE) self.hass.block_till_done() self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE)) - vacuum.toggle(self.hass, ENTITY_VACUUM_COMPLETE) + common.toggle(self.hass, ENTITY_VACUUM_COMPLETE) self.hass.block_till_done() self.assertTrue(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE)) - vacuum.start_pause(self.hass, ENTITY_VACUUM_COMPLETE) + common.start_pause(self.hass, ENTITY_VACUUM_COMPLETE) self.hass.block_till_done() self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE)) - vacuum.start_pause(self.hass, ENTITY_VACUUM_COMPLETE) + common.start_pause(self.hass, ENTITY_VACUUM_COMPLETE) self.hass.block_till_done() self.assertTrue(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE)) - vacuum.stop(self.hass, ENTITY_VACUUM_COMPLETE) + common.stop(self.hass, ENTITY_VACUUM_COMPLETE) self.hass.block_till_done() self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE)) @@ -136,39 +138,39 @@ class TestVacuumDemo(unittest.TestCase): self.assertLess(state.attributes.get(ATTR_BATTERY_LEVEL), 100) self.assertNotEqual("Charging", state.attributes.get(ATTR_STATUS)) - vacuum.locate(self.hass, ENTITY_VACUUM_COMPLETE) + common.locate(self.hass, ENTITY_VACUUM_COMPLETE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_COMPLETE) self.assertIn("I'm over here", state.attributes.get(ATTR_STATUS)) - vacuum.return_to_base(self.hass, ENTITY_VACUUM_COMPLETE) + common.return_to_base(self.hass, ENTITY_VACUUM_COMPLETE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_COMPLETE) self.assertIn("Returning home", state.attributes.get(ATTR_STATUS)) - vacuum.set_fan_speed(self.hass, FAN_SPEEDS[-1], + common.set_fan_speed(self.hass, FAN_SPEEDS[-1], entity_id=ENTITY_VACUUM_COMPLETE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_COMPLETE) self.assertEqual(FAN_SPEEDS[-1], state.attributes.get(ATTR_FAN_SPEED)) - vacuum.clean_spot(self.hass, entity_id=ENTITY_VACUUM_COMPLETE) + common.clean_spot(self.hass, entity_id=ENTITY_VACUUM_COMPLETE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_COMPLETE) self.assertIn("spot", state.attributes.get(ATTR_STATUS)) self.assertEqual(STATE_ON, state.state) - vacuum.start(self.hass, ENTITY_VACUUM_STATE) + common.start(self.hass, ENTITY_VACUUM_STATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_STATE) self.assertEqual(STATE_CLEANING, state.state) - vacuum.pause(self.hass, ENTITY_VACUUM_STATE) + common.pause(self.hass, ENTITY_VACUUM_STATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_STATE) self.assertEqual(STATE_PAUSED, state.state) - vacuum.stop(self.hass, ENTITY_VACUUM_STATE) + common.stop(self.hass, ENTITY_VACUUM_STATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_STATE) self.assertEqual(STATE_IDLE, state.state) @@ -177,18 +179,18 @@ class TestVacuumDemo(unittest.TestCase): self.assertLess(state.attributes.get(ATTR_BATTERY_LEVEL), 100) self.assertNotEqual(STATE_DOCKED, state.state) - vacuum.return_to_base(self.hass, ENTITY_VACUUM_STATE) + common.return_to_base(self.hass, ENTITY_VACUUM_STATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_STATE) self.assertEqual(STATE_RETURNING, state.state) - vacuum.set_fan_speed(self.hass, FAN_SPEEDS[-1], + common.set_fan_speed(self.hass, FAN_SPEEDS[-1], entity_id=ENTITY_VACUUM_STATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_STATE) self.assertEqual(FAN_SPEEDS[-1], state.attributes.get(ATTR_FAN_SPEED)) - vacuum.clean_spot(self.hass, entity_id=ENTITY_VACUUM_STATE) + common.clean_spot(self.hass, entity_id=ENTITY_VACUUM_STATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_STATE) self.assertEqual(STATE_CLEANING, state.state) @@ -199,11 +201,11 @@ class TestVacuumDemo(unittest.TestCase): self.hass.block_till_done() self.assertTrue(vacuum.is_on(self.hass, ENTITY_VACUUM_NONE)) - vacuum.turn_off(self.hass, ENTITY_VACUUM_NONE) + common.turn_off(self.hass, ENTITY_VACUUM_NONE) self.hass.block_till_done() self.assertTrue(vacuum.is_on(self.hass, ENTITY_VACUUM_NONE)) - vacuum.stop(self.hass, ENTITY_VACUUM_NONE) + common.stop(self.hass, ENTITY_VACUUM_NONE) self.hass.block_till_done() self.assertTrue(vacuum.is_on(self.hass, ENTITY_VACUUM_NONE)) @@ -211,37 +213,37 @@ class TestVacuumDemo(unittest.TestCase): self.hass.block_till_done() self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_NONE)) - vacuum.turn_on(self.hass, ENTITY_VACUUM_NONE) + common.turn_on(self.hass, ENTITY_VACUUM_NONE) self.hass.block_till_done() self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_NONE)) - vacuum.toggle(self.hass, ENTITY_VACUUM_NONE) + common.toggle(self.hass, ENTITY_VACUUM_NONE) self.hass.block_till_done() self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_NONE)) # Non supported methods: - vacuum.start_pause(self.hass, ENTITY_VACUUM_NONE) + common.start_pause(self.hass, ENTITY_VACUUM_NONE) self.hass.block_till_done() self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_NONE)) - vacuum.locate(self.hass, ENTITY_VACUUM_NONE) + common.locate(self.hass, ENTITY_VACUUM_NONE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_NONE) self.assertIsNone(state.attributes.get(ATTR_STATUS)) - vacuum.return_to_base(self.hass, ENTITY_VACUUM_NONE) + common.return_to_base(self.hass, ENTITY_VACUUM_NONE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_NONE) self.assertIsNone(state.attributes.get(ATTR_STATUS)) - vacuum.set_fan_speed(self.hass, FAN_SPEEDS[-1], + common.set_fan_speed(self.hass, FAN_SPEEDS[-1], entity_id=ENTITY_VACUUM_NONE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_NONE) self.assertNotEqual(FAN_SPEEDS[-1], state.attributes.get(ATTR_FAN_SPEED)) - vacuum.clean_spot(self.hass, entity_id=ENTITY_VACUUM_BASIC) + common.clean_spot(self.hass, entity_id=ENTITY_VACUUM_BASIC) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_BASIC) self.assertNotIn("spot", state.attributes.get(ATTR_STATUS)) @@ -252,7 +254,7 @@ class TestVacuumDemo(unittest.TestCase): self.hass.block_till_done() self.assertTrue(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE)) - vacuum.pause(self.hass, ENTITY_VACUUM_COMPLETE) + common.pause(self.hass, ENTITY_VACUUM_COMPLETE) self.hass.block_till_done() self.assertTrue(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE)) @@ -260,22 +262,22 @@ class TestVacuumDemo(unittest.TestCase): self.hass.block_till_done() self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE)) - vacuum.start(self.hass, ENTITY_VACUUM_COMPLETE) + common.start(self.hass, ENTITY_VACUUM_COMPLETE) self.hass.block_till_done() self.assertFalse(vacuum.is_on(self.hass, ENTITY_VACUUM_COMPLETE)) # StateVacuumDevice does not support on/off - vacuum.turn_on(self.hass, entity_id=ENTITY_VACUUM_STATE) + common.turn_on(self.hass, entity_id=ENTITY_VACUUM_STATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_STATE) self.assertNotEqual(STATE_CLEANING, state.state) - vacuum.turn_off(self.hass, entity_id=ENTITY_VACUUM_STATE) + common.turn_off(self.hass, entity_id=ENTITY_VACUUM_STATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_STATE) self.assertNotEqual(STATE_RETURNING, state.state) - vacuum.toggle(self.hass, entity_id=ENTITY_VACUUM_STATE) + common.toggle(self.hass, entity_id=ENTITY_VACUUM_STATE) self.hass.block_till_done() state = self.hass.states.get(ENTITY_VACUUM_STATE) self.assertNotEqual(STATE_CLEANING, state.state) @@ -287,7 +289,7 @@ class TestVacuumDemo(unittest.TestCase): self.hass, DOMAIN, SERVICE_SEND_COMMAND) params = {"rotate": 150, "speed": 20} - vacuum.send_command( + common.send_command( self.hass, 'test_command', entity_id=ENTITY_VACUUM_BASIC, params=params) @@ -305,7 +307,7 @@ class TestVacuumDemo(unittest.TestCase): set_fan_speed_calls = mock_service( self.hass, DOMAIN, SERVICE_SET_FAN_SPEED) - vacuum.set_fan_speed( + common.set_fan_speed( self.hass, FAN_SPEEDS[0], entity_id=ENTITY_VACUUM_COMPLETE) self.hass.block_till_done() @@ -326,7 +328,7 @@ class TestVacuumDemo(unittest.TestCase): old_state_complete = self.hass.states.get(ENTITY_VACUUM_COMPLETE) old_state_state = self.hass.states.get(ENTITY_VACUUM_STATE) - vacuum.set_fan_speed( + common.set_fan_speed( self.hass, FAN_SPEEDS[0], entity_id=group_vacuums) self.hass.block_till_done() @@ -356,7 +358,7 @@ class TestVacuumDemo(unittest.TestCase): old_state_basic = self.hass.states.get(ENTITY_VACUUM_BASIC) old_state_complete = self.hass.states.get(ENTITY_VACUUM_COMPLETE) - vacuum.send_command( + common.send_command( self.hass, 'test_command', params={"p1": 3}, entity_id=group_vacuums) diff --git a/tests/components/vacuum/test_mqtt.py b/tests/components/vacuum/test_mqtt.py index ba2288e3fc6..ddd4289c24d 100644 --- a/tests/components/vacuum/test_mqtt.py +++ b/tests/components/vacuum/test_mqtt.py @@ -9,8 +9,10 @@ from homeassistant.components.mqtt import CONF_COMMAND_TOPIC from homeassistant.const import ( CONF_PLATFORM, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, CONF_NAME) from homeassistant.setup import setup_component + from tests.common import ( fire_mqtt_message, get_test_home_assistant, mock_mqtt_component) +from tests.components.vacuum import common class TestVacuumMQTT(unittest.TestCase): @@ -69,55 +71,55 @@ class TestVacuumMQTT(unittest.TestCase): vacuum.DOMAIN: self.default_config, })) - vacuum.turn_on(self.hass, 'vacuum.mqtttest') + common.turn_on(self.hass, 'vacuum.mqtttest') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'vacuum/command', 'turn_on', 0, False) self.mock_publish.async_publish.reset_mock() - vacuum.turn_off(self.hass, 'vacuum.mqtttest') + common.turn_off(self.hass, 'vacuum.mqtttest') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'vacuum/command', 'turn_off', 0, False) self.mock_publish.async_publish.reset_mock() - vacuum.stop(self.hass, 'vacuum.mqtttest') + common.stop(self.hass, 'vacuum.mqtttest') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'vacuum/command', 'stop', 0, False) self.mock_publish.async_publish.reset_mock() - vacuum.clean_spot(self.hass, 'vacuum.mqtttest') + common.clean_spot(self.hass, 'vacuum.mqtttest') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'vacuum/command', 'clean_spot', 0, False) self.mock_publish.async_publish.reset_mock() - vacuum.locate(self.hass, 'vacuum.mqtttest') + common.locate(self.hass, 'vacuum.mqtttest') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'vacuum/command', 'locate', 0, False) self.mock_publish.async_publish.reset_mock() - vacuum.start_pause(self.hass, 'vacuum.mqtttest') + common.start_pause(self.hass, 'vacuum.mqtttest') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'vacuum/command', 'start_pause', 0, False) self.mock_publish.async_publish.reset_mock() - vacuum.return_to_base(self.hass, 'vacuum.mqtttest') + common.return_to_base(self.hass, 'vacuum.mqtttest') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'vacuum/command', 'return_to_base', 0, False) self.mock_publish.async_publish.reset_mock() - vacuum.set_fan_speed(self.hass, 'high', 'vacuum.mqtttest') + common.set_fan_speed(self.hass, 'high', 'vacuum.mqtttest') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'vacuum/set_fan_speed', 'high', 0, False) self.mock_publish.async_publish.reset_mock() - vacuum.send_command(self.hass, '44 FE 93', entity_id='vacuum.mqtttest') + common.send_command(self.hass, '44 FE 93', entity_id='vacuum.mqtttest') self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( 'vacuum/send_command', '44 FE 93', 0, False) diff --git a/tests/components/websocket_api/__init__.py b/tests/components/websocket_api/__init__.py new file mode 100644 index 00000000000..c218c6165d4 --- /dev/null +++ b/tests/components/websocket_api/__init__.py @@ -0,0 +1,2 @@ +"""Tests for the websocket API.""" +API_PASSWORD = 'test1234' diff --git a/tests/components/websocket_api/conftest.py b/tests/components/websocket_api/conftest.py new file mode 100644 index 00000000000..b7825600cb1 --- /dev/null +++ b/tests/components/websocket_api/conftest.py @@ -0,0 +1,36 @@ +"""Fixtures for websocket tests.""" +import pytest + +from homeassistant.setup import async_setup_component +from homeassistant.components.websocket_api.http import URL +from homeassistant.components.websocket_api.auth import TYPE_AUTH_REQUIRED + +from . import API_PASSWORD + + +@pytest.fixture +def websocket_client(hass, hass_ws_client): + """Create a websocket client.""" + return hass.loop.run_until_complete(hass_ws_client(hass)) + + +@pytest.fixture +def no_auth_websocket_client(hass, loop, aiohttp_client): + """Websocket connection that requires authentication.""" + assert loop.run_until_complete( + async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + })) + + client = loop.run_until_complete(aiohttp_client(hass.http.app)) + ws = loop.run_until_complete(client.ws_connect(URL)) + + auth_ok = loop.run_until_complete(ws.receive_json()) + assert auth_ok['type'] == TYPE_AUTH_REQUIRED + + yield ws + + if not ws.closed: + loop.run_until_complete(ws.close()) diff --git a/tests/components/websocket_api/test_auth.py b/tests/components/websocket_api/test_auth.py new file mode 100644 index 00000000000..ed54b509aaa --- /dev/null +++ b/tests/components/websocket_api/test_auth.py @@ -0,0 +1,190 @@ +"""Test auth of websocket API.""" +from unittest.mock import patch + +from homeassistant.components.websocket_api.const import URL +from homeassistant.components.websocket_api.auth import ( + TYPE_AUTH, TYPE_AUTH_INVALID, TYPE_AUTH_OK, TYPE_AUTH_REQUIRED) + +from homeassistant.components.websocket_api import commands +from homeassistant.setup import async_setup_component + +from tests.common import mock_coro + +from . import API_PASSWORD + + +async def test_auth_via_msg(no_auth_websocket_client): + """Test authenticating.""" + await no_auth_websocket_client.send_json({ + 'type': TYPE_AUTH, + 'api_password': API_PASSWORD + }) + + msg = await no_auth_websocket_client.receive_json() + + assert msg['type'] == TYPE_AUTH_OK + + +async def test_auth_via_msg_incorrect_pass(no_auth_websocket_client): + """Test authenticating.""" + with patch('homeassistant.components.websocket_api.auth.' + 'process_wrong_login', return_value=mock_coro()) \ + as mock_process_wrong_login: + await no_auth_websocket_client.send_json({ + 'type': TYPE_AUTH, + 'api_password': API_PASSWORD + 'wrong' + }) + + msg = await no_auth_websocket_client.receive_json() + + assert mock_process_wrong_login.called + assert msg['type'] == TYPE_AUTH_INVALID + assert msg['message'] == 'Invalid access token or password' + + +async def test_pre_auth_only_auth_allowed(no_auth_websocket_client): + """Verify that before authentication, only auth messages are allowed.""" + await no_auth_websocket_client.send_json({ + 'type': commands.TYPE_CALL_SERVICE, + 'domain': 'domain_test', + 'service': 'test_service', + 'service_data': { + 'hello': 'world' + } + }) + + msg = await no_auth_websocket_client.receive_json() + + assert msg['type'] == TYPE_AUTH_INVALID + assert msg['message'].startswith('Auth message incorrectly formatted') + + +async def test_auth_active_with_token(hass, aiohttp_client, hass_access_token): + """Test authenticating with a token.""" + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) + + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(URL) as ws: + with patch('homeassistant.auth.AuthManager.active') as auth_active: + auth_active.return_value = True + auth_msg = await ws.receive_json() + assert auth_msg['type'] == TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': TYPE_AUTH, + 'access_token': hass_access_token + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == TYPE_AUTH_OK + + +async def test_auth_active_user_inactive(hass, aiohttp_client, + hass_access_token): + """Test authenticating with a token.""" + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + refresh_token.user.is_active = False + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) + + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(URL) as ws: + with patch('homeassistant.auth.AuthManager.active') as auth_active: + auth_active.return_value = True + auth_msg = await ws.receive_json() + assert auth_msg['type'] == TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': TYPE_AUTH, + 'access_token': hass_access_token + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == TYPE_AUTH_INVALID + + +async def test_auth_active_with_password_not_allow(hass, aiohttp_client): + """Test authenticating with a token.""" + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) + + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(URL) as ws: + with patch('homeassistant.auth.AuthManager.active', + return_value=True): + auth_msg = await ws.receive_json() + assert auth_msg['type'] == TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': TYPE_AUTH, + 'api_password': API_PASSWORD + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == TYPE_AUTH_INVALID + + +async def test_auth_legacy_support_with_password(hass, aiohttp_client): + """Test authenticating with a token.""" + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) + + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(URL) as ws: + with patch('homeassistant.auth.AuthManager.active', + return_value=True),\ + patch('homeassistant.auth.AuthManager.support_legacy', + return_value=True): + auth_msg = await ws.receive_json() + assert auth_msg['type'] == TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': TYPE_AUTH, + 'api_password': API_PASSWORD + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == TYPE_AUTH_OK + + +async def test_auth_with_invalid_token(hass, aiohttp_client): + """Test authenticating with a token.""" + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) + + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(URL) as ws: + with patch('homeassistant.auth.AuthManager.active') as auth_active: + auth_active.return_value = True + auth_msg = await ws.receive_json() + assert auth_msg['type'] == TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': TYPE_AUTH, + 'access_token': 'incorrect' + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == TYPE_AUTH_INVALID diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py new file mode 100644 index 00000000000..84c29533859 --- /dev/null +++ b/tests/components/websocket_api/test_commands.py @@ -0,0 +1,263 @@ +"""Tests for WebSocket API commands.""" +from unittest.mock import patch + +from async_timeout import timeout + +from homeassistant.core import callback +from homeassistant.components.websocket_api.const import URL +from homeassistant.components.websocket_api.auth import ( + TYPE_AUTH, TYPE_AUTH_OK, TYPE_AUTH_REQUIRED +) +from homeassistant.components.websocket_api import const, commands +from homeassistant.setup import async_setup_component + +from tests.common import async_mock_service + +from . import API_PASSWORD + + +async def test_call_service(hass, websocket_client): + """Test call service command.""" + calls = [] + + @callback + def service_call(call): + calls.append(call) + + hass.services.async_register('domain_test', 'test_service', service_call) + + await websocket_client.send_json({ + 'id': 5, + 'type': commands.TYPE_CALL_SERVICE, + 'domain': 'domain_test', + 'service': 'test_service', + 'service_data': { + 'hello': 'world' + } + }) + + msg = await websocket_client.receive_json() + assert msg['id'] == 5 + assert msg['type'] == const.TYPE_RESULT + assert msg['success'] + + assert len(calls) == 1 + call = calls[0] + + assert call.domain == 'domain_test' + assert call.service == 'test_service' + assert call.data == {'hello': 'world'} + + +async def test_subscribe_unsubscribe_events(hass, websocket_client): + """Test subscribe/unsubscribe events command.""" + init_count = sum(hass.bus.async_listeners().values()) + + await websocket_client.send_json({ + 'id': 5, + 'type': commands.TYPE_SUBSCRIBE_EVENTS, + 'event_type': 'test_event' + }) + + msg = await websocket_client.receive_json() + assert msg['id'] == 5 + assert msg['type'] == const.TYPE_RESULT + assert msg['success'] + + # Verify we have a new listener + assert sum(hass.bus.async_listeners().values()) == init_count + 1 + + hass.bus.async_fire('ignore_event') + hass.bus.async_fire('test_event', {'hello': 'world'}) + hass.bus.async_fire('ignore_event') + + with timeout(3, loop=hass.loop): + msg = await websocket_client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == commands.TYPE_EVENT + event = msg['event'] + + assert event['event_type'] == 'test_event' + assert event['data'] == {'hello': 'world'} + assert event['origin'] == 'LOCAL' + + await websocket_client.send_json({ + 'id': 6, + 'type': commands.TYPE_UNSUBSCRIBE_EVENTS, + 'subscription': 5 + }) + + msg = await websocket_client.receive_json() + assert msg['id'] == 6 + assert msg['type'] == const.TYPE_RESULT + assert msg['success'] + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count + + +async def test_get_states(hass, websocket_client): + """Test get_states command.""" + hass.states.async_set('greeting.hello', 'world') + hass.states.async_set('greeting.bye', 'universe') + + await websocket_client.send_json({ + 'id': 5, + 'type': commands.TYPE_GET_STATES, + }) + + msg = await websocket_client.receive_json() + assert msg['id'] == 5 + assert msg['type'] == const.TYPE_RESULT + assert msg['success'] + + states = [] + for state in hass.states.async_all(): + state = state.as_dict() + state['last_changed'] = state['last_changed'].isoformat() + state['last_updated'] = state['last_updated'].isoformat() + states.append(state) + + assert msg['result'] == states + + +async def test_get_services(hass, websocket_client): + """Test get_services command.""" + await websocket_client.send_json({ + 'id': 5, + 'type': commands.TYPE_GET_SERVICES, + }) + + msg = await websocket_client.receive_json() + assert msg['id'] == 5 + assert msg['type'] == const.TYPE_RESULT + assert msg['success'] + assert msg['result'] == hass.services.async_services() + + +async def test_get_config(hass, websocket_client): + """Test get_config command.""" + await websocket_client.send_json({ + 'id': 5, + 'type': commands.TYPE_GET_CONFIG, + }) + + msg = await websocket_client.receive_json() + assert msg['id'] == 5 + assert msg['type'] == const.TYPE_RESULT + assert msg['success'] + + if 'components' in msg['result']: + msg['result']['components'] = set(msg['result']['components']) + if 'whitelist_external_dirs' in msg['result']: + msg['result']['whitelist_external_dirs'] = \ + set(msg['result']['whitelist_external_dirs']) + + assert msg['result'] == hass.config.as_dict() + + +async def test_ping(websocket_client): + """Test get_panels command.""" + await websocket_client.send_json({ + 'id': 5, + 'type': commands.TYPE_PING, + }) + + msg = await websocket_client.receive_json() + assert msg['id'] == 5 + assert msg['type'] == commands.TYPE_PONG + + +async def test_call_service_context_with_user(hass, aiohttp_client, + hass_access_token): + """Test that the user is set in the service call context.""" + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) + + calls = async_mock_service(hass, 'domain_test', 'test_service') + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(URL) as ws: + with patch('homeassistant.auth.AuthManager.active') as auth_active: + auth_active.return_value = True + auth_msg = await ws.receive_json() + assert auth_msg['type'] == TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': TYPE_AUTH, + 'access_token': hass_access_token + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == TYPE_AUTH_OK + + await ws.send_json({ + 'id': 5, + 'type': commands.TYPE_CALL_SERVICE, + 'domain': 'domain_test', + 'service': 'test_service', + 'service_data': { + 'hello': 'world' + } + }) + + msg = await ws.receive_json() + assert msg['success'] + + refresh_token = await hass.auth.async_validate_access_token( + hass_access_token) + + assert len(calls) == 1 + call = calls[0] + assert call.domain == 'domain_test' + assert call.service == 'test_service' + assert call.data == {'hello': 'world'} + assert call.context.user_id == refresh_token.user.id + + +async def test_call_service_context_no_user(hass, aiohttp_client): + """Test that connection without user sets context.""" + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) + + calls = async_mock_service(hass, 'domain_test', 'test_service') + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(URL) as ws: + auth_msg = await ws.receive_json() + assert auth_msg['type'] == TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': TYPE_AUTH, + 'api_password': API_PASSWORD + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == TYPE_AUTH_OK + + await ws.send_json({ + 'id': 5, + 'type': commands.TYPE_CALL_SERVICE, + 'domain': 'domain_test', + 'service': 'test_service', + 'service_data': { + 'hello': 'world' + } + }) + + msg = await ws.receive_json() + assert msg['success'] + + assert len(calls) == 1 + call = calls[0] + assert call.domain == 'domain_test' + assert call.service == 'test_service' + assert call.data == {'hello': 'world'} + assert call.context.user_id is None diff --git a/tests/components/websocket_api/test_init.py b/tests/components/websocket_api/test_init.py new file mode 100644 index 00000000000..a7e54e8146a --- /dev/null +++ b/tests/components/websocket_api/test_init.py @@ -0,0 +1,92 @@ +"""Tests for the Home Assistant Websocket API.""" +import asyncio +from unittest.mock import patch, Mock + +from aiohttp import WSMsgType +import pytest + +from homeassistant.components.websocket_api import const, commands, messages + + +@pytest.fixture +def mock_low_queue(): + """Mock a low queue.""" + with patch('homeassistant.components.websocket_api.http.MAX_PENDING_MSG', + 5): + yield + + +@asyncio.coroutine +def test_invalid_message_format(websocket_client): + """Test sending invalid JSON.""" + yield from websocket_client.send_json({'type': 5}) + + msg = yield from websocket_client.receive_json() + + assert msg['type'] == const.TYPE_RESULT + error = msg['error'] + assert error['code'] == const.ERR_INVALID_FORMAT + assert error['message'].startswith('Message incorrectly formatted') + + +@asyncio.coroutine +def test_invalid_json(websocket_client): + """Test sending invalid JSON.""" + yield from websocket_client.send_str('this is not JSON') + + msg = yield from websocket_client.receive() + + assert msg.type == WSMsgType.close + + +@asyncio.coroutine +def test_quiting_hass(hass, websocket_client): + """Test sending invalid JSON.""" + with patch.object(hass.loop, 'stop'): + yield from hass.async_stop() + + msg = yield from websocket_client.receive() + + assert msg.type == WSMsgType.CLOSE + + +@asyncio.coroutine +def test_pending_msg_overflow(hass, mock_low_queue, websocket_client): + """Test get_panels command.""" + for idx in range(10): + yield from websocket_client.send_json({ + 'id': idx + 1, + 'type': commands.TYPE_PING, + }) + msg = yield from websocket_client.receive() + assert msg.type == WSMsgType.close + + +@asyncio.coroutine +def test_unknown_command(websocket_client): + """Test get_panels command.""" + yield from websocket_client.send_json({ + 'id': 5, + 'type': 'unknown_command', + }) + + msg = yield from websocket_client.receive_json() + assert not msg['success'] + assert msg['error']['code'] == const.ERR_UNKNOWN_COMMAND + + +async def test_handler_failing(hass, websocket_client): + """Test a command that raises.""" + hass.components.websocket_api.async_register_command( + 'bla', Mock(side_effect=TypeError), + messages.BASE_COMMAND_MESSAGE_SCHEMA.extend({'type': 'bla'})) + await websocket_client.send_json({ + 'id': 5, + 'type': 'bla', + }) + + msg = await websocket_client.receive_json() + assert msg['id'] == 5 + assert msg['type'] == const.TYPE_RESULT + assert not msg['success'] + assert msg['error']['code'] == const.ERR_UNKNOWN_ERROR diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 1857d14ad84..a2290d8aabf 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -458,6 +458,33 @@ def test_network_complete(hass, mock_openzwave): assert len(events) == 1 +@asyncio.coroutine +def test_network_complete_some_dead(hass, mock_openzwave): + """Test Node network complete some dead event.""" + mock_receivers = [] + + def mock_connect(receiver, signal, *args, **kwargs): + if signal == MockNetwork.SIGNAL_ALL_NODES_QUERIED_SOME_DEAD: + mock_receivers.append(receiver) + + with patch('pydispatch.dispatcher.connect', new=mock_connect): + yield from async_setup_component(hass, 'zwave', {'zwave': {}}) + + assert len(mock_receivers) == 1 + + events = [] + + def listener(event): + events.append(event) + + hass.bus.async_listen(const.EVENT_NETWORK_COMPLETE_SOME_DEAD, listener) + + hass.async_add_job(mock_receivers[0]) + yield from hass.async_block_till_done() + + assert len(events) == 1 + + class TestZWaveDeviceEntityValues(unittest.TestCase): """Tests for the ZWaveDeviceEntityValues helper.""" @@ -749,9 +776,11 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): self.device_config = {'mock_component.registry_id': { zwave.CONF_IGNORED: True }} - self.registry.async_get_or_create( - 'mock_component', zwave.DOMAIN, '567-1000', - suggested_object_id='registry_id') + with patch.object(self.registry, 'async_schedule_save'): + self.registry.async_get_or_create( + 'mock_component', zwave.DOMAIN, '567-1000', + suggested_object_id='registry_id') + zwave.ZWaveDeviceEntityValues( hass=self.hass, schema=self.mock_schema, @@ -1359,6 +1388,53 @@ class TestZWaveServices(unittest.TestCase): assert node.refresh_info.called assert len(node.refresh_info.mock_calls) == 1 + def test_set_node_value(self): + """Test zwave set_node_value service.""" + value = MockValue( + index=12, + command_class=const.COMMAND_CLASS_INDICATOR, + data=4 + ) + node = MockNode(node_id=14, + command_classes=[const.COMMAND_CLASS_INDICATOR]) + node.values = {12: value} + node.get_values.return_value = node.values + self.zwave_network.nodes = {14: node} + + self.hass.services.call('zwave', 'set_node_value', { + const.ATTR_NODE_ID: 14, + const.ATTR_VALUE_ID: 12, + const.ATTR_CONFIG_VALUE: 2, + }) + self.hass.block_till_done() + + assert self.zwave_network.nodes[14].values[12].data == 2 + + def test_refresh_node_value(self): + """Test zwave refresh_node_value service.""" + node = MockNode(node_id=14, + command_classes=[const.COMMAND_CLASS_INDICATOR], + network=self.zwave_network) + value = MockValue( + node=node, + index=12, + command_class=const.COMMAND_CLASS_INDICATOR, + data=2 + ) + value.refresh = MagicMock() + + node.values = {12: value} + node.get_values.return_value = node.values + self.zwave_network.nodes = {14: node} + + self.hass.services.call('zwave', 'refresh_node_value', { + const.ATTR_NODE_ID: 14, + const.ATTR_VALUE_ID: 12 + }) + self.hass.block_till_done() + + assert value.refresh.called + def test_heal_node(self): """Test zwave heal_node service.""" node = MockNode(node_id=19) diff --git a/tests/fixtures/geo_rss_events.xml b/tests/fixtures/geo_rss_events.xml deleted file mode 100644 index 212994756d2..00000000000 --- a/tests/fixtures/geo_rss_events.xml +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - Title 1 - Description 1 - Category 1 - Sun, 30 Jul 2017 09:00:00 UTC - GUID 1 - -32.916667 151.75 - - - - Title 2 - Description 2 - Category 2 - Sun, 30 Jul 2017 09:05:00 GMT - GUID 2 - 148.601111 - -32.256944 - - - - Title 3 - Description 3 - Category 3 - Sun, 30 Jul 2017 09:05:00 GMT - GUID 3 - - -33.283333 149.1 - -33.2999997 149.1 - -33.2999997 149.1166663888889 - -33.283333 149.1166663888889 - -33.283333 149.1 - - - - - Title 4 - Description 4 - Category 4 - Sun, 30 Jul 2017 09:15:00 GMT - GUID 4 - 52.518611 13.408333 - - - - Title 5 - Description 5 - Category 5 - Sun, 30 Jul 2017 09:20:00 GMT - GUID 5 - - - - - Title 6 - Description 6 - Category 6 - 2017-07-30T09:25:00.000Z - Link 6 - -33.75801 150.70544 - - - - Title 1 - Description 1 - Category 1 - Sun, 30 Jul 2017 09:00:00 UTC - GUID 1 - 45.256 -110.45 46.46 -109.48 43.84 -109.86 - - - \ No newline at end of file diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index b251846c491..a87ad3d483a 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -17,7 +17,10 @@ async def test_get_or_create_returns_same_entry(registry): config_entry_id='1234', connections={('ethernet', '12:34:56:78:90:AB:CD:EF')}, identifiers={('bridgeid', '0123')}, - manufacturer='manufacturer', model='model') + sw_version='sw-version', + name='name', + manufacturer='manufacturer', + model='model') entry2 = registry.async_get_or_create( config_entry_id='1234', connections={('ethernet', '11:22:33:44:55:66:77:88')}, @@ -25,15 +28,19 @@ async def test_get_or_create_returns_same_entry(registry): manufacturer='manufacturer', model='model') entry3 = registry.async_get_or_create( config_entry_id='1234', - connections={('ethernet', '12:34:56:78:90:AB:CD:EF')}, - identifiers={('bridgeid', '1234')}, - manufacturer='manufacturer', model='model') + connections={('ethernet', '12:34:56:78:90:AB:CD:EF')} + ) assert len(registry.devices) == 1 assert entry.id == entry2.id assert entry.id == entry3.id assert entry.identifiers == {('bridgeid', '0123')} + assert entry3.manufacturer == 'manufacturer' + assert entry3.model == 'model' + assert entry3.name == 'name' + assert entry3.sw_version == 'sw-version' + async def test_requirement_for_identifier_or_connection(registry): """Make sure we do require some descriptor of device.""" diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 631d446d186..e985771e486 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -719,6 +719,8 @@ async def test_device_info_called(hass): assert await entity_platform.async_setup_entry(config_entry) await hass.async_block_till_done() + assert len(hass.states.async_entity_ids()) == 2 + device = registry.async_get_device({('hue', '1234')}, set()) assert device is not None assert device.identifiers == {('hue', '1234')} @@ -728,3 +730,45 @@ async def test_device_info_called(hass): assert device.name == 'test-name' assert device.sw_version == 'test-sw' assert device.hub_device_id == hub.id + + +async def test_device_info_not_overrides(hass): + """Test device info is forwarded correctly.""" + registry = await hass.helpers.device_registry.async_get_registry() + device = registry.async_get_or_create( + config_entry_id='bla', + connections={('mac', 'abcd')}, + manufacturer='test-manufacturer', + model='test-model' + ) + + assert device.manufacturer == 'test-manufacturer' + assert device.model == 'test-model' + + async def async_setup_entry(hass, config_entry, async_add_entities): + """Mock setup entry method.""" + async_add_entities([ + MockEntity(unique_id='qwer', device_info={ + 'connections': {('mac', 'abcd')}, + }), + ]) + return True + + platform = MockPlatform( + async_setup_entry=async_setup_entry + ) + config_entry = MockConfigEntry(entry_id='super-mock-id') + entity_platform = MockEntityPlatform( + hass, + platform_name=config_entry.domain, + platform=platform + ) + + assert await entity_platform.async_setup_entry(config_entry) + await hass.async_block_till_done() + + device2 = registry.async_get_device(set(), {('mac', 'abcd')}) + assert device2 is not None + assert device.id == device2.id + assert device2.manufacturer == 'test-manufacturer' + assert device2.model == 'test-model' diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index deefcec773a..5b57ca75d51 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -13,6 +13,7 @@ import homeassistant.core as ha from homeassistant.const import MATCH_ALL from homeassistant.helpers.event import ( async_call_later, + call_later, track_point_in_utc_time, track_point_in_time, track_utc_time_change, @@ -645,6 +646,22 @@ class TestEventHelpers(unittest.TestCase): self.hass.block_till_done() self.assertEqual(0, len(specific_runs)) + def test_call_later(self): + """Test calling an action later.""" + def action(): pass + now = datetime(2017, 12, 19, 15, 40, 0, tzinfo=dt_util.UTC) + + with patch('homeassistant.helpers.event' + '.async_track_point_in_utc_time') as mock, \ + patch('homeassistant.util.dt.utcnow', return_value=now): + call_later(self.hass, 3, action) + + assert len(mock.mock_calls) == 1 + p_hass, p_action, p_point = mock.mock_calls[0][1] + assert p_hass is self.hass + assert p_action is action + assert p_point == now + timedelta(seconds=3) + @asyncio.coroutine def test_async_call_later(hass): @@ -659,7 +676,7 @@ def test_async_call_later(hass): assert len(mock.mock_calls) == 1 p_hass, p_action, p_point = mock.mock_calls[0][1] - assert hass is hass + assert p_hass is hass assert p_action is action assert p_point == now + timedelta(seconds=3) assert remove is mock() diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 6cb75899d35..38b8a7cd380 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -15,6 +15,7 @@ from tests.common import async_fire_time_changed, mock_coro MOCK_VERSION = 1 MOCK_KEY = 'storage-test' MOCK_DATA = {'hello': 'world'} +MOCK_DATA2 = {'goodbye': 'cruel world'} @pytest.fixture diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 6f426c290c5..dc8106e0ed3 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -562,6 +562,36 @@ class TestHelpersTemplate(unittest.TestCase): """, self.hass) self.assertEqual('LHR', tpl.render()) + def test_bitwise_and(self): + """Test bitwise_and method.""" + tpl = template.Template(""" +{{ 8 | bitwise_and(8) }} + """, self.hass) + self.assertEqual(str(8 & 8), tpl.render()) + tpl = template.Template(""" +{{ 10 | bitwise_and(2) }} + """, self.hass) + self.assertEqual(str(10 & 2), tpl.render()) + tpl = template.Template(""" +{{ 8 | bitwise_and(2) }} + """, self.hass) + self.assertEqual(str(8 & 2), tpl.render()) + + def test_bitwise_or(self): + """Test bitwise_or method.""" + tpl = template.Template(""" +{{ 8 | bitwise_or(8) }} + """, self.hass) + self.assertEqual(str(8 | 8), tpl.render()) + tpl = template.Template(""" +{{ 10 | bitwise_or(2) }} + """, self.hass) + self.assertEqual(str(10 | 2), tpl.render()) + tpl = template.Template(""" +{{ 8 | bitwise_or(2) }} + """, self.hass) + self.assertEqual(str(8 | 2), tpl.render()) + def test_distance_function_with_1_state(self): """Test distance function with 1 state.""" self.hass.states.set('test.object', 'happy', { diff --git a/tests/mock/zwave.py b/tests/mock/zwave.py index 59d97ddb621..36735b1693b 100644 --- a/tests/mock/zwave.py +++ b/tests/mock/zwave.py @@ -88,7 +88,8 @@ class MockNetwork(MagicMock): SIGNAL_NODE_QUERIES_COMPLETE = 'mock_NodeQueriesComplete' SIGNAL_AWAKE_NODES_QUERIED = 'mock_AwakeNodesQueried' SIGNAL_ALL_NODES_QUERIED = 'mock_AllNodesQueried' - SIGNAL_ALL_NODES_QUERIED_SOME_DEAD = 'mock_AllNodesQueriedSomeDead' + SIGNAL_ALL_NODES_QUERIED_SOME_DEAD = \ + 'mock_AllNodesQueriedSomeDead' SIGNAL_MSG_COMPLETE = 'mock_MsgComplete' SIGNAL_NOTIFICATION = 'mock_Notification' SIGNAL_CONTROLLER_COMMAND = 'mock_ControllerCommand' diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 57d63eb8271..340118502b1 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -6,6 +6,7 @@ from unittest.mock import MagicMock, patch import pytest from homeassistant import config_entries, loader, data_entry_flow +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.setup import async_setup_component from homeassistant.util import dt @@ -49,7 +50,11 @@ def test_remove_entry(hass, manager): MockModule('comp', async_unload_entry=mock_unload_entry)) MockConfigEntry(domain='test', entry_id='test1').add_to_manager(manager) - MockConfigEntry(domain='test', entry_id='test2').add_to_manager(manager) + MockConfigEntry( + domain='test', + entry_id='test2', + state=config_entries.ENTRY_STATE_LOADED + ).add_to_manager(manager) MockConfigEntry(domain='test', entry_id='test3').add_to_manager(manager) assert [item.entry_id for item in manager.async_entries()] == \ @@ -79,7 +84,11 @@ def test_remove_entry_raises(hass, manager): MockModule('comp', async_unload_entry=mock_unload_entry)) MockConfigEntry(domain='test', entry_id='test1').add_to_manager(manager) - MockConfigEntry(domain='test', entry_id='test2').add_to_manager(manager) + MockConfigEntry( + domain='test', + entry_id='test2', + state=config_entries.ENTRY_STATE_LOADED + ).add_to_manager(manager) MockConfigEntry(domain='test', entry_id='test3').add_to_manager(manager) assert [item.entry_id for item in manager.async_entries()] == \ @@ -94,6 +103,33 @@ def test_remove_entry_raises(hass, manager): ['test1', 'test3'] +@asyncio.coroutine +def test_remove_entry_if_not_loaded(hass, manager): + """Test that we can remove an entry.""" + mock_unload_entry = MagicMock(return_value=mock_coro(True)) + + loader.set_component( + hass, 'test', + MockModule('comp', async_unload_entry=mock_unload_entry)) + + MockConfigEntry(domain='test', entry_id='test1').add_to_manager(manager) + MockConfigEntry(domain='test', entry_id='test2').add_to_manager(manager) + MockConfigEntry(domain='test', entry_id='test3').add_to_manager(manager) + + assert [item.entry_id for item in manager.async_entries()] == \ + ['test1', 'test2', 'test3'] + + result = yield from manager.async_remove('test2') + + assert result == { + 'require_restart': False + } + assert [item.entry_id for item in manager.async_entries()] == \ + ['test1', 'test3'] + + assert len(mock_unload_entry.mock_calls) == 1 + + @asyncio.coroutine def test_add_entry_calls_setup_entry(hass, manager): """Test we call setup_config_entry.""" @@ -315,3 +351,66 @@ async def test_loading_default_config(hass): await manager.async_load() assert len(manager.async_entries()) == 0 + + +async def test_updating_entry_data(manager): + """Test that we can update an entry data.""" + entry = MockConfigEntry( + domain='test', + data={'first': True}, + ) + entry.add_to_manager(manager) + + manager.async_update_entry(entry, data={ + 'second': True + }) + + assert entry.data == { + 'second': True + } + + +async def test_setup_raise_not_ready(hass, caplog): + """Test a setup raising not ready.""" + entry = MockConfigEntry(domain='test') + + mock_setup_entry = MagicMock(side_effect=ConfigEntryNotReady) + loader.set_component( + hass, 'test', MockModule('test', async_setup_entry=mock_setup_entry)) + + with patch('homeassistant.helpers.event.async_call_later') as mock_call: + await entry.async_setup(hass) + + assert len(mock_call.mock_calls) == 1 + assert 'Config entry for test not ready yet' in caplog.text + p_hass, p_wait_time, p_setup = mock_call.mock_calls[0][1] + + assert p_hass is hass + assert p_wait_time == 5 + assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + + mock_setup_entry.side_effect = None + mock_setup_entry.return_value = mock_coro(True) + + await p_setup(None) + assert entry.state == config_entries.ENTRY_STATE_LOADED + + +async def test_setup_retrying_during_unload(hass): + """Test if we unload an entry that is in retry mode.""" + entry = MockConfigEntry(domain='test') + + mock_setup_entry = MagicMock(side_effect=ConfigEntryNotReady) + loader.set_component( + hass, 'test', MockModule('test', async_setup_entry=mock_setup_entry)) + + with patch('homeassistant.helpers.event.async_call_later') as mock_call: + await entry.async_setup(hass) + + assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + assert len(mock_call.return_value.mock_calls) == 0 + + await entry.async_unload(hass) + + assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert len(mock_call.return_value.mock_calls) == 1 diff --git a/tests/test_loader.py b/tests/test_loader.py index 4beb7db570e..c4adb971593 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -79,10 +79,10 @@ def test_component_loader_non_existing(hass): @asyncio.coroutine def test_component_wrapper(hass): """Test component wrapper.""" - calls = async_mock_service(hass, 'light', 'turn_on') + calls = async_mock_service(hass, 'persistent_notification', 'create') components = loader.Components(hass) - components.light.async_turn_on('light.test') + components.persistent_notification.async_create('message') yield from hass.async_block_till_done() assert len(calls) == 1 diff --git a/tests/util/test_json.py b/tests/util/test_json.py new file mode 100644 index 00000000000..53f62682b5e --- /dev/null +++ b/tests/util/test_json.py @@ -0,0 +1,75 @@ +"""Test Home Assistant json utility functions.""" +import os +import unittest +import sys +from tempfile import mkdtemp + +from homeassistant.util.json import (SerializationError, + load_json, save_json) +from homeassistant.exceptions import HomeAssistantError + +# Test data that can be saved as JSON +TEST_JSON_A = {"a": 1, "B": "two"} +TEST_JSON_B = {"a": "one", "B": 2} +# Test data that can not be saved as JSON (keys must be strings) +TEST_BAD_OBJECT = {("A",): 1} +# Test data that can not be loaded as JSON +TEST_BAD_SERIALIED = "THIS IS NOT JSON\n" + + +class TestJSON(unittest.TestCase): + """Test util.json save and load.""" + + def setUp(self): + """Set up for tests.""" + self.tmp_dir = mkdtemp() + + def tearDown(self): + """Clean up after tests.""" + for fname in os.listdir(self.tmp_dir): + os.remove(os.path.join(self.tmp_dir, fname)) + os.rmdir(self.tmp_dir) + + def _path_for(self, leaf_name): + return os.path.join(self.tmp_dir, leaf_name+".json") + + def test_save_and_load(self): + """Test saving and loading back.""" + fname = self._path_for("test1") + save_json(fname, TEST_JSON_A) + data = load_json(fname) + self.assertEqual(data, TEST_JSON_A) + + # Skipped on Windows + @unittest.skipIf(sys.platform.startswith('win'), + "private permissions not supported on Windows") + def test_save_and_load_private(self): + """Test we can load private files and that they are protected.""" + fname = self._path_for("test2") + save_json(fname, TEST_JSON_A, private=True) + data = load_json(fname) + self.assertEqual(data, TEST_JSON_A) + stats = os.stat(fname) + self.assertEqual(stats.st_mode & 0o77, 0) + + def test_overwrite_and_reload(self): + """Test that we can overwrite an existing file and read back.""" + fname = self._path_for("test3") + save_json(fname, TEST_JSON_A) + save_json(fname, TEST_JSON_B) + data = load_json(fname) + self.assertEqual(data, TEST_JSON_B) + + def test_save_bad_data(self): + """Test error from trying to save unserialisable data.""" + fname = self._path_for("test4") + with self.assertRaises(SerializationError): + save_json(fname, TEST_BAD_OBJECT) + + def test_load_bad_data(self): + """Test error from trying to load unserialisable data.""" + fname = self._path_for("test5") + with open(fname, "w") as fh: + fh.write(TEST_BAD_SERIALIED) + with self.assertRaises(HomeAssistantError): + load_json(fname) diff --git a/tox.ini b/tox.ini index 60dacd5d8cb..dcfb209ef3a 100644 --- a/tox.ini +++ b/tox.ini @@ -13,6 +13,7 @@ whitelist_externals = /usr/bin/env install_command = /usr/bin/env LANG=C.UTF-8 pip install {opts} {packages} commands = pytest --timeout=9 --duration=10 {posargs} + {toxinidir}/script/check_dirty deps = -r{toxinidir}/requirements_test_all.txt -c{toxinidir}/homeassistant/package_constraints.txt @@ -29,6 +30,7 @@ whitelist_externals = /usr/bin/env install_command = /usr/bin/env LANG=C.UTF-8 pip install {opts} {packages} commands = pytest --timeout=9 --duration=10 --cov --cov-report= {posargs} + {toxinidir}/script/check_dirty deps = -r{toxinidir}/requirements_test_all.txt -c{toxinidir}/homeassistant/package_constraints.txt