From 5d850c5b1923216833add3c18fa8f9f651529def Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Jan 2020 21:06:22 +0100 Subject: [PATCH 01/41] Bumped version to 0.104.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a9642e88e15..c7131c41d56 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 104 -PATCH_VERSION = "0.dev0" +PATCH_VERSION = "0b0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From c7e300e5b1684994e1ec37c90171bfa08a6262ba Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Wed, 8 Jan 2020 23:51:30 -0600 Subject: [PATCH 02/41] Fix upnp raw sensor state formatting when None (#30444) --- homeassistant/components/upnp/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 06e4a86401f..81fd5c025b9 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -126,6 +126,9 @@ class RawUPnPIGDSensor(UpnpSensor): @property def state(self) -> str: """Return the state of the device.""" + if self._state is None: + return None + return format(self._state, "d") @property From e58b41ef239d294a66082cf7c72d7eee00744b52 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Jan 2020 21:22:56 +0100 Subject: [PATCH 03/41] Implement capability attributes (#30545) * Implement capability attributes * Fix HeOS update order * Fix test --- homeassistant/components/fan/__init__.py | 6 ++++- homeassistant/components/heos/media_player.py | 4 ++- .../components/media_player/__init__.py | 20 ++++++++++++-- homeassistant/components/vacuum/__init__.py | 13 ++++++++- .../components/water_heater/__init__.py | 27 +++++++++++++------ tests/components/fan/test_init.py | 2 +- 6 files changed, 58 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index fe6843ed6b9..44b33af0c6e 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -47,7 +47,6 @@ ATTR_DIRECTION = "direction" PROP_TO_ATTR = { "speed": ATTR_SPEED, - "speed_list": ATTR_SPEED_LIST, "oscillating": ATTR_OSCILLATING, "current_direction": ATTR_DIRECTION, } @@ -178,6 +177,11 @@ class FanEntity(ToggleEntity): """Return the current direction of the fan.""" return None + @property + def capability_attributes(self): + """Return capabilitiy attributes.""" + return {ATTR_SPEED_LIST: self.speed_list} + @property def state_attributes(self) -> dict: """Return optional state attributes.""" diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 10ea28ca16c..9016a8b3cea 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -115,7 +115,6 @@ class HeosMediaPlayer(MediaPlayerDevice): async def async_added_to_hass(self): """Device added to hass.""" - self._source_manager = self.hass.data[HEOS_DOMAIN][DATA_SOURCE_MANAGER] # Update state when attributes of the player change self._signals.append( self._player.heos.dispatcher.connect( @@ -242,6 +241,9 @@ class HeosMediaPlayer(MediaPlayerDevice): current_support = [CONTROL_TO_SUPPORT[control] for control in controls] self._supported_features = reduce(ior, current_support, BASE_SUPPORTED_FEATURES) + if self._source_manager is None: + self._source_manager = self.hass.data[HEOS_DOMAIN][DATA_SOURCE_MANAGER] + async def async_will_remove_from_hass(self): """Disconnect the device when removed.""" for signal_remove in self._signals: diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 1375a0ed429..83c117d6c05 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -149,9 +149,7 @@ ATTR_TO_PROPERTY = [ ATTR_APP_ID, ATTR_APP_NAME, ATTR_INPUT_SOURCE, - ATTR_INPUT_SOURCE_LIST, ATTR_SOUND_MODE, - ATTR_SOUND_MODE_LIST, ATTR_MEDIA_SHUFFLE, ] @@ -784,6 +782,24 @@ class MediaPlayerDevice(Entity): return ENTITY_IMAGE_URL.format(self.entity_id, self.access_token, image_hash) + @property + def capability_attributes(self): + """Return capabilitiy attributes.""" + supported_features = self.supported_features + data = {} + + if supported_features & SUPPORT_SELECT_SOURCE: + source_list = self.source_list + if source_list: + data[ATTR_INPUT_SOURCE_LIST] = source_list + + if supported_features & SUPPORT_SELECT_SOUND_MODE: + sound_mode_list = self.sound_mode_list + if sound_mode_list: + data[ATTR_SOUND_MODE_LIST] = sound_mode_list + + return data + @property def state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 5dd4682e7cc..225a6ed72bc 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -246,6 +246,12 @@ class VacuumDevice(_BaseVacuum, ToggleEntity): battery_level=self.battery_level, charging=charging ) + @property + def capability_attributes(self): + """Return capabilitiy attributes.""" + if self.fan_speed is not None: + return {ATTR_FAN_SPEED_LIST: self.fan_speed_list} + @property def state_attributes(self): """Return the state attributes of the vacuum cleaner.""" @@ -260,7 +266,6 @@ class VacuumDevice(_BaseVacuum, ToggleEntity): if self.fan_speed is not None: data[ATTR_FAN_SPEED] = self.fan_speed - data[ATTR_FAN_SPEED_LIST] = self.fan_speed_list return data @@ -323,6 +328,12 @@ class StateVacuumDevice(_BaseVacuum): battery_level=self.battery_level, charging=charging ) + @property + def capability_attributes(self): + """Return capabilitiy attributes.""" + if self.fan_speed is not None: + return {ATTR_FAN_SPEED_LIST: self.fan_speed_list} + @property def state_attributes(self): """Return the state attributes of the vacuum cleaner.""" diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index c5ba009717c..8da94ff1098 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -143,6 +143,25 @@ class WaterHeaterDevice(Entity): return PRECISION_TENTHS return PRECISION_WHOLE + @property + def capability_attributes(self): + """Return capabilitiy attributes.""" + supported_features = self.supported_features + + data = { + ATTR_MIN_TEMP: show_temp( + self.hass, self.min_temp, self.temperature_unit, self.precision + ), + ATTR_MAX_TEMP: show_temp( + self.hass, self.max_temp, self.temperature_unit, self.precision + ), + } + + if supported_features & SUPPORT_OPERATION_MODE: + data[ATTR_OPERATION_LIST] = self.operation_list + + return data + @property def state_attributes(self): """Return the optional state attributes.""" @@ -153,12 +172,6 @@ class WaterHeaterDevice(Entity): self.temperature_unit, self.precision, ), - ATTR_MIN_TEMP: show_temp( - self.hass, self.min_temp, self.temperature_unit, self.precision - ), - ATTR_MAX_TEMP: show_temp( - self.hass, self.max_temp, self.temperature_unit, self.precision - ), ATTR_TEMPERATURE: show_temp( self.hass, self.target_temperature, @@ -183,8 +196,6 @@ class WaterHeaterDevice(Entity): if supported_features & SUPPORT_OPERATION_MODE: data[ATTR_OPERATION_MODE] = self.current_operation - if self.operation_list: - data[ATTR_OPERATION_LIST] = self.operation_list if supported_features & SUPPORT_AWAY_MODE: is_away = self.is_away_mode_on diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py index 316504381ec..5d66edea9c7 100644 --- a/tests/components/fan/test_init.py +++ b/tests/components/fan/test_init.py @@ -31,7 +31,7 @@ class TestFanEntity(unittest.TestCase): assert "off" == self.fan.state assert 0 == len(self.fan.speed_list) assert 0 == self.fan.supported_features - assert {"speed_list": []} == self.fan.state_attributes + assert {"speed_list": []} == self.fan.capability_attributes # Test set_speed not required self.fan.oscillate(True) with pytest.raises(NotImplementedError): From 0b3b589457110f1b2671298c80fa31ff948f91b0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 10 Jan 2020 21:35:31 +0100 Subject: [PATCH 04/41] Add Ring config flow (#30564) * Add Ring config flow * Address comments + migrate platforms to config entry * Migrate camera too * Address comments * Fix order config flows * setup -> async_setup --- .../components/ring/.translations/en.json | 28 +++++ homeassistant/components/ring/__init__.py | 112 ++++++++++++++---- .../components/ring/binary_sensor.py | 39 ++---- homeassistant/components/ring/camera.py | 66 ++++------- homeassistant/components/ring/config_flow.py | 105 ++++++++++++++++ homeassistant/components/ring/light.py | 15 ++- homeassistant/components/ring/manifest.json | 3 +- homeassistant/components/ring/sensor.py | 44 +++---- homeassistant/components/ring/strings.json | 27 +++++ homeassistant/components/ring/switch.py | 15 ++- homeassistant/generated/config_flows.py | 1 + tests/components/ring/common.py | 13 +- tests/components/ring/conftest.py | 4 + tests/components/ring/test_binary_sensor.py | 22 +++- tests/components/ring/test_config_flow.py | 58 +++++++++ tests/components/ring/test_init.py | 6 +- tests/components/ring/test_sensor.py | 23 +++- 17 files changed, 435 insertions(+), 146 deletions(-) create mode 100644 homeassistant/components/ring/.translations/en.json create mode 100644 homeassistant/components/ring/config_flow.py create mode 100644 homeassistant/components/ring/strings.json create mode 100644 tests/components/ring/test_config_flow.py diff --git a/homeassistant/components/ring/.translations/en.json b/homeassistant/components/ring/.translations/en.json new file mode 100644 index 00000000000..db4665b6c0a --- /dev/null +++ b/homeassistant/components/ring/.translations/en.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "2fa": { + "data": { + "2fa": "Two-factor code" + }, + "title": "Enter two-factor authentication" + }, + "user": { + "data": { + "password": "Password", + "username": "Username" + }, + "title": "Connect to the device" + } + }, + "title": "Ring" + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index a68749b2c67..18c753f4dc9 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -1,12 +1,16 @@ """Support for Ring Doorbell/Chimes.""" +import asyncio from datetime import timedelta +from functools import partial import logging +from pathlib import Path from requests.exceptions import ConnectTimeout, HTTPError from ring_doorbell import Ring import voluptuous as vol -from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import track_time_interval @@ -21,6 +25,7 @@ NOTIFICATION_TITLE = "Ring Setup" DATA_RING_DOORBELLS = "ring_doorbells" DATA_RING_STICKUP_CAMS = "ring_stickup_cams" DATA_RING_CHIMES = "ring_chimes" +DATA_TRACK_INTERVAL = "ring_track_interval" DOMAIN = "ring" DEFAULT_CACHEDB = ".ring_cache.pickle" @@ -29,13 +34,14 @@ SIGNAL_UPDATE_RING = "ring_update" SCAN_INTERVAL = timedelta(seconds=10) +PLATFORMS = ("binary_sensor", "light", "sensor", "switch", "camera") + CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( + vol.Optional(DOMAIN): vol.Schema( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, } ) }, @@ -43,27 +49,39 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass, config): +async def async_setup(hass, config): """Set up the Ring component.""" - conf = config[DOMAIN] - username = conf[CONF_USERNAME] - password = conf[CONF_PASSWORD] - scan_interval = conf[CONF_SCAN_INTERVAL] + if DOMAIN not in config: + return True + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "username": config[DOMAIN]["username"], + "password": config[DOMAIN]["password"], + }, + ) + ) + return True + + +async def async_setup_entry(hass, entry): + """Set up a config entry.""" + cache = hass.config.path(DEFAULT_CACHEDB) try: - cache = hass.config.path(DEFAULT_CACHEDB) - ring = Ring(username=username, password=password, cache_file=cache) - if not ring.is_connected: - return False - hass.data[DATA_RING_CHIMES] = chimes = ring.chimes - hass.data[DATA_RING_DOORBELLS] = doorbells = ring.doorbells - hass.data[DATA_RING_STICKUP_CAMS] = stickup_cams = ring.stickup_cams - - ring_devices = chimes + doorbells + stickup_cams - + ring = await hass.async_add_executor_job( + partial( + Ring, + username=entry.data["username"], + password="invalid-password", + cache_file=cache, + ) + ) except (ConnectTimeout, HTTPError) as ex: _LOGGER.error("Unable to connect to Ring service: %s", str(ex)) - hass.components.persistent_notification.create( + hass.components.persistent_notification.async_create( "Error: {}
" "You will need to restart hass after fixing." "".format(ex), @@ -72,6 +90,28 @@ def setup(hass, config): ) return False + if not ring.is_connected: + _LOGGER.error("Unable to connect to Ring service") + return False + + await hass.async_add_executor_job(finish_setup_entry, hass, ring) + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +def finish_setup_entry(hass, ring): + """Finish setting up entry.""" + hass.data[DATA_RING_CHIMES] = chimes = ring.chimes + hass.data[DATA_RING_DOORBELLS] = doorbells = ring.doorbells + hass.data[DATA_RING_STICKUP_CAMS] = stickup_cams = ring.stickup_cams + + ring_devices = chimes + doorbells + stickup_cams + def service_hub_refresh(service): hub_refresh() @@ -92,6 +132,36 @@ def setup(hass, config): hass.services.register(DOMAIN, "update", service_hub_refresh) # register scan interval for ring - track_time_interval(hass, timer_hub_refresh, scan_interval) + hass.data[DATA_TRACK_INTERVAL] = track_time_interval( + hass, timer_hub_refresh, SCAN_INTERVAL + ) - return True + +async def async_unload_entry(hass, entry): + """Unload Ring entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if not unload_ok: + return False + + await hass.async_add_executor_job(hass.data[DATA_TRACK_INTERVAL]) + + hass.services.async_remove(DOMAIN, "update") + + hass.data.pop(DATA_RING_DOORBELLS) + hass.data.pop(DATA_RING_STICKUP_CAMS) + hass.data.pop(DATA_RING_CHIMES) + hass.data.pop(DATA_TRACK_INTERVAL) + + return unload_ok + + +async def async_remove_entry(hass, entry): + """Act when an entry is removed.""" + await hass.async_add_executor_job(Path(hass.config.path(DEFAULT_CACHEDB)).unlink) diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 86d26ec25b4..0706752ffb2 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -2,22 +2,10 @@ from datetime import timedelta import logging -import voluptuous as vol +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.const import ATTR_ATTRIBUTION -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_ENTITY_NAMESPACE, - CONF_MONITORED_CONDITIONS, -) -import homeassistant.helpers.config_validation as cv - -from . import ( - ATTRIBUTION, - DATA_RING_DOORBELLS, - DATA_RING_STICKUP_CAMS, - DEFAULT_ENTITY_NAMESPACE, -) +from . import ATTRIBUTION, DATA_RING_DOORBELLS, DATA_RING_STICKUP_CAMS _LOGGER = logging.getLogger(__name__) @@ -29,35 +17,24 @@ SENSOR_TYPES = { "motion": ["Motion", ["doorbell", "stickup_cams"], "motion"], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional( - CONF_ENTITY_NAMESPACE, default=DEFAULT_ENTITY_NAMESPACE - ): cv.string, - vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - } -) - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up a sensor for a Ring device.""" +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Ring binary sensors from a config entry.""" ring_doorbells = hass.data[DATA_RING_DOORBELLS] ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS] sensors = [] for device in ring_doorbells: # ring.doorbells is doing I/O - for sensor_type in config[CONF_MONITORED_CONDITIONS]: + for sensor_type in SENSOR_TYPES: if "doorbell" in SENSOR_TYPES[sensor_type][1]: sensors.append(RingBinarySensor(hass, device, sensor_type)) for device in ring_stickup_cams: # ring.stickup_cams is doing I/O - for sensor_type in config[CONF_MONITORED_CONDITIONS]: + for sensor_type in SENSOR_TYPES: if "stickup_cams" in SENSOR_TYPES[sensor_type][1]: sensors.append(RingBinarySensor(hass, device, sensor_type)) - add_entities(sensors, True) + async_add_entities(sensors, True) class RingBinarySensor(BinarySensorDevice): diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 1d2fe6ff67b..a3b34afa056 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -5,13 +5,11 @@ import logging from haffmpeg.camera import CameraMjpeg from haffmpeg.tools import IMAGE_JPEG, ImageFrame -import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.camera import Camera from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import dt as dt_util @@ -20,77 +18,57 @@ from . import ( ATTRIBUTION, DATA_RING_DOORBELLS, DATA_RING_STICKUP_CAMS, - NOTIFICATION_ID, SIGNAL_UPDATE_RING, ) -CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" - FORCE_REFRESH_INTERVAL = timedelta(minutes=45) _LOGGER = logging.getLogger(__name__) -NOTIFICATION_TITLE = "Ring Camera Setup" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string} -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a Ring Door Bell and StickUp Camera.""" ring_doorbell = hass.data[DATA_RING_DOORBELLS] ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS] cams = [] - cams_no_plan = [] for camera in ring_doorbell + ring_stickup_cams: - if camera.has_subscription: - cams.append(RingCam(hass, camera, config)) - else: - cams_no_plan.append(camera) + if not camera.has_subscription: + continue - # show notification for all cameras without an active subscription - if cams_no_plan: - cameras = str(", ".join([camera.name for camera in cams_no_plan])) + camera = await hass.async_add_executor_job(RingCam, hass, camera) + cams.append(camera) - err_msg = ( - """A Ring Protect Plan is required for the""" - """ following cameras: {}.""".format(cameras) - ) - - _LOGGER.error(err_msg) - hass.components.persistent_notification.create( - "Error: {}
" - "You will need to restart hass after fixing." - "".format(err_msg), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - - add_entities(cams, True) - return True + async_add_entities(cams, True) class RingCam(Camera): """An implementation of a Ring Door Bell camera.""" - def __init__(self, hass, camera, device_info): + def __init__(self, hass, camera): """Initialize a Ring Door Bell camera.""" super().__init__() self._camera = camera self._hass = hass self._name = self._camera.name self._ffmpeg = hass.data[DATA_FFMPEG] - self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS) self._last_video_id = self._camera.last_recording_id self._video_url = self._camera.recording_url(self._last_video_id) self._utcnow = dt_util.utcnow() self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow + self._disp_disconnect = None async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect(self.hass, SIGNAL_UPDATE_RING, self._update_callback) + self._disp_disconnect = async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_RING, self._update_callback + ) + + async def async_will_remove_from_hass(self): + """Disconnect callbacks.""" + if self._disp_disconnect: + self._disp_disconnect() + self._disp_disconnect = None @callback def _update_callback(self): @@ -131,11 +109,7 @@ class RingCam(Camera): return image = await asyncio.shield( - ffmpeg.get_image( - self._video_url, - output_format=IMAGE_JPEG, - extra_cmd=self._ffmpeg_arguments, - ) + ffmpeg.get_image(self._video_url, output_format=IMAGE_JPEG,) ) return image @@ -146,7 +120,7 @@ class RingCam(Camera): return stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) - await stream.open_camera(self._video_url, extra_cmd=self._ffmpeg_arguments) + await stream.open_camera(self._video_url) try: stream_reader = await stream.get_reader() diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py new file mode 100644 index 00000000000..bdb60cc26c5 --- /dev/null +++ b/homeassistant/components/ring/config_flow.py @@ -0,0 +1,105 @@ +"""Config flow for Ring integration.""" +from functools import partial +import logging + +from oauthlib.oauth2 import AccessDeniedError +from ring_doorbell import Ring +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions + +from . import DEFAULT_CACHEDB, DOMAIN # pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect.""" + cache = hass.config.path(DEFAULT_CACHEDB) + + def otp_callback(): + if "2fa" in data: + return data["2fa"] + + raise Require2FA + + try: + ring = await hass.async_add_executor_job( + partial( + Ring, + username=data["username"], + password=data["password"], + cache_file=cache, + auth_callback=otp_callback, + ) + ) + except AccessDeniedError: + raise InvalidAuth + + if not ring.is_connected: + raise InvalidAuth + + +class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Ring.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + user_pass = None + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + if self._async_current_entries(): + return self.async_abort(reason="already_configured") + + errors = {} + if user_input is not None: + try: + await validate_input(self.hass, user_input) + await self.async_set_unique_id(user_input["username"]) + + return self.async_create_entry( + title=user_input["username"], + data={"username": user_input["username"]}, + ) + except Require2FA: + self.user_pass = user_input + + return await self.async_step_2fa() + + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({"username": str, "password": str}), + errors=errors, + ) + + async def async_step_2fa(self, user_input=None): + """Handle 2fa step.""" + if user_input: + return await self.async_step_user({**self.user_pass, **user_input}) + + return self.async_show_form( + step_id="2fa", data_schema=vol.Schema({"2fa": str}), + ) + + async def async_step_import(self, user_input): + """Handle import.""" + if self._async_current_entries(): + return self.async_abort(reason="already_configured") + + return await self.async_step_user(user_input) + + +class Require2FA(exceptions.HomeAssistantError): + """Error to indicate we require 2FA.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index fe048731352..1b360f24f1f 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -23,7 +23,7 @@ ON_STATE = "on" OFF_STATE = "off" -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Create the lights for the Ring devices.""" cameras = hass.data[DATA_RING_STICKUP_CAMS] lights = [] @@ -32,7 +32,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if device.has_capability("light"): lights.append(RingLight(device)) - add_entities(lights, True) + async_add_entities(lights, True) class RingLight(Light): @@ -44,10 +44,19 @@ class RingLight(Light): self._unique_id = self._device.id self._light_on = False self._no_updates_until = dt_util.utcnow() + self._disp_disconnect = None async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect(self.hass, SIGNAL_UPDATE_RING, self._update_callback) + self._disp_disconnect = async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_RING, self._update_callback + ) + + async def async_will_remove_from_hass(self): + """Disconnect callbacks.""" + if self._disp_disconnect: + self._disp_disconnect() + self._disp_disconnect = None @callback def _update_callback(self): diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 124df7d162b..b8a3c26bd8b 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/ring", "requirements": ["ring_doorbell==0.2.9"], "dependencies": ["ffmpeg"], - "codeowners": [] + "codeowners": [], + "config_flow": true } diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index b54c750664e..532f15f94c1 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -1,16 +1,8 @@ """This component provides HA sensor support for Ring Door Bell/Chimes.""" import logging -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_ENTITY_NAMESPACE, - CONF_MONITORED_CONDITIONS, -) +from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level @@ -20,7 +12,6 @@ from . import ( DATA_RING_CHIMES, DATA_RING_DOORBELLS, DATA_RING_STICKUP_CAMS, - DEFAULT_ENTITY_NAMESPACE, SIGNAL_UPDATE_RING, ) @@ -67,19 +58,8 @@ SENSOR_TYPES = { ], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional( - CONF_ENTITY_NAMESPACE, default=DEFAULT_ENTITY_NAMESPACE - ): cv.string, - vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - } -) - -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a sensor for a Ring device.""" ring_chimes = hass.data[DATA_RING_CHIMES] ring_doorbells = hass.data[DATA_RING_DOORBELLS] @@ -87,22 +67,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors = [] for device in ring_chimes: - for sensor_type in config[CONF_MONITORED_CONDITIONS]: + for sensor_type in SENSOR_TYPES: if "chime" in SENSOR_TYPES[sensor_type][1]: sensors.append(RingSensor(hass, device, sensor_type)) for device in ring_doorbells: - for sensor_type in config[CONF_MONITORED_CONDITIONS]: + for sensor_type in SENSOR_TYPES: if "doorbell" in SENSOR_TYPES[sensor_type][1]: sensors.append(RingSensor(hass, device, sensor_type)) for device in ring_stickup_cams: - for sensor_type in config[CONF_MONITORED_CONDITIONS]: + for sensor_type in SENSOR_TYPES: if "stickup_cams" in SENSOR_TYPES[sensor_type][1]: sensors.append(RingSensor(hass, device, sensor_type)) - add_entities(sensors, True) - return True + async_add_entities(sensors, True) class RingSensor(Entity): @@ -122,10 +101,19 @@ class RingSensor(Entity): self._state = None self._tz = str(hass.config.time_zone) self._unique_id = f"{self._data.id}-{self._sensor_type}" + self._disp_disconnect = None async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect(self.hass, SIGNAL_UPDATE_RING, self._update_callback) + self._disp_disconnect = async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_RING, self._update_callback + ) + + async def async_will_remove_from_hass(self): + """Disconnect callbacks.""" + if self._disp_disconnect: + self._disp_disconnect() + self._disp_disconnect = None @callback def _update_callback(self): diff --git a/homeassistant/components/ring/strings.json b/homeassistant/components/ring/strings.json new file mode 100644 index 00000000000..6dff7c00ba6 --- /dev/null +++ b/homeassistant/components/ring/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "title": "Ring", + "step": { + "user": { + "title": "Sign-in with Ring account", + "data": { + "username": "Username", + "password": "Password" + } + }, + "2fa": { + "title": "Two-factor authentication", + "data": { + "2fa": "Two-factor code" + } + } + }, + "error": { + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "Device is already configured" + } + } +} diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index 86f5c65d87c..51c9e64377b 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -22,7 +22,7 @@ SIREN_ICON = "mdi:alarm-bell" SKIP_UPDATES_DELAY = timedelta(seconds=5) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Create the switches for the Ring devices.""" cameras = hass.data[DATA_RING_STICKUP_CAMS] switches = [] @@ -30,7 +30,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if device.has_capability("siren"): switches.append(SirenSwitch(device)) - add_entities(switches, True) + async_add_entities(switches, True) class BaseRingSwitch(SwitchDevice): @@ -41,10 +41,19 @@ class BaseRingSwitch(SwitchDevice): self._device = device self._device_type = device_type self._unique_id = f"{self._device.id}-{self._device_type}" + self._disp_disconnect = None async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect(self.hass, SIGNAL_UPDATE_RING, self._update_callback) + self._disp_disconnect = async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_RING, self._update_callback + ) + + async def async_will_remove_from_hass(self): + """Disconnect callbacks.""" + if self._disp_disconnect: + self._disp_disconnect() + self._disp_disconnect = None @callback def _update_callback(self): diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 6f3f0e714f6..dcae6fd065e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -66,6 +66,7 @@ FLOWS = [ "point", "ps4", "rainmachine", + "ring", "sentry", "simplisafe", "smartthings", diff --git a/tests/components/ring/common.py b/tests/components/ring/common.py index e5042a935d6..1afc597415e 100644 --- a/tests/components/ring/common.py +++ b/tests/components/ring/common.py @@ -1,14 +1,15 @@ """Common methods used across the tests for ring devices.""" +from unittest.mock import patch + from homeassistant.components.ring import DOMAIN -from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry + async def setup_platform(hass, platform): """Set up the ring platform and prerequisites.""" - config = { - DOMAIN: {CONF_USERNAME: "foo", CONF_PASSWORD: "bar", CONF_SCAN_INTERVAL: 1000}, - platform: {"platform": DOMAIN}, - } - assert await async_setup_component(hass, platform, config) + MockConfigEntry(domain=DOMAIN, data={"username": "foo"}).add_to_hass(hass) + with patch("homeassistant.components.ring.PLATFORMS", [platform]): + assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() diff --git a/tests/components/ring/conftest.py b/tests/components/ring/conftest.py index b61840769a2..e4b516496e7 100644 --- a/tests/components/ring/conftest.py +++ b/tests/components/ring/conftest.py @@ -36,6 +36,10 @@ def requests_mock_fixture(ring_mock): "https://api.ring.com/clients_api/ring_devices", text=load_fixture("ring_devices.json"), ) + mock.get( + "https://api.ring.com/clients_api/dings/active", + text=load_fixture("ring_ding_active.json"), + ) # Mocks the response for getting the history of a device mock.get( "https://api.ring.com/clients_api/doorbots/987652/history", diff --git a/tests/components/ring/test_binary_sensor.py b/tests/components/ring/test_binary_sensor.py index c0b538b8eff..5a04017f54b 100644 --- a/tests/components/ring/test_binary_sensor.py +++ b/tests/components/ring/test_binary_sensor.py @@ -1,13 +1,20 @@ """The tests for the Ring binary sensor platform.""" +from asyncio import run_coroutine_threadsafe import os import unittest +from unittest.mock import patch import requests_mock from homeassistant.components import ring as base_ring from homeassistant.components.ring import binary_sensor as ring -from tests.common import get_test_config_dir, get_test_home_assistant, load_fixture +from tests.common import ( + get_test_config_dir, + get_test_home_assistant, + load_fixture, + mock_storage, +) from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG @@ -68,8 +75,17 @@ class TestRingBinarySensorSetup(unittest.TestCase): text=load_fixture("ring_chime_health_attrs.json"), ) - base_ring.setup(self.hass, VALID_CONFIG) - ring.setup_platform(self.hass, self.config, self.add_entities, None) + with mock_storage(), patch("homeassistant.components.ring.PLATFORMS", []): + run_coroutine_threadsafe( + base_ring.async_setup(self.hass, VALID_CONFIG), self.hass.loop + ).result() + run_coroutine_threadsafe( + self.hass.async_block_till_done(), self.hass.loop + ).result() + run_coroutine_threadsafe( + ring.async_setup_entry(self.hass, None, self.add_entities), + self.hass.loop, + ).result() for device in self.DEVICES: device.update() diff --git a/tests/components/ring/test_config_flow.py b/tests/components/ring/test_config_flow.py new file mode 100644 index 00000000000..46925069c31 --- /dev/null +++ b/tests/components/ring/test_config_flow.py @@ -0,0 +1,58 @@ +"""Test the Ring config flow.""" +from unittest.mock import Mock, patch + +from homeassistant import config_entries, setup +from homeassistant.components.ring import DOMAIN +from homeassistant.components.ring.config_flow import InvalidAuth + +from tests.common import mock_coro + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.ring.config_flow.Ring", + return_value=Mock(is_connected=True), + ), patch( + "homeassistant.components.ring.async_setup", return_value=mock_coro(True) + ) as mock_setup, patch( + "homeassistant.components.ring.async_setup_entry", return_value=mock_coro(True), + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "hello@home-assistant.io", "password": "test-password"}, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "hello@home-assistant.io" + assert result2["data"] == { + "username": "hello@home-assistant.io", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.ring.config_flow.Ring", side_effect=InvalidAuth, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "hello@home-assistant.io", "password": "test-password"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} diff --git a/tests/components/ring/test_init.py b/tests/components/ring/test_init.py index 4d3fede89a9..cfc19da78bf 100644 --- a/tests/components/ring/test_init.py +++ b/tests/components/ring/test_init.py @@ -1,4 +1,5 @@ """The tests for the Ring component.""" +from asyncio import run_coroutine_threadsafe from copy import deepcopy from datetime import timedelta import os @@ -59,7 +60,10 @@ class TestRing(unittest.TestCase): "https://api.ring.com/clients_api/doorbots/987652/health", text=load_fixture("ring_doorboot_health_attrs.json"), ) - response = ring.setup(self.hass, self.config) + response = run_coroutine_threadsafe( + ring.async_setup(self.hass, self.config), self.hass.loop + ).result() + assert response @requests_mock.Mocker() diff --git a/tests/components/ring/test_sensor.py b/tests/components/ring/test_sensor.py index dd9d36f80a1..0102020e3c2 100644 --- a/tests/components/ring/test_sensor.py +++ b/tests/components/ring/test_sensor.py @@ -1,6 +1,8 @@ """The tests for the Ring sensor platform.""" +from asyncio import run_coroutine_threadsafe import os import unittest +from unittest.mock import patch import requests_mock @@ -8,7 +10,12 @@ from homeassistant.components import ring as base_ring import homeassistant.components.ring.sensor as ring from homeassistant.helpers.icon import icon_for_battery_level -from tests.common import get_test_config_dir, get_test_home_assistant, load_fixture +from tests.common import ( + get_test_config_dir, + get_test_home_assistant, + load_fixture, + mock_storage, +) from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG @@ -76,8 +83,18 @@ class TestRingSensorSetup(unittest.TestCase): "https://api.ring.com/clients_api/chimes/999999/health", text=load_fixture("ring_chime_health_attrs.json"), ) - base_ring.setup(self.hass, VALID_CONFIG) - ring.setup_platform(self.hass, self.config, self.add_entities, None) + + with mock_storage(), patch("homeassistant.components.ring.PLATFORMS", []): + run_coroutine_threadsafe( + base_ring.async_setup(self.hass, VALID_CONFIG), self.hass.loop + ).result() + run_coroutine_threadsafe( + self.hass.async_block_till_done(), self.hass.loop + ).result() + run_coroutine_threadsafe( + ring.async_setup_entry(self.hass, None, self.add_entities), + self.hass.loop, + ).result() for device in self.DEVICES: device.update() From 5a8311e9d83df010c581da428e96bec7c5e74744 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 8 Jan 2020 22:33:51 +0100 Subject: [PATCH 05/41] Fix problem with restoring POE control (#30597) --- homeassistant/components/unifi/switch.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 5b64f573ccd..b1f62131eb4 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -42,12 +42,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): _, mac = entity.unique_id.split("-", 1) - if mac in controller.api.clients or mac not in controller.api.clients_all: + if mac in controller.api.clients: + switches_off.append(entity.unique_id) continue - client = controller.api.clients_all[mac] - controller.api.clients.process_raw([client.raw]) - switches_off.append(entity.unique_id) + if mac in controller.api.clients_all: + client = controller.api.clients_all[mac] + controller.api.clients.process_raw([client.raw]) + switches_off.append(entity.unique_id) + continue @callback def update_controller(): From a6492cc7bbf7270084c7dc88e4e8ffa7d6c8d964 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 9 Jan 2020 11:09:34 +0100 Subject: [PATCH 06/41] Set body size for Proxy / streams to 16mb (#30608) --- homeassistant/components/http/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index c720d134c9f..0d93461f90f 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -54,6 +54,8 @@ DEFAULT_DEVELOPMENT = "0" DEFAULT_CORS = "https://cast.home-assistant.io" NO_LOGIN_ATTEMPT_THRESHOLD = -1 +MAX_CLIENT_SIZE: int = 1024 ** 2 * 16 + HTTP_SCHEMA = vol.Schema( { @@ -188,7 +190,9 @@ class HomeAssistantHTTP: ssl_profile, ): """Initialize the HTTP Home Assistant server.""" - app = self.app = web.Application(middlewares=[]) + app = self.app = web.Application( + middlewares=[], client_max_size=MAX_CLIENT_SIZE + ) app[KEY_HASS] = hass # This order matters From c785b791a2f07573bc5b64a3f38b570fc1278eb3 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 10 Jan 2020 19:56:41 +0100 Subject: [PATCH 07/41] Bump Adafruit_BBIO to 1.1.1 (#30630) --- homeassistant/components/bbb_gpio/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bbb_gpio/manifest.json b/homeassistant/components/bbb_gpio/manifest.json index e919e0c66bf..42670d510da 100644 --- a/homeassistant/components/bbb_gpio/manifest.json +++ b/homeassistant/components/bbb_gpio/manifest.json @@ -2,7 +2,7 @@ "domain": "bbb_gpio", "name": "BeagleBone Black GPIO", "documentation": "https://www.home-assistant.io/integrations/bbb_gpio", - "requirements": ["Adafruit_BBIO==1.0.0"], + "requirements": ["Adafruit_BBIO==1.1.1"], "dependencies": [], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index bd9a5bdf66e..d7cceb5d8b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -31,7 +31,7 @@ Adafruit-GPIO==1.0.3 Adafruit-SHT31==1.0.2 # homeassistant.components.bbb_gpio -# Adafruit_BBIO==1.0.0 +# Adafruit_BBIO==1.1.1 # homeassistant.components.homekit HAP-python==2.6.0 From 626c01b4a0915c461dd49f15d68a39ac0bce2ce6 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Fri, 10 Jan 2020 12:55:39 -0600 Subject: [PATCH 08/41] Do not save last_seen if older than prev_seen (#30647) Also add warnings when updates skipped similar to google_maps --- .../components/life360/device_tracker.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py index b2ba1ca3164..b7b0415a1b3 100644 --- a/homeassistant/components/life360/device_tracker.py +++ b/homeassistant/components/life360/device_tracker.py @@ -195,7 +195,10 @@ class Life360Scanner: ) reported = False - self._dev_data[dev_id] = last_seen or prev_seen, reported + # Don't remember last_seen unless it's really an update. + if not last_seen or prev_seen and last_seen <= prev_seen: + last_seen = prev_seen + self._dev_data[dev_id] = last_seen, reported return prev_seen @@ -218,7 +221,17 @@ class Life360Scanner: return # Only update when we truly have an update. - if not last_seen or prev_seen and last_seen <= prev_seen: + if not last_seen: + _LOGGER.warning("%s: Ignoring update because timestamp is missing", dev_id) + return + if prev_seen and last_seen < prev_seen: + _LOGGER.warning( + "%s: Ignoring update because timestamp is older than last timestamp", + dev_id, + ) + _LOGGER.debug("%s < %s", last_seen, prev_seen) + return + if last_seen == prev_seen: return lat = loc.get("latitude") From f505310f1182a1a0d58336754fd3a90a2ea78a71 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 10 Jan 2020 13:37:13 -0800 Subject: [PATCH 09/41] Bumped version to 0.104.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c7131c41d56..a5ad2e4991b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 104 -PATCH_VERSION = "0b0" +PATCH_VERSION = "0b1" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From 449e6841a7b059bd66fc70ea28d605c48ba7bd47 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sat, 11 Jan 2020 00:33:48 +0000 Subject: [PATCH 10/41] Fix HomeKit with entity registry restoration where supported_features is a non-None falsey (#30657) * Fix homekit with #30094 * Fix test --- homeassistant/helpers/entity_registry.py | 6 +- tests/components/homekit/test_type_covers.py | 73 +++++++++++++++ tests/components/homekit/test_type_fans.py | 40 ++++++++ tests/components/homekit/test_type_lights.py | 36 ++++++++ .../homekit/test_type_media_players.py | 56 ++++++++++++ .../homekit/test_type_thermostats.py | 91 +++++++++++++++++++ tests/helpers/test_entity_registry.py | 2 +- 7 files changed, 300 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 2673162a841..bac406174f4 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -490,13 +490,13 @@ def async_setup_entity_restore( attrs: Dict[str, Any] = {ATTR_RESTORED: True} - if entry.capabilities: + if entry.capabilities is not None: attrs.update(entry.capabilities) - if entry.supported_features: + if entry.supported_features is not None: attrs[ATTR_SUPPORTED_FEATURES] = entry.supported_features - if entry.device_class: + if entry.device_class is not None: attrs[ATTR_DEVICE_CLASS] = entry.device_class states.async_set(entry.entity_id, STATE_UNAVAILABLE, attrs) diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 7bf92b28de2..fb73c132e30 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -13,6 +13,7 @@ from homeassistant.components.homekit.const import ATTR_VALUE from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + EVENT_HOMEASSISTANT_START, STATE_CLOSED, STATE_CLOSING, STATE_OPEN, @@ -20,6 +21,8 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) +from homeassistant.core import CoreState +from homeassistant.helpers import entity_registry from tests.common import async_mock_service from tests.components.homekit.common import patch_debounce @@ -308,3 +311,73 @@ async def test_window_open_close_stop(hass, hk_driver, cls, events): assert acc.char_position_state.value == 2 assert len(events) == 3 assert events[-1].data[ATTR_VALUE] is None + + +async def test_window_basic_restore(hass, hk_driver, cls, events): + """Test setting up an entity from state in the event registry.""" + hass.state = CoreState.not_running + + registry = await entity_registry.async_get_registry(hass) + + registry.async_get_or_create( + "cover", "generic", "1234", suggested_object_id="simple", + ) + registry.async_get_or_create( + "cover", + "generic", + "9012", + suggested_object_id="all_info_set", + capabilities={}, + supported_features=SUPPORT_STOP, + device_class="mock-device-class", + ) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) + await hass.async_block_till_done() + + acc = cls.window_basic(hass, hk_driver, "Cover", "cover.simple", 2, None) + assert acc.category == 14 + assert acc.char_current_position is not None + assert acc.char_target_position is not None + assert acc.char_position_state is not None + + acc = cls.window_basic(hass, hk_driver, "Cover", "cover.all_info_set", 2, None) + assert acc.category == 14 + assert acc.char_current_position is not None + assert acc.char_target_position is not None + assert acc.char_position_state is not None + + +async def test_window_restore(hass, hk_driver, cls, events): + """Test setting up an entity from state in the event registry.""" + hass.state = CoreState.not_running + + registry = await entity_registry.async_get_registry(hass) + + registry.async_get_or_create( + "cover", "generic", "1234", suggested_object_id="simple", + ) + registry.async_get_or_create( + "cover", + "generic", + "9012", + suggested_object_id="all_info_set", + capabilities={}, + supported_features=SUPPORT_STOP, + device_class="mock-device-class", + ) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) + await hass.async_block_till_done() + + acc = cls.window(hass, hk_driver, "Cover", "cover.simple", 2, None) + assert acc.category == 14 + assert acc.char_current_position is not None + assert acc.char_target_position is not None + assert acc.char_position_state is not None + + acc = cls.window(hass, hk_driver, "Cover", "cover.all_info_set", 2, None) + assert acc.category == 14 + assert acc.char_current_position is not None + assert acc.char_target_position is not None + assert acc.char_position_state is not None diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index 5631791d7a2..9bcca3cc452 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -24,10 +24,13 @@ from homeassistant.components.homekit.util import HomeKitSpeedMapping from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + EVENT_HOMEASSISTANT_START, STATE_OFF, STATE_ON, STATE_UNKNOWN, ) +from homeassistant.core import CoreState +from homeassistant.helpers import entity_registry from tests.common import async_mock_service from tests.components.homekit.common import patch_debounce @@ -226,3 +229,40 @@ async def test_fan_speed(hass, hk_driver, cls, events): assert call_set_speed[0].data[ATTR_SPEED] == "ludicrous" assert len(events) == 1 assert events[-1].data[ATTR_VALUE] == "ludicrous" + + +async def test_fan_restore(hass, hk_driver, cls, events): + """Test setting up an entity from state in the event registry.""" + hass.state = CoreState.not_running + + registry = await entity_registry.async_get_registry(hass) + + registry.async_get_or_create( + "fan", "generic", "1234", suggested_object_id="simple", + ) + registry.async_get_or_create( + "fan", + "generic", + "9012", + suggested_object_id="all_info_set", + capabilities={"speed_list": ["off", "low", "medium", "high"]}, + supported_features=SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION, + device_class="mock-device-class", + ) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) + await hass.async_block_till_done() + + acc = cls.fan(hass, hk_driver, "Fan", "fan.simple", 2, None) + assert acc.category == 3 + assert acc.char_active is not None + assert acc.char_direction is None + assert acc.char_speed is None + assert acc.char_swing is None + + acc = cls.fan(hass, hk_driver, "Fan", "fan.all_info_set", 2, None) + assert acc.category == 3 + assert acc.char_active is not None + assert acc.char_direction is not None + assert acc.char_speed is not None + assert acc.char_swing is not None diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 510cfa4f666..c1811a2e2fc 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -17,10 +17,13 @@ from homeassistant.components.light import ( from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + EVENT_HOMEASSISTANT_START, STATE_OFF, STATE_ON, STATE_UNKNOWN, ) +from homeassistant.core import CoreState +from homeassistant.helpers import entity_registry from tests.common import async_mock_service from tests.components.homekit.common import patch_debounce @@ -205,3 +208,36 @@ async def test_light_rgb_color(hass, hk_driver, cls, events): assert call_turn_on[0].data[ATTR_HS_COLOR] == (145, 75) assert len(events) == 1 assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)" + + +async def test_light_restore(hass, hk_driver, cls, events): + """Test setting up an entity from state in the event registry.""" + hass.state = CoreState.not_running + + registry = await entity_registry.async_get_registry(hass) + + registry.async_get_or_create( + "light", "hue", "1234", suggested_object_id="simple", + ) + registry.async_get_or_create( + "light", + "hue", + "9012", + suggested_object_id="all_info_set", + capabilities={"max": 100}, + supported_features=5, + device_class="mock-device-class", + ) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) + await hass.async_block_till_done() + + acc = cls.light(hass, hk_driver, "Light", "light.simple", 2, None) + assert acc.category == 5 # Lightbulb + assert acc.chars == [] + assert acc.char_on.value == 0 + + acc = cls.light(hass, hk_driver, "Light", "light.all_info_set", 2, None) + assert acc.category == 5 # Lightbulb + assert acc.chars == ["Brightness"] + assert acc.char_on.value == 0 diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index aa007b4d04c..366617ee988 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -24,12 +24,15 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + EVENT_HOMEASSISTANT_START, STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING, ) +from homeassistant.core import CoreState +from homeassistant.helpers import entity_registry from tests.common import async_mock_service @@ -336,3 +339,56 @@ async def test_media_player_television_basic(hass, hk_driver, events, caplog): assert acc.char_active.value == 1 assert not caplog.messages or "Error" not in caplog.messages[-1] + + +async def test_tv_restore(hass, hk_driver, events): + """Test setting up an entity from state in the event registry.""" + hass.state = CoreState.not_running + + registry = await entity_registry.async_get_registry(hass) + + registry.async_get_or_create( + "media_player", + "generic", + "1234", + suggested_object_id="simple", + device_class=DEVICE_CLASS_TV, + ) + registry.async_get_or_create( + "media_player", + "generic", + "9012", + suggested_object_id="all_info_set", + capabilities={ + ATTR_INPUT_SOURCE_LIST: ["HDMI 1", "HDMI 2", "HDMI 3", "HDMI 4"], + }, + supported_features=3469, + device_class=DEVICE_CLASS_TV, + ) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) + await hass.async_block_till_done() + + acc = TelevisionMediaPlayer( + hass, hk_driver, "MediaPlayer", "media_player.simple", 2, None + ) + assert acc.category == 31 + assert acc.chars_tv == [] + assert acc.chars_speaker == [] + assert acc.support_select_source is False + assert not hasattr(acc, "char_input_source") + + acc = TelevisionMediaPlayer( + hass, hk_driver, "MediaPlayer", "media_player.all_info_set", 2, None + ) + assert acc.category == 31 + assert acc.chars_tv == ["RemoteKey"] + assert acc.chars_speaker == [ + "Name", + "Active", + "VolumeControlType", + "VolumeSelector", + "Volume", + ] + assert acc.support_select_source is True + assert acc.char_input_source is not None diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 174b72f780a..c96cfdae602 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -41,8 +41,11 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, CONF_TEMPERATURE_UNIT, + EVENT_HOMEASSISTANT_START, TEMP_FAHRENHEIT, ) +from homeassistant.core import CoreState +from homeassistant.helpers import entity_registry from tests.common import async_mock_service from tests.components.homekit.common import patch_debounce @@ -517,6 +520,51 @@ async def test_thermostat_temperature_step_whole(hass, hk_driver, cls): assert acc.char_target_temp.properties[PROP_MIN_STEP] == 1.0 +async def test_thermostat_restore(hass, hk_driver, cls, events): + """Test setting up an entity from state in the event registry.""" + hass.state = CoreState.not_running + + registry = await entity_registry.async_get_registry(hass) + + registry.async_get_or_create( + "climate", "generic", "1234", suggested_object_id="simple", + ) + registry.async_get_or_create( + "climate", + "generic", + "9012", + suggested_object_id="all_info_set", + capabilities={ + ATTR_MIN_TEMP: 60, + ATTR_MAX_TEMP: 70, + ATTR_HVAC_MODES: [HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF], + }, + supported_features=0, + device_class="mock-device-class", + ) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) + await hass.async_block_till_done() + + acc = cls.thermostat(hass, hk_driver, "Climate", "climate.simple", 2, None) + assert acc.category == 9 + assert acc.get_temperature_range() == (7, 35) + assert set(acc.char_target_heat_cool.properties["ValidValues"].keys()) == { + "cool", + "heat", + "heat_cool", + "off", + } + + acc = cls.thermostat(hass, hk_driver, "Climate", "climate.all_info_set", 2, None) + assert acc.category == 9 + assert acc.get_temperature_range() == (60.0, 70.0) + assert set(acc.char_target_heat_cool.properties["ValidValues"].keys()) == { + "heat_cool", + "off", + } + + async def test_thermostat_hvac_modes(hass, hk_driver, cls): """Test if unsupported HVAC modes are deactivated in HomeKit.""" entity_id = "climate.test" @@ -671,3 +719,46 @@ async def test_water_heater_get_temperature_range(hass, hk_driver, cls): ) await hass.async_block_till_done() assert acc.get_temperature_range() == (15.5, 21.0) + + +async def test_water_heater_restore(hass, hk_driver, cls, events): + """Test setting up an entity from state in the event registry.""" + hass.state = CoreState.not_running + + registry = await entity_registry.async_get_registry(hass) + + registry.async_get_or_create( + "water_heater", "generic", "1234", suggested_object_id="simple", + ) + registry.async_get_or_create( + "water_heater", + "generic", + "9012", + suggested_object_id="all_info_set", + capabilities={ATTR_MIN_TEMP: 60, ATTR_MAX_TEMP: 70}, + supported_features=0, + device_class="mock-device-class", + ) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) + await hass.async_block_till_done() + + acc = cls.thermostat(hass, hk_driver, "WaterHeater", "water_heater.simple", 2, None) + assert acc.category == 9 + assert acc.get_temperature_range() == (7, 35) + assert set(acc.char_current_heat_cool.properties["ValidValues"].keys()) == { + "Cool", + "Heat", + "Off", + } + + acc = cls.thermostat( + hass, hk_driver, "WaterHeater", "water_heater.all_info_set", 2, None + ) + assert acc.category == 9 + assert acc.get_temperature_range() == (60.0, 70.0) + assert set(acc.char_current_heat_cool.properties["ValidValues"].keys()) == { + "Cool", + "Heat", + "Off", + } diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 7f45ff0d174..e532d99f333 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -511,7 +511,7 @@ async def test_restore_states(hass): simple = hass.states.get("light.simple") assert simple is not None assert simple.state == STATE_UNAVAILABLE - assert simple.attributes == {"restored": True} + assert simple.attributes == {"restored": True, "supported_features": 0} disabled = hass.states.get("light.disabled") assert disabled is None From 2eff988a5c2babf3c6448d208035748bb576871a Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 10 Jan 2020 20:30:58 -0500 Subject: [PATCH 11/41] Fix ZHA temperature sensor restoration (#30661) * Add test for restoring state for zha temp. * Don't restore unit of measurement for ZHA sensors. Properly restore ZHA temperature sensor state. --- homeassistant/components/zha/sensor.py | 20 +++- tests/components/zha/test_sensor.py | 123 +++++++++++++++++++++++-- 2 files changed, 135 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 2d39d562bf5..3b73a9793c9 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -12,9 +12,15 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_TEMPERATURE, DOMAIN, ) -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, POWER_WATT, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + POWER_WATT, + STATE_UNKNOWN, + TEMP_CELSIUS, +) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.temperature import fahrenheit_to_celsius from .core.const import ( CHANNEL_ELECTRICAL_MEASUREMENT, @@ -160,7 +166,6 @@ class Sensor(ZhaEntity): def async_restore_last_state(self, last_state): """Restore previous state.""" self._state = last_state.state - self._unit = last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @callback async def async_state_attr_provider(self): @@ -277,3 +282,14 @@ class Temperature(Sensor): _device_class = DEVICE_CLASS_TEMPERATURE _divisor = 100 _unit = TEMP_CELSIUS + + @callback + def async_restore_last_state(self, last_state): + """Restore previous state.""" + if last_state.state == STATE_UNKNOWN: + return + if last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) != TEMP_CELSIUS: + ftemp = float(last_state.state) + self._state = round(fahrenheit_to_celsius(ftemp), 1) + return + self._state = last_state.state diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index b2daf4da765..3e02542a4fb 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -1,4 +1,5 @@ """Test zha sensor.""" +import pytest import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.homeautomation as homeautomation import zigpy.zcl.clusters.measurement as measurement @@ -6,7 +7,20 @@ import zigpy.zcl.clusters.smartenergy as smartenergy import zigpy.zcl.foundation as zcl_f from homeassistant.components.sensor import DOMAIN -from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN +import homeassistant.config as config_util +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_UNIT_OF_MEASUREMENT, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.helpers import restore_state +from homeassistant.util import dt as dt_util from .common import ( async_enable_traffic, @@ -39,7 +53,7 @@ async def test_sensor(hass, config_entry, zha_gateway): # ensure the sensor entity was created for each id in cluster_ids for cluster_id in cluster_ids: zigpy_device_info = zigpy_device_infos[cluster_id] - entity_id = zigpy_device_info["entity_id"] + entity_id = zigpy_device_info[ATTR_ENTITY_ID] assert hass.states.get(entity_id).state == STATE_UNAVAILABLE # allow traffic to flow through the gateway and devices @@ -55,7 +69,7 @@ async def test_sensor(hass, config_entry, zha_gateway): # test that the sensors now have a state of unknown for cluster_id in cluster_ids: zigpy_device_info = zigpy_device_infos[cluster_id] - entity_id = zigpy_device_info["entity_id"] + entity_id = zigpy_device_info[ATTR_ENTITY_ID] assert hass.states.get(entity_id).state == STATE_UNKNOWN # get the humidity device info and test the associated sensor logic @@ -128,7 +142,7 @@ async def async_build_devices(hass, zha_gateway, config_entry, cluster_ids): device_info["cluster"] = zigpy_device.endpoints.get(1).in_clusters[cluster_id] zha_device = zha_gateway.get_device(zigpy_device.ieee) device_info["zha_device"] = zha_device - device_info["entity_id"] = await find_entity_id(DOMAIN, zha_device, hass) + device_info[ATTR_ENTITY_ID] = await find_entity_id(DOMAIN, zha_device, hass) await hass.async_block_till_done() return device_infos @@ -187,6 +201,103 @@ def assert_state(hass, device_info, state, unit_of_measurement): This is used to ensure that the logic in each sensor class handled the attribute report it received correctly. """ - hass_state = hass.states.get(device_info["entity_id"]) + hass_state = hass.states.get(device_info[ATTR_ENTITY_ID]) assert hass_state.state == state - assert hass_state.attributes.get("unit_of_measurement") == unit_of_measurement + assert hass_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == unit_of_measurement + + +@pytest.fixture +def hass_ms(hass): + """Hass instance with measurement system.""" + + async def _hass_ms(meas_sys): + await config_util.async_process_ha_core_config( + hass, {CONF_UNIT_SYSTEM: meas_sys} + ) + await hass.async_block_till_done() + return hass + + return _hass_ms + + +@pytest.fixture +def core_rs(hass_storage): + """Core.restore_state fixture.""" + + def _storage(entity_id, uom, state): + now = dt_util.utcnow().isoformat() + + hass_storage[restore_state.STORAGE_KEY] = { + "version": restore_state.STORAGE_VERSION, + "key": restore_state.STORAGE_KEY, + "data": [ + { + "state": { + "entity_id": entity_id, + "state": str(state), + "attributes": {ATTR_UNIT_OF_MEASUREMENT: uom}, + "last_changed": now, + "last_updated": now, + "context": { + "id": "3c2243ff5f30447eb12e7348cfd5b8ff", + "user_id": None, + }, + }, + "last_seen": now, + } + ], + } + return + + return _storage + + +@pytest.mark.parametrize( + "uom, raw_temp, expected, restore", + [ + (TEMP_CELSIUS, 2900, 29, False), + (TEMP_CELSIUS, 2900, 29, True), + (TEMP_FAHRENHEIT, 2900, 84, False), + (TEMP_FAHRENHEIT, 2900, 84, True), + ], +) +async def test_temp_uom( + uom, raw_temp, expected, restore, hass_ms, config_entry, zha_gateway, core_rs +): + """Test zha temperature sensor unit of measurement.""" + + entity_id = "sensor.fake1026_fakemodel1026_004f3202_temperature" + if restore: + core_rs(entity_id, uom, state=(expected - 2)) + + hass = await hass_ms( + CONF_UNIT_SYSTEM_METRIC if uom == TEMP_CELSIUS else CONF_UNIT_SYSTEM_IMPERIAL + ) + + # list of cluster ids to create devices and sensor entities for + temp_cluster = measurement.TemperatureMeasurement + cluster_ids = [temp_cluster.cluster_id] + + # devices that were created from cluster_ids list above + zigpy_device_infos = await async_build_devices( + hass, zha_gateway, config_entry, cluster_ids + ) + + zigpy_device_info = zigpy_device_infos[temp_cluster.cluster_id] + zha_device = zigpy_device_info["zha_device"] + if not restore: + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + # allow traffic to flow through the gateway and devices + await async_enable_traffic(hass, zha_gateway, [zha_device]) + + # test that the sensors now have a state of unknown + if not restore: + assert hass.states.get(entity_id).state == STATE_UNKNOWN + + await send_attribute_report(hass, zigpy_device_info["cluster"], 0, raw_temp) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state is not None + assert round(float(state.state)) == expected + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == uom From 80a1084f2d647c177dd507231ce2fc46bdaab955 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Sat, 11 Jan 2020 03:59:11 +0000 Subject: [PATCH 12/41] Update ON/OFF condition and triggers to match documentation and UI, issue #30462 (#30663) --- homeassistant/components/binary_sensor/device_condition.py | 4 ++-- homeassistant/components/binary_sensor/device_trigger.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index 842790e0178..aa9a9d25e72 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -90,7 +90,7 @@ IS_ON = [ CONF_IS_GAS, CONF_IS_HOT, CONF_IS_LIGHT, - CONF_IS_LOCKED, + CONF_IS_NOT_LOCKED, CONF_IS_MOIST, CONF_IS_MOTION, CONF_IS_MOVING, @@ -112,7 +112,7 @@ IS_OFF = [ CONF_IS_NOT_COLD, CONF_IS_NOT_CONNECTED, CONF_IS_NOT_HOT, - CONF_IS_NOT_LOCKED, + CONF_IS_LOCKED, CONF_IS_NOT_MOIST, CONF_IS_NOT_MOVING, CONF_IS_NOT_OCCUPIED, diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index 288cc101d93..f4799828c68 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -91,7 +91,7 @@ TURNED_ON = [ CONF_GAS, CONF_HOT, CONF_LIGHT, - CONF_LOCKED, + CONF_NOT_LOCKED, CONF_MOIST, CONF_MOTION, CONF_MOVING, @@ -113,7 +113,7 @@ TURNED_OFF = [ CONF_NOT_COLD, CONF_NOT_CONNECTED, CONF_NOT_HOT, - CONF_NOT_LOCKED, + CONF_LOCKED, CONF_NOT_MOIST, CONF_NOT_MOVING, CONF_NOT_OCCUPIED, From 0fc2cce4dc22862d93fc1a4d75a7fe0ea9f8a8f0 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 9 Jan 2020 00:31:49 +0000 Subject: [PATCH 13/41] [ci skip] Translation update --- .../components/axis/.translations/lb.json | 3 ++- .../components/brother/.translations/da.json | 1 + .../components/brother/.translations/en.json | 1 + .../components/brother/.translations/lb.json | 23 +++++++++++++++++++ .../components/brother/.translations/pl.json | 1 + .../components/brother/.translations/ru.json | 1 + .../components/brother/.translations/sl.json | 23 +++++++++++++++++++ .../components/deconz/.translations/lb.json | 8 ++++++- .../components/deconz/.translations/pl.json | 8 ++++++- .../components/deconz/.translations/ru.json | 8 ++++++- .../components/deconz/.translations/sl.json | 8 ++++++- .../components/gios/.translations/da.json | 3 +++ .../components/gios/.translations/en.json | 3 +++ .../components/gios/.translations/lb.json | 20 ++++++++++++++++ .../components/gios/.translations/pl.json | 3 +++ .../components/local_ip/.translations/lb.json | 16 +++++++++++++ .../components/sentry/.translations/lb.json | 18 +++++++++++++++ 17 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/brother/.translations/lb.json create mode 100644 homeassistant/components/brother/.translations/sl.json create mode 100644 homeassistant/components/gios/.translations/lb.json create mode 100644 homeassistant/components/local_ip/.translations/lb.json create mode 100644 homeassistant/components/sentry/.translations/lb.json diff --git a/homeassistant/components/axis/.translations/lb.json b/homeassistant/components/axis/.translations/lb.json index 24ee0e24125..589932cd68e 100644 --- a/homeassistant/components/axis/.translations/lb.json +++ b/homeassistant/components/axis/.translations/lb.json @@ -4,7 +4,8 @@ "already_configured": "Apparat ass scho konfigur\u00e9iert", "bad_config_file": "Feelerhaft Donn\u00e9e\u00eb aus der Konfiguratioun's Datei", "link_local_address": "Lokal Link Adressen ginn net \u00ebnnerst\u00ebtzt", - "not_axis_device": "Entdeckten Apparat ass keen Axis Apparat" + "not_axis_device": "Entdeckten Apparat ass keen Axis Apparat", + "updated_configuration": "Konfiguratioun vum Apparat gouf mat der neier Adress aktualis\u00e9iert" }, "error": { "already_configured": "Apparat ass scho konfigur\u00e9iert", diff --git a/homeassistant/components/brother/.translations/da.json b/homeassistant/components/brother/.translations/da.json index 70b5857796b..2ec79228194 100644 --- a/homeassistant/components/brother/.translations/da.json +++ b/homeassistant/components/brother/.translations/da.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Denne printer er allerede konfigureret.", "unsupported_model": "Denne printermodel underst\u00f8ttes ikke." }, "error": { diff --git a/homeassistant/components/brother/.translations/en.json b/homeassistant/components/brother/.translations/en.json index b9b3bd55651..d586bcea1f8 100644 --- a/homeassistant/components/brother/.translations/en.json +++ b/homeassistant/components/brother/.translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "This printer is already configured.", "unsupported_model": "This printer model is not supported." }, "error": { diff --git a/homeassistant/components/brother/.translations/lb.json b/homeassistant/components/brother/.translations/lb.json new file mode 100644 index 00000000000..e9ffc2c4da7 --- /dev/null +++ b/homeassistant/components/brother/.translations/lb.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "unsupported_model": "D\u00ebse Printer Modell g\u00ebtt net \u00ebnnerst\u00ebtzt." + }, + "error": { + "connection_error": "Feeler bei der Verbindung.", + "snmp_error": "SNMP Server ausgeschalt oder Printer net \u00ebnnerst\u00ebtzt.", + "wrong_host": "Ong\u00ebltege Numm oder IP Adresse" + }, + "step": { + "user": { + "data": { + "host": "Printer Numm oder IP Adresse", + "type": "Typ vum Printer" + }, + "description": "Brother Printer Integratioun ariichten. Am Fall vun Problemer kuckt op: https://www.home-assistant.io/integrations/brother", + "title": "Brother Printer" + } + }, + "title": "Brother Printer" + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/pl.json b/homeassistant/components/brother/.translations/pl.json index 658c54354ac..14fe4024f34 100644 --- a/homeassistant/components/brother/.translations/pl.json +++ b/homeassistant/components/brother/.translations/pl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Ta drukarka jest ju\u017c skonfigurowana.", "unsupported_model": "Ten model drukarki nie jest obs\u0142ugiwany." }, "error": { diff --git a/homeassistant/components/brother/.translations/ru.json b/homeassistant/components/brother/.translations/ru.json index 995ddeec3d4..8bce23e5292 100644 --- a/homeassistant/components/brother/.translations/ru.json +++ b/homeassistant/components/brother/.translations/ru.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "unsupported_model": "\u042d\u0442\u0430 \u043c\u043e\u0434\u0435\u043b\u044c \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." }, "error": { diff --git a/homeassistant/components/brother/.translations/sl.json b/homeassistant/components/brother/.translations/sl.json new file mode 100644 index 00000000000..99caf69a86f --- /dev/null +++ b/homeassistant/components/brother/.translations/sl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "unsupported_model": "Ta model tiskalnika ni podprt." + }, + "error": { + "connection_error": "Napaka v povezavi.", + "snmp_error": "Stre\u017enik SNMP je izklopljen ali tiskalnik ni podprt.", + "wrong_host": "Neveljavno ime gostitelja ali IP naslov." + }, + "step": { + "user": { + "data": { + "host": "Gostiteljsko ime tiskalnika ali naslov IP", + "type": "Vrsta tiskalnika" + }, + "description": "Nastavite integracijo tiskalnika Brother. \u010ce imate te\u017eave s konfiguracijo, pojdite na: https://www.home-assistant.io/integrations/brother", + "title": "Brother Tiskalnik" + } + }, + "title": "Brother Tiskalnik" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index 07f88732c62..4b04cfa03ce 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -77,15 +77,21 @@ "remote_button_short_release": "\"{subtype}\" Kn\u00e4ppche lassgelooss", "remote_button_triple_press": "\"{subtype}\" Kn\u00e4ppche dr\u00e4imol gedr\u00e9ckt", "remote_double_tap": "Apparat \"{subtype}\" zwee mol gedr\u00e9ckt", + "remote_double_tap_any_side": "Apparat gouf 2 mol ugetippt op enger S\u00e4it", "remote_falling": "Apparat am fr\u00e4ie Fall", + "remote_flip_180_degrees": "Apparat \u00ebm 180 Grad gedr\u00e9int", + "remote_flip_90_degrees": "Apparat \u00ebm 90 Grad gedr\u00e9int", "remote_gyro_activated": "Apparat ger\u00ebselt", "remote_moved": "Apparat beweegt mat \"{subtype}\" erop", + "remote_moved_any_side": "Apparat gouf mat enger S\u00e4it bewegt", "remote_rotate_from_side_1": "Apparat rot\u00e9iert vun der \"S\u00e4it 1\" op \"{subtype}\"", "remote_rotate_from_side_2": "Apparat rot\u00e9iert vun der \"S\u00e4it 2\" op \"{subtype}\"", "remote_rotate_from_side_3": "Apparat rot\u00e9iert vun der \"S\u00e4it 3\" op \"{subtype}\"", "remote_rotate_from_side_4": "Apparat rot\u00e9iert vun der \"S\u00e4it 4\" op \"{subtype}\"", "remote_rotate_from_side_5": "Apparat rot\u00e9iert vun der \"S\u00e4it 5\" op \"{subtype}\"", - "remote_rotate_from_side_6": "Apparat rot\u00e9iert vun der \"S\u00e4it\" 6 op \"{subtype}\"" + "remote_rotate_from_side_6": "Apparat rot\u00e9iert vun der \"S\u00e4it\" 6 op \"{subtype}\"", + "remote_turned_clockwise": "Apparat mam Auere Wee gedr\u00e9int", + "remote_turned_counter_clockwise": "Apparat g\u00e9int den Auere Wee gedr\u00e9int" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index eafecf87d03..df85e7b8d1d 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -77,15 +77,21 @@ "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony", "remote_button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty", "remote_double_tap": "urz\u0105dzenie \"{subtype}\" zostanie dwukrotnie pukni\u0119te", + "remote_double_tap_any_side": "urz\u0105dzenie dwukrotnie pukni\u0119te z dowolnej strony", "remote_falling": "urz\u0105dzenie zarejestruje swobodny spadek", + "remote_flip_180_degrees": "urz\u0105dzenie odwr\u00f3cone o 180 stopni", + "remote_flip_90_degrees": "urz\u0105dzenie odwr\u00f3cone o 90 stopni", "remote_gyro_activated": "nast\u0105pi potrz\u0105\u015bni\u0119cie urz\u0105dzeniem", "remote_moved": "urz\u0105dzenie poruszone z \"{subtype}\" w g\u00f3r\u0119", + "remote_moved_any_side": "urz\u0105dzenie przesuni\u0119te dowoln\u0105 stron\u0105 do g\u00f3ry", "remote_rotate_from_side_1": "urz\u0105dzenie obr\u00f3cone ze \"strona 1\" na \"{subtype}\"", "remote_rotate_from_side_2": "urz\u0105dzenie obr\u00f3cone ze \"strona 2\" na \"{subtype}\"", "remote_rotate_from_side_3": "urz\u0105dzenie obr\u00f3cone ze \"strona 3\" na \"{subtype}\"", "remote_rotate_from_side_4": "urz\u0105dzenie obr\u00f3cone ze \"strona 4\" na \"{subtype}\"", "remote_rotate_from_side_5": "urz\u0105dzenie obr\u00f3cone ze \"strona 5\" na \"{subtype}\"", - "remote_rotate_from_side_6": "urz\u0105dzenie obr\u00f3cone ze \"strona 6\" na \"{subtype}\"" + "remote_rotate_from_side_6": "urz\u0105dzenie obr\u00f3cone ze \"strona 6\" na \"{subtype}\"", + "remote_turned_clockwise": "urz\u0105dzenie obr\u00f3cone zgodnie z ruchem wskaz\u00f3wek zegara", + "remote_turned_counter_clockwise": "urz\u0105dzenie obr\u00f3cone przeciwnie do ruchu wskaz\u00f3wek zegara" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index 57ec138c402..87c3fb646f2 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -77,15 +77,21 @@ "remote_button_short_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430", "remote_button_triple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430", "remote_double_tap": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c \"{subtype}\" \u043f\u043e\u0441\u0442\u0443\u0447\u0430\u043b\u0438 \u0434\u0432\u0430\u0436\u0434\u044b", + "remote_double_tap_any_side": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c \u043f\u043e\u0441\u0442\u0443\u0447\u0430\u043b\u0438 \u0434\u0432\u0430\u0436\u0434\u044b", "remote_falling": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432 \u0441\u0432\u043e\u0431\u043e\u0434\u043d\u043e\u043c \u043f\u0430\u0434\u0435\u043d\u0438\u0438", + "remote_flip_180_degrees": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u043d\u0430 180 \u0433\u0440\u0430\u0434\u0443\u0441\u043e\u0432", + "remote_flip_90_degrees": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u043d\u0430 90 \u0433\u0440\u0430\u0434\u0443\u0441\u043e\u0432", "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u0441\u0442\u0440\u044f\u0445\u043d\u0443\u043b\u0438", "remote_moved": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u0434\u0432\u0438\u043d\u0443\u043b\u0438, \u043a\u043e\u0433\u0434\u0430 \"{subtype}\" \u0441\u0432\u0435\u0440\u0445\u0443", + "remote_moved_any_side": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u0434\u0432\u0438\u043d\u0443\u043b\u0438", "remote_rotate_from_side_1": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 1 \u043d\u0430 \"{subtype}\"", "remote_rotate_from_side_2": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 2 \u043d\u0430 \"{subtype}\"", "remote_rotate_from_side_3": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 3 \u043d\u0430 \"{subtype}\"", "remote_rotate_from_side_4": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 4 \u043d\u0430 \"{subtype}\"", "remote_rotate_from_side_5": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 5 \u043d\u0430 \"{subtype}\"", - "remote_rotate_from_side_6": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 6 \u043d\u0430 \"{subtype}\"" + "remote_rotate_from_side_6": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 6 \u043d\u0430 \"{subtype}\"", + "remote_turned_clockwise": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u043f\u043e \u0447\u0430\u0441\u043e\u0432\u043e\u0439 \u0441\u0442\u0440\u0435\u043b\u043a\u0435", + "remote_turned_counter_clockwise": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u043f\u0440\u043e\u0442\u0438\u0432 \u0447\u0430\u0441\u043e\u0432\u043e\u0439 \u0441\u0442\u0440\u0435\u043b\u043a\u0438" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index ca4c6a3d636..385de6f0f01 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -77,15 +77,21 @@ "remote_button_short_release": "Gumb \"{subtype}\" spro\u0161\u010den", "remote_button_triple_press": "Gumb \"{subtype}\" trikrat kliknjen", "remote_double_tap": "Naprava \"{subtype}\" dvakrat dotaknjena", + "remote_double_tap_any_side": "Naprava je bila dvojno tapnjena na katerokoli stran", "remote_falling": "Naprava v prostem padu", + "remote_flip_180_degrees": "Naprava se je obrnila za 180 stopinj", + "remote_flip_90_degrees": "Naprava se je obrnila za 90 stopinj", "remote_gyro_activated": "Naprava se je pretresla", "remote_moved": "Naprava je premaknjena s \"{subtype}\" navzgor", + "remote_moved_any_side": "Naprava se je premikala s katero koli stranjo navzgor", "remote_rotate_from_side_1": "Naprava je zasukana iz \"strani 1\" v \"{subtype}\"", "remote_rotate_from_side_2": "Naprava je zasukana iz \"strani 2\" v \"{subtype}\"", "remote_rotate_from_side_3": "Naprava je zasukana iz \"strani 3\" v \"{subtype}\"", "remote_rotate_from_side_4": "Naprava je zasukana iz \"strani 4\" v \"{subtype}\"", "remote_rotate_from_side_5": "Naprava je zasukana iz \"strani 5\" v \"{subtype}\"", - "remote_rotate_from_side_6": "Naprava je zasukana iz \"strani 6\" v \"{subtype}\"" + "remote_rotate_from_side_6": "Naprava je zasukana iz \"strani 6\" v \"{subtype}\"", + "remote_turned_clockwise": "Naprava se je obrnila v smeri urinega kazalca", + "remote_turned_counter_clockwise": "Naprava se je obrnila v nasprotni smeri urinega kazalca" } }, "options": { diff --git a/homeassistant/components/gios/.translations/da.json b/homeassistant/components/gios/.translations/da.json index b4855da7951..bd0e947f1dc 100644 --- a/homeassistant/components/gios/.translations/da.json +++ b/homeassistant/components/gios/.translations/da.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "GIO\u015a-integration for denne m\u00e5lestation er allerede konfigureret." + }, "error": { "cannot_connect": "Kan ikke oprette forbindelse til GIO\u015a-serveren.", "invalid_sensors_data": "Ugyldige sensordata for denne m\u00e5lestation.", diff --git a/homeassistant/components/gios/.translations/en.json b/homeassistant/components/gios/.translations/en.json index 2ff0d8c60f3..0a85aaa9d15 100644 --- a/homeassistant/components/gios/.translations/en.json +++ b/homeassistant/components/gios/.translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "GIO\u015a integration for this measuring station is already configured." + }, "error": { "cannot_connect": "Cannot connect to the GIO\u015a server.", "invalid_sensors_data": "Invalid sensors' data for this measuring station.", diff --git a/homeassistant/components/gios/.translations/lb.json b/homeassistant/components/gios/.translations/lb.json new file mode 100644 index 00000000000..ed42ad3a7ae --- /dev/null +++ b/homeassistant/components/gios/.translations/lb.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "Konnt sech net mam GIO\u015a Server verbannen.", + "invalid_sensors_data": "Ong\u00eblteg Sensor Donn\u00e9e\u00eb fir d\u00ebs Miess Statioun", + "wrong_station_id": "ID vun der Miess Statioun ass net korrekt." + }, + "step": { + "user": { + "data": { + "name": "Numm vun der Integratioun", + "station_id": "ID vun der Miess Statioun" + }, + "description": "GIO\u015a (Polnesch Chefinspektorat vum \u00cbmweltschutz) Loft Qualit\u00e9it Integratioun ariichten. Fir w\u00e9ider H\u00ebllef mat der Konfiuratioun kuckt hei: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Polnesch Chefinspektorat vum \u00cbmweltschutz)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/pl.json b/homeassistant/components/gios/.translations/pl.json index d3623004fba..677762c2930 100644 --- a/homeassistant/components/gios/.translations/pl.json +++ b/homeassistant/components/gios/.translations/pl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Integracja GIO\u015a dla tej stacji pomiarowej jest ju\u017c skonfigurowana." + }, "error": { "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z serwerem GIO\u015a.", "invalid_sensors_data": "Nieprawid\u0142owe dane sensor\u00f3w dla tej stacji pomiarowej.", diff --git a/homeassistant/components/local_ip/.translations/lb.json b/homeassistant/components/local_ip/.translations/lb.json new file mode 100644 index 00000000000..aa249f184ce --- /dev/null +++ b/homeassistant/components/local_ip/.translations/lb.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Integratioun ass scho konfigur\u00e9iert mat engem Sensor mat deem Numm" + }, + "step": { + "user": { + "data": { + "name": "Numm vum Sensor" + }, + "title": "Lokal IP Adresse" + } + }, + "title": "Lokal IP Adresse" + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/lb.json b/homeassistant/components/sentry/.translations/lb.json new file mode 100644 index 00000000000..e91f57a1585 --- /dev/null +++ b/homeassistant/components/sentry/.translations/lb.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Sentry ass scho konfigur\u00e9iert" + }, + "error": { + "bad_dsn": "Ong\u00eblteg DSN", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "description": "Gitt \u00e4r Sentry DSN un", + "title": "Sentry" + } + }, + "title": "Sentry" + } +} \ No newline at end of file From d0357ceecd9e1570317767d3b92fd33746aadd66 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 10 Jan 2020 00:31:50 +0000 Subject: [PATCH 14/41] [ci skip] Translation update --- .../components/adguard/.translations/ko.json | 2 +- .../components/axis/.translations/it.json | 3 ++- .../components/brother/.translations/it.json | 24 +++++++++++++++++++ .../components/brother/.translations/no.json | 1 + .../brother/.translations/zh-Hant.json | 1 + .../components/deconz/.translations/it.json | 8 ++++++- .../components/deconz/.translations/no.json | 8 ++++++- .../deconz/.translations/zh-Hant.json | 8 ++++++- .../components/gios/.translations/it.json | 23 ++++++++++++++++++ .../components/gios/.translations/no.json | 3 +++ .../components/gios/.translations/ru.json | 3 +++ .../gios/.translations/zh-Hant.json | 3 +++ .../components/local_ip/.translations/it.json | 16 +++++++++++++ .../components/sentry/.translations/it.json | 18 ++++++++++++++ 14 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/brother/.translations/it.json create mode 100644 homeassistant/components/gios/.translations/it.json create mode 100644 homeassistant/components/local_ip/.translations/it.json create mode 100644 homeassistant/components/sentry/.translations/it.json diff --git a/homeassistant/components/adguard/.translations/ko.json b/homeassistant/components/adguard/.translations/ko.json index e1f39259292..02bbb75cd2b 100644 --- a/homeassistant/components/adguard/.translations/ko.json +++ b/homeassistant/components/adguard/.translations/ko.json @@ -11,7 +11,7 @@ }, "step": { "hassio_confirm": { - "description": "Hass.io {addon} \uc560\ub4dc\uc628\uc73c\ub85c AdGuard Home \uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant \ub97c \uad6c\uc131 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Hass.io {addon} \uc560\ub4dc\uc628\uc73c\ub85c AdGuard Home \uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant \ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Hass.io \uc560\ub4dc\uc628\uc758 AdGuard Home" }, "user": { diff --git a/homeassistant/components/axis/.translations/it.json b/homeassistant/components/axis/.translations/it.json index 3f303140c68..9e2eecf5747 100644 --- a/homeassistant/components/axis/.translations/it.json +++ b/homeassistant/components/axis/.translations/it.json @@ -4,7 +4,8 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "bad_config_file": "Dati errati dal file di configurazione", "link_local_address": "Gli indirizzi locali di collegamento non sono supportati", - "not_axis_device": "Il dispositivo rilevato non \u00e8 un dispositivo Axis" + "not_axis_device": "Il dispositivo rilevato non \u00e8 un dispositivo Axis", + "updated_configuration": "Configurazione del dispositivo aggiornata con nuovo indirizzo host" }, "error": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", diff --git a/homeassistant/components/brother/.translations/it.json b/homeassistant/components/brother/.translations/it.json new file mode 100644 index 00000000000..43bdb7aec7b --- /dev/null +++ b/homeassistant/components/brother/.translations/it.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Questa stampante \u00e8 gi\u00e0 configurata.", + "unsupported_model": "Questo modello di stampante non \u00e8 supportato." + }, + "error": { + "connection_error": "Errore di connessione.", + "snmp_error": "Server SNMP spento o stampante non supportata.", + "wrong_host": "Nome host o indirizzo IP non valido." + }, + "step": { + "user": { + "data": { + "host": "Nome host o indirizzo IP della stampante", + "type": "Tipo di stampante" + }, + "description": "Configurare l'integrazione della stampante Brother. In caso di problemi con la configurazione, visitare: https://www.home-assistant.io/integrations/brother", + "title": "Stampante Brother" + } + }, + "title": "Stampante Brother" + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/no.json b/homeassistant/components/brother/.translations/no.json index 57cfd03d216..d4cf935f156 100644 --- a/homeassistant/components/brother/.translations/no.json +++ b/homeassistant/components/brother/.translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Denne skriveren er allerede konfigurert.", "unsupported_model": "Denne skrivermodellen er ikke st\u00f8ttet." }, "error": { diff --git a/homeassistant/components/brother/.translations/zh-Hant.json b/homeassistant/components/brother/.translations/zh-Hant.json index 0ee27bf77d4..cff89ea38ca 100644 --- a/homeassistant/components/brother/.translations/zh-Hant.json +++ b/homeassistant/components/brother/.translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u6b64\u5370\u8868\u6a5f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "unsupported_model": "\u4e0d\u652f\u63f4\u6b64\u6b3e\u5370\u8868\u6a5f\u3002" }, "error": { diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index 99e5622129f..980409d6987 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -77,15 +77,21 @@ "remote_button_short_release": "Pulsante \"{subtype}\" rilasciato", "remote_button_triple_press": "Pulsante \"{subtype}\" cliccato tre volte", "remote_double_tap": "Dispositivo \"{subtype}\" toccato due volte", + "remote_double_tap_any_side": "Dispositivo toccato due volte su qualsiasi lato", "remote_falling": "Dispositivo in caduta libera", + "remote_flip_180_degrees": "Dispositivo capovolto di 180 gradi", + "remote_flip_90_degrees": "Dispositivo capovolto di 90 gradi", "remote_gyro_activated": "Dispositivo in vibrazione", "remote_moved": "Dispositivo spostato con \"{subtype}\" verso l'alto", + "remote_moved_any_side": "Dispositivo spostato con qualsiasi lato verso l'alto", "remote_rotate_from_side_1": "Dispositivo ruotato da \"lato 1\" a \"{subtype}\"", "remote_rotate_from_side_2": "Dispositivo ruotato da \"lato 2\" a \"{subtype}\"", "remote_rotate_from_side_3": "Dispositivo ruotato da \"lato 3\" a \"{subtype}\"", "remote_rotate_from_side_4": "Dispositivo ruotato da \"lato 4\" a \"{subtype}\"", "remote_rotate_from_side_5": "Dispositivo ruotato da \"lato 5\" a \"{subtype}\"", - "remote_rotate_from_side_6": "Dispositivo ruotato da \"lato 6\" a \"{subtype}\"" + "remote_rotate_from_side_6": "Dispositivo ruotato da \"lato 6\" a \"{subtype}\"", + "remote_turned_clockwise": "Dispositivo ruotato in senso orario", + "remote_turned_counter_clockwise": "Dispositivo ruotato in senso antiorario" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index 6d1969e98a7..d6133542c64 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -77,15 +77,21 @@ "remote_button_short_release": "\"{subtype}\"-knappen sluppet", "remote_button_triple_press": "\"{subtype}\"-knappen trippel klikket", "remote_double_tap": "Enheten \" {subtype} \" dobbeltklikket", + "remote_double_tap_any_side": "Enheten dobbeltklikket p\u00e5 alle sider", "remote_falling": "Enheten er i fritt fall", + "remote_flip_180_degrees": "Enheten er snudd 180 grader", + "remote_flip_90_degrees": "Enheten er snudd 90 grader", "remote_gyro_activated": "Enhet er ristet", "remote_moved": "Enheten ble flyttet med \"{under type}\" opp", + "remote_moved_any_side": "Enheten flyttet med alle sider opp", "remote_rotate_from_side_1": "Enheten rotert fra \"side 1\" til \" {subtype} \"", "remote_rotate_from_side_2": "Enheten rotert fra \"side 2\" til \" {subtype} \"", "remote_rotate_from_side_3": "Enheten rotert fra \"side 3\" til \" {subtype} \"", "remote_rotate_from_side_4": "Enheten rotert fra \"side 4\" til \" {subtype} \"", "remote_rotate_from_side_5": "Enheten rotert fra \"side 5\" til \" {subtype} \"", - "remote_rotate_from_side_6": "Enheten rotert fra \"side 6\" til \" {subtype} \"" + "remote_rotate_from_side_6": "Enheten rotert fra \"side 6\" til \" {subtype} \"", + "remote_turned_clockwise": "Enheten dreide med klokken", + "remote_turned_counter_clockwise": "Enheten dreide mot klokken" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index 4df506ad76f..96ab68a8dbb 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -77,15 +77,21 @@ "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e", "remote_button_triple_press": "\"{subtype}\" \u6309\u9215\u4e09\u9023\u9ede\u64ca", "remote_double_tap": "\u8a2d\u5099 \"{subtype}\" \u96d9\u6572", + "remote_double_tap_any_side": "\u8a2d\u5099\u4efb\u4e00\u9762\u96d9\u9ede\u9078", "remote_falling": "\u8a2d\u5099\u81ea\u7531\u843d\u4e0b", + "remote_flip_180_degrees": "\u8a2d\u5099\u65cb\u8f49 180 \u5ea6", + "remote_flip_90_degrees": "\u8a2d\u5099\u65cb\u8f49 90 \u5ea6", "remote_gyro_activated": "\u8a2d\u5099\u6416\u6643", "remote_moved": "\u8a2d\u5099\u79fb\u52d5\u81f3 \"{subtype}\" \u671d\u4e0a", + "remote_moved_any_side": "\u8a2d\u5099\u4efb\u4e00\u9762\u671d\u4e0a", "remote_rotate_from_side_1": "\u8a2d\u5099\u7531\u300c\u7b2c 1 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", "remote_rotate_from_side_2": "\u8a2d\u5099\u7531\u300c\u7b2c 2 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", "remote_rotate_from_side_3": "\u8a2d\u5099\u7531\u300c\u7b2c 3 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", "remote_rotate_from_side_4": "\u8a2d\u5099\u7531\u300c\u7b2c 4 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", "remote_rotate_from_side_5": "\u8a2d\u5099\u7531\u300c\u7b2c 5 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", - "remote_rotate_from_side_6": "\u8a2d\u5099\u7531\u300c\u7b2c 6 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d" + "remote_rotate_from_side_6": "\u8a2d\u5099\u7531\u300c\u7b2c 6 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", + "remote_turned_clockwise": "\u8a2d\u5099\u9806\u6642\u91dd\u65cb\u8f49", + "remote_turned_counter_clockwise": "\u8a2d\u5099\u9006\u6642\u91dd\u65cb\u8f49" } }, "options": { diff --git a/homeassistant/components/gios/.translations/it.json b/homeassistant/components/gios/.translations/it.json new file mode 100644 index 00000000000..b3d1b9a71cf --- /dev/null +++ b/homeassistant/components/gios/.translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "L'integrazione GIO\u015a per questa stazione di misurazione \u00e8 gi\u00e0 configurata." + }, + "error": { + "cannot_connect": "Impossibile connettersi al server GIO\u015a.", + "invalid_sensors_data": "Dati dei sensori non validi per questa stazione di misura.", + "wrong_station_id": "L'ID della stazione di misura non \u00e8 corretto." + }, + "step": { + "user": { + "data": { + "name": "Nome dell'integrazione", + "station_id": "ID della stazione di misura" + }, + "description": "Impostare l'integrazione della qualit\u00e0 dell'aria GIO\u015a (Ispettorato capo polacco di protezione ambientale). Se hai bisogno di aiuto con la configurazione dai un'occhiata qui: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Ispettorato capo polacco di protezione ambientale)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/no.json b/homeassistant/components/gios/.translations/no.json index 3abfe3bfbb8..b045c51e563 100644 --- a/homeassistant/components/gios/.translations/no.json +++ b/homeassistant/components/gios/.translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "GIO\u015a-integrasjon for denne m\u00e5lestasjonen er allerede konfigurert." + }, "error": { "cannot_connect": "Kan ikke koble til GIO\u015a-tjeneren", "invalid_sensors_data": "Ugyldig sensordata for denne m\u00e5lestasjonen", diff --git a/homeassistant/components/gios/.translations/ru.json b/homeassistant/components/gios/.translations/ru.json index ea2c2997d4d..69ffff98517 100644 --- a/homeassistant/components/gios/.translations/ru.json +++ b/homeassistant/components/gios/.translations/ru.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, "error": { "cannot_connect": "\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 GIO\u015a.", "invalid_sensors_data": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432 \u0434\u043b\u044f \u044d\u0442\u043e\u0439 \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438.", diff --git a/homeassistant/components/gios/.translations/zh-Hant.json b/homeassistant/components/gios/.translations/zh-Hant.json index 19d13572c72..3f10f2eb37b 100644 --- a/homeassistant/components/gios/.translations/zh-Hant.json +++ b/homeassistant/components/gios/.translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u6b64 GIO\u015a \u76e3\u6e2c\u7ad9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + }, "error": { "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 GIO\u015a \u4f3a\u670d\u5668\u3002", "invalid_sensors_data": "\u6b64\u76e3\u6e2c\u7ad9\u50b3\u611f\u5668\u8cc7\u6599\u7121\u6548\u3002", diff --git a/homeassistant/components/local_ip/.translations/it.json b/homeassistant/components/local_ip/.translations/it.json new file mode 100644 index 00000000000..a33e892c6ec --- /dev/null +++ b/homeassistant/components/local_ip/.translations/it.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "L'integrazione \u00e8 gi\u00e0 configurata con un sensore esistente con questo nome" + }, + "step": { + "user": { + "data": { + "name": "Nome del sensore" + }, + "title": "Indirizzo IP locale" + } + }, + "title": "Indirizzo IP locale" + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/it.json b/homeassistant/components/sentry/.translations/it.json new file mode 100644 index 00000000000..4d0cd3178e7 --- /dev/null +++ b/homeassistant/components/sentry/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Sentry \u00e8 gi\u00e0 configurato" + }, + "error": { + "bad_dsn": "DSN non valido", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "description": "Inserisci il tuo DSN Sentry", + "title": "Sentry" + } + }, + "title": "Sentry" + } +} \ No newline at end of file From 77d184ee25a957eee3ed354856abe4e09262ee1b Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 11 Jan 2020 00:31:55 +0000 Subject: [PATCH 15/41] [ci skip] Translation update --- .../components/axis/.translations/ru.json | 6 ++--- .../components/brother/.translations/es.json | 24 +++++++++++++++++ .../components/brother/.translations/lb.json | 1 + .../components/brother/.translations/ru.json | 2 +- .../cert_expiry/.translations/zh-Hant.json | 2 +- .../components/daikin/.translations/ru.json | 2 +- .../components/deconz/.translations/es.json | 8 +++++- .../components/deconz/.translations/ru.json | 4 +-- .../components/elgato/.translations/ru.json | 2 +- .../components/gios/.translations/es.json | 3 +++ .../components/gios/.translations/lb.json | 3 +++ .../homekit_controller/.translations/ru.json | 2 +- .../homematicip_cloud/.translations/ru.json | 2 +- .../huawei_lte/.translations/ru.json | 4 +-- .../components/hue/.translations/ru.json | 4 +-- .../components/ring/.translations/da.json | 27 +++++++++++++++++++ .../components/ring/.translations/en.json | 5 ++-- .../samsungtv/.translations/da.json | 26 ++++++++++++++++++ .../samsungtv/.translations/en.json | 26 ++++++++++++++++++ .../samsungtv/.translations/es.json | 26 ++++++++++++++++++ .../samsungtv/.translations/it.json | 26 ++++++++++++++++++ .../samsungtv/.translations/lb.json | 26 ++++++++++++++++++ .../samsungtv/.translations/no.json | 26 ++++++++++++++++++ .../samsungtv/.translations/ru.json | 26 ++++++++++++++++++ .../components/solarlog/.translations/ru.json | 4 +-- .../components/toon/.translations/ru.json | 2 +- .../components/tradfri/.translations/ru.json | 4 +-- .../components/wled/.translations/ru.json | 2 +- 28 files changed, 270 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/brother/.translations/es.json create mode 100644 homeassistant/components/ring/.translations/da.json create mode 100644 homeassistant/components/samsungtv/.translations/da.json create mode 100644 homeassistant/components/samsungtv/.translations/en.json create mode 100644 homeassistant/components/samsungtv/.translations/es.json create mode 100644 homeassistant/components/samsungtv/.translations/it.json create mode 100644 homeassistant/components/samsungtv/.translations/lb.json create mode 100644 homeassistant/components/samsungtv/.translations/no.json create mode 100644 homeassistant/components/samsungtv/.translations/ru.json diff --git a/homeassistant/components/axis/.translations/ru.json b/homeassistant/components/axis/.translations/ru.json index 3506b636baa..b0da189d20f 100644 --- a/homeassistant/components/axis/.translations/ru.json +++ b/homeassistant/components/axis/.translations/ru.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "bad_config_file": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438.", "link_local_address": "\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", "not_axis_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Axis.", "updated_configuration": "\u0410\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d." }, "error": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e.", "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, diff --git a/homeassistant/components/brother/.translations/es.json b/homeassistant/components/brother/.translations/es.json new file mode 100644 index 00000000000..f4e53e20793 --- /dev/null +++ b/homeassistant/components/brother/.translations/es.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Esta impresora ya est\u00e1 configurada.", + "unsupported_model": "Este modelo de impresora no es compatible." + }, + "error": { + "connection_error": "Error de conexi\u00f3n.", + "snmp_error": "El servidor SNMP est\u00e1 apagado o la impresora no es compatible.", + "wrong_host": "Nombre del host o direcci\u00f3n IP no v\u00e1lidos." + }, + "step": { + "user": { + "data": { + "host": "Nombre del host o direcci\u00f3n IP de la impresora", + "type": "Tipo de impresora" + }, + "description": "Configure la integraci\u00f3n de impresoras Brother. Si tiene problemas con la configuraci\u00f3n, vaya a: https://www.home-assistant.io/integrations/brother", + "title": "Impresora Brother" + } + }, + "title": "Impresora Brother" + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/lb.json b/homeassistant/components/brother/.translations/lb.json index e9ffc2c4da7..dd051b1bb0c 100644 --- a/homeassistant/components/brother/.translations/lb.json +++ b/homeassistant/components/brother/.translations/lb.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "D\u00ebse Printer ass scho konfigur\u00e9iert.", "unsupported_model": "D\u00ebse Printer Modell g\u00ebtt net \u00ebnnerst\u00ebtzt." }, "error": { diff --git a/homeassistant/components/brother/.translations/ru.json b/homeassistant/components/brother/.translations/ru.json index 8bce23e5292..eb12f2f1225 100644 --- a/homeassistant/components/brother/.translations/ru.json +++ b/homeassistant/components/brother/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "unsupported_model": "\u042d\u0442\u0430 \u043c\u043e\u0434\u0435\u043b\u044c \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." }, "error": { diff --git a/homeassistant/components/cert_expiry/.translations/zh-Hant.json b/homeassistant/components/cert_expiry/.translations/zh-Hant.json index c710deae5c1..a14361376df 100644 --- a/homeassistant/components/cert_expiry/.translations/zh-Hant.json +++ b/homeassistant/components/cert_expiry/.translations/zh-Hant.json @@ -21,6 +21,6 @@ "title": "\u5b9a\u7fa9\u8a8d\u8b49\u9032\u884c\u6e2c\u8a66" } }, - "title": "\u8a8d\u8b49\u5df2\u904e\u671f" + "title": "\u6191\u8b49\u671f\u9650" } } \ No newline at end of file diff --git a/homeassistant/components/daikin/.translations/ru.json b/homeassistant/components/daikin/.translations/ru.json index 00a517f701f..c9ab31597d7 100644 --- a/homeassistant/components/daikin/.translations/ru.json +++ b/homeassistant/components/daikin/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "device_fail": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", "device_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." }, diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json index adbe68153f7..6f5513d9729 100644 --- a/homeassistant/components/deconz/.translations/es.json +++ b/homeassistant/components/deconz/.translations/es.json @@ -77,15 +77,21 @@ "remote_button_short_release": "Bot\u00f3n \"{subtype}\" liberado", "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", "remote_double_tap": "Dispositivo \" {subtype} \" doble pulsaci\u00f3n", + "remote_double_tap_any_side": "Dispositivo con doble toque en cualquier lado", "remote_falling": "Dispositivo en ca\u00edda libre", + "remote_flip_180_degrees": "Dispositivo volteado 180 grados", + "remote_flip_90_degrees": "Dispositivo volteado 90 grados", "remote_gyro_activated": "Dispositivo sacudido", "remote_moved": "Dispositivo movido con \"{subtipo}\" hacia arriba", + "remote_moved_any_side": "Dispositivo movido con cualquier lado hacia arriba", "remote_rotate_from_side_1": "Dispositivo girado del \"lado 1\" al \" {subtype} \"", "remote_rotate_from_side_2": "Dispositivo girado del \"lado 2\" al \" {subtype} \"", "remote_rotate_from_side_3": "Dispositivo girado del \"lado 3\" al \" {subtype} \"", "remote_rotate_from_side_4": "Dispositivo girado del \"lado 4\" al \" {subtype} \"", "remote_rotate_from_side_5": "Dispositivo girado del \"lado 5\" al \" {subtype} \"", - "remote_rotate_from_side_6": "Dispositivo girado de \"lado 6\" a \" {subtype} \"" + "remote_rotate_from_side_6": "Dispositivo girado de \"lado 6\" a \" {subtype} \"", + "remote_turned_clockwise": "Dispositivo girado en el sentido de las agujas del reloj", + "remote_turned_counter_clockwise": "Dispositivo girado en sentido contrario a las agujas del reloj" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index 87c3fb646f2..29b584fb9bb 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", "not_deconz_bridge": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c deCONZ.", "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.", diff --git a/homeassistant/components/elgato/.translations/ru.json b/homeassistant/components/elgato/.translations/ru.json index 7f52b5adee5..2b5fb72c507 100644 --- a/homeassistant/components/elgato/.translations/ru.json +++ b/homeassistant/components/elgato/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Elgato Key Light." }, "error": { diff --git a/homeassistant/components/gios/.translations/es.json b/homeassistant/components/gios/.translations/es.json index 9be1581329a..fb9eead7d2c 100644 --- a/homeassistant/components/gios/.translations/es.json +++ b/homeassistant/components/gios/.translations/es.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "La integraci\u00f3n de GIO\u015a para esta estaci\u00f3n de medici\u00f3n ya est\u00e1 configurada." + }, "error": { "cannot_connect": "No se puede conectar al servidor GIO\u015a.", "invalid_sensors_data": "Datos de sensores no v\u00e1lidos para esta estaci\u00f3n de medici\u00f3n.", diff --git a/homeassistant/components/gios/.translations/lb.json b/homeassistant/components/gios/.translations/lb.json index ed42ad3a7ae..3b23ba5eee5 100644 --- a/homeassistant/components/gios/.translations/lb.json +++ b/homeassistant/components/gios/.translations/lb.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "GIO\u015a Integratioun fir d\u00ebs Miess Statioun ass scho konfigur\u00e9iert." + }, "error": { "cannot_connect": "Konnt sech net mam GIO\u015a Server verbannen.", "invalid_sensors_data": "Ong\u00eblteg Sensor Donn\u00e9e\u00eb fir d\u00ebs Miess Statioun", diff --git a/homeassistant/components/homekit_controller/.translations/ru.json b/homeassistant/components/homekit_controller/.translations/ru.json index 44a57a1eb25..41393acb26b 100644 --- a/homeassistant/components/homekit_controller/.translations/ru.json +++ b/homeassistant/components/homekit_controller/.translations/ru.json @@ -3,7 +3,7 @@ "abort": { "accessory_not_found_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435, \u0442\u0430\u043a \u043a\u0430\u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043d\u0430\u0439\u0434\u0435\u043d\u043e.", "already_configured": "\u0410\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440 \u0443\u0436\u0435 \u0441\u0432\u044f\u0437\u0430\u043d \u0441 \u044d\u0442\u0438\u043c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u043e\u043c.", - "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", + "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "already_paired": "\u042d\u0442\u043e\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440 \u0443\u0436\u0435 \u0441\u0432\u044f\u0437\u0430\u043d \u0441 \u0434\u0440\u0443\u0433\u0438\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u0431\u0440\u043e\u0441 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", "ignored_model": "\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430 HomeKit \u0434\u043b\u044f \u044d\u0442\u043e\u0439 \u043c\u043e\u0434\u0435\u043b\u0438 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u0430, \u0442\u0430\u043a \u043a\u0430\u043a \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u043b\u043d\u0430\u044f \u043d\u0430\u0442\u0438\u0432\u043d\u0430\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f.", "invalid_config_entry": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u0433\u043e\u0442\u043e\u0432\u043e\u0435 \u043a \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044e, \u043d\u043e \u0432 Home Assistant \u0443\u0436\u0435 \u0435\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u043b\u0438\u043a\u0442\u0443\u044e\u0449\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0434\u043b\u044f \u043d\u0435\u0433\u043e, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0443\u0434\u0430\u043b\u0438\u0442\u044c.", diff --git a/homeassistant/components/homematicip_cloud/.translations/ru.json b/homeassistant/components/homematicip_cloud/.translations/ru.json index 35f52a7b284..1ba33b0e6ee 100644 --- a/homeassistant/components/homematicip_cloud/.translations/ru.json +++ b/homeassistant/components/homematicip_cloud/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0442\u043e\u0447\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\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." }, diff --git a/homeassistant/components/huawei_lte/.translations/ru.json b/homeassistant/components/huawei_lte/.translations/ru.json index 3850b86167a..c7c9e2033ef 100644 --- a/homeassistant/components/huawei_lte/.translations/ru.json +++ b/homeassistant/components/huawei_lte/.translations/ru.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "not_huawei_lte": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Huawei LTE" }, "error": { diff --git a/homeassistant/components/hue/.translations/ru.json b/homeassistant/components/hue/.translations/ru.json index c749a498e44..3425cb82d01 100644 --- a/homeassistant/components/hue/.translations/ru.json +++ b/homeassistant/components/hue/.translations/ru.json @@ -2,8 +2,8 @@ "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": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443.", "discover_timeout": "\u0428\u043b\u044e\u0437 Philips Hue \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d.", "no_bridges": "\u0428\u043b\u044e\u0437\u044b Philips Hue \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", diff --git a/homeassistant/components/ring/.translations/da.json b/homeassistant/components/ring/.translations/da.json new file mode 100644 index 00000000000..45aebd1ebd5 --- /dev/null +++ b/homeassistant/components/ring/.translations/da.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Enheden er allerede konfigureret" + }, + "error": { + "invalid_auth": "Ugyldig godkendelse", + "unknown": "Uventet fejl" + }, + "step": { + "2fa": { + "data": { + "2fa": "Tofaktorkode" + }, + "title": "Tofaktorgodkendelse" + }, + "user": { + "data": { + "password": "Adgangskode", + "username": "Brugernavn" + }, + "title": "Log ind med Ring-konto" + } + }, + "title": "Ring" + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/.translations/en.json b/homeassistant/components/ring/.translations/en.json index db4665b6c0a..54caa8f96e7 100644 --- a/homeassistant/components/ring/.translations/en.json +++ b/homeassistant/components/ring/.translations/en.json @@ -4,7 +4,6 @@ "already_configured": "Device is already configured" }, "error": { - "cannot_connect": "Failed to connect, please try again", "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, @@ -13,14 +12,14 @@ "data": { "2fa": "Two-factor code" }, - "title": "Enter two-factor authentication" + "title": "Two-factor authentication" }, "user": { "data": { "password": "Password", "username": "Username" }, - "title": "Connect to the device" + "title": "Sign-in with Ring account" } }, "title": "Ring" diff --git a/homeassistant/components/samsungtv/.translations/da.json b/homeassistant/components/samsungtv/.translations/da.json new file mode 100644 index 00000000000..594127688c2 --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/da.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Dette Samsung-tv er allerede konfigureret.", + "already_in_progress": "Samsung-tv-konfiguration er allerede i gang.", + "auth_missing": "Home Assistant er ikke godkendt til at oprette forbindelse til dette Samsung-tv.", + "not_found": "Der blev ikke fundet nogen underst\u00f8ttede Samsung-tv-enheder p\u00e5 netv\u00e6rket.", + "not_supported": "Dette Samsung TV underst\u00f8ttes i \u00f8jeblikket ikke." + }, + "step": { + "confirm": { + "description": "Vil du konfigurere Samsung-tv {model}? Hvis du aldrig har oprettet forbindelse til Home Assistant f\u00f8r, b\u00f8r du se en popup p\u00e5 dit tv, der beder om godkendelse. Manuelle konfigurationer for dette tv vil blive overskrevet.", + "title": "Samsung-tv" + }, + "user": { + "data": { + "host": "V\u00e6rt eller IP-adresse", + "name": "Navn" + }, + "description": "Indtast dine Samsung-tv-oplysninger. Hvis du aldrig har oprettet forbindelse til Home Assistant f\u00f8r, b\u00f8r du se en popup p\u00e5 dit tv, der beder om godkendelse.", + "title": "Samsung-tv" + } + }, + "title": "Samsung-tv" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/en.json b/homeassistant/components/samsungtv/.translations/en.json new file mode 100644 index 00000000000..24ab81c007c --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "This Samsung TV is already configured.", + "already_in_progress": "Samsung TV configuration is already in progress.", + "auth_missing": "Home Assistant is not authenticated to connect to this Samsung TV.", + "not_found": "No supported Samsung TV devices found on the network.", + "not_supported": "This Samsung TV devices is currently not supported." + }, + "step": { + "confirm": { + "description": "Do you want to set up Samsung TV {model}? If you never connected Home Assistant before you should see a popup on your TV asking for authentication. Manual configurations for this TV will be overwritten.", + "title": "Samsung TV" + }, + "user": { + "data": { + "host": "Host or IP address", + "name": "Name" + }, + "description": "Enter your Samsung TV information. If you never connected Home Assistant before you should see a popup on your TV asking for authentication.", + "title": "Samsung TV" + } + }, + "title": "Samsung TV" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/es.json b/homeassistant/components/samsungtv/.translations/es.json new file mode 100644 index 00000000000..3535d4bc65f --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/es.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Este televisor Samsung ya est\u00e1 configurado.", + "already_in_progress": "La configuraci\u00f3n del televisor Samsung ya est\u00e1 en progreso.", + "auth_missing": "Home Assistant no est\u00e1 autenticado para conectarse a este televisor Samsung.", + "not_found": "No se encontraron televisiones Samsung compatibles en la red.", + "not_supported": "Esta televisi\u00f3n Samsung actualmente no es compatible." + }, + "step": { + "confirm": { + "description": "\u00bfDesea configurar el televisor Samsung {model} ? Si nunca conect\u00f3 Home Assistant antes, deber\u00eda ver una ventana emergente en su televisor pidiendo autenticaci\u00f3n. Las configuraciones manuales para este televisor se sobrescribir\u00e1n.", + "title": "Samsung TV" + }, + "user": { + "data": { + "host": "Host o direcci\u00f3n IP", + "name": "Nombre" + }, + "description": "Introduce la informaci\u00f3n de tu televisor Samsung. Si nunca conect\u00f3 Home Assistant antes de ver una ventana emergente en su televisor pidiendo autenticaci\u00f3n.", + "title": "Samsung TV" + } + }, + "title": "Samsung TV" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/it.json b/homeassistant/components/samsungtv/.translations/it.json new file mode 100644 index 00000000000..c783db24720 --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/it.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Questo Samsung TV \u00e8 gi\u00e0 configurato.", + "already_in_progress": "La configurazione di Samsung TV \u00e8 gi\u00e0 in corso.", + "auth_missing": "Home Assistant non \u00e8 autenticato per connettersi a questo Samsung TV.", + "not_found": "Nessun dispositivo Samsung TV supportato trovato sulla rete.", + "not_supported": "Questo dispositivo Samsung TV non \u00e8 attualmente supportato." + }, + "step": { + "confirm": { + "description": "Vuoi configurare Samsung TV {model} ? Se non hai mai collegato Home Assistant dovresti vedere un popup sul televisore in cui viene richiesta l'autenticazione. Le configurazioni manuali per questo televisore verranno sovrascritte.", + "title": "Samsung TV" + }, + "user": { + "data": { + "host": "Host o indirizzo IP", + "name": "Nome" + }, + "description": "Inserisci le informazioni del tuo Samsung TV. Se non hai mai connesso Home Assistant dovresti vedere un popup sul televisore in cui viene richiesta l'autenticazione.", + "title": "Samsung TV" + } + }, + "title": "Samsung TV" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/lb.json b/homeassistant/components/samsungtv/.translations/lb.json new file mode 100644 index 00000000000..fe1f02e55ea --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/lb.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00ebs Samsung TV ass scho konfigur\u00e9iert.", + "already_in_progress": "Konfiguratioun fir d\u00ebs Samsung TV ass schonn am gaang.", + "auth_missing": "Home Assistant ass net authentifiz\u00e9iert fir sech mat d\u00ebsem Samsung TV ze verbannen.", + "not_found": "Keng \u00ebnnerst\u00ebtzte Samsung TV am Netzwierk fonnt.", + "not_supported": "D\u00ebsen Samsung TV Modell g\u00ebtt momentan net \u00ebnnerst\u00ebtzt" + }, + "step": { + "confirm": { + "description": "W\u00ebllt dir de Samsung TV {model} ariichten?. Falls dir Home Assistant nach ni domat verbonnen hutt misst den TV eng Meldung mat enger Authentifiz\u00e9ierung uweisen. Manuell Konfiguratioun g\u00ebtt iwwerschriwwen.", + "title": "Samsnung TV" + }, + "user": { + "data": { + "host": "Numm oder IP Adresse", + "name": "Numm" + }, + "description": "Gitt \u00e4r Samsung TV Informatiounen un. Falls dir Home Assistant nach ni domat verbonnen hutt misst den TV eng Meldung mat enger Authentifiz\u00e9ierung uweisen.", + "title": "Samsnung TV" + } + }, + "title": "Samsung TV" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/no.json b/homeassistant/components/samsungtv/.translations/no.json new file mode 100644 index 00000000000..dcd437642b2 --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Denne Samsung TV-en er allerede konfigurert.", + "already_in_progress": "Samsung TV-konfigurasjon p\u00e5g\u00e5r allerede.", + "auth_missing": "Home Assistant er ikke autentisert for \u00e5 koble til denne Samsung TV-en.", + "not_found": "Ingen st\u00f8ttede Samsung TV-enheter funnet i nettverket.", + "not_supported": "Denne Samsung TV-enhetene st\u00f8ttes forel\u00f8pig ikke." + }, + "step": { + "confirm": { + "description": "Vil du sette opp Samsung TV {model} ? Hvis du aldri koblet til Home Assistant f\u00f8r, vil en popup p\u00e5 TVen be om godkjenning. Manuelle konfigurasjoner for denne TVen vil bli overskrevet.", + "title": "Samsung TV" + }, + "user": { + "data": { + "host": "Vert eller IP-adresse", + "name": "Navn" + }, + "description": "Skriv inn Samsung TV-informasjonen din. Hvis du aldri koblet til Home Assistant f\u00f8r, vil en popup p\u00e5 TVen be om godkjenning.", + "title": "Samsung TV" + } + }, + "title": "Samsung TV" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/ru.json b/homeassistant/components/samsungtv/.translations/ru.json new file mode 100644 index 00000000000..d5dd11a1b80 --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/ru.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "auth_missing": "Home Assistant \u043d\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u043d \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u044d\u0442\u043e\u043c\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.", + "not_found": "\u0412 \u0441\u0435\u0442\u0438 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432.", + "not_supported": "\u042d\u0442\u0430 \u043c\u043e\u0434\u0435\u043b\u044c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." + }, + "step": { + "confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Samsung {model}? \u0415\u0441\u043b\u0438 \u044d\u0442\u043e\u0442 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0440\u0430\u043d\u0435\u0435 \u043d\u0435 \u0431\u044b\u043b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043a Home Assistant, \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u043e\u044f\u0432\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u043f\u043b\u044b\u0432\u0430\u044e\u0449\u0435\u0435 \u043e\u043a\u043d\u043e \u0441 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043d\u044b\u0435 \u0432\u0440\u0443\u0447\u043d\u0443\u044e, \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u0430\u043d\u044b.", + "title": "\u0422\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Samsung" + }, + "user": { + "data": { + "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435 Samsung. \u0415\u0441\u043b\u0438 \u044d\u0442\u043e\u0442 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0440\u0430\u043d\u0435\u0435 \u043d\u0435 \u0431\u044b\u043b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043a Home Assistant, \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u043e\u044f\u0432\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u043f\u043b\u044b\u0432\u0430\u044e\u0449\u0435\u0435 \u043e\u043a\u043d\u043e \u0441 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "title": "\u0422\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Samsung" + } + }, + "title": "\u0422\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Samsung" + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/ru.json b/homeassistant/components/solarlog/.translations/ru.json index 7f40935e5a5..b64496c4591 100644 --- a/homeassistant/components/solarlog/.translations/ru.json +++ b/homeassistant/components/solarlog/.translations/ru.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430." }, "step": { diff --git a/homeassistant/components/toon/.translations/ru.json b/homeassistant/components/toon/.translations/ru.json index 427f717e3ad..75b46d3f600 100644 --- a/homeassistant/components/toon/.translations/ru.json +++ b/homeassistant/components/toon/.translations/ru.json @@ -9,7 +9,7 @@ }, "error": { "credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", - "display_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0434\u0438\u0441\u043f\u043b\u0435\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + "display_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "step": { "authenticate": { diff --git a/homeassistant/components/tradfri/.translations/ru.json b/homeassistant/components/tradfri/.translations/ru.json index 2e3dc8331be..7d2925fd3f2 100644 --- a/homeassistant/components/tradfri/.translations/ru.json +++ b/homeassistant/components/tradfri/.translations/ru.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430." + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443.", diff --git a/homeassistant/components/wled/.translations/ru.json b/homeassistant/components/wled/.translations/ru.json index cd4c3c3b066..a884a20b337 100644 --- a/homeassistant/components/wled/.translations/ru.json +++ b/homeassistant/components/wled/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." }, "error": { From 86bdba2ea262c7ff82727fcb2aada02b8fd42064 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 10 Jan 2020 20:18:35 -0800 Subject: [PATCH 16/41] Bumped version to 0.104.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a5ad2e4991b..3f62458a0a5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 104 -PATCH_VERSION = "0b1" +PATCH_VERSION = "0b2" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From 718f3438ea4b71f94df9364f7f28ac6323fad526 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 11 Jan 2020 16:04:39 -0800 Subject: [PATCH 17/41] Upgrade Ring to new version (#30666) * Upgrade Ring to new version * Move legacy cleanup down * Fix test --- homeassistant/components/ring/__init__.py | 61 ++++++++----------- .../components/ring/binary_sensor.py | 15 ++++- homeassistant/components/ring/camera.py | 16 +++-- homeassistant/components/ring/config_flow.py | 25 +++----- homeassistant/components/ring/light.py | 13 +++- homeassistant/components/ring/manifest.json | 2 +- homeassistant/components/ring/sensor.py | 17 ++++-- homeassistant/components/ring/switch.py | 13 +++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ring/common.py | 4 +- tests/components/ring/conftest.py | 11 +--- tests/components/ring/test_binary_sensor.py | 15 +---- tests/components/ring/test_config_flow.py | 10 ++- tests/components/ring/test_init.py | 10 +-- tests/components/ring/test_sensor.py | 28 +++------ 16 files changed, 121 insertions(+), 123 deletions(-) diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index 18c753f4dc9..7addc116b06 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -5,8 +5,7 @@ from functools import partial import logging from pathlib import Path -from requests.exceptions import ConnectTimeout, HTTPError -from ring_doorbell import Ring +from ring_doorbell import Auth, Ring import voluptuous as vol from homeassistant import config_entries @@ -14,6 +13,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import track_time_interval +from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) @@ -28,7 +28,6 @@ DATA_RING_CHIMES = "ring_chimes" DATA_TRACK_INTERVAL = "ring_track_interval" DOMAIN = "ring" -DEFAULT_CACHEDB = ".ring_cache.pickle" DEFAULT_ENTITY_NAMESPACE = "ring" SIGNAL_UPDATE_RING = "ring_update" @@ -54,6 +53,14 @@ async def async_setup(hass, config): if DOMAIN not in config: return True + def legacy_cleanup(): + """Clean up old tokens.""" + old_cache = Path(hass.config.path(".ring_cache.pickle")) + if old_cache.is_file(): + old_cache.unlink() + + await hass.async_add_executor_job(legacy_cleanup) + hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, @@ -69,30 +76,20 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up a config entry.""" - cache = hass.config.path(DEFAULT_CACHEDB) - try: - ring = await hass.async_add_executor_job( - partial( - Ring, - username=entry.data["username"], - password="invalid-password", - cache_file=cache, - ) - ) - except (ConnectTimeout, HTTPError) as ex: - _LOGGER.error("Unable to connect to Ring service: %s", str(ex)) - hass.components.persistent_notification.async_create( - "Error: {}
" - "You will need to restart hass after fixing." - "".format(ex), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - return False - if not ring.is_connected: - _LOGGER.error("Unable to connect to Ring service") - return False + def token_updater(token): + """Handle from sync context when token is updated.""" + run_callback_threadsafe( + hass.loop, + partial( + hass.config_entries.async_update_entry, + entry, + data={**entry.data, "token": token}, + ), + ).result() + + auth = Auth(entry.data["token"], token_updater) + ring = Ring(auth) await hass.async_add_executor_job(finish_setup_entry, hass, ring) @@ -106,9 +103,10 @@ async def async_setup_entry(hass, entry): def finish_setup_entry(hass, ring): """Finish setting up entry.""" - hass.data[DATA_RING_CHIMES] = chimes = ring.chimes - hass.data[DATA_RING_DOORBELLS] = doorbells = ring.doorbells - hass.data[DATA_RING_STICKUP_CAMS] = stickup_cams = ring.stickup_cams + devices = ring.devices + hass.data[DATA_RING_CHIMES] = chimes = devices["chimes"] + hass.data[DATA_RING_DOORBELLS] = doorbells = devices["doorbells"] + hass.data[DATA_RING_STICKUP_CAMS] = stickup_cams = devices["stickup_cams"] ring_devices = chimes + doorbells + stickup_cams @@ -160,8 +158,3 @@ async def async_unload_entry(hass, entry): hass.data.pop(DATA_TRACK_INTERVAL) return unload_ok - - -async def async_remove_entry(hass, entry): - """Act when an entry is removed.""" - await hass.async_add_executor_job(Path(hass.config.path(DEFAULT_CACHEDB)).unlink) diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 0706752ffb2..29337f29689 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -5,7 +5,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.const import ATTR_ATTRIBUTION -from . import ATTRIBUTION, DATA_RING_DOORBELLS, DATA_RING_STICKUP_CAMS +from . import ATTRIBUTION, DATA_RING_DOORBELLS, DATA_RING_STICKUP_CAMS, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -72,14 +72,23 @@ class RingBinarySensor(BinarySensorDevice): """Return a unique ID.""" return self._unique_id + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._data.id)}, + "sw_version": self._data.firmware, + "name": self._data.name, + "model": self._data.kind, + "manufacturer": "Ring", + } + @property def device_state_attributes(self): """Return the state attributes.""" attrs = {} attrs[ATTR_ATTRIBUTION] = ATTRIBUTION - attrs["device_id"] = self._data.id - attrs["firmware"] = self._data.firmware attrs["timezone"] = self._data.timezone if self._data.alert and self._data.alert_expires_at: diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index a3b34afa056..2b0fe14a1d4 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -18,6 +18,7 @@ from . import ( ATTRIBUTION, DATA_RING_DOORBELLS, DATA_RING_STICKUP_CAMS, + DOMAIN, SIGNAL_UPDATE_RING, ) @@ -86,16 +87,23 @@ class RingCam(Camera): """Return a unique ID.""" return self._camera.id + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._camera.id)}, + "sw_version": self._camera.firmware, + "name": self._camera.name, + "model": self._camera.kind, + "manufacturer": "Ring", + } + @property def device_state_attributes(self): """Return the state attributes.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, - "device_id": self._camera.id, - "firmware": self._camera.firmware, - "kind": self._camera.kind, "timezone": self._camera.timezone, - "type": self._camera.family, "video_url": self._video_url, "last_video_id": self._last_video_id, } diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py index bdb60cc26c5..98555277baf 100644 --- a/homeassistant/components/ring/config_flow.py +++ b/homeassistant/components/ring/config_flow.py @@ -1,21 +1,19 @@ """Config flow for Ring integration.""" -from functools import partial import logging from oauthlib.oauth2 import AccessDeniedError -from ring_doorbell import Ring +from ring_doorbell import Auth import voluptuous as vol from homeassistant import config_entries, core, exceptions -from . import DEFAULT_CACHEDB, DOMAIN # pylint: disable=unused-import +from . import DOMAIN # pylint: disable=unused-import _LOGGER = logging.getLogger(__name__) async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect.""" - cache = hass.config.path(DEFAULT_CACHEDB) def otp_callback(): if "2fa" in data: @@ -23,21 +21,16 @@ async def validate_input(hass: core.HomeAssistant, data): raise Require2FA + auth = Auth() + try: - ring = await hass.async_add_executor_job( - partial( - Ring, - username=data["username"], - password=data["password"], - cache_file=cache, - auth_callback=otp_callback, - ) + token = await hass.async_add_executor_job( + auth.fetch_token, data["username"], data["password"], otp_callback, ) except AccessDeniedError: raise InvalidAuth - if not ring.is_connected: - raise InvalidAuth + return token class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -56,12 +49,12 @@ class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: try: - await validate_input(self.hass, user_input) + token = await validate_input(self.hass, user_input) await self.async_set_unique_id(user_input["username"]) return self.async_create_entry( title=user_input["username"], - data={"username": user_input["username"]}, + data={"username": user_input["username"], "token": token}, ) except Require2FA: self.user_pass = user_input diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index 1b360f24f1f..b7fa67a391f 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -7,7 +7,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util -from . import DATA_RING_STICKUP_CAMS, SIGNAL_UPDATE_RING +from . import DATA_RING_STICKUP_CAMS, DOMAIN, SIGNAL_UPDATE_RING _LOGGER = logging.getLogger(__name__) @@ -84,6 +84,17 @@ class RingLight(Light): """If the switch is currently on or off.""" return self._light_on + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._device.id)}, + "sw_version": self._device.firmware, + "name": self._device.name, + "model": self._device.kind, + "manufacturer": "Ring", + } + def _set_light(self, new_state): """Update light state, and causes Home Assistant to correctly update.""" self._device.lights = new_state diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index b8a3c26bd8b..d6570fad5cb 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -2,7 +2,7 @@ "domain": "ring", "name": "Ring", "documentation": "https://www.home-assistant.io/integrations/ring", - "requirements": ["ring_doorbell==0.2.9"], + "requirements": ["ring_doorbell==0.4.0"], "dependencies": ["ffmpeg"], "codeowners": [], "config_flow": true diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 532f15f94c1..89b042ba862 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -12,6 +12,7 @@ from . import ( DATA_RING_CHIMES, DATA_RING_DOORBELLS, DATA_RING_STICKUP_CAMS, + DOMAIN, SIGNAL_UPDATE_RING, ) @@ -108,6 +109,7 @@ class RingSensor(Entity): self._disp_disconnect = async_dispatcher_connect( self.hass, SIGNAL_UPDATE_RING, self._update_callback ) + await self.hass.async_add_executor_job(self._data.update) async def async_will_remove_from_hass(self): """Disconnect callbacks.""" @@ -140,17 +142,24 @@ class RingSensor(Entity): """Return a unique ID.""" return self._unique_id + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._data.id)}, + "sw_version": self._data.firmware, + "name": self._data.name, + "model": self._data.kind, + "manufacturer": "Ring", + } + @property def device_state_attributes(self): """Return the state attributes.""" attrs = {} attrs[ATTR_ATTRIBUTION] = ATTRIBUTION - attrs["device_id"] = self._data.id - attrs["firmware"] = self._data.firmware - attrs["kind"] = self._data.kind attrs["timezone"] = self._data.timezone - attrs["type"] = self._data.family attrs["wifi_name"] = self._data.wifi_name if self._extra and self._sensor_type.startswith("last_"): diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index 51c9e64377b..e23e757d825 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -7,7 +7,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util -from . import DATA_RING_STICKUP_CAMS, SIGNAL_UPDATE_RING +from . import DATA_RING_STICKUP_CAMS, DOMAIN, SIGNAL_UPDATE_RING _LOGGER = logging.getLogger(__name__) @@ -76,6 +76,17 @@ class BaseRingSwitch(SwitchDevice): """Update controlled via the hub.""" return False + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._device.id)}, + "sw_version": self._device.firmware, + "name": self._device.name, + "model": self._device.kind, + "manufacturer": "Ring", + } + class SirenSwitch(BaseRingSwitch): """Creates a switch to turn the ring cameras siren on and off.""" diff --git a/requirements_all.txt b/requirements_all.txt index d7cceb5d8b9..253014c211a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1753,7 +1753,7 @@ rfk101py==0.0.1 rflink==0.0.50 # homeassistant.components.ring -ring_doorbell==0.2.9 +ring_doorbell==0.4.0 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c52ca769b78..13a73e4c1b3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -567,7 +567,7 @@ restrictedpython==5.0 rflink==0.0.50 # homeassistant.components.ring -ring_doorbell==0.2.9 +ring_doorbell==0.4.0 # homeassistant.components.yamaha rxv==0.6.0 diff --git a/tests/components/ring/common.py b/tests/components/ring/common.py index 1afc597415e..93a6e4f91e0 100644 --- a/tests/components/ring/common.py +++ b/tests/components/ring/common.py @@ -9,7 +9,9 @@ from tests.common import MockConfigEntry async def setup_platform(hass, platform): """Set up the ring platform and prerequisites.""" - MockConfigEntry(domain=DOMAIN, data={"username": "foo"}).add_to_hass(hass) + MockConfigEntry(domain=DOMAIN, data={"username": "foo", "token": {}}).add_to_hass( + hass + ) with patch("homeassistant.components.ring.PLATFORMS", [platform]): assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() diff --git a/tests/components/ring/conftest.py b/tests/components/ring/conftest.py index e4b516496e7..a4cfaf0065d 100644 --- a/tests/components/ring/conftest.py +++ b/tests/components/ring/conftest.py @@ -1,21 +1,12 @@ """Configuration for Ring tests.""" -from asynctest import patch import pytest import requests_mock from tests.common import load_fixture -@pytest.fixture(name="ring_mock") -def ring_save_mock(): - """Fixture to mock a ring.""" - with patch("ring_doorbell._exists_cache", return_value=False): - with patch("ring_doorbell._save_cache", return_value=True) as save_mock: - yield save_mock - - @pytest.fixture(name="requests_mock") -def requests_mock_fixture(ring_mock): +def requests_mock_fixture(): """Fixture to provide a requests mocker.""" with requests_mock.mock() as mock: # Note all devices have an id of 987652, but a different device_id. diff --git a/tests/components/ring/test_binary_sensor.py b/tests/components/ring/test_binary_sensor.py index 5a04017f54b..4ca83b2451b 100644 --- a/tests/components/ring/test_binary_sensor.py +++ b/tests/components/ring/test_binary_sensor.py @@ -1,6 +1,5 @@ """The tests for the Ring binary sensor platform.""" from asyncio import run_coroutine_threadsafe -import os import unittest from unittest.mock import patch @@ -9,12 +8,7 @@ import requests_mock from homeassistant.components import ring as base_ring from homeassistant.components.ring import binary_sensor as ring -from tests.common import ( - get_test_config_dir, - get_test_home_assistant, - load_fixture, - mock_storage, -) +from tests.common import get_test_home_assistant, load_fixture, mock_storage from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG @@ -28,15 +22,9 @@ class TestRingBinarySensorSetup(unittest.TestCase): for device in devices: self.DEVICES.append(device) - def cleanup(self): - """Cleanup any data created from the tests.""" - if os.path.isfile(self.cache): - os.remove(self.cache) - def setUp(self): """Initialize values for this testcase class.""" self.hass = get_test_home_assistant() - self.cache = get_test_config_dir(base_ring.DEFAULT_CACHEDB) self.config = { "username": "foo", "password": "bar", @@ -46,7 +34,6 @@ class TestRingBinarySensorSetup(unittest.TestCase): def tearDown(self): """Stop everything that was started.""" self.hass.stop() - self.cleanup() @requests_mock.Mocker() def test_binary_sensor(self, mock): diff --git a/tests/components/ring/test_config_flow.py b/tests/components/ring/test_config_flow.py index 46925069c31..5712106333f 100644 --- a/tests/components/ring/test_config_flow.py +++ b/tests/components/ring/test_config_flow.py @@ -18,8 +18,10 @@ async def test_form(hass): assert result["errors"] == {} with patch( - "homeassistant.components.ring.config_flow.Ring", - return_value=Mock(is_connected=True), + "homeassistant.components.ring.config_flow.Auth", + return_value=Mock( + fetch_token=Mock(return_value={"access_token": "mock-token"}) + ), ), patch( "homeassistant.components.ring.async_setup", return_value=mock_coro(True) ) as mock_setup, patch( @@ -34,6 +36,7 @@ async def test_form(hass): assert result2["title"] == "hello@home-assistant.io" assert result2["data"] == { "username": "hello@home-assistant.io", + "token": {"access_token": "mock-token"}, } await hass.async_block_till_done() assert len(mock_setup.mock_calls) == 1 @@ -47,7 +50,8 @@ async def test_form_invalid_auth(hass): ) with patch( - "homeassistant.components.ring.config_flow.Ring", side_effect=InvalidAuth, + "homeassistant.components.ring.config_flow.Auth.fetch_token", + side_effect=InvalidAuth, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/ring/test_init.py b/tests/components/ring/test_init.py index cfc19da78bf..809c71562c0 100644 --- a/tests/components/ring/test_init.py +++ b/tests/components/ring/test_init.py @@ -2,7 +2,6 @@ from asyncio import run_coroutine_threadsafe from copy import deepcopy from datetime import timedelta -import os import unittest import requests_mock @@ -10,7 +9,7 @@ import requests_mock from homeassistant import setup import homeassistant.components.ring as ring -from tests.common import get_test_config_dir, get_test_home_assistant, load_fixture +from tests.common import get_test_home_assistant, load_fixture ATTRIBUTION = "Data provided by Ring.com" @@ -22,21 +21,14 @@ VALID_CONFIG = { class TestRing(unittest.TestCase): """Tests the Ring component.""" - def cleanup(self): - """Cleanup any data created from the tests.""" - if os.path.isfile(self.cache): - os.remove(self.cache) - def setUp(self): """Initialize values for this test case class.""" self.hass = get_test_home_assistant() - self.cache = get_test_config_dir(ring.DEFAULT_CACHEDB) self.config = VALID_CONFIG def tearDown(self): # pylint: disable=invalid-name """Stop everything that was started.""" self.hass.stop() - self.cleanup() @requests_mock.Mocker() def test_setup(self, mock): diff --git a/tests/components/ring/test_sensor.py b/tests/components/ring/test_sensor.py index 0102020e3c2..039c9d0625f 100644 --- a/tests/components/ring/test_sensor.py +++ b/tests/components/ring/test_sensor.py @@ -1,6 +1,5 @@ """The tests for the Ring sensor platform.""" from asyncio import run_coroutine_threadsafe -import os import unittest from unittest.mock import patch @@ -10,12 +9,7 @@ from homeassistant.components import ring as base_ring import homeassistant.components.ring.sensor as ring from homeassistant.helpers.icon import icon_for_battery_level -from tests.common import ( - get_test_config_dir, - get_test_home_assistant, - load_fixture, - mock_storage, -) +from tests.common import get_test_home_assistant, load_fixture, mock_storage from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG @@ -29,15 +23,9 @@ class TestRingSensorSetup(unittest.TestCase): for device in devices: self.DEVICES.append(device) - def cleanup(self): - """Cleanup any data created from the tests.""" - if os.path.isfile(self.cache): - os.remove(self.cache) - def setUp(self): """Initialize values for this testcase class.""" self.hass = get_test_home_assistant() - self.cache = get_test_config_dir(base_ring.DEFAULT_CACHEDB) self.config = { "username": "foo", "password": "bar", @@ -55,7 +43,6 @@ class TestRingSensorSetup(unittest.TestCase): def tearDown(self): """Stop everything that was started.""" self.hass.stop() - self.cleanup() @requests_mock.Mocker() def test_sensor(self, mock): @@ -97,6 +84,13 @@ class TestRingSensorSetup(unittest.TestCase): ).result() for device in self.DEVICES: + # Mimick add to hass + device.hass = self.hass + run_coroutine_threadsafe( + device.async_added_to_hass(), self.hass.loop, + ).result() + + # Entity update data from ring data device.update() if device.name == "Front Battery": expected_icon = icon_for_battery_level( @@ -104,18 +98,12 @@ class TestRingSensorSetup(unittest.TestCase): ) assert device.icon == expected_icon assert 80 == device.state - assert "hp_cam_v1" == device.device_state_attributes["kind"] - assert "stickup_cams" == device.device_state_attributes["type"] if device.name == "Front Door Battery": assert 100 == device.state - assert "lpd_v1" == device.device_state_attributes["kind"] - assert "chimes" != device.device_state_attributes["type"] if device.name == "Downstairs Volume": assert 2 == device.state - assert "1.2.3" == device.device_state_attributes["firmware"] assert "ring_mock_wifi" == device.device_state_attributes["wifi_name"] assert "mdi:bell-ring" == device.icon - assert "chimes" == device.device_state_attributes["type"] if device.name == "Front Door Last Activity": assert not device.device_state_attributes["answered"] assert "America/New_York" == device.device_state_attributes["timezone"] From e3b37c44d66b23b61b3f05a4132a588c5840a2de Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 11 Jan 2020 16:17:03 -0800 Subject: [PATCH 18/41] Bumped version to 0.104.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3f62458a0a5..b5f95265698 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 104 -PATCH_VERSION = "0b2" +PATCH_VERSION = "0b3" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From b383a5195fb60c822eab536c3be3da7b6bb6aec0 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sun, 12 Jan 2020 05:01:13 +0100 Subject: [PATCH 19/41] Fix Error with HomematicIP Cloud Cover (#30667) --- homeassistant/components/homematicip_cloud/cover.py | 8 ++++++-- tests/components/homematicip_cloud/test_cover.py | 6 +++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index e3efe9a9508..32f38637e36 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -58,7 +58,9 @@ class HomematicipCoverShutter(HomematicipGenericDevice, CoverDevice): @property def current_cover_position(self) -> int: """Return current position of cover.""" - return int((1 - self._device.shutterLevel) * 100) + if self._device.shutterLevel is not None: + return int((1 - self._device.shutterLevel) * 100) + return None async def async_set_cover_position(self, **kwargs) -> None: """Move the cover to a specific position.""" @@ -93,7 +95,9 @@ class HomematicipCoverSlats(HomematicipCoverShutter, CoverDevice): @property def current_cover_tilt_position(self) -> int: """Return current tilt position of cover.""" - return int((1 - self._device.slatsLevel) * 100) + if self._device.slatsLevel is not None: + return int((1 - self._device.slatsLevel) * 100) + return None async def async_set_cover_tilt_position(self, **kwargs) -> None: """Move the cover to a specific tilt position.""" diff --git a/tests/components/homematicip_cloud/test_cover.py b/tests/components/homematicip_cloud/test_cover.py index 728d60d5501..5b267628ae3 100644 --- a/tests/components/homematicip_cloud/test_cover.py +++ b/tests/components/homematicip_cloud/test_cover.py @@ -7,7 +7,7 @@ from homeassistant.components.cover import ( DOMAIN as COVER_DOMAIN, ) from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN -from homeassistant.const import STATE_CLOSED, STATE_OPEN +from homeassistant.const import STATE_CLOSED, STATE_OPEN, STATE_UNKNOWN from homeassistant.setup import async_setup_component from .helper import async_manipulate_test_data, get_and_check_entity_basics @@ -87,7 +87,7 @@ async def test_hmip_cover_shutter(hass, default_mock_hap): await async_manipulate_test_data(hass, hmip_device, "shutterLevel", None) ha_state = hass.states.get(entity_id) - assert ha_state.state == STATE_CLOSED + assert ha_state.state == STATE_UNKNOWN async def test_hmip_cover_slats(hass, default_mock_hap): @@ -154,7 +154,7 @@ async def test_hmip_cover_slats(hass, default_mock_hap): await async_manipulate_test_data(hass, hmip_device, "shutterLevel", None) ha_state = hass.states.get(entity_id) - assert ha_state.state == STATE_OPEN + assert ha_state.state == STATE_UNKNOWN async def test_hmip_garage_door_tormatic(hass, default_mock_hap): From c00af14ee2c63b9d9c545568f9312aaf7be0827b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 11 Jan 2020 18:22:08 -0800 Subject: [PATCH 20/41] Ring OTP improvement (#30688) * Fix otp flow * Update Ring to 0.5 Co-authored-by: steve-gombos <3118886+steve-gombos@users.noreply.github.com> --- homeassistant/components/ring/config_flow.py | 12 ++++-------- homeassistant/components/ring/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py index 98555277baf..6d177a4db49 100644 --- a/homeassistant/components/ring/config_flow.py +++ b/homeassistant/components/ring/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Ring integration.""" import logging -from oauthlib.oauth2 import AccessDeniedError +from oauthlib.oauth2 import AccessDeniedError, MissingTokenError from ring_doorbell import Auth import voluptuous as vol @@ -15,18 +15,14 @@ _LOGGER = logging.getLogger(__name__) async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect.""" - def otp_callback(): - if "2fa" in data: - return data["2fa"] - - raise Require2FA - auth = Auth() try: token = await hass.async_add_executor_job( - auth.fetch_token, data["username"], data["password"], otp_callback, + auth.fetch_token, data["username"], data["password"], data.get("2fa"), ) + except MissingTokenError: + raise Require2FA except AccessDeniedError: raise InvalidAuth diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index d6570fad5cb..fccbf9a5319 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -2,7 +2,7 @@ "domain": "ring", "name": "Ring", "documentation": "https://www.home-assistant.io/integrations/ring", - "requirements": ["ring_doorbell==0.4.0"], + "requirements": ["ring_doorbell==0.5.0"], "dependencies": ["ffmpeg"], "codeowners": [], "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index 253014c211a..35dceaf2a9c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1753,7 +1753,7 @@ rfk101py==0.0.1 rflink==0.0.50 # homeassistant.components.ring -ring_doorbell==0.4.0 +ring_doorbell==0.5.0 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 13a73e4c1b3..eb48440be14 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -567,7 +567,7 @@ restrictedpython==5.0 rflink==0.0.50 # homeassistant.components.ring -ring_doorbell==0.4.0 +ring_doorbell==0.5.0 # homeassistant.components.yamaha rxv==0.6.0 From dc57d670d8292e6961a54ea003815eafe15ecdbb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 11 Jan 2020 18:21:57 -0800 Subject: [PATCH 21/41] Log error when integration is missing platform setup (#30690) --- homeassistant/helpers/entity_platform.py | 10 ++++++++++ tests/helpers/test_entity_platform.py | 14 ++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index b8fef8deca2..0560cf84fb3 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -83,6 +83,16 @@ class EntityPlatform: platform = self.platform hass = self.hass + if not hasattr(platform, "async_setup_platform") and not hasattr( + platform, "setup_platform" + ): + self.logger.error( + "The %s platform for the %s integration does not support platform setup. Please remove it from your config.", + self.platform_name, + self.domain, + ) + return + @callback def async_create_setup_task(): """Get task to set up platform.""" diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 97534d7dff7..7797bf5057b 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -834,3 +834,17 @@ async def test_override_restored_entities(hass): state = hass.states.get("test_domain.world") assert state.state == "on" + + +async def test_platform_with_no_setup(hass, caplog): + """Test setting up a platform that doesnt' support setup.""" + entity_platform = MockEntityPlatform( + hass, domain="mock-integration", platform_name="mock-platform", platform=None + ) + + await entity_platform.async_setup(None) + + assert ( + "The mock-platform platform for the mock-integration integration does not support platform setup." + in caplog.text + ) From 3715f821d74855734bdccc1b39ca186b04fbf393 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 11 Jan 2020 19:59:43 -0800 Subject: [PATCH 22/41] Fix update person validation (#30691) --- homeassistant/components/person/__init__.py | 2 +- tests/components/person/test_init.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index fcf3bee45b9..c34fb89a718 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -214,7 +214,7 @@ class PersonStorageCollection(collection.StorageCollection): user_id = update_data.get(CONF_USER_ID) - if user_id is not None: + if user_id is not None and user_id != data.get(CONF_USER_ID): await self._validate_user_id(user_id) return {**data, **update_data} diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py index 5eaec6d5bf1..699fb58a539 100644 --- a/tests/components/person/test_init.py +++ b/tests/components/person/test_init.py @@ -514,6 +514,18 @@ async def test_ws_update(hass, hass_ws_client, storage_setup): "id": 6, "type": "person/update", "person_id": persons[0]["id"], + "user_id": persons[0]["user_id"], + } + ) + resp = await client.receive_json() + + assert resp["success"] + + resp = await client.send_json( + { + "id": 7, + "type": "person/update", + "person_id": persons[0]["id"], "name": "Updated Name", "device_trackers": [DEVICE_TRACKER_2], "user_id": None, From 1b89fef88825e3d6b4e676bfc4b42366b90ab4b1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 11 Jan 2020 20:16:43 -0800 Subject: [PATCH 23/41] Bumped version to 0.104.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b5f95265698..8556fd1a7d8 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 104 -PATCH_VERSION = "0b3" +PATCH_VERSION = "0b4" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From 80f3cb7d798b59091e8fc44eecc0d8083997c42f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 12 Jan 2020 16:37:34 +0100 Subject: [PATCH 24/41] Fix discovery for oauth2 flow implementations (#30700) * Fix discovery for oauth2 flow implementations * Fix user step * Add tests --- .../helpers/config_entry_oauth2_flow.py | 17 +++++- .../helpers/test_config_entry_oauth2_flow.py | 58 +++++++++++++++++++ 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 2fdfea8673f..d29dae735f8 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -259,10 +259,21 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): """ return self.async_create_entry(title=self.flow_impl.name, data=data) + async def async_step_discovery(self, user_input: dict = None) -> dict: + """Handle a flow initialized by discovery.""" + await self.async_set_unique_id(self.DOMAIN) + self._abort_if_unique_id_configured() + + assert self.hass is not None + if self.hass.config_entries.async_entries(self.DOMAIN): + return self.async_abort(reason="already_configured") + + return await self.async_step_pick_implementation() + async_step_user = async_step_pick_implementation - async_step_ssdp = async_step_pick_implementation - async_step_zeroconf = async_step_pick_implementation - async_step_homekit = async_step_pick_implementation + async_step_ssdp = async_step_discovery + async_step_zeroconf = async_step_discovery + async_step_homekit = async_step_discovery @classmethod def async_register_implementation( diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 366c295874d..a72f3f51ee7 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -122,6 +122,64 @@ async def test_abort_if_authorization_timeout(hass, flow_handler, local_impl): assert result["reason"] == "authorize_url_timeout" +async def test_step_discovery(hass, flow_handler, local_impl): + """Check flow triggers from discovery.""" + hass.config.api.base_url = "https://example.com" + flow_handler.async_register_implementation(hass, local_impl) + config_entry_oauth2_flow.async_register_implementation( + hass, TEST_DOMAIN, MockOAuth2Implementation() + ) + + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "pick_implementation" + + +async def test_abort_discovered_multiple(hass, flow_handler, local_impl): + """Test if aborts when discovered multiple times.""" + hass.config.api.base_url = "https://example.com" + flow_handler.async_register_implementation(hass, local_impl) + config_entry_oauth2_flow.async_register_implementation( + hass, TEST_DOMAIN, MockOAuth2Implementation() + ) + + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": config_entries.SOURCE_SSDP} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "pick_implementation" + + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_in_progress" + + +async def test_abort_discovered_existing_entries(hass, flow_handler, local_impl): + """Test if abort discovery when entries exists.""" + hass.config.api.base_url = "https://example.com" + flow_handler.async_register_implementation(hass, local_impl) + config_entry_oauth2_flow.async_register_implementation( + hass, TEST_DOMAIN, MockOAuth2Implementation() + ) + + entry = MockConfigEntry(domain=TEST_DOMAIN, data={},) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": config_entries.SOURCE_SSDP} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + async def test_full_flow( hass, flow_handler, local_impl, aiohttp_client, aioclient_mock ): From 4a8ecb82a839fb3313e1abcbcf302177413d8950 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Sun, 12 Jan 2020 14:21:07 -0600 Subject: [PATCH 25/41] Revert "Forget auth token when going offline so we can reconnect (#26630)" (#30705) This reverts commit 2d6d6ba90e5e2f179a07c4ecd7de1744e86a8025. --- homeassistant/components/amcrest/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index b934a7e0549..63daeb04731 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -132,8 +132,6 @@ class AmcrestChecker(Http): offline = not self.available if offline and was_online: _LOGGER.error("%s camera offline: Too many errors", self._wrap_name) - with self._token_lock: - self._token = None dispatcher_send( self._hass, service_signal(SERVICE_UPDATE, self._wrap_name) ) From c6b96f7250401354f6e3921b7a67d096d085fd66 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 13 Jan 2020 09:13:46 -0800 Subject: [PATCH 26/41] Fix Ring wifi sensors (#30735) * Fix Ring wifi sensors * Update before adding --- homeassistant/components/ring/sensor.py | 27 +++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 89b042ba862..874c056ec7d 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -69,18 +69,33 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors = [] for device in ring_chimes: for sensor_type in SENSOR_TYPES: - if "chime" in SENSOR_TYPES[sensor_type][1]: - sensors.append(RingSensor(hass, device, sensor_type)) + if "chime" not in SENSOR_TYPES[sensor_type][1]: + continue + + if sensor_type in ("wifi_signal_category", "wifi_signal_strength"): + await hass.async_add_executor_job(device.update) + + sensors.append(RingSensor(hass, device, sensor_type)) for device in ring_doorbells: for sensor_type in SENSOR_TYPES: - if "doorbell" in SENSOR_TYPES[sensor_type][1]: - sensors.append(RingSensor(hass, device, sensor_type)) + if "doorbell" not in SENSOR_TYPES[sensor_type][1]: + continue + + if sensor_type in ("wifi_signal_category", "wifi_signal_strength"): + await hass.async_add_executor_job(device.update) + + sensors.append(RingSensor(hass, device, sensor_type)) for device in ring_stickup_cams: for sensor_type in SENSOR_TYPES: - if "stickup_cams" in SENSOR_TYPES[sensor_type][1]: - sensors.append(RingSensor(hass, device, sensor_type)) + if "stickup_cams" not in SENSOR_TYPES[sensor_type][1]: + continue + + if sensor_type in ("wifi_signal_category", "wifi_signal_strength"): + await hass.async_add_executor_job(device.update) + + sensors.append(RingSensor(hass, device, sensor_type)) async_add_entities(sensors, True) From c138a93454eedf035f593be20bd42019f94433ac Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Sun, 12 Jan 2020 17:55:11 +0100 Subject: [PATCH 27/41] update aiopylgtv to 0.2.5 (#30702) --- homeassistant/components/webostv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index b154f5ef8ec..d842ada3fbb 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -2,7 +2,7 @@ "domain": "webostv", "name": "LG webOS Smart TV", "documentation": "https://www.home-assistant.io/integrations/webostv", - "requirements": ["aiopylgtv==0.2.4"], + "requirements": ["aiopylgtv==0.2.5"], "dependencies": ["configurator"], "codeowners": ["@bendavid"] } diff --git a/requirements_all.txt b/requirements_all.txt index 35dceaf2a9c..8bdc3444a69 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -190,7 +190,7 @@ aionotion==1.1.0 aiopvapi==1.6.14 # homeassistant.components.webostv -aiopylgtv==0.2.4 +aiopylgtv==0.2.5 # homeassistant.components.switcher_kis aioswitcher==2019.4.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eb48440be14..1220a84a225 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -69,7 +69,7 @@ aiohue==1.10.1 aionotion==1.1.0 # homeassistant.components.webostv -aiopylgtv==0.2.4 +aiopylgtv==0.2.5 # homeassistant.components.switcher_kis aioswitcher==2019.4.26 From 9fa0779c1f55b5f0c19e01e8e7825194872ed451 Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Mon, 13 Jan 2020 19:11:06 +0100 Subject: [PATCH 28/41] update aiopylgtv to 0.2.6 (#30739) --- homeassistant/components/webostv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index d842ada3fbb..ddd7be6f3da 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -2,7 +2,7 @@ "domain": "webostv", "name": "LG webOS Smart TV", "documentation": "https://www.home-assistant.io/integrations/webostv", - "requirements": ["aiopylgtv==0.2.5"], + "requirements": ["aiopylgtv==0.2.6"], "dependencies": ["configurator"], "codeowners": ["@bendavid"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8bdc3444a69..a6c9f2bc9d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -190,7 +190,7 @@ aionotion==1.1.0 aiopvapi==1.6.14 # homeassistant.components.webostv -aiopylgtv==0.2.5 +aiopylgtv==0.2.6 # homeassistant.components.switcher_kis aioswitcher==2019.4.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1220a84a225..5ec1f858fbd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -69,7 +69,7 @@ aiohue==1.10.1 aionotion==1.1.0 # homeassistant.components.webostv -aiopylgtv==0.2.5 +aiopylgtv==0.2.6 # homeassistant.components.switcher_kis aioswitcher==2019.4.26 From 8e5f46d5b5a384e5547ca12813820e4ea7fd4f0c Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 13 Jan 2020 13:10:22 -0500 Subject: [PATCH 29/41] Bump ZHA quirks to 0.0.31 (#30740) * Bump ZHA quirks version * update requirements --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 3beca6fd3c5..e3d0eda3e02 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ "bellows-homeassistant==0.12.0", - "zha-quirks==0.0.30", + "zha-quirks==0.0.31", "zigpy-deconz==0.7.0", "zigpy-homeassistant==0.12.0", "zigpy-xbee-homeassistant==0.8.0", diff --git a/requirements_all.txt b/requirements_all.txt index a6c9f2bc9d0..db306c6f5ea 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2113,7 +2113,7 @@ zengge==0.2 zeroconf==0.24.4 # homeassistant.components.zha -zha-quirks==0.0.30 +zha-quirks==0.0.31 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5ec1f858fbd..75e647fb3d2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -673,7 +673,7 @@ yahooweather==0.10 zeroconf==0.24.4 # homeassistant.components.zha -zha-quirks==0.0.30 +zha-quirks==0.0.31 # homeassistant.components.zha zigpy-deconz==0.7.0 From 4bc520c724b339864c2f37f3be805fd3e6167c6a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 Jan 2020 12:54:45 -0800 Subject: [PATCH 30/41] Update Ring to 0.6.0 (#30748) * Update Ring to 0.6.0 * Update sensor tests * update -> async_update * Delete temp files * Address comments * Final tweaks * Remove stale print --- CODEOWNERS | 1 + homeassistant/components/ring/__init__.py | 169 +++++++++++++----- .../components/ring/binary_sensor.py | 90 ++++++---- homeassistant/components/ring/camera.py | 70 ++++---- homeassistant/components/ring/config_flow.py | 4 +- homeassistant/components/ring/light.py | 25 +-- homeassistant/components/ring/manifest.json | 4 +- homeassistant/components/ring/sensor.py | 163 ++++++++++------- homeassistant/components/ring/switch.py | 27 +-- homeassistant/helpers/entity_platform.py | 3 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ring/conftest.py | 10 +- tests/components/ring/test_binary_sensor.py | 95 ++-------- tests/components/ring/test_light.py | 6 +- tests/components/ring/test_sensor.py | 142 ++++----------- tests/components/ring/test_switch.py | 6 +- tests/fixtures/ring_devices.json | 8 +- tests/fixtures/ring_devices_updated.json | 8 +- 19 files changed, 417 insertions(+), 418 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index b30e15d36ca..6e4ea0e8b77 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -274,6 +274,7 @@ homeassistant/components/rainmachine/* @bachya homeassistant/components/random/* @fabaff homeassistant/components/repetier/* @MTrab homeassistant/components/rfxtrx/* @danielhiversen +homeassistant/components/ring/* @balloob homeassistant/components/rmvtransport/* @cgtobi homeassistant/components/roomba/* @pschmitt homeassistant/components/saj/* @fredericvl diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index 7addc116b06..b35ff630310 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -4,15 +4,17 @@ from datetime import timedelta from functools import partial import logging from pathlib import Path +from time import time from ring_doorbell import Auth, Ring import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, __version__ +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send +from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) @@ -22,14 +24,14 @@ ATTRIBUTION = "Data provided by Ring.com" NOTIFICATION_ID = "ring_notification" NOTIFICATION_TITLE = "Ring Setup" -DATA_RING_DOORBELLS = "ring_doorbells" -DATA_RING_STICKUP_CAMS = "ring_stickup_cams" -DATA_RING_CHIMES = "ring_chimes" +DATA_HISTORY = "ring_history" +DATA_HEALTH_DATA_TRACKER = "ring_health_data" DATA_TRACK_INTERVAL = "ring_track_interval" DOMAIN = "ring" DEFAULT_ENTITY_NAMESPACE = "ring" SIGNAL_UPDATE_RING = "ring_update" +SIGNAL_UPDATE_HEALTH_RING = "ring_health_update" SCAN_INTERVAL = timedelta(seconds=10) @@ -88,51 +90,42 @@ async def async_setup_entry(hass, entry): ), ).result() - auth = Auth(entry.data["token"], token_updater) + auth = Auth(f"HomeAssistant/{__version__}", entry.data["token"], token_updater) ring = Ring(auth) - await hass.async_add_executor_job(finish_setup_entry, hass, ring) + await hass.async_add_executor_job(ring.update_data) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ring for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) ) - return True + if hass.services.has_service(DOMAIN, "update"): + return True - -def finish_setup_entry(hass, ring): - """Finish setting up entry.""" - devices = ring.devices - hass.data[DATA_RING_CHIMES] = chimes = devices["chimes"] - hass.data[DATA_RING_DOORBELLS] = doorbells = devices["doorbells"] - hass.data[DATA_RING_STICKUP_CAMS] = stickup_cams = devices["stickup_cams"] - - ring_devices = chimes + doorbells + stickup_cams - - def service_hub_refresh(service): - hub_refresh() - - def timer_hub_refresh(event_time): - hub_refresh() - - def hub_refresh(): - """Call ring to refresh information.""" - _LOGGER.debug("Updating Ring Hub component") - - for camera in ring_devices: - _LOGGER.debug("Updating camera %s", camera.name) - camera.update() - - dispatcher_send(hass, SIGNAL_UPDATE_RING) + async def refresh_all(_): + """Refresh all ring accounts.""" + await asyncio.gather( + *[ + hass.async_add_executor_job(api.update_data) + for api in hass.data[DOMAIN].values() + ] + ) + async_dispatcher_send(hass, SIGNAL_UPDATE_RING) # register service - hass.services.register(DOMAIN, "update", service_hub_refresh) + hass.services.async_register(DOMAIN, "update", refresh_all) # register scan interval for ring - hass.data[DATA_TRACK_INTERVAL] = track_time_interval( - hass, timer_hub_refresh, SCAN_INTERVAL + hass.data[DATA_TRACK_INTERVAL] = async_track_time_interval( + hass, refresh_all, SCAN_INTERVAL ) + hass.data[DATA_HEALTH_DATA_TRACKER] = HealthDataUpdater(hass) + hass.data[DATA_HISTORY] = HistoryCache(hass) + + return True async def async_unload_entry(hass, entry): @@ -148,13 +141,103 @@ async def async_unload_entry(hass, entry): if not unload_ok: return False - await hass.async_add_executor_job(hass.data[DATA_TRACK_INTERVAL]) + hass.data[DOMAIN].pop(entry.entry_id) + if len(hass.data[DOMAIN]) != 0: + return True + + # Last entry unloaded, clean up + hass.data.pop(DATA_TRACK_INTERVAL)() + hass.data.pop(DATA_HEALTH_DATA_TRACKER) + hass.data.pop(DATA_HISTORY) hass.services.async_remove(DOMAIN, "update") - hass.data.pop(DATA_RING_DOORBELLS) - hass.data.pop(DATA_RING_STICKUP_CAMS) - hass.data.pop(DATA_RING_CHIMES) - hass.data.pop(DATA_TRACK_INTERVAL) + return True - return unload_ok + +class HealthDataUpdater: + """Data storage for health data.""" + + def __init__(self, hass): + """Track devices that need health data updated.""" + self.hass = hass + self.devices = {} + self._unsub_interval = None + + async def track_device(self, config_entry_id, device): + """Track a device.""" + if not self.devices: + self._unsub_interval = async_track_time_interval( + self.hass, self.refresh_all, SCAN_INTERVAL + ) + + key = (config_entry_id, device.device_id) + + if key not in self.devices: + self.devices[key] = { + "device": device, + "count": 1, + } + else: + self.devices[key]["count"] += 1 + + await self.hass.async_add_executor_job(device.update_health_data) + + @callback + def untrack_device(self, config_entry_id, device): + """Untrack a device.""" + key = (config_entry_id, device.device_id) + self.devices[key]["count"] -= 1 + + if self.devices[key]["count"] == 0: + self.devices.pop(key) + + if not self.devices: + self._unsub_interval() + self._unsub_interval = None + + def refresh_all(self, _): + """Refresh all registered devices.""" + for info in self.devices.values(): + info["device"].update_health_data() + + dispatcher_send(self.hass, SIGNAL_UPDATE_HEALTH_RING) + + +class HistoryCache: + """Helper to fetch history.""" + + STALE_AFTER = 10 # seconds + + def __init__(self, hass): + """Initialize history cache.""" + self.hass = hass + self.cache = {} + + async def async_get_history(self, config_entry_id, device): + """Get history of a device.""" + key = (config_entry_id, device.device_id) + + if key in self.cache: + info = self.cache[key] + + # We're already fetching data, join that task + if "task" in info: + return await info["task"] + + # We have valid cache info, return that + if time() - info["created_at"] < self.STALE_AFTER: + return info["data"] + + self.cache.pop(key) + + # Fetch data + task = self.hass.async_add_executor_job(partial(device.history, limit=10)) + + self.cache[key] = {"task": task} + + data = await task + + self.cache[key] = {"created_at": time(), "data": data} + + return data diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 29337f29689..2dd3682951f 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -4,8 +4,10 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import ATTRIBUTION, DATA_RING_DOORBELLS, DATA_RING_STICKUP_CAMS, DOMAIN +from . import ATTRIBUTION, DOMAIN, SIGNAL_UPDATE_RING _LOGGER = logging.getLogger(__name__) @@ -13,26 +15,25 @@ SCAN_INTERVAL = timedelta(seconds=10) # Sensor types: Name, category, device_class SENSOR_TYPES = { - "ding": ["Ding", ["doorbell"], "occupancy"], - "motion": ["Motion", ["doorbell", "stickup_cams"], "motion"], + "ding": ["Ding", ["doorbots", "authorized_doorbots"], "occupancy"], + "motion": ["Motion", ["doorbots", "authorized_doorbots", "stickup_cams"], "motion"], } async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Ring binary sensors from a config entry.""" - ring_doorbells = hass.data[DATA_RING_DOORBELLS] - ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS] + ring = hass.data[DOMAIN][config_entry.entry_id] + devices = ring.devices() sensors = [] - for device in ring_doorbells: # ring.doorbells is doing I/O - for sensor_type in SENSOR_TYPES: - if "doorbell" in SENSOR_TYPES[sensor_type][1]: - sensors.append(RingBinarySensor(hass, device, sensor_type)) - for device in ring_stickup_cams: # ring.stickup_cams is doing I/O + for device_type in ("doorbots", "authorized_doorbots", "stickup_cams"): for sensor_type in SENSOR_TYPES: - if "stickup_cams" in SENSOR_TYPES[sensor_type][1]: - sensors.append(RingBinarySensor(hass, device, sensor_type)) + if device_type not in SENSOR_TYPES[sensor_type][1]: + continue + + for device in devices[device_type]: + sensors.append(RingBinarySensor(ring, device, sensor_type)) async_add_entities(sensors, True) @@ -40,17 +41,41 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class RingBinarySensor(BinarySensorDevice): """A binary sensor implementation for Ring device.""" - def __init__(self, hass, data, sensor_type): + def __init__(self, ring, device, sensor_type): """Initialize a sensor for Ring device.""" - super().__init__() self._sensor_type = sensor_type - self._data = data + self._ring = ring + self._device = device self._name = "{0} {1}".format( - self._data.name, SENSOR_TYPES.get(self._sensor_type)[0] + self._device.name, SENSOR_TYPES.get(self._sensor_type)[0] ) self._device_class = SENSOR_TYPES.get(self._sensor_type)[2] self._state = None - self._unique_id = f"{self._data.id}-{self._sensor_type}" + self._unique_id = f"{self._device.id}-{self._sensor_type}" + self._disp_disconnect = None + + async def async_added_to_hass(self): + """Register callbacks.""" + self._disp_disconnect = async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_RING, self._update_callback + ) + + async def async_will_remove_from_hass(self): + """Disconnect callbacks.""" + if self._disp_disconnect: + self._disp_disconnect() + self._disp_disconnect = None + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) + _LOGGER.debug("Updating Ring binary sensor %s (callback)", self.name) + + @property + def should_poll(self): + """Return False, updates are controlled via the hub.""" + return False @property def name(self): @@ -76,10 +101,9 @@ class RingBinarySensor(BinarySensorDevice): def device_info(self): """Return device info.""" return { - "identifiers": {(DOMAIN, self._data.id)}, - "sw_version": self._data.firmware, - "name": self._data.name, - "model": self._data.kind, + "identifiers": {(DOMAIN, self._device.device_id)}, + "name": self._device.name, + "model": self._device.model, "manufacturer": "Ring", } @@ -89,22 +113,16 @@ class RingBinarySensor(BinarySensorDevice): attrs = {} attrs[ATTR_ATTRIBUTION] = ATTRIBUTION - attrs["timezone"] = self._data.timezone - - if self._data.alert and self._data.alert_expires_at: - attrs["expires_at"] = self._data.alert_expires_at - attrs["state"] = self._data.alert.get("state") + if self._device.alert and self._device.alert_expires_at: + attrs["expires_at"] = self._device.alert_expires_at + attrs["state"] = self._device.alert.get("state") return attrs - def update(self): + async def async_update(self): """Get the latest data and updates the state.""" - self._data.check_alerts() - - if self._data.alert: - if self._sensor_type == self._data.alert.get( - "kind" - ) and self._data.account_id == self._data.alert.get("doorbot_id"): - self._state = True - else: - self._state = False + self._state = any( + alert["kind"] == self._sensor_type + and alert["doorbot_id"] == self._device.id + for alert in self._ring.active_alerts() + ) diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 2b0fe14a1d4..8ef876e4a00 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -1,6 +1,7 @@ """This component provides support to the Ring Door Bell camera.""" import asyncio from datetime import timedelta +from itertools import chain import logging from haffmpeg.camera import CameraMjpeg @@ -14,13 +15,7 @@ from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import dt as dt_util -from . import ( - ATTRIBUTION, - DATA_RING_DOORBELLS, - DATA_RING_STICKUP_CAMS, - DOMAIN, - SIGNAL_UPDATE_RING, -) +from . import ATTRIBUTION, DATA_HISTORY, DOMAIN, SIGNAL_UPDATE_RING FORCE_REFRESH_INTERVAL = timedelta(minutes=45) @@ -29,16 +24,17 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a Ring Door Bell and StickUp Camera.""" - ring_doorbell = hass.data[DATA_RING_DOORBELLS] - ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS] + ring = hass.data[DOMAIN][config_entry.entry_id] + devices = ring.devices() cams = [] - for camera in ring_doorbell + ring_stickup_cams: + for camera in chain( + devices["doorbots"], devices["authorized_doorbots"], devices["stickup_cams"] + ): if not camera.has_subscription: continue - camera = await hass.async_add_executor_job(RingCam, hass, camera) - cams.append(camera) + cams.append(RingCam(config_entry.entry_id, hass.data[DATA_FFMPEG], camera)) async_add_entities(cams, True) @@ -46,17 +42,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class RingCam(Camera): """An implementation of a Ring Door Bell camera.""" - def __init__(self, hass, camera): + def __init__(self, config_entry_id, ffmpeg, device): """Initialize a Ring Door Bell camera.""" super().__init__() - self._camera = camera - self._hass = hass - self._name = self._camera.name - self._ffmpeg = hass.data[DATA_FFMPEG] - self._last_video_id = self._camera.last_recording_id - self._video_url = self._camera.recording_url(self._last_video_id) + self._config_entry_id = config_entry_id + self._device = device + self._name = self._device.name + self._ffmpeg = ffmpeg + self._last_video_id = None + self._video_url = None self._utcnow = dt_util.utcnow() - self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow + self._expires_at = self._utcnow - FORCE_REFRESH_INTERVAL self._disp_disconnect = None async def async_added_to_hass(self): @@ -85,16 +81,15 @@ class RingCam(Camera): @property def unique_id(self): """Return a unique ID.""" - return self._camera.id + return self._device.id @property def device_info(self): """Return device info.""" return { - "identifiers": {(DOMAIN, self._camera.id)}, - "sw_version": self._camera.firmware, - "name": self._camera.name, - "model": self._camera.kind, + "identifiers": {(DOMAIN, self._device.device_id)}, + "name": self._device.name, + "model": self._device.model, "manufacturer": "Ring", } @@ -103,7 +98,6 @@ class RingCam(Camera): """Return the state attributes.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, - "timezone": self._camera.timezone, "video_url": self._video_url, "last_video_id": self._last_video_id, } @@ -123,7 +117,6 @@ class RingCam(Camera): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - if self._video_url is None: return @@ -141,22 +134,20 @@ class RingCam(Camera): finally: await stream.close() - @property - def should_poll(self): - """Return False, updates are controlled via the hub.""" - return False - - def update(self): + async def async_update(self): """Update camera entity and refresh attributes.""" _LOGGER.debug("Checking if Ring DoorBell needs to refresh video_url") self._utcnow = dt_util.utcnow() - try: - last_event = self._camera.history(limit=1)[0] - except (IndexError, TypeError): + data = await self.hass.data[DATA_HISTORY].async_get_history( + self._config_entry_id, self._device + ) + + if not data: return + last_event = data[0] last_recording_id = last_event["id"] video_status = last_event["recording"]["status"] @@ -164,9 +155,12 @@ class RingCam(Camera): self._last_video_id != last_recording_id or self._utcnow >= self._expires_at ): - video_url = self._camera.recording_url(last_recording_id) + video_url = await self.hass.async_add_executor_job( + self._device.recording_url, last_recording_id + ) + if video_url: - _LOGGER.info("Ring DoorBell properties refreshed") + _LOGGER.debug("Ring DoorBell properties refreshed") # update attributes if new video or if URL has expired self._last_video_id = last_recording_id diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py index 6d177a4db49..57f873bd1a6 100644 --- a/homeassistant/components/ring/config_flow.py +++ b/homeassistant/components/ring/config_flow.py @@ -5,7 +5,7 @@ from oauthlib.oauth2 import AccessDeniedError, MissingTokenError from ring_doorbell import Auth import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant import config_entries, const, core, exceptions from . import DOMAIN # pylint: disable=unused-import @@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__) async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect.""" - auth = Auth() + auth = Auth(f"HomeAssistant/{const.__version__}") try: token = await hass.async_add_executor_job( diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index b7fa67a391f..10572e2e0ae 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -7,7 +7,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util -from . import DATA_RING_STICKUP_CAMS, DOMAIN, SIGNAL_UPDATE_RING +from . import DOMAIN, SIGNAL_UPDATE_RING _LOGGER = logging.getLogger(__name__) @@ -25,10 +25,12 @@ OFF_STATE = "off" async def async_setup_entry(hass, config_entry, async_add_entities): """Create the lights for the Ring devices.""" - cameras = hass.data[DATA_RING_STICKUP_CAMS] + ring = hass.data[DOMAIN][config_entry.entry_id] + + devices = ring.devices() lights = [] - for device in cameras: + for device in devices["stickup_cams"]: if device.has_capability("light"): lights.append(RingLight(device)) @@ -64,6 +66,11 @@ class RingLight(Light): _LOGGER.debug("Updating Ring light %s (callback)", self.name) self.async_schedule_update_ha_state(True) + @property + def should_poll(self): + """Update controlled via the hub.""" + return False + @property def name(self): """Name of the light.""" @@ -74,11 +81,6 @@ class RingLight(Light): """Return a unique ID.""" return self._unique_id - @property - def should_poll(self): - """Update controlled via the hub.""" - return False - @property def is_on(self): """If the switch is currently on or off.""" @@ -88,10 +90,9 @@ class RingLight(Light): def device_info(self): """Return device info.""" return { - "identifiers": {(DOMAIN, self._device.id)}, - "sw_version": self._device.firmware, + "identifiers": {(DOMAIN, self._device.device_id)}, "name": self._device.name, - "model": self._device.kind, + "model": self._device.model, "manufacturer": "Ring", } @@ -110,7 +111,7 @@ class RingLight(Light): """Turn the light off.""" self._set_light(OFF_STATE) - def update(self): + async def async_update(self): """Update current state of the light.""" if self._no_updates_until > dt_util.utcnow(): _LOGGER.debug("Skipping update...") diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index fccbf9a5319..d46f12af511 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -2,8 +2,8 @@ "domain": "ring", "name": "Ring", "documentation": "https://www.home-assistant.io/integrations/ring", - "requirements": ["ring_doorbell==0.5.0"], + "requirements": ["ring_doorbell==0.6.0"], "dependencies": ["ffmpeg"], - "codeowners": [], + "codeowners": ["@balloob"], "config_flow": true } diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 874c056ec7d..fe909636e83 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -9,93 +9,98 @@ from homeassistant.helpers.icon import icon_for_battery_level from . import ( ATTRIBUTION, - DATA_RING_CHIMES, - DATA_RING_DOORBELLS, - DATA_RING_STICKUP_CAMS, + DATA_HEALTH_DATA_TRACKER, + DATA_HISTORY, DOMAIN, + SIGNAL_UPDATE_HEALTH_RING, SIGNAL_UPDATE_RING, ) _LOGGER = logging.getLogger(__name__) -# Sensor types: Name, category, units, icon, kind +# Sensor types: Name, category, units, icon, kind, device_class SENSOR_TYPES = { - "battery": ["Battery", ["doorbell", "stickup_cams"], "%", "battery-50", None], + "battery": [ + "Battery", + ["doorbots", "authorized_doorbots", "stickup_cams"], + "%", + None, + None, + "battery", + ], "last_activity": [ "Last Activity", - ["doorbell", "stickup_cams"], + ["doorbots", "authorized_doorbots", "stickup_cams"], None, "history", None, + "timestamp", + ], + "last_ding": [ + "Last Ding", + ["doorbots", "authorized_doorbots"], + None, + "history", + "ding", + "timestamp", ], - "last_ding": ["Last Ding", ["doorbell"], None, "history", "ding"], "last_motion": [ "Last Motion", - ["doorbell", "stickup_cams"], + ["doorbots", "authorized_doorbots", "stickup_cams"], None, "history", "motion", + "timestamp", ], "volume": [ "Volume", - ["chime", "doorbell", "stickup_cams"], + ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], None, "bell-ring", None, + None, ], "wifi_signal_category": [ "WiFi Signal Category", - ["chime", "doorbell", "stickup_cams"], + ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], None, "wifi", None, + None, ], "wifi_signal_strength": [ "WiFi Signal Strength", - ["chime", "doorbell", "stickup_cams"], + ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], "dBm", "wifi", None, + "signal_strength", ], } async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a sensor for a Ring device.""" - ring_chimes = hass.data[DATA_RING_CHIMES] - ring_doorbells = hass.data[DATA_RING_DOORBELLS] - ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS] + ring = hass.data[DOMAIN][config_entry.entry_id] + devices = ring.devices() + # Makes a ton of requests. We will make this a config entry option in the future + wifi_enabled = False sensors = [] - for device in ring_chimes: + + for device_type in ("chimes", "doorbots", "authorized_doorbots", "stickup_cams"): for sensor_type in SENSOR_TYPES: - if "chime" not in SENSOR_TYPES[sensor_type][1]: + if device_type not in SENSOR_TYPES[sensor_type][1]: continue - if sensor_type in ("wifi_signal_category", "wifi_signal_strength"): - await hass.async_add_executor_job(device.update) - - sensors.append(RingSensor(hass, device, sensor_type)) - - for device in ring_doorbells: - for sensor_type in SENSOR_TYPES: - if "doorbell" not in SENSOR_TYPES[sensor_type][1]: + if not wifi_enabled and sensor_type.startswith("wifi_"): continue - if sensor_type in ("wifi_signal_category", "wifi_signal_strength"): - await hass.async_add_executor_job(device.update) + for device in devices[device_type]: + if device_type == "battery" and device.battery_life is None: + continue - sensors.append(RingSensor(hass, device, sensor_type)) - - for device in ring_stickup_cams: - for sensor_type in SENSOR_TYPES: - if "stickup_cams" not in SENSOR_TYPES[sensor_type][1]: - continue - - if sensor_type in ("wifi_signal_category", "wifi_signal_strength"): - await hass.async_add_executor_job(device.update) - - sensors.append(RingSensor(hass, device, sensor_type)) + sensors.append(RingSensor(config_entry.entry_id, device, sensor_type)) async_add_entities(sensors, True) @@ -103,28 +108,42 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class RingSensor(Entity): """A sensor implementation for Ring device.""" - def __init__(self, hass, data, sensor_type): + def __init__(self, config_entry_id, device, sensor_type): """Initialize a sensor for Ring device.""" - super().__init__() + self._config_entry_id = config_entry_id self._sensor_type = sensor_type - self._data = data + self._device = device self._extra = None self._icon = "mdi:{}".format(SENSOR_TYPES.get(self._sensor_type)[3]) self._kind = SENSOR_TYPES.get(self._sensor_type)[4] self._name = "{0} {1}".format( - self._data.name, SENSOR_TYPES.get(self._sensor_type)[0] + self._device.name, SENSOR_TYPES.get(self._sensor_type)[0] ) self._state = None - self._tz = str(hass.config.time_zone) - self._unique_id = f"{self._data.id}-{self._sensor_type}" + self._unique_id = f"{self._device.id}-{self._sensor_type}" self._disp_disconnect = None + self._disp_disconnect_health = None async def async_added_to_hass(self): """Register callbacks.""" self._disp_disconnect = async_dispatcher_connect( self.hass, SIGNAL_UPDATE_RING, self._update_callback ) - await self.hass.async_add_executor_job(self._data.update) + if self._sensor_type not in ("wifi_signal_category", "wifi_signal_strength"): + return + + self._disp_disconnect_health = async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_HEALTH_RING, self._update_callback + ) + await self.hass.data[DATA_HEALTH_DATA_TRACKER].track_device( + self._config_entry_id, self._device + ) + # Write the state, it was not available when doing initial update. + if self._sensor_type == "wifi_signal_category": + self._state = self._device.wifi_signal_category + + if self._sensor_type == "wifi_signal_strength": + self._state = self._device.wifi_signal_strength async def async_will_remove_from_hass(self): """Disconnect callbacks.""" @@ -132,6 +151,17 @@ class RingSensor(Entity): self._disp_disconnect() self._disp_disconnect = None + if self._disp_disconnect_health: + self._disp_disconnect_health() + self._disp_disconnect_health = None + + if self._sensor_type not in ("wifi_signal_category", "wifi_signal_strength"): + return + + self.hass.data[DATA_HEALTH_DATA_TRACKER].untrack_device( + self._config_entry_id, self._device + ) + @callback def _update_callback(self): """Call update method.""" @@ -157,14 +187,18 @@ class RingSensor(Entity): """Return a unique ID.""" return self._unique_id + @property + def device_class(self): + """Return sensor device class.""" + return SENSOR_TYPES[self._sensor_type][5] + @property def device_info(self): """Return device info.""" return { - "identifiers": {(DOMAIN, self._data.id)}, - "sw_version": self._data.firmware, - "name": self._data.name, - "model": self._data.kind, + "identifiers": {(DOMAIN, self._device.device_id)}, + "name": self._device.name, + "model": self._device.model, "manufacturer": "Ring", } @@ -174,8 +208,6 @@ class RingSensor(Entity): attrs = {} attrs[ATTR_ATTRIBUTION] = ATTRIBUTION - attrs["timezone"] = self._data.timezone - attrs["wifi_name"] = self._data.wifi_name if self._extra and self._sensor_type.startswith("last_"): attrs["created_at"] = self._extra["created_at"] @@ -199,29 +231,34 @@ class RingSensor(Entity): """Return the units of measurement.""" return SENSOR_TYPES.get(self._sensor_type)[2] - def update(self): + async def async_update(self): """Get the latest data and updates the state.""" _LOGGER.debug("Updating data from %s sensor", self._name) if self._sensor_type == "volume": - self._state = self._data.volume + self._state = self._device.volume if self._sensor_type == "battery": - self._state = self._data.battery_life + self._state = self._device.battery_life if self._sensor_type.startswith("last_"): - history = self._data.history( - limit=5, timezone=self._tz, kind=self._kind, enforce_limit=True + history = await self.hass.data[DATA_HISTORY].async_get_history( + self._config_entry_id, self._device ) - if history: - self._extra = history[0] - created_at = self._extra["created_at"] - self._state = "{0:0>2}:{1:0>2}".format( - created_at.hour, created_at.minute - ) + + found = None + for entry in history: + if entry["kind"] == self._kind: + found = entry + break + + if found: + self._extra = found + created_at = found["created_at"] + self._state = created_at.isoformat() if self._sensor_type == "wifi_signal_category": - self._state = self._data.wifi_signal_category + self._state = self._device.wifi_signal_category if self._sensor_type == "wifi_signal_strength": - self._state = self._data.wifi_signal_strength + self._state = self._device.wifi_signal_strength diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index e23e757d825..06f81732784 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -7,7 +7,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util -from . import DATA_RING_STICKUP_CAMS, DOMAIN, SIGNAL_UPDATE_RING +from . import DOMAIN, SIGNAL_UPDATE_RING _LOGGER = logging.getLogger(__name__) @@ -24,9 +24,11 @@ SKIP_UPDATES_DELAY = timedelta(seconds=5) async def async_setup_entry(hass, config_entry, async_add_entities): """Create the switches for the Ring devices.""" - cameras = hass.data[DATA_RING_STICKUP_CAMS] + ring = hass.data[DOMAIN][config_entry.entry_id] + devices = ring.devices() switches = [] - for device in cameras: + + for device in devices["stickup_cams"]: if device.has_capability("siren"): switches.append(SirenSwitch(device)) @@ -58,9 +60,14 @@ class BaseRingSwitch(SwitchDevice): @callback def _update_callback(self): """Call update method.""" - _LOGGER.debug("Updating Ring sensor %s (callback)", self.name) + _LOGGER.debug("Updating Ring switch %s (callback)", self.name) self.async_schedule_update_ha_state(True) + @property + def should_poll(self): + """Update controlled via the hub.""" + return False + @property def name(self): """Name of the device.""" @@ -71,19 +78,13 @@ class BaseRingSwitch(SwitchDevice): """Return a unique ID.""" return self._unique_id - @property - def should_poll(self): - """Update controlled via the hub.""" - return False - @property def device_info(self): """Return device info.""" return { - "identifiers": {(DOMAIN, self._device.id)}, - "sw_version": self._device.firmware, + "identifiers": {(DOMAIN, self._device.device_id)}, "name": self._device.name, - "model": self._device.kind, + "model": self._device.model, "manufacturer": "Ring", } @@ -122,7 +123,7 @@ class SirenSwitch(BaseRingSwitch): """Return the icon.""" return SIREN_ICON - def update(self): + async def async_update(self): """Update state of the siren.""" if self._no_updates_until > dt_util.utcnow(): _LOGGER.debug("Skipping update...") diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 0560cf84fb3..53ad54c5ed1 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -146,7 +146,8 @@ class EntityPlatform: warn_task = hass.loop.call_later( SLOW_SETUP_WARNING, logger.warning, - "Setup of platform %s is taking over %s seconds.", + "Setup of %s platform %s is taking over %s seconds.", + self.domain, self.platform_name, SLOW_SETUP_WARNING, ) diff --git a/requirements_all.txt b/requirements_all.txt index db306c6f5ea..9090aef073d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1753,7 +1753,7 @@ rfk101py==0.0.1 rflink==0.0.50 # homeassistant.components.ring -ring_doorbell==0.5.0 +ring_doorbell==0.6.0 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75e647fb3d2..82c740afa93 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -567,7 +567,7 @@ restrictedpython==5.0 rflink==0.0.50 # homeassistant.components.ring -ring_doorbell==0.5.0 +ring_doorbell==0.6.0 # homeassistant.components.yamaha rxv==0.6.0 diff --git a/tests/components/ring/conftest.py b/tests/components/ring/conftest.py index a4cfaf0065d..5df85662ac8 100644 --- a/tests/components/ring/conftest.py +++ b/tests/components/ring/conftest.py @@ -1,4 +1,6 @@ """Configuration for Ring tests.""" +import re + import pytest import requests_mock @@ -33,17 +35,19 @@ def requests_mock_fixture(): ) # Mocks the response for getting the history of a device mock.get( - "https://api.ring.com/clients_api/doorbots/987652/history", + re.compile( + r"https:\/\/api\.ring\.com\/clients_api\/doorbots\/\d+\/history" + ), text=load_fixture("ring_doorbots.json"), ) # Mocks the response for getting the health of a device mock.get( - "https://api.ring.com/clients_api/doorbots/987652/health", + re.compile(r"https:\/\/api\.ring\.com\/clients_api\/doorbots\/\d+\/health"), text=load_fixture("ring_doorboot_health_attrs.json"), ) # Mocks the response for getting a chimes health mock.get( - "https://api.ring.com/clients_api/chimes/999999/health", + re.compile(r"https:\/\/api\.ring\.com\/clients_api\/chimes\/\d+\/health"), text=load_fixture("ring_chime_health_attrs.json"), ) diff --git a/tests/components/ring/test_binary_sensor.py b/tests/components/ring/test_binary_sensor.py index 4ca83b2451b..8615138d56e 100644 --- a/tests/components/ring/test_binary_sensor.py +++ b/tests/components/ring/test_binary_sensor.py @@ -1,87 +1,22 @@ """The tests for the Ring binary sensor platform.""" -from asyncio import run_coroutine_threadsafe -import unittest from unittest.mock import patch -import requests_mock - -from homeassistant.components import ring as base_ring -from homeassistant.components.ring import binary_sensor as ring - -from tests.common import get_test_home_assistant, load_fixture, mock_storage -from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG +from .common import setup_platform -class TestRingBinarySensorSetup(unittest.TestCase): - """Test the Ring Binary Sensor platform.""" +async def test_binary_sensor(hass, requests_mock): + """Test the Ring binary sensors.""" + with patch( + "ring_doorbell.Ring.active_alerts", + return_value=[{"kind": "motion", "doorbot_id": 987654}], + ): + await setup_platform(hass, "binary_sensor") - DEVICES = [] + motion_state = hass.states.get("binary_sensor.front_door_motion") + assert motion_state is not None + assert motion_state.state == "on" + assert motion_state.attributes["device_class"] == "motion" - def add_entities(self, devices, action): - """Mock add devices.""" - for device in devices: - self.DEVICES.append(device) - - def setUp(self): - """Initialize values for this testcase class.""" - self.hass = get_test_home_assistant() - self.config = { - "username": "foo", - "password": "bar", - "monitored_conditions": ["ding", "motion"], - } - - def tearDown(self): - """Stop everything that was started.""" - self.hass.stop() - - @requests_mock.Mocker() - def test_binary_sensor(self, mock): - """Test the Ring sensor class and methods.""" - mock.post( - "https://oauth.ring.com/oauth/token", text=load_fixture("ring_oauth.json") - ) - mock.post( - "https://api.ring.com/clients_api/session", - text=load_fixture("ring_session.json"), - ) - mock.get( - "https://api.ring.com/clients_api/ring_devices", - text=load_fixture("ring_devices.json"), - ) - mock.get( - "https://api.ring.com/clients_api/dings/active", - text=load_fixture("ring_ding_active.json"), - ) - mock.get( - "https://api.ring.com/clients_api/doorbots/987652/health", - text=load_fixture("ring_doorboot_health_attrs.json"), - ) - mock.get( - "https://api.ring.com/clients_api/chimes/999999/health", - text=load_fixture("ring_chime_health_attrs.json"), - ) - - with mock_storage(), patch("homeassistant.components.ring.PLATFORMS", []): - run_coroutine_threadsafe( - base_ring.async_setup(self.hass, VALID_CONFIG), self.hass.loop - ).result() - run_coroutine_threadsafe( - self.hass.async_block_till_done(), self.hass.loop - ).result() - run_coroutine_threadsafe( - ring.async_setup_entry(self.hass, None, self.add_entities), - self.hass.loop, - ).result() - - for device in self.DEVICES: - device.update() - if device.name == "Front Door Ding": - assert "on" == device.state - assert "America/New_York" == device.device_state_attributes["timezone"] - elif device.name == "Front Door Motion": - assert "off" == device.state - assert "motion" == device.device_class - - assert device.entity_picture is None - assert ATTRIBUTION == device.device_state_attributes["attribution"] + ding_state = hass.states.get("binary_sensor.front_door_ding") + assert ding_state is not None + assert ding_state.state == "off" diff --git a/tests/components/ring/test_light.py b/tests/components/ring/test_light.py index 56d39173d63..6cc727b1a1c 100644 --- a/tests/components/ring/test_light.py +++ b/tests/components/ring/test_light.py @@ -12,10 +12,10 @@ async def test_entity_registry(hass, requests_mock): entity_registry = await hass.helpers.entity_registry.async_get_registry() entry = entity_registry.async_get("light.front_light") - assert entry.unique_id == "aacdef123" + assert entry.unique_id == 765432 entry = entity_registry.async_get("light.internal_light") - assert entry.unique_id == "aacdef124" + assert entry.unique_id == 345678 async def test_light_off_reports_correctly(hass, requests_mock): @@ -42,7 +42,7 @@ async def test_light_can_be_turned_on(hass, requests_mock): # Mocks the response for turning a light on requests_mock.put( - "https://api.ring.com/clients_api/doorbots/987652/floodlight_light_on", + "https://api.ring.com/clients_api/doorbots/765432/floodlight_light_on", text=load_fixture("ring_doorbot_siren_on_response.json"), ) diff --git a/tests/components/ring/test_sensor.py b/tests/components/ring/test_sensor.py index 039c9d0625f..f86e6b25959 100644 --- a/tests/components/ring/test_sensor.py +++ b/tests/components/ring/test_sensor.py @@ -1,122 +1,46 @@ """The tests for the Ring sensor platform.""" -from asyncio import run_coroutine_threadsafe -import unittest -from unittest.mock import patch +from .common import setup_platform -import requests_mock - -from homeassistant.components import ring as base_ring -import homeassistant.components.ring.sensor as ring -from homeassistant.helpers.icon import icon_for_battery_level - -from tests.common import get_test_home_assistant, load_fixture, mock_storage -from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG +WIFI_ENABLED = False -class TestRingSensorSetup(unittest.TestCase): - """Test the Ring platform.""" +async def test_sensor(hass, requests_mock): + """Test the Ring sensors.""" + await setup_platform(hass, "sensor") - DEVICES = [] + front_battery_state = hass.states.get("sensor.front_battery") + assert front_battery_state is not None + assert front_battery_state.state == "80" - def add_entities(self, devices, action): - """Mock add devices.""" - for device in devices: - self.DEVICES.append(device) + front_door_battery_state = hass.states.get("sensor.front_door_battery") + assert front_door_battery_state is not None + assert front_door_battery_state.state == "100" - def setUp(self): - """Initialize values for this testcase class.""" - self.hass = get_test_home_assistant() - self.config = { - "username": "foo", - "password": "bar", - "monitored_conditions": [ - "battery", - "last_activity", - "last_ding", - "last_motion", - "volume", - "wifi_signal_category", - "wifi_signal_strength", - ], - } + downstairs_volume_state = hass.states.get("sensor.downstairs_volume") + assert downstairs_volume_state is not None + assert downstairs_volume_state.state == "2" - def tearDown(self): - """Stop everything that was started.""" - self.hass.stop() + front_door_last_activity_state = hass.states.get("sensor.front_door_last_activity") + assert front_door_last_activity_state is not None - @requests_mock.Mocker() - def test_sensor(self, mock): - """Test the Ring sensor class and methods.""" - mock.post( - "https://oauth.ring.com/oauth/token", text=load_fixture("ring_oauth.json") - ) - mock.post( - "https://api.ring.com/clients_api/session", - text=load_fixture("ring_session.json"), - ) - mock.get( - "https://api.ring.com/clients_api/ring_devices", - text=load_fixture("ring_devices.json"), - ) - mock.get( - "https://api.ring.com/clients_api/doorbots/987652/history", - text=load_fixture("ring_doorbots.json"), - ) - mock.get( - "https://api.ring.com/clients_api/doorbots/987652/health", - text=load_fixture("ring_doorboot_health_attrs.json"), - ) - mock.get( - "https://api.ring.com/clients_api/chimes/999999/health", - text=load_fixture("ring_chime_health_attrs.json"), - ) + downstairs_wifi_signal_strength_state = hass.states.get( + "sensor.downstairs_wifi_signal_strength" + ) - with mock_storage(), patch("homeassistant.components.ring.PLATFORMS", []): - run_coroutine_threadsafe( - base_ring.async_setup(self.hass, VALID_CONFIG), self.hass.loop - ).result() - run_coroutine_threadsafe( - self.hass.async_block_till_done(), self.hass.loop - ).result() - run_coroutine_threadsafe( - ring.async_setup_entry(self.hass, None, self.add_entities), - self.hass.loop, - ).result() + if not WIFI_ENABLED: + return - for device in self.DEVICES: - # Mimick add to hass - device.hass = self.hass - run_coroutine_threadsafe( - device.async_added_to_hass(), self.hass.loop, - ).result() + assert downstairs_wifi_signal_strength_state is not None + assert downstairs_wifi_signal_strength_state.state == "-39" - # Entity update data from ring data - device.update() - if device.name == "Front Battery": - expected_icon = icon_for_battery_level( - battery_level=int(device.state), charging=False - ) - assert device.icon == expected_icon - assert 80 == device.state - if device.name == "Front Door Battery": - assert 100 == device.state - if device.name == "Downstairs Volume": - assert 2 == device.state - assert "ring_mock_wifi" == device.device_state_attributes["wifi_name"] - assert "mdi:bell-ring" == device.icon - if device.name == "Front Door Last Activity": - assert not device.device_state_attributes["answered"] - assert "America/New_York" == device.device_state_attributes["timezone"] + front_door_wifi_signal_category_state = hass.states.get( + "sensor.front_door_wifi_signal_category" + ) + assert front_door_wifi_signal_category_state is not None + assert front_door_wifi_signal_category_state.state == "good" - if device.name == "Downstairs WiFi Signal Strength": - assert -39 == device.state - - if device.name == "Front Door WiFi Signal Category": - assert "good" == device.state - - if device.name == "Front Door WiFi Signal Strength": - assert -58 == device.state - - assert device.entity_picture is None - assert ATTRIBUTION == device.device_state_attributes["attribution"] - assert not device.should_poll + front_door_wifi_signal_strength_state = hass.states.get( + "sensor.front_door_wifi_signal_strength" + ) + assert front_door_wifi_signal_strength_state is not None + assert front_door_wifi_signal_strength_state.state == "-58" diff --git a/tests/components/ring/test_switch.py b/tests/components/ring/test_switch.py index 15f4dd86a39..e2a86014f1c 100644 --- a/tests/components/ring/test_switch.py +++ b/tests/components/ring/test_switch.py @@ -12,10 +12,10 @@ async def test_entity_registry(hass, requests_mock): entity_registry = await hass.helpers.entity_registry.async_get_registry() entry = entity_registry.async_get("switch.front_siren") - assert entry.unique_id == "aacdef123-siren" + assert entry.unique_id == "765432-siren" entry = entity_registry.async_get("switch.internal_siren") - assert entry.unique_id == "aacdef124-siren" + assert entry.unique_id == "345678-siren" async def test_siren_off_reports_correctly(hass, requests_mock): @@ -43,7 +43,7 @@ async def test_siren_can_be_turned_on(hass, requests_mock): # Mocks the response for turning a siren on requests_mock.put( - "https://api.ring.com/clients_api/doorbots/987652/siren_on", + "https://api.ring.com/clients_api/doorbots/765432/siren_on", text=load_fixture("ring_doorbot_siren_on_response.json"), ) diff --git a/tests/fixtures/ring_devices.json b/tests/fixtures/ring_devices.json index 557aef3535c..2d2ec893a74 100644 --- a/tests/fixtures/ring_devices.json +++ b/tests/fixtures/ring_devices.json @@ -9,7 +9,7 @@ "do_not_disturb": {"seconds_left": 0}, "features": {"ringtones_enabled": true}, "firmware_version": "1.2.3", - "id": 999999, + "id": 123456, "kind": "chime", "latitude": 12.000000, "longitude": -70.12345, @@ -42,7 +42,7 @@ "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.4.26", - "id": 987652, + "id": 987654, "kind": "lpd_v1", "latitude": 12.000000, "longitude": -70.12345, @@ -93,7 +93,7 @@ "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.9.3", - "id": 987652, + "id": 765432, "kind": "hp_cam_v1", "latitude": 12.000000, "led_status": "off", @@ -231,7 +231,7 @@ "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.9.3", - "id": 987652, + "id": 345678, "kind": "hp_cam_v1", "latitude": 12.000000, "led_status": "on", diff --git a/tests/fixtures/ring_devices_updated.json b/tests/fixtures/ring_devices_updated.json index fa3c0586101..3668a2b13dc 100644 --- a/tests/fixtures/ring_devices_updated.json +++ b/tests/fixtures/ring_devices_updated.json @@ -9,7 +9,7 @@ "do_not_disturb": {"seconds_left": 0}, "features": {"ringtones_enabled": true}, "firmware_version": "1.2.3", - "id": 999999, + "id": 123456, "kind": "chime", "latitude": 12.000000, "longitude": -70.12345, @@ -42,7 +42,7 @@ "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.4.26", - "id": 987652, + "id": 987654, "kind": "lpd_v1", "latitude": 12.000000, "longitude": -70.12345, @@ -93,7 +93,7 @@ "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.9.3", - "id": 987652, + "id": 765432, "kind": "hp_cam_v1", "latitude": 12.000000, "led_status": "on", @@ -231,7 +231,7 @@ "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.9.3", - "id": 987652, + "id": 345678, "kind": "hp_cam_v1", "latitude": 12.000000, "led_status": "off", From f5af77d00c901f4f0b2c1f1fd62dc3ef48b2e7d4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 13 Jan 2020 23:33:45 -0800 Subject: [PATCH 31/41] Set default locale for cloud Alexa config (#30749) --- homeassistant/components/alexa/config.py | 6 ++++-- homeassistant/components/cloud/alexa_config.py | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index a6e45c61953..bd579dc4dad 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -1,10 +1,12 @@ """Config helpers for Alexa.""" +from abc import ABC, abstractmethod + from homeassistant.core import callback from .state_report import async_enable_proactive_mode -class AbstractConfig: +class AbstractConfig(ABC): """Hold the configuration for Alexa.""" _unsub_proactive_report = None @@ -29,9 +31,9 @@ class AbstractConfig: return None @property + @abstractmethod def locale(self): """Return config locale.""" - return None @property def entity_config(self): diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 45e1fab1101..8d1527b1930 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -78,6 +78,12 @@ class AlexaConfig(alexa_config.AbstractConfig): return self._endpoint + @property + def locale(self): + """Return config locale.""" + # Not clear how to determine locale atm. + return "en-US" + @property def entity_config(self): """Return entity config.""" From 413545bd917d613451a9bd83552632c50327f6c1 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Tue, 14 Jan 2020 02:27:49 -0800 Subject: [PATCH 32/41] Bump teslajsonpy to 0.2.3 (#30750) --- homeassistant/components/tesla/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json index 81a6b8ea0db..e3392074679 100644 --- a/homeassistant/components/tesla/manifest.json +++ b/homeassistant/components/tesla/manifest.json @@ -3,7 +3,7 @@ "name": "Tesla", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tesla", - "requirements": ["teslajsonpy==0.2.2"], + "requirements": ["teslajsonpy==0.2.3"], "dependencies": [], "codeowners": ["@zabuldon", "@alandtse"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9090aef073d..09019e3ec0e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1957,7 +1957,7 @@ temperusb==1.5.3 # tensorflow==1.13.2 # homeassistant.components.tesla -teslajsonpy==0.2.2 +teslajsonpy==0.2.3 # homeassistant.components.thermoworks_smoke thermoworks_smoke==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 82c740afa93..0ac540103f3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -616,7 +616,7 @@ sunwatcher==0.2.1 tellduslive==0.10.10 # homeassistant.components.tesla -teslajsonpy==0.2.2 +teslajsonpy==0.2.3 # homeassistant.components.toon toonapilib==3.2.4 From 64224f46ecf1d5503faa93224fd2882ed8723149 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Thu, 9 Jan 2020 00:32:57 +0100 Subject: [PATCH 33/41] Update pyhomematic to 0.1.63 (#30594) --- homeassistant/components/homematic/__init__.py | 1 + homeassistant/components/homematic/binary_sensor.py | 1 + homeassistant/components/homematic/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index cfaffad6ce3..01bc94ce58f 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -173,6 +173,7 @@ HM_DEVICE_TYPES = { "IPMultiIO", "TiltIP", "IPShutterContactSabotage", + "IPContact", ], DISCOVER_COVER: ["Blind", "KeyBlind", "IPKeyBlind", "IPKeyBlindTilt"], DISCOVER_LOCKS: ["KeyMatic"], diff --git a/homeassistant/components/homematic/binary_sensor.py b/homeassistant/components/homematic/binary_sensor.py index cc2907c64fb..1832652406d 100644 --- a/homeassistant/components/homematic/binary_sensor.py +++ b/homeassistant/components/homematic/binary_sensor.py @@ -29,6 +29,7 @@ SENSOR_TYPES_CLASS = { "SmokeV2": DEVICE_CLASS_SMOKE, "TiltSensor": None, "WeatherSensor": None, + "IPContact": DEVICE_CLASS_OPENING, } diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index 709e99a232e..c4e09c36b8e 100644 --- a/homeassistant/components/homematic/manifest.json +++ b/homeassistant/components/homematic/manifest.json @@ -2,7 +2,7 @@ "domain": "homematic", "name": "Homematic", "documentation": "https://www.home-assistant.io/integrations/homematic", - "requirements": ["pyhomematic==0.1.62"], + "requirements": ["pyhomematic==0.1.63"], "dependencies": [], "codeowners": ["@pvizeli", "@danielperna84"] } diff --git a/requirements_all.txt b/requirements_all.txt index 09019e3ec0e..239d4ab9cf7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1288,7 +1288,7 @@ pyhik==0.2.5 pyhiveapi==0.2.19.3 # homeassistant.components.homematic -pyhomematic==0.1.62 +pyhomematic==0.1.63 # homeassistant.components.homeworks pyhomeworks==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0ac540103f3..cd30b43de99 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -442,7 +442,7 @@ pyhaversion==3.1.0 pyheos==0.6.0 # homeassistant.components.homematic -pyhomematic==0.1.62 +pyhomematic==0.1.63 # homeassistant.components.icloud pyicloud==0.9.1 From f530ea10af76140381889496f225356112682ebe Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 14 Jan 2020 11:26:59 +0100 Subject: [PATCH 34/41] Refactor HomeMatic / Fix issue with 0.104/dev (#30752) * Refactor HomeMatic / Fix issue with 0.104/dev * Fix lock --- .../components/homematic/__init__.py | 552 ++---------------- .../components/homematic/binary_sensor.py | 6 +- homeassistant/components/homematic/climate.py | 5 +- homeassistant/components/homematic/const.py | 212 +++++++ homeassistant/components/homematic/cover.py | 10 +- homeassistant/components/homematic/entity.py | 297 ++++++++++ homeassistant/components/homematic/light.py | 5 +- homeassistant/components/homematic/lock.py | 8 +- homeassistant/components/homematic/notify.py | 3 +- homeassistant/components/homematic/sensor.py | 8 +- homeassistant/components/homematic/switch.py | 10 +- 11 files changed, 595 insertions(+), 521 deletions(-) create mode 100644 homeassistant/components/homematic/const.py create mode 100644 homeassistant/components/homematic/entity.py diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 01bc94ce58f..24c9e37a3be 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -1,5 +1,5 @@ """Support for HomeMatic devices.""" -from datetime import datetime, timedelta +from datetime import datetime from functools import partial import logging @@ -18,232 +18,68 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, - STATE_UNKNOWN, ) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity + +from .const import ( + ATTR_ADDRESS, + ATTR_CHANNEL, + ATTR_DISCOVER_DEVICES, + ATTR_DISCOVERY_TYPE, + ATTR_ERRORCODE, + ATTR_INTERFACE, + ATTR_LOW_BAT, + ATTR_LOWBAT, + ATTR_MESSAGE, + ATTR_PARAM, + ATTR_PARAMSET, + ATTR_PARAMSET_KEY, + ATTR_TIME, + ATTR_UNIQUE_ID, + ATTR_VALUE, + ATTR_VALUE_TYPE, + CONF_CALLBACK_IP, + CONF_CALLBACK_PORT, + CONF_INTERFACES, + CONF_JSONPORT, + CONF_LOCAL_IP, + CONF_LOCAL_PORT, + CONF_PATH, + CONF_PORT, + CONF_RESOLVENAMES, + CONF_RESOLVENAMES_OPTIONS, + DATA_CONF, + DATA_HOMEMATIC, + DATA_STORE, + DISCOVER_BATTERY, + DISCOVER_BINARY_SENSORS, + DISCOVER_CLIMATE, + DISCOVER_COVER, + DISCOVER_LIGHTS, + DISCOVER_LOCKS, + DISCOVER_SENSORS, + DISCOVER_SWITCHES, + DOMAIN, + EVENT_ERROR, + EVENT_IMPULSE, + EVENT_KEYPRESS, + HM_DEVICE_TYPES, + HM_IGNORE_DISCOVERY_NODE, + HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS, + HM_IMPULSE_EVENTS, + HM_PRESS_EVENTS, + SERVICE_PUT_PARAMSET, + SERVICE_RECONNECT, + SERVICE_SET_DEVICE_VALUE, + SERVICE_SET_INSTALL_MODE, + SERVICE_SET_VARIABLE_VALUE, + SERVICE_VIRTUALKEY, +) +from .entity import HMHub _LOGGER = logging.getLogger(__name__) -DOMAIN = "homematic" - -SCAN_INTERVAL_HUB = timedelta(seconds=300) -SCAN_INTERVAL_VARIABLES = timedelta(seconds=30) - -DISCOVER_SWITCHES = "homematic.switch" -DISCOVER_LIGHTS = "homematic.light" -DISCOVER_SENSORS = "homematic.sensor" -DISCOVER_BINARY_SENSORS = "homematic.binary_sensor" -DISCOVER_COVER = "homematic.cover" -DISCOVER_CLIMATE = "homematic.climate" -DISCOVER_LOCKS = "homematic.locks" -DISCOVER_BATTERY = "homematic.battery" - -ATTR_DISCOVER_DEVICES = "devices" -ATTR_PARAM = "param" -ATTR_CHANNEL = "channel" -ATTR_ADDRESS = "address" -ATTR_VALUE = "value" -ATTR_VALUE_TYPE = "value_type" -ATTR_INTERFACE = "interface" -ATTR_ERRORCODE = "error" -ATTR_MESSAGE = "message" -ATTR_TIME = "time" -ATTR_UNIQUE_ID = "unique_id" -ATTR_PARAMSET_KEY = "paramset_key" -ATTR_PARAMSET = "paramset" -ATTR_DISCOVERY_TYPE = "discovery_type" -ATTR_LOW_BAT = "LOW_BAT" -ATTR_LOWBAT = "LOWBAT" - - -EVENT_KEYPRESS = "homematic.keypress" -EVENT_IMPULSE = "homematic.impulse" -EVENT_ERROR = "homematic.error" - -SERVICE_VIRTUALKEY = "virtualkey" -SERVICE_RECONNECT = "reconnect" -SERVICE_SET_VARIABLE_VALUE = "set_variable_value" -SERVICE_SET_DEVICE_VALUE = "set_device_value" -SERVICE_SET_INSTALL_MODE = "set_install_mode" -SERVICE_PUT_PARAMSET = "put_paramset" - -HM_DEVICE_TYPES = { - DISCOVER_SWITCHES: [ - "Switch", - "SwitchPowermeter", - "IOSwitch", - "IPSwitch", - "RFSiren", - "IPSwitchPowermeter", - "HMWIOSwitch", - "Rain", - "EcoLogic", - "IPKeySwitchPowermeter", - "IPGarage", - "IPKeySwitch", - "IPKeySwitchLevel", - "IPMultiIO", - ], - DISCOVER_LIGHTS: [ - "Dimmer", - "KeyDimmer", - "IPKeyDimmer", - "IPDimmer", - "ColorEffectLight", - "IPKeySwitchLevel", - ], - DISCOVER_SENSORS: [ - "SwitchPowermeter", - "Motion", - "MotionV2", - "RemoteMotion", - "MotionIP", - "ThermostatWall", - "AreaThermostat", - "RotaryHandleSensor", - "WaterSensor", - "PowermeterGas", - "LuxSensor", - "WeatherSensor", - "WeatherStation", - "ThermostatWall2", - "TemperatureDiffSensor", - "TemperatureSensor", - "CO2Sensor", - "IPSwitchPowermeter", - "HMWIOSwitch", - "FillingLevel", - "ValveDrive", - "EcoLogic", - "IPThermostatWall", - "IPSmoke", - "RFSiren", - "PresenceIP", - "IPAreaThermostat", - "IPWeatherSensor", - "RotaryHandleSensorIP", - "IPPassageSensor", - "IPKeySwitchPowermeter", - "IPThermostatWall230V", - "IPWeatherSensorPlus", - "IPWeatherSensorBasic", - "IPBrightnessSensor", - "IPGarage", - "UniversalSensor", - "MotionIPV2", - "IPMultiIO", - "IPThermostatWall2", - ], - DISCOVER_CLIMATE: [ - "Thermostat", - "ThermostatWall", - "MAXThermostat", - "ThermostatWall2", - "MAXWallThermostat", - "IPThermostat", - "IPThermostatWall", - "ThermostatGroup", - "IPThermostatWall230V", - "IPThermostatWall2", - ], - DISCOVER_BINARY_SENSORS: [ - "ShutterContact", - "Smoke", - "SmokeV2", - "Motion", - "MotionV2", - "MotionIP", - "RemoteMotion", - "WeatherSensor", - "TiltSensor", - "IPShutterContact", - "HMWIOSwitch", - "MaxShutterContact", - "Rain", - "WiredSensor", - "PresenceIP", - "IPWeatherSensor", - "IPPassageSensor", - "SmartwareMotion", - "IPWeatherSensorPlus", - "MotionIPV2", - "WaterIP", - "IPMultiIO", - "TiltIP", - "IPShutterContactSabotage", - "IPContact", - ], - DISCOVER_COVER: ["Blind", "KeyBlind", "IPKeyBlind", "IPKeyBlindTilt"], - DISCOVER_LOCKS: ["KeyMatic"], -} - -HM_IGNORE_DISCOVERY_NODE = ["ACTUAL_TEMPERATURE", "ACTUAL_HUMIDITY"] - -HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = { - "ACTUAL_TEMPERATURE": [ - "IPAreaThermostat", - "IPWeatherSensor", - "IPWeatherSensorPlus", - "IPWeatherSensorBasic", - "IPThermostatWall", - "IPThermostatWall2", - ] -} - -HM_ATTRIBUTE_SUPPORT = { - "LOWBAT": ["battery", {0: "High", 1: "Low"}], - "LOW_BAT": ["battery", {0: "High", 1: "Low"}], - "ERROR": ["error", {0: "No"}], - "ERROR_SABOTAGE": ["sabotage", {0: "No", 1: "Yes"}], - "SABOTAGE": ["sabotage", {0: "No", 1: "Yes"}], - "RSSI_PEER": ["rssi_peer", {}], - "RSSI_DEVICE": ["rssi_device", {}], - "VALVE_STATE": ["valve", {}], - "LEVEL": ["level", {}], - "BATTERY_STATE": ["battery", {}], - "CONTROL_MODE": [ - "mode", - {0: "Auto", 1: "Manual", 2: "Away", 3: "Boost", 4: "Comfort", 5: "Lowering"}, - ], - "POWER": ["power", {}], - "CURRENT": ["current", {}], - "VOLTAGE": ["voltage", {}], - "OPERATING_VOLTAGE": ["voltage", {}], - "WORKING": ["working", {0: "No", 1: "Yes"}], - "STATE_UNCERTAIN": ["state_uncertain", {}], -} - -HM_PRESS_EVENTS = [ - "PRESS_SHORT", - "PRESS_LONG", - "PRESS_CONT", - "PRESS_LONG_RELEASE", - "PRESS", -] - -HM_IMPULSE_EVENTS = ["SEQUENCE_OK"] - -CONF_RESOLVENAMES_OPTIONS = ["metadata", "json", "xml", False] - -DATA_HOMEMATIC = "homematic" -DATA_STORE = "homematic_store" -DATA_CONF = "homematic_conf" - -CONF_INTERFACES = "interfaces" -CONF_LOCAL_IP = "local_ip" -CONF_LOCAL_PORT = "local_port" -CONF_PORT = "port" -CONF_PATH = "path" -CONF_CALLBACK_IP = "callback_ip" -CONF_CALLBACK_PORT = "callback_port" -CONF_RESOLVENAMES = "resolvenames" -CONF_JSONPORT = "jsonport" -CONF_VARIABLES = "variables" -CONF_DEVICES = "devices" -CONF_PRIMARY = "primary" - DEFAULT_LOCAL_IP = "0.0.0.0" DEFAULT_LOCAL_PORT = 0 DEFAULT_RESOLVENAMES = False @@ -776,277 +612,3 @@ def _device_from_servicecall(hass, service): for devices in hass.data[DATA_HOMEMATIC].devices.values(): if address in devices: return devices[address] - - -class HMHub(Entity): - """The HomeMatic hub. (CCU2/HomeGear).""" - - def __init__(self, hass, homematic, name): - """Initialize HomeMatic hub.""" - self.hass = hass - self.entity_id = "{}.{}".format(DOMAIN, name.lower()) - self._homematic = homematic - self._variables = {} - self._name = name - self._state = None - - # Load data - self.hass.helpers.event.track_time_interval(self._update_hub, SCAN_INTERVAL_HUB) - self.hass.add_job(self._update_hub, None) - - self.hass.helpers.event.track_time_interval( - self._update_variables, SCAN_INTERVAL_VARIABLES - ) - self.hass.add_job(self._update_variables, None) - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def should_poll(self): - """Return false. HomeMatic Hub object updates variables.""" - return False - - @property - def state(self): - """Return the state of the entity.""" - return self._state - - @property - def state_attributes(self): - """Return the state attributes.""" - attr = self._variables.copy() - return attr - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return "mdi:gradient" - - def _update_hub(self, now): - """Retrieve latest state.""" - service_message = self._homematic.getServiceMessages(self._name) - state = None if service_message is None else len(service_message) - - # state have change? - if self._state != state: - self._state = state - self.schedule_update_ha_state() - - def _update_variables(self, now): - """Retrieve all variable data and update hmvariable states.""" - variables = self._homematic.getAllSystemVariables(self._name) - if variables is None: - return - - state_change = False - for key, value in variables.items(): - if key in self._variables and value == self._variables[key]: - continue - - state_change = True - self._variables.update({key: value}) - - if state_change: - self.schedule_update_ha_state() - - def hm_set_variable(self, name, value): - """Set variable value on CCU/Homegear.""" - if name not in self._variables: - _LOGGER.error("Variable %s not found on %s", name, self.name) - return - old_value = self._variables.get(name) - if isinstance(old_value, bool): - value = cv.boolean(value) - else: - value = float(value) - self._homematic.setSystemVariable(self.name, name, value) - - self._variables.update({name: value}) - self.schedule_update_ha_state() - - -class HMDevice(Entity): - """The HomeMatic device base object.""" - - def __init__(self, config): - """Initialize a generic HomeMatic device.""" - self._name = config.get(ATTR_NAME) - self._address = config.get(ATTR_ADDRESS) - self._interface = config.get(ATTR_INTERFACE) - self._channel = config.get(ATTR_CHANNEL) - self._state = config.get(ATTR_PARAM) - self._unique_id = config.get(ATTR_UNIQUE_ID) - self._data = {} - self._homematic = None - self._hmdevice = None - self._connected = False - self._available = False - - # Set parameter to uppercase - if self._state: - self._state = self._state.upper() - - async def async_added_to_hass(self): - """Load data init callbacks.""" - await self.hass.async_add_job(self.link_homematic) - - @property - def unique_id(self): - """Return unique ID. HomeMatic entity IDs are unique by default.""" - return self._unique_id.replace(" ", "_") - - @property - def should_poll(self): - """Return false. HomeMatic states are pushed by the XML-RPC Server.""" - return False - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def available(self): - """Return true if device is available.""" - return self._available - - @property - def device_state_attributes(self): - """Return device specific state attributes.""" - attr = {} - - # Generate a dictionary with attributes - for node, data in HM_ATTRIBUTE_SUPPORT.items(): - # Is an attribute and exists for this object - if node in self._data: - value = data[1].get(self._data[node], self._data[node]) - attr[data[0]] = value - - # Static attributes - attr["id"] = self._hmdevice.ADDRESS - attr["interface"] = self._interface - - return attr - - def link_homematic(self): - """Connect to HomeMatic.""" - if self._connected: - return True - - # Initialize - self._homematic = self.hass.data[DATA_HOMEMATIC] - self._hmdevice = self._homematic.devices[self._interface][self._address] - self._connected = True - - try: - # Initialize datapoints of this object - self._init_data() - self._load_data_from_hm() - - # Link events from pyhomematic - self._subscribe_homematic_events() - self._available = not self._hmdevice.UNREACH - except Exception as err: # pylint: disable=broad-except - self._connected = False - _LOGGER.error("Exception while linking %s: %s", self._address, str(err)) - - def _hm_event_callback(self, device, caller, attribute, value): - """Handle all pyhomematic device events.""" - _LOGGER.debug("%s received event '%s' value: %s", self._name, attribute, value) - has_changed = False - - # Is data needed for this instance? - if attribute in self._data: - # Did data change? - if self._data[attribute] != value: - self._data[attribute] = value - has_changed = True - - # Availability has changed - if self.available != (not self._hmdevice.UNREACH): - self._available = not self._hmdevice.UNREACH - has_changed = True - - # If it has changed data point, update Home Assistant - if has_changed: - self.schedule_update_ha_state() - - def _subscribe_homematic_events(self): - """Subscribe all required events to handle job.""" - channels_to_sub = set() - - # Push data to channels_to_sub from hmdevice metadata - for metadata in ( - self._hmdevice.SENSORNODE, - self._hmdevice.BINARYNODE, - self._hmdevice.ATTRIBUTENODE, - self._hmdevice.WRITENODE, - self._hmdevice.EVENTNODE, - self._hmdevice.ACTIONNODE, - ): - for node, channels in metadata.items(): - # Data is needed for this instance - if node in self._data: - # chan is current channel - if len(channels) == 1: - channel = channels[0] - else: - channel = self._channel - - # Prepare for subscription - try: - channels_to_sub.add(int(channel)) - except (ValueError, TypeError): - _LOGGER.error("Invalid channel in metadata from %s", self._name) - - # Set callbacks - for channel in channels_to_sub: - _LOGGER.debug("Subscribe channel %d from %s", channel, self._name) - self._hmdevice.setEventCallback( - callback=self._hm_event_callback, bequeath=False, channel=channel - ) - - def _load_data_from_hm(self): - """Load first value from pyhomematic.""" - if not self._connected: - return False - - # Read data from pyhomematic - for metadata, funct in ( - (self._hmdevice.ATTRIBUTENODE, self._hmdevice.getAttributeData), - (self._hmdevice.WRITENODE, self._hmdevice.getWriteData), - (self._hmdevice.SENSORNODE, self._hmdevice.getSensorData), - (self._hmdevice.BINARYNODE, self._hmdevice.getBinaryData), - ): - for node in metadata: - if metadata[node] and node in self._data: - self._data[node] = funct(name=node, channel=self._channel) - - return True - - def _hm_set_state(self, value): - """Set data to main datapoint.""" - if self._state in self._data: - self._data[self._state] = value - - def _hm_get_state(self): - """Get data from main datapoint.""" - if self._state in self._data: - return self._data[self._state] - return None - - def _init_data(self): - """Generate a data dict (self._data) from the HomeMatic metadata.""" - # Add all attributes to data dictionary - for data_note in self._hmdevice.ATTRIBUTENODE: - self._data.update({data_note: STATE_UNKNOWN}) - - # Initialize device specific data - self._init_data_struct() - - def _init_data_struct(self): - """Generate a data dictionary from the HomeMatic device metadata.""" - raise NotImplementedError diff --git a/homeassistant/components/homematic/binary_sensor.py b/homeassistant/components/homematic/binary_sensor.py index 1832652406d..731525c8460 100644 --- a/homeassistant/components/homematic/binary_sensor.py +++ b/homeassistant/components/homematic/binary_sensor.py @@ -9,9 +9,9 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_SMOKE, BinarySensorDevice, ) -from homeassistant.components.homematic import ATTR_DISCOVERY_TYPE, DISCOVER_BATTERY -from . import ATTR_DISCOVER_DEVICES, HMDevice +from .const import ATTR_DISCOVER_DEVICES, ATTR_DISCOVERY_TYPE, DISCOVER_BATTERY +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -45,7 +45,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): else: devices.append(HMBinarySensor(conf)) - add_entities(devices) + add_entities(devices, True) class HMBinarySensor(HMDevice, BinarySensorDevice): diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index 935ebb9b497..b4ab277a75b 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -14,7 +14,8 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from . import ATTR_DISCOVER_DEVICES, HM_ATTRIBUTE_SUPPORT, HMDevice +from .const import ATTR_DISCOVER_DEVICES, HM_ATTRIBUTE_SUPPORT +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -44,7 +45,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): new_device = HMThermostat(conf) devices.append(new_device) - add_entities(devices) + add_entities(devices, True) class HMThermostat(HMDevice, ClimateDevice): diff --git a/homeassistant/components/homematic/const.py b/homeassistant/components/homematic/const.py new file mode 100644 index 00000000000..cd2d528044a --- /dev/null +++ b/homeassistant/components/homematic/const.py @@ -0,0 +1,212 @@ +"""Constants for the homematic component.""" + +DOMAIN = "homematic" + +DISCOVER_SWITCHES = "homematic.switch" +DISCOVER_LIGHTS = "homematic.light" +DISCOVER_SENSORS = "homematic.sensor" +DISCOVER_BINARY_SENSORS = "homematic.binary_sensor" +DISCOVER_COVER = "homematic.cover" +DISCOVER_CLIMATE = "homematic.climate" +DISCOVER_LOCKS = "homematic.locks" +DISCOVER_BATTERY = "homematic.battery" + +ATTR_DISCOVER_DEVICES = "devices" +ATTR_PARAM = "param" +ATTR_CHANNEL = "channel" +ATTR_ADDRESS = "address" +ATTR_VALUE = "value" +ATTR_VALUE_TYPE = "value_type" +ATTR_INTERFACE = "interface" +ATTR_ERRORCODE = "error" +ATTR_MESSAGE = "message" +ATTR_TIME = "time" +ATTR_UNIQUE_ID = "unique_id" +ATTR_PARAMSET_KEY = "paramset_key" +ATTR_PARAMSET = "paramset" +ATTR_DISCOVERY_TYPE = "discovery_type" +ATTR_LOW_BAT = "LOW_BAT" +ATTR_LOWBAT = "LOWBAT" + +EVENT_KEYPRESS = "homematic.keypress" +EVENT_IMPULSE = "homematic.impulse" +EVENT_ERROR = "homematic.error" + +SERVICE_VIRTUALKEY = "virtualkey" +SERVICE_RECONNECT = "reconnect" +SERVICE_SET_VARIABLE_VALUE = "set_variable_value" +SERVICE_SET_DEVICE_VALUE = "set_device_value" +SERVICE_SET_INSTALL_MODE = "set_install_mode" +SERVICE_PUT_PARAMSET = "put_paramset" + +HM_DEVICE_TYPES = { + DISCOVER_SWITCHES: [ + "Switch", + "SwitchPowermeter", + "IOSwitch", + "IPSwitch", + "RFSiren", + "IPSwitchPowermeter", + "HMWIOSwitch", + "Rain", + "EcoLogic", + "IPKeySwitchPowermeter", + "IPGarage", + "IPKeySwitch", + "IPKeySwitchLevel", + "IPMultiIO", + ], + DISCOVER_LIGHTS: [ + "Dimmer", + "KeyDimmer", + "IPKeyDimmer", + "IPDimmer", + "ColorEffectLight", + "IPKeySwitchLevel", + ], + DISCOVER_SENSORS: [ + "SwitchPowermeter", + "Motion", + "MotionV2", + "RemoteMotion", + "MotionIP", + "ThermostatWall", + "AreaThermostat", + "RotaryHandleSensor", + "WaterSensor", + "PowermeterGas", + "LuxSensor", + "WeatherSensor", + "WeatherStation", + "ThermostatWall2", + "TemperatureDiffSensor", + "TemperatureSensor", + "CO2Sensor", + "IPSwitchPowermeter", + "HMWIOSwitch", + "FillingLevel", + "ValveDrive", + "EcoLogic", + "IPThermostatWall", + "IPSmoke", + "RFSiren", + "PresenceIP", + "IPAreaThermostat", + "IPWeatherSensor", + "RotaryHandleSensorIP", + "IPPassageSensor", + "IPKeySwitchPowermeter", + "IPThermostatWall230V", + "IPWeatherSensorPlus", + "IPWeatherSensorBasic", + "IPBrightnessSensor", + "IPGarage", + "UniversalSensor", + "MotionIPV2", + "IPMultiIO", + "IPThermostatWall2", + ], + DISCOVER_CLIMATE: [ + "Thermostat", + "ThermostatWall", + "MAXThermostat", + "ThermostatWall2", + "MAXWallThermostat", + "IPThermostat", + "IPThermostatWall", + "ThermostatGroup", + "IPThermostatWall230V", + "IPThermostatWall2", + ], + DISCOVER_BINARY_SENSORS: [ + "ShutterContact", + "Smoke", + "SmokeV2", + "Motion", + "MotionV2", + "MotionIP", + "RemoteMotion", + "WeatherSensor", + "TiltSensor", + "IPShutterContact", + "HMWIOSwitch", + "MaxShutterContact", + "Rain", + "WiredSensor", + "PresenceIP", + "IPWeatherSensor", + "IPPassageSensor", + "SmartwareMotion", + "IPWeatherSensorPlus", + "MotionIPV2", + "WaterIP", + "IPMultiIO", + "TiltIP", + "IPShutterContactSabotage", + "IPContact", + ], + DISCOVER_COVER: ["Blind", "KeyBlind", "IPKeyBlind", "IPKeyBlindTilt"], + DISCOVER_LOCKS: ["KeyMatic"], +} + +HM_IGNORE_DISCOVERY_NODE = ["ACTUAL_TEMPERATURE", "ACTUAL_HUMIDITY"] + +HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = { + "ACTUAL_TEMPERATURE": [ + "IPAreaThermostat", + "IPWeatherSensor", + "IPWeatherSensorPlus", + "IPWeatherSensorBasic", + "IPThermostatWall", + "IPThermostatWall2", + ] +} + +HM_ATTRIBUTE_SUPPORT = { + "LOWBAT": ["battery", {0: "High", 1: "Low"}], + "LOW_BAT": ["battery", {0: "High", 1: "Low"}], + "ERROR": ["error", {0: "No"}], + "ERROR_SABOTAGE": ["sabotage", {0: "No", 1: "Yes"}], + "SABOTAGE": ["sabotage", {0: "No", 1: "Yes"}], + "RSSI_PEER": ["rssi_peer", {}], + "RSSI_DEVICE": ["rssi_device", {}], + "VALVE_STATE": ["valve", {}], + "LEVEL": ["level", {}], + "BATTERY_STATE": ["battery", {}], + "CONTROL_MODE": [ + "mode", + {0: "Auto", 1: "Manual", 2: "Away", 3: "Boost", 4: "Comfort", 5: "Lowering"}, + ], + "POWER": ["power", {}], + "CURRENT": ["current", {}], + "VOLTAGE": ["voltage", {}], + "OPERATING_VOLTAGE": ["voltage", {}], + "WORKING": ["working", {0: "No", 1: "Yes"}], + "STATE_UNCERTAIN": ["state_uncertain", {}], +} + +HM_PRESS_EVENTS = [ + "PRESS_SHORT", + "PRESS_LONG", + "PRESS_CONT", + "PRESS_LONG_RELEASE", + "PRESS", +] + +HM_IMPULSE_EVENTS = ["SEQUENCE_OK"] + +CONF_RESOLVENAMES_OPTIONS = ["metadata", "json", "xml", False] + +DATA_HOMEMATIC = "homematic" +DATA_STORE = "homematic_store" +DATA_CONF = "homematic_conf" + +CONF_INTERFACES = "interfaces" +CONF_LOCAL_IP = "local_ip" +CONF_LOCAL_PORT = "local_port" +CONF_PORT = "port" +CONF_PATH = "path" +CONF_CALLBACK_IP = "callback_ip" +CONF_CALLBACK_PORT = "callback_port" +CONF_RESOLVENAMES = "resolvenames" +CONF_JSONPORT = "jsonport" diff --git a/homeassistant/components/homematic/cover.py b/homeassistant/components/homematic/cover.py index 893b3ce8921..0dea1181d73 100644 --- a/homeassistant/components/homematic/cover.py +++ b/homeassistant/components/homematic/cover.py @@ -6,9 +6,9 @@ from homeassistant.components.cover import ( ATTR_TILT_POSITION, CoverDevice, ) -from homeassistant.const import STATE_UNKNOWN -from . import ATTR_DISCOVER_DEVICES, HMDevice +from .const import ATTR_DISCOVER_DEVICES +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -23,7 +23,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): new_device = HMCover(conf) devices.append(new_device) - add_entities(devices) + add_entities(devices, True) class HMCover(HMDevice, CoverDevice): @@ -68,9 +68,9 @@ class HMCover(HMDevice, CoverDevice): def _init_data_struct(self): """Generate a data dictionary (self._data) from metadata.""" self._state = "LEVEL" - self._data.update({self._state: STATE_UNKNOWN}) + self._data.update({self._state: None}) if "LEVEL_2" in self._hmdevice.WRITENODE: - self._data.update({"LEVEL_2": STATE_UNKNOWN}) + self._data.update({"LEVEL_2": None}) @property def current_cover_tilt_position(self): diff --git a/homeassistant/components/homematic/entity.py b/homeassistant/components/homematic/entity.py new file mode 100644 index 00000000000..4ed893bbf14 --- /dev/null +++ b/homeassistant/components/homematic/entity.py @@ -0,0 +1,297 @@ +"""Homematic base entity.""" +from abc import abstractmethod +from datetime import timedelta +import logging + +from homeassistant.const import ATTR_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +from .const import ( + ATTR_ADDRESS, + ATTR_CHANNEL, + ATTR_INTERFACE, + ATTR_PARAM, + ATTR_UNIQUE_ID, + DATA_HOMEMATIC, + DOMAIN, + HM_ATTRIBUTE_SUPPORT, +) + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL_HUB = timedelta(seconds=300) +SCAN_INTERVAL_VARIABLES = timedelta(seconds=30) + + +class HMDevice(Entity): + """The HomeMatic device base object.""" + + def __init__(self, config): + """Initialize a generic HomeMatic device.""" + self._name = config.get(ATTR_NAME) + self._address = config.get(ATTR_ADDRESS) + self._interface = config.get(ATTR_INTERFACE) + self._channel = config.get(ATTR_CHANNEL) + self._state = config.get(ATTR_PARAM) + self._unique_id = config.get(ATTR_UNIQUE_ID) + self._data = {} + self._homematic = None + self._hmdevice = None + self._connected = False + self._available = False + + # Set parameter to uppercase + if self._state: + self._state = self._state.upper() + + async def async_added_to_hass(self): + """Load data init callbacks.""" + await self.hass.async_add_job(self._subscribe_homematic_events) + + @property + def unique_id(self): + """Return unique ID. HomeMatic entity IDs are unique by default.""" + return self._unique_id.replace(" ", "_") + + @property + def should_poll(self): + """Return false. HomeMatic states are pushed by the XML-RPC Server.""" + return False + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def available(self): + """Return true if device is available.""" + return self._available + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + attr = {} + + # Generate a dictionary with attributes + for node, data in HM_ATTRIBUTE_SUPPORT.items(): + # Is an attribute and exists for this object + if node in self._data: + value = data[1].get(self._data[node], self._data[node]) + attr[data[0]] = value + + # Static attributes + attr["id"] = self._hmdevice.ADDRESS + attr["interface"] = self._interface + + return attr + + def update(self): + """Connect to HomeMatic init values.""" + if self._connected: + return True + + # Initialize + self._homematic = self.hass.data[DATA_HOMEMATIC] + self._hmdevice = self._homematic.devices[self._interface][self._address] + self._connected = True + + try: + # Initialize datapoints of this object + self._init_data() + self._load_data_from_hm() + + # Link events from pyhomematic + self._available = not self._hmdevice.UNREACH + except Exception as err: # pylint: disable=broad-except + self._connected = False + _LOGGER.error("Exception while linking %s: %s", self._address, str(err)) + + def _hm_event_callback(self, device, caller, attribute, value): + """Handle all pyhomematic device events.""" + _LOGGER.debug("%s received event '%s' value: %s", self._name, attribute, value) + has_changed = False + + # Is data needed for this instance? + if attribute in self._data: + # Did data change? + if self._data[attribute] != value: + self._data[attribute] = value + has_changed = True + + # Availability has changed + if self.available != (not self._hmdevice.UNREACH): + self._available = not self._hmdevice.UNREACH + has_changed = True + + # If it has changed data point, update Home Assistant + if has_changed: + self.schedule_update_ha_state() + + def _subscribe_homematic_events(self): + """Subscribe all required events to handle job.""" + channels_to_sub = set() + + # Push data to channels_to_sub from hmdevice metadata + for metadata in ( + self._hmdevice.SENSORNODE, + self._hmdevice.BINARYNODE, + self._hmdevice.ATTRIBUTENODE, + self._hmdevice.WRITENODE, + self._hmdevice.EVENTNODE, + self._hmdevice.ACTIONNODE, + ): + for node, channels in metadata.items(): + # Data is needed for this instance + if node in self._data: + # chan is current channel + if len(channels) == 1: + channel = channels[0] + else: + channel = self._channel + + # Prepare for subscription + try: + channels_to_sub.add(int(channel)) + except (ValueError, TypeError): + _LOGGER.error("Invalid channel in metadata from %s", self._name) + + # Set callbacks + for channel in channels_to_sub: + _LOGGER.debug("Subscribe channel %d from %s", channel, self._name) + self._hmdevice.setEventCallback( + callback=self._hm_event_callback, bequeath=False, channel=channel + ) + + def _load_data_from_hm(self): + """Load first value from pyhomematic.""" + if not self._connected: + return False + + # Read data from pyhomematic + for metadata, funct in ( + (self._hmdevice.ATTRIBUTENODE, self._hmdevice.getAttributeData), + (self._hmdevice.WRITENODE, self._hmdevice.getWriteData), + (self._hmdevice.SENSORNODE, self._hmdevice.getSensorData), + (self._hmdevice.BINARYNODE, self._hmdevice.getBinaryData), + ): + for node in metadata: + if metadata[node] and node in self._data: + self._data[node] = funct(name=node, channel=self._channel) + + return True + + def _hm_set_state(self, value): + """Set data to main datapoint.""" + if self._state in self._data: + self._data[self._state] = value + + def _hm_get_state(self): + """Get data from main datapoint.""" + if self._state in self._data: + return self._data[self._state] + return None + + def _init_data(self): + """Generate a data dict (self._data) from the HomeMatic metadata.""" + # Add all attributes to data dictionary + for data_note in self._hmdevice.ATTRIBUTENODE: + self._data.update({data_note: None}) + + # Initialize device specific data + self._init_data_struct() + + @abstractmethod + def _init_data_struct(self): + """Generate a data dictionary from the HomeMatic device metadata.""" + + +class HMHub(Entity): + """The HomeMatic hub. (CCU2/HomeGear).""" + + def __init__(self, hass, homematic, name): + """Initialize HomeMatic hub.""" + self.hass = hass + self.entity_id = "{}.{}".format(DOMAIN, name.lower()) + self._homematic = homematic + self._variables = {} + self._name = name + self._state = None + + # Load data + self.hass.helpers.event.track_time_interval(self._update_hub, SCAN_INTERVAL_HUB) + self.hass.add_job(self._update_hub, None) + + self.hass.helpers.event.track_time_interval( + self._update_variables, SCAN_INTERVAL_VARIABLES + ) + self.hass.add_job(self._update_variables, None) + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def should_poll(self): + """Return false. HomeMatic Hub object updates variables.""" + return False + + @property + def state(self): + """Return the state of the entity.""" + return self._state + + @property + def state_attributes(self): + """Return the state attributes.""" + attr = self._variables.copy() + return attr + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return "mdi:gradient" + + def _update_hub(self, now): + """Retrieve latest state.""" + service_message = self._homematic.getServiceMessages(self._name) + state = None if service_message is None else len(service_message) + + # state have change? + if self._state != state: + self._state = state + self.schedule_update_ha_state() + + def _update_variables(self, now): + """Retrieve all variable data and update hmvariable states.""" + variables = self._homematic.getAllSystemVariables(self._name) + if variables is None: + return + + state_change = False + for key, value in variables.items(): + if key in self._variables and value == self._variables[key]: + continue + + state_change = True + self._variables.update({key: value}) + + if state_change: + self.schedule_update_ha_state() + + def hm_set_variable(self, name, value): + """Set variable value on CCU/Homegear.""" + if name not in self._variables: + _LOGGER.error("Variable %s not found on %s", name, self.name) + return + old_value = self._variables.get(name) + if isinstance(old_value, bool): + value = cv.boolean(value) + else: + value = float(value) + self._homematic.setSystemVariable(self.name, name, value) + + self._variables.update({name: value}) + self.schedule_update_ha_state() diff --git a/homeassistant/components/homematic/light.py b/homeassistant/components/homematic/light.py index 29992bccef3..52b2f9a7996 100644 --- a/homeassistant/components/homematic/light.py +++ b/homeassistant/components/homematic/light.py @@ -12,7 +12,8 @@ from homeassistant.components.light import ( Light, ) -from . import ATTR_DISCOVER_DEVICES, HMDevice +from .const import ATTR_DISCOVER_DEVICES +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -29,7 +30,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): new_device = HMLight(conf) devices.append(new_device) - add_entities(devices) + add_entities(devices, True) class HMLight(HMDevice, Light): diff --git a/homeassistant/components/homematic/lock.py b/homeassistant/components/homematic/lock.py index 7f796b32885..0094ecd2e81 100644 --- a/homeassistant/components/homematic/lock.py +++ b/homeassistant/components/homematic/lock.py @@ -2,9 +2,9 @@ import logging from homeassistant.components.lock import SUPPORT_OPEN, LockDevice -from homeassistant.const import STATE_UNKNOWN -from . import ATTR_DISCOVER_DEVICES, HMDevice +from .const import ATTR_DISCOVER_DEVICES +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -18,7 +18,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for conf in discovery_info[ATTR_DISCOVER_DEVICES]: devices.append(HMLock(conf)) - add_entities(devices) + add_entities(devices, True) class HMLock(HMDevice, LockDevice): @@ -44,7 +44,7 @@ class HMLock(HMDevice, LockDevice): def _init_data_struct(self): """Generate the data dictionary (self._data) from metadata.""" self._state = "STATE" - self._data.update({self._state: STATE_UNKNOWN}) + self._data.update({self._state: None}) @property def supported_features(self): diff --git a/homeassistant/components/homematic/notify.py b/homeassistant/components/homematic/notify.py index 9fd94b9832c..3d48adc6df2 100644 --- a/homeassistant/components/homematic/notify.py +++ b/homeassistant/components/homematic/notify.py @@ -11,7 +11,7 @@ from homeassistant.components.notify import ( import homeassistant.helpers.config_validation as cv import homeassistant.helpers.template as template_helper -from . import ( +from .const import ( ATTR_ADDRESS, ATTR_CHANNEL, ATTR_INTERFACE, @@ -22,6 +22,7 @@ from . import ( ) _LOGGER = logging.getLogger(__name__) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index 10c402a0dd4..bba8325650d 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -8,10 +8,10 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, ENERGY_WATT_HOUR, POWER_WATT, - STATE_UNKNOWN, ) -from . import ATTR_DISCOVER_DEVICES, HMDevice +from .const import ATTR_DISCOVER_DEVICES +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -82,7 +82,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): new_device = HMSensor(conf) devices.append(new_device) - add_entities(devices) + add_entities(devices, True) class HMSensor(HMDevice): @@ -117,6 +117,6 @@ class HMSensor(HMDevice): def _init_data_struct(self): """Generate a data dictionary (self._data) from metadata.""" if self._state: - self._data.update({self._state: STATE_UNKNOWN}) + self._data.update({self._state: None}) else: _LOGGER.critical("Unable to initialize sensor: %s", self._name) diff --git a/homeassistant/components/homematic/switch.py b/homeassistant/components/homematic/switch.py index b77b3a1f700..53679818083 100644 --- a/homeassistant/components/homematic/switch.py +++ b/homeassistant/components/homematic/switch.py @@ -2,9 +2,9 @@ import logging from homeassistant.components.switch import SwitchDevice -from homeassistant.const import STATE_UNKNOWN -from . import ATTR_DISCOVER_DEVICES, HMDevice +from .const import ATTR_DISCOVER_DEVICES +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -19,7 +19,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): new_device = HMSwitch(conf) devices.append(new_device) - add_entities(devices) + add_entities(devices, True) class HMSwitch(HMDevice, SwitchDevice): @@ -55,8 +55,8 @@ class HMSwitch(HMDevice, SwitchDevice): def _init_data_struct(self): """Generate the data dictionary (self._data) from metadata.""" self._state = "STATE" - self._data.update({self._state: STATE_UNKNOWN}) + self._data.update({self._state: None}) # Need sensor values for SwitchPowermeter for node in self._hmdevice.SENSORNODE: - self._data.update({node: STATE_UNKNOWN}) + self._data.update({node: None}) From 9de800ab6a447c78ec4b796cfee5ecaf797dddea Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 14 Jan 2020 17:41:52 +0100 Subject: [PATCH 35/41] bump aiokef to 0.2.5 which uses locks (#30753) --- homeassistant/components/kef/manifest.json | 2 +- homeassistant/components/kef/media_player.py | 1 - requirements_all.txt | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/kef/manifest.json b/homeassistant/components/kef/manifest.json index 30335c409ee..b950b144cf9 100644 --- a/homeassistant/components/kef/manifest.json +++ b/homeassistant/components/kef/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/kef", "dependencies": [], "codeowners": ["@basnijholt"], - "requirements": ["aiokef==0.2.2", "getmac==0.8.1"] + "requirements": ["aiokef==0.2.5", "getmac==0.8.1"] } diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index f0c2de2a86a..177b2fccd13 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -40,7 +40,6 @@ DEFAULT_INVERSE_SPEAKER_MODE = False DOMAIN = "kef" SCAN_INTERVAL = timedelta(seconds=30) -PARALLEL_UPDATES = 0 SOURCES = {"LSX": ["Wifi", "Bluetooth", "Aux", "Opt"]} SOURCES["LS50"] = SOURCES["LSX"] + ["Usb"] diff --git a/requirements_all.txt b/requirements_all.txt index 239d4ab9cf7..c4fd9c6c5d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -172,7 +172,7 @@ aioimaplib==0.7.15 aiokafka==0.5.1 # homeassistant.components.kef -aiokef==0.2.2 +aiokef==0.2.5 # homeassistant.components.lifx aiolifx==0.6.7 From fe9c85aaf1fb5b163c81b3a547646d0dd8ca6804 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 14 Jan 2020 16:09:35 +0100 Subject: [PATCH 36/41] Fix HomeKit behavior with lights supporting color and temperature (#30756) --- .../components/homekit/type_lights.py | 12 ++++++++++-- tests/components/homekit/test_type_lights.py | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 7f195b276d6..3fc6a0628ff 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -66,15 +66,20 @@ class Light(HomeAccessory): self._features = self.hass.states.get(self.entity_id).attributes.get( ATTR_SUPPORTED_FEATURES ) + if self._features & SUPPORT_BRIGHTNESS: self.chars.append(CHAR_BRIGHTNESS) - if self._features & SUPPORT_COLOR_TEMP: - self.chars.append(CHAR_COLOR_TEMPERATURE) + if self._features & SUPPORT_COLOR: self.chars.append(CHAR_HUE) self.chars.append(CHAR_SATURATION) self._hue = None self._saturation = None + elif self._features & SUPPORT_COLOR_TEMP: + # ColorTemperature and Hue characteristic should not be + # exposed both. Both states are tracked separately in HomeKit, + # causing "source of truth" problems. + self.chars.append(CHAR_COLOR_TEMPERATURE) serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars) self.char_on = serv_light.configure_char( @@ -88,6 +93,7 @@ class Light(HomeAccessory): self.char_brightness = serv_light.configure_char( CHAR_BRIGHTNESS, value=100, setter_callback=self.set_brightness ) + if CHAR_COLOR_TEMPERATURE in self.chars: min_mireds = self.hass.states.get(self.entity_id).attributes.get( ATTR_MIN_MIREDS, 153 @@ -101,10 +107,12 @@ class Light(HomeAccessory): properties={PROP_MIN_VALUE: min_mireds, PROP_MAX_VALUE: max_mireds}, setter_callback=self.set_color_temperature, ) + if CHAR_HUE in self.chars: self.char_hue = serv_light.configure_char( CHAR_HUE, value=0, setter_callback=self.set_hue ) + if CHAR_SATURATION in self.chars: self.char_saturation = serv_light.configure_char( CHAR_SATURATION, value=75, setter_callback=self.set_saturation diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index c1811a2e2fc..f357702040b 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -177,6 +177,25 @@ async def test_light_color_temperature(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] == "color temperature at 250" +async def test_light_color_temperature_and_rgb_color(hass, hk_driver, cls, events): + """Test light with color temperature and rgb color not exposing temperature.""" + entity_id = "light.demo" + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP | SUPPORT_COLOR, + ATTR_COLOR_TEMP: 190, + ATTR_HS_COLOR: (260, 90), + }, + ) + await hass.async_block_till_done() + acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None) + + assert not hasattr(acc, "char_color_temperature") + + async def test_light_rgb_color(hass, hk_driver, cls, events): """Test light with rgb_color.""" entity_id = "light.demo" From 8dbdf0b0e18407d787571654a497b39e12ba09c3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 Jan 2020 12:59:50 -0800 Subject: [PATCH 37/41] Bumped version to 0.104.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8556fd1a7d8..183fdfbcc1b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 104 -PATCH_VERSION = "0b4" +PATCH_VERSION = "0b5" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From 1c70435df69d861b5fc6694f6ab0701e676f7b10 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 14 Jan 2020 23:02:49 +0100 Subject: [PATCH 38/41] Revert #29701 (#30766) * Revert #29701 * fix format * Fix lint --- homeassistant/components/zwave/__init__.py | 5 - homeassistant/components/zwave/cover.py | 144 --------------------- 2 files changed, 149 deletions(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 414e7f0017c..9b9236de1c2 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -79,14 +79,12 @@ CONF_REFRESH_DELAY = "delay" CONF_DEVICE_CONFIG = "device_config" CONF_DEVICE_CONFIG_GLOB = "device_config_glob" CONF_DEVICE_CONFIG_DOMAIN = "device_config_domain" -CONF_TILT_OPEN_POSITION = "tilt_open_position" DEFAULT_CONF_IGNORED = False DEFAULT_CONF_INVERT_OPENCLOSE_BUTTONS = False DEFAULT_CONF_INVERT_PERCENT = False DEFAULT_CONF_REFRESH_VALUE = False DEFAULT_CONF_REFRESH_DELAY = 5 -DEFAULT_CONF_TILT_OPEN_POSITION = 50 SUPPORTED_PLATFORMS = [ "binary_sensor", @@ -216,9 +214,6 @@ DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema( vol.Optional( CONF_REFRESH_DELAY, default=DEFAULT_CONF_REFRESH_DELAY ): cv.positive_int, - vol.Optional( - CONF_TILT_OPEN_POSITION, default=DEFAULT_CONF_TILT_OPEN_POSITION - ): cv.positive_int, } ) diff --git a/homeassistant/components/zwave/cover.py b/homeassistant/components/zwave/cover.py index b6f54fc7d07..95cc994e4ff 100644 --- a/homeassistant/components/zwave/cover.py +++ b/homeassistant/components/zwave/cover.py @@ -3,7 +3,6 @@ import logging from homeassistant.components.cover import ( ATTR_POSITION, - ATTR_TILT_POSITION, DOMAIN, SUPPORT_CLOSE, SUPPORT_OPEN, @@ -15,13 +14,11 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import ( CONF_INVERT_OPENCLOSE_BUTTONS, CONF_INVERT_PERCENT, - CONF_TILT_OPEN_POSITION, ZWaveDeviceEntity, workaround, ) from .const import ( COMMAND_CLASS_BARRIER_OPERATOR, - COMMAND_CLASS_MANUFACTURER_PROPRIETARY, COMMAND_CLASS_SWITCH_BINARY, COMMAND_CLASS_SWITCH_MULTILEVEL, DATA_NETWORK, @@ -32,23 +29,6 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE -def _to_hex_str(id_in_bytes): - """Convert a two byte value to a hex string. - - Example: 0x1234 --> '0x1234' - """ - return f"0x{id_in_bytes:04x}" - - -# For some reason node.manufacturer_id is of type string. So we need to convert -# the values. -FIBARO = _to_hex_str(workaround.FIBARO) -FIBARO222_SHUTTERS = [ - _to_hex_str(workaround.FGR222_SHUTTER2), - _to_hex_str(workaround.FGRM222_SHUTTER2), -] - - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old method of setting up Z-Wave covers.""" pass @@ -73,17 +53,6 @@ def get_device(hass, values, node_config, **kwargs): values.primary.command_class == COMMAND_CLASS_SWITCH_MULTILEVEL and values.primary.index == 0 ): - if ( - values.primary.node.manufacturer_id == FIBARO - and values.primary.node.product_type in FIBARO222_SHUTTERS - ): - return FibaroFGRM222( - hass, - values, - invert_buttons, - invert_percent, - node_config.get(CONF_TILT_OPEN_POSITION), - ) return ZwaveRollershutter(hass, values, invert_buttons, invert_percent) if values.primary.command_class == COMMAND_CLASS_SWITCH_BINARY: return ZwaveGarageDoorSwitch(values) @@ -243,116 +212,3 @@ class ZwaveGarageDoorBarrier(ZwaveGarageDoorBase): def open_cover(self, **kwargs): """Open the garage door.""" self.values.primary.data = "Opened" - - -class FibaroFGRM222(ZwaveRollershutter): - """Implementation of proprietary features for Fibaro FGR-222 / FGRM-222. - - This adds support for the tilt feature for the ventian blind mode. - To enable this you need to configure the devices to use the venetian blind - mode and to enable the proprietary command class: - * Set "3: Reports type to Blind position reports sent" - to value "the main controller using Fibaro Command Class" - * Set "10: Roller Shutter operating modes" - to value "2 - Venetian Blind Mode, with positioning" - """ - - def __init__( - self, hass, values, invert_buttons, invert_percent, open_tilt_position: int - ): - """Initialize the FGRM-222.""" - self._value_blinds = None - self._value_tilt = None - self._has_tilt_mode = False # type: bool - self._open_tilt_position = 50 # type: int - if open_tilt_position is not None: - self._open_tilt_position = open_tilt_position - super().__init__(hass, values, invert_buttons, invert_percent) - - @property - def current_cover_tilt_position(self) -> int: - """Get the tilt of the blinds. - - Saturate values <5 and >94 so that it's easier to detect the end - positions in automations. - """ - if not self._has_tilt_mode: - return None - if self._value_tilt.data <= 5: - return 0 - if self._value_tilt.data >= 95: - return 100 - return self._value_tilt.data - - def set_cover_tilt_position(self, **kwargs): - """Move the cover tilt to a specific position.""" - if not self._has_tilt_mode: - _LOGGER.error("Can't set cover tilt as device is not yet set up.") - else: - # Limit the range to [0-99], as this what that the ZWave command - # accepts. - tilt_position = max(0, min(99, kwargs.get(ATTR_TILT_POSITION))) - _LOGGER.debug("setting tilt to %d", tilt_position) - self._value_tilt.data = tilt_position - - def open_cover_tilt(self, **kwargs): - """Set slats to horizontal position.""" - self.set_cover_tilt_position(tilt_position=self._open_tilt_position) - - def close_cover_tilt(self, **kwargs): - """Close the slats.""" - self.set_cover_tilt_position(tilt_position=0) - - def set_cover_position(self, **kwargs): - """Move the roller shutter to a specific position. - - If the venetian blinds mode is not activated, fall back to - the behavior of the parent class. - """ - if not self._has_tilt_mode: - super().set_cover_position(**kwargs) - else: - _LOGGER.debug("Setting cover position to %s", kwargs.get(ATTR_POSITION)) - self._value_blinds.data = kwargs.get(ATTR_POSITION) - - def _configure_values(self): - """Get the value objects from the node.""" - for value in self.node.get_values( - class_id=COMMAND_CLASS_MANUFACTURER_PROPRIETARY - ).values(): - if value is None: - continue - if value.index == 0: - self._value_blinds = value - elif value.index == 1: - self._value_tilt = value - else: - _LOGGER.warning( - "Undefined index %d for this command class", value.index - ) - - if self._value_tilt is not None: - # We reached here because the user has configured the Fibaro to - # report using the MANUFACTURER_PROPRIETARY command class. The only - # reason for the user to configure this way is if tilt support is - # needed (aka venetian blind mode). Therefore, turn it on. - # - # Note: This is safe to do even if the user has accidentally set - # this configuration parameter, or configuration parameter 10 to - # something other than venetian blind mode. The controller will just - # ignore potential tilt settings sent from Home Assistant in this - # case. - self._has_tilt_mode = True - _LOGGER.info( - "Zwave node %s is a Fibaro FGR-222/FGRM-222 with tilt support.", - self.node_id, - ) - - def update_properties(self): - """React on properties being updated.""" - if not self._has_tilt_mode: - self._configure_values() - if self._value_blinds is not None: - self._current_position = self._value_blinds.data - else: - super().update_properties() From 3fd14ca3cf846a55bc171ca9b81f400790f81f63 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 15 Jan 2020 08:10:42 -0800 Subject: [PATCH 39/41] Refactor Ring data handling (#30777) * Refactor Ring data handling * Add async_ to methods --- homeassistant/components/ring/__init__.py | 241 +++++++----- .../components/ring/binary_sensor.py | 103 +++-- homeassistant/components/ring/camera.py | 94 ++--- homeassistant/components/ring/config_flow.py | 3 - homeassistant/components/ring/entity.py | 53 +++ homeassistant/components/ring/light.py | 64 +--- homeassistant/components/ring/sensor.py | 354 +++++++++--------- homeassistant/components/ring/switch.py | 73 +--- tests/components/ring/test_binary_sensor.py | 11 +- 9 files changed, 514 insertions(+), 482 deletions(-) create mode 100644 homeassistant/components/ring/entity.py diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index b35ff630310..7b4fbb15b30 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -4,16 +4,16 @@ from datetime import timedelta from functools import partial import logging from pathlib import Path -from time import time +from typing import Optional +from oauthlib.oauth2 import AccessDeniedError from ring_doorbell import Auth, Ring import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, __version__ -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.async_ import run_callback_threadsafe @@ -24,16 +24,8 @@ ATTRIBUTION = "Data provided by Ring.com" NOTIFICATION_ID = "ring_notification" NOTIFICATION_TITLE = "Ring Setup" -DATA_HISTORY = "ring_history" -DATA_HEALTH_DATA_TRACKER = "ring_health_data" -DATA_TRACK_INTERVAL = "ring_track_interval" - DOMAIN = "ring" DEFAULT_ENTITY_NAMESPACE = "ring" -SIGNAL_UPDATE_RING = "ring_update" -SIGNAL_UPDATE_HEALTH_RING = "ring_health_update" - -SCAN_INTERVAL = timedelta(seconds=10) PLATFORMS = ("binary_sensor", "light", "sensor", "switch", "camera") @@ -93,9 +85,36 @@ async def async_setup_entry(hass, entry): auth = Auth(f"HomeAssistant/{__version__}", entry.data["token"], token_updater) ring = Ring(auth) - await hass.async_add_executor_job(ring.update_data) + try: + await hass.async_add_executor_job(ring.update_data) + except AccessDeniedError: + _LOGGER.error("Access token is no longer valid. Please set up Ring again") + return False - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ring + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + "api": ring, + "devices": ring.devices(), + "device_data": GlobalDataUpdater( + hass, entry.entry_id, ring, "update_devices", timedelta(minutes=1) + ), + "dings_data": GlobalDataUpdater( + hass, entry.entry_id, ring, "update_dings", timedelta(seconds=10) + ), + "history_data": DeviceDataUpdater( + hass, + entry.entry_id, + ring, + lambda device: device.history(limit=10), + timedelta(minutes=1), + ), + "health_data": DeviceDataUpdater( + hass, + entry.entry_id, + ring, + lambda device: device.update_health_data(), + timedelta(minutes=1), + ), + } for component in PLATFORMS: hass.async_create_task( @@ -105,25 +124,16 @@ async def async_setup_entry(hass, entry): if hass.services.has_service(DOMAIN, "update"): return True - async def refresh_all(_): - """Refresh all ring accounts.""" - await asyncio.gather( - *[ - hass.async_add_executor_job(api.update_data) - for api in hass.data[DOMAIN].values() - ] - ) - async_dispatcher_send(hass, SIGNAL_UPDATE_RING) + async def async_refresh_all(_): + """Refresh all ring data.""" + for info in hass.data[DOMAIN].values(): + await info["device_data"].async_refresh_all() + await info["dings_data"].async_refresh_all() + await hass.async_add_executor_job(info["history_data"].refresh_all) + await hass.async_add_executor_job(info["health_data"].refresh_all) # register service - hass.services.async_register(DOMAIN, "update", refresh_all) - - # register scan interval for ring - hass.data[DATA_TRACK_INTERVAL] = async_track_time_interval( - hass, refresh_all, SCAN_INTERVAL - ) - hass.data[DATA_HEALTH_DATA_TRACKER] = HealthDataUpdater(hass) - hass.data[DATA_HISTORY] = HistoryCache(hass) + hass.services.async_register(DOMAIN, "update", async_refresh_all) return True @@ -146,98 +156,141 @@ async def async_unload_entry(hass, entry): if len(hass.data[DOMAIN]) != 0: return True - # Last entry unloaded, clean up - hass.data.pop(DATA_TRACK_INTERVAL)() - hass.data.pop(DATA_HEALTH_DATA_TRACKER) - hass.data.pop(DATA_HISTORY) + # Last entry unloaded, clean up service hass.services.async_remove(DOMAIN, "update") return True -class HealthDataUpdater: - """Data storage for health data.""" +class GlobalDataUpdater: + """Data storage for single API endpoint.""" - def __init__(self, hass): - """Track devices that need health data updated.""" + def __init__( + self, + hass: HomeAssistant, + config_entry_id: str, + ring: Ring, + update_method: str, + update_interval: timedelta, + ): + """Initialize global data updater.""" self.hass = hass + self.config_entry_id = config_entry_id + self.ring = ring + self.update_method = update_method + self.update_interval = update_interval + self.listeners = [] + self._unsub_interval = None + + @callback + def async_add_listener(self, update_callback): + """Listen for data updates.""" + # This is the first listener, set up interval. + if not self.listeners: + self._unsub_interval = async_track_time_interval( + self.hass, self.async_refresh_all, self.update_interval + ) + + self.listeners.append(update_callback) + + @callback + def async_remove_listener(self, update_callback): + """Remove data update.""" + self.listeners.remove(update_callback) + + if not self.listeners: + self._unsub_interval() + self._unsub_interval = None + + async def async_refresh_all(self, _now: Optional[int] = None) -> None: + """Time to update.""" + if not self.listeners: + return + + try: + await self.hass.async_add_executor_job( + getattr(self.ring, self.update_method) + ) + except AccessDeniedError: + _LOGGER.error("Ring access token is no longer valid. Set up Ring again") + await self.hass.config_entries.async_unload(self.config_entry_id) + return + + for update_callback in self.listeners: + update_callback() + + +class DeviceDataUpdater: + """Data storage for device data.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry_id: str, + ring: Ring, + update_method: str, + update_interval: timedelta, + ): + """Initialize device data updater.""" + self.hass = hass + self.config_entry_id = config_entry_id + self.ring = ring + self.update_method = update_method + self.update_interval = update_interval self.devices = {} self._unsub_interval = None - async def track_device(self, config_entry_id, device): + async def async_track_device(self, device, update_callback): """Track a device.""" if not self.devices: self._unsub_interval = async_track_time_interval( - self.hass, self.refresh_all, SCAN_INTERVAL + self.hass, self.refresh_all, self.update_interval ) - key = (config_entry_id, device.device_id) - - if key not in self.devices: - self.devices[key] = { + if device.device_id not in self.devices: + self.devices[device.device_id] = { "device": device, - "count": 1, + "update_callbacks": [update_callback], + "data": None, } + # Store task so that other concurrent requests can wait for us to finish and + # data be available. + self.devices[device.device_id]["task"] = asyncio.current_task() + self.devices[device.device_id][ + "data" + ] = await self.hass.async_add_executor_job(self.update_method, device) + self.devices[device.device_id].pop("task") else: - self.devices[key]["count"] += 1 + self.devices[device.device_id]["update_callbacks"].append(update_callback) + # If someone is currently fetching data as part of the initialization, wait for them + if "task" in self.devices[device.device_id]: + await self.devices[device.device_id]["task"] - await self.hass.async_add_executor_job(device.update_health_data) + update_callback(self.devices[device.device_id]["data"]) @callback - def untrack_device(self, config_entry_id, device): + def async_untrack_device(self, device, update_callback): """Untrack a device.""" - key = (config_entry_id, device.device_id) - self.devices[key]["count"] -= 1 + self.devices[device.device_id]["update_callbacks"].remove(update_callback) - if self.devices[key]["count"] == 0: - self.devices.pop(key) + if not self.devices[device.device_id]["update_callbacks"]: + self.devices.pop(device.device_id) if not self.devices: self._unsub_interval() self._unsub_interval = None - def refresh_all(self, _): + def refresh_all(self, _=None): """Refresh all registered devices.""" for info in self.devices.values(): - info["device"].update_health_data() + try: + data = info["data"] = self.update_method(info["device"]) + except AccessDeniedError: + _LOGGER.error("Ring access token is no longer valid. Set up Ring again") + self.hass.add_job( + self.hass.config_entries.async_unload(self.config_entry_id) + ) + return - dispatcher_send(self.hass, SIGNAL_UPDATE_HEALTH_RING) - - -class HistoryCache: - """Helper to fetch history.""" - - STALE_AFTER = 10 # seconds - - def __init__(self, hass): - """Initialize history cache.""" - self.hass = hass - self.cache = {} - - async def async_get_history(self, config_entry_id, device): - """Get history of a device.""" - key = (config_entry_id, device.device_id) - - if key in self.cache: - info = self.cache[key] - - # We're already fetching data, join that task - if "task" in info: - return await info["task"] - - # We have valid cache info, return that - if time() - info["created_at"] < self.STALE_AFTER: - return info["data"] - - self.cache.pop(key) - - # Fetch data - task = self.hass.async_add_executor_job(partial(device.history, limit=10)) - - self.cache[key] = {"task": task} - - data = await task - - self.cache[key] = {"created_at": time(), "data": data} - - return data + for update_callback in info["update_callbacks"]: + self.hass.loop.call_soon_threadsafe(update_callback, data) diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 2dd3682951f..7b20ff948d1 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -1,13 +1,12 @@ """This component provides HA sensor support for Ring Door Bell/Chimes.""" -from datetime import timedelta +from datetime import datetime, timedelta import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import ATTRIBUTION, DOMAIN, SIGNAL_UPDATE_RING +from . import DOMAIN +from .entity import RingEntityMixin _LOGGER = logging.getLogger(__name__) @@ -22,8 +21,8 @@ SENSOR_TYPES = { async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Ring binary sensors from a config entry.""" - ring = hass.data[DOMAIN][config_entry.entry_id] - devices = ring.devices() + ring = hass.data[DOMAIN][config_entry.entry_id]["api"] + devices = hass.data[DOMAIN][config_entry.entry_id]["devices"] sensors = [] @@ -33,49 +32,62 @@ async def async_setup_entry(hass, config_entry, async_add_entities): continue for device in devices[device_type]: - sensors.append(RingBinarySensor(ring, device, sensor_type)) + sensors.append( + RingBinarySensor(config_entry.entry_id, ring, device, sensor_type) + ) - async_add_entities(sensors, True) + async_add_entities(sensors) -class RingBinarySensor(BinarySensorDevice): +class RingBinarySensor(RingEntityMixin, BinarySensorDevice): """A binary sensor implementation for Ring device.""" - def __init__(self, ring, device, sensor_type): + _active_alert = None + + def __init__(self, config_entry_id, ring, device, sensor_type): """Initialize a sensor for Ring device.""" - self._sensor_type = sensor_type + super().__init__(config_entry_id, device) self._ring = ring - self._device = device + self._sensor_type = sensor_type self._name = "{0} {1}".format( - self._device.name, SENSOR_TYPES.get(self._sensor_type)[0] + self._device.name, SENSOR_TYPES.get(sensor_type)[0] ) - self._device_class = SENSOR_TYPES.get(self._sensor_type)[2] + self._device_class = SENSOR_TYPES.get(sensor_type)[2] self._state = None - self._unique_id = f"{self._device.id}-{self._sensor_type}" - self._disp_disconnect = None + self._unique_id = f"{device.id}-{sensor_type}" + self._update_alert() async def async_added_to_hass(self): """Register callbacks.""" - self._disp_disconnect = async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_RING, self._update_callback - ) + await super().async_added_to_hass() + self.ring_objects["dings_data"].async_add_listener(self._dings_update_callback) + self._dings_update_callback() async def async_will_remove_from_hass(self): """Disconnect callbacks.""" - if self._disp_disconnect: - self._disp_disconnect() - self._disp_disconnect = None + await super().async_will_remove_from_hass() + self.ring_objects["dings_data"].async_remove_listener( + self._dings_update_callback + ) @callback - def _update_callback(self): + def _dings_update_callback(self): """Call update method.""" - self.async_schedule_update_ha_state(True) - _LOGGER.debug("Updating Ring binary sensor %s (callback)", self.name) + self._update_alert() + self.async_write_ha_state() - @property - def should_poll(self): - """Return False, updates are controlled via the hub.""" - return False + @callback + def _update_alert(self): + """Update active alert.""" + self._active_alert = next( + ( + alert + for alert in self._ring.active_alerts() + if alert["kind"] == self._sensor_type + and alert["doorbot_id"] == self._device.id + ), + None, + ) @property def name(self): @@ -85,7 +97,7 @@ class RingBinarySensor(BinarySensorDevice): @property def is_on(self): """Return True if the binary sensor is on.""" - return self._state + return self._active_alert is not None @property def device_class(self): @@ -97,32 +109,17 @@ class RingBinarySensor(BinarySensorDevice): """Return a unique ID.""" return self._unique_id - @property - def device_info(self): - """Return device info.""" - return { - "identifiers": {(DOMAIN, self._device.device_id)}, - "name": self._device.name, - "model": self._device.model, - "manufacturer": "Ring", - } - @property def device_state_attributes(self): """Return the state attributes.""" - attrs = {} - attrs[ATTR_ATTRIBUTION] = ATTRIBUTION + attrs = super().device_state_attributes - if self._device.alert and self._device.alert_expires_at: - attrs["expires_at"] = self._device.alert_expires_at - attrs["state"] = self._device.alert.get("state") + if self._active_alert is None: + return attrs + + attrs["state"] = self._active_alert["state"] + attrs["expires_at"] = datetime.fromtimestamp( + self._active_alert.get("now") + self._active_alert.get("expires_in") + ).isoformat() return attrs - - async def async_update(self): - """Get the latest data and updates the state.""" - self._state = any( - alert["kind"] == self._sensor_type - and alert["doorbot_id"] == self._device.id - for alert in self._ring.active_alerts() - ) diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 8ef876e4a00..07d87c85714 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -12,10 +12,10 @@ from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import dt as dt_util -from . import ATTRIBUTION, DATA_HISTORY, DOMAIN, SIGNAL_UPDATE_RING +from . import ATTRIBUTION, DOMAIN +from .entity import RingEntityMixin FORCE_REFRESH_INTERVAL = timedelta(minutes=45) @@ -24,8 +24,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a Ring Door Bell and StickUp Camera.""" - ring = hass.data[DOMAIN][config_entry.entry_id] - devices = ring.devices() + devices = hass.data[DOMAIN][config_entry.entry_id]["devices"] cams = [] for camera in chain( @@ -36,42 +35,52 @@ async def async_setup_entry(hass, config_entry, async_add_entities): cams.append(RingCam(config_entry.entry_id, hass.data[DATA_FFMPEG], camera)) - async_add_entities(cams, True) + async_add_entities(cams) -class RingCam(Camera): +class RingCam(RingEntityMixin, Camera): """An implementation of a Ring Door Bell camera.""" def __init__(self, config_entry_id, ffmpeg, device): """Initialize a Ring Door Bell camera.""" - super().__init__() - self._config_entry_id = config_entry_id - self._device = device + super().__init__(config_entry_id, device) + self._name = self._device.name self._ffmpeg = ffmpeg + self._last_event = None self._last_video_id = None self._video_url = None self._utcnow = dt_util.utcnow() self._expires_at = self._utcnow - FORCE_REFRESH_INTERVAL - self._disp_disconnect = None async def async_added_to_hass(self): """Register callbacks.""" - self._disp_disconnect = async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_RING, self._update_callback + await super().async_added_to_hass() + + await self.ring_objects["history_data"].async_track_device( + self._device, self._history_update_callback ) async def async_will_remove_from_hass(self): """Disconnect callbacks.""" - if self._disp_disconnect: - self._disp_disconnect() - self._disp_disconnect = None + await super().async_will_remove_from_hass() + + self.ring_objects["history_data"].async_untrack_device( + self._device, self._history_update_callback + ) @callback - def _update_callback(self): + def _history_update_callback(self, history_data): """Call update method.""" - self.async_schedule_update_ha_state(True) - _LOGGER.debug("Updating Ring camera %s (callback)", self.name) + if history_data: + self._last_event = history_data[0] + self.async_schedule_update_ha_state(True) + else: + self._last_event = None + self._last_video_id = None + self._video_url = None + self._expires_at = self._utcnow + self.async_write_ha_state() @property def name(self): @@ -83,16 +92,6 @@ class RingCam(Camera): """Return a unique ID.""" return self._device.id - @property - def device_info(self): - """Return device info.""" - return { - "identifiers": {(DOMAIN, self._device.device_id)}, - "name": self._device.name, - "model": self._device.model, - "manufacturer": "Ring", - } - @property def device_state_attributes(self): """Return the state attributes.""" @@ -104,7 +103,6 @@ class RingCam(Camera): async def async_camera_image(self): """Return a still image response from the camera.""" - ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop) if self._video_url is None: @@ -136,33 +134,23 @@ class RingCam(Camera): async def async_update(self): """Update camera entity and refresh attributes.""" - _LOGGER.debug("Checking if Ring DoorBell needs to refresh video_url") - - self._utcnow = dt_util.utcnow() - - data = await self.hass.data[DATA_HISTORY].async_get_history( - self._config_entry_id, self._device - ) - - if not data: + if self._last_event is None: return - last_event = data[0] - last_recording_id = last_event["id"] - video_status = last_event["recording"]["status"] + if self._last_event["recording"]["status"] != "ready": + return - if video_status == "ready" and ( - self._last_video_id != last_recording_id or self._utcnow >= self._expires_at + if ( + self._last_video_id == self._last_event["id"] + and self._utcnow <= self._expires_at ): + return - video_url = await self.hass.async_add_executor_job( - self._device.recording_url, last_recording_id - ) + video_url = await self.hass.async_add_executor_job( + self._device.recording_url, self._last_event["id"] + ) - if video_url: - _LOGGER.debug("Ring DoorBell properties refreshed") - - # update attributes if new video or if URL has expired - self._last_video_id = last_recording_id - self._video_url = video_url - self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow + if video_url: + self._last_video_id = self._last_event["id"] + self._video_url = video_url + self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py index 57f873bd1a6..a25e0283753 100644 --- a/homeassistant/components/ring/config_flow.py +++ b/homeassistant/components/ring/config_flow.py @@ -39,9 +39,6 @@ class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle the initial step.""" - if self._async_current_entries(): - return self.async_abort(reason="already_configured") - errors = {} if user_input is not None: try: diff --git a/homeassistant/components/ring/entity.py b/homeassistant/components/ring/entity.py new file mode 100644 index 00000000000..6eb87cb8f9b --- /dev/null +++ b/homeassistant/components/ring/entity.py @@ -0,0 +1,53 @@ +"""Base class for Ring entity.""" +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.core import callback + +from . import ATTRIBUTION, DOMAIN + + +class RingEntityMixin: + """Base implementation for Ring device.""" + + def __init__(self, config_entry_id, device): + """Initialize a sensor for Ring device.""" + super().__init__() + self._config_entry_id = config_entry_id + self._device = device + + async def async_added_to_hass(self): + """Register callbacks.""" + self.ring_objects["device_data"].async_add_listener(self._update_callback) + + async def async_will_remove_from_hass(self): + """Disconnect callbacks.""" + self.ring_objects["device_data"].async_remove_listener(self._update_callback) + + @callback + def _update_callback(self): + """Call update method.""" + self.async_write_ha_state() + + @property + def ring_objects(self): + """Return the Ring API objects.""" + return self.hass.data[DOMAIN][self._config_entry_id] + + @property + def should_poll(self): + """Return False, updates are controlled via the hub.""" + return False + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} + + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._device.device_id)}, + "name": self._device.name, + "model": self._device.model, + "manufacturer": "Ring", + } diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index 10572e2e0ae..86ef55af16d 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -4,10 +4,10 @@ import logging from homeassistant.components.light import Light from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util -from . import DOMAIN, SIGNAL_UPDATE_RING +from . import DOMAIN +from .entity import RingEntityMixin _LOGGER = logging.getLogger(__name__) @@ -25,51 +25,35 @@ OFF_STATE = "off" async def async_setup_entry(hass, config_entry, async_add_entities): """Create the lights for the Ring devices.""" - ring = hass.data[DOMAIN][config_entry.entry_id] + devices = hass.data[DOMAIN][config_entry.entry_id]["devices"] - devices = ring.devices() lights = [] for device in devices["stickup_cams"]: if device.has_capability("light"): - lights.append(RingLight(device)) + lights.append(RingLight(config_entry.entry_id, device)) - async_add_entities(lights, True) + async_add_entities(lights) -class RingLight(Light): +class RingLight(RingEntityMixin, Light): """Creates a switch to turn the ring cameras light on and off.""" - def __init__(self, device): + def __init__(self, config_entry_id, device): """Initialize the light.""" - self._device = device - self._unique_id = self._device.id - self._light_on = False + super().__init__(config_entry_id, device) + self._unique_id = device.id + self._light_on = device.lights == ON_STATE self._no_updates_until = dt_util.utcnow() - self._disp_disconnect = None - - async def async_added_to_hass(self): - """Register callbacks.""" - self._disp_disconnect = async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_RING, self._update_callback - ) - - async def async_will_remove_from_hass(self): - """Disconnect callbacks.""" - if self._disp_disconnect: - self._disp_disconnect() - self._disp_disconnect = None @callback def _update_callback(self): """Call update method.""" - _LOGGER.debug("Updating Ring light %s (callback)", self.name) - self.async_schedule_update_ha_state(True) + if self._no_updates_until > dt_util.utcnow(): + return - @property - def should_poll(self): - """Update controlled via the hub.""" - return False + self._light_on = self._device.lights == ON_STATE + self.async_write_ha_state() @property def name(self): @@ -86,22 +70,12 @@ class RingLight(Light): """If the switch is currently on or off.""" return self._light_on - @property - def device_info(self): - """Return device info.""" - return { - "identifiers": {(DOMAIN, self._device.device_id)}, - "name": self._device.name, - "model": self._device.model, - "manufacturer": "Ring", - } - def _set_light(self, new_state): """Update light state, and causes Home Assistant to correctly update.""" self._device.lights = new_state self._light_on = new_state == ON_STATE self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY - self.async_schedule_update_ha_state(True) + self.async_schedule_update_ha_state() def turn_on(self, **kwargs): """Turn the light on for 30 seconds.""" @@ -110,11 +84,3 @@ class RingLight(Light): def turn_off(self, **kwargs): """Turn the light off.""" self._set_light(OFF_STATE) - - async def async_update(self): - """Update current state of the light.""" - if self._no_updates_until > dt_util.utcnow(): - _LOGGER.debug("Skipping update...") - return - - self._light_on = self._device.lights == ON_STATE diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index fe909636e83..2b921dddd2f 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -1,88 +1,20 @@ """This component provides HA sensor support for Ring Door Bell/Chimes.""" import logging -from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level -from . import ( - ATTRIBUTION, - DATA_HEALTH_DATA_TRACKER, - DATA_HISTORY, - DOMAIN, - SIGNAL_UPDATE_HEALTH_RING, - SIGNAL_UPDATE_RING, -) +from . import DOMAIN +from .entity import RingEntityMixin _LOGGER = logging.getLogger(__name__) -# Sensor types: Name, category, units, icon, kind, device_class -SENSOR_TYPES = { - "battery": [ - "Battery", - ["doorbots", "authorized_doorbots", "stickup_cams"], - "%", - None, - None, - "battery", - ], - "last_activity": [ - "Last Activity", - ["doorbots", "authorized_doorbots", "stickup_cams"], - None, - "history", - None, - "timestamp", - ], - "last_ding": [ - "Last Ding", - ["doorbots", "authorized_doorbots"], - None, - "history", - "ding", - "timestamp", - ], - "last_motion": [ - "Last Motion", - ["doorbots", "authorized_doorbots", "stickup_cams"], - None, - "history", - "motion", - "timestamp", - ], - "volume": [ - "Volume", - ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], - None, - "bell-ring", - None, - None, - ], - "wifi_signal_category": [ - "WiFi Signal Category", - ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], - None, - "wifi", - None, - None, - ], - "wifi_signal_strength": [ - "WiFi Signal Strength", - ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], - "dBm", - "wifi", - None, - "signal_strength", - ], -} - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a sensor for a Ring device.""" - ring = hass.data[DOMAIN][config_entry.entry_id] - devices = ring.devices() + devices = hass.data[DOMAIN][config_entry.entry_id]["devices"] + # Makes a ton of requests. We will make this a config entry option in the future wifi_enabled = False @@ -100,72 +32,29 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if device_type == "battery" and device.battery_life is None: continue - sensors.append(RingSensor(config_entry.entry_id, device, sensor_type)) + sensors.append( + SENSOR_TYPES[sensor_type][6]( + config_entry.entry_id, device, sensor_type + ) + ) - async_add_entities(sensors, True) + async_add_entities(sensors) -class RingSensor(Entity): +class RingSensor(RingEntityMixin, Entity): """A sensor implementation for Ring device.""" def __init__(self, config_entry_id, device, sensor_type): """Initialize a sensor for Ring device.""" - self._config_entry_id = config_entry_id + super().__init__(config_entry_id, device) self._sensor_type = sensor_type - self._device = device self._extra = None - self._icon = "mdi:{}".format(SENSOR_TYPES.get(self._sensor_type)[3]) - self._kind = SENSOR_TYPES.get(self._sensor_type)[4] + self._icon = "mdi:{}".format(SENSOR_TYPES.get(sensor_type)[3]) + self._kind = SENSOR_TYPES.get(sensor_type)[4] self._name = "{0} {1}".format( - self._device.name, SENSOR_TYPES.get(self._sensor_type)[0] + self._device.name, SENSOR_TYPES.get(sensor_type)[0] ) - self._state = None - self._unique_id = f"{self._device.id}-{self._sensor_type}" - self._disp_disconnect = None - self._disp_disconnect_health = None - - async def async_added_to_hass(self): - """Register callbacks.""" - self._disp_disconnect = async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_RING, self._update_callback - ) - if self._sensor_type not in ("wifi_signal_category", "wifi_signal_strength"): - return - - self._disp_disconnect_health = async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_HEALTH_RING, self._update_callback - ) - await self.hass.data[DATA_HEALTH_DATA_TRACKER].track_device( - self._config_entry_id, self._device - ) - # Write the state, it was not available when doing initial update. - if self._sensor_type == "wifi_signal_category": - self._state = self._device.wifi_signal_category - - if self._sensor_type == "wifi_signal_strength": - self._state = self._device.wifi_signal_strength - - async def async_will_remove_from_hass(self): - """Disconnect callbacks.""" - if self._disp_disconnect: - self._disp_disconnect() - self._disp_disconnect = None - - if self._disp_disconnect_health: - self._disp_disconnect_health() - self._disp_disconnect_health = None - - if self._sensor_type not in ("wifi_signal_category", "wifi_signal_strength"): - return - - self.hass.data[DATA_HEALTH_DATA_TRACKER].untrack_device( - self._config_entry_id, self._device - ) - - @callback - def _update_callback(self): - """Call update method.""" - self.async_schedule_update_ha_state(True) + self._unique_id = f"{device.id}-{sensor_type}" @property def should_poll(self): @@ -180,7 +69,11 @@ class RingSensor(Entity): @property def state(self): """Return the state of the sensor.""" - return self._state + if self._sensor_type == "volume": + return self._device.volume + + if self._sensor_type == "battery": + return self._device.battery_life @property def unique_id(self): @@ -192,37 +85,12 @@ class RingSensor(Entity): """Return sensor device class.""" return SENSOR_TYPES[self._sensor_type][5] - @property - def device_info(self): - """Return device info.""" - return { - "identifiers": {(DOMAIN, self._device.device_id)}, - "name": self._device.name, - "model": self._device.model, - "manufacturer": "Ring", - } - - @property - def device_state_attributes(self): - """Return the state attributes.""" - attrs = {} - - attrs[ATTR_ATTRIBUTION] = ATTRIBUTION - - if self._extra and self._sensor_type.startswith("last_"): - attrs["created_at"] = self._extra["created_at"] - attrs["answered"] = self._extra["answered"] - attrs["recording_status"] = self._extra["recording"]["status"] - attrs["category"] = self._extra["kind"] - - return attrs - @property def icon(self): """Icon to use in the frontend, if any.""" - if self._sensor_type == "battery" and self._state is not None: + if self._sensor_type == "battery" and self._device.battery_life is not None: return icon_for_battery_level( - battery_level=int(self._state), charging=False + battery_level=self._device.battery_life, charging=False ) return self._icon @@ -231,34 +99,168 @@ class RingSensor(Entity): """Return the units of measurement.""" return SENSOR_TYPES.get(self._sensor_type)[2] - async def async_update(self): - """Get the latest data and updates the state.""" - _LOGGER.debug("Updating data from %s sensor", self._name) - if self._sensor_type == "volume": - self._state = self._device.volume +class HealthDataRingSensor(RingSensor): + """Ring sensor that relies on health data.""" - if self._sensor_type == "battery": - self._state = self._device.battery_life + async def async_added_to_hass(self): + """Register callbacks.""" + await super().async_added_to_hass() - if self._sensor_type.startswith("last_"): - history = await self.hass.data[DATA_HISTORY].async_get_history( - self._config_entry_id, self._device - ) + await self.ring_objects["health_data"].async_track_device( + self._device, self._health_update_callback + ) - found = None - for entry in history: + async def async_will_remove_from_hass(self): + """Disconnect callbacks.""" + await super().async_will_remove_from_hass() + + self.ring_objects["health_data"].async_untrack_device( + self._device, self._health_update_callback + ) + + @callback + def _health_update_callback(self, _health_data): + """Call update method.""" + self.async_write_ha_state() + + @property + def state(self): + """Return the state of the sensor.""" + if self._sensor_type == "wifi_signal_category": + return self._device.wifi_signal_category + + if self._sensor_type == "wifi_signal_strength": + return self._device.wifi_signal_strength + + +class HistoryRingSensor(RingSensor): + """Ring sensor that relies on history data.""" + + _latest_event = None + + async def async_added_to_hass(self): + """Register callbacks.""" + await super().async_added_to_hass() + + await self.ring_objects["history_data"].async_track_device( + self._device, self._history_update_callback + ) + + async def async_will_remove_from_hass(self): + """Disconnect callbacks.""" + await super().async_will_remove_from_hass() + + self.ring_objects["history_data"].async_untrack_device( + self._device, self._history_update_callback + ) + + @callback + def _history_update_callback(self, history_data): + """Call update method.""" + if not history_data: + return + + found = None + if self._kind is None: + found = history_data[0] + else: + for entry in history_data: if entry["kind"] == self._kind: found = entry break - if found: - self._extra = found - created_at = found["created_at"] - self._state = created_at.isoformat() + if not found: + return - if self._sensor_type == "wifi_signal_category": - self._state = self._device.wifi_signal_category + self._latest_event = found + self.async_write_ha_state() - if self._sensor_type == "wifi_signal_strength": - self._state = self._device.wifi_signal_strength + @property + def state(self): + """Return the state of the sensor.""" + if self._latest_event is None: + return None + + return self._latest_event["created_at"].isoformat() + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attrs = super().device_state_attributes + + if self._latest_event: + attrs["created_at"] = self._latest_event["created_at"] + attrs["answered"] = self._latest_event["answered"] + attrs["recording_status"] = self._latest_event["recording"]["status"] + attrs["category"] = self._latest_event["kind"] + + return attrs + + +# Sensor types: Name, category, units, icon, kind, device_class, class +SENSOR_TYPES = { + "battery": [ + "Battery", + ["doorbots", "authorized_doorbots", "stickup_cams"], + "%", + None, + None, + "battery", + RingSensor, + ], + "last_activity": [ + "Last Activity", + ["doorbots", "authorized_doorbots", "stickup_cams"], + None, + "history", + None, + "timestamp", + HistoryRingSensor, + ], + "last_ding": [ + "Last Ding", + ["doorbots", "authorized_doorbots"], + None, + "history", + "ding", + "timestamp", + HistoryRingSensor, + ], + "last_motion": [ + "Last Motion", + ["doorbots", "authorized_doorbots", "stickup_cams"], + None, + "history", + "motion", + "timestamp", + HistoryRingSensor, + ], + "volume": [ + "Volume", + ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], + None, + "bell-ring", + None, + None, + RingSensor, + ], + "wifi_signal_category": [ + "WiFi Signal Category", + ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], + None, + "wifi", + None, + None, + HealthDataRingSensor, + ], + "wifi_signal_strength": [ + "WiFi Signal Strength", + ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], + "dBm", + "wifi", + None, + "signal_strength", + HealthDataRingSensor, + ], +} diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index 06f81732784..65eed83d98e 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -4,10 +4,10 @@ import logging from homeassistant.components.switch import SwitchDevice from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util -from . import DOMAIN, SIGNAL_UPDATE_RING +from . import DOMAIN +from .entity import RingEntityMixin _LOGGER = logging.getLogger(__name__) @@ -24,49 +24,24 @@ SKIP_UPDATES_DELAY = timedelta(seconds=5) async def async_setup_entry(hass, config_entry, async_add_entities): """Create the switches for the Ring devices.""" - ring = hass.data[DOMAIN][config_entry.entry_id] - devices = ring.devices() + devices = hass.data[DOMAIN][config_entry.entry_id]["devices"] switches = [] for device in devices["stickup_cams"]: if device.has_capability("siren"): - switches.append(SirenSwitch(device)) + switches.append(SirenSwitch(config_entry.entry_id, device)) - async_add_entities(switches, True) + async_add_entities(switches) -class BaseRingSwitch(SwitchDevice): +class BaseRingSwitch(RingEntityMixin, SwitchDevice): """Represents a switch for controlling an aspect of a ring device.""" - def __init__(self, device, device_type): + def __init__(self, config_entry_id, device, device_type): """Initialize the switch.""" - self._device = device + super().__init__(config_entry_id, device) self._device_type = device_type self._unique_id = f"{self._device.id}-{self._device_type}" - self._disp_disconnect = None - - async def async_added_to_hass(self): - """Register callbacks.""" - self._disp_disconnect = async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_RING, self._update_callback - ) - - async def async_will_remove_from_hass(self): - """Disconnect callbacks.""" - if self._disp_disconnect: - self._disp_disconnect() - self._disp_disconnect = None - - @callback - def _update_callback(self): - """Call update method.""" - _LOGGER.debug("Updating Ring switch %s (callback)", self.name) - self.async_schedule_update_ha_state(True) - - @property - def should_poll(self): - """Update controlled via the hub.""" - return False @property def name(self): @@ -78,25 +53,24 @@ class BaseRingSwitch(SwitchDevice): """Return a unique ID.""" return self._unique_id - @property - def device_info(self): - """Return device info.""" - return { - "identifiers": {(DOMAIN, self._device.device_id)}, - "name": self._device.name, - "model": self._device.model, - "manufacturer": "Ring", - } - class SirenSwitch(BaseRingSwitch): """Creates a switch to turn the ring cameras siren on and off.""" - def __init__(self, device): + def __init__(self, config_entry_id, device): """Initialize the switch for a device with a siren.""" - super().__init__(device, "siren") + super().__init__(config_entry_id, device, "siren") self._no_updates_until = dt_util.utcnow() - self._siren_on = False + self._siren_on = device.siren > 0 + + @callback + def _update_callback(self): + """Call update method.""" + if self._no_updates_until > dt_util.utcnow(): + return + + self._siren_on = self._device.siren > 0 + self.async_write_ha_state() def _set_switch(self, new_state): """Update switch state, and causes Home Assistant to correctly update.""" @@ -122,10 +96,3 @@ class SirenSwitch(BaseRingSwitch): def icon(self): """Return the icon.""" return SIREN_ICON - - async def async_update(self): - """Update state of the siren.""" - if self._no_updates_until > dt_util.utcnow(): - _LOGGER.debug("Skipping update...") - return - self._siren_on = self._device.siren > 0 diff --git a/tests/components/ring/test_binary_sensor.py b/tests/components/ring/test_binary_sensor.py index 8615138d56e..0b73c739503 100644 --- a/tests/components/ring/test_binary_sensor.py +++ b/tests/components/ring/test_binary_sensor.py @@ -1,4 +1,5 @@ """The tests for the Ring binary sensor platform.""" +from time import time from unittest.mock import patch from .common import setup_platform @@ -8,7 +9,15 @@ async def test_binary_sensor(hass, requests_mock): """Test the Ring binary sensors.""" with patch( "ring_doorbell.Ring.active_alerts", - return_value=[{"kind": "motion", "doorbot_id": 987654}], + return_value=[ + { + "kind": "motion", + "doorbot_id": 987654, + "state": "ringing", + "now": time(), + "expires_in": 180, + } + ], ): await setup_platform(hass, "binary_sensor") From 1a6535ff8bd8f2dc7459571b57ace89f35dfb3a9 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 15 Jan 2020 16:09:05 +0000 Subject: [PATCH 40/41] Restore unit_of_measurement from entity registry (#30780) * Restore unit_of_measurement from entity registry * Lint fix --- homeassistant/helpers/entity_platform.py | 1 + homeassistant/helpers/entity_registry.py | 12 +++++++ tests/common.py | 5 +++ tests/components/homekit/test_type_sensors.py | 35 +++++++++++++++++++ tests/helpers/test_entity_platform.py | 2 ++ 5 files changed, 55 insertions(+) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 53ad54c5ed1..0e4d80ac080 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -355,6 +355,7 @@ class EntityPlatform: capabilities=entity.capability_attributes, supported_features=entity.supported_features, device_class=entity.device_class, + unit_of_measurement=entity.unit_of_measurement, ) entity.registry_entry = entry diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index bac406174f4..66d1bb94f60 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -18,6 +18,7 @@ import attr from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, + ATTR_UNIT_OF_MEASUREMENT, EVENT_HOMEASSISTANT_START, STATE_UNAVAILABLE, ) @@ -77,6 +78,7 @@ class RegistryEntry: capabilities: Optional[Dict[str, Any]] = attr.ib(default=None) supported_features: int = attr.ib(default=0) device_class: Optional[str] = attr.ib(default=None) + unit_of_measurement: Optional[str] = attr.ib(default=None) domain = attr.ib(type=str, init=False, repr=False) @domain.default @@ -164,6 +166,7 @@ class EntityRegistry: capabilities: Optional[Dict[str, Any]] = None, supported_features: Optional[int] = None, device_class: Optional[str] = None, + unit_of_measurement: Optional[str] = None, ) -> RegistryEntry: """Get entity. Create if it doesn't exist.""" config_entry_id = None @@ -180,6 +183,7 @@ class EntityRegistry: capabilities=capabilities or _UNDEF, supported_features=supported_features or _UNDEF, device_class=device_class or _UNDEF, + unit_of_measurement=unit_of_measurement or _UNDEF, # When we changed our slugify algorithm, we invalidated some # stored entity IDs with either a __ or ending in _. # Fix introduced in 0.86 (Jan 23, 2019). Next line can be @@ -210,6 +214,7 @@ class EntityRegistry: capabilities=capabilities, supported_features=supported_features or 0, device_class=device_class, + unit_of_measurement=unit_of_measurement, ) self.entities[entity_id] = entity _LOGGER.info("Registered new %s.%s entity: %s", domain, platform, entity_id) @@ -279,6 +284,7 @@ class EntityRegistry: capabilities=_UNDEF, supported_features=_UNDEF, device_class=_UNDEF, + unit_of_measurement=_UNDEF, ): """Private facing update properties method.""" old = self.entities[entity_id] @@ -293,6 +299,7 @@ class EntityRegistry: ("capabilities", capabilities), ("supported_features", supported_features), ("device_class", device_class), + ("unit_of_measurement", unit_of_measurement), ): if value is not _UNDEF and value != getattr(old, attr_name): changes[attr_name] = value @@ -369,6 +376,7 @@ class EntityRegistry: capabilities=entity.get("capabilities") or {}, supported_features=entity.get("supported_features", 0), device_class=entity.get("device_class"), + unit_of_measurement=entity.get("unit_of_measurement"), ) self.entities = entities @@ -395,6 +403,7 @@ class EntityRegistry: "capabilities": entry.capabilities, "supported_features": entry.supported_features, "device_class": entry.device_class, + "unit_of_measurement": entry.unit_of_measurement, } for entry in self.entities.values() ] @@ -499,6 +508,9 @@ def async_setup_entity_restore( if entry.device_class is not None: attrs[ATTR_DEVICE_CLASS] = entry.device_class + if entry.unit_of_measurement is not None: + attrs[ATTR_UNIT_OF_MEASUREMENT] = entry.unit_of_measurement + states.async_set(entry.entity_id, STATE_UNAVAILABLE, attrs) hass.bus.async_listen(EVENT_HOMEASSISTANT_START, _write_unavailable_states) diff --git a/tests/common.py b/tests/common.py index fd40b08635f..5a00a2bc7df 100644 --- a/tests/common.py +++ b/tests/common.py @@ -922,6 +922,11 @@ class MockEntity(entity.Entity): """Info how device should be classified.""" return self._handle("device_class") + @property + def unit_of_measurement(self): + """Info on the units the entity state is in.""" + return self._handle("unit_of_measurement") + @property def capability_attributes(self): """Info about capabilities.""" diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 43533840cc6..969ea0bddc8 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -1,4 +1,5 @@ """Test different accessory types: Sensors.""" +from homeassistant.components.homekit import get_accessory from homeassistant.components.homekit.const import ( PROP_CELSIUS, THRESHOLD_CO, @@ -17,6 +18,7 @@ from homeassistant.components.homekit.type_sensors import ( from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, + EVENT_HOMEASSISTANT_START, STATE_HOME, STATE_NOT_HOME, STATE_OFF, @@ -25,6 +27,8 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.core import CoreState +from homeassistant.helpers import entity_registry async def test_temperature(hass, hk_driver): @@ -262,3 +266,34 @@ async def test_binary_device_classes(hass, hk_driver): acc = BinarySensor(hass, hk_driver, "Binary Sensor", entity_id, 2, None) assert acc.get_service(service).display_name == service assert acc.char_detected.display_name == char + + +async def test_sensor_restore(hass, hk_driver, events): + """Test setting up an entity from state in the event registry.""" + hass.state = CoreState.not_running + + registry = await entity_registry.async_get_registry(hass) + + registry.async_get_or_create( + "sensor", + "generic", + "1234", + suggested_object_id="temperature", + device_class="temperature", + ) + registry.async_get_or_create( + "sensor", + "generic", + "12345", + suggested_object_id="humidity", + device_class="humidity", + unit_of_measurement="%", + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) + await hass.async_block_till_done() + + acc = get_accessory(hass, hk_driver, hass.states.get("sensor.temperature"), 2, {}) + assert acc.category == 10 + + acc = get_accessory(hass, hk_driver, hass.states.get("sensor.humidity"), 2, {}) + assert acc.category == 10 diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 7797bf5057b..8eea8ad004f 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -804,6 +804,7 @@ async def test_entity_info_added_to_entity_registry(hass): capability_attributes={"max": 100}, supported_features=5, device_class="mock-device-class", + unit_of_measurement="%", ) await component.async_add_entities([entity_default]) @@ -815,6 +816,7 @@ async def test_entity_info_added_to_entity_registry(hass): assert entry_default.capabilities == {"max": 100} assert entry_default.supported_features == 5 assert entry_default.device_class == "mock-device-class" + assert entry_default.unit_of_measurement == "%" async def test_override_restored_entities(hass): From 3d9b6332c8d276431a3b70efb5b21c8b0bfe822c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 15 Jan 2020 20:22:09 +0100 Subject: [PATCH 41/41] Bumped version to 0.104.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 183fdfbcc1b..9095cdc5ced 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 104 -PATCH_VERSION = "0b5" +PATCH_VERSION = "0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0)