diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index e453cdfd1a1..9ec081166bb 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -88,8 +88,8 @@ CAMERA_SERVICE_PLAY_STREAM = CAMERA_SERVICE_SCHEMA.extend({ CAMERA_SERVICE_RECORD = CAMERA_SERVICE_SCHEMA.extend({ vol.Required(CONF_FILENAME): cv.template, - vol.Optional(CONF_DURATION, default=30): int, - vol.Optional(CONF_LOOKBACK, default=0): int, + vol.Optional(CONF_DURATION, default=30): vol.Coerce(int), + vol.Optional(CONF_LOOKBACK, default=0): vol.Coerce(int), }) WS_TYPE_CAMERA_THUMBNAIL = 'camera_thumbnail' diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index 1b4ee039053..8adac658625 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -19,6 +19,7 @@ _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['libpyfoscam==1.0'] CONF_IP = 'ip' +CONF_RTSP_PORT = 'rtsp_port' DEFAULT_NAME = 'Foscam Camera' DEFAULT_PORT = 88 @@ -31,6 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_USERNAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_RTSP_PORT): cv.port }) @@ -58,11 +60,12 @@ class FoscamCam(Camera): self._foscam_session = FoscamCamera( ip_address, port, self._username, self._password, verbose=False) - self._rtsp_port = None - result, response = self._foscam_session.get_port_info() - if result == 0: - self._rtsp_port = response.get('rtspPort') or \ - response.get('mediaPort') + self._rtsp_port = device_info.get(CONF_RTSP_PORT) + if not self._rtsp_port: + result, response = self._foscam_session.get_port_info() + if result == 0: + self._rtsp_port = response.get('rtspPort') or \ + response.get('mediaPort') def camera_image(self): """Return a still image response from the camera.""" diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 91224c6f54d..4f7c99618c1 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -118,6 +118,7 @@ class HassIOIngress(HomeAssistantView): return web.Response( headers=headers, status=result.status, + content_type=result.content_type, body=body ) @@ -145,8 +146,7 @@ def _init_header( # filter flags for name, value in request.headers.items(): - if name in (hdrs.CONTENT_LENGTH, hdrs.CONTENT_TYPE, - hdrs.CONTENT_ENCODING): + if name in (hdrs.CONTENT_LENGTH, hdrs.CONTENT_ENCODING): continue headers[name] = value diff --git a/homeassistant/components/konnected/switch.py b/homeassistant/components/konnected/switch.py index 7384d62900e..3db602215b9 100644 --- a/homeassistant/components/konnected/switch.py +++ b/homeassistant/components/konnected/switch.py @@ -41,9 +41,10 @@ class KonnectedSwitch(ToggleEntity): self._pause = self._data.get(CONF_PAUSE) self._repeat = self._data.get(CONF_REPEAT) self._state = self._boolean_state(self._data.get(ATTR_STATE)) - self._unique_id = '{}-{}'.format(device_id, hash(frozenset( - {self._pin_num, self._momentary, self._pause, self._repeat}))) self._name = self._data.get(CONF_NAME) + self._unique_id = '{}-{}-{}-{}-{}'.format( + device_id, self._pin_num, self._momentary, + self._pause, self._repeat) @property def unique_id(self) -> str: diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index 61c50e97c6e..05d240da909 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -42,6 +42,11 @@ ATTR_OS_NAME = 'os_name' ATTR_OS_VERSION = 'os_version' ATTR_PUSH_TOKEN = 'push_token' ATTR_PUSH_URL = 'push_url' +ATTR_PUSH_RATE_LIMITS = 'rateLimits' +ATTR_PUSH_RATE_LIMITS_ERRORS = 'errors' +ATTR_PUSH_RATE_LIMITS_MAXIMUM = 'maximum' +ATTR_PUSH_RATE_LIMITS_RESETS_AT = 'resetsAt' +ATTR_PUSH_RATE_LIMITS_SUCCESSFUL = 'successful' ATTR_SUPPORTS_ENCRYPTION = 'supports_encryption' ATTR_EVENT_DATA = 'event_data' @@ -67,6 +72,8 @@ ERR_SENSOR_DUPLICATE_UNIQUE_ID = 'duplicate_unique_id' WEBHOOK_TYPE_CALL_SERVICE = 'call_service' WEBHOOK_TYPE_FIRE_EVENT = 'fire_event' +WEBHOOK_TYPE_GET_CONFIG = 'get_config' +WEBHOOK_TYPE_GET_ZONES = 'get_zones' WEBHOOK_TYPE_REGISTER_SENSOR = 'register_sensor' WEBHOOK_TYPE_RENDER_TEMPLATE = 'render_template' WEBHOOK_TYPE_UPDATE_LOCATION = 'update_location' @@ -74,6 +81,7 @@ WEBHOOK_TYPE_UPDATE_REGISTRATION = 'update_registration' WEBHOOK_TYPE_UPDATE_SENSOR_STATES = 'update_sensor_states' WEBHOOK_TYPES = [WEBHOOK_TYPE_CALL_SERVICE, WEBHOOK_TYPE_FIRE_EVENT, + WEBHOOK_TYPE_GET_CONFIG, WEBHOOK_TYPE_GET_ZONES, WEBHOOK_TYPE_REGISTER_SENSOR, WEBHOOK_TYPE_RENDER_TEMPLATE, WEBHOOK_TYPE_UPDATE_LOCATION, WEBHOOK_TYPE_UPDATE_REGISTRATION, @@ -163,7 +171,7 @@ REGISTER_SENSOR_SCHEMA = vol.Schema({ vol.Required(ATTR_SENSOR_NAME): cv.string, vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, - vol.Required(ATTR_SENSOR_UOM): cv.string, + vol.Optional(ATTR_SENSOR_UOM): cv.string, vol.Required(ATTR_SENSOR_STATE): vol.Any(bool, str, int, float), vol.Optional(ATTR_SENSOR_ICON, default='mdi:cellphone'): cv.icon, }) diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index 60bd8b4e1d6..ee593588ef8 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -6,6 +6,7 @@ from typing import Callable, Dict, Tuple from aiohttp.web import json_response, Response from homeassistant.core import Context +from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.typing import HomeAssistantType from .const import (ATTR_APP_DATA, ATTR_APP_ID, ATTR_APP_NAME, @@ -133,9 +134,9 @@ def savable_state(hass: HomeAssistantType) -> Dict: def webhook_response(data, *, registration: Dict, status: int = 200, headers: Dict = None) -> Response: """Return a encrypted response if registration supports it.""" - data = json.dumps(data) + data = json.dumps(data, cls=JSONEncoder) - if CONF_SECRET in registration: + if registration[ATTR_SUPPORTS_ENCRYPTION]: keylen, encrypt = setup_encrypt() key = registration[CONF_SECRET].encode("utf-8") diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index 0120b1a6ffb..8d2ac1b97ec 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -8,13 +8,18 @@ import async_timeout from homeassistant.components.notify import ( ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, BaseNotificationService) -from homeassistant.components.mobile_app.const import ( - ATTR_APP_DATA, ATTR_APP_ID, ATTR_APP_VERSION, ATTR_DEVICE_NAME, - ATTR_OS_VERSION, ATTR_PUSH_TOKEN, ATTR_PUSH_URL, DATA_CONFIG_ENTRIES, - DOMAIN) + from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.util.dt as dt_util +from .const import (ATTR_APP_DATA, ATTR_APP_ID, ATTR_APP_VERSION, + ATTR_DEVICE_NAME, ATTR_OS_VERSION, ATTR_PUSH_RATE_LIMITS, + ATTR_PUSH_RATE_LIMITS_ERRORS, + ATTR_PUSH_RATE_LIMITS_MAXIMUM, + ATTR_PUSH_RATE_LIMITS_RESETS_AT, + ATTR_PUSH_RATE_LIMITS_SUCCESSFUL, ATTR_PUSH_TOKEN, + ATTR_PUSH_URL, DATA_CONFIG_ENTRIES, DOMAIN) + _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['mobile_app'] @@ -38,16 +43,21 @@ def push_registrations(hass): # pylint: disable=invalid-name def log_rate_limits(hass, device_name, resp, level=logging.INFO): """Output rate limit log line at given level.""" - rate_limits = resp['rateLimits'] - resetsAt = dt_util.parse_datetime(rate_limits['resetsAt']) - resetsAtTime = resetsAt - datetime.now(timezone.utc) + if ATTR_PUSH_RATE_LIMITS not in resp: + return + + rate_limits = resp[ATTR_PUSH_RATE_LIMITS] + resetsAt = rate_limits[ATTR_PUSH_RATE_LIMITS_RESETS_AT] + resetsAtTime = (dt_util.parse_datetime(resetsAt) - + datetime.now(timezone.utc)) rate_limit_msg = ("mobile_app push notification rate limits for %s: " "%d sent, %d allowed, %d errors, " "resets in %s") _LOGGER.log(level, rate_limit_msg, device_name, - rate_limits['successful'], - rate_limits['maximum'], rate_limits['errors'], + rate_limits[ATTR_PUSH_RATE_LIMITS_SUCCESSFUL], + rate_limits[ATTR_PUSH_RATE_LIMITS_MAXIMUM], + rate_limits[ATTR_PUSH_RATE_LIMITS_ERRORS], str(resetsAtTime).split(".")[0]) diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index c6a53ce57ec..b2846a6002b 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -55,4 +55,4 @@ class MobileAppSensor(MobileAppEntity): @property def unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" - return self._config[ATTR_SENSOR_UOM] + return self._config.get(ATTR_SENSOR_UOM) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index aafa6046d11..28ef6bccd6a 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -8,6 +8,8 @@ from homeassistant.components.device_tracker import (ATTR_ATTRIBUTES, ATTR_DEV_ID, DOMAIN as DT_DOMAIN, SERVICE_SEE as DT_SEE) +from homeassistant.components.frontend import MANIFEST_JSON +from homeassistant.components.zone.const import DOMAIN as ZONE_DOMAIN from homeassistant.const import (ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA, CONF_WEBHOOK_ID, HTTP_BAD_REQUEST, @@ -33,8 +35,9 @@ from .const import (ATTR_ALTITUDE, ATTR_BATTERY, ATTR_COURSE, ATTR_DEVICE_ID, DATA_STORE, DOMAIN, ERR_ENCRYPTION_REQUIRED, ERR_SENSOR_DUPLICATE_UNIQUE_ID, ERR_SENSOR_NOT_REGISTERED, SIGNAL_SENSOR_UPDATE, WEBHOOK_PAYLOAD_SCHEMA, - WEBHOOK_SCHEMAS, WEBHOOK_TYPE_CALL_SERVICE, - WEBHOOK_TYPE_FIRE_EVENT, WEBHOOK_TYPE_REGISTER_SENSOR, + WEBHOOK_SCHEMAS, WEBHOOK_TYPES, WEBHOOK_TYPE_CALL_SERVICE, + WEBHOOK_TYPE_FIRE_EVENT, WEBHOOK_TYPE_GET_CONFIG, + WEBHOOK_TYPE_GET_ZONES, WEBHOOK_TYPE_REGISTER_SENSOR, WEBHOOK_TYPE_RENDER_TEMPLATE, WEBHOOK_TYPE_UPDATE_LOCATION, WEBHOOK_TYPE_UPDATE_REGISTRATION, WEBHOOK_TYPE_UPDATE_SENSOR_STATES) @@ -87,16 +90,19 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str, enc_data = req_data[ATTR_WEBHOOK_ENCRYPTED_DATA] webhook_payload = _decrypt_payload(registration[CONF_SECRET], enc_data) - if webhook_type not in WEBHOOK_SCHEMAS: + if webhook_type not in WEBHOOK_TYPES: _LOGGER.error('Received invalid webhook type: %s', webhook_type) return empty_okay_response() - try: - data = WEBHOOK_SCHEMAS[webhook_type](webhook_payload) - except vol.Invalid as ex: - err = vol.humanize.humanize_error(webhook_payload, ex) - _LOGGER.error('Received invalid webhook payload: %s', err) - return empty_okay_response(headers=headers) + data = webhook_payload + + if webhook_type in WEBHOOK_SCHEMAS: + try: + data = WEBHOOK_SCHEMAS[webhook_type](webhook_payload) + except vol.Invalid as ex: + err = vol.humanize.humanize_error(webhook_payload, ex) + _LOGGER.error('Received invalid webhook payload: %s', err) + return empty_okay_response(headers=headers) context = registration_context(registration) @@ -139,18 +145,26 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str, if webhook_type == WEBHOOK_TYPE_UPDATE_LOCATION: see_payload = { ATTR_DEV_ID: registration[ATTR_DEVICE_ID], - ATTR_LOCATION_NAME: data.get(ATTR_LOCATION_NAME), - ATTR_GPS: data.get(ATTR_GPS), - ATTR_GPS_ACCURACY: data.get(ATTR_GPS_ACCURACY), - ATTR_BATTERY: data.get(ATTR_BATTERY), - ATTR_ATTRIBUTES: { - ATTR_SPEED: data.get(ATTR_SPEED), - ATTR_ALTITUDE: data.get(ATTR_ALTITUDE), - ATTR_COURSE: data.get(ATTR_COURSE), - ATTR_VERTICAL_ACCURACY: data.get(ATTR_VERTICAL_ACCURACY), - } + ATTR_GPS: data[ATTR_GPS], + ATTR_GPS_ACCURACY: data[ATTR_GPS_ACCURACY], } + for key in (ATTR_LOCATION_NAME, ATTR_BATTERY): + value = data.get(key) + if value is not None: + see_payload[key] = value + + attrs = {} + + for key in (ATTR_ALTITUDE, ATTR_COURSE, + ATTR_SPEED, ATTR_VERTICAL_ACCURACY): + value = data.get(key) + if value is not None: + attrs[key] = value + + if attrs: + see_payload[ATTR_ATTRIBUTES] = attrs + try: await hass.services.async_call(DT_DOMAIN, DT_SEE, see_payload, @@ -214,7 +228,7 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str, data[ATTR_SENSOR_TYPE]) async_dispatcher_send(hass, register_signal, data) - return webhook_response({"status": "registered"}, + return webhook_response({'success': True}, registration=registration, status=HTTP_CREATED, headers=headers) @@ -257,7 +271,29 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str, async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, new_state) - resp[unique_id] = {"status": "okay"} + resp[unique_id] = {'success': True} return webhook_response(resp, registration=registration, headers=headers) + + if webhook_type == WEBHOOK_TYPE_GET_ZONES: + zones = (hass.states.get(entity_id) for entity_id + in sorted(hass.states.async_entity_ids(ZONE_DOMAIN))) + return webhook_response(list(zones), registration=registration, + headers=headers) + + if webhook_type == WEBHOOK_TYPE_GET_CONFIG: + + hass_config = hass.config.as_dict() + + return webhook_response({ + 'latitude': hass_config['latitude'], + 'longitude': hass_config['longitude'], + 'elevation': hass_config['elevation'], + 'unit_system': hass_config['unit_system'], + 'location_name': hass_config['location_name'], + 'time_zone': hass_config['time_zone'], + 'components': hass_config['components'], + 'version': hass_config['version'], + 'theme_color': MANIFEST_JSON['theme_color'], + }, registration=registration, headers=headers) diff --git a/homeassistant/components/mopar/__init__.py b/homeassistant/components/mopar/__init__.py index d845d585765..4ee9f3219b4 100644 --- a/homeassistant/components/mopar/__init__.py +++ b/homeassistant/components/mopar/__init__.py @@ -53,12 +53,13 @@ def setup(hass, config): """Set up the Mopar component.""" import motorparts + conf = config[DOMAIN] cookie = hass.config.path(COOKIE_FILE) try: session = motorparts.get_session( - config[CONF_USERNAME], - config[CONF_PASSWORD], - config[CONF_PIN], + conf[CONF_USERNAME], + conf[CONF_PASSWORD], + conf[CONF_PIN], cookie_path=cookie ) except motorparts.MoparError: @@ -69,7 +70,7 @@ def setup(hass, config): data.update(now=None) track_time_interval( - hass, data.update, config[CONF_SCAN_INTERVAL] + hass, data.update, conf[CONF_SCAN_INTERVAL] ) def handle_horn(call): diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 6808729685e..8d3f541972e 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -121,3 +121,8 @@ class TadoDataStore: """Wrap for setZoneOverlay(..).""" self.tado.setZoneOverlay(zone_id, mode, temperature, duration) self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg + + def set_zone_off(self, zone_id, mode): + """Set a zone to off.""" + self.tado.setZoneOverlay(zone_id, mode, None, None, 'HEATING', 'OFF') + self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 56c670184b5..90d5f076974 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -363,7 +363,7 @@ class TadoClimate(ClimateDevice): if self._current_operation == CONST_MODE_OFF: _LOGGER.info("Switching mytado.com to OFF for zone %s", self.zone_name) - self._store.set_zone_overlay(self.zone_id, CONST_OVERLAY_MANUAL) + self._store.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL) self._overlay_mode = self._current_operation return diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 99382bb1da9..0cb9d41fe4b 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -134,7 +134,7 @@ def setup(hass, config): discovery.listen(hass, SERVICE_YEELIGHT, device_discovered) def update(event): - for device in yeelight_data.values(): + for device in list(yeelight_data.values()): device.update() track_time_interval( @@ -185,8 +185,8 @@ class YeelightDevice: @property def bulb(self): """Return bulb device.""" - import yeelight if self._bulb_device is None: + import yeelight try: self._bulb_device = yeelight.Bulb(self._ipaddr, model=self._model) @@ -241,33 +241,27 @@ class YeelightDevice: def turn_on(self, duration=DEFAULT_TRANSITION, light_type=None): """Turn on device.""" - import yeelight - - if not light_type: - light_type = yeelight.enums.LightType.Main + from yeelight import BulbException try: self.bulb.turn_on(duration=duration, light_type=light_type) - except yeelight.BulbException as ex: + except BulbException as ex: _LOGGER.error("Unable to turn the bulb on: %s", ex) return def turn_off(self, duration=DEFAULT_TRANSITION, light_type=None): """Turn off device.""" - import yeelight - - if not light_type: - light_type = yeelight.enums.LightType.Main + from yeelight import BulbException try: self.bulb.turn_off(duration=duration, light_type=light_type) - except yeelight.BulbException as ex: + except BulbException as ex: _LOGGER.error("Unable to turn the bulb off: %s", ex) return def update(self): """Read new properties from the device.""" - import yeelight + from yeelight import BulbException if not self.bulb: return @@ -275,7 +269,7 @@ class YeelightDevice: try: self.bulb.get_properties(UPDATE_REQUEST_PROPERTIES) self._available = True - except yeelight.BulbException as ex: + except BulbException as ex: if self._available: # just inform once _LOGGER.error("Unable to update bulb status: %s", ex) self._available = False diff --git a/homeassistant/components/yeelight/binary_sensor.py b/homeassistant/components/yeelight/binary_sensor.py index cf7bbc5244e..d39af08f768 100644 --- a/homeassistant/components/yeelight/binary_sensor.py +++ b/homeassistant/components/yeelight/binary_sensor.py @@ -4,7 +4,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.components.yeelight import DATA_YEELIGHT, DATA_UPDATED +from . import DATA_YEELIGHT, DATA_UPDATED DEPENDENCIES = ['yeelight'] diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 912a4f99c92..74796a524b0 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -15,7 +15,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, SUPPORT_FLASH, SUPPORT_EFFECT, Light) import homeassistant.util.color as color_util -from homeassistant.components.yeelight import ( +from . import ( CONF_TRANSITION, DATA_YEELIGHT, CONF_MODE_MUSIC, CONF_SAVE_ON_CHANGE, CONF_CUSTOM_EFFECTS, DATA_UPDATED, YEELIGHT_SERVICE_SCHEMA, DOMAIN, ATTR_TRANSITIONS, @@ -189,6 +189,8 @@ class YeelightLight(Light): def __init__(self, device, custom_effects=None): """Initialize the Yeelight light.""" + from yeelight.enums import LightType + self.config = device.config self._device = device @@ -202,6 +204,8 @@ class YeelightLight(Light): self._min_mireds = None self._max_mireds = None + self._light_type = LightType.Main + if custom_effects: self._custom_effects = custom_effects else: @@ -281,8 +285,7 @@ class YeelightLight(Light): @property def light_type(self): """Return light type.""" - import yeelight - return yeelight.enums.LightType.Main + return self._light_type def _get_hs_from_properties(self): rgb = self._get_property('rgb') @@ -589,21 +592,19 @@ class YeelightAmbientLight(YeelightLight): def __init__(self, *args, **kwargs): """Initialize the Yeelight Ambient light.""" + from yeelight.enums import LightType + super().__init__(*args, **kwargs) self._min_mireds = kelvin_to_mired(6500) self._max_mireds = kelvin_to_mired(1700) + self._light_type = LightType.Ambient + @property def name(self) -> str: """Return the name of the device if any.""" return "{} ambilight".format(self.device.name) - @property - def light_type(self): - """Return light type.""" - import yeelight - return yeelight.enums.LightType.Ambient - @property def _is_nightlight_enabled(self): return False diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 6ba4efa9b0f..fc29fd0cdd2 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -172,6 +172,7 @@ class Light(ZhaEntity, light.Light): duration = transition * 10 if transition else DEFAULT_DURATION brightness = kwargs.get(light.ATTR_BRIGHTNESS) + t_log = {} if (brightness is not None or transition) and \ self._supported_features & light.SUPPORT_BRIGHTNESS: if brightness is not None: @@ -182,7 +183,9 @@ class Light(ZhaEntity, light.Light): level, duration ) + t_log['move_to_level_with_on_off'] = success if not success: + self.debug("turned on: %s", t_log) return self._state = bool(level) if level: @@ -190,7 +193,9 @@ class Light(ZhaEntity, light.Light): if brightness is None or brightness: success = await self._on_off_channel.on() + t_log['on_off'] = success if not success: + self.debug("turned on: %s", t_log) return self._state = True @@ -199,7 +204,9 @@ class Light(ZhaEntity, light.Light): temperature = kwargs[light.ATTR_COLOR_TEMP] success = await self._color_channel.move_to_color_temp( temperature, duration) + t_log['move_to_color_temp'] = success if not success: + self.debug("turned on: %s", t_log) return self._color_temp = temperature @@ -212,10 +219,13 @@ class Light(ZhaEntity, light.Light): int(xy_color[1] * 65535), duration, ) + t_log['move_to_color'] = success if not success: + self.debug("turned on: %s", t_log) return self._hs_color = hs_color + self.debug("turned on: %s", t_log) self.async_schedule_update_ha_state() async def async_turn_off(self, **kwargs): @@ -229,7 +239,7 @@ class Light(ZhaEntity, light.Light): ) else: success = await self._on_off_channel.off() - _LOGGER.debug("%s was turned off: %s", self.entity_id, success) + self.debug("turned off: %s", success) if not success: return self._state = False @@ -238,13 +248,21 @@ class Light(ZhaEntity, light.Light): async def async_update(self): """Attempt to retrieve on off state from the light.""" await super().async_update() + await self.async_get_state() + + async def async_get_state(self, from_cache=True): + """Attempt to retrieve on off state from the light.""" if self._on_off_channel: self._state = await self._on_off_channel.get_attribute_value( - 'on_off') + 'on_off', from_cache=from_cache) if self._level_channel: self._brightness = await self._level_channel.get_attribute_value( - 'current_level') + 'current_level', from_cache=from_cache) async def refresh(self, time): - """Call async_update at an interval.""" - await self.async_update() + """Call async_get_state at an interval.""" + await self.async_get_state(from_cache=False) + + def debug(self, msg, *args): + """Log debug message.""" + _LOGGER.debug('%s: ' + msg, self.entity_id, *args) diff --git a/homeassistant/components/zone/config_flow.py b/homeassistant/components/zone/config_flow.py index bf221a828ad..a7b968676d6 100644 --- a/homeassistant/components/zone/config_flow.py +++ b/homeassistant/components/zone/config_flow.py @@ -14,7 +14,7 @@ from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE @callback def configured_zones(hass): - """Return a set of the configured hosts.""" + """Return a set of the configured zones.""" return set((slugify(entry.data[CONF_NAME])) for entry in hass.config_entries.async_entries(DOMAIN)) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6ece276307e..28bdf2f7fc3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 91 -PATCH_VERSION = '1' +PATCH_VERSION = '2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) diff --git a/tests/components/mobile_app/test_entity.py b/tests/components/mobile_app/test_entity.py index 5dc285cfe9e..3d8e575f686 100644 --- a/tests/components/mobile_app/test_entity.py +++ b/tests/components/mobile_app/test_entity.py @@ -35,7 +35,7 @@ async def test_sensor(hass, create_registrations, webhook_client): # noqa: F401 assert reg_resp.status == 201 json = await reg_resp.json() - assert json == {'status': 'registered'} + assert json == {'success': True} entity = hass.states.get('sensor.battery_state') assert entity is not None @@ -122,7 +122,7 @@ async def test_sensor_id_no_dupes(hass, create_registrations, # noqa: F401, F81 assert reg_resp.status == 201 reg_json = await reg_resp.json() - assert reg_json == {'status': 'registered'} + assert reg_json == {'success': True} dupe_resp = await webhook_client.post(webhook_url, json=payload) diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index a70e8ba1275..43eac28ec18 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -4,8 +4,10 @@ import logging import pytest from homeassistant.components.mobile_app.const import CONF_SECRET +from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.core import callback +from homeassistant.setup import async_setup_component from tests.common import async_mock_service @@ -100,6 +102,64 @@ async def test_webhook_update_registration(webhook_client, hass_client): # noqa assert CONF_SECRET not in update_json +async def test_webhook_handle_get_zones(hass, create_registrations, # noqa: F401, F811, E501 + webhook_client): # noqa: F811 + """Test that we can get zones properly.""" + await async_setup_component(hass, ZONE_DOMAIN, { + ZONE_DOMAIN: { + 'name': 'test', + 'latitude': 32.880837, + 'longitude': -117.237561, + 'radius': 250, + } + }) + + resp = await webhook_client.post( + '/api/webhook/{}'.format(create_registrations[1]['webhook_id']), + json={'type': 'get_zones'} + ) + + assert resp.status == 200 + + json = await resp.json() + assert len(json) == 1 + assert json[0]['entity_id'] == 'zone.home' + + +async def test_webhook_handle_get_config(hass, create_registrations, # noqa: F401, F811, E501 + webhook_client): # noqa: F811 + """Test that we can get config properly.""" + resp = await webhook_client.post( + '/api/webhook/{}'.format(create_registrations[1]['webhook_id']), + json={'type': 'get_config'} + ) + + assert resp.status == 200 + + json = await resp.json() + if 'components' in json: + json['components'] = set(json['components']) + if 'whitelist_external_dirs' in json: + json['whitelist_external_dirs'] = \ + set(json['whitelist_external_dirs']) + + hass_config = hass.config.as_dict() + + expected_dict = { + 'latitude': hass_config['latitude'], + 'longitude': hass_config['longitude'], + 'elevation': hass_config['elevation'], + 'unit_system': hass_config['unit_system'], + 'location_name': hass_config['location_name'], + 'time_zone': hass_config['time_zone'], + 'components': hass_config['components'], + 'version': hass_config['version'], + 'theme_color': '#03A9F4', # Default frontend theme color + } + + assert expected_dict == json + + async def test_webhook_returns_error_incorrect_json(webhook_client, # noqa: F401, F811, E501 create_registrations, # noqa: F401, F811, E501 caplog): # noqa: E501 F811