From 15b1a9ecea3e08bfc2d242930ef69d6963f32a4f Mon Sep 17 00:00:00 2001 From: clssn Date: Thu, 30 Apr 2020 14:23:30 +0200 Subject: [PATCH 01/58] Add numato integration (#33816) * Add support for Numato 32 port USB GPIO boards Included are a binary_sensor, sensor and switch component implementations. The binary_sensor interface pushes updates via registered callback functions, so no need to poll here. Unit tests are included to test against a Numato device mockup. * Refactor numato configuration due to PR finding * Resolve minor review findings * Bump numato-gpio requirement * Load numato platforms during domain setup According to review finding * Guard from platform setup without discovery_info According to review finding * Move numato API state into hass.data According to review finding. * Avoid side effects in numato entity constructors According to review finding * Keep only first line of numato module docstrings Removed reference to the documentation. Requested by reviewer. * Minor improvements inspired by review findings * Fix async tests Pytest fixture was returning from the yield too early executing teardown code during test execution. * Improve test coverage * Configure GPIO ports early Review finding * Move read_gpio callback to outside the loop Also continue on failed switch setup, resolve other minor review findings and correct some error messages * Bump numato-gpio requirement This fixes a crash during cleanup. When any device had a communication problem, its cleanup would raise an exception which was not handled, fell through to the caller and prevented the remaining devices from being cleaned up. * Call services directly Define local helper functions for better readability. Resolves a review finding. * Assert something in every test So not only coverage is satisfied but things are actually tested to be in the expected state. Resolves a review finding. * Clarify scope of notification tests Make unit test for hass NumatoAPI independent of Home Assistant (very basic test of notifications). Improve the regular operations test for notifications. * Test for hass.states after operating switches Resolves a review finding. * Check for wrong port directions * WIP: Split numato tests to multiple files test_hass_binary_sensor_notification still fails. * Remove pytest asyncio decorator Apears to be redundant. Resolves a review finding. * Call switch services directly. Resolves a review finding. * Remove obsolete inline pylint config Co-Authored-By: Martin Hjelmare * Improve the numato_gpio module mockup Resolves a review finding. * Remove needless explicit conversions to str Resolves review findings. * Test setup of binary_sensor callbacks * Fix test_hass_binary_sensor_notification * Add forgotten await Review finding. Co-authored-by: Martin Hjelmare --- CODEOWNERS | 1 + homeassistant/components/numato/__init__.py | 248 ++++++++++++++++++ .../components/numato/binary_sensor.py | 120 +++++++++ homeassistant/components/numato/manifest.json | 8 + homeassistant/components/numato/sensor.py | 123 +++++++++ homeassistant/components/numato/switch.py | 108 ++++++++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/numato/__init__.py | 1 + tests/components/numato/common.py | 49 ++++ tests/components/numato/conftest.py | 28 ++ tests/components/numato/numato_mock.py | 68 +++++ tests/components/numato/test_binary_sensor.py | 62 +++++ tests/components/numato/test_init.py | 161 ++++++++++++ tests/components/numato/test_sensor.py | 38 +++ tests/components/numato/test_switch.py | 114 ++++++++ 16 files changed, 1135 insertions(+) create mode 100644 homeassistant/components/numato/__init__.py create mode 100644 homeassistant/components/numato/binary_sensor.py create mode 100644 homeassistant/components/numato/manifest.json create mode 100644 homeassistant/components/numato/sensor.py create mode 100644 homeassistant/components/numato/switch.py create mode 100644 tests/components/numato/__init__.py create mode 100644 tests/components/numato/common.py create mode 100644 tests/components/numato/conftest.py create mode 100644 tests/components/numato/numato_mock.py create mode 100644 tests/components/numato/test_binary_sensor.py create mode 100644 tests/components/numato/test_init.py create mode 100644 tests/components/numato/test_sensor.py create mode 100644 tests/components/numato/test_switch.py diff --git a/CODEOWNERS b/CODEOWNERS index 51ce87e702f15c..43959f67c30be8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -267,6 +267,7 @@ homeassistant/components/nsw_fuel_station/* @nickw444 homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte homeassistant/components/nuheat/* @bdraco homeassistant/components/nuki/* @pvizeli +homeassistant/components/numato/* @clssn homeassistant/components/nut/* @bdraco homeassistant/components/nws/* @MatthewFlamm homeassistant/components/nzbget/* @chriscla diff --git a/homeassistant/components/numato/__init__.py b/homeassistant/components/numato/__init__.py new file mode 100644 index 00000000000000..e5eeaa31846079 --- /dev/null +++ b/homeassistant/components/numato/__init__.py @@ -0,0 +1,248 @@ +"""Support for controlling GPIO pins of a Numato Labs USB GPIO expander.""" +import logging + +import numato_gpio as gpio +import voluptuous as vol + +from homeassistant.const import ( + CONF_BINARY_SENSORS, + CONF_ID, + CONF_NAME, + CONF_SENSORS, + CONF_SWITCHES, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, +) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "numato" + +CONF_INVERT_LOGIC = "invert_logic" +CONF_DISCOVER = "discover" +CONF_DEVICES = "devices" +CONF_DEVICE_ID = "id" +CONF_PORTS = "ports" +CONF_SRC_RANGE = "source_range" +CONF_DST_RANGE = "destination_range" +CONF_DST_UNIT = "unit" +DEFAULT_INVERT_LOGIC = False +DEFAULT_SRC_RANGE = [0, 1024] +DEFAULT_DST_RANGE = [0.0, 100.0] +DEFAULT_UNIT = "%" +DEFAULT_DEV = [f"/dev/ttyACM{i}" for i in range(10)] + +PORT_RANGE = range(1, 8) # ports 0-7 are ADC capable + +DATA_PORTS_IN_USE = "ports_in_use" +DATA_API = "api" + + +def int_range(rng): + """Validate the input array to describe a range by two integers.""" + if not (isinstance(rng[0], int) and isinstance(rng[1], int)): + raise vol.Invalid(f"Only integers are allowed: {rng}") + if len(rng) != 2: + raise vol.Invalid(f"Only two numbers allowed in a range: {rng}") + if rng[0] > rng[1]: + raise vol.Invalid(f"Lower range bound must come first: {rng}") + return rng + + +def float_range(rng): + """Validate the input array to describe a range by two floats.""" + try: + coe = vol.Coerce(float) + coe(rng[0]) + coe(rng[1]) + except vol.CoerceInvalid: + raise vol.Invalid(f"Only int or float values are allowed: {rng}") + if len(rng) != 2: + raise vol.Invalid(f"Only two numbers allowed in a range: {rng}") + if rng[0] > rng[1]: + raise vol.Invalid(f"Lower range bound must come first: {rng}") + return rng + + +def adc_port_number(num): + """Validate input number to be in the range of ADC enabled ports.""" + try: + num = int(num) + except (ValueError): + raise vol.Invalid(f"Port numbers must be integers: {num}") + if num not in range(1, 8): + raise vol.Invalid(f"Only port numbers from 1 to 7 are ADC capable: {num}") + return num + + +ADC_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_SRC_RANGE, default=DEFAULT_SRC_RANGE): int_range, + vol.Optional(CONF_DST_RANGE, default=DEFAULT_DST_RANGE): float_range, + vol.Optional(CONF_DST_UNIT, default=DEFAULT_UNIT): cv.string, + } +) + +PORTS_SCHEMA = vol.Schema({cv.positive_int: cv.string}) + +IO_PORTS_SCHEMA = vol.Schema( + { + vol.Required(CONF_PORTS): PORTS_SCHEMA, + vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, + } +) + +DEVICE_SCHEMA = vol.Schema( + { + vol.Required(CONF_ID): cv.positive_int, + CONF_BINARY_SENSORS: IO_PORTS_SCHEMA, + CONF_SWITCHES: IO_PORTS_SCHEMA, + CONF_SENSORS: {CONF_PORTS: {adc_port_number: ADC_SCHEMA}}, + } +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: { + CONF_DEVICES: vol.All(cv.ensure_list, [DEVICE_SCHEMA]), + vol.Optional(CONF_DISCOVER, default=DEFAULT_DEV): vol.All( + cv.ensure_list, [cv.string] + ), + }, + }, + extra=vol.ALLOW_EXTRA, +) + + +def setup(hass, config): + """Initialize the numato integration. + + Discovers available Numato devices and loads the binary_sensor, sensor and + switch platforms. + + Returns False on error during device discovery (e.g. duplicate ID), + otherwise returns True. + + No exceptions should occur, since the platforms are initialized on a best + effort basis, which means, errors are handled locally. + """ + hass.data[DOMAIN] = config[DOMAIN] + + try: + gpio.discover(config[DOMAIN][CONF_DISCOVER]) + except gpio.NumatoGpioError as err: + _LOGGER.info("Error discovering Numato devices: %s", err) + gpio.cleanup() + return False + + _LOGGER.info( + "Initializing Numato 32 port USB GPIO expanders with IDs: %s", + ", ".join(str(d) for d in gpio.devices), + ) + + hass.data[DOMAIN][DATA_API] = NumatoAPI() + + def cleanup_gpio(event): + """Stuff to do before stopping.""" + _LOGGER.debug("Clean up Numato GPIO") + gpio.cleanup() + if DATA_API in hass.data[DOMAIN]: + hass.data[DOMAIN][DATA_API].ports_registered.clear() + + def prepare_gpio(event): + """Stuff to do when home assistant starts.""" + _LOGGER.debug("Setup cleanup at stop for Numato GPIO") + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio) + + load_platform(hass, "binary_sensor", DOMAIN, {}, config) + load_platform(hass, "sensor", DOMAIN, {}, config) + load_platform(hass, "switch", DOMAIN, {}, config) + return True + + +# pylint: disable=no-self-use +class NumatoAPI: + """Home-Assistant specific API for numato device access.""" + + def __init__(self): + """Initialize API state.""" + self.ports_registered = dict() + + def check_port_free(self, device_id, port, direction): + """Check whether a port is still free set up. + + Fail with exception if it has already been registered. + """ + if (device_id, port) not in self.ports_registered: + self.ports_registered[(device_id, port)] = direction + else: + raise gpio.NumatoGpioError( + "Device {} port {} already in use as {}.".format( + device_id, + port, + "input" + if self.ports_registered[(device_id, port)] == gpio.IN + else "output", + ) + ) + + def check_device_id(self, device_id): + """Check whether a device has been discovered. + + Fail with exception. + """ + if device_id not in gpio.devices: + raise gpio.NumatoGpioError(f"Device {device_id} not available.") + + def check_port(self, device_id, port, direction): + """Raise an error if the port setup doesn't match the direction.""" + self.check_device_id(device_id) + if (device_id, port) not in self.ports_registered: + raise gpio.NumatoGpioError( + f"Port {port} is not set up for numato device {device_id}." + ) + msg = { + gpio.OUT: f"Trying to write to device {device_id} port {port} set up as input.", + gpio.IN: f"Trying to read from device {device_id} port {port} set up as output.", + } + if self.ports_registered[(device_id, port)] != direction: + raise gpio.NumatoGpioError(msg[direction]) + + def setup_output(self, device_id, port): + """Set up a GPIO as output.""" + self.check_device_id(device_id) + self.check_port_free(device_id, port, gpio.OUT) + gpio.devices[device_id].setup(port, gpio.OUT) + + def setup_input(self, device_id, port): + """Set up a GPIO as input.""" + self.check_device_id(device_id) + gpio.devices[device_id].setup(port, gpio.IN) + self.check_port_free(device_id, port, gpio.IN) + + def write_output(self, device_id, port, value): + """Write a value to a GPIO.""" + self.check_port(device_id, port, gpio.OUT) + gpio.devices[device_id].write(port, value) + + def read_input(self, device_id, port): + """Read a value from a GPIO.""" + self.check_port(device_id, port, gpio.IN) + return gpio.devices[device_id].read(port) + + def read_adc_input(self, device_id, port): + """Read an ADC value from a GPIO ADC port.""" + self.check_port(device_id, port, gpio.IN) + self.check_device_id(device_id) + return gpio.devices[device_id].adc_read(port) + + def edge_detect(self, device_id, port, event_callback): + """Add detection for RISING and FALLING events.""" + self.check_port(device_id, port, gpio.IN) + gpio.devices[device_id].add_event_detect(port, event_callback, gpio.BOTH) + gpio.devices[device_id].notify = True diff --git a/homeassistant/components/numato/binary_sensor.py b/homeassistant/components/numato/binary_sensor.py new file mode 100644 index 00000000000000..ff61cb3cbb0cc0 --- /dev/null +++ b/homeassistant/components/numato/binary_sensor.py @@ -0,0 +1,120 @@ +"""Binary sensor platform integration for Numato USB GPIO expanders.""" +from functools import partial +import logging + +from numato_gpio import NumatoGpioError + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send + +from . import ( + CONF_BINARY_SENSORS, + CONF_DEVICES, + CONF_ID, + CONF_INVERT_LOGIC, + CONF_PORTS, + DATA_API, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +NUMATO_SIGNAL = "numato_signal_{}_{}" + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the configured Numato USB GPIO binary sensor ports.""" + if discovery_info is None: + return + + def read_gpio(device_id, port, level): + """Send signal to entity to have it update state.""" + dispatcher_send(hass, NUMATO_SIGNAL.format(device_id, port), level) + + api = hass.data[DOMAIN][DATA_API] + binary_sensors = [] + devices = hass.data[DOMAIN][CONF_DEVICES] + for device in [d for d in devices if CONF_BINARY_SENSORS in d]: + device_id = device[CONF_ID] + platform = device[CONF_BINARY_SENSORS] + invert_logic = platform[CONF_INVERT_LOGIC] + ports = platform[CONF_PORTS] + for port, port_name in ports.items(): + try: + + api.setup_input(device_id, port) + api.edge_detect(device_id, port, partial(read_gpio, device_id)) + + except NumatoGpioError as err: + _LOGGER.error( + "Failed to initialize binary sensor '%s' on Numato device %s port %s: %s", + port_name, + device_id, + port, + err, + ) + continue + + binary_sensors.append( + NumatoGpioBinarySensor(port_name, device_id, port, invert_logic, api,) + ) + add_entities(binary_sensors, True) + + +class NumatoGpioBinarySensor(BinarySensorDevice): + """Represents a binary sensor (input) port of a Numato GPIO expander.""" + + def __init__(self, name, device_id, port, invert_logic, api): + """Initialize the Numato GPIO based binary sensor object.""" + self._name = name or DEVICE_DEFAULT_NAME + self._device_id = device_id + self._port = port + self._invert_logic = invert_logic + self._state = None + self._api = api + + async def async_added_to_hass(self): + """Connect state update callback.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + NUMATO_SIGNAL.format(self._device_id, self._port), + self._async_update_state, + ) + ) + + @callback + def _async_update_state(self, level): + """Update entity state.""" + self._state = level + self.async_write_ha_state() + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return the state of the entity.""" + return self._state != self._invert_logic + + def update(self): + """Update the GPIO state.""" + try: + self._state = self._api.read_input(self._device_id, self._port) + except NumatoGpioError as err: + self._state = None + _LOGGER.error( + "Failed to update Numato device %s port %s: %s", + self._device_id, + self._port, + err, + ) diff --git a/homeassistant/components/numato/manifest.json b/homeassistant/components/numato/manifest.json new file mode 100644 index 00000000000000..4e9857cd579914 --- /dev/null +++ b/homeassistant/components/numato/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "numato", + "name": "Numato USB GPIO Expander", + "documentation": "https://www.home-assistant.io/integrations/numato", + "requirements": ["numato-gpio==0.7.1"], + "codeowners": ["@clssn"], + "quality_scale": "internal" +} diff --git a/homeassistant/components/numato/sensor.py b/homeassistant/components/numato/sensor.py new file mode 100644 index 00000000000000..e268d32a29399a --- /dev/null +++ b/homeassistant/components/numato/sensor.py @@ -0,0 +1,123 @@ +"""Sensor platform integration for ADC ports of Numato USB GPIO expanders.""" +import logging + +from numato_gpio import NumatoGpioError + +from homeassistant.const import CONF_ID, CONF_NAME, CONF_SENSORS +from homeassistant.helpers.entity import Entity + +from . import ( + CONF_DEVICES, + CONF_DST_RANGE, + CONF_DST_UNIT, + CONF_PORTS, + CONF_SRC_RANGE, + DATA_API, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +ICON = "mdi:gauge" + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the configured Numato USB GPIO ADC sensor ports.""" + if discovery_info is None: + return + + api = hass.data[DOMAIN][DATA_API] + sensors = [] + devices = hass.data[DOMAIN][CONF_DEVICES] + for device in [d for d in devices if CONF_SENSORS in d]: + device_id = device[CONF_ID] + ports = device[CONF_SENSORS][CONF_PORTS] + for port, adc_def in ports.items(): + try: + api.setup_input(device_id, port) + except NumatoGpioError as err: + _LOGGER.error( + "Failed to initialize sensor '%s' on Numato device %s port %s: %s", + adc_def[CONF_NAME], + device_id, + port, + err, + ) + continue + sensors.append( + NumatoGpioAdc( + adc_def[CONF_NAME], + device_id, + port, + adc_def[CONF_SRC_RANGE], + adc_def[CONF_DST_RANGE], + adc_def[CONF_DST_UNIT], + api, + ) + ) + add_entities(sensors, True) + + +class NumatoGpioAdc(Entity): + """Represents an ADC port of a Numato USB GPIO expander.""" + + def __init__(self, name, device_id, port, src_range, dst_range, dst_unit, api): + """Initialize the sensor.""" + self._name = name + self._device_id = device_id + self._port = port + self._src_range = src_range + self._dst_range = dst_range + self._state = None + self._unit_of_measurement = dst_unit + self._api = api + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit_of_measurement + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return ICON + + def update(self): + """Get the latest data and updates the state.""" + try: + adc_val = self._api.read_adc_input(self._device_id, self._port) + adc_val = self._clamp_to_source_range(adc_val) + self._state = self._linear_scale_to_dest_range(adc_val) + except NumatoGpioError as err: + self._state = None + _LOGGER.error( + "Failed to update Numato device %s ADC-port %s: %s", + self._device_id, + self._port, + err, + ) + + def _clamp_to_source_range(self, val): + # clamp to source range + val = max(val, self._src_range[0]) + val = min(val, self._src_range[1]) + return val + + def _linear_scale_to_dest_range(self, val): + # linear scale to dest range + src_len = self._src_range[1] - self._src_range[0] + adc_val_rel = val - self._src_range[0] + ratio = float(adc_val_rel) / float(src_len) + dst_len = self._dst_range[1] - self._dst_range[0] + dest_val = self._dst_range[0] + ratio * dst_len + return dest_val diff --git a/homeassistant/components/numato/switch.py b/homeassistant/components/numato/switch.py new file mode 100644 index 00000000000000..2f1be0cf311f1b --- /dev/null +++ b/homeassistant/components/numato/switch.py @@ -0,0 +1,108 @@ +"""Switch platform integration for Numato USB GPIO expanders.""" +import logging + +from numato_gpio import NumatoGpioError + +from homeassistant.const import ( + CONF_DEVICES, + CONF_ID, + CONF_SWITCHES, + DEVICE_DEFAULT_NAME, +) +from homeassistant.helpers.entity import ToggleEntity + +from . import CONF_INVERT_LOGIC, CONF_PORTS, DATA_API, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the configured Numato USB GPIO switch ports.""" + if discovery_info is None: + return + + api = hass.data[DOMAIN][DATA_API] + switches = [] + devices = hass.data[DOMAIN][CONF_DEVICES] + for device in [d for d in devices if CONF_SWITCHES in d]: + device_id = device[CONF_ID] + platform = device[CONF_SWITCHES] + invert_logic = platform[CONF_INVERT_LOGIC] + ports = platform[CONF_PORTS] + for port, port_name in ports.items(): + try: + api.setup_output(device_id, port) + api.write_output(device_id, port, 1 if invert_logic else 0) + except NumatoGpioError as err: + _LOGGER.error( + "Failed to initialize switch '%s' on Numato device %s port %s: %s", + port_name, + device_id, + port, + err, + ) + continue + switches.append( + NumatoGpioSwitch(port_name, device_id, port, invert_logic, api,) + ) + add_entities(switches, True) + + +class NumatoGpioSwitch(ToggleEntity): + """Representation of a Numato USB GPIO switch port.""" + + def __init__(self, name, device_id, port, invert_logic, api): + """Initialize the port.""" + self._name = name or DEVICE_DEFAULT_NAME + self._device_id = device_id + self._port = port + self._invert_logic = invert_logic + self._state = False + self._api = api + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def is_on(self): + """Return true if port is turned on.""" + return self._state + + def turn_on(self, **kwargs): + """Turn the port on.""" + try: + self._api.write_output( + self._device_id, self._port, 0 if self._invert_logic else 1 + ) + self._state = True + self.schedule_update_ha_state() + except NumatoGpioError as err: + _LOGGER.error( + "Failed to turn on Numato device %s port %s: %s", + self._device_id, + self._port, + err, + ) + + def turn_off(self, **kwargs): + """Turn the port off.""" + try: + self._api.write_output( + self._device_id, self._port, 1 if self._invert_logic else 0 + ) + self._state = False + self.schedule_update_ha_state() + except NumatoGpioError as err: + _LOGGER.error( + "Failed to turn off Numato device %s port %s: %s", + self._device_id, + self._port, + err, + ) diff --git a/requirements_all.txt b/requirements_all.txt index 3a039d18832890..74feb6866fcf43 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -951,6 +951,9 @@ nsw-fuel-api-client==1.0.10 # homeassistant.components.nuheat nuheat==0.3.0 +# homeassistant.components.numato +numato-gpio==0.7.1 + # homeassistant.components.iqvia # homeassistant.components.opencv # homeassistant.components.tensorflow diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 47362e55aee8f8..07773687d888cb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -377,6 +377,9 @@ nsw-fuel-api-client==1.0.10 # homeassistant.components.nuheat nuheat==0.3.0 +# homeassistant.components.numato +numato-gpio==0.7.1 + # homeassistant.components.iqvia # homeassistant.components.opencv # homeassistant.components.tensorflow diff --git a/tests/components/numato/__init__.py b/tests/components/numato/__init__.py new file mode 100644 index 00000000000000..bcef00f9594076 --- /dev/null +++ b/tests/components/numato/__init__.py @@ -0,0 +1 @@ +"""Tests for the numato integration.""" diff --git a/tests/components/numato/common.py b/tests/components/numato/common.py new file mode 100644 index 00000000000000..18ece6690bc67b --- /dev/null +++ b/tests/components/numato/common.py @@ -0,0 +1,49 @@ +"""Definitions shared by all numato tests.""" + +from numato_gpio import NumatoGpioError + +NUMATO_CFG = { + "numato": { + "discover": ["/ttyACM0", "/ttyACM1"], + "devices": [ + { + "id": 0, + "binary_sensors": { + "invert_logic": False, + "ports": { + "2": "numato_binary_sensor_mock_port2", + "3": "numato_binary_sensor_mock_port3", + "4": "numato_binary_sensor_mock_port4", + }, + }, + "sensors": { + "ports": { + "1": { + "name": "numato_adc_mock_port1", + "source_range": [100, 1023], + "destination_range": [0, 10], + "unit": "mocks", + } + }, + }, + "switches": { + "invert_logic": False, + "ports": { + "5": "numato_switch_mock_port5", + "6": "numato_switch_mock_port6", + }, + }, + } + ], + } +} + + +def mockup_raise(*args, **kwargs): + """Mockup to replace regular functions for error injection.""" + raise NumatoGpioError("Error mockup") + + +def mockup_return(*args, **kwargs): + """Mockup to replace regular functions for error injection.""" + return False diff --git a/tests/components/numato/conftest.py b/tests/components/numato/conftest.py new file mode 100644 index 00000000000000..c6fd13a099eb69 --- /dev/null +++ b/tests/components/numato/conftest.py @@ -0,0 +1,28 @@ +"""Fixtures for numato tests.""" + +from copy import deepcopy + +import pytest + +from homeassistant.components import numato + +from . import numato_mock +from .common import NUMATO_CFG + + +@pytest.fixture +def config(): + """Provide a copy of the numato domain's test configuration. + + This helps to quickly change certain aspects of the configuration scoped + to each individual test. + """ + return deepcopy(NUMATO_CFG) + + +@pytest.fixture +def numato_fixture(monkeypatch): + """Inject the numato mockup into numato homeassistant module.""" + module_mock = numato_mock.NumatoModuleMock() + monkeypatch.setattr(numato, "gpio", module_mock) + return module_mock diff --git a/tests/components/numato/numato_mock.py b/tests/components/numato/numato_mock.py new file mode 100644 index 00000000000000..1f8b24027ded43 --- /dev/null +++ b/tests/components/numato/numato_mock.py @@ -0,0 +1,68 @@ +"""Mockup for the numato component interface.""" +from numato_gpio import NumatoGpioError + + +class NumatoModuleMock: + """Mockup for the numato_gpio module.""" + + NumatoGpioError = NumatoGpioError + + def __init__(self): + """Initialize the numato_gpio module mockup class.""" + self.devices = {} + + class NumatoDeviceMock: + """Mockup for the numato_gpio.NumatoUsbGpio class.""" + + def __init__(self, device): + """Initialize numato device mockup.""" + self.device = device + self.callbacks = {} + self.ports = set() + self.values = {} + + def setup(self, port, direction): + """Mockup for setup.""" + self.ports.add(port) + self.values[port] = None + + def write(self, port, value): + """Mockup for write.""" + self.values[port] = value + + def read(self, port): + """Mockup for read.""" + return 1 + + def adc_read(self, port): + """Mockup for adc_read.""" + return 1023 + + def add_event_detect(self, port, callback, direction): + """Mockup for add_event_detect.""" + self.callbacks[port] = callback + + def notify(self, enable): + """Mockup for notify.""" + + def mockup_inject_notification(self, port, value): + """Make the mockup execute a notification callback.""" + self.callbacks[port](port, value) + + OUT = 0 + IN = 1 + + RISING = 1 + FALLING = 2 + BOTH = 3 + + def discover(self, _=None): + """Mockup for the numato device discovery. + + Ignore the device list argument, mock discovers /dev/ttyACM0. + """ + self.devices[0] = NumatoModuleMock.NumatoDeviceMock("/dev/ttyACM0") + + def cleanup(self): + """Mockup for the numato device cleanup.""" + self.devices.clear() diff --git a/tests/components/numato/test_binary_sensor.py b/tests/components/numato/test_binary_sensor.py new file mode 100644 index 00000000000000..5aa6aea2b8d687 --- /dev/null +++ b/tests/components/numato/test_binary_sensor.py @@ -0,0 +1,62 @@ +"""Tests for the numato binary_sensor platform.""" +from homeassistant.helpers import discovery +from homeassistant.setup import async_setup_component + +from .common import NUMATO_CFG, mockup_raise + +MOCKUP_ENTITY_IDS = { + "binary_sensor.numato_binary_sensor_mock_port2", + "binary_sensor.numato_binary_sensor_mock_port3", + "binary_sensor.numato_binary_sensor_mock_port4", +} + + +async def test_failing_setups_no_entities(hass, numato_fixture, monkeypatch): + """When port setup fails, no entity shall be created.""" + monkeypatch.setattr(numato_fixture.NumatoDeviceMock, "setup", mockup_raise) + assert await async_setup_component(hass, "numato", NUMATO_CFG) + await hass.async_block_till_done() + for entity_id in MOCKUP_ENTITY_IDS: + assert entity_id not in hass.states.async_entity_ids() + + +async def test_setup_callbacks(hass, numato_fixture, monkeypatch): + """During setup a callback shall be registered.""" + + numato_fixture.discover() + + def mock_add_event_detect(self, port, callback, direction): + assert self == numato_fixture.devices[0] + assert port == 1 + assert callback is callable + assert direction == numato_fixture.BOTH + + monkeypatch.setattr( + numato_fixture.devices[0], "add_event_detect", mock_add_event_detect + ) + assert await async_setup_component(hass, "numato", NUMATO_CFG) + + +async def test_hass_binary_sensor_notification(hass, numato_fixture): + """Test regular operations from within Home Assistant.""" + assert await async_setup_component(hass, "numato", NUMATO_CFG) + await hass.async_block_till_done() # wait until services are registered + assert ( + hass.states.get("binary_sensor.numato_binary_sensor_mock_port2").state == "on" + ) + await hass.async_add_executor_job(numato_fixture.devices[0].callbacks[2], 2, False) + await hass.async_block_till_done() + assert ( + hass.states.get("binary_sensor.numato_binary_sensor_mock_port2").state == "off" + ) + + +async def test_binary_sensor_setup_without_discovery_info(hass, config, numato_fixture): + """Test handling of empty discovery_info.""" + numato_fixture.discover() + await discovery.async_load_platform(hass, "binary_sensor", "numato", None, config) + for entity_id in MOCKUP_ENTITY_IDS: + assert entity_id not in hass.states.async_entity_ids() + await hass.async_block_till_done() # wait for numato platform to be loaded + for entity_id in MOCKUP_ENTITY_IDS: + assert entity_id in hass.states.async_entity_ids() diff --git a/tests/components/numato/test_init.py b/tests/components/numato/test_init.py new file mode 100644 index 00000000000000..dd5643be12a2de --- /dev/null +++ b/tests/components/numato/test_init.py @@ -0,0 +1,161 @@ +"""Tests for the numato integration.""" +from numato_gpio import NumatoGpioError +import pytest + +from homeassistant.components import numato +from homeassistant.setup import async_setup_component + +from .common import NUMATO_CFG, mockup_raise, mockup_return + + +async def test_setup_no_devices(hass, numato_fixture, monkeypatch): + """Test handling of an 'empty' discovery. + + Platform setups are expected to return after handling errors locally + without raising. + """ + monkeypatch.setattr(numato_fixture, "discover", mockup_return) + assert await async_setup_component(hass, "numato", NUMATO_CFG) + assert len(numato_fixture.devices) == 0 + + +async def test_fail_setup_raising_discovery(hass, numato_fixture, caplog, monkeypatch): + """Test handling of an exception during discovery. + + Setup shall return False. + """ + monkeypatch.setattr(numato_fixture, "discover", mockup_raise) + assert not await async_setup_component(hass, "numato", NUMATO_CFG) + await hass.async_block_till_done() + + +async def test_hass_numato_api_wrong_port_directions(hass, numato_fixture): + """Test handling of wrong port directions. + + This won't happen in the current platform implementation but would raise + in case of an introduced bug in the platforms. + """ + numato_fixture.discover() + api = numato.NumatoAPI() + api.setup_output(0, 5) + api.setup_input(0, 2) + api.setup_input(0, 6) + with pytest.raises(NumatoGpioError): + api.read_adc_input(0, 5) # adc_read from output + api.read_input(0, 6) # read from output + api.write_output(0, 2, 1) # write to input + + +async def test_hass_numato_api_errors(hass, numato_fixture, monkeypatch): + """Test whether Home Assistant numato API (re-)raises errors.""" + numato_fixture.discover() + monkeypatch.setattr(numato_fixture.devices[0], "setup", mockup_raise) + monkeypatch.setattr(numato_fixture.devices[0], "adc_read", mockup_raise) + monkeypatch.setattr(numato_fixture.devices[0], "read", mockup_raise) + monkeypatch.setattr(numato_fixture.devices[0], "write", mockup_raise) + api = numato.NumatoAPI() + with pytest.raises(NumatoGpioError): + api.setup_input(0, 5) + api.read_adc_input(0, 1) + api.read_input(0, 2) + api.write_output(0, 2, 1) + + +async def test_invalid_port_number(hass, numato_fixture, config): + """Test validation of ADC port number type.""" + sensorports_cfg = config["numato"]["devices"][0]["sensors"]["ports"] + port1_config = sensorports_cfg["1"] + sensorports_cfg["one"] = port1_config + del sensorports_cfg["1"] + assert not await async_setup_component(hass, "numato", config) + await hass.async_block_till_done() + assert not numato_fixture.devices + + +async def test_too_low_adc_port_number(hass, numato_fixture, config): + """Test handling of failing component setup. + + Tries setting up an ADC on a port below (0) the allowed range. + """ + + sensorports_cfg = config["numato"]["devices"][0]["sensors"]["ports"] + sensorports_cfg.update({0: {"name": "toolow"}}) + assert not await async_setup_component(hass, "numato", config) + assert not numato_fixture.devices + + +async def test_too_high_adc_port_number(hass, numato_fixture, config): + """Test handling of failing component setup. + + Tries setting up an ADC on a port above (8) the allowed range. + """ + sensorports_cfg = config["numato"]["devices"][0]["sensors"]["ports"] + sensorports_cfg.update({8: {"name": "toohigh"}}) + assert not await async_setup_component(hass, "numato", config) + assert not numato_fixture.devices + + +async def test_invalid_adc_range_value_type(hass, numato_fixture, config): + """Test validation of ADC range config's types. + + Replaces the source range beginning by a string. + """ + sensorports_cfg = config["numato"]["devices"][0]["sensors"]["ports"] + sensorports_cfg["1"]["source_range"][0] = "zero" + assert not await async_setup_component(hass, "numato", config) + assert not numato_fixture.devices + + +async def test_invalid_adc_source_range_length(hass, numato_fixture, config): + """Test validation of ADC range config's length. + + Adds an element to the source range. + """ + sensorports_cfg = config["numato"]["devices"][0]["sensors"]["ports"] + sensorports_cfg["1"]["source_range"].append(42) + assert not await async_setup_component(hass, "numato", config) + assert not numato_fixture.devices + + +async def test_invalid_adc_source_range_order(hass, numato_fixture, config): + """Test validation of ADC range config's order. + + Sets the source range to a decreasing [2, 1]. + """ + sensorports_cfg = config["numato"]["devices"][0]["sensors"]["ports"] + sensorports_cfg["1"]["source_range"] = [2, 1] + assert not await async_setup_component(hass, "numato", config) + assert not numato_fixture.devices + + +async def test_invalid_adc_destination_range_value_type(hass, numato_fixture, config): + """Test validation of ADC range . + + Replaces the destination range beginning by a string. + """ + sensorports_cfg = config["numato"]["devices"][0]["sensors"]["ports"] + sensorports_cfg["1"]["destination_range"][0] = "zero" + assert not await async_setup_component(hass, "numato", config) + assert not numato_fixture.devices + + +async def test_invalid_adc_destination_range_length(hass, numato_fixture, config): + """Test validation of ADC range config's length. + + Adds an element to the destination range. + """ + sensorports_cfg = config["numato"]["devices"][0]["sensors"]["ports"] + sensorports_cfg["1"]["destination_range"].append(42) + assert not await async_setup_component(hass, "numato", config) + assert not numato_fixture.devices + + +async def test_invalid_adc_destination_range_order(hass, numato_fixture, config): + """Test validation of ADC range config's order. + + Sets the destination range to a decreasing [2, 1]. + """ + sensorports_cfg = config["numato"]["devices"][0]["sensors"]["ports"] + sensorports_cfg["1"]["destination_range"] = [2, 1] + assert not await async_setup_component(hass, "numato", config) + assert not numato_fixture.devices diff --git a/tests/components/numato/test_sensor.py b/tests/components/numato/test_sensor.py new file mode 100644 index 00000000000000..c6d176dbc90725 --- /dev/null +++ b/tests/components/numato/test_sensor.py @@ -0,0 +1,38 @@ +"""Tests for the numato sensor platform.""" +from homeassistant.const import STATE_UNKNOWN +from homeassistant.helpers import discovery +from homeassistant.setup import async_setup_component + +from .common import NUMATO_CFG, mockup_raise + +MOCKUP_ENTITY_IDS = { + "sensor.numato_adc_mock_port1", +} + + +async def test_failing_setups_no_entities(hass, numato_fixture, monkeypatch): + """When port setup fails, no entity shall be created.""" + monkeypatch.setattr(numato_fixture.NumatoDeviceMock, "setup", mockup_raise) + assert await async_setup_component(hass, "numato", NUMATO_CFG) + await hass.async_block_till_done() + for entity_id in MOCKUP_ENTITY_IDS: + assert entity_id not in hass.states.async_entity_ids() + + +async def test_failing_sensor_update(hass, numato_fixture, monkeypatch): + """Test condition when a sensor update fails.""" + monkeypatch.setattr(numato_fixture.NumatoDeviceMock, "adc_read", mockup_raise) + assert await async_setup_component(hass, "numato", NUMATO_CFG) + await hass.async_block_till_done() + assert hass.states.get("sensor.numato_adc_mock_port1").state is STATE_UNKNOWN + + +async def test_sensor_setup_without_discovery_info(hass, config, numato_fixture): + """Test handling of empty discovery_info.""" + numato_fixture.discover() + await discovery.async_load_platform(hass, "sensor", "numato", None, config) + for entity_id in MOCKUP_ENTITY_IDS: + assert entity_id not in hass.states.async_entity_ids() + await hass.async_block_till_done() # wait for numato platform to be loaded + for entity_id in MOCKUP_ENTITY_IDS: + assert entity_id in hass.states.async_entity_ids() diff --git a/tests/components/numato/test_switch.py b/tests/components/numato/test_switch.py new file mode 100644 index 00000000000000..91cda5c2a3785f --- /dev/null +++ b/tests/components/numato/test_switch.py @@ -0,0 +1,114 @@ +"""Tests for the numato switch platform.""" +from homeassistant.components import switch +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.helpers import discovery +from homeassistant.setup import async_setup_component + +from .common import NUMATO_CFG, mockup_raise + +MOCKUP_ENTITY_IDS = { + "switch.numato_switch_mock_port5", + "switch.numato_switch_mock_port6", +} + + +async def test_failing_setups_no_entities(hass, numato_fixture, monkeypatch): + """When port setup fails, no entity shall be created.""" + monkeypatch.setattr(numato_fixture.NumatoDeviceMock, "setup", mockup_raise) + assert await async_setup_component(hass, "numato", NUMATO_CFG) + await hass.async_block_till_done() + for entity_id in MOCKUP_ENTITY_IDS: + assert entity_id not in hass.states.async_entity_ids() + + +async def test_regular_hass_operations(hass, numato_fixture): + """Test regular operations from within Home Assistant.""" + assert await async_setup_component(hass, "numato", NUMATO_CFG) + await hass.async_block_till_done() # wait until services are registered + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.numato_switch_mock_port5"}, + blocking=True, + ) + assert hass.states.get("switch.numato_switch_mock_port5").state == "on" + assert numato_fixture.devices[0].values[5] == 1 + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.numato_switch_mock_port6"}, + blocking=True, + ) + assert hass.states.get("switch.numato_switch_mock_port6").state == "on" + assert numato_fixture.devices[0].values[6] == 1 + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.numato_switch_mock_port5"}, + blocking=True, + ) + assert hass.states.get("switch.numato_switch_mock_port5").state == "off" + assert numato_fixture.devices[0].values[5] == 0 + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.numato_switch_mock_port6"}, + blocking=True, + ) + assert hass.states.get("switch.numato_switch_mock_port6").state == "off" + assert numato_fixture.devices[0].values[6] == 0 + + +async def test_failing_hass_operations(hass, numato_fixture, monkeypatch): + """Test failing operations called from within Home Assistant. + + Switches remain in their initial 'off' state when the device can't + be written to. + """ + assert await async_setup_component(hass, "numato", NUMATO_CFG) + + await hass.async_block_till_done() # wait until services are registered + monkeypatch.setattr(numato_fixture.devices[0], "write", mockup_raise) + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.numato_switch_mock_port5"}, + blocking=True, + ) + assert hass.states.get("switch.numato_switch_mock_port5").state == "off" + assert not numato_fixture.devices[0].values[5] + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.numato_switch_mock_port6"}, + blocking=True, + ) + assert hass.states.get("switch.numato_switch_mock_port6").state == "off" + assert not numato_fixture.devices[0].values[6] + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.numato_switch_mock_port5"}, + blocking=True, + ) + assert hass.states.get("switch.numato_switch_mock_port5").state == "off" + assert not numato_fixture.devices[0].values[5] + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.numato_switch_mock_port6"}, + blocking=True, + ) + assert hass.states.get("switch.numato_switch_mock_port6").state == "off" + assert not numato_fixture.devices[0].values[6] + + +async def test_switch_setup_without_discovery_info(hass, config, numato_fixture): + """Test handling of empty discovery_info.""" + numato_fixture.discover() + await discovery.async_load_platform(hass, "switch", "numato", None, config) + for entity_id in MOCKUP_ENTITY_IDS: + assert entity_id not in hass.states.async_entity_ids() + await hass.async_block_till_done() # wait for numato platform to be loaded + for entity_id in MOCKUP_ENTITY_IDS: + assert entity_id in hass.states.async_entity_ids() From a65edc8dc146a8424bfd2a11f59090538cdd265a Mon Sep 17 00:00:00 2001 From: Frederik Gladhorn Date: Thu, 30 Apr 2020 14:52:53 +0200 Subject: [PATCH 02/58] Fix crash in NAD integration (#34571) Some amplifiers/receivers do not report any volume (due to them not knowing), for example NAD C 356BEE is documented to return an empty string for volume. At least don't crash when we get None back. --- homeassistant/components/nad/media_player.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nad/media_player.py b/homeassistant/components/nad/media_player.py index c9015b85fd16fd..2d9afbb7541999 100644 --- a/homeassistant/components/nad/media_player.py +++ b/homeassistant/components/nad/media_player.py @@ -197,7 +197,10 @@ def update(self): else: self._mute = True - self._volume = self.calc_volume(self._nad_receiver.main_volume("?")) + volume = self._nad_receiver.main_volume("?") + # Some receivers cannot report the volume, e.g. C 356BEE, + # instead they only support stepping the volume up or down + self._volume = self.calc_volume(volume) if volume is not None else None self._source = self._source_dict.get(self._nad_receiver.main_source("?")) def calc_volume(self, decibel): From 29a05a6a65be590ab83bccc702b95dd56fb65c51 Mon Sep 17 00:00:00 2001 From: Thomas Le Gentil <20202649+kifeo@users.noreply.github.com> Date: Thu, 30 Apr 2020 15:08:11 +0200 Subject: [PATCH 03/58] Add fortigate deprecation message (#34854) --- .../components/fortigate/__init__.py | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/fortigate/__init__.py b/homeassistant/components/fortigate/__init__.py index 6de55ae3d65703..2dbd7ef45c0f97 100644 --- a/homeassistant/components/fortigate/__init__.py +++ b/homeassistant/components/fortigate/__init__.py @@ -21,18 +21,21 @@ DATA_FGT = DOMAIN CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_DEVICES, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - } - ) - }, + vol.All( + cv.deprecated(DOMAIN, invalidation_version="0.112.0"), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_DEVICES, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) From 55bf5514ad6a6cf5945a398001671ac46169ac06 Mon Sep 17 00:00:00 2001 From: alxrdn Date: Thu, 30 Apr 2020 15:30:37 +0200 Subject: [PATCH 04/58] Add overlay options wrapper to rpi_camera (#34461) * add overlay options wrapper to rpi_camera * Refactor to set config yaml section under the top level integration domain key * Remove return values that are not checked Co-Authored-By: Martin Hjelmare * Remove superfluous debug log messages * Return if not set up via discovery * Add convenience reference to hass.data[DOMAIN] * Black formatting * Isort * Exclude all rpi_camera modules Co-authored-by: Martin Hjelmare --- .coveragerc | 2 +- .../components/rpi_camera/__init__.py | 88 +++++++++++++++++ homeassistant/components/rpi_camera/camera.py | 94 ++++++++----------- homeassistant/components/rpi_camera/const.py | 22 +++++ 4 files changed, 148 insertions(+), 58 deletions(-) create mode 100644 homeassistant/components/rpi_camera/const.py diff --git a/.coveragerc b/.coveragerc index cf9fa59397da42..e2aa4243a46300 100644 --- a/.coveragerc +++ b/.coveragerc @@ -611,7 +611,7 @@ omit = homeassistant/components/roomba/vacuum.py homeassistant/components/route53/* homeassistant/components/rova/sensor.py - homeassistant/components/rpi_camera/camera.py + homeassistant/components/rpi_camera/* homeassistant/components/rpi_gpio/* homeassistant/components/rpi_gpio/cover.py homeassistant/components/rpi_gpio_pwm/light.py diff --git a/homeassistant/components/rpi_camera/__init__.py b/homeassistant/components/rpi_camera/__init__.py index 04638e463a1e63..2f962872d8cafe 100644 --- a/homeassistant/components/rpi_camera/__init__.py +++ b/homeassistant/components/rpi_camera/__init__.py @@ -1 +1,89 @@ """The rpi_camera component.""" +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_FILE_PATH, CONF_NAME +from homeassistant.helpers import config_validation as cv, discovery + +from .const import ( + CONF_HORIZONTAL_FLIP, + CONF_IMAGE_HEIGHT, + CONF_IMAGE_QUALITY, + CONF_IMAGE_ROTATION, + CONF_IMAGE_WIDTH, + CONF_OVERLAY_METADATA, + CONF_OVERLAY_TIMESTAMP, + CONF_TIMELAPSE, + CONF_VERTICAL_FLIP, + DEFAULT_HORIZONTAL_FLIP, + DEFAULT_IMAGE_HEIGHT, + DEFAULT_IMAGE_QUALITY, + DEFAULT_IMAGE_ROTATION, + DEFAULT_IMAGE_WIDTH, + DEFAULT_NAME, + DEFAULT_TIMELAPSE, + DEFAULT_VERTICAL_FLIP, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_FILE_PATH): cv.isfile, + vol.Optional( + CONF_HORIZONTAL_FLIP, default=DEFAULT_HORIZONTAL_FLIP + ): vol.All(vol.Coerce(int), vol.Range(min=0, max=1)), + vol.Optional( + CONF_IMAGE_HEIGHT, default=DEFAULT_IMAGE_HEIGHT + ): vol.Coerce(int), + vol.Optional( + CONF_IMAGE_QUALITY, default=DEFAULT_IMAGE_QUALITY + ): vol.All(vol.Coerce(int), vol.Range(min=0, max=100)), + vol.Optional( + CONF_IMAGE_ROTATION, default=DEFAULT_IMAGE_ROTATION + ): vol.All(vol.Coerce(int), vol.Range(min=0, max=359)), + vol.Optional(CONF_IMAGE_WIDTH, default=DEFAULT_IMAGE_WIDTH): vol.Coerce( + int + ), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OVERLAY_METADATA): vol.All( + vol.Coerce(int), vol.Range(min=4, max=2056) + ), + vol.Optional(CONF_OVERLAY_TIMESTAMP): cv.string, + vol.Optional(CONF_TIMELAPSE, default=DEFAULT_TIMELAPSE): vol.Coerce( + int + ), + vol.Optional( + CONF_VERTICAL_FLIP, default=DEFAULT_VERTICAL_FLIP + ): vol.All(vol.Coerce(int), vol.Range(min=0, max=1)), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +def setup(hass, config): + """Set up the rpi_camera integration.""" + config_domain = config[DOMAIN] + hass.data[DOMAIN] = { + CONF_FILE_PATH: config_domain.get(CONF_FILE_PATH), + CONF_HORIZONTAL_FLIP: config_domain.get(CONF_HORIZONTAL_FLIP), + CONF_IMAGE_WIDTH: config_domain.get(CONF_IMAGE_WIDTH), + CONF_IMAGE_HEIGHT: config_domain.get(CONF_IMAGE_HEIGHT), + CONF_IMAGE_QUALITY: config_domain.get(CONF_IMAGE_QUALITY), + CONF_IMAGE_ROTATION: config_domain.get(CONF_IMAGE_ROTATION), + CONF_NAME: config_domain.get(CONF_NAME), + CONF_OVERLAY_METADATA: config_domain.get(CONF_OVERLAY_METADATA), + CONF_OVERLAY_TIMESTAMP: config_domain.get(CONF_OVERLAY_TIMESTAMP), + CONF_TIMELAPSE: config_domain.get(CONF_TIMELAPSE), + CONF_VERTICAL_FLIP: config_domain.get(CONF_VERTICAL_FLIP), + } + + discovery.load_platform(hass, "camera", DOMAIN, {}, config) + + return True diff --git a/homeassistant/components/rpi_camera/camera.py b/homeassistant/components/rpi_camera/camera.py index bf04e0ef492372..47ce87c4a8d6a1 100644 --- a/homeassistant/components/rpi_camera/camera.py +++ b/homeassistant/components/rpi_camera/camera.py @@ -5,53 +5,24 @@ import subprocess from tempfile import NamedTemporaryFile -import voluptuous as vol - -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.camera import Camera from homeassistant.const import CONF_FILE_PATH, CONF_NAME, EVENT_HOMEASSISTANT_STOP -from homeassistant.helpers import config_validation as cv - -_LOGGER = logging.getLogger(__name__) -CONF_HORIZONTAL_FLIP = "horizontal_flip" -CONF_IMAGE_HEIGHT = "image_height" -CONF_IMAGE_QUALITY = "image_quality" -CONF_IMAGE_ROTATION = "image_rotation" -CONF_IMAGE_WIDTH = "image_width" -CONF_TIMELAPSE = "timelapse" -CONF_VERTICAL_FLIP = "vertical_flip" - -DEFAULT_HORIZONTAL_FLIP = 0 -DEFAULT_IMAGE_HEIGHT = 480 -DEFAULT_IMAGE_QUALITY = 7 -DEFAULT_IMAGE_ROTATION = 0 -DEFAULT_IMAGE_WIDTH = 640 -DEFAULT_NAME = "Raspberry Pi Camera" -DEFAULT_TIMELAPSE = 1000 -DEFAULT_VERTICAL_FLIP = 0 - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_FILE_PATH): cv.isfile, - vol.Optional(CONF_HORIZONTAL_FLIP, default=DEFAULT_HORIZONTAL_FLIP): vol.All( - vol.Coerce(int), vol.Range(min=0, max=1) - ), - vol.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_IMAGE_HEIGHT): vol.Coerce(int), - vol.Optional(CONF_IMAGE_QUALITY, default=DEFAULT_IMAGE_QUALITY): vol.All( - vol.Coerce(int), vol.Range(min=0, max=100) - ), - vol.Optional(CONF_IMAGE_ROTATION, default=DEFAULT_IMAGE_ROTATION): vol.All( - vol.Coerce(int), vol.Range(min=0, max=359) - ), - vol.Optional(CONF_IMAGE_WIDTH, default=DEFAULT_IMAGE_WIDTH): vol.Coerce(int), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_TIMELAPSE, default=1000): vol.Coerce(int), - vol.Optional(CONF_VERTICAL_FLIP, default=DEFAULT_VERTICAL_FLIP): vol.All( - vol.Coerce(int), vol.Range(min=0, max=1) - ), - } +from .const import ( + CONF_HORIZONTAL_FLIP, + CONF_IMAGE_HEIGHT, + CONF_IMAGE_QUALITY, + CONF_IMAGE_ROTATION, + CONF_IMAGE_WIDTH, + CONF_OVERLAY_METADATA, + CONF_OVERLAY_TIMESTAMP, + CONF_TIMELAPSE, + CONF_VERTICAL_FLIP, + DOMAIN, ) +_LOGGER = logging.getLogger(__name__) + def kill_raspistill(*args): """Kill any previously running raspistill process..""" @@ -62,24 +33,18 @@ def kill_raspistill(*args): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Raspberry Camera.""" + # We only want this platform to be set up via discovery. + # prevent initializing by erroneous platform config section in yaml conf + if discovery_info is None: + return + if shutil.which("raspistill") is None: _LOGGER.error("'raspistill' was not found") - return False - - setup_config = { - CONF_NAME: config.get(CONF_NAME), - CONF_IMAGE_WIDTH: config.get(CONF_IMAGE_WIDTH), - CONF_IMAGE_HEIGHT: config.get(CONF_IMAGE_HEIGHT), - CONF_IMAGE_QUALITY: config.get(CONF_IMAGE_QUALITY), - CONF_IMAGE_ROTATION: config.get(CONF_IMAGE_ROTATION), - CONF_TIMELAPSE: config.get(CONF_TIMELAPSE), - CONF_HORIZONTAL_FLIP: config.get(CONF_HORIZONTAL_FLIP), - CONF_VERTICAL_FLIP: config.get(CONF_VERTICAL_FLIP), - CONF_FILE_PATH: config.get(CONF_FILE_PATH), - } + return hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, kill_raspistill) + setup_config = hass.data[DOMAIN] file_path = setup_config[CONF_FILE_PATH] def delete_temp_file(*args): @@ -100,7 +65,7 @@ def delete_temp_file(*args): # Check whether the file path has been whitelisted elif not hass.config.is_allowed_path(file_path): _LOGGER.error("'%s' is not a whitelisted directory", file_path) - return False + return add_entities([RaspberryCamera(setup_config)]) @@ -142,6 +107,16 @@ def __init__(self, device_info): if device_info[CONF_VERTICAL_FLIP]: cmd_args.append("-vf") + if device_info[CONF_OVERLAY_METADATA]: + cmd_args.append("-a") + cmd_args.append(str(device_info[CONF_OVERLAY_METADATA])) + + if device_info[CONF_OVERLAY_TIMESTAMP]: + cmd_args.append("-a") + cmd_args.append("4") + cmd_args.append("-a") + cmd_args.append(str(device_info[CONF_OVERLAY_TIMESTAMP])) + subprocess.Popen(cmd_args, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) def camera_image(self): @@ -153,3 +128,8 @@ def camera_image(self): def name(self): """Return the name of this camera.""" return self._name + + @property + def frame_interval(self): + """Return the interval between frames of the stream.""" + return self._config[CONF_TIMELAPSE] / 1000 diff --git a/homeassistant/components/rpi_camera/const.py b/homeassistant/components/rpi_camera/const.py new file mode 100644 index 00000000000000..19da9e2a28691a --- /dev/null +++ b/homeassistant/components/rpi_camera/const.py @@ -0,0 +1,22 @@ +"""Consts used by rpi_camera.""" + +DOMAIN = "rpi_camera" + +CONF_HORIZONTAL_FLIP = "horizontal_flip" +CONF_IMAGE_HEIGHT = "image_height" +CONF_IMAGE_QUALITY = "image_quality" +CONF_IMAGE_ROTATION = "image_rotation" +CONF_IMAGE_WIDTH = "image_width" +CONF_OVERLAY_METADATA = "overlay_metadata" +CONF_OVERLAY_TIMESTAMP = "overlay_timestamp" +CONF_TIMELAPSE = "timelapse" +CONF_VERTICAL_FLIP = "vertical_flip" + +DEFAULT_HORIZONTAL_FLIP = 0 +DEFAULT_IMAGE_HEIGHT = 480 +DEFAULT_IMAGE_QUALITY = 7 +DEFAULT_IMAGE_ROTATION = 0 +DEFAULT_IMAGE_WIDTH = 640 +DEFAULT_NAME = "Raspberry Pi Camera" +DEFAULT_TIMELAPSE = 1000 +DEFAULT_VERTICAL_FLIP = 0 From d43617c41d6507f2d2b77aadf4fa1ebaf0058b14 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 30 Apr 2020 15:43:02 +0200 Subject: [PATCH 05/58] Bump brother to 0.1.14 (#34930) --- homeassistant/components/brother/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index 48df788c93a42f..7f59aaa9c2cc33 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -3,7 +3,7 @@ "name": "Brother Printer", "documentation": "https://www.home-assistant.io/integrations/brother", "codeowners": ["@bieniu"], - "requirements": ["brother==0.1.13"], + "requirements": ["brother==0.1.14"], "zeroconf": ["_printer._tcp.local."], "config_flow": true, "quality_scale": "platinum" diff --git a/requirements_all.txt b/requirements_all.txt index 74feb6866fcf43..d166d68ae7f3cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -365,7 +365,7 @@ bravia-tv==1.0.2 broadlink==0.13.2 # homeassistant.components.brother -brother==0.1.13 +brother==0.1.14 # homeassistant.components.brottsplatskartan brottsplatskartan==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 07773687d888cb..607acf46478d48 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -150,7 +150,7 @@ bravia-tv==1.0.2 broadlink==0.13.2 # homeassistant.components.brother -brother==0.1.13 +brother==0.1.14 # homeassistant.components.buienradar buienradar==1.0.4 From bf5cc22bef3a65e278056dd94b51c78e5089272c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Apr 2020 13:34:25 -0500 Subject: [PATCH 06/58] Fix preservation of homekit fan speed on toggle (#34971) --- homeassistant/components/homekit/type_fans.py | 4 ++- homeassistant/components/homekit/util.py | 2 +- tests/components/homekit/test_type_fans.py | 26 +++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index b7208b1746cff1..291b3ffed907db 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -165,7 +165,9 @@ def update_state(self, new_state): self.char_direction.set_value(hk_direction) # Handle Speed - if self.char_speed is not None: + if self.char_speed is not None and state != STATE_OFF: + # We do not change the homekit speed when turning off + # as it will clear the restore state speed = new_state.attributes.get(ATTR_SPEED) hk_speed_value = self.speed_mapping.speed_to_homekit(speed) if hk_speed_value is not None and self.char_speed.value != hk_speed_value: diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 0295440df499d6..b8d98ad2304178 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -200,7 +200,7 @@ def speed_to_homekit(self, speed): if speed is None: return None speed_range = self.speed_ranges[speed] - return speed_range.target + return round(speed_range.target) def speed_to_states(self, speed): """Map HomeKit speed to Home Assistant speed state.""" diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index ca6e03217f3eb4..4d2ace24eab87d 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -304,6 +304,7 @@ async def test_fan_speed(hass, hk_driver, cls, events): call_set_speed = async_mock_service(hass, DOMAIN, "set_speed") char_speed_iid = acc.char_speed.to_HAP()[HAP_REPR_IID] + char_active_iid = acc.char_active.to_HAP()[HAP_REPR_IID] hk_driver.set_characteristics( { @@ -320,12 +321,37 @@ async def test_fan_speed(hass, hk_driver, cls, events): await hass.async_add_executor_job(acc.char_speed.client_update_value, 42) await hass.async_block_till_done() acc.speed_mapping.speed_to_states.assert_called_with(42) + assert acc.char_speed.value == 42 + assert acc.char_active.value == 1 + assert call_set_speed[0] assert call_set_speed[0].data[ATTR_ENTITY_ID] == entity_id assert call_set_speed[0].data[ATTR_SPEED] == "ludicrous" assert len(events) == 1 assert events[-1].data[ATTR_VALUE] == "ludicrous" + # Verify speed is preserved from off to on + hass.states.async_set(entity_id, STATE_OFF, {ATTR_SPEED: SPEED_OFF}) + await hass.async_block_till_done() + assert acc.char_speed.value == 42 + assert acc.char_active.value == 0 + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_active_iid, + HAP_REPR_VALUE: 1, + }, + ] + }, + "mock_addr", + ) + await hass.async_block_till_done() + assert acc.char_speed.value == 42 + assert acc.char_active.value == 1 + async def test_fan_set_all_one_shot(hass, hk_driver, cls, events): """Test fan with speed.""" From 435a88636a2a13c777ed7e831b198ade3c7a77f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 30 Apr 2020 22:37:58 +0300 Subject: [PATCH 07/58] Address new issues flagged by flake8 3.8.0a2 (#34964) --- homeassistant/components/discord/notify.py | 1 + homeassistant/components/group/light.py | 2 +- .../components/streamlabswater/__init__.py | 4 +++- homeassistant/components/wunderlist/__init__.py | 2 +- homeassistant/components/zha/core/helpers.py | 2 +- script/scaffold/docs.py | 2 +- .../geo_json_events/test_geo_location.py | 4 ++-- tests/components/huawei_lte/test_config_flow.py | 2 +- .../minecraft_server/test_config_flow.py | 4 ++-- tests/components/ssdp/test_init.py | 4 ++-- tests/components/system_log/test_init.py | 2 +- .../test/alarm_control_panel.py | 4 ++-- .../custom_components/test/cover.py | 16 ++++++++-------- .../custom_components/test/lock.py | 4 ++-- 14 files changed, 28 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index fb36a60eeccb12..11f83d80179a44 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -65,6 +65,7 @@ async def async_send_message(self, message, **kwargs): images.append(image) else: _LOGGER.warning("Image not found: %s", image) + # pylint: disable=unused-variable @discord_bot.event async def on_ready(): diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index eb043177ebaa6b..56408f410b81e3 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -326,7 +326,7 @@ def _mean_int(*args): def _mean_tuple(*args): """Return the mean values along the columns of the supplied values.""" - return tuple(sum(l) / len(l) for l in zip(*args)) + return tuple(sum(x) / len(x) for x in zip(*args)) def _reduce_attribute( diff --git a/homeassistant/components/streamlabswater/__init__.py b/homeassistant/components/streamlabswater/__init__.py index 836bc9b41830dd..336e92358ee667 100644 --- a/homeassistant/components/streamlabswater/__init__.py +++ b/homeassistant/components/streamlabswater/__init__.py @@ -59,7 +59,9 @@ def setup(hass, config): "Streamlabs Water Monitor auto-detected location_id=%s", location_id ) else: - location = next((l for l in locations if location_id == l["locationId"]), None) + location = next( + (loc for loc in locations if location_id == loc["locationId"]), None + ) if location is None: _LOGGER.error("Supplied location_id is invalid") return False diff --git a/homeassistant/components/wunderlist/__init__.py b/homeassistant/components/wunderlist/__init__.py index 4d9ff6e2235743..954088e4b2132d 100644 --- a/homeassistant/components/wunderlist/__init__.py +++ b/homeassistant/components/wunderlist/__init__.py @@ -85,7 +85,7 @@ def create_task(self, call): def _list_by_name(self, name): """Return a list ID by name.""" lists = self._client.get_lists() - tmp = [l for l in lists if l["title"] == name] + tmp = [lst for lst in lists if lst["title"] == name] if tmp: return tmp[0]["id"] return None diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 4441ac9071774a..bb8a202e789c68 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -101,7 +101,7 @@ def mean_int(*args): def mean_tuple(*args): """Return the mean values along the columns of the supplied values.""" - return tuple(sum(l) / len(l) for l in zip(*args)) + return tuple(sum(x) / len(x) for x in zip(*args)) def reduce_attribute( diff --git a/script/scaffold/docs.py b/script/scaffold/docs.py index 8186b857e80958..f4416a7b7e85fe 100644 --- a/script/scaffold/docs.py +++ b/script/scaffold/docs.py @@ -69,7 +69,7 @@ def print_relevant_docs(template: str, info: Info) -> None: print() print( - f"The next step is to look at the files and deal with all areas marked as TODO." + "The next step is to look at the files and deal with all areas marked as TODO." ) if "extra" in data: diff --git a/tests/components/geo_json_events/test_geo_location.py b/tests/components/geo_json_events/test_geo_location.py index e10125f84ac67a..6b7535a8c85182 100644 --- a/tests/components/geo_json_events/test_geo_location.py +++ b/tests/components/geo_json_events/test_geo_location.py @@ -189,8 +189,8 @@ async def test_setup_race_condition(hass): # Set up some mock feed entries for this test. mock_entry_1 = _generate_mock_feed_entry("1234", "Title 1", 15.5, (-31.0, 150.0)) - delete_signal = f"geo_json_events_delete_1234" - update_signal = f"geo_json_events_update_1234" + delete_signal = "geo_json_events_delete_1234" + update_signal = "geo_json_events_update_1234" # Patching 'utcnow' to gain more control over the timed update. utcnow = dt_util.utcnow() diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py index 86de1ad8bd1872..ae1e81847271dd 100644 --- a/tests/components/huawei_lte/test_config_flow.py +++ b/tests/components/huawei_lte/test_config_flow.py @@ -138,7 +138,7 @@ async def test_success(hass, login_requests_mock): login_requests_mock.request( ANY, f"{FIXTURE_USER_INPUT[CONF_URL]}api/user/login", - text=f"OK", + text="OK", ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT diff --git a/tests/components/minecraft_server/test_config_flow.py b/tests/components/minecraft_server/test_config_flow.py index bc49ed08109e68..7db6dc33b5a090 100644 --- a/tests/components/minecraft_server/test_config_flow.py +++ b/tests/components/minecraft_server/test_config_flow.py @@ -68,12 +68,12 @@ def __init__(self): USER_INPUT_PORT_TOO_SMALL = { CONF_NAME: DEFAULT_NAME, - CONF_HOST: f"mc.dummyserver.com:1023", + CONF_HOST: "mc.dummyserver.com:1023", } USER_INPUT_PORT_TOO_LARGE = { CONF_NAME: DEFAULT_NAME, - CONF_HOST: f"mc.dummyserver.com:65536", + CONF_HOST: "mc.dummyserver.com:65536", } SRV_RECORDS = asyncio.Future() diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 62b7c2bbde282e..b6499af5601d5b 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -61,7 +61,7 @@ async def test_scan_not_all_present(hass, aioclient_mock): """Test match fails if some specified attributes are not present.""" aioclient_mock.get( "http://1.1.1.1", - text=f""" + text=""" Paulus @@ -96,7 +96,7 @@ async def test_scan_not_all_match(hass, aioclient_mock): """Test match fails if some specified attribute values differ.""" aioclient_mock.get( "http://1.1.1.1", - text=f""" + text=""" Paulus diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index 92f0ed9fd16f58..6408b1625f5a20 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -139,7 +139,7 @@ async def test_remove_older_logs(hass, hass_client): def log_msg(nr=2): """Log an error at same line.""" - _LOGGER.error(f"error message %s", nr) + _LOGGER.error("error message %s", nr) async def test_dedup_logs(hass, hass_client): diff --git a/tests/testing_config/custom_components/test/alarm_control_panel.py b/tests/testing_config/custom_components/test/alarm_control_panel.py index 5edb1ca2f70523..6535f5aa1f595e 100644 --- a/tests/testing_config/custom_components/test/alarm_control_panel.py +++ b/tests/testing_config/custom_components/test/alarm_control_panel.py @@ -32,12 +32,12 @@ def init(empty=False): if empty else { "arm_code": MockAlarm( - name=f"Alarm arm code", + name="Alarm arm code", code_arm_required=True, unique_id="unique_arm_code", ), "no_arm_code": MockAlarm( - name=f"Alarm no arm code", + name="Alarm no arm code", code_arm_required=False, unique_id="unique_no_arm_code", ), diff --git a/tests/testing_config/custom_components/test/cover.py b/tests/testing_config/custom_components/test/cover.py index f7b0dae15ee19b..095489ce7b432a 100644 --- a/tests/testing_config/custom_components/test/cover.py +++ b/tests/testing_config/custom_components/test/cover.py @@ -29,29 +29,29 @@ def init(empty=False): if empty else [ MockCover( - name=f"Simple cover", + name="Simple cover", is_on=True, - unique_id=f"unique_cover", + unique_id="unique_cover", supports_tilt=False, ), MockCover( - name=f"Set position cover", + name="Set position cover", is_on=True, - unique_id=f"unique_set_pos_cover", + unique_id="unique_set_pos_cover", current_cover_position=50, supports_tilt=False, ), MockCover( - name=f"Set tilt position cover", + name="Set tilt position cover", is_on=True, - unique_id=f"unique_set_pos_tilt_cover", + unique_id="unique_set_pos_tilt_cover", current_cover_tilt_position=50, supports_tilt=True, ), MockCover( - name=f"Tilt cover", + name="Tilt cover", is_on=True, - unique_id=f"unique_tilt_cover", + unique_id="unique_tilt_cover", supports_tilt=True, ), ] diff --git a/tests/testing_config/custom_components/test/lock.py b/tests/testing_config/custom_components/test/lock.py index 4894f00fd759c2..a6ce9e102d582e 100644 --- a/tests/testing_config/custom_components/test/lock.py +++ b/tests/testing_config/custom_components/test/lock.py @@ -19,13 +19,13 @@ def init(empty=False): if empty else { "support_open": MockLock( - name=f"Support open Lock", + name="Support open Lock", is_locked=True, supported_features=SUPPORT_OPEN, unique_id="unique_support_open", ), "no_support_open": MockLock( - name=f"No support open Lock", + name="No support open Lock", is_locked=True, supported_features=0, unique_id="unique_no_support_open", From d4b66717a42dea0a10ddbd39846ee613e191c781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Gabriel?= Date: Thu, 30 Apr 2020 17:04:53 -0300 Subject: [PATCH 08/58] Remove panasonic_viera from legacy discovery (#34909) --- homeassistant/components/discovery/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index b9b3f51f60d3bc..227995db971c8f 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -66,7 +66,6 @@ SERVICE_OCTOPRINT: ("octoprint", None), SERVICE_FREEBOX: ("freebox", None), SERVICE_YEELIGHT: ("yeelight", None), - "panasonic_viera": ("media_player", "panasonic_viera"), "yamaha": ("media_player", "yamaha"), "logitech_mediaserver": ("media_player", "squeezebox"), "denonavr": ("media_player", "denonavr"), From 6b16c34fd0ba89743ad3caf04fac1a6d62ce121a Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Thu, 30 Apr 2020 22:07:07 +0200 Subject: [PATCH 09/58] Improve logging for unregistered webhooks (#34882) * Improve logging for unregistered webhooks * Address comment, KEY_REAL_IP * Address comment, read(64) * Lint --- homeassistant/components/webhook/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index 84ebdaddad07bf..04332166c60a8e 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -7,6 +7,7 @@ from homeassistant.components import websocket_api from homeassistant.components.http.view import HomeAssistantView +from homeassistant.components.http.const import KEY_REAL_IP from homeassistant.const import HTTP_OK from homeassistant.core import callback from homeassistant.loader import bind_hass @@ -71,7 +72,14 @@ async def async_handle_webhook(hass, webhook_id, request): # Always respond successfully to not give away if a hook exists or not. if webhook is None: - _LOGGER.warning("Received message for unregistered webhook %s", webhook_id) + peer_ip = request[KEY_REAL_IP] + _LOGGER.warning( + "Received message for unregistered webhook %s from %s", webhook_id, peer_ip + ) + # Look at content to provide some context for received webhook + # Limit to 64 chars to avoid flooding the log + content = await request.content.read(64) + _LOGGER.debug("%s...", content) return Response(status=HTTP_OK) try: From ec47216388059166c925991cdc6606e6ce8d8c31 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Apr 2020 13:29:50 -0700 Subject: [PATCH 10/58] Use built-in test helpers on 3.8 (#34901) --- .../components/hassio/addon_panel.py | 9 +- homeassistant/components/homekit/__init__.py | 2 +- .../components/homekit/accessories.py | 2 +- homeassistant/components/smhi/weather.py | 19 +- homeassistant/components/vera/__init__.py | 3 +- .../config_flow/tests/test_config_flow.py | 4 +- .../tests/test_config_flow.py | 4 +- tests/async_mock.py | 8 + tests/auth/providers/test_command_line.py | 5 +- tests/auth/providers/test_homeassistant.py | 3 +- tests/auth/providers/test_insecure_example.py | 5 +- tests/auth/test_auth_store.py | 12 +- tests/common.py | 59 +++-- tests/components/adguard/test_config_flow.py | 16 +- tests/components/airly/test_config_flow.py | 2 +- .../components/airvisual/test_config_flow.py | 2 +- tests/components/almond/test_config_flow.py | 7 +- .../ambiclimate/test_config_flow.py | 17 +- tests/components/arcam_fmj/conftest.py | 3 +- .../components/arcam_fmj/test_media_player.py | 3 +- tests/components/asuswrt/test_sensor.py | 10 +- tests/components/atag/test_config_flow.py | 2 +- tests/components/august/mocks.py | 24 +- tests/components/august/test_camera.py | 5 +- tests/components/august/test_config_flow.py | 3 +- tests/components/august/test_gateway.py | 11 +- tests/components/august/test_init.py | 2 +- tests/components/auth/test_indieauth.py | 11 +- .../automatic/test_device_tracker.py | 3 +- .../automation/test_homeassistant.py | 8 +- tests/components/awair/test_sensor.py | 3 +- tests/components/aws/test_init.py | 18 +- tests/components/axis/test_config_flow.py | 3 +- tests/components/axis/test_device.py | 2 +- tests/components/axis/test_init.py | 5 +- tests/components/braviatv/test_config_flow.py | 3 +- tests/components/brother/__init__.py | 3 +- tests/components/brother/test_config_flow.py | 2 +- tests/components/brother/test_init.py | 3 +- tests/components/brother/test_sensor.py | 3 +- tests/components/caldav/test_calendar.py | 3 +- tests/components/camera/test_init.py | 2 +- tests/components/cast/test_media_player.py | 2 +- .../cert_expiry/test_config_flow.py | 3 +- tests/components/cert_expiry/test_init.py | 3 +- tests/components/cert_expiry/test_sensors.py | 3 +- tests/components/cloud/test_account_link.py | 4 +- tests/components/cloud/test_alexa_config.py | 16 +- tests/components/cloud/test_binary_sensor.py | 4 +- tests/components/cloud/test_client.py | 3 +- tests/components/cloud/test_google_config.py | 9 +- tests/components/cloud/test_http_api.py | 39 ++-- tests/components/config/test_automation.py | 4 +- tests/components/config/test_core.py | 3 +- tests/components/config/test_customize.py | 4 +- tests/components/config/test_group.py | 6 +- tests/components/config/test_init.py | 14 +- tests/components/config/test_scene.py | 4 +- tests/components/conftest.py | 3 +- .../components/coolmaster/test_config_flow.py | 4 +- tests/components/coronavirus/conftest.py | 3 +- tests/components/deconz/test_climate.py | 4 +- tests/components/deconz/test_cover.py | 4 +- tests/components/deconz/test_deconz_event.py | 31 +-- tests/components/deconz/test_gateway.py | 2 +- tests/components/deconz/test_init.py | 4 +- tests/components/deconz/test_light.py | 4 +- tests/components/deconz/test_scene.py | 4 +- tests/components/deconz/test_services.py | 3 +- tests/components/deconz/test_switch.py | 4 +- tests/components/demo/test_media_player.py | 2 +- .../device_sun_light_trigger/test_init.py | 2 +- tests/components/device_tracker/test_init.py | 2 +- tests/components/directv/test_config_flow.py | 2 +- tests/components/directv/test_media_player.py | 2 +- tests/components/directv/test_remote.py | 3 +- tests/components/discovery/test_init.py | 2 +- tests/components/doorbird/test_config_flow.py | 3 +- tests/components/dsmr/test_sensor.py | 8 +- tests/components/dynalite/common.py | 5 +- tests/components/dynalite/test_bridge.py | 9 +- tests/components/dynalite/test_config_flow.py | 4 +- tests/components/dynalite/test_init.py | 3 +- tests/components/dyson/test_air_quality.py | 23 +- tests/components/dyson/test_climate.py | 6 +- tests/components/dyson/test_fan.py | 58 ++--- tests/components/dyson/test_sensor.py | 6 +- .../ee_brightbox/test_device_tracker.py | 3 +- tests/components/elkm1/test_config_flow.py | 6 +- tests/components/esphome/test_config_flow.py | 18 +- tests/components/flume/test_config_flow.py | 3 +- .../components/flunearyou/test_config_flow.py | 2 +- tests/components/flux/test_switch.py | 2 +- tests/components/freebox/test_config_flow.py | 12 +- tests/components/frontend/test_init.py | 2 +- tests/components/gdacs/test_config_flow.py | 3 +- tests/components/gdacs/test_geo_location.py | 3 +- tests/components/gdacs/test_init.py | 4 +- tests/components/gdacs/test_sensor.py | 3 +- .../generic_thermostat/test_climate.py | 14 +- .../geo_json_events/test_geo_location.py | 3 +- .../geonetnz_quakes/test_geo_location.py | 3 +- tests/components/geonetnz_quakes/test_init.py | 4 +- .../components/geonetnz_quakes/test_sensor.py | 3 +- .../components/geonetnz_volcano/test_init.py | 6 +- .../geonetnz_volcano/test_sensor.py | 7 +- tests/components/gios/test_config_flow.py | 3 +- tests/components/google_assistant/__init__.py | 4 +- .../google_assistant/test_helpers.py | 2 +- .../components/google_assistant/test_http.py | 4 +- .../google_assistant/test_report_state.py | 15 +- .../google_assistant/test_smart_home.py | 2 +- .../components/google_assistant/test_trait.py | 2 +- tests/components/griddy/test_config_flow.py | 4 +- tests/components/griddy/test_sensor.py | 2 +- tests/components/group/test_light.py | 6 +- tests/components/harmony/test_config_flow.py | 8 +- tests/components/hassio/conftest.py | 9 +- tests/components/hassio/test_addon_panel.py | 6 +- tests/components/hassio/test_auth.py | 11 +- tests/components/hassio/test_discovery.py | 12 +- tests/components/hassio/test_init.py | 3 +- tests/components/heos/conftest.py | 2 +- tests/components/heos/test_config_flow.py | 3 +- tests/components/heos/test_init.py | 3 +- tests/components/hisense_aehw4a1/test_init.py | 3 +- tests/components/homeassistant/test_init.py | 2 +- tests/components/homekit/test_aidmanager.py | 2 +- tests/components/homekit/test_homekit.py | 4 +- tests/components/homekit/test_init.py | 4 +- .../components/homekit_controller/conftest.py | 5 +- .../homekit_controller/test_config_flow.py | 14 +- .../components/homematicip_cloud/conftest.py | 6 +- tests/components/homematicip_cloud/helper.py | 2 +- .../homematicip_cloud/test_config_flow.py | 3 +- .../homematicip_cloud/test_device.py | 3 +- .../components/homematicip_cloud/test_hap.py | 3 +- .../components/homematicip_cloud/test_init.py | 14 +- tests/components/http/test_ban.py | 3 +- tests/components/hue/test_bridge.py | 7 +- tests/components/hue/test_config_flow.py | 12 +- tests/components/hue/test_init.py | 15 +- .../test_config_flow.py | 9 +- .../components/image_processing/test_init.py | 3 +- tests/components/ipma/test_config_flow.py | 17 +- tests/components/ipma/test_weather.py | 3 +- tests/components/ipp/test_sensor.py | 3 +- tests/components/izone/test_config_flow.py | 3 +- .../components/konnected/test_config_flow.py | 2 +- tests/components/konnected/test_init.py | 2 +- tests/components/konnected/test_panel.py | 2 +- tests/components/logbook/test_init.py | 2 +- .../logi_circle/test_config_flow.py | 5 +- tests/components/lovelace/test_resources.py | 4 +- .../components/luftdaten/test_config_flow.py | 3 +- tests/components/media_player/test_init.py | 6 +- tests/components/melcloud/test_config_flow.py | 2 +- tests/components/met/conftest.py | 6 +- tests/components/met/test_config_flow.py | 2 +- tests/components/microsoft_face/test_init.py | 3 +- tests/components/mikrotik/test_hub.py | 2 +- tests/components/mikrotik/test_init.py | 9 +- .../minecraft_server/test_config_flow.py | 2 +- tests/components/minio/test_minio.py | 2 +- .../components/monoprice/test_config_flow.py | 2 +- .../components/monoprice/test_media_player.py | 2 +- tests/components/mqtt/test_device_tracker.py | 2 +- tests/components/mqtt/test_discovery.py | 8 +- tests/components/mqtt/test_init.py | 6 +- tests/components/mqtt/test_light.py | 3 +- tests/components/mqtt/test_light_json.py | 3 +- tests/components/mqtt/test_light_template.py | 3 +- tests/components/mqtt/test_server.py | 14 +- tests/components/mqtt/test_switch.py | 6 +- .../mqtt_json/test_device_tracker.py | 2 +- tests/components/myq/test_config_flow.py | 2 +- tests/components/myq/util.py | 3 +- tests/components/mythicbeastsdns/test_init.py | 10 +- tests/components/ness_alarm/test_init.py | 3 +- tests/components/nest/test_config_flow.py | 15 +- tests/components/netatmo/test_config_flow.py | 3 +- tests/components/nexia/test_config_flow.py | 4 +- tests/components/nexia/util.py | 2 +- tests/components/notion/test_config_flow.py | 14 +- .../test_geo_location.py | 2 +- tests/components/nuheat/mocks.py | 3 +- tests/components/nuheat/test_climate.py | 4 +- tests/components/nuheat/test_config_flow.py | 3 +- tests/components/nut/test_config_flow.py | 3 +- tests/components/nut/util.py | 3 +- tests/components/nws/conftest.py | 12 +- tests/components/nws/test_config_flow.py | 3 +- .../openalpr_cloud/test_image_processing.py | 16 +- .../openalpr_local/test_image_processing.py | 5 +- .../opentherm_gw/test_config_flow.py | 37 ++- .../components/owntracks/test_config_flow.py | 8 +- .../owntracks/test_device_tracker.py | 2 +- .../panasonic_viera/test_config_flow.py | 2 +- tests/components/person/test_init.py | 2 +- tests/components/pi_hole/test_init.py | 15 +- tests/components/plex/test_config_flow.py | 2 +- tests/components/plex/test_init.py | 170 +++++++------- tests/components/plex/test_server.py | 214 +++++++++--------- tests/components/point/test_config_flow.py | 7 +- tests/components/powerwall/mocks.py | 2 +- .../powerwall/test_binary_sensor.py | 4 +- .../components/powerwall/test_config_flow.py | 3 +- tests/components/powerwall/test_sensor.py | 4 +- tests/components/ps4/conftest.py | 3 +- tests/components/ps4/test_config_flow.py | 2 +- tests/components/ps4/test_init.py | 3 +- tests/components/ps4/test_media_player.py | 2 +- tests/components/ptvsd/test_ptvsd.py | 7 +- tests/components/qwikswitch/test_init.py | 2 +- tests/components/rachio/test_config_flow.py | 4 +- .../rainmachine/test_config_flow.py | 12 +- tests/components/ring/test_config_flow.py | 8 +- tests/components/rmvtransport/test_sensor.py | 4 +- tests/components/roku/test_config_flow.py | 2 +- tests/components/roku/test_init.py | 2 +- tests/components/roku/test_media_player.py | 2 +- tests/components/roomba/test_config_flow.py | 2 +- .../components/samsungtv/test_config_flow.py | 23 +- tests/components/samsungtv/test_init.py | 10 +- .../components/samsungtv/test_media_player.py | 47 ++-- tests/components/sense/test_config_flow.py | 3 +- tests/components/sentry/test_config_flow.py | 10 +- tests/components/shell_command/test_init.py | 4 +- tests/components/shopping_list/conftest.py | 2 +- .../components/simplisafe/test_config_flow.py | 2 +- tests/components/smartthings/conftest.py | 2 +- .../smartthings/test_config_flow.py | 12 +- tests/components/smartthings/test_init.py | 2 +- tests/components/smartthings/test_smartapp.py | 6 +- tests/components/smhi/test_config_flow.py | 37 ++- tests/components/smhi/test_weather.py | 23 +- tests/components/solarlog/test_config_flow.py | 13 +- tests/components/sonos/conftest.py | 2 +- .../soundtouch/test_media_player.py | 3 +- tests/components/stream/test_init.py | 4 +- tests/components/switcher_kis/conftest.py | 9 +- tests/components/tado/test_config_flow.py | 2 +- tests/components/tesla/test_config_flow.py | 14 +- tests/components/tradfri/conftest.py | 3 +- tests/components/tradfri/test_config_flow.py | 2 +- tests/components/tradfri/test_init.py | 3 +- tests/components/uk_transport/test_sensor.py | 2 +- tests/components/unifi/conftest.py | 3 +- tests/components/unifi/test_config_flow.py | 2 +- tests/components/unifi/test_controller.py | 2 +- tests/components/unifi/test_device_tracker.py | 3 +- tests/components/unifi/test_init.py | 5 +- .../unifi_direct/test_device_tracker.py | 6 +- tests/components/updater/test_init.py | 16 +- tests/components/upnp/test_init.py | 11 +- tests/components/vera/test_init.py | 2 +- tests/components/vilfo/test_config_flow.py | 5 +- tests/components/vizio/conftest.py | 3 +- tests/components/vizio/test_media_player.py | 2 +- tests/components/webostv/test_media_player.py | 2 +- tests/components/websocket_api/test_http.py | 2 +- tests/components/withings/test_common.py | 3 +- tests/components/withings/test_init.py | 3 +- tests/components/wled/test_light.py | 2 +- tests/components/wled/test_sensor.py | 2 +- tests/components/wled/test_switch.py | 2 +- tests/components/wwlln/test_config_flow.py | 3 +- .../components/xiaomi/test_device_tracker.py | 11 +- .../xiaomi_miio/test_config_flow.py | 3 +- tests/components/zeroconf/test_init.py | 3 +- tests/components/zha/common.py | 19 +- tests/components/zha/conftest.py | 12 +- tests/components/zha/test_channels.py | 17 +- tests/components/zha/test_config_flow.py | 27 ++- tests/components/zha/test_cover.py | 6 +- tests/components/zha/test_device.py | 8 +- tests/components/zha/test_discover.py | 9 +- tests/components/zha/test_light.py | 10 +- tests/components/zone/test_init.py | 2 +- tests/components/zwave/test_init.py | 14 +- tests/conftest.py | 3 +- tests/helpers/test_area_registry.py | 4 +- tests/helpers/test_config_entry_flow.py | 2 +- tests/helpers/test_debounce.py | 8 +- tests/helpers/test_device_registry.py | 4 +- tests/helpers/test_entity_component.py | 23 +- tests/helpers/test_entity_platform.py | 7 +- tests/helpers/test_entity_registry.py | 4 +- tests/helpers/test_restore_state.py | 8 +- tests/helpers/test_script.py | 4 +- tests/helpers/test_service.py | 12 +- tests/helpers/test_storage.py | 2 +- tests/helpers/test_translation.py | 34 +-- tests/helpers/test_update_coordinator.py | 6 +- tests/test_bootstrap.py | 2 +- tests/test_config.py | 46 ++-- tests/test_config_entries.py | 109 +++++---- tests/test_core.py | 2 +- tests/test_loader.py | 2 +- tests/test_requirements.py | 79 +++---- tests/test_setup.py | 2 +- tests/util/test_location.py | 2 +- tests/util/test_package.py | 7 +- 303 files changed, 1202 insertions(+), 1359 deletions(-) create mode 100644 tests/async_mock.py diff --git a/homeassistant/components/hassio/addon_panel.py b/homeassistant/components/hassio/addon_panel.py index d8616a825789ef..9e44b961a1c534 100644 --- a/homeassistant/components/hassio/addon_panel.py +++ b/homeassistant/components/hassio/addon_panel.py @@ -75,12 +75,9 @@ async def get_panels(self): return {} -def _register_panel(hass, addon, data): - """Init coroutine to register the panel. - - Return coroutine. - """ - return hass.components.panel_custom.async_register_panel( +async def _register_panel(hass, addon, data): + """Init coroutine to register the panel.""" + await hass.components.panel_custom.async_register_panel( frontend_url_path=addon, webcomponent_name="hassio-main", sidebar_title=data[ATTR_TITLE], diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 35620e9e11d15e..479e1e4dab95f5 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -484,7 +484,7 @@ def _start(self, bridged_states): ) _LOGGER.debug("Driver start") - self.hass.async_add_executor_job(self.driver.start) + self.hass.add_job(self.driver.start) self.status = STATUS_RUNNING async def async_stop(self, *args): diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 4f0a840770c906..ab3d1ae85071c4 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -165,7 +165,7 @@ async def run_handler(self): Run inside the Home Assistant event loop. """ state = self.hass.states.get(self.entity_id) - self.hass.async_add_executor_job(self.update_state_callback, None, None, state) + self.hass.async_add_job(self.update_state_callback, None, None, state) async_track_state_change(self.hass, self.entity_id, self.update_state_callback) battery_charging_state = None diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index 0c5450b5ddd691..f09491bf611c1e 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -99,15 +99,6 @@ def unique_id(self) -> str: @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self) -> None: """Refresh the forecast data from SMHI weather API.""" - - def fail(): - """Postpone updates.""" - self._fail_count += 1 - if self._fail_count < 3: - self.hass.helpers.event.async_call_later( - RETRY_TIMEOUT, self.retry_update() - ) - try: with async_timeout.timeout(10): self._forecasts = await self.get_weather_forecast() @@ -115,11 +106,15 @@ def fail(): except (asyncio.TimeoutError, SmhiForecastException): _LOGGER.error("Failed to connect to SMHI API, retry in 5 minutes") - fail() + self._fail_count += 1 + if self._fail_count < 3: + self.hass.helpers.event.async_call_later( + RETRY_TIMEOUT, self.retry_update + ) - async def retry_update(self): + async def retry_update(self, _): """Retry refresh weather forecast.""" - self.async_update() + await self.async_update() async def get_weather_forecast(self) -> []: """Return the current forecasts from SMHI API.""" diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index c98833a7daa014..9b2d44fd94beda 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -90,8 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b controller.start() hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, - lambda event: hass.async_add_executor_job(controller.stop), + EVENT_HOMEASSISTANT_STOP, lambda event: controller.stop() ) try: diff --git a/script/scaffold/templates/config_flow/tests/test_config_flow.py b/script/scaffold/templates/config_flow/tests/test_config_flow.py index 3d829b5cc3226f..d6dee8d0bd0bf0 100644 --- a/script/scaffold/templates/config_flow/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow/tests/test_config_flow.py @@ -1,10 +1,10 @@ """Test the NEW_NAME config flow.""" -from asynctest import patch - from homeassistant import config_entries, setup from homeassistant.components.NEW_DOMAIN.config_flow import CannotConnect, InvalidAuth from homeassistant.components.NEW_DOMAIN.const import DOMAIN +from tests.async_mock import patch + async def test_form(hass): """Test we get the form.""" diff --git a/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py b/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py index 8a543a04af3074..7dda65645070f3 100644 --- a/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py @@ -1,6 +1,4 @@ """Test the NEW_NAME config flow.""" -from asynctest import patch - from homeassistant import config_entries, setup from homeassistant.components.NEW_DOMAIN.const import ( DOMAIN, @@ -9,6 +7,8 @@ ) from homeassistant.helpers import config_entry_oauth2_flow +from tests.async_mock import patch + CLIENT_ID = "1234" CLIENT_SECRET = "5678" diff --git a/tests/async_mock.py b/tests/async_mock.py new file mode 100644 index 00000000000000..1942b2ca284b8a --- /dev/null +++ b/tests/async_mock.py @@ -0,0 +1,8 @@ +"""Mock utilities that are async aware.""" +import sys + +if sys.version_info[:2] < (3, 8): + from asynctest.mock import * # noqa + from asynctest.mock import CoroutineMock as AsyncMock # noqa +else: + from unittest.mock import * # noqa diff --git a/tests/auth/providers/test_command_line.py b/tests/auth/providers/test_command_line.py index abcf124b9c4686..3915950cedb330 100644 --- a/tests/auth/providers/test_command_line.py +++ b/tests/auth/providers/test_command_line.py @@ -1,7 +1,6 @@ """Tests for the command_line auth provider.""" import os -from unittest.mock import Mock import uuid import pytest @@ -11,7 +10,7 @@ from homeassistant.auth.providers import command_line from homeassistant.const import CONF_TYPE -from tests.common import mock_coro +from tests.async_mock import AsyncMock @pytest.fixture @@ -63,7 +62,7 @@ async def test_match_existing_credentials(store, provider): data={"username": "good-user"}, is_new=False, ) - provider.async_credentials = Mock(return_value=mock_coro([existing])) + provider.async_credentials = AsyncMock(return_value=[existing]) credentials = await provider.async_get_or_create_credentials( {"username": "good-user", "password": "irrelevant"} ) diff --git a/tests/auth/providers/test_homeassistant.py b/tests/auth/providers/test_homeassistant.py index 9cfdfc30aa5c70..f9ac8fbb92a30b 100644 --- a/tests/auth/providers/test_homeassistant.py +++ b/tests/auth/providers/test_homeassistant.py @@ -1,7 +1,6 @@ """Test the Home Assistant local auth provider.""" import asyncio -from asynctest import Mock, patch import pytest import voluptuous as vol @@ -12,6 +11,8 @@ homeassistant as hass_auth, ) +from tests.async_mock import Mock, patch + @pytest.fixture def data(hass): diff --git a/tests/auth/providers/test_insecure_example.py b/tests/auth/providers/test_insecure_example.py index c5b3a8db038395..c2b16cbafabab8 100644 --- a/tests/auth/providers/test_insecure_example.py +++ b/tests/auth/providers/test_insecure_example.py @@ -1,5 +1,4 @@ """Tests for the insecure example auth provider.""" -from unittest.mock import Mock import uuid import pytest @@ -7,7 +6,7 @@ from homeassistant.auth import AuthManager, auth_store, models as auth_models from homeassistant.auth.providers import insecure_example -from tests.common import mock_coro +from tests.async_mock import AsyncMock @pytest.fixture @@ -63,7 +62,7 @@ async def test_match_existing_credentials(store, provider): data={"username": "user-test"}, is_new=False, ) - provider.async_credentials = Mock(return_value=mock_coro([existing])) + provider.async_credentials = AsyncMock(return_value=[existing]) credentials = await provider.async_get_or_create_credentials( {"username": "user-test", "password": "password-test"} ) diff --git a/tests/auth/test_auth_store.py b/tests/auth/test_auth_store.py index 109eb20fb6c37d..78ab9829ab634c 100644 --- a/tests/auth/test_auth_store.py +++ b/tests/auth/test_auth_store.py @@ -1,10 +1,10 @@ """Tests for the auth store.""" import asyncio -import asynctest - from homeassistant.auth import auth_store +from tests.async_mock import patch + async def test_loading_no_group_data_format(hass, hass_storage): """Test we correctly load old data without any groups.""" @@ -229,12 +229,12 @@ async def test_system_groups_store_id_and_name(hass, hass_storage): async def test_loading_race_condition(hass): """Test only one storage load called when concurrent loading occurred .""" store = auth_store.AuthStore(hass) - with asynctest.patch( + with patch( "homeassistant.helpers.entity_registry.async_get_registry" - ) as mock_ent_registry, asynctest.patch( + ) as mock_ent_registry, patch( "homeassistant.helpers.device_registry.async_get_registry" - ) as mock_dev_registry, asynctest.patch( - "homeassistant.helpers.storage.Store.async_load" + ) as mock_dev_registry, patch( + "homeassistant.helpers.storage.Store.async_load", return_value=None ) as mock_load: results = await asyncio.gather(store.async_get_users(), store.async_get_users()) diff --git a/tests/common.py b/tests/common.py index 5a8250e5686c1a..b76fa23bbf2e47 100644 --- a/tests/common.py +++ b/tests/common.py @@ -14,7 +14,6 @@ import uuid from aiohttp.test_utils import unused_port as get_test_instance_port # noqa -from asynctest import MagicMock, Mock, patch from homeassistant import auth, config_entries, core as ha, loader from homeassistant.auth import ( @@ -60,6 +59,8 @@ from homeassistant.util.unit_system import METRIC_SYSTEM import homeassistant.util.yaml.loader as yaml_loader +from tests.async_mock import AsyncMock, MagicMock, Mock, patch + _LOGGER = logging.getLogger(__name__) INSTANCES = [] CLIENT_ID = "https://example.com/app" @@ -159,20 +160,37 @@ async def async_test_home_assistant(loop): def async_add_job(target, *args): """Add job.""" - if isinstance(target, Mock): - return mock_coro(target(*args)) + check_target = target + while isinstance(check_target, ft.partial): + check_target = check_target.func + + if isinstance(check_target, Mock) and not isinstance(target, AsyncMock): + fut = asyncio.Future() + fut.set_result(target(*args)) + return fut + return orig_async_add_job(target, *args) def async_add_executor_job(target, *args): """Add executor job.""" - if isinstance(target, Mock): - return mock_coro(target(*args)) + check_target = target + while isinstance(check_target, ft.partial): + check_target = check_target.func + + if isinstance(check_target, Mock): + fut = asyncio.Future() + fut.set_result(target(*args)) + return fut + return orig_async_add_executor_job(target, *args) def async_create_task(coroutine): """Create task.""" - if isinstance(coroutine, Mock): - return mock_coro() + if isinstance(coroutine, Mock) and not isinstance(coroutine, AsyncMock): + fut = asyncio.Future() + fut.set_result(None) + return fut + return orig_async_create_task(coroutine) hass.async_add_job = async_add_job @@ -311,15 +329,16 @@ async def async_mock_mqtt_component(hass, config=None): if config is None: config = {mqtt.CONF_BROKER: "mock-broker"} - async def _async_fire_mqtt_message(topic, payload, qos, retain): + @ha.callback + def _async_fire_mqtt_message(topic, payload, qos, retain): async_fire_mqtt_message(hass, topic, payload, qos, retain) with patch("paho.mqtt.client.Client") as mock_client: - mock_client().connect.return_value = 0 - mock_client().subscribe.return_value = (0, 0) - mock_client().unsubscribe.return_value = (0, 0) - mock_client().publish.return_value = (0, 0) - mock_client().publish.side_effect = _async_fire_mqtt_message + mock_client = mock_client.return_value + mock_client.connect.return_value = 0 + mock_client.subscribe.return_value = (0, 0) + mock_client.unsubscribe.return_value = (0, 0) + mock_client.publish.side_effect = _async_fire_mqtt_message result = await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) assert result @@ -503,7 +522,7 @@ def __init__( self.async_setup = async_setup if setup is None and async_setup is None: - self.async_setup = mock_coro_func(True) + self.async_setup = AsyncMock(return_value=True) if async_setup_entry is not None: self.async_setup_entry = async_setup_entry @@ -561,7 +580,7 @@ def __init__( self.async_setup_entry = async_setup_entry if setup_platform is None and async_setup_platform is None: - self.async_setup_platform = mock_coro_func() + self.async_setup_platform = AsyncMock(return_value=None) class MockEntityPlatform(entity_platform.EntityPlatform): @@ -731,14 +750,10 @@ def mock_coro(return_value=None, exception=None): def mock_coro_func(return_value=None, exception=None): """Return a method to create a coro function that returns a value.""" - @asyncio.coroutine - def coro(*args, **kwargs): - """Fake coroutine.""" - if exception: - raise exception - return return_value + if exception: + return AsyncMock(side_effect=exception) - return coro + return AsyncMock(return_value=return_value) @contextmanager diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index a0d575deac0468..d0e874bacdc19e 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -1,5 +1,4 @@ """Tests for the AdGuard Home config flow.""" -from unittest.mock import patch import aiohttp @@ -15,7 +14,8 @@ CONF_VERIFY_SSL, ) -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import patch +from tests.common import MockConfigEntry FIXTURE_USER_INPUT = { CONF_HOST: "127.0.0.1", @@ -156,22 +156,16 @@ async def test_hassio_update_instance_running(hass, aioclient_mock): entry.add_to_hass(hass) with patch.object( - hass.config_entries, - "async_forward_entry_setup", - side_effect=lambda *_: mock_coro(True), + hass.config_entries, "async_forward_entry_setup", return_value=True, ) as mock_load: assert await hass.config_entries.async_setup(entry.entry_id) assert entry.state == config_entries.ENTRY_STATE_LOADED assert len(mock_load.mock_calls) == 2 with patch.object( - hass.config_entries, - "async_forward_entry_unload", - side_effect=lambda *_: mock_coro(True), + hass.config_entries, "async_forward_entry_unload", return_value=True, ) as mock_unload, patch.object( - hass.config_entries, - "async_forward_entry_setup", - side_effect=lambda *_: mock_coro(True), + hass.config_entries, "async_forward_entry_setup", return_value=True, ) as mock_load: result = await hass.config_entries.flow.async_init( "adguard", diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py index 83e7d5d210ecb4..243a92258eb00f 100644 --- a/tests/components/airly/test_config_flow.py +++ b/tests/components/airly/test_config_flow.py @@ -2,7 +2,6 @@ import json from airly.exceptions import AirlyError -from asynctest import patch from homeassistant import data_entry_flow from homeassistant.components.airly.const import DOMAIN @@ -15,6 +14,7 @@ HTTP_FORBIDDEN, ) +from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture CONFIG = { diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index 9127e6b2780764..fcb2360b6c6e01 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -1,5 +1,4 @@ """Define tests for the AirVisual config flow.""" -from asynctest import patch from pyairvisual.errors import InvalidKeyError, NodeProError from homeassistant import data_entry_flow @@ -21,6 +20,7 @@ ) from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/almond/test_config_flow.py b/tests/components/almond/test_config_flow.py index 0b4869ee2a6f4c..d1403c017aab50 100644 --- a/tests/components/almond/test_config_flow.py +++ b/tests/components/almond/test_config_flow.py @@ -1,14 +1,13 @@ """Test the Almond config flow.""" import asyncio -from asynctest import patch - from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.almond import config_flow from homeassistant.components.almond.const import DOMAIN from homeassistant.helpers import config_entry_oauth2_flow -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import patch +from tests.common import MockConfigEntry CLIENT_ID_VALUE = "1234" CLIENT_SECRET_VALUE = "5678" @@ -16,7 +15,7 @@ async def test_import(hass): """Test that we can import a config entry.""" - with patch("pyalmond.WebAlmondAPI.async_list_apps", side_effect=mock_coro): + with patch("pyalmond.WebAlmondAPI.async_list_apps"): assert await setup.async_setup_component( hass, "almond", diff --git a/tests/components/ambiclimate/test_config_flow.py b/tests/components/ambiclimate/test_config_flow.py index 045eac1328b0b6..6dee15c27f9683 100644 --- a/tests/components/ambiclimate/test_config_flow.py +++ b/tests/components/ambiclimate/test_config_flow.py @@ -1,13 +1,12 @@ """Tests for the Ambiclimate config flow.""" import ambiclimate -from asynctest import Mock, patch from homeassistant import data_entry_flow from homeassistant.components.ambiclimate import config_flow from homeassistant.setup import async_setup_component from homeassistant.util import aiohttp -from tests.common import mock_coro +from tests.async_mock import AsyncMock, patch async def init_config_flow(hass): @@ -67,9 +66,7 @@ async def test_full_flow_implementation(hass): assert "response_type=code" in url assert "redirect_uri=https%3A%2F%2Fhass.com%2Fapi%2Fambiclimate" in url - with patch( - "ambiclimate.AmbiclimateOAuth.get_access_token", return_value=mock_coro("test") - ): + with patch("ambiclimate.AmbiclimateOAuth.get_access_token", return_value="test"): result = await flow.async_step_code("123ABC") assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "Ambiclimate" @@ -77,9 +74,7 @@ async def test_full_flow_implementation(hass): assert result["data"]["client_secret"] == "secret" assert result["data"]["client_id"] == "id" - with patch( - "ambiclimate.AmbiclimateOAuth.get_access_token", return_value=mock_coro(None) - ): + with patch("ambiclimate.AmbiclimateOAuth.get_access_token", return_value=None): result = await flow.async_step_code("123ABC") assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -96,9 +91,7 @@ async def test_abort_invalid_code(hass): config_flow.register_flow_implementation(hass, None, None) flow = await init_config_flow(hass) - with patch( - "ambiclimate.AmbiclimateOAuth.get_access_token", return_value=mock_coro(None) - ): + with patch("ambiclimate.AmbiclimateOAuth.get_access_token", return_value=None): result = await flow.async_step_code("invalid") assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "access_token" @@ -118,7 +111,7 @@ async def test_already_setup(hass): async def test_view(hass): """Test view.""" - hass.config_entries.flow.async_init = Mock() + hass.config_entries.flow.async_init = AsyncMock() request = aiohttp.MockRequest(b"", query_string="code=test_code") request.app = {"hass": hass} diff --git a/tests/components/arcam_fmj/conftest.py b/tests/components/arcam_fmj/conftest.py index ec9c6bb1f36855..e515b71468bc4f 100644 --- a/tests/components/arcam_fmj/conftest.py +++ b/tests/components/arcam_fmj/conftest.py @@ -1,7 +1,6 @@ """Tests for the arcam_fmj component.""" from arcam.fmj.client import Client from arcam.fmj.state import State -from asynctest import Mock import pytest from homeassistant.components.arcam_fmj import DEVICE_SCHEMA @@ -9,6 +8,8 @@ from homeassistant.components.arcam_fmj.media_player import ArcamFmj from homeassistant.const import CONF_HOST, CONF_PORT +from tests.async_mock import Mock + MOCK_HOST = "127.0.0.1" MOCK_PORT = 1234 MOCK_TURN_ON = { diff --git a/tests/components/arcam_fmj/test_media_player.py b/tests/components/arcam_fmj/test_media_player.py index a6b36a71d1cf4d..5a73e770129eab 100644 --- a/tests/components/arcam_fmj/test_media_player.py +++ b/tests/components/arcam_fmj/test_media_player.py @@ -2,7 +2,6 @@ from math import isclose from arcam.fmj import DecodeMode2CH, DecodeModeMCH, IncomingAudioFormat, SourceCodes -from asynctest.mock import ANY, MagicMock, Mock, PropertyMock, patch import pytest from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC @@ -10,6 +9,8 @@ from .conftest import MOCK_ENTITY_ID, MOCK_HOST, MOCK_NAME, MOCK_PORT +from tests.async_mock import ANY, MagicMock, Mock, PropertyMock, patch + MOCK_TURN_ON = { "service": "switch.turn_on", "data": {"entity_id": "switch.test"}, diff --git a/tests/components/asuswrt/test_sensor.py b/tests/components/asuswrt/test_sensor.py index 991be0bac50f9d..6d58b90928044d 100644 --- a/tests/components/asuswrt/test_sensor.py +++ b/tests/components/asuswrt/test_sensor.py @@ -2,7 +2,6 @@ from datetime import datetime, timedelta from aioasuswrt.asuswrt import Device -from asynctest import CoroutineMock, patch from homeassistant.components import sensor from homeassistant.components.asuswrt import ( @@ -20,6 +19,7 @@ import homeassistant.util.dt as dt_util from homeassistant.util.dt import utcnow +from tests.async_mock import AsyncMock, patch from tests.common import async_fire_time_changed VALID_CONFIG_ROUTER_SSH = { @@ -54,10 +54,10 @@ async def test_sensors(hass: HomeAssistant): """Test creating an AsusWRT sensor.""" with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: - AsusWrt().connection.async_connect = CoroutineMock() - AsusWrt().async_get_connected_devices = CoroutineMock(return_value=MOCK_DEVICES) - AsusWrt().async_get_bytes_total = CoroutineMock(return_value=MOCK_BYTES_TOTAL) - AsusWrt().async_get_current_transfer_rates = CoroutineMock( + AsusWrt().connection.async_connect = AsyncMock() + AsusWrt().async_get_connected_devices = AsyncMock(return_value=MOCK_DEVICES) + AsusWrt().async_get_bytes_total = AsyncMock(return_value=MOCK_BYTES_TOTAL) + AsusWrt().async_get_current_transfer_rates = AsyncMock( return_value=MOCK_CURRENT_TRANSFER_RATES ) diff --git a/tests/components/atag/test_config_flow.py b/tests/components/atag/test_config_flow.py index bda4ccc9023f59..c860b85c240923 100644 --- a/tests/components/atag/test_config_flow.py +++ b/tests/components/atag/test_config_flow.py @@ -1,13 +1,13 @@ """Tests for the Atag config flow.""" from unittest.mock import PropertyMock -from asynctest import patch from pyatag import AtagException from homeassistant import config_entries, data_entry_flow from homeassistant.components.atag import DOMAIN from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_PORT +from tests.async_mock import patch from tests.common import MockConfigEntry FIXTURE_USER_INPUT = { diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index 39b18411d664f7..0e1e9866c1dd6a 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -3,8 +3,6 @@ import os import time -from asynctest import mock -from asynctest.mock import CoroutineMock, MagicMock, PropertyMock from august.activity import ( ACTIVITY_ACTIONS_DOOR_OPERATION, ACTIVITY_ACTIONS_DOORBELL_DING, @@ -29,6 +27,8 @@ ) from homeassistant.setup import async_setup_component +# from tests.async_mock import AsyncMock +from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch from tests.common import load_fixture @@ -43,10 +43,8 @@ def _mock_get_config(): } -@mock.patch("homeassistant.components.august.gateway.ApiAsync") -@mock.patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate" -) +@patch("homeassistant.components.august.gateway.ApiAsync") +@patch("homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate") async def _mock_setup_august(hass, api_instance, authenticate_mock, api_mock): """Set up august integration.""" authenticate_mock.side_effect = MagicMock( @@ -150,37 +148,37 @@ async def _mock_setup_august_with_api_side_effects(hass, api_call_side_effects): api_instance = MagicMock(name="Api") if api_call_side_effects["get_lock_detail"]: - type(api_instance).async_get_lock_detail = CoroutineMock( + type(api_instance).async_get_lock_detail = AsyncMock( side_effect=api_call_side_effects["get_lock_detail"] ) if api_call_side_effects["get_operable_locks"]: - type(api_instance).async_get_operable_locks = CoroutineMock( + type(api_instance).async_get_operable_locks = AsyncMock( side_effect=api_call_side_effects["get_operable_locks"] ) if api_call_side_effects["get_doorbells"]: - type(api_instance).async_get_doorbells = CoroutineMock( + type(api_instance).async_get_doorbells = AsyncMock( side_effect=api_call_side_effects["get_doorbells"] ) if api_call_side_effects["get_doorbell_detail"]: - type(api_instance).async_get_doorbell_detail = CoroutineMock( + type(api_instance).async_get_doorbell_detail = AsyncMock( side_effect=api_call_side_effects["get_doorbell_detail"] ) if api_call_side_effects["get_house_activities"]: - type(api_instance).async_get_house_activities = CoroutineMock( + type(api_instance).async_get_house_activities = AsyncMock( side_effect=api_call_side_effects["get_house_activities"] ) if api_call_side_effects["lock_return_activities"]: - type(api_instance).async_lock_return_activities = CoroutineMock( + type(api_instance).async_lock_return_activities = AsyncMock( side_effect=api_call_side_effects["lock_return_activities"] ) if api_call_side_effects["unlock_return_activities"]: - type(api_instance).async_unlock_return_activities = CoroutineMock( + type(api_instance).async_unlock_return_activities = AsyncMock( side_effect=api_call_side_effects["unlock_return_activities"] ) diff --git a/tests/components/august/test_camera.py b/tests/components/august/test_camera.py index e47bafece42ecc..3ec1b2d608cc49 100644 --- a/tests/components/august/test_camera.py +++ b/tests/components/august/test_camera.py @@ -1,9 +1,8 @@ """The camera tests for the august platform.""" -from asynctest import mock - from homeassistant.const import STATE_IDLE +from tests.async_mock import patch from tests.components.august.mocks import ( _create_august_with_devices, _mock_doorbell_from_fixture, @@ -14,7 +13,7 @@ async def test_create_doorbell(hass, aiohttp_client): """Test creation of a doorbell.""" doorbell_one = await _mock_doorbell_from_fixture(hass, "get_doorbell.json") - with mock.patch.object( + with patch.object( doorbell_one, "async_get_doorbell_image", create=False, return_value="image" ): await _create_august_with_devices(hass, [doorbell_one]) diff --git a/tests/components/august/test_config_flow.py b/tests/components/august/test_config_flow.py index 8d29ba650faff2..ed75bc3685c9d8 100644 --- a/tests/components/august/test_config_flow.py +++ b/tests/components/august/test_config_flow.py @@ -1,5 +1,4 @@ """Test the August config flow.""" -from asynctest import patch from august.authenticator import ValidationResult from homeassistant import config_entries, setup @@ -17,6 +16,8 @@ ) from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME +from tests.async_mock import patch + async def test_form(hass): """Test we get the form.""" diff --git a/tests/components/august/test_gateway.py b/tests/components/august/test_gateway.py index f5fe35b4b19443..b9d1959d18a687 100644 --- a/tests/components/august/test_gateway.py +++ b/tests/components/august/test_gateway.py @@ -1,11 +1,10 @@ """The gateway tests for the august platform.""" from unittest.mock import MagicMock -from asynctest import mock - from homeassistant.components.august.const import DOMAIN from homeassistant.components.august.gateway import AugustGateway +from tests.async_mock import patch from tests.components.august.mocks import _mock_august_authentication, _mock_get_config @@ -14,11 +13,9 @@ async def test_refresh_access_token(hass): await _patched_refresh_access_token(hass, "new_token", 5678) -@mock.patch( - "homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate" -) -@mock.patch("homeassistant.components.august.gateway.AuthenticatorAsync.should_refresh") -@mock.patch( +@patch("homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate") +@patch("homeassistant.components.august.gateway.AuthenticatorAsync.should_refresh") +@patch( "homeassistant.components.august.gateway.AuthenticatorAsync.async_refresh_access_token" ) async def _patched_refresh_access_token( diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index c287a26b34f9cf..f29403c9f21b28 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -1,7 +1,6 @@ """The tests for the august platform.""" import asyncio -from asynctest import patch from august.exceptions import AugustApiAIOHTTPError from homeassistant import setup @@ -27,6 +26,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import MockConfigEntry from tests.components.august.mocks import ( _create_august_with_devices, diff --git a/tests/components/auth/test_indieauth.py b/tests/components/auth/test_indieauth.py index b359144ab97766..3d1ba068c85253 100644 --- a/tests/components/auth/test_indieauth.py +++ b/tests/components/auth/test_indieauth.py @@ -1,12 +1,11 @@ """Tests for the client validator.""" import asyncio -from unittest.mock import patch import pytest from homeassistant.components.auth import indieauth -from tests.common import mock_coro +from tests.async_mock import patch from tests.test_util.aiohttp import AiohttpClientMocker @@ -113,9 +112,7 @@ async def test_verify_redirect_uri(): None, "http://ex.com", "http://ex.com/callback" ) - with patch.object( - indieauth, "fetch_redirect_uris", side_effect=lambda *_: mock_coro([]) - ): + with patch.object(indieauth, "fetch_redirect_uris", return_value=[]): # Different domain assert not await indieauth.verify_redirect_uri( None, "http://ex.com", "http://different.com/callback" @@ -174,9 +171,7 @@ async def test_find_link_tag_max_size(hass, mock_session): ) async def test_verify_redirect_uri_android_ios(client_id): """Test that we verify redirect uri correctly for Android/iOS.""" - with patch.object( - indieauth, "fetch_redirect_uris", side_effect=lambda *_: mock_coro([]) - ): + with patch.object(indieauth, "fetch_redirect_uris", return_value=[]): assert await indieauth.verify_redirect_uri( None, client_id, "homeassistant://auth-callback" ) diff --git a/tests/components/automatic/test_device_tracker.py b/tests/components/automatic/test_device_tracker.py index 09ea7c6185841f..492421a77cc31e 100644 --- a/tests/components/automatic/test_device_tracker.py +++ b/tests/components/automatic/test_device_tracker.py @@ -3,11 +3,12 @@ import logging import aioautomatic -from asynctest import MagicMock, patch from homeassistant.components.automatic.device_tracker import async_setup_scanner from homeassistant.setup import async_setup_component +from tests.async_mock import MagicMock, patch + _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/automation/test_homeassistant.py b/tests/components/automation/test_homeassistant.py index a0985e549763d8..b0db66e16a96bb 100644 --- a/tests/components/automation/test_homeassistant.py +++ b/tests/components/automation/test_homeassistant.py @@ -1,11 +1,12 @@ """The tests for the Event automation.""" -from unittest.mock import Mock, patch +from unittest.mock import patch import homeassistant.components.automation as automation from homeassistant.core import CoreState from homeassistant.setup import async_setup_component -from tests.common import async_mock_service, mock_coro +from tests.async_mock import AsyncMock +from tests.common import async_mock_service async def test_if_fires_on_hass_start(hass): @@ -30,8 +31,7 @@ async def test_if_fires_on_hass_start(hass): assert len(calls) == 1 with patch( - "homeassistant.config.async_hass_config_yaml", - Mock(return_value=mock_coro(config)), + "homeassistant.config.async_hass_config_yaml", AsyncMock(return_value=config), ): await hass.services.async_call( automation.DOMAIN, automation.SERVICE_RELOAD, blocking=True diff --git a/tests/components/awair/test_sensor.py b/tests/components/awair/test_sensor.py index 8ae5bb8017fdf9..d1a3b933d05abf 100644 --- a/tests/components/awair/test_sensor.py +++ b/tests/components/awair/test_sensor.py @@ -5,8 +5,6 @@ import json import logging -from asynctest import patch - from homeassistant.components.awair.sensor import ( ATTR_LAST_API_UPDATE, ATTR_TIMESTAMP, @@ -29,6 +27,7 @@ from homeassistant.setup import async_setup_component from homeassistant.util.dt import parse_datetime, utcnow +from tests.async_mock import patch from tests.common import async_fire_time_changed, load_fixture DISCOVERY_CONFIG = {"sensor": {"platform": "awair", "access_token": "qwerty"}} diff --git a/tests/components/aws/test_init.py b/tests/components/aws/test_init.py index c7fa9d0a5c1a81..045ad2ff609b79 100644 --- a/tests/components/aws/test_init.py +++ b/tests/components/aws/test_init.py @@ -1,32 +1,32 @@ """Tests for the aws component config and setup.""" -from asynctest import CoroutineMock, MagicMock, patch as async_patch - from homeassistant.components import aws from homeassistant.setup import async_setup_component +from tests.async_mock import AsyncMock, MagicMock, patch as async_patch + class MockAioSession: """Mock AioSession.""" def __init__(self, *args, **kwargs): """Init a mock session.""" - self.get_user = CoroutineMock() - self.invoke = CoroutineMock() - self.publish = CoroutineMock() - self.send_message = CoroutineMock() + self.get_user = AsyncMock() + self.invoke = AsyncMock() + self.publish = AsyncMock() + self.send_message = AsyncMock() def create_client(self, *args, **kwargs): # pylint: disable=no-self-use """Create a mocked client.""" return MagicMock( - __aenter__=CoroutineMock( - return_value=CoroutineMock( + __aenter__=AsyncMock( + return_value=AsyncMock( get_user=self.get_user, # iam invoke=self.invoke, # lambda publish=self.publish, # sns send_message=self.send_message, # sqs ) ), - __aexit__=CoroutineMock(), + __aexit__=AsyncMock(), ) diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index e0bf06e3468704..d3972332c896fb 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -1,11 +1,10 @@ """Test Axis config flow.""" -from asynctest import Mock, patch - from homeassistant.components import axis from homeassistant.components.axis import config_flow from .test_device import MAC, MODEL, NAME, setup_axis_integration +from tests.async_mock import Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index 3d2ed432c1ca1f..74b0ab3b992433 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -1,13 +1,13 @@ """Test Axis device.""" from copy import deepcopy -from asynctest import Mock, patch import axis as axislib import pytest from homeassistant import config_entries from homeassistant.components import axis +from tests.async_mock import Mock, patch from tests.common import MockConfigEntry MAC = "00408C12345" diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index d11f8c91fc31d5..5803f31d5260d3 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -6,7 +6,8 @@ from .test_device import MAC, setup_axis_integration -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import AsyncMock +from tests.common import MockConfigEntry async def test_setup_no_config(hass): @@ -30,7 +31,7 @@ async def test_setup_entry_fails(hass): config_entry.add_to_hass(hass) mock_device = Mock() - mock_device.async_setup.return_value = mock_coro(False) + mock_device.async_setup = AsyncMock(return_value=False) with patch.object(axis, "AxisNetworkDevice") as mock_device_class: mock_device_class.return_value = mock_device diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py index 93d1bccba73777..87fa26c242a523 100644 --- a/tests/components/braviatv/test_config_flow.py +++ b/tests/components/braviatv/test_config_flow.py @@ -1,11 +1,10 @@ """Define tests for the Bravia TV config flow.""" -from asynctest import patch - from homeassistant import data_entry_flow from homeassistant.components.braviatv.const import CONF_IGNORED_SOURCES, DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN +from tests.async_mock import patch from tests.common import MockConfigEntry BRAVIA_SYSTEM_INFO = { diff --git a/tests/components/brother/__init__.py b/tests/components/brother/__init__.py index d6c1fedd31d16a..1a3ba2a3e20137 100644 --- a/tests/components/brother/__init__.py +++ b/tests/components/brother/__init__.py @@ -1,11 +1,10 @@ """Tests for Brother Printer integration.""" import json -from asynctest import patch - from homeassistant.components.brother.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_TYPE +from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index 3f07fca49f042d..06e58b83522e18 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -1,7 +1,6 @@ """Define tests for the Brother Printer config flow.""" import json -from asynctest import patch from brother import SnmpError, UnsupportedModel from homeassistant import data_entry_flow @@ -9,6 +8,7 @@ from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_TYPE +from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture CONFIG = {CONF_HOST: "localhost", CONF_TYPE: "laser"} diff --git a/tests/components/brother/test_init.py b/tests/components/brother/test_init.py index 13378e9dbb9165..04c3c130fc98aa 100644 --- a/tests/components/brother/test_init.py +++ b/tests/components/brother/test_init.py @@ -1,6 +1,4 @@ """Test init of Brother integration.""" -from asynctest import patch - from homeassistant.components.brother.const import DOMAIN from homeassistant.config_entries import ( ENTRY_STATE_LOADED, @@ -9,6 +7,7 @@ ) from homeassistant.const import CONF_HOST, CONF_TYPE, STATE_UNAVAILABLE +from tests.async_mock import patch from tests.common import MockConfigEntry from tests.components.brother import init_integration diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py index e88c22f3f4074d..8e1d52fd2a8eac 100644 --- a/tests/components/brother/test_sensor.py +++ b/tests/components/brother/test_sensor.py @@ -2,8 +2,6 @@ from datetime import timedelta import json -from asynctest import patch - from homeassistant.components.brother.const import UNIT_PAGES from homeassistant.const import ( ATTR_ENTITY_ID, @@ -16,6 +14,7 @@ from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow +from tests.async_mock import patch from tests.common import async_fire_time_changed, load_fixture from tests.components.brother import init_integration diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index fa6f331363f366..8e88c6e564e988 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -2,7 +2,6 @@ import datetime from unittest.mock import MagicMock, Mock -from asynctest import patch from caldav.objects import Event import pytest @@ -10,6 +9,8 @@ from homeassistant.setup import async_setup_component from homeassistant.util import dt +from tests.async_mock import patch + # pylint: disable=redefined-outer-name DEVICE_DATA = {"name": "Private Calendar", "device_id": "Private Calendar"} diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index e8ab05abb90839..401350ec93cf41 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -4,7 +4,6 @@ import io from unittest.mock import PropertyMock, mock_open -from asynctest import patch import pytest from homeassistant.components import camera @@ -15,6 +14,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.components.camera import common diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index e2c1064218dc66..2adb8b63052cf8 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -3,7 +3,6 @@ from typing import Optional from uuid import UUID -from asynctest import MagicMock, Mock, patch import attr import pytest @@ -14,6 +13,7 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component +from tests.async_mock import MagicMock, Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py index 1b2cc175dcb71c..9618525ef32b7a 100644 --- a/tests/components/cert_expiry/test_config_flow.py +++ b/tests/components/cert_expiry/test_config_flow.py @@ -2,14 +2,13 @@ import socket import ssl -from asynctest import patch - from homeassistant import data_entry_flow from homeassistant.components.cert_expiry.const import DEFAULT_PORT, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from .const import HOST, PORT +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/cert_expiry/test_init.py b/tests/components/cert_expiry/test_init.py index d4419b48370ac6..3a2aeb84734515 100644 --- a/tests/components/cert_expiry/test_init.py +++ b/tests/components/cert_expiry/test_init.py @@ -1,8 +1,6 @@ """Tests for Cert Expiry setup.""" from datetime import timedelta -from asynctest import patch - from homeassistant.components.cert_expiry.const import DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED @@ -12,6 +10,7 @@ from .const import HOST, PORT +from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/cert_expiry/test_sensors.py b/tests/components/cert_expiry/test_sensors.py index 6594b0988e723a..9fcd1ac3efecf3 100644 --- a/tests/components/cert_expiry/test_sensors.py +++ b/tests/components/cert_expiry/test_sensors.py @@ -3,13 +3,12 @@ import socket import ssl -from asynctest import patch - from homeassistant.const import CONF_HOST, CONF_PORT, STATE_UNAVAILABLE import homeassistant.util.dt as dt_util from .const import HOST, PORT +from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py index a5f3bef73539ae..ae34318c452cfe 100644 --- a/tests/components/cloud/test_account_link.py +++ b/tests/components/cloud/test_account_link.py @@ -3,7 +3,6 @@ import logging from time import time -from asynctest import CoroutineMock, Mock, patch import pytest from homeassistant import config_entries, data_entry_flow @@ -11,6 +10,7 @@ from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.util.dt import utcnow +from tests.async_mock import AsyncMock, Mock, patch from tests.common import async_fire_time_changed, mock_platform TEST_DOMAIN = "oauth2_test" @@ -109,7 +109,7 @@ async def test_implementation(hass, flow_handler): flow_finished = asyncio.Future() helper = Mock( - async_get_authorize_url=CoroutineMock(return_value="http://example.com/auth"), + async_get_authorize_url=AsyncMock(return_value="http://example.com/auth"), async_get_tokens=Mock(return_value=flow_finished), ) diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index f65b810d690b2f..c239636e1d3037 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -1,12 +1,12 @@ """Test Alexa config.""" import contextlib -from unittest.mock import Mock, patch from homeassistant.components.cloud import ALEXA_SCHEMA, alexa_config from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from homeassistant.util.dt import utcnow -from tests.common import async_fire_time_changed, mock_coro +from tests.async_mock import AsyncMock, Mock, patch +from tests.common import async_fire_time_changed async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs): @@ -28,7 +28,7 @@ async def test_alexa_config_report_state(hass, cloud_prefs): assert conf.should_report_state is False assert conf.is_reporting_states is False - with patch.object(conf, "async_get_access_token", return_value=mock_coro("hello")): + with patch.object(conf, "async_get_access_token", AsyncMock(return_value="hello")): await cloud_prefs.async_update(alexa_report_state=True) await hass.async_block_till_done() @@ -60,7 +60,7 @@ async def test_alexa_config_invalidate_token(hass, cloud_prefs, aioclient_mock): cloud_prefs, Mock( alexa_access_token_url="http://example/alexa_token", - auth=Mock(async_check_token=Mock(side_effect=mock_coro)), + auth=Mock(async_check_token=AsyncMock()), websession=hass.helpers.aiohttp_client.async_get_clientsession(), ), ) @@ -189,13 +189,9 @@ async def test_alexa_update_report_state(hass, cloud_prefs): alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) with patch( - "homeassistant.components.cloud.alexa_config.AlexaConfig." - "async_sync_entities", - side_effect=mock_coro, + "homeassistant.components.cloud.alexa_config.AlexaConfig.async_sync_entities", ) as mock_sync, patch( - "homeassistant.components.cloud.alexa_config." - "AlexaConfig.async_enable_proactive_mode", - side_effect=mock_coro, + "homeassistant.components.cloud.alexa_config.AlexaConfig.async_enable_proactive_mode", ): await cloud_prefs.async_update(alexa_report_state=True) await hass.async_block_till_done() diff --git a/tests/components/cloud/test_binary_sensor.py b/tests/components/cloud/test_binary_sensor.py index c4ad22abb2f298..38c3067873fa1a 100644 --- a/tests/components/cloud/test_binary_sensor.py +++ b/tests/components/cloud/test_binary_sensor.py @@ -1,11 +1,11 @@ """Tests for the cloud binary sensor.""" from unittest.mock import Mock -from asynctest import patch - from homeassistant.components.cloud.const import DISPATCHER_REMOTE_UPDATE from homeassistant.setup import async_setup_component +from tests.async_mock import patch + async def test_remote_connection_sensor(hass): """Test the remote connection sensor.""" diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index b9e6524b62ef2e..0ce79daab15c72 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -12,6 +12,7 @@ from . import mock_cloud, mock_cloud_prefs +from tests.async_mock import AsyncMock from tests.common import mock_coro from tests.components.alexa import test_smart_home as test_alexa @@ -221,7 +222,7 @@ async def test_set_username(hass): prefs = MagicMock( alexa_enabled=False, google_enabled=False, - async_set_username=MagicMock(return_value=mock_coro()), + async_set_username=AsyncMock(return_value=None), ) client = CloudClient(hass, prefs, None, {}, {}) client.cloud = MagicMock(is_logged_in=True, username="mock-username") diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index b08b950a590d14..9866270473de0f 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -1,8 +1,6 @@ """Test the Cloud Google Config.""" from unittest.mock import Mock -from asynctest import patch - from homeassistant.components.cloud import GACTIONS_SCHEMA from homeassistant.components.cloud.google_config import CloudGoogleConfig from homeassistant.components.google_assistant import helpers as ga_helpers @@ -11,7 +9,8 @@ from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from homeassistant.util.dt import utcnow -from tests.common import async_fire_time_changed, mock_coro +from tests.async_mock import AsyncMock, patch +from tests.common import async_fire_time_changed async def test_google_update_report_state(hass, cloud_prefs): @@ -43,12 +42,12 @@ async def test_sync_entities(aioclient_mock, hass, cloud_prefs): GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, - Mock(auth=Mock(async_check_token=Mock(side_effect=mock_coro))), + Mock(auth=Mock(async_check_token=AsyncMock())), ) with patch( "hass_nabucasa.cloud_api.async_google_actions_request_sync", - return_value=mock_coro(Mock(status=HTTP_NOT_FOUND)), + return_value=Mock(status=HTTP_NOT_FOUND), ) as mock_request_sync: assert await config.async_sync_entities("user") == HTTP_NOT_FOUND assert len(mock_request_sync.mock_calls) == 1 diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 2cfca8e6b92cb9..df506d2d8fc719 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -1,9 +1,7 @@ """Tests for the HTTP API for the cloud component.""" import asyncio from ipaddress import ip_network -from unittest.mock import MagicMock, Mock -from asynctest import patch from hass_nabucasa import thingtalk from hass_nabucasa.auth import Unauthenticated, UnknownError from hass_nabucasa.const import STATE_CONNECTED @@ -20,7 +18,7 @@ from . import mock_cloud, mock_cloud_prefs -from tests.common import mock_coro +from tests.async_mock import AsyncMock, MagicMock, Mock, patch from tests.components.google_assistant import MockConfig SUBSCRIPTION_INFO_URL = "https://api-test.hass.io/subscription_info" @@ -29,9 +27,7 @@ @pytest.fixture() def mock_auth(): """Mock check token.""" - with patch( - "hass_nabucasa.auth.CognitoAuth.async_check_token", side_effect=mock_coro - ): + with patch("hass_nabucasa.auth.CognitoAuth.async_check_token"): yield @@ -89,7 +85,7 @@ async def test_google_actions_sync(mock_cognito, mock_cloud_login, cloud_client) """Test syncing Google Actions.""" with patch( "hass_nabucasa.cloud_api.async_google_actions_request_sync", - return_value=mock_coro(Mock(status=200)), + return_value=Mock(status=200), ) as mock_request_sync: req = await cloud_client.post("/api/cloud/google_actions/sync") assert req.status == 200 @@ -100,7 +96,7 @@ async def test_google_actions_sync_fails(mock_cognito, mock_cloud_login, cloud_c """Test syncing Google Actions gone bad.""" with patch( "hass_nabucasa.cloud_api.async_google_actions_request_sync", - return_value=mock_coro(Mock(status=HTTP_INTERNAL_SERVER_ERROR)), + return_value=Mock(status=HTTP_INTERNAL_SERVER_ERROR), ) as mock_request_sync: req = await cloud_client.post("/api/cloud/google_actions/sync") assert req.status == HTTP_INTERNAL_SERVER_ERROR @@ -109,7 +105,7 @@ async def test_google_actions_sync_fails(mock_cognito, mock_cloud_login, cloud_c async def test_login_view(hass, cloud_client): """Test logging in.""" - hass.data["cloud"] = MagicMock(login=MagicMock(return_value=mock_coro())) + hass.data["cloud"] = MagicMock(login=AsyncMock()) req = await cloud_client.post( "/api/cloud/login", json={"email": "my_username", "password": "my_password"} @@ -184,7 +180,7 @@ async def test_login_view_unknown_error(cloud_client): async def test_logout_view(hass, cloud_client): """Test logging out.""" cloud = hass.data["cloud"] = MagicMock() - cloud.logout.return_value = mock_coro() + cloud.logout = AsyncMock(return_value=None) req = await cloud_client.post("/api/cloud/logout") assert req.status == 200 data = await req.json() @@ -450,8 +446,7 @@ async def test_websocket_subscription_not_logged_in(hass, hass_ws_client): """Test querying the status.""" client = await hass_ws_client(hass) with patch( - "hass_nabucasa.Cloud.fetch_subscription_info", - return_value=mock_coro({"return": "value"}), + "hass_nabucasa.Cloud.fetch_subscription_info", return_value={"return": "value"}, ): await client.send_json({"id": 5, "type": "cloud/subscription"}) response = await client.receive_json() @@ -529,7 +524,7 @@ async def test_enabling_webhook(hass, hass_ws_client, setup_api, mock_cloud_logi """Test we call right code to enable webhooks.""" client = await hass_ws_client(hass) with patch( - "hass_nabucasa.cloudhooks.Cloudhooks.async_create", return_value=mock_coro() + "hass_nabucasa.cloudhooks.Cloudhooks.async_create", return_value={} ) as mock_enable: await client.send_json( {"id": 5, "type": "cloud/cloudhook/create", "webhook_id": "mock-webhook-id"} @@ -544,9 +539,7 @@ async def test_enabling_webhook(hass, hass_ws_client, setup_api, mock_cloud_logi async def test_disabling_webhook(hass, hass_ws_client, setup_api, mock_cloud_login): """Test we call right code to disable webhooks.""" client = await hass_ws_client(hass) - with patch( - "hass_nabucasa.cloudhooks.Cloudhooks.async_delete", return_value=mock_coro() - ) as mock_disable: + with patch("hass_nabucasa.cloudhooks.Cloudhooks.async_delete") as mock_disable: await client.send_json( {"id": 5, "type": "cloud/cloudhook/delete", "webhook_id": "mock-webhook-id"} ) @@ -562,9 +555,7 @@ async def test_enabling_remote(hass, hass_ws_client, setup_api, mock_cloud_login client = await hass_ws_client(hass) cloud = hass.data[DOMAIN] - with patch( - "hass_nabucasa.remote.RemoteUI.connect", return_value=mock_coro() - ) as mock_connect: + with patch("hass_nabucasa.remote.RemoteUI.connect") as mock_connect: await client.send_json({"id": 5, "type": "cloud/remote/connect"}) response = await client.receive_json() assert response["success"] @@ -578,9 +569,7 @@ async def test_disabling_remote(hass, hass_ws_client, setup_api, mock_cloud_logi client = await hass_ws_client(hass) cloud = hass.data[DOMAIN] - with patch( - "hass_nabucasa.remote.RemoteUI.disconnect", return_value=mock_coro() - ) as mock_disconnect: + with patch("hass_nabucasa.remote.RemoteUI.disconnect") as mock_disconnect: await client.send_json({"id": 5, "type": "cloud/remote/disconnect"}) response = await client.receive_json() assert response["success"] @@ -670,9 +659,7 @@ async def test_enabling_remote_trusted_networks_other( client = await hass_ws_client(hass) cloud = hass.data[DOMAIN] - with patch( - "hass_nabucasa.remote.RemoteUI.connect", return_value=mock_coro() - ) as mock_connect: + with patch("hass_nabucasa.remote.RemoteUI.connect") as mock_connect: await client.send_json({"id": 5, "type": "cloud/remote/connect"}) response = await client.receive_json() @@ -885,7 +872,7 @@ async def test_thingtalk_convert(hass, hass_ws_client, setup_api): with patch( "homeassistant.components.cloud.http_api.thingtalk.async_convert", - return_value=mock_coro({"hello": "world"}), + return_value={"hello": "world"}, ): await client.send_json( {"id": 5, "type": "cloud/thingtalk/convert", "query": "some-data"} diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index 45ffa1d08eca97..1d160870169a6a 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -1,11 +1,11 @@ """Test Automation config panel.""" import json -from asynctest import patch - from homeassistant.bootstrap import async_setup_component from homeassistant.components import config +from tests.async_mock import patch + async def test_get_device_config(hass, hass_client): """Test getting device config.""" diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index a722333c03708c..7c87d8da68926a 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -1,5 +1,4 @@ """Test hassbian config.""" -from asynctest import patch import pytest from homeassistant.bootstrap import async_setup_component @@ -8,6 +7,8 @@ from homeassistant.const import CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL from homeassistant.util import dt as dt_util, location +from tests.async_mock import patch + ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE diff --git a/tests/components/config/test_customize.py b/tests/components/config/test_customize.py index d8c9ea19b70ac2..30a475dab77bdd 100644 --- a/tests/components/config/test_customize.py +++ b/tests/components/config/test_customize.py @@ -1,12 +1,12 @@ """Test Customize config panel.""" import json -from asynctest import patch - from homeassistant.bootstrap import async_setup_component from homeassistant.components import config from homeassistant.config import DATA_CUSTOMIZE +from tests.async_mock import patch + async def test_get_entity(hass, hass_client): """Test getting entity.""" diff --git a/tests/components/config/test_group.py b/tests/components/config/test_group.py index d00e0317e9ec79..f555660fb7a4f2 100644 --- a/tests/components/config/test_group.py +++ b/tests/components/config/test_group.py @@ -2,11 +2,11 @@ import json from unittest.mock import patch -from asynctest import CoroutineMock - from homeassistant.bootstrap import async_setup_component from homeassistant.components import config +from tests.async_mock import AsyncMock + VIEW_NAME = "api:config:group:config" @@ -52,7 +52,7 @@ def mock_write(path, data): """Mock writing data.""" written.append(data) - mock_call = CoroutineMock() + mock_call = AsyncMock() with patch("homeassistant.components.config._read", mock_read), patch( "homeassistant.components.config._write", mock_write diff --git a/tests/components/config/test_init.py b/tests/components/config/test_init.py index 7f9b62d71f607b..6dd16fef7ec577 100644 --- a/tests/components/config/test_init.py +++ b/tests/components/config/test_init.py @@ -1,11 +1,11 @@ """Test config init.""" -from unittest.mock import patch from homeassistant.components import config from homeassistant.const import EVENT_COMPONENT_LOADED from homeassistant.setup import ATTR_COMPONENT, async_setup_component -from tests.common import mock_component, mock_coro +from tests.async_mock import patch +from tests.common import mock_component async def test_config_setup(hass, loop): @@ -20,8 +20,9 @@ async def test_load_on_demand_already_loaded(hass, aiohttp_client): with patch.object(config, "SECTIONS", []), patch.object( config, "ON_DEMAND", ["zwave"] - ), patch("homeassistant.components.config.zwave.async_setup") as stp: - stp.return_value = mock_coro(True) + ), patch( + "homeassistant.components.config.zwave.async_setup", return_value=True + ) as stp: await async_setup_component(hass, "config", {}) @@ -38,8 +39,9 @@ async def test_load_on_demand_on_load(hass, aiohttp_client): assert "config.zwave" not in hass.config.components - with patch("homeassistant.components.config.zwave.async_setup") as stp: - stp.return_value = mock_coro(True) + with patch( + "homeassistant.components.config.zwave.async_setup", return_value=True + ) as stp: hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: "zwave"}) await hass.async_block_till_done() diff --git a/tests/components/config/test_scene.py b/tests/components/config/test_scene.py index b51628f87aec43..dcaa950f342595 100644 --- a/tests/components/config/test_scene.py +++ b/tests/components/config/test_scene.py @@ -1,12 +1,12 @@ """Test Automation config panel.""" import json -from asynctest import patch - from homeassistant.bootstrap import async_setup_component from homeassistant.components import config from homeassistant.util.yaml import dump +from tests.async_mock import patch + async def test_update_scene(hass, hass_client): """Test updating a scene.""" diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 1670e0c2485289..96ab3bca543ed8 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -1,7 +1,8 @@ """Fixtures for component testing.""" -from asynctest import patch import pytest +from tests.async_mock import patch + @pytest.fixture(autouse=True) def prevent_io(): diff --git a/tests/components/coolmaster/test_config_flow.py b/tests/components/coolmaster/test_config_flow.py index 81219c41ff8e0c..49058fc183e9bb 100644 --- a/tests/components/coolmaster/test_config_flow.py +++ b/tests/components/coolmaster/test_config_flow.py @@ -1,9 +1,9 @@ """Test the Coolmaster config flow.""" -from asynctest import patch - from homeassistant import config_entries, setup from homeassistant.components.coolmaster.const import AVAILABLE_MODES, DOMAIN +from tests.async_mock import patch + def _flow_data(): options = {"host": "1.1.1.1"} diff --git a/tests/components/coronavirus/conftest.py b/tests/components/coronavirus/conftest.py index 45d2a00e69d963..6e49d2aa164ba7 100644 --- a/tests/components/coronavirus/conftest.py +++ b/tests/components/coronavirus/conftest.py @@ -1,8 +1,9 @@ """Test helpers.""" -from asynctest import Mock, patch import pytest +from tests.async_mock import Mock, patch + @pytest.fixture(autouse=True) def mock_cases(): diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index c03dc72019e924..ddc89295cbad76 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -1,14 +1,14 @@ """deCONZ climate platform tests.""" from copy import deepcopy -from asynctest import patch - from homeassistant.components import deconz import homeassistant.components.climate as climate from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from tests.async_mock import patch + SENSORS = { "1": { "id": "Thermostat id", diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 0eda8eb71abf2b..4f45e36c5b105c 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -1,14 +1,14 @@ """deCONZ cover platform tests.""" from copy import deepcopy -from asynctest import patch - from homeassistant.components import deconz import homeassistant.components.cover as cover from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from tests.async_mock import patch + COVERS = { "1": { "id": "Level controllable cover id", diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index dd3289dea2301d..fc8d4f9d1bad63 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -1,12 +1,12 @@ """Test deCONZ remote events.""" from copy import deepcopy -from asynctest import Mock - from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from tests.common import async_capture_events + SENSORS = { "1": { "id": "Switch 1 id", @@ -67,53 +67,40 @@ async def test_deconz_events(hass): switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level") assert switch_2_battery_level.state == "100" - mock_listener = Mock() - unsub = hass.bus.async_listen(CONF_DECONZ_EVENT, mock_listener) + events = async_capture_events(hass, CONF_DECONZ_EVENT) gateway.api.sensors["1"].update({"state": {"buttonevent": 2000}}) await hass.async_block_till_done() - assert len(mock_listener.mock_calls) == 1 - assert mock_listener.mock_calls[0][1][0].data == { + assert len(events) == 1 + assert events[0].data == { "id": "switch_1", "unique_id": "00:00:00:00:00:00:00:01", "event": 2000, } - unsub() - - mock_listener = Mock() - unsub = hass.bus.async_listen(CONF_DECONZ_EVENT, mock_listener) - gateway.api.sensors["3"].update({"state": {"buttonevent": 2000}}) await hass.async_block_till_done() - assert len(mock_listener.mock_calls) == 1 - assert mock_listener.mock_calls[0][1][0].data == { + assert len(events) == 2 + assert events[1].data == { "id": "switch_3", "unique_id": "00:00:00:00:00:00:00:03", "event": 2000, "gesture": 1, } - unsub() - - mock_listener = Mock() - unsub = hass.bus.async_listen(CONF_DECONZ_EVENT, mock_listener) - gateway.api.sensors["4"].update({"state": {"gesture": 0}}) await hass.async_block_till_done() - assert len(mock_listener.mock_calls) == 1 - assert mock_listener.mock_calls[0][1][0].data == { + assert len(events) == 3 + assert events[2].data == { "id": "switch_4", "unique_id": "00:00:00:00:00:00:00:04", "event": 1000, "gesture": 0, } - unsub() - await gateway.async_reset() assert len(hass.states.async_all()) == 0 diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index e8c9da42adac0f..9af1a3151e88f1 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -1,7 +1,6 @@ """Test deCONZ gateway.""" from copy import deepcopy -from asynctest import Mock, patch import pydeconz import pytest @@ -9,6 +8,7 @@ from homeassistant.components import deconz, ssdp from homeassistant.helpers.dispatcher import async_dispatcher_connect +from tests.async_mock import Mock, patch from tests.common import MockConfigEntry API_KEY = "1234567890ABCDEF" diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index 3e2760bc632380..8d9d387c91ca6a 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -2,12 +2,12 @@ import asyncio from copy import deepcopy -from asynctest import patch - from homeassistant.components import deconz from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from tests.async_mock import patch + ENTRY1_HOST = "1.2.3.4" ENTRY1_PORT = 80 ENTRY1_API_KEY = "1234567890ABCDEF" diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index e39722fdacbe31..229c085916ecdc 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -1,14 +1,14 @@ """deCONZ light platform tests.""" from copy import deepcopy -from asynctest import patch - from homeassistant.components import deconz import homeassistant.components.light as light from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from tests.async_mock import patch + GROUPS = { "1": { "id": "Light group id", diff --git a/tests/components/deconz/test_scene.py b/tests/components/deconz/test_scene.py index 3593fa32355fa9..538c849e831dce 100644 --- a/tests/components/deconz/test_scene.py +++ b/tests/components/deconz/test_scene.py @@ -1,14 +1,14 @@ """deCONZ scene platform tests.""" from copy import deepcopy -from asynctest import patch - from homeassistant.components import deconz import homeassistant.components.scene as scene from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from tests.async_mock import patch + GROUPS = { "1": { "id": "Light group id", diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index 07985e4d9f4b38..e880ea1000ba83 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -1,6 +1,5 @@ """deCONZ service tests.""" -from asynctest import Mock, patch import pytest import voluptuous as vol @@ -9,6 +8,8 @@ from .test_gateway import BRIDGEID, setup_deconz_integration +from tests.async_mock import Mock, patch + GROUP = { "1": { "id": "Group 1 id", diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index 6e151ebd47a173..b441868859beb8 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -1,14 +1,14 @@ """deCONZ switch platform tests.""" from copy import deepcopy -from asynctest import patch - from homeassistant.components import deconz import homeassistant.components.switch as switch from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration +from tests.async_mock import patch + SWITCHES = { "1": { "id": "On off switch id", diff --git a/tests/components/demo/test_media_player.py b/tests/components/demo/test_media_player.py index 26d0d2165e7e4b..e663600f84f90d 100644 --- a/tests/components/demo/test_media_player.py +++ b/tests/components/demo/test_media_player.py @@ -1,5 +1,4 @@ """The tests for the Demo Media player platform.""" -from asynctest import patch import pytest import voluptuous as vol @@ -7,6 +6,7 @@ from homeassistant.helpers.aiohttp_client import DATA_CLIENTSESSION from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.components.media_player import common TEST_ENTITY_ID = "media_player.walkman" diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py index 1b51d93ae6c165..23e400adac4f5d 100644 --- a/tests/components/device_sun_light_trigger/test_init.py +++ b/tests/components/device_sun_light_trigger/test_init.py @@ -2,7 +2,6 @@ # pylint: disable=protected-access from datetime import datetime -from asynctest import patch import pytest from homeassistant.components import ( @@ -23,6 +22,7 @@ from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util +from tests.async_mock import patch from tests.common import async_fire_time_changed diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 6f384faa15cb8e..1a366b0d2df4b1 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -4,7 +4,6 @@ import logging import os -from asynctest import Mock, call, patch import pytest from homeassistant.components import zone @@ -28,6 +27,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import Mock, call, patch from tests.common import ( assert_setup_component, async_fire_time_changed, diff --git a/tests/components/directv/test_config_flow.py b/tests/components/directv/test_config_flow.py index c5cfec50637939..78eddf57c309cf 100644 --- a/tests/components/directv/test_config_flow.py +++ b/tests/components/directv/test_config_flow.py @@ -1,6 +1,5 @@ """Test the DirecTV config flow.""" from aiohttp import ClientError as HTTPClientError -from asynctest import patch from homeassistant.components.directv.const import CONF_RECEIVER_ID, DOMAIN from homeassistant.components.ssdp import ATTR_UPNP_SERIAL @@ -13,6 +12,7 @@ ) from homeassistant.helpers.typing import HomeAssistantType +from tests.async_mock import patch from tests.components.directv import ( HOST, MOCK_SSDP_DISCOVERY_INFO, diff --git a/tests/components/directv/test_media_player.py b/tests/components/directv/test_media_player.py index 8b428c1b708bbf..7203325fbbe95d 100644 --- a/tests/components/directv/test_media_player.py +++ b/tests/components/directv/test_media_player.py @@ -2,7 +2,6 @@ from datetime import datetime, timedelta from typing import Optional -from asynctest import patch from pytest import fixture from homeassistant.components.directv.media_player import ( @@ -55,6 +54,7 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt as dt_util +from tests.async_mock import patch from tests.components.directv import setup_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/directv/test_remote.py b/tests/components/directv/test_remote.py index f93d839b78cf83..b00f62c0e0c423 100644 --- a/tests/components/directv/test_remote.py +++ b/tests/components/directv/test_remote.py @@ -1,6 +1,4 @@ """The tests for the DirecTV remote platform.""" -from asynctest import patch - from homeassistant.components.remote import ( ATTR_COMMAND, DOMAIN as REMOTE_DOMAIN, @@ -9,6 +7,7 @@ from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.helpers.typing import HomeAssistantType +from tests.async_mock import patch from tests.components.directv import setup_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/discovery/test_init.py b/tests/components/discovery/test_init.py index 86865209de304c..9490707e0f6e26 100644 --- a/tests/components/discovery/test_init.py +++ b/tests/components/discovery/test_init.py @@ -1,7 +1,6 @@ """The tests for the discovery component.""" from unittest.mock import MagicMock -from asynctest import patch import pytest from homeassistant import config_entries @@ -9,6 +8,7 @@ from homeassistant.components import discovery from homeassistant.util.dt import utcnow +from tests.async_mock import patch from tests.common import async_fire_time_changed, mock_coro # One might consider to "mock" services, but it's easy enough to just use diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index 8b49f87bd0bd80..7c7c034c6b4dcf 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -1,13 +1,12 @@ """Test the DoorBird config flow.""" import urllib -from asynctest import MagicMock, patch - from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.doorbird import CONF_CUSTOM_URL, CONF_TOKEN from homeassistant.components.doorbird.const import CONF_EVENTS, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry, init_recorder_component VALID_CONFIG = { diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 67c6d3bc58d83d..936a1e6803766b 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -11,13 +11,13 @@ from itertools import chain, repeat from unittest.mock import DEFAULT, Mock -import asynctest import pytest from homeassistant.bootstrap import async_setup_component from homeassistant.components.dsmr.sensor import DerivativeDSMREntity from homeassistant.const import ENERGY_KILO_WATT_HOUR, TIME_HOURS, VOLUME_CUBIC_METERS +import tests.async_mock from tests.common import assert_setup_component @@ -26,8 +26,8 @@ def mock_connection_factory(monkeypatch): """Mock the create functions for serial and TCP Asyncio connections.""" from dsmr_parser.clients.protocol import DSMRProtocol - transport = asynctest.Mock(spec=asyncio.Transport) - protocol = asynctest.Mock(spec=DSMRProtocol) + transport = tests.async_mock.Mock(spec=asyncio.Transport) + protocol = tests.async_mock.Mock(spec=DSMRProtocol) async def connection_factory(*args, **kwargs): """Return mocked out Asyncio classes.""" @@ -327,7 +327,7 @@ async def test_connection_errors_retry(hass, monkeypatch, mock_connection_factor config = {"platform": "dsmr", "reconnect_interval": 0} # override the mock to have it fail the first time and succeed after - first_fail_connection_factory = asynctest.CoroutineMock( + first_fail_connection_factory = tests.async_mock.AsyncMock( return_value=(transport, protocol), side_effect=chain([TimeoutError], repeat(DEFAULT)), ) diff --git a/tests/components/dynalite/common.py b/tests/components/dynalite/common.py index b90e61204441ea..f72e3f481b6ee7 100644 --- a/tests/components/dynalite/common.py +++ b/tests/components/dynalite/common.py @@ -1,9 +1,8 @@ """Common functions for tests.""" -from asynctest import CoroutineMock, Mock, call, patch - from homeassistant.components import dynalite from homeassistant.helpers import entity_registry +from tests.async_mock import AsyncMock, Mock, call, patch from tests.common import MockConfigEntry ATTR_SERVICE = "service" @@ -38,7 +37,7 @@ async def create_entity_from_device(hass, device): with patch( "homeassistant.components.dynalite.bridge.DynaliteDevices" ) as mock_dyn_dev: - mock_dyn_dev().async_setup = CoroutineMock(return_value=True) + mock_dyn_dev().async_setup = AsyncMock(return_value=True) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() new_device_func = mock_dyn_dev.mock_calls[1][2]["new_device_func"] diff --git a/tests/components/dynalite/test_bridge.py b/tests/components/dynalite/test_bridge.py index 0b093fd5c3e114..ea73f75a3905d4 100644 --- a/tests/components/dynalite/test_bridge.py +++ b/tests/components/dynalite/test_bridge.py @@ -1,10 +1,9 @@ """Test Dynalite bridge.""" -from asynctest import CoroutineMock, Mock, patch - from homeassistant.components import dynalite from homeassistant.helpers.dispatcher import async_dispatcher_connect +from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry @@ -16,7 +15,7 @@ async def test_update_device(hass): with patch( "homeassistant.components.dynalite.bridge.DynaliteDevices" ) as mock_dyn_dev: - mock_dyn_dev().async_setup = CoroutineMock(return_value=True) + mock_dyn_dev().async_setup = AsyncMock(return_value=True) assert await hass.config_entries.async_setup(entry.entry_id) # Not waiting so it add the devices before registration update_device_func = mock_dyn_dev.mock_calls[1][2]["update_device_func"] @@ -46,7 +45,7 @@ async def test_add_devices_then_register(hass): with patch( "homeassistant.components.dynalite.bridge.DynaliteDevices" ) as mock_dyn_dev: - mock_dyn_dev().async_setup = CoroutineMock(return_value=True) + mock_dyn_dev().async_setup = AsyncMock(return_value=True) assert await hass.config_entries.async_setup(entry.entry_id) # Not waiting so it add the devices before registration new_device_func = mock_dyn_dev.mock_calls[1][2]["new_device_func"] @@ -79,7 +78,7 @@ async def test_register_then_add_devices(hass): with patch( "homeassistant.components.dynalite.bridge.DynaliteDevices" ) as mock_dyn_dev: - mock_dyn_dev().async_setup = CoroutineMock(return_value=True) + mock_dyn_dev().async_setup = AsyncMock(return_value=True) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() new_device_func = mock_dyn_dev.mock_calls[1][2]["new_device_func"] diff --git a/tests/components/dynalite/test_config_flow.py b/tests/components/dynalite/test_config_flow.py index 1a1cdc16f49a18..11b4d6b524ce66 100644 --- a/tests/components/dynalite/test_config_flow.py +++ b/tests/components/dynalite/test_config_flow.py @@ -1,11 +1,11 @@ """Test Dynalite config flow.""" -from asynctest import CoroutineMock, patch import pytest from homeassistant import config_entries from homeassistant.components import dynalite +from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry @@ -69,7 +69,7 @@ async def test_existing_update(hass): with patch( "homeassistant.components.dynalite.bridge.DynaliteDevices" ) as mock_dyn_dev: - mock_dyn_dev().async_setup = CoroutineMock(return_value=True) + mock_dyn_dev().async_setup = AsyncMock(return_value=True) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() mock_dyn_dev().configure.assert_called_once() diff --git a/tests/components/dynalite/test_init.py b/tests/components/dynalite/test_init.py index 8e2290a9c407af..8b1393cb32dd96 100644 --- a/tests/components/dynalite/test_init.py +++ b/tests/components/dynalite/test_init.py @@ -1,12 +1,11 @@ """Test Dynalite __init__.""" -from asynctest import call, patch - import homeassistant.components.dynalite.const as dynalite from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_ROOM from homeassistant.setup import async_setup_component +from tests.async_mock import call, patch from tests.common import MockConfigEntry diff --git a/tests/components/dyson/test_air_quality.py b/tests/components/dyson/test_air_quality.py index fcd801616c951f..ab11a1ad897029 100644 --- a/tests/components/dyson/test_air_quality.py +++ b/tests/components/dyson/test_air_quality.py @@ -2,7 +2,6 @@ import json from unittest import mock -import asynctest from libpurecool.dyson_pure_cool import DysonPureCool from libpurecool.dyson_pure_state_v2 import DysonEnvironmentalSensorV2State @@ -19,6 +18,8 @@ from .common import load_mock_device +from tests.async_mock import patch + def _get_dyson_purecool_device(): """Return a valid device as provided by the Dyson web services.""" @@ -46,8 +47,8 @@ def _get_config(): } -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -65,8 +66,8 @@ async def test_purecool_aiq_attributes(devices, login, hass): assert attributes[dyson.ATTR_VOC] == 35 -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -108,8 +109,8 @@ async def test_purecool_aiq_update_state(devices, login, hass): assert attributes[dyson.ATTR_VOC] == 55 -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -124,8 +125,8 @@ async def test_purecool_component_setup_only_once(devices, login, hass): assert len(hass.data[dyson.DYSON_AIQ_DEVICES]) == 1 -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -140,8 +141,8 @@ async def test_purecool_aiq_without_discovery(devices, login, hass): assert add_entities_mock.call_count == 0 -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) diff --git a/tests/components/dyson/test_climate.py b/tests/components/dyson/test_climate.py index 345eae6f55302d..af17d1f0ab4a8a 100644 --- a/tests/components/dyson/test_climate.py +++ b/tests/components/dyson/test_climate.py @@ -2,7 +2,6 @@ import unittest from unittest import mock -import asynctest from libpurecool.const import FocusMode, HeatMode, HeatState, HeatTarget from libpurecool.dyson_pure_hotcool_link import DysonPureHotCoolLink from libpurecool.dyson_pure_state import DysonPureHotCoolState @@ -14,6 +13,7 @@ from .common import load_mock_device +from tests.async_mock import patch from tests.common import get_test_home_assistant @@ -344,11 +344,11 @@ def test_property_target_temperature(self): assert entity.target_temperature == 23 -@asynctest.patch( +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_device_heat_on(), _get_device_cool()], ) -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) async def test_setup_component_with_parent_discovery( mocked_login, mocked_devices, hass ): diff --git a/tests/components/dyson/test_fan.py b/tests/components/dyson/test_fan.py index 7801c897723df0..d4db6051960a94 100644 --- a/tests/components/dyson/test_fan.py +++ b/tests/components/dyson/test_fan.py @@ -3,7 +3,6 @@ import unittest from unittest import mock -import asynctest from libpurecool.const import FanMode, FanSpeed, NightMode, Oscillation from libpurecool.dyson_pure_cool import DysonPureCool from libpurecool.dyson_pure_cool_link import DysonPureCoolLink @@ -28,6 +27,7 @@ from .common import load_mock_device +from tests.async_mock import patch from tests.common import get_test_home_assistant @@ -386,8 +386,8 @@ def test_service_set_night_mode(self): dyson_device.set_night_mode.assert_called_with(True) -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecoollink_device()], ) @@ -404,8 +404,8 @@ async def test_purecoollink_attributes(devices, login, hass): assert attributes[ATTR_OSCILLATING] is True -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -426,8 +426,8 @@ async def test_purecool_turn_on(devices, login, hass): assert device.turn_on.call_count == 1 -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -470,8 +470,8 @@ async def test_purecool_set_speed(devices, login, hass): device.set_fan_speed.assert_called_with(FanSpeed.FAN_SPEED_10) -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -492,8 +492,8 @@ async def test_purecool_turn_off(devices, login, hass): assert device.turn_off.call_count == 1 -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -526,8 +526,8 @@ async def test_purecool_set_dyson_speed(devices, login, hass): device.set_fan_speed.assert_called_with(FanSpeed.FAN_SPEED_2) -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -562,8 +562,8 @@ async def test_purecool_oscillate(devices, login, hass): assert device.disable_oscillation.call_count == 1 -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -599,8 +599,8 @@ async def test_purecool_set_night_mode(devices, login, hass): assert device.disable_night_mode.call_count == 1 -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -635,8 +635,8 @@ async def test_purecool_set_auto_mode(devices, login, hass): assert device.disable_auto_mode.call_count == 1 -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -671,8 +671,8 @@ async def test_purecool_set_angle(devices, login, hass): device.enable_oscillation.assert_called_with(90, 180) -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -707,8 +707,8 @@ async def test_purecool_set_flow_direction_front(devices, login, hass): assert device.disable_frontal_direction.call_count == 1 -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -743,8 +743,8 @@ async def test_purecool_set_timer(devices, login, hass): assert device.disable_sleep_timer.call_count == 1 -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -804,8 +804,8 @@ async def test_purecool_update_state(devices, login, hass): assert attributes[dyson.ATTR_DYSON_SPEED_LIST] == _get_supported_speeds() -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) @@ -865,8 +865,8 @@ async def test_purecool_update_state_filter_inv(devices, login, hass): assert attributes[dyson.ATTR_DYSON_SPEED_LIST] == _get_supported_speeds() -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) diff --git a/tests/components/dyson/test_sensor.py b/tests/components/dyson/test_sensor.py index 4d3d1c96101607..92bd3bba9aaf94 100644 --- a/tests/components/dyson/test_sensor.py +++ b/tests/components/dyson/test_sensor.py @@ -2,7 +2,6 @@ import unittest from unittest import mock -import asynctest from libpurecool.dyson_pure_cool import DysonPureCool from libpurecool.dyson_pure_cool_link import DysonPureCoolLink @@ -20,6 +19,7 @@ from .common import load_mock_device +from tests.async_mock import patch from tests.common import get_test_home_assistant @@ -258,8 +258,8 @@ def test_dyson_air_quality_sensor_with_values(self): assert sensor.entity_id == "sensor.dyson_1" -@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@asynctest.patch( +@patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@patch( "libpurecool.dyson.DysonAccount.devices", return_value=[_get_dyson_purecool_device()], ) diff --git a/tests/components/ee_brightbox/test_device_tracker.py b/tests/components/ee_brightbox/test_device_tracker.py index f862539f1dfb56..64f24ec289c1ea 100644 --- a/tests/components/ee_brightbox/test_device_tracker.py +++ b/tests/components/ee_brightbox/test_device_tracker.py @@ -1,7 +1,6 @@ """Tests for the EE BrightBox device scanner.""" from datetime import datetime -from asynctest import patch from eebrightbox import EEBrightBoxException import pytest @@ -9,6 +8,8 @@ from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM from homeassistant.setup import async_setup_component +from tests.async_mock import patch + def _configure_mock_get_devices(eebrightbox_mock): eebrightbox_instance = eebrightbox_mock.return_value diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index 2f701a2e146998..992483529a55dd 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -1,16 +1,16 @@ """Test the Elk-M1 Control config flow.""" -from asynctest import CoroutineMock, MagicMock, PropertyMock, patch - from homeassistant import config_entries, setup from homeassistant.components.elkm1.const import DOMAIN +from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch + def mock_elk(invalid_auth=None, sync_complete=None): """Mock m1lib Elk.""" mocked_elk = MagicMock() type(mocked_elk).invalid_auth = PropertyMock(return_value=invalid_auth) - type(mocked_elk).sync_complete = CoroutineMock() + type(mocked_elk).sync_complete = AsyncMock() return mocked_elk diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 4b951f9a369574..b55621226fad38 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -1,12 +1,12 @@ """Test config flow.""" from collections import namedtuple -from unittest.mock import MagicMock, patch import pytest from homeassistant.components.esphome import DATA_KEY, config_flow -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import AsyncMock, MagicMock, patch +from tests.common import MockConfigEntry MockDeviceInfo = namedtuple("DeviceInfo", ["uses_password", "name"]) @@ -24,8 +24,8 @@ def mock_constructor(loop, host, port, password): return mock_client mock_client.side_effect = mock_constructor - mock_client.connect.return_value = mock_coro() - mock_client.disconnect.return_value = mock_coro() + mock_client.connect = AsyncMock() + mock_client.disconnect = AsyncMock() yield mock_client @@ -53,7 +53,7 @@ async def test_user_connection_works(hass, mock_client): result = await flow.async_step_user(user_input=None) assert result["type"] == "form" - mock_client.device_info.return_value = mock_coro(MockDeviceInfo(False, "test")) + mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test")) result = await flow.async_step_user(user_input={"host": "127.0.0.1", "port": 80}) @@ -119,7 +119,7 @@ async def test_user_with_password(hass, mock_client): flow = _setup_flow_handler(hass) await flow.async_step_user(user_input=None) - mock_client.device_info.return_value = mock_coro(MockDeviceInfo(True, "test")) + mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(True, "test")) result = await flow.async_step_user(user_input={"host": "127.0.0.1", "port": 6053}) @@ -142,7 +142,7 @@ async def test_user_invalid_password(hass, mock_api_connection_error, mock_clien flow = _setup_flow_handler(hass) await flow.async_step_user(user_input=None) - mock_client.device_info.return_value = mock_coro(MockDeviceInfo(True, "test")) + mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(True, "test")) await flow.async_step_user(user_input={"host": "127.0.0.1", "port": 6053}) mock_client.connect.side_effect = mock_api_connection_error @@ -163,7 +163,7 @@ async def test_discovery_initiation(hass, mock_client): "properties": {}, } - mock_client.device_info.return_value = mock_coro(MockDeviceInfo(False, "test8266")) + mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test8266")) result = await flow.async_step_zeroconf(user_input=service_info) assert result["type"] == "form" @@ -245,7 +245,7 @@ async def test_discovery_duplicate_data(hass, mock_client): "properties": {"address": "test8266.local"}, } - mock_client.device_info.return_value = mock_coro(MockDeviceInfo(False, "test8266")) + mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test8266")) result = await hass.config_entries.flow.async_init( "esphome", data=service_info, context={"source": "zeroconf"} diff --git a/tests/components/flume/test_config_flow.py b/tests/components/flume/test_config_flow.py index 6ce3391c2c631a..a408a652f325d2 100644 --- a/tests/components/flume/test_config_flow.py +++ b/tests/components/flume/test_config_flow.py @@ -1,10 +1,11 @@ """Test the flume config flow.""" -from asynctest import MagicMock, patch import requests.exceptions from homeassistant import config_entries, setup from homeassistant.components.flume.const import DOMAIN +from tests.async_mock import MagicMock, patch + def _get_mocked_flume_device_list(): flume_device_list_mock = MagicMock() diff --git a/tests/components/flunearyou/test_config_flow.py b/tests/components/flunearyou/test_config_flow.py index 21fcb4798db387..a3a0d41e885bd5 100644 --- a/tests/components/flunearyou/test_config_flow.py +++ b/tests/components/flunearyou/test_config_flow.py @@ -1,5 +1,4 @@ """Define tests for the flunearyou config flow.""" -from asynctest import patch from pyflunearyou.errors import FluNearYouError from homeassistant import data_entry_flow @@ -7,6 +6,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py index c0befe8e69ca06..ed16e94a283191 100644 --- a/tests/components/flux/test_switch.py +++ b/tests/components/flux/test_switch.py @@ -1,5 +1,4 @@ """The tests for the Flux switch platform.""" -from asynctest.mock import patch import pytest from homeassistant.components import light, switch @@ -14,6 +13,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import ( assert_setup_component, async_fire_time_changed, diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py index 4d8fadf0654cdd..a4c8a950299a9b 100644 --- a/tests/components/freebox/test_config_flow.py +++ b/tests/components/freebox/test_config_flow.py @@ -4,7 +4,6 @@ HttpRequestError, InvalidTokenError, ) -from asynctest import CoroutineMock, patch import pytest from homeassistant import data_entry_flow @@ -12,6 +11,7 @@ from homeassistant.config_entries import SOURCE_DISCOVERY, SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_PORT +from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry HOST = "myrouter.freeboxos.fr" @@ -22,17 +22,17 @@ def mock_controller_connect(): """Mock a successful connection.""" with patch("homeassistant.components.freebox.router.Freepybox") as service_mock: - service_mock.return_value.open = CoroutineMock() - service_mock.return_value.system.get_config = CoroutineMock( + service_mock.return_value.open = AsyncMock() + service_mock.return_value.system.get_config = AsyncMock( return_value={ "mac": "abcd", "model_info": {"pretty_name": "Pretty Model"}, "firmware_version": "123", } ) - service_mock.return_value.lan.get_hosts_list = CoroutineMock() - service_mock.return_value.connection.get_status = CoroutineMock() - service_mock.return_value.close = CoroutineMock() + service_mock.return_value.lan.get_hosts_list = AsyncMock() + service_mock.return_value.connection.get_status = AsyncMock() + service_mock.return_value.close = AsyncMock() yield service_mock diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index ef2548718305e6..10f55bd4db3b3c 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -1,7 +1,6 @@ """The tests for Home Assistant frontend.""" import re -from asynctest import patch import pytest from homeassistant.components.frontend import ( @@ -17,6 +16,7 @@ from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import async_capture_events CONFIG_THEMES = {DOMAIN: {CONF_THEMES: {"happy": {"primary-color": "red"}}}} diff --git a/tests/components/gdacs/test_config_flow.py b/tests/components/gdacs/test_config_flow.py index c3c5f5609c4330..10e4312eb38886 100644 --- a/tests/components/gdacs/test_config_flow.py +++ b/tests/components/gdacs/test_config_flow.py @@ -1,7 +1,6 @@ """Define tests for the GDACS config flow.""" from datetime import timedelta -from asynctest import patch import pytest from homeassistant import data_entry_flow @@ -13,6 +12,8 @@ CONF_SCAN_INTERVAL, ) +from tests.async_mock import patch + @pytest.fixture(name="gdacs_setup", autouse=True) def gdacs_setup_fixture(): diff --git a/tests/components/gdacs/test_geo_location.py b/tests/components/gdacs/test_geo_location.py index 3b49fe2af71a63..2162340154f1ea 100644 --- a/tests/components/gdacs/test_geo_location.py +++ b/tests/components/gdacs/test_geo_location.py @@ -1,8 +1,6 @@ """The tests for the GDACS Feed integration.""" import datetime -from asynctest import patch - from homeassistant.components import gdacs from homeassistant.components.gdacs import DEFAULT_SCAN_INTERVAL, DOMAIN, FEED from homeassistant.components.gdacs.geo_location import ( @@ -35,6 +33,7 @@ import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM +from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.gdacs import _generate_mock_feed_entry diff --git a/tests/components/gdacs/test_init.py b/tests/components/gdacs/test_init.py index 40bda2a196b982..c0ac83ebcc2a7d 100644 --- a/tests/components/gdacs/test_init.py +++ b/tests/components/gdacs/test_init.py @@ -1,8 +1,8 @@ """Define tests for the GDACS general setup.""" -from asynctest import patch - from homeassistant.components.gdacs import DOMAIN, FEED +from tests.async_mock import patch + async def test_component_unload_config_entry(hass, config_entry): """Test that loading and unloading of a config entry works.""" diff --git a/tests/components/gdacs/test_sensor.py b/tests/components/gdacs/test_sensor.py index 5e8fd5ad30f796..aa8c2a43428125 100644 --- a/tests/components/gdacs/test_sensor.py +++ b/tests/components/gdacs/test_sensor.py @@ -1,6 +1,4 @@ """The tests for the GDACS Feed integration.""" -from asynctest import patch - from homeassistant.components import gdacs from homeassistant.components.gdacs import DEFAULT_SCAN_INTERVAL from homeassistant.components.gdacs.sensor import ( @@ -20,6 +18,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.gdacs import _generate_mock_feed_entry diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index 264146a6fdaf9b..f0b29539ed5124 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -1,7 +1,6 @@ """The tests for the generic_thermostat.""" import datetime -from asynctest import mock import pytest import pytz import voluptuous as vol @@ -32,6 +31,7 @@ from homeassistant.setup import async_setup_component from homeassistant.util.unit_system import METRIC_SYSTEM +from tests.async_mock import patch from tests.common import assert_setup_component, mock_restore_cache from tests.components.climate import common @@ -622,7 +622,7 @@ async def test_temp_change_ac_trigger_on_long_enough(hass, setup_comp_4): fake_changed = datetime.datetime( 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc ) - with mock.patch( + with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): calls = _setup_switch(hass, False) @@ -650,7 +650,7 @@ async def test_temp_change_ac_trigger_off_long_enough(hass, setup_comp_4): fake_changed = datetime.datetime( 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc ) - with mock.patch( + with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): calls = _setup_switch(hass, True) @@ -733,7 +733,7 @@ async def test_temp_change_ac_trigger_on_long_enough_2(hass, setup_comp_5): fake_changed = datetime.datetime( 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc ) - with mock.patch( + with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): calls = _setup_switch(hass, False) @@ -761,7 +761,7 @@ async def test_temp_change_ac_trigger_off_long_enough_2(hass, setup_comp_5): fake_changed = datetime.datetime( 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc ) - with mock.patch( + with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): calls = _setup_switch(hass, True) @@ -852,7 +852,7 @@ async def test_temp_change_heater_trigger_on_long_enough(hass, setup_comp_6): fake_changed = datetime.datetime( 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc ) - with mock.patch( + with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): calls = _setup_switch(hass, False) @@ -871,7 +871,7 @@ async def test_temp_change_heater_trigger_off_long_enough(hass, setup_comp_6): fake_changed = datetime.datetime( 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc ) - with mock.patch( + with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): calls = _setup_switch(hass, True) diff --git a/tests/components/geo_json_events/test_geo_location.py b/tests/components/geo_json_events/test_geo_location.py index 6b7535a8c85182..d79fa3cb18d29c 100644 --- a/tests/components/geo_json_events/test_geo_location.py +++ b/tests/components/geo_json_events/test_geo_location.py @@ -1,6 +1,4 @@ """The tests for the geojson platform.""" -from asynctest.mock import MagicMock, call, patch - from homeassistant.components import geo_location from homeassistant.components.geo_json_events.geo_location import ( ATTR_EXTERNAL_ID, @@ -23,6 +21,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import MagicMock, call, patch from tests.common import assert_setup_component, async_fire_time_changed URL = "http://geo.json.local/geo_json_events.json" diff --git a/tests/components/geonetnz_quakes/test_geo_location.py b/tests/components/geonetnz_quakes/test_geo_location.py index 0264baa8e87e53..89644445ef1ff1 100644 --- a/tests/components/geonetnz_quakes/test_geo_location.py +++ b/tests/components/geonetnz_quakes/test_geo_location.py @@ -1,8 +1,6 @@ """The tests for the GeoNet NZ Quakes Feed integration.""" import datetime -from asynctest import patch - from homeassistant.components import geonetnz_quakes from homeassistant.components.geo_location import ATTR_SOURCE from homeassistant.components.geonetnz_quakes import DEFAULT_SCAN_INTERVAL, DOMAIN, FEED @@ -31,6 +29,7 @@ import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM +from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.geonetnz_quakes import _generate_mock_feed_entry diff --git a/tests/components/geonetnz_quakes/test_init.py b/tests/components/geonetnz_quakes/test_init.py index 85724879f7b15f..87f2f2a7947aec 100644 --- a/tests/components/geonetnz_quakes/test_init.py +++ b/tests/components/geonetnz_quakes/test_init.py @@ -1,8 +1,8 @@ """Define tests for the GeoNet NZ Quakes general setup.""" -from asynctest import patch - from homeassistant.components.geonetnz_quakes import DOMAIN, FEED +from tests.async_mock import patch + async def test_component_unload_config_entry(hass, config_entry): """Test that loading and unloading of a config entry works.""" diff --git a/tests/components/geonetnz_quakes/test_sensor.py b/tests/components/geonetnz_quakes/test_sensor.py index 7d7f8333bc097f..f02a803994e80b 100644 --- a/tests/components/geonetnz_quakes/test_sensor.py +++ b/tests/components/geonetnz_quakes/test_sensor.py @@ -1,8 +1,6 @@ """The tests for the GeoNet NZ Quakes Feed integration.""" import datetime -from asynctest import patch - from homeassistant.components import geonetnz_quakes from homeassistant.components.geonetnz_quakes import DEFAULT_SCAN_INTERVAL from homeassistant.components.geonetnz_quakes.sensor import ( @@ -22,6 +20,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.geonetnz_quakes import _generate_mock_feed_entry diff --git a/tests/components/geonetnz_volcano/test_init.py b/tests/components/geonetnz_volcano/test_init.py index 3e2566ffb81711..4edf8f452fe1e2 100644 --- a/tests/components/geonetnz_volcano/test_init.py +++ b/tests/components/geonetnz_volcano/test_init.py @@ -1,15 +1,15 @@ """Define tests for the GeoNet NZ Volcano general setup.""" -from asynctest import CoroutineMock, patch - from homeassistant.components.geonetnz_volcano import DOMAIN, FEED +from tests.async_mock import AsyncMock, patch + async def test_component_unload_config_entry(hass, config_entry): """Test that loading and unloading of a config entry works.""" config_entry.add_to_hass(hass) with patch( "aio_geojson_geonetnz_volcano.GeonetnzVolcanoFeedManager.update", - new_callable=CoroutineMock, + new_callable=AsyncMock, ) as mock_feed_manager_update: # Load config entry. assert await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/geonetnz_volcano/test_sensor.py b/tests/components/geonetnz_volcano/test_sensor.py index 8f71e3c475708b..ccf6248bfa9063 100644 --- a/tests/components/geonetnz_volcano/test_sensor.py +++ b/tests/components/geonetnz_volcano/test_sensor.py @@ -1,6 +1,4 @@ """The tests for the GeoNet NZ Volcano Feed integration.""" -from asynctest import CoroutineMock, patch - from homeassistant.components import geonetnz_volcano from homeassistant.components.geo_location import ATTR_DISTANCE from homeassistant.components.geonetnz_volcano import DEFAULT_SCAN_INTERVAL @@ -23,6 +21,7 @@ import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM +from tests.async_mock import AsyncMock, patch from tests.common import async_fire_time_changed from tests.components.geonetnz_volcano import _generate_mock_feed_entry @@ -49,7 +48,7 @@ async def test_setup(hass): # Patching 'utcnow' to gain more control over the timed update. utcnow = dt_util.utcnow() with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( - "aio_geojson_client.feed.GeoJsonFeed.update", new_callable=CoroutineMock + "aio_geojson_client.feed.GeoJsonFeed.update", new_callable=AsyncMock ) as mock_feed_update: mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_2, mock_entry_3] assert await async_setup_component(hass, geonetnz_volcano.DOMAIN, CONFIG) @@ -139,7 +138,7 @@ async def test_setup_imperial(hass): # Patching 'utcnow' to gain more control over the timed update. utcnow = dt_util.utcnow() with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( - "aio_geojson_client.feed.GeoJsonFeed.update", new_callable=CoroutineMock + "aio_geojson_client.feed.GeoJsonFeed.update", new_callable=AsyncMock ) as mock_feed_update, patch( "aio_geojson_client.feed.GeoJsonFeed.__init__" ) as mock_feed_init: diff --git a/tests/components/gios/test_config_flow.py b/tests/components/gios/test_config_flow.py index 3a4aff6d9ad695..b2f7ceec9e4f01 100644 --- a/tests/components/gios/test_config_flow.py +++ b/tests/components/gios/test_config_flow.py @@ -1,5 +1,4 @@ """Define tests for the GIOS config flow.""" -from asynctest import patch from gios import ApiError from homeassistant import data_entry_flow @@ -7,6 +6,8 @@ from homeassistant.components.gios.const import CONF_STATION_ID from homeassistant.const import CONF_NAME +from tests.async_mock import patch + CONFIG = { CONF_NAME: "Foo", CONF_STATION_ID: 123, diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 79684bdeb44e81..8f6b0908f83439 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -1,8 +1,8 @@ """Tests for the Google Assistant integration.""" -from asynctest.mock import MagicMock - from homeassistant.components.google_assistant import helpers +from tests.async_mock import MagicMock + def mock_google_config_store(agent_user_ids=None): """Fake a storage for google assistant.""" diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py index 9f6f83fab59c4e..4943212bd5aca5 100644 --- a/tests/components/google_assistant/test_helpers.py +++ b/tests/components/google_assistant/test_helpers.py @@ -1,7 +1,6 @@ """Test Google Assistant helpers.""" from datetime import timedelta -from asynctest.mock import Mock, call, patch import pytest from homeassistant.components.google_assistant import helpers @@ -14,6 +13,7 @@ from . import MockConfig +from tests.async_mock import Mock, call, patch from tests.common import ( async_capture_events, async_fire_time_changed, diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py index ff159e4e10c04a..4b9461e6304cf3 100644 --- a/tests/components/google_assistant/test_http.py +++ b/tests/components/google_assistant/test_http.py @@ -1,8 +1,6 @@ """Test Google http services.""" from datetime import datetime, timedelta, timezone -from asynctest import ANY, patch - from homeassistant.components.google_assistant import GOOGLE_ASSISTANT_SCHEMA from homeassistant.components.google_assistant.const import ( HOMEGRAPH_TOKEN_URL, @@ -14,6 +12,8 @@ _get_homegraph_token, ) +from tests.async_mock import ANY, patch + DUMMY_CONFIG = GOOGLE_ASSISTANT_SCHEMA( { "project_id": "1234", diff --git a/tests/components/google_assistant/test_report_state.py b/tests/components/google_assistant/test_report_state.py index fd9cad27ffa57b..7dd3faba7abd20 100644 --- a/tests/components/google_assistant/test_report_state.py +++ b/tests/components/google_assistant/test_report_state.py @@ -1,12 +1,11 @@ """Test Google report state.""" -from unittest.mock import patch - from homeassistant.components.google_assistant import error, report_state from homeassistant.util.dt import utcnow from . import BASIC_CONFIG -from tests.common import async_fire_time_changed, mock_coro +from tests.async_mock import AsyncMock, patch +from tests.common import async_fire_time_changed async def test_report_state(hass, caplog): @@ -15,7 +14,7 @@ async def test_report_state(hass, caplog): hass.states.async_set("switch.ac", "on") with patch.object( - BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", AsyncMock() ) as mock_report, patch.object(report_state, "INITIAL_REPORT_DELAY", 0): unsub = report_state.async_enable_report_state(hass, BASIC_CONFIG) @@ -34,7 +33,7 @@ async def test_report_state(hass, caplog): } with patch.object( - BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", AsyncMock() ) as mock_report: hass.states.async_set("light.kitchen", "on") await hass.async_block_till_done() @@ -47,7 +46,7 @@ async def test_report_state(hass, caplog): # Test that state changes that change something that Google doesn't care about # do not trigger a state report. with patch.object( - BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", AsyncMock() ) as mock_report: hass.states.async_set( "light.kitchen", "on", {"irrelevant": "should_be_ignored"} @@ -58,7 +57,7 @@ async def test_report_state(hass, caplog): # Test that entities that we can't query don't report a state with patch.object( - BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", AsyncMock() ) as mock_report, patch( "homeassistant.components.google_assistant.report_state.GoogleEntity.query_serialize", side_effect=error.SmartHomeError("mock-error", "mock-msg"), @@ -72,7 +71,7 @@ async def test_report_state(hass, caplog): unsub() with patch.object( - BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", AsyncMock() ) as mock_report: hass.states.async_set("light.kitchen", "on") await hass.async_block_till_done() diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index d3c9da94f04553..c469b8b9efe767 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -1,5 +1,4 @@ """Test Google Smart Home.""" -from asynctest import Mock, patch import pytest from homeassistant.components import camera @@ -28,6 +27,7 @@ from . import BASIC_CONFIG, MockConfig +from tests.async_mock import Mock, patch from tests.common import mock_area_registry, mock_device_registry, mock_registry REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf" diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index fed084586ccec0..f133db26d89234 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -2,7 +2,6 @@ import logging from unittest.mock import Mock -from asynctest import patch import pytest from homeassistant.components import ( @@ -46,6 +45,7 @@ from . import BASIC_CONFIG, MockConfig +from tests.async_mock import patch from tests.common import async_mock_service _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/griddy/test_config_flow.py b/tests/components/griddy/test_config_flow.py index 1ab29aebece5eb..79f99d7f8b1ec4 100644 --- a/tests/components/griddy/test_config_flow.py +++ b/tests/components/griddy/test_config_flow.py @@ -1,11 +1,11 @@ """Test the Griddy Power config flow.""" import asyncio -from asynctest import MagicMock, patch - from homeassistant import config_entries, setup from homeassistant.components.griddy.const import DOMAIN +from tests.async_mock import MagicMock, patch + async def test_form(hass): """Test we get the form.""" diff --git a/tests/components/griddy/test_sensor.py b/tests/components/griddy/test_sensor.py index 995327a9b562fa..ae3d0c3be84e78 100644 --- a/tests/components/griddy/test_sensor.py +++ b/tests/components/griddy/test_sensor.py @@ -2,12 +2,12 @@ import json import os -from asynctest import patch from griddypower.async_api import GriddyPriceData from homeassistant.components.griddy import CONF_LOADZONE, DOMAIN from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import load_fixture diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 94d78d6287713e..ed807ed57d9b20 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -1,8 +1,6 @@ """The tests for the Group Light platform.""" from unittest.mock import MagicMock -import asynctest - from homeassistant.components.group import DOMAIN import homeassistant.components.group.light as group from homeassistant.components.light import ( @@ -32,6 +30,8 @@ ) from homeassistant.setup import async_setup_component +import tests.async_mock + async def test_default_state(hass): """Test light group default state.""" @@ -559,7 +559,7 @@ async def test_invalid_service_calls(hass): grouped_light = add_entities.call_args[0][0][0] grouped_light.hass = hass - with asynctest.patch.object(hass.services, "async_call") as mock_call: + with tests.async_mock.patch.object(hass.services, "async_call") as mock_call: await grouped_light.async_turn_on(brightness=150, four_oh_four="404") data = {ATTR_ENTITY_ID: ["light.test1", "light.test2"], ATTR_BRIGHTNESS: 150} mock_call.assert_called_once_with( diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index 30421756d22514..079923330e2a4e 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -1,15 +1,15 @@ """Test the Logitech Harmony Hub config flow.""" -from asynctest import CoroutineMock, MagicMock, patch - from homeassistant import config_entries, setup from homeassistant.components.harmony.config_flow import CannotConnect from homeassistant.components.harmony.const import DOMAIN +from tests.async_mock import AsyncMock, MagicMock, patch + def _get_mock_harmonyapi(connect=None, close=None): harmonyapi_mock = MagicMock() - type(harmonyapi_mock).connect = CoroutineMock(return_value=connect) - type(harmonyapi_mock).close = CoroutineMock(return_value=close) + type(harmonyapi_mock).connect = AsyncMock(return_value=connect) + type(harmonyapi_mock).close = AsyncMock(return_value=close) return harmonyapi_mock diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index 9a50da4ce41414..abcf62915b64f4 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -1,6 +1,5 @@ """Fixtures for Hass.io.""" import os -from unittest.mock import Mock, patch import pytest @@ -10,7 +9,7 @@ from . import HASSIO_TOKEN -from tests.common import mock_coro +from tests.async_mock import Mock, patch @pytest.fixture @@ -18,7 +17,7 @@ def hassio_env(): """Fixture to inject hassio env.""" with patch.dict(os.environ, {"HASSIO": "127.0.0.1"}), patch( "homeassistant.components.hassio.HassIO.is_connected", - Mock(return_value=mock_coro({"result": "ok", "data": {}})), + return_value={"result": "ok", "data": {}}, ), patch.dict(os.environ, {"HASSIO_TOKEN": "123456"}), patch( "homeassistant.components.hassio.HassIO.get_homeassistant_info", Mock(side_effect=HassioAPIError()), @@ -31,10 +30,10 @@ def hassio_stubs(hassio_env, hass, hass_client, aioclient_mock): """Create mock hassio http client.""" with patch( "homeassistant.components.hassio.HassIO.update_hass_api", - return_value=mock_coro({"result": "ok"}), + return_value={"result": "ok"}, ) as hass_api, patch( "homeassistant.components.hassio.HassIO.update_hass_timezone", - return_value=mock_coro({"result": "ok"}), + return_value={"result": "ok"}, ), patch( "homeassistant.components.hassio.HassIO.get_homeassistant_info", side_effect=HassioAPIError(), diff --git a/tests/components/hassio/test_addon_panel.py b/tests/components/hassio/test_addon_panel.py index d2ad673111d8ad..9b11fd6dfc2b21 100644 --- a/tests/components/hassio/test_addon_panel.py +++ b/tests/components/hassio/test_addon_panel.py @@ -1,11 +1,9 @@ """Test add-on panel.""" -from unittest.mock import Mock, patch - import pytest from homeassistant.setup import async_setup_component -from tests.common import mock_coro +from tests.async_mock import patch @pytest.fixture(autouse=True) @@ -49,7 +47,6 @@ async def test_hassio_addon_panel_startup(hass, aioclient_mock, hassio_env): with patch( "homeassistant.components.hassio.addon_panel._register_panel", - Mock(return_value=mock_coro()), ) as mock_panel: await async_setup_component(hass, "hassio", {}) await hass.async_block_till_done() @@ -92,7 +89,6 @@ async def test_hassio_addon_panel_api(hass, aioclient_mock, hassio_env, hass_cli with patch( "homeassistant.components.hassio.addon_panel._register_panel", - Mock(return_value=mock_coro()), ) as mock_panel: await async_setup_component(hass, "hassio", {}) await hass.async_block_till_done() diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py index 621efa1cb9efe5..e97c5bc66fb1c8 100644 --- a/tests/components/hassio/test_auth.py +++ b/tests/components/hassio/test_auth.py @@ -1,10 +1,9 @@ """The tests for the hassio component.""" -from unittest.mock import Mock, patch from homeassistant.const import HTTP_INTERNAL_SERVER_ERROR from homeassistant.exceptions import HomeAssistantError -from tests.common import mock_coro +from tests.async_mock import Mock, patch async def test_auth_success(hass, hassio_client_supervisor): @@ -12,7 +11,6 @@ async def test_auth_success(hass, hassio_client_supervisor): with patch( "homeassistant.auth.providers.homeassistant." "HassAuthProvider.async_validate_login", - Mock(return_value=mock_coro()), ) as mock_login: resp = await hassio_client_supervisor.post( "/api/hassio_auth", @@ -29,7 +27,6 @@ async def test_auth_fails_no_supervisor(hass, hassio_client): with patch( "homeassistant.auth.providers.homeassistant." "HassAuthProvider.async_validate_login", - Mock(return_value=mock_coro()), ) as mock_login: resp = await hassio_client.post( "/api/hassio_auth", @@ -46,7 +43,6 @@ async def test_auth_fails_no_auth(hass, hassio_noauth_client): with patch( "homeassistant.auth.providers.homeassistant." "HassAuthProvider.async_validate_login", - Mock(return_value=mock_coro()), ) as mock_login: resp = await hassio_noauth_client.post( "/api/hassio_auth", @@ -110,7 +106,6 @@ async def test_login_success_extra(hass, hassio_client_supervisor): with patch( "homeassistant.auth.providers.homeassistant." "HassAuthProvider.async_validate_login", - Mock(return_value=mock_coro()), ) as mock_login: resp = await hassio_client_supervisor.post( "/api/hassio_auth", @@ -131,7 +126,6 @@ async def test_password_success(hass, hassio_client_supervisor): """Test no auth needed for .""" with patch( "homeassistant.components.hassio.auth.HassIOPasswordReset._change_password", - Mock(return_value=mock_coro()), ) as mock_change: resp = await hassio_client_supervisor.post( "/api/hassio_auth/password_reset", @@ -147,7 +141,6 @@ async def test_password_fails_no_supervisor(hass, hassio_client): """Test if only supervisor can access.""" with patch( "homeassistant.auth.providers.homeassistant.Data.async_save", - Mock(return_value=mock_coro()), ) as mock_save: resp = await hassio_client.post( "/api/hassio_auth/password_reset", @@ -163,7 +156,6 @@ async def test_password_fails_no_auth(hass, hassio_noauth_client): """Test if only supervisor can access.""" with patch( "homeassistant.auth.providers.homeassistant.Data.async_save", - Mock(return_value=mock_coro()), ) as mock_save: resp = await hassio_noauth_client.post( "/api/hassio_auth/password_reset", @@ -179,7 +171,6 @@ async def test_password_no_user(hass, hassio_client_supervisor): """Test no auth needed for .""" with patch( "homeassistant.auth.providers.homeassistant.Data.async_save", - Mock(return_value=mock_coro()), ) as mock_save: resp = await hassio_client_supervisor.post( "/api/hassio_auth/password_reset", diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py index a0d6444004137e..fd4fa26f813d9b 100644 --- a/tests/components/hassio/test_discovery.py +++ b/tests/components/hassio/test_discovery.py @@ -1,11 +1,9 @@ """Test config flow.""" -from unittest.mock import Mock, patch - from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component -from tests.common import mock_coro +from tests.async_mock import Mock, patch async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client): @@ -41,7 +39,7 @@ async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client): with patch( "homeassistant.components.mqtt.config_flow.FlowHandler.async_step_hassio", - Mock(return_value=mock_coro({"type": "abort"})), + return_value={"type": "abort"}, ) as mock_mqtt: hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -91,13 +89,13 @@ async def test_hassio_discovery_startup_done(hass, aioclient_mock, hassio_client with patch( "homeassistant.components.hassio.HassIO.update_hass_api", - Mock(return_value=mock_coro({"result": "ok"})), + return_value={"result": "ok"}, ), patch( "homeassistant.components.hassio.HassIO.get_homeassistant_info", Mock(side_effect=HassioAPIError()), ), patch( "homeassistant.components.mqtt.config_flow.FlowHandler.async_step_hassio", - Mock(return_value=mock_coro({"type": "abort"})), + return_value={"type": "abort"}, ) as mock_mqtt: await hass.async_start() await async_setup_component(hass, "hassio", {}) @@ -144,7 +142,7 @@ async def test_hassio_discovery_webhook(hass, aioclient_mock, hassio_client): with patch( "homeassistant.components.mqtt.config_flow.FlowHandler.async_step_hassio", - Mock(return_value=mock_coro({"type": "abort"})), + return_value={"type": "abort"}, ) as mock_mqtt: resp = await hassio_client.post( "/api/hassio_push/discovery/testuuid", diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 26caec65b40fa9..13bae0014488c9 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -1,7 +1,6 @@ """The tests for the hassio component.""" import os -from asynctest import patch import pytest from homeassistant.auth.const import GROUP_ID_ADMIN @@ -9,6 +8,8 @@ from homeassistant.components.hassio import STORAGE_KEY from homeassistant.setup import async_setup_component +from tests.async_mock import patch + MOCK_ENVIRON = {"HASSIO": "127.0.0.1", "HASSIO_TOKEN": "abcdefgh"} diff --git a/tests/components/heos/conftest.py b/tests/components/heos/conftest.py index 5201b7f7b8ada9..86be36e81883c3 100644 --- a/tests/components/heos/conftest.py +++ b/tests/components/heos/conftest.py @@ -1,7 +1,6 @@ """Configuration for HEOS tests.""" from typing import Dict, Sequence -from asynctest.mock import Mock, patch as patch from pyheos import Dispatcher, Heos, HeosPlayer, HeosSource, InputSource, const import pytest @@ -9,6 +8,7 @@ from homeassistant.components.heos import DOMAIN from homeassistant.const import CONF_HOST +from tests.async_mock import Mock, patch as patch from tests.common import MockConfigEntry diff --git a/tests/components/heos/test_config_flow.py b/tests/components/heos/test_config_flow.py index b83923943bd849..d90c426324056f 100644 --- a/tests/components/heos/test_config_flow.py +++ b/tests/components/heos/test_config_flow.py @@ -1,7 +1,6 @@ """Tests for the Heos config flow module.""" from urllib.parse import urlparse -from asynctest import patch from pyheos import HeosError from homeassistant import data_entry_flow @@ -10,6 +9,8 @@ from homeassistant.components.heos.const import DATA_DISCOVERED_HOSTS from homeassistant.const import CONF_HOST +from tests.async_mock import patch + async def test_flow_aborts_already_setup(hass, config_entry): """Test flow aborts when entry already setup.""" diff --git a/tests/components/heos/test_init.py b/tests/components/heos/test_init.py index cfbdcb9198abdb..a6852e3db41890 100644 --- a/tests/components/heos/test_init.py +++ b/tests/components/heos/test_init.py @@ -1,7 +1,6 @@ """Tests for the init module.""" import asyncio -from asynctest import Mock, patch from pyheos import CommandFailedError, HeosError, const import pytest @@ -20,6 +19,8 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.setup import async_setup_component +from tests.async_mock import Mock, patch + async def test_async_setup_creates_entry(hass, config): """Test component setup creates entry from config.""" diff --git a/tests/components/hisense_aehw4a1/test_init.py b/tests/components/hisense_aehw4a1/test_init.py index f2af78fe160d68..498e8c4c306239 100644 --- a/tests/components/hisense_aehw4a1/test_init.py +++ b/tests/components/hisense_aehw4a1/test_init.py @@ -1,11 +1,12 @@ """Tests for the Hisense AEH-W4A1 init file.""" -from asynctest import patch from pyaehw4a1 import exceptions from homeassistant import config_entries, data_entry_flow from homeassistant.components import hisense_aehw4a1 from homeassistant.setup import async_setup_component +from tests.async_mock import patch + async def test_creating_entry_sets_up_climate_discovery(hass): """Test setting up Hisense AEH-W4A1 loads the climate component.""" diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index fddd149942e6cd..b9309d70d63dbb 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -3,7 +3,6 @@ import asyncio import unittest -from asynctest import Mock, patch import pytest import voluptuous as vol import yaml @@ -33,6 +32,7 @@ from homeassistant.helpers import entity from homeassistant.setup import async_setup_component +from tests.async_mock import Mock, patch from tests.common import ( async_capture_events, async_mock_service, diff --git a/tests/components/homekit/test_aidmanager.py b/tests/components/homekit/test_aidmanager.py index 12d12082a33fa6..258f26e78a61db 100644 --- a/tests/components/homekit/test_aidmanager.py +++ b/tests/components/homekit/test_aidmanager.py @@ -2,7 +2,6 @@ import os from zlib import adler32 -from asynctest import patch import pytest from homeassistant.components.homekit.aidmanager import ( @@ -13,6 +12,7 @@ from homeassistant.helpers import device_registry from homeassistant.helpers.storage import STORAGE_DIR +from tests.async_mock import patch from tests.common import MockConfigEntry, mock_device_registry, mock_registry diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index b63ee6d0bd9780..8a1d911ef04b73 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1,7 +1,6 @@ """Tests for the HomeKit component.""" from unittest.mock import ANY, Mock, patch -from asynctest import CoroutineMock import pytest from zeroconf import InterfaceChoice @@ -44,6 +43,7 @@ from homeassistant.helpers import device_registry from homeassistant.helpers.entityfilter import generate_filter +from tests.async_mock import AsyncMock from tests.common import MockConfigEntry, mock_device_registry, mock_registry from tests.components.homekit.common import patch_debounce @@ -104,7 +104,7 @@ async def test_setup_auto_start_disabled(hass): with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit: mock_homekit.return_value = homekit = Mock() - type(homekit).async_start = CoroutineMock() + type(homekit).async_start = AsyncMock() assert await setup.async_setup_component(hass, DOMAIN, config) mock_homekit.assert_any_call( diff --git a/tests/components/homekit/test_init.py b/tests/components/homekit/test_init.py index e01588305d5137..6d01413da8f0d7 100644 --- a/tests/components/homekit/test_init.py +++ b/tests/components/homekit/test_init.py @@ -1,6 +1,4 @@ """Test HomeKit initialization.""" -from asynctest import patch - from homeassistant import core as ha from homeassistant.components import logbook from homeassistant.components.homekit.const import ( @@ -12,6 +10,8 @@ from homeassistant.const import ATTR_ENTITY_ID, ATTR_SERVICE from homeassistant.setup import async_setup_component +from tests.async_mock import patch + async def test_humanify_homekit_changed_event(hass, hk_driver): """Test humanifying HomeKit changed event.""" diff --git a/tests/components/homekit_controller/conftest.py b/tests/components/homekit_controller/conftest.py index 99e86335cdb867..ac4a0b4b5d620a 100644 --- a/tests/components/homekit_controller/conftest.py +++ b/tests/components/homekit_controller/conftest.py @@ -3,9 +3,10 @@ from unittest import mock from aiohomekit.testing import FakeController -import asynctest import pytest +import tests.async_mock + @pytest.fixture def utcnow(request): @@ -20,5 +21,5 @@ def utcnow(request): def controller(hass): """Replace aiohomekit.Controller with an instance of aiohomekit.testing.FakeController.""" instance = FakeController() - with asynctest.patch("aiohomekit.Controller", return_value=instance): + with tests.async_mock.patch("aiohomekit.Controller", return_value=instance): yield instance diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 302104c0f492ea..a9aef723164edc 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -5,12 +5,12 @@ from aiohomekit.model import Accessories, Accessory from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -import asynctest -from asynctest import patch import pytest from homeassistant.components.homekit_controller import config_flow +import tests.async_mock +from tests.async_mock import patch from tests.common import MockConfigEntry PAIRING_START_FORM_ERRORS = [ @@ -63,15 +63,15 @@ def _setup_flow_handler(hass, pairing=None): flow.hass = hass flow.context = {} - finish_pairing = asynctest.CoroutineMock(return_value=pairing) + finish_pairing = tests.async_mock.AsyncMock(return_value=pairing) discovery = mock.Mock() discovery.device_id = "00:00:00:00:00:00" - discovery.start_pairing = asynctest.CoroutineMock(return_value=finish_pairing) + discovery.start_pairing = tests.async_mock.AsyncMock(return_value=finish_pairing) flow.controller = mock.Mock() flow.controller.pairings = {} - flow.controller.find_ip_by_device_id = asynctest.CoroutineMock( + flow.controller.find_ip_by_device_id = tests.async_mock.AsyncMock( return_value=discovery ) @@ -368,7 +368,7 @@ async def test_pair_abort_errors_on_finish(hass, controller, exception, expected # User initiates pairing - this triggers the device to show a pairing code # and then HA to show a pairing form - finish_pairing = asynctest.CoroutineMock(side_effect=exception("error")) + finish_pairing = tests.async_mock.AsyncMock(side_effect=exception("error")) with patch.object(device, "start_pairing", return_value=finish_pairing): result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -408,7 +408,7 @@ async def test_pair_form_errors_on_finish(hass, controller, exception, expected) # User initiates pairing - this triggers the device to show a pairing code # and then HA to show a pairing form - finish_pairing = asynctest.CoroutineMock(side_effect=exception("error")) + finish_pairing = tests.async_mock.AsyncMock(side_effect=exception("error")) with patch.object(device, "start_pairing", return_value=finish_pairing): result = await hass.config_entries.flow.async_configure(result["flow_id"]) diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index b1933604fbe8d1..3ada7e7de0a27d 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -1,5 +1,4 @@ """Initializer helpers for HomematicIP fake server.""" -from asynctest import CoroutineMock, MagicMock, Mock, patch from homematicip.aio.auth import AsyncAuth from homematicip.aio.connection import AsyncConnection from homematicip.aio.home import AsyncHome @@ -23,6 +22,7 @@ from .helper import AUTH_TOKEN, HAPID, HAPPIN, HomeFactory +from tests.async_mock import AsyncMock, MagicMock, Mock, patch from tests.common import MockConfigEntry @@ -37,8 +37,8 @@ def _rest_call_side_effect(path, body=None): connection._restCall.side_effect = ( # pylint: disable=protected-access _rest_call_side_effect ) - connection.api_call = CoroutineMock(return_value=True) - connection.init = CoroutineMock(side_effect=True) + connection.api_call = AsyncMock(return_value=True) + connection.init = AsyncMock(side_effect=True) return connection diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index 403dbd873bef70..fede095e57d6a9 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -1,7 +1,6 @@ """Helper for HomematicIP Cloud Tests.""" import json -from asynctest import Mock, patch from homematicip.aio.class_maps import ( TYPE_CLASS_MAP, TYPE_GROUP_MAP, @@ -22,6 +21,7 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component +from tests.async_mock import Mock, patch from tests.common import load_fixture HAPID = "3014F7110000000000000001" diff --git a/tests/components/homematicip_cloud/test_config_flow.py b/tests/components/homematicip_cloud/test_config_flow.py index e6e145fefbacac..e9ecab2dbfbbc6 100644 --- a/tests/components/homematicip_cloud/test_config_flow.py +++ b/tests/components/homematicip_cloud/test_config_flow.py @@ -1,6 +1,4 @@ """Tests for HomematicIP Cloud config flow.""" -from asynctest import patch - from homeassistant.components.homematicip_cloud.const import ( DOMAIN as HMIPC_DOMAIN, HMIPC_AUTHTOKEN, @@ -9,6 +7,7 @@ HMIPC_PIN, ) +from tests.async_mock import patch from tests.common import MockConfigEntry DEFAULT_CONFIG = {HMIPC_HAPID: "ABC123", HMIPC_PIN: "123", HMIPC_NAME: "hmip"} diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 71efac3a7c91dd..8a8d52d167a82c 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -1,5 +1,4 @@ """Common tests for HomematicIP devices.""" -from asynctest import patch from homematicip.base.enums import EventType from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN @@ -14,6 +13,8 @@ get_and_check_entity_basics, ) +from tests.async_mock import patch + async def test_hmip_load_all_supported_devices(hass, default_mock_hap_factory): """Ensure that all supported devices could be loaded.""" diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index e6e143973f3dbf..ca701622e900cb 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -1,6 +1,5 @@ """Test HomematicIP Cloud accesspoint.""" -from asynctest import Mock, patch from homematicip.aio.auth import AsyncAuth from homematicip.base.base_connection import HmipConnectionError import pytest @@ -22,6 +21,8 @@ from .helper import HAPID, HAPPIN +from tests.async_mock import Mock, patch + async def test_auth_setup(hass): """Test auth setup for client registration.""" diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index 8f2753bc499dd0..5b201da8aa815d 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -1,6 +1,5 @@ """Test HomematicIP Cloud setup process.""" -from asynctest import CoroutineMock, Mock, patch from homematicip.base.base_connection import HmipConnectionError from homeassistant.components.homematicip_cloud.const import ( @@ -21,6 +20,7 @@ from homeassistant.const import CONF_NAME from homeassistant.setup import async_setup_component +from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry @@ -139,12 +139,12 @@ async def test_unload_entry(hass): with patch("homeassistant.components.homematicip_cloud.HomematicipHAP") as mock_hap: instance = mock_hap.return_value - instance.async_setup = CoroutineMock(return_value=True) + instance.async_setup = AsyncMock(return_value=True) instance.home.id = "1" instance.home.modelType = "mock-type" instance.home.name = "mock-name" instance.home.currentAPVersion = "mock-ap-version" - instance.async_reset = CoroutineMock(return_value=True) + instance.async_reset = AsyncMock(return_value=True) assert await async_setup_component(hass, HMIPC_DOMAIN, {}) @@ -181,12 +181,12 @@ async def test_setup_services_and_unload_services(hass): with patch("homeassistant.components.homematicip_cloud.HomematicipHAP") as mock_hap: instance = mock_hap.return_value - instance.async_setup = CoroutineMock(return_value=True) + instance.async_setup = AsyncMock(return_value=True) instance.home.id = "1" instance.home.modelType = "mock-type" instance.home.name = "mock-name" instance.home.currentAPVersion = "mock-ap-version" - instance.async_reset = CoroutineMock(return_value=True) + instance.async_reset = AsyncMock(return_value=True) assert await async_setup_component(hass, HMIPC_DOMAIN, {}) @@ -214,12 +214,12 @@ async def test_setup_two_haps_unload_one_by_one(hass): with patch("homeassistant.components.homematicip_cloud.HomematicipHAP") as mock_hap: instance = mock_hap.return_value - instance.async_setup = CoroutineMock(return_value=True) + instance.async_setup = AsyncMock(return_value=True) instance.home.id = "1" instance.home.modelType = "mock-type" instance.home.name = "mock-name" instance.home.currentAPVersion = "mock-ap-version" - instance.async_reset = CoroutineMock(return_value=True) + instance.async_reset = AsyncMock(return_value=True) assert await async_setup_component(hass, HMIPC_DOMAIN, {}) diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index ddf08de42b4a04..d13581e12a2d1d 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -7,7 +7,6 @@ from aiohttp import web from aiohttp.web_exceptions import HTTPUnauthorized from aiohttp.web_middlewares import middleware -from asynctest import patch import pytest import homeassistant.components.http as http @@ -25,6 +24,8 @@ from . import mock_real_ip +from tests.async_mock import patch + SUPERVISOR_IP = "1.2.3.4" BANNED_IPS = ["200.201.202.203", "100.64.0.2"] BANNED_IPS_WITH_SUPERVISOR = BANNED_IPS + [SUPERVISOR_IP] diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 780c77e0196361..385097514f8ab4 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -1,15 +1,16 @@ """Test Hue bridge.""" -from asynctest import CoroutineMock, Mock, patch import pytest from homeassistant.components.hue import bridge, errors from homeassistant.exceptions import ConfigEntryNotReady +from tests.async_mock import AsyncMock, Mock, patch + async def test_bridge_setup(hass): """Test a successful setup.""" entry = Mock() - api = Mock(initialize=CoroutineMock()) + api = Mock(initialize=AsyncMock()) entry.data = {"host": "1.2.3.4", "username": "mock-username"} hue_bridge = bridge.HueBridge(hass, entry, False, False) @@ -92,7 +93,7 @@ async def test_reset_unloads_entry_if_setup(hass): async def test_handle_unauthorized(hass): """Test handling an unauthorized error on update.""" - entry = Mock(async_setup=CoroutineMock()) + entry = Mock(async_setup=AsyncMock()) entry.data = {"host": "1.2.3.4", "username": "mock-username"} hue_bridge = bridge.HueBridge(hass, entry, False, False) diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 87d4dc2b887973..b5ea2e4e0eacb0 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -5,7 +5,6 @@ from aiohttp import client_exceptions import aiohue from aiohue.discovery import URL_NUPNP -from asynctest import CoroutineMock, patch import pytest import voluptuous as vol @@ -13,6 +12,7 @@ from homeassistant.components import ssdp from homeassistant.components.hue import config_flow, const +from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry @@ -41,7 +41,7 @@ async def create_user(username): mock_create_user = create_user mock_bridge.create_user = mock_create_user - mock_bridge.initialize = CoroutineMock() + mock_bridge.initialize = AsyncMock() return mock_bridge @@ -190,7 +190,7 @@ async def test_flow_timeout_discovery(hass): async def test_flow_link_timeout(hass): """Test config flow.""" mock_bridge = get_mock_bridge( - mock_create_user=CoroutineMock(side_effect=asyncio.TimeoutError), + mock_create_user=AsyncMock(side_effect=asyncio.TimeoutError), ) with patch( "homeassistant.components.hue.config_flow.discover_nupnp", @@ -211,7 +211,7 @@ async def test_flow_link_timeout(hass): async def test_flow_link_unknown_error(hass): """Test if a unknown error happened during the linking processes.""" - mock_bridge = get_mock_bridge(mock_create_user=CoroutineMock(side_effect=OSError),) + mock_bridge = get_mock_bridge(mock_create_user=AsyncMock(side_effect=OSError),) with patch( "homeassistant.components.hue.config_flow.discover_nupnp", return_value=[mock_bridge], @@ -232,7 +232,7 @@ async def test_flow_link_unknown_error(hass): async def test_flow_link_button_not_pressed(hass): """Test config flow .""" mock_bridge = get_mock_bridge( - mock_create_user=CoroutineMock(side_effect=aiohue.LinkButtonNotPressed), + mock_create_user=AsyncMock(side_effect=aiohue.LinkButtonNotPressed), ) with patch( "homeassistant.components.hue.config_flow.discover_nupnp", @@ -254,7 +254,7 @@ async def test_flow_link_button_not_pressed(hass): async def test_flow_link_unknown_host(hass): """Test config flow .""" mock_bridge = get_mock_bridge( - mock_create_user=CoroutineMock(side_effect=client_exceptions.ClientOSError), + mock_create_user=AsyncMock(side_effect=client_exceptions.ClientOSError), ) with patch( "homeassistant.components.hue.config_flow.discover_nupnp", diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index 51ea3f2ae71623..a144902bbc8870 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -1,11 +1,10 @@ """Test Hue setup process.""" from unittest.mock import Mock -from asynctest import CoroutineMock, patch - from homeassistant.components import hue from homeassistant.setup import async_setup_component +from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry, mock_coro @@ -102,9 +101,9 @@ async def test_config_passed_to_config_entry(hass): mock_registry = Mock() with patch.object(hue, "HueBridge") as mock_bridge, patch( "homeassistant.helpers.device_registry.async_get_registry", - return_value=mock_coro(mock_registry), + return_value=mock_registry, ): - mock_bridge.return_value.async_setup.return_value = mock_coro(True) + mock_bridge.return_value.async_setup = AsyncMock(return_value=True) mock_bridge.return_value.api.config = Mock( mac="mock-mac", bridgeid="mock-bridgeid", @@ -159,13 +158,13 @@ async def test_unload_entry(hass): "homeassistant.helpers.device_registry.async_get_registry", return_value=mock_coro(Mock()), ): - mock_bridge.return_value.async_setup.return_value = mock_coro(True) + mock_bridge.return_value.async_setup = AsyncMock(return_value=True) mock_bridge.return_value.api.config = Mock(bridgeid="aabbccddeeff") assert await async_setup_component(hass, hue.DOMAIN, {}) is True assert len(mock_bridge.return_value.mock_calls) == 1 - mock_bridge.return_value.async_reset.return_value = mock_coro(True) + mock_bridge.return_value.async_reset = AsyncMock(return_value=True) assert await hue.async_unload_entry(hass, entry) assert len(mock_bridge.return_value.async_reset.mock_calls) == 1 assert hass.data[hue.DOMAIN] == {} @@ -180,7 +179,7 @@ async def test_setting_unique_id(hass): "homeassistant.helpers.device_registry.async_get_registry", return_value=mock_coro(Mock()), ): - mock_bridge.return_value.async_setup.return_value = mock_coro(True) + mock_bridge.return_value.async_setup = AsyncMock(return_value=True) mock_bridge.return_value.api.config = Mock(bridgeid="mock-id") assert await async_setup_component(hass, hue.DOMAIN, {}) is True @@ -201,7 +200,7 @@ async def test_security_vuln_check(hass): "HueBridge", Mock( return_value=Mock( - async_setup=CoroutineMock(return_value=True), api=Mock(config=config) + async_setup=AsyncMock(return_value=True), api=Mock(config=config) ) ), ): diff --git a/tests/components/hunterdouglas_powerview/test_config_flow.py b/tests/components/hunterdouglas_powerview/test_config_flow.py index 67b2c2dc954b91..e5b0d47230411e 100644 --- a/tests/components/hunterdouglas_powerview/test_config_flow.py +++ b/tests/components/hunterdouglas_powerview/test_config_flow.py @@ -2,11 +2,10 @@ import asyncio import json -from asynctest import CoroutineMock, MagicMock, patch - from homeassistant import config_entries, setup from homeassistant.components.hunterdouglas_powerview.const import DOMAIN +from tests.async_mock import AsyncMock, MagicMock, patch from tests.common import load_fixture @@ -15,13 +14,11 @@ def _get_mock_powerview_userdata(userdata=None, get_resources=None): if not userdata: userdata = json.loads(load_fixture("hunterdouglas_powerview/userdata.json")) if get_resources: - type(mock_powerview_userdata).get_resources = CoroutineMock( + type(mock_powerview_userdata).get_resources = AsyncMock( side_effect=get_resources ) else: - type(mock_powerview_userdata).get_resources = CoroutineMock( - return_value=userdata - ) + type(mock_powerview_userdata).get_resources = AsyncMock(return_value=userdata) return mock_powerview_userdata diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index 399523dd779941..a2a2ec2d7a6d35 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -1,8 +1,6 @@ """The tests for the image_processing component.""" from unittest.mock import PropertyMock -from asynctest import patch - import homeassistant.components.http as http import homeassistant.components.image_processing as ip from homeassistant.const import ATTR_ENTITY_PICTURE @@ -10,6 +8,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import ( assert_setup_component, get_test_home_assistant, diff --git a/tests/components/ipma/test_config_flow.py b/tests/components/ipma/test_config_flow.py index fd44f8b2a58742..b3b8968f451451 100644 --- a/tests/components/ipma/test_config_flow.py +++ b/tests/components/ipma/test_config_flow.py @@ -1,10 +1,9 @@ """Tests for IPMA config flow.""" -from unittest.mock import Mock, patch from homeassistant.components.ipma import config_flow from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE -from tests.common import mock_coro +from tests.async_mock import Mock, patch async def test_show_config_form(): @@ -58,8 +57,8 @@ async def test_flow_show_form(): flow = config_flow.IpmaFlowHandler() flow.hass = hass - with patch.object( - flow, "_show_config_form", return_value=mock_coro() + with patch( + "homeassistant.components.ipma.config_flow.IpmaFlowHandler._show_config_form" ) as config_form: await flow.async_step_user() assert len(config_form.mock_calls) == 1 @@ -77,10 +76,10 @@ async def test_flow_entry_created_from_user_input(): test_data = {"name": "home", CONF_LONGITUDE: "0", CONF_LATITUDE: "0"} # Test that entry created when user_input name not exists - with patch.object( - flow, "_show_config_form", return_value=mock_coro() + with patch( + "homeassistant.components.ipma.config_flow.IpmaFlowHandler._show_config_form" ) as config_form, patch.object( - flow.hass.config_entries, "async_entries", return_value=mock_coro() + flow.hass.config_entries, "async_entries", return_value=[], ) as config_entries: result = await flow.async_step_user(user_input=test_data) @@ -104,8 +103,8 @@ async def test_flow_entry_config_entry_already_exists(): test_data = {"name": "home", CONF_LONGITUDE: "0", CONF_LATITUDE: "0"} # Test that entry created when user_input name not exists - with patch.object( - flow, "_show_config_form", return_value=mock_coro() + with patch( + "homeassistant.components.ipma.config_flow.IpmaFlowHandler._show_config_form" ) as config_form, patch.object( flow.hass.config_entries, "async_entries", return_value={"home": test_data} ) as config_entries: diff --git a/tests/components/ipma/test_weather.py b/tests/components/ipma/test_weather.py index b3d398377f0275..e7542070d2c262 100644 --- a/tests/components/ipma/test_weather.py +++ b/tests/components/ipma/test_weather.py @@ -1,8 +1,6 @@ """The tests for the IPMA weather component.""" from collections import namedtuple -from asynctest import patch - from homeassistant.components import weather from homeassistant.components.weather import ( ATTR_FORECAST, @@ -23,6 +21,7 @@ from homeassistant.setup import async_setup_component from homeassistant.util.dt import now +from tests.async_mock import patch from tests.common import MockConfigEntry TEST_CONFIG = {"name": "HomeTown", "latitude": "40.00", "longitude": "-8.00"} diff --git a/tests/components/ipp/test_sensor.py b/tests/components/ipp/test_sensor.py index e6830f559c6b6f..51caadfceb3107 100644 --- a/tests/components/ipp/test_sensor.py +++ b/tests/components/ipp/test_sensor.py @@ -1,14 +1,13 @@ """Tests for the IPP sensor platform.""" from datetime import datetime -from asynctest import patch - from homeassistant.components.ipp.const import DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, UNIT_PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util +from tests.async_mock import patch from tests.components.ipp import init_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/izone/test_config_flow.py b/tests/components/izone/test_config_flow.py index 942d95cc503867..72b80e75bcf2df 100644 --- a/tests/components/izone/test_config_flow.py +++ b/tests/components/izone/test_config_flow.py @@ -1,11 +1,12 @@ """Tests for iZone.""" -from asynctest import Mock, patch import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components.izone.const import DISPATCH_CONTROLLER_DISCOVERED, IZONE +from tests.async_mock import Mock, patch + @pytest.fixture def mock_disco(): diff --git a/tests/components/konnected/test_config_flow.py b/tests/components/konnected/test_config_flow.py index 0bf6e7846ae125..a0d870b37ff0a7 100644 --- a/tests/components/konnected/test_config_flow.py +++ b/tests/components/konnected/test_config_flow.py @@ -1,10 +1,10 @@ """Tests for Konnected Alarm Panel config flow.""" -from asynctest import patch import pytest from homeassistant.components import konnected from homeassistant.components.konnected import config_flow +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/konnected/test_init.py b/tests/components/konnected/test_init.py index f87c66fe412c95..1bdd1278b93d86 100644 --- a/tests/components/konnected/test_init.py +++ b/tests/components/konnected/test_init.py @@ -1,5 +1,4 @@ """Test Konnected setup process.""" -from asynctest import patch import pytest from homeassistant.components import konnected @@ -7,6 +6,7 @@ from homeassistant.const import HTTP_NOT_FOUND from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/konnected/test_panel.py b/tests/components/konnected/test_panel.py index f1ae8a4357c002..f167c558d016b8 100644 --- a/tests/components/konnected/test_panel.py +++ b/tests/components/konnected/test_panel.py @@ -1,10 +1,10 @@ """Test Konnected setup process.""" -from asynctest import patch import pytest from homeassistant.components.konnected import config_flow, panel from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 814e304f9f5527..abe6f6ec515a16 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -5,7 +5,6 @@ import logging import unittest -from asynctest import patch import pytest import voluptuous as vol @@ -28,6 +27,7 @@ from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import get_test_home_assistant, init_recorder_component from tests.components.recorder.common import trigger_db_commit diff --git a/tests/components/logi_circle/test_config_flow.py b/tests/components/logi_circle/test_config_flow.py index 7ba3816b5e2b74..43dea15f7e687a 100644 --- a/tests/components/logi_circle/test_config_flow.py +++ b/tests/components/logi_circle/test_config_flow.py @@ -13,6 +13,7 @@ ) from homeassistant.setup import async_setup_component +from tests.async_mock import AsyncMock from tests.common import mock_coro @@ -51,8 +52,8 @@ def mock_logi_circle(): "homeassistant.components.logi_circle.config_flow.LogiCircle" ) as logi_circle: LogiCircle = logi_circle() - LogiCircle.authorize = Mock(return_value=mock_coro(return_value=True)) - LogiCircle.close = Mock(return_value=mock_coro(return_value=True)) + LogiCircle.authorize = AsyncMock(return_value=True) + LogiCircle.close = AsyncMock(return_value=True) LogiCircle.account = mock_coro(return_value={"accountId": "testId"}) LogiCircle.authorize_url = "http://authorize.url" yield LogiCircle diff --git a/tests/components/lovelace/test_resources.py b/tests/components/lovelace/test_resources.py index a44af14d3a057b..d32dc9388f1596 100644 --- a/tests/components/lovelace/test_resources.py +++ b/tests/components/lovelace/test_resources.py @@ -2,11 +2,11 @@ import copy import uuid -from asynctest import patch - from homeassistant.components.lovelace import dashboard, resources from homeassistant.setup import async_setup_component +from tests.async_mock import patch + RESOURCE_EXAMPLES = [ {"type": "js", "url": "/local/bla.js"}, {"type": "css", "url": "/local/bla.css"}, diff --git a/tests/components/luftdaten/test_config_flow.py b/tests/components/luftdaten/test_config_flow.py index e0e54a6b790cad..70b306d34c0633 100644 --- a/tests/components/luftdaten/test_config_flow.py +++ b/tests/components/luftdaten/test_config_flow.py @@ -1,13 +1,12 @@ """Define tests for the Luftdaten config flow.""" from datetime import timedelta -from asynctest import patch - from homeassistant import data_entry_flow from homeassistant.components.luftdaten import DOMAIN, config_flow from homeassistant.components.luftdaten.const import CONF_SENSOR_ID from homeassistant.const import CONF_SCAN_INTERVAL, CONF_SHOW_ON_MAP +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index 330581d2d147d6..50924af5d76d8e 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -1,13 +1,11 @@ """Test the base functions of the media player.""" import base64 -from asynctest import patch - from homeassistant.components import media_player from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.setup import async_setup_component -from tests.common import mock_coro +from tests.async_mock import patch async def test_get_image(hass, hass_ws_client, caplog): @@ -21,7 +19,7 @@ async def test_get_image(hass, hass_ws_client, caplog): with patch( "homeassistant.components.media_player.MediaPlayerEntity." "async_get_media_image", - return_value=mock_coro((b"image", "image/jpeg")), + return_value=(b"image", "image/jpeg"), ): await client.send_json( { diff --git a/tests/components/melcloud/test_config_flow.py b/tests/components/melcloud/test_config_flow.py index c936807484a189..e6b36306986d78 100644 --- a/tests/components/melcloud/test_config_flow.py +++ b/tests/components/melcloud/test_config_flow.py @@ -2,7 +2,6 @@ import asyncio from aiohttp import ClientError, ClientResponseError -from asynctest import patch import pymelcloud import pytest @@ -10,6 +9,7 @@ from homeassistant.components.melcloud.const import DOMAIN from homeassistant.const import HTTP_FORBIDDEN, HTTP_INTERNAL_SERVER_ERROR +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/met/conftest.py b/tests/components/met/conftest.py index e475889863d1e4..164a8498465a37 100644 --- a/tests/components/met/conftest.py +++ b/tests/components/met/conftest.py @@ -1,9 +1,7 @@ """Fixtures for Met weather testing.""" -from unittest.mock import patch - import pytest -from tests.common import mock_coro +from tests.async_mock import AsyncMock, patch @pytest.fixture @@ -11,7 +9,7 @@ def mock_weather(): """Mock weather data.""" with patch("metno.MetWeatherData") as mock_data: mock_data = mock_data.return_value - mock_data.fetching_data.side_effect = lambda: mock_coro(True) + mock_data.fetching_data = AsyncMock(return_value=True) mock_data.get_current_weather.return_value = { "condition": "cloudy", "temperature": 15, diff --git a/tests/components/met/test_config_flow.py b/tests/components/met/test_config_flow.py index 980994f3fb2101..a4c48f38245983 100644 --- a/tests/components/met/test_config_flow.py +++ b/tests/components/met/test_config_flow.py @@ -1,10 +1,10 @@ """Tests for Met.no config flow.""" -from asynctest import patch import pytest from homeassistant.components.met.const import DOMAIN, HOME_LOCATION_NAME from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/microsoft_face/test_init.py b/tests/components/microsoft_face/test_init.py index 478a3fb29bdc57..abd35eca3e1671 100644 --- a/tests/components/microsoft_face/test_init.py +++ b/tests/components/microsoft_face/test_init.py @@ -1,8 +1,6 @@ """The tests for the microsoft face platform.""" import asyncio -from asynctest import patch - from homeassistant.components import camera, microsoft_face as mf from homeassistant.components.microsoft_face import ( ATTR_CAMERA_ENTITY, @@ -19,6 +17,7 @@ from homeassistant.const import ATTR_NAME from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant, load_fixture diff --git a/tests/components/mikrotik/test_hub.py b/tests/components/mikrotik/test_hub.py index 9a2f75f7015762..ecfb9add717d0b 100644 --- a/tests/components/mikrotik/test_hub.py +++ b/tests/components/mikrotik/test_hub.py @@ -1,5 +1,4 @@ """Test Mikrotik hub.""" -from asynctest import patch import librouteros from homeassistant import config_entries @@ -7,6 +6,7 @@ from . import ARP_DATA, DHCP_DATA, MOCK_DATA, MOCK_OPTIONS, WIRELESS_DATA +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/mikrotik/test_init.py b/tests/components/mikrotik/test_init.py index 1a634916781ce3..b3dca7269eb183 100644 --- a/tests/components/mikrotik/test_init.py +++ b/tests/components/mikrotik/test_init.py @@ -1,11 +1,10 @@ """Test Mikrotik setup process.""" -from asynctest import CoroutineMock, Mock, patch - from homeassistant.components import mikrotik from homeassistant.setup import async_setup_component from . import MOCK_DATA +from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry @@ -25,7 +24,7 @@ async def test_successful_config_entry(hass): "homeassistant.helpers.device_registry.async_get_registry", return_value=mock_registry, ): - mock_hub.return_value.async_setup = CoroutineMock(return_value=True) + mock_hub.return_value.async_setup = AsyncMock(return_value=True) mock_hub.return_value.serial_num = "12345678" mock_hub.return_value.model = "RB750" mock_hub.return_value.hostname = "mikrotik" @@ -55,7 +54,7 @@ async def test_hub_fail_setup(hass): entry.add_to_hass(hass) with patch.object(mikrotik, "MikrotikHub") as mock_hub: - mock_hub.return_value.async_setup = CoroutineMock(return_value=False) + mock_hub.return_value.async_setup = AsyncMock(return_value=False) assert await mikrotik.async_setup_entry(hass, entry) is False assert mikrotik.DOMAIN not in hass.data @@ -69,7 +68,7 @@ async def test_unload_entry(hass): with patch.object(mikrotik, "MikrotikHub") as mock_hub, patch( "homeassistant.helpers.device_registry.async_get_registry", return_value=Mock(), ): - mock_hub.return_value.async_setup = CoroutineMock(return_value=True) + mock_hub.return_value.async_setup = AsyncMock(return_value=True) mock_hub.return_value.serial_num = "12345678" mock_hub.return_value.model = "RB750" mock_hub.return_value.hostname = "mikrotik" diff --git a/tests/components/minecraft_server/test_config_flow.py b/tests/components/minecraft_server/test_config_flow.py index 7db6dc33b5a090..17ec9080e86ccc 100644 --- a/tests/components/minecraft_server/test_config_flow.py +++ b/tests/components/minecraft_server/test_config_flow.py @@ -3,7 +3,6 @@ import asyncio import aiodns -from asynctest import patch from mcstatus.pinger import PingResponse from homeassistant.components.minecraft_server.const import ( @@ -20,6 +19,7 @@ ) from homeassistant.helpers.typing import HomeAssistantType +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/minio/test_minio.py b/tests/components/minio/test_minio.py index 4397f446a190d6..a1f3e107152253 100644 --- a/tests/components/minio/test_minio.py +++ b/tests/components/minio/test_minio.py @@ -3,7 +3,6 @@ import json from unittest.mock import MagicMock -from asynctest import call, patch import pytest from homeassistant.components.minio import ( @@ -20,6 +19,7 @@ from homeassistant.core import callback from homeassistant.setup import async_setup_component +from tests.async_mock import call, patch from tests.components.minio.common import TEST_EVENT diff --git a/tests/components/monoprice/test_config_flow.py b/tests/components/monoprice/test_config_flow.py index ecafa17e174814..8c6e2a3916c666 100644 --- a/tests/components/monoprice/test_config_flow.py +++ b/tests/components/monoprice/test_config_flow.py @@ -1,5 +1,4 @@ """Test the Monoprice 6-Zone Amplifier config flow.""" -from asynctest import patch from serial import SerialException from homeassistant import config_entries, data_entry_flow, setup @@ -12,6 +11,7 @@ ) from homeassistant.const import CONF_PORT +from tests.async_mock import patch from tests.common import MockConfigEntry CONFIG = { diff --git a/tests/components/monoprice/test_media_player.py b/tests/components/monoprice/test_media_player.py index 0006364b94edb8..f70a19f51fc1e4 100644 --- a/tests/components/monoprice/test_media_player.py +++ b/tests/components/monoprice/test_media_player.py @@ -1,7 +1,6 @@ """The tests for Monoprice Media player platform.""" from collections import defaultdict -from asynctest import patch from serial import SerialException from homeassistant.components.media_player.const import ( @@ -34,6 +33,7 @@ ) from homeassistant.helpers.entity_component import async_update_entity +from tests.async_mock import patch from tests.common import MockConfigEntry MOCK_CONFIG = {CONF_PORT: "fake port", CONF_SOURCES: {"1": "one", "3": "three"}} diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index ababe8395f34be..32f826422a8ffa 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -1,11 +1,11 @@ """The tests for the MQTT device tracker platform.""" -from asynctest import patch import pytest from homeassistant.components.device_tracker.const import DOMAIN, SOURCE_TYPE_BLUETOOTH from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import async_fire_mqtt_message diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 1b2f76d6c5ef29..9d8ede4f516520 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -13,10 +13,10 @@ from homeassistant.components.mqtt.discovery import ALREADY_DISCOVERED, async_start from homeassistant.const import STATE_OFF, STATE_ON +from tests.async_mock import AsyncMock from tests.common import ( MockConfigEntry, async_fire_mqtt_message, - mock_coro, mock_device_registry, mock_registry, ) @@ -57,7 +57,7 @@ async def test_invalid_topic(hass, mqtt_mock): domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"} ) - mock_dispatcher_send.return_value = mock_coro() + mock_dispatcher_send = AsyncMock(return_value=None) await async_start(hass, "homeassistant", {}, entry) async_fire_mqtt_message( @@ -76,7 +76,7 @@ async def test_invalid_json(hass, mqtt_mock, caplog): domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"} ) - mock_dispatcher_send.return_value = mock_coro() + mock_dispatcher_send = AsyncMock(return_value=None) await async_start(hass, "homeassistant", {}, entry) async_fire_mqtt_message( @@ -96,7 +96,7 @@ async def test_only_valid_components(hass, mqtt_mock, caplog): invalid_component = "timer" - mock_dispatcher_send.return_value = mock_coro() + mock_dispatcher_send = AsyncMock(return_value=None) await async_start(hass, "homeassistant", {}, entry) async_fire_mqtt_message( diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 207529c2ad334f..a139a94253062f 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -4,7 +4,6 @@ import ssl import unittest -from asynctest import CoroutineMock, MagicMock, call, mock_open, patch import pytest import voluptuous as vol @@ -24,6 +23,7 @@ from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow +from tests.async_mock import AsyncMock, MagicMock, call, mock_open, patch from tests.common import ( MockConfigEntry, async_fire_mqtt_message, @@ -60,8 +60,8 @@ def entity_reg(hass): def mock_mqtt(): """Make sure connection is established.""" with patch("homeassistant.components.mqtt.MQTT") as mock_mqtt: - mock_mqtt.return_value.async_connect = CoroutineMock(return_value=True) - mock_mqtt.return_value.async_disconnect = CoroutineMock(return_value=True) + mock_mqtt.return_value.async_connect = AsyncMock(return_value=True) + mock_mqtt.return_value.async_disconnect = AsyncMock(return_value=True) yield mock_mqtt diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 205e7400eb322f..ccf5935cecc9ca 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -153,8 +153,6 @@ payload_off: "off" """ -from asynctest import call, patch - from homeassistant.components import light, mqtt from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON @@ -183,6 +181,7 @@ help_test_update_with_json_attrs_not_dict, ) +from tests.async_mock import call, patch from tests.common import ( MockConfigEntry, assert_setup_component, diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 1e3ac34af89b5e..d4712e9f8350a6 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -89,8 +89,6 @@ """ import json -from asynctest import call, patch - from homeassistant.components import light from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -123,6 +121,7 @@ help_test_update_with_json_attrs_not_dict, ) +from tests.async_mock import call, patch from tests.common import async_fire_mqtt_message from tests.components.light import common diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 20b5ecefd89c92..29adc555bc5547 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -26,8 +26,6 @@ If your light doesn't support RGB feature, omit `(red|green|blue)_template`. """ -from asynctest import patch - from homeassistant.components import light from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -60,6 +58,7 @@ help_test_update_with_json_attrs_not_dict, ) +from tests.async_mock import patch from tests.common import assert_setup_component, async_fire_mqtt_message from tests.components.light import common diff --git a/tests/components/mqtt/test_server.py b/tests/components/mqtt/test_server.py index 3186b3a2734fb6..b3320d6aaca146 100644 --- a/tests/components/mqtt/test_server.py +++ b/tests/components/mqtt/test_server.py @@ -1,13 +1,13 @@ """The tests for the MQTT component embedded server.""" from unittest.mock import MagicMock, Mock -from asynctest import CoroutineMock, patch import pytest import homeassistant.components.mqtt as mqtt from homeassistant.const import CONF_PASSWORD from homeassistant.setup import setup_component +from tests.async_mock import AsyncMock, patch from tests.common import get_test_home_assistant, mock_coro @@ -29,15 +29,15 @@ def teardown_method(self, method): @patch("passlib.apps.custom_app_context", Mock(return_value="")) @patch("tempfile.NamedTemporaryFile", Mock(return_value=MagicMock())) - @patch("hbmqtt.broker.Broker", Mock(return_value=MagicMock(start=CoroutineMock()))) - @patch("hbmqtt.broker.Broker.start", Mock(return_value=mock_coro())) + @patch("hbmqtt.broker.Broker", Mock(return_value=MagicMock(start=AsyncMock()))) + @patch("hbmqtt.broker.Broker.start", AsyncMock(return_value=None)) @patch("homeassistant.components.mqtt.MQTT") def test_creating_config_with_pass_and_no_http_pass(self, mock_mqtt): """Test if the MQTT server gets started with password. Since 0.77, MQTT server has to set up its own password. """ - mock_mqtt().async_connect.return_value = mock_coro(True) + mock_mqtt().async_connect = AsyncMock(return_value=True) self.hass.bus.listen_once = MagicMock() password = "mqtt_secret" @@ -51,15 +51,15 @@ def test_creating_config_with_pass_and_no_http_pass(self, mock_mqtt): @patch("passlib.apps.custom_app_context", Mock(return_value="")) @patch("tempfile.NamedTemporaryFile", Mock(return_value=MagicMock())) - @patch("hbmqtt.broker.Broker", Mock(return_value=MagicMock(start=CoroutineMock()))) - @patch("hbmqtt.broker.Broker.start", Mock(return_value=mock_coro())) + @patch("hbmqtt.broker.Broker", Mock(return_value=MagicMock(start=AsyncMock()))) + @patch("hbmqtt.broker.Broker.start", AsyncMock(return_value=None)) @patch("homeassistant.components.mqtt.MQTT") def test_creating_config_with_pass_and_http_pass(self, mock_mqtt): """Test if the MQTT server gets started with password. Since 0.77, MQTT server has to set up its own password. """ - mock_mqtt().async_connect.return_value = mock_coro(True) + mock_mqtt().async_connect = AsyncMock(return_value=True) self.hass.bus.listen_once = MagicMock() password = "mqtt_secret" diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index 1aaeb154dc250c..c34d32a3c9deb0 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -1,5 +1,4 @@ """The tests for the MQTT switch platform.""" -from asynctest import patch import pytest from homeassistant.components import switch @@ -29,7 +28,8 @@ help_test_update_with_json_attrs_not_dict, ) -from tests.common import async_fire_mqtt_message, async_mock_mqtt_component, mock_coro +from tests.async_mock import patch +from tests.common import async_fire_mqtt_message, async_mock_mqtt_component from tests.components.switch import common DEFAULT_CONFIG = { @@ -81,7 +81,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mock_publish): with patch( "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", - return_value=mock_coro(fake_state), + return_value=fake_state, ): assert await async_setup_component( hass, diff --git a/tests/components/mqtt_json/test_device_tracker.py b/tests/components/mqtt_json/test_device_tracker.py index 9efff135fe25ae..864b3c232edad8 100644 --- a/tests/components/mqtt_json/test_device_tracker.py +++ b/tests/components/mqtt_json/test_device_tracker.py @@ -3,7 +3,6 @@ import logging import os -from asynctest import patch import pytest from homeassistant.components.device_tracker.legacy import ( @@ -13,6 +12,7 @@ from homeassistant.const import CONF_PLATFORM from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import async_fire_mqtt_message, async_mock_mqtt_component _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/myq/test_config_flow.py b/tests/components/myq/test_config_flow.py index 7620a9ad176dcb..ed022df0dd7007 100644 --- a/tests/components/myq/test_config_flow.py +++ b/tests/components/myq/test_config_flow.py @@ -1,11 +1,11 @@ """Test the MyQ config flow.""" -from asynctest import patch from pymyq.errors import InvalidCredentialsError, MyQError from homeassistant import config_entries, setup from homeassistant.components.myq.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/myq/util.py b/tests/components/myq/util.py index 7cff7bd2af9e31..61e49a98b8389c 100644 --- a/tests/components/myq/util.py +++ b/tests/components/myq/util.py @@ -2,12 +2,11 @@ import json -from asynctest import patch - from homeassistant.components.myq.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/mythicbeastsdns/test_init.py b/tests/components/mythicbeastsdns/test_init.py index ee037a029edf48..e8efac2c01d493 100644 --- a/tests/components/mythicbeastsdns/test_init.py +++ b/tests/components/mythicbeastsdns/test_init.py @@ -1,11 +1,11 @@ """Test the Mythic Beasts DNS component.""" import logging -import asynctest - from homeassistant.components import mythicbeastsdns from homeassistant.setup import async_setup_component +from tests.async_mock import patch + _LOGGER = logging.getLogger(__name__) @@ -20,7 +20,7 @@ async def mbddns_update_mock(domain, password, host, ttl=60, session=None): return True -@asynctest.mock.patch("mbddns.update", new=mbddns_update_mock) +@patch("mbddns.update", new=mbddns_update_mock) async def test_update(hass): """Run with correct values and check true is returned.""" result = await async_setup_component( @@ -37,7 +37,7 @@ async def test_update(hass): assert result -@asynctest.mock.patch("mbddns.update", new=mbddns_update_mock) +@patch("mbddns.update", new=mbddns_update_mock) async def test_update_fails_if_wrong_token(hass): """Run with incorrect token and check false is returned.""" result = await async_setup_component( @@ -54,7 +54,7 @@ async def test_update_fails_if_wrong_token(hass): assert not result -@asynctest.mock.patch("mbddns.update", new=mbddns_update_mock) +@patch("mbddns.update", new=mbddns_update_mock) async def test_update_fails_if_invalid_host(hass): """Run with invalid characters in host and check false is returned.""" result = await async_setup_component( diff --git a/tests/components/ness_alarm/test_init.py b/tests/components/ness_alarm/test_init.py index 9da361852e9017..f959b5345dd4fc 100644 --- a/tests/components/ness_alarm/test_init.py +++ b/tests/components/ness_alarm/test_init.py @@ -1,7 +1,6 @@ """Tests for the ness_alarm component.""" from enum import Enum -from asynctest import MagicMock, patch import pytest from homeassistant.components import alarm_control_panel @@ -32,6 +31,8 @@ ) from homeassistant.setup import async_setup_component +from tests.async_mock import MagicMock, patch + VALID_CONFIG = { DOMAIN: { CONF_HOST: "alarm.local", diff --git a/tests/components/nest/test_config_flow.py b/tests/components/nest/test_config_flow.py index ec6218fb0d767a..6c0c0197a74865 100644 --- a/tests/components/nest/test_config_flow.py +++ b/tests/components/nest/test_config_flow.py @@ -6,6 +6,7 @@ from homeassistant.components.nest import DOMAIN, config_flow from homeassistant.setup import async_setup_component +from tests.async_mock import AsyncMock from tests.common import mock_coro @@ -33,8 +34,8 @@ async def test_abort_if_already_setup(hass): async def test_full_flow_implementation(hass): """Test registering an implementation and finishing flow works.""" - gen_authorize_url = Mock(return_value=mock_coro("https://example.com")) - convert_code = Mock(return_value=mock_coro({"access_token": "yoo"})) + gen_authorize_url = AsyncMock(return_value="https://example.com") + convert_code = AsyncMock(return_value={"access_token": "yoo"}) config_flow.register_flow_implementation( hass, "test", "Test", gen_authorize_url, convert_code ) @@ -62,7 +63,7 @@ async def test_full_flow_implementation(hass): async def test_not_pick_implementation_if_only_one(hass): """Test we allow picking implementation if we have two.""" - gen_authorize_url = Mock(return_value=mock_coro("https://example.com")) + gen_authorize_url = AsyncMock(return_value="https://example.com") config_flow.register_flow_implementation( hass, "test", "Test", gen_authorize_url, None ) @@ -104,7 +105,7 @@ async def test_abort_if_exception_generating_auth_url(hass): async def test_verify_code_timeout(hass): """Test verify code timing out.""" - gen_authorize_url = Mock(return_value=mock_coro("https://example.com")) + gen_authorize_url = AsyncMock(return_value="https://example.com") convert_code = Mock(side_effect=asyncio.TimeoutError) config_flow.register_flow_implementation( hass, "test", "Test", gen_authorize_url, convert_code @@ -124,7 +125,7 @@ async def test_verify_code_timeout(hass): async def test_verify_code_invalid(hass): """Test verify code invalid.""" - gen_authorize_url = Mock(return_value=mock_coro("https://example.com")) + gen_authorize_url = AsyncMock(return_value="https://example.com") convert_code = Mock(side_effect=config_flow.CodeInvalid) config_flow.register_flow_implementation( hass, "test", "Test", gen_authorize_url, convert_code @@ -144,7 +145,7 @@ async def test_verify_code_invalid(hass): async def test_verify_code_unknown_error(hass): """Test verify code unknown error.""" - gen_authorize_url = Mock(return_value=mock_coro("https://example.com")) + gen_authorize_url = AsyncMock(return_value="https://example.com") convert_code = Mock(side_effect=config_flow.NestAuthError) config_flow.register_flow_implementation( hass, "test", "Test", gen_authorize_url, convert_code @@ -164,7 +165,7 @@ async def test_verify_code_unknown_error(hass): async def test_verify_code_exception(hass): """Test verify code blows up.""" - gen_authorize_url = Mock(return_value=mock_coro("https://example.com")) + gen_authorize_url = AsyncMock(return_value="https://example.com") convert_code = Mock(side_effect=ValueError) config_flow.register_flow_implementation( hass, "test", "Test", gen_authorize_url, convert_code diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index 29a1d4f53d54fe..047dd4c0c40bc9 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -1,6 +1,4 @@ """Test the Netatmo config flow.""" -from asynctest import patch - from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.netatmo import config_flow from homeassistant.components.netatmo.const import ( @@ -10,6 +8,7 @@ ) from homeassistant.helpers import config_entry_oauth2_flow +from tests.async_mock import patch from tests.common import MockConfigEntry CLIENT_ID = "1234" diff --git a/tests/components/nexia/test_config_flow.py b/tests/components/nexia/test_config_flow.py index ff6f85902872de..0dce512cff4cba 100644 --- a/tests/components/nexia/test_config_flow.py +++ b/tests/components/nexia/test_config_flow.py @@ -1,12 +1,12 @@ """Test the nexia config flow.""" -from asynctest import patch -from asynctest.mock import MagicMock from requests.exceptions import ConnectTimeout from homeassistant import config_entries, setup from homeassistant.components.nexia.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from tests.async_mock import MagicMock, patch + async def test_form(hass): """Test we get the form.""" diff --git a/tests/components/nexia/util.py b/tests/components/nexia/util.py index cc2b11afcbe548..2da56d50f37edd 100644 --- a/tests/components/nexia/util.py +++ b/tests/components/nexia/util.py @@ -1,7 +1,6 @@ """Tests for the nexia integration.""" import uuid -from asynctest import patch from nexia.home import NexiaHome import requests_mock @@ -9,6 +8,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/notion/test_config_flow.py b/tests/components/notion/test_config_flow.py index 7d3ddb1cf4b1e1..6aaf7df95051d6 100644 --- a/tests/components/notion/test_config_flow.py +++ b/tests/components/notion/test_config_flow.py @@ -1,6 +1,5 @@ """Define tests for the Notion config flow.""" import aionotion -from asynctest import patch import pytest from homeassistant import data_entry_flow @@ -8,20 +7,21 @@ from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import AsyncMock, patch +from tests.common import MockConfigEntry @pytest.fixture -def mock_client_coro(): +def mock_client(): """Define a fixture for a client creation coroutine.""" - return mock_coro() + return AsyncMock(return_value=None) @pytest.fixture -def mock_aionotion(mock_client_coro): +def mock_aionotion(mock_client): """Mock the aionotion library.""" with patch("homeassistant.components.notion.config_flow.async_get_client") as mock_: - mock_.return_value = mock_client_coro + mock_.side_effect = mock_client yield mock_ @@ -42,7 +42,7 @@ async def test_duplicate_error(hass): @pytest.mark.parametrize( - "mock_client_coro", [mock_coro(exception=aionotion.errors.NotionError)] + "mock_client", [AsyncMock(side_effect=aionotion.errors.NotionError)] ) async def test_invalid_credentials(hass, mock_aionotion): """Test that an invalid API/App Key throws an error.""" diff --git a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py index 6834c557bd5192..a5167104d48b69 100644 --- a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py +++ b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py @@ -3,7 +3,6 @@ from unittest.mock import ANY from aio_geojson_nsw_rfs_incidents import NswRuralFireServiceIncidentsFeed -from asynctest.mock import MagicMock, call, patch from homeassistant.components import geo_location from homeassistant.components.geo_location import ATTR_SOURCE @@ -37,6 +36,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import MagicMock, call, patch from tests.common import assert_setup_component, async_fire_time_changed CONFIG = { diff --git a/tests/components/nuheat/mocks.py b/tests/components/nuheat/mocks.py index a9adfd3aa5719a..9755335ccc175d 100644 --- a/tests/components/nuheat/mocks.py +++ b/tests/components/nuheat/mocks.py @@ -1,10 +1,11 @@ """The test for the NuHeat thermostat module.""" -from asynctest.mock import MagicMock, Mock from nuheat.config import SCHEDULE_HOLD, SCHEDULE_RUN, SCHEDULE_TEMPORARY_HOLD from homeassistant.components.nuheat.const import DOMAIN from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME +from tests.async_mock import MagicMock, Mock + def _get_mock_thermostat_run(): serial_number = "12345" diff --git a/tests/components/nuheat/test_climate.py b/tests/components/nuheat/test_climate.py index 7bf52026ef9217..b407461fa89fba 100644 --- a/tests/components/nuheat/test_climate.py +++ b/tests/components/nuheat/test_climate.py @@ -1,6 +1,4 @@ """The test for the NuHeat thermostat module.""" -from asynctest.mock import patch - from homeassistant.components.nuheat.const import DOMAIN from homeassistant.setup import async_setup_component @@ -13,6 +11,8 @@ _mock_get_config, ) +from tests.async_mock import patch + async def test_climate_thermostat_run(hass): """Test a thermostat with the schedule running.""" diff --git a/tests/components/nuheat/test_config_flow.py b/tests/components/nuheat/test_config_flow.py index 338509f09d1750..4c392841142a5d 100644 --- a/tests/components/nuheat/test_config_flow.py +++ b/tests/components/nuheat/test_config_flow.py @@ -1,5 +1,4 @@ """Test the NuHeat config flow.""" -from asynctest import MagicMock, patch import requests from homeassistant import config_entries, setup @@ -8,6 +7,8 @@ from .mocks import _get_mock_thermostat_run +from tests.async_mock import MagicMock, patch + async def test_form_user(hass): """Test we get the form with user source.""" diff --git a/tests/components/nut/test_config_flow.py b/tests/components/nut/test_config_flow.py index ac20c989de93b3..7eb0ac20184c6c 100644 --- a/tests/components/nut/test_config_flow.py +++ b/tests/components/nut/test_config_flow.py @@ -1,12 +1,11 @@ """Test the Network UPS Tools (NUT) config flow.""" -from asynctest import patch - from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.nut.const import DOMAIN from homeassistant.const import CONF_RESOURCES, CONF_SCAN_INTERVAL from .util import _get_mock_pynutclient +from tests.async_mock import patch from tests.common import MockConfigEntry VALID_CONFIG = { diff --git a/tests/components/nut/util.py b/tests/components/nut/util.py index 788076a7c9f21a..5622438d70b2e6 100644 --- a/tests/components/nut/util.py +++ b/tests/components/nut/util.py @@ -2,12 +2,11 @@ import json -from asynctest import MagicMock, patch - from homeassistant.components.nut.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT, CONF_RESOURCES from homeassistant.core import HomeAssistant +from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/nws/conftest.py b/tests/components/nws/conftest.py index 14cee7aa7cbf5e..ac8428ddf48be6 100644 --- a/tests/components/nws/conftest.py +++ b/tests/components/nws/conftest.py @@ -3,7 +3,7 @@ import pytest -from tests.common import mock_coro +from tests.async_mock import AsyncMock from tests.components.nws.const import DEFAULT_FORECAST, DEFAULT_OBSERVATION @@ -12,10 +12,10 @@ def mock_simple_nws(): """Mock pynws SimpleNWS with default values.""" with patch("homeassistant.components.nws.SimpleNWS") as mock_nws: instance = mock_nws.return_value - instance.set_station.return_value = mock_coro() - instance.update_observation.return_value = mock_coro() - instance.update_forecast.return_value = mock_coro() - instance.update_forecast_hourly.return_value = mock_coro() + instance.set_station = AsyncMock(return_value=None) + instance.update_observation = AsyncMock(return_value=None) + instance.update_forecast = AsyncMock(return_value=None) + instance.update_forecast_hourly = AsyncMock(return_value=None) instance.station = "ABC" instance.stations = ["ABC"] instance.observation = DEFAULT_OBSERVATION @@ -29,7 +29,7 @@ def mock_simple_nws_config(): """Mock pynws SimpleNWS with default values in config_flow.""" with patch("homeassistant.components.nws.config_flow.SimpleNWS") as mock_nws: instance = mock_nws.return_value - instance.set_station.return_value = mock_coro() + instance.set_station = AsyncMock(return_value=None) instance.station = "ABC" instance.stations = ["ABC"] yield mock_nws diff --git a/tests/components/nws/test_config_flow.py b/tests/components/nws/test_config_flow.py index d4957d4c989c68..bca852fa379670 100644 --- a/tests/components/nws/test_config_flow.py +++ b/tests/components/nws/test_config_flow.py @@ -1,10 +1,11 @@ """Test the National Weather Service (NWS) config flow.""" import aiohttp -from asynctest import patch from homeassistant import config_entries, setup from homeassistant.components.nws.const import DOMAIN +from tests.async_mock import patch + async def test_form(hass, mock_simple_nws_config): """Test we get the form.""" diff --git a/tests/components/openalpr_cloud/test_image_processing.py b/tests/components/openalpr_cloud/test_image_processing.py index f5a246bcb4d760..c164f2f03a282c 100644 --- a/tests/components/openalpr_cloud/test_image_processing.py +++ b/tests/components/openalpr_cloud/test_image_processing.py @@ -1,19 +1,13 @@ """The tests for the openalpr cloud platform.""" import asyncio -from asynctest import PropertyMock, patch - from homeassistant.components import camera, image_processing as ip from homeassistant.components.openalpr_cloud.image_processing import OPENALPR_API_URL from homeassistant.core import callback from homeassistant.setup import setup_component -from tests.common import ( - assert_setup_component, - get_test_home_assistant, - load_fixture, - mock_coro, -) +from tests.async_mock import PropertyMock, patch +from tests.common import assert_setup_component, get_test_home_assistant, load_fixture from tests.components.image_processing import common @@ -146,7 +140,7 @@ def test_openalpr_process_image(self, aioclient_mock): with patch( "homeassistant.components.camera.async_get_image", - return_value=mock_coro(camera.Image("image/jpeg", b"image")), + return_value=camera.Image("image/jpeg", b"image"), ): common.scan(self.hass, entity_id="image_processing.test_local") self.hass.block_till_done() @@ -179,7 +173,7 @@ def test_openalpr_process_image_api_error(self, aioclient_mock): with patch( "homeassistant.components.camera.async_get_image", - return_value=mock_coro(camera.Image("image/jpeg", b"image")), + return_value=camera.Image("image/jpeg", b"image"), ): common.scan(self.hass, entity_id="image_processing.test_local") self.hass.block_till_done() @@ -195,7 +189,7 @@ def test_openalpr_process_image_api_timeout(self, aioclient_mock): with patch( "homeassistant.components.camera.async_get_image", - return_value=mock_coro(camera.Image("image/jpeg", b"image")), + return_value=camera.Image("image/jpeg", b"image"), ): common.scan(self.hass, entity_id="image_processing.test_local") self.hass.block_till_done() diff --git a/tests/components/openalpr_local/test_image_processing.py b/tests/components/openalpr_local/test_image_processing.py index 3f62c906974355..996d23184a2c19 100644 --- a/tests/components/openalpr_local/test_image_processing.py +++ b/tests/components/openalpr_local/test_image_processing.py @@ -1,16 +1,15 @@ """The tests for the openalpr local platform.""" -from asynctest import MagicMock, PropertyMock, patch - import homeassistant.components.image_processing as ip from homeassistant.const import ATTR_ENTITY_PICTURE from homeassistant.core import callback from homeassistant.setup import setup_component +from tests.async_mock import MagicMock, PropertyMock, patch from tests.common import assert_setup_component, get_test_home_assistant, load_fixture from tests.components.image_processing import common -async def mock_async_subprocess(): +def mock_async_subprocess(): """Get a Popen mock back.""" async_popen = MagicMock() diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index 2dec360eca058f..003d2ad7170078 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -1,7 +1,6 @@ """Test the Opentherm Gateway config flow.""" import asyncio -from asynctest import patch from pyotgw.vars import OTGW_ABOUT from serial import SerialException @@ -13,7 +12,8 @@ ) from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME, PRECISION_HALVES -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import patch +from tests.common import MockConfigEntry async def test_form_user(hass): @@ -26,16 +26,13 @@ async def test_form_user(hass): assert result["errors"] == {} with patch( - "homeassistant.components.opentherm_gw.async_setup", - return_value=mock_coro(True), + "homeassistant.components.opentherm_gw.async_setup", return_value=True, ) as mock_setup, patch( - "homeassistant.components.opentherm_gw.async_setup_entry", - return_value=mock_coro(True), + "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "pyotgw.pyotgw.connect", - return_value=mock_coro({OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}), + "pyotgw.pyotgw.connect", return_value={OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}, ) as mock_pyotgw_connect, patch( - "pyotgw.pyotgw.disconnect", return_value=mock_coro(None) + "pyotgw.pyotgw.disconnect", return_value=None ) as mock_pyotgw_disconnect: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} @@ -59,16 +56,13 @@ async def test_form_import(hass): """Test import from existing config.""" await setup.async_setup_component(hass, "persistent_notification", {}) with patch( - "homeassistant.components.opentherm_gw.async_setup", - return_value=mock_coro(True), + "homeassistant.components.opentherm_gw.async_setup", return_value=True, ) as mock_setup, patch( - "homeassistant.components.opentherm_gw.async_setup_entry", - return_value=mock_coro(True), + "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "pyotgw.pyotgw.connect", - return_value=mock_coro({OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}), + "pyotgw.pyotgw.connect", return_value={OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}, ) as mock_pyotgw_connect, patch( - "pyotgw.pyotgw.disconnect", return_value=mock_coro(None) + "pyotgw.pyotgw.disconnect", return_value=None ) as mock_pyotgw_disconnect: result = await hass.config_entries.flow.async_init( DOMAIN, @@ -102,16 +96,13 @@ async def test_form_duplicate_entries(hass): ) with patch( - "homeassistant.components.opentherm_gw.async_setup", - return_value=mock_coro(True), + "homeassistant.components.opentherm_gw.async_setup", return_value=True, ) as mock_setup, patch( - "homeassistant.components.opentherm_gw.async_setup_entry", - return_value=mock_coro(True), + "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "pyotgw.pyotgw.connect", - return_value=mock_coro({OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}), + "pyotgw.pyotgw.connect", return_value={OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}, ) as mock_pyotgw_connect, patch( - "pyotgw.pyotgw.disconnect", return_value=mock_coro(None) + "pyotgw.pyotgw.disconnect", return_value=None ) as mock_pyotgw_disconnect: result1 = await hass.config_entries.flow.async_configure( flow1["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py index 514a559ac1d7a0..676e47523fa5af 100644 --- a/tests/components/owntracks/test_config_flow.py +++ b/tests/components/owntracks/test_config_flow.py @@ -1,5 +1,4 @@ """Tests for OwnTracks config flow.""" -from asynctest import Mock, patch import pytest from homeassistant import data_entry_flow @@ -9,7 +8,8 @@ from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import Mock, patch +from tests.common import MockConfigEntry CONF_WEBHOOK_URL = "webhook_url" @@ -140,7 +140,7 @@ async def test_unload(hass): with patch( "homeassistant.config_entries.ConfigEntries.async_forward_entry_unload", - return_value=mock_coro(), + return_value=None, ) as mock_unload: assert await hass.config_entries.async_unload(entry.entry_id) @@ -157,7 +157,7 @@ async def test_with_cloud_sub(hass): "homeassistant.components.cloud.async_active_subscription", return_value=True ), patch( "homeassistant.components.cloud.async_create_cloudhook", - return_value=mock_coro("https://hooks.nabu.casa/ABCD"), + return_value="https://hooks.nabu.casa/ABCD", ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"}, data={} diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py index bdd1199008c8bc..d71f0fe0aee800 100644 --- a/tests/components/owntracks/test_device_tracker.py +++ b/tests/components/owntracks/test_device_tracker.py @@ -1,13 +1,13 @@ """The tests for the Owntracks device tracker.""" import json -from asynctest import patch import pytest from homeassistant.components import owntracks from homeassistant.const import STATE_NOT_HOME from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import ( MockConfigEntry, async_fire_mqtt_message, diff --git a/tests/components/panasonic_viera/test_config_flow.py b/tests/components/panasonic_viera/test_config_flow.py index fd2738508ea1e8..cc7c3f58e82b76 100644 --- a/tests/components/panasonic_viera/test_config_flow.py +++ b/tests/components/panasonic_viera/test_config_flow.py @@ -1,7 +1,6 @@ """Test the Panasonic Viera config flow.""" from unittest.mock import Mock -from asynctest import patch from panasonic_viera import TV_TYPE_ENCRYPTED, TV_TYPE_NONENCRYPTED, SOAPError import pytest @@ -20,6 +19,7 @@ ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PIN, CONF_PORT +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py index 763506199839e3..887f0d94fefad2 100644 --- a/tests/components/person/test_init.py +++ b/tests/components/person/test_init.py @@ -1,7 +1,6 @@ """The tests for the person component.""" import logging -from asynctest import patch import pytest from homeassistant.components import person @@ -24,6 +23,7 @@ from homeassistant.helpers import collection, entity_registry from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import assert_setup_component, mock_component, mock_restore_cache DEVICE_TRACKER = "device_tracker.test_tracker" diff --git a/tests/components/pi_hole/test_init.py b/tests/components/pi_hole/test_init.py index c2d9ec77f03e1b..236e8eadde8233 100644 --- a/tests/components/pi_hole/test_init.py +++ b/tests/components/pi_hole/test_init.py @@ -2,10 +2,9 @@ from unittest.mock import patch -from asynctest import CoroutineMock - from homeassistant.components import pi_hole +from tests.async_mock import AsyncMock from tests.common import async_setup_component ZERO_DATA = { @@ -25,7 +24,7 @@ async def test_setup_minimal_config(hass): """Tests component setup with minimal config.""" with patch("homeassistant.components.pi_hole.Hole") as _hole: - _hole.return_value.get_data = CoroutineMock(return_value=None) + _hole.return_value.get_data = AsyncMock(return_value=None) _hole.return_value.data = ZERO_DATA assert await async_setup_component( @@ -82,7 +81,7 @@ async def test_setup_minimal_config(hass): async def test_setup_name_config(hass): """Tests component setup with a custom name.""" with patch("homeassistant.components.pi_hole.Hole") as _hole: - _hole.return_value.get_data = CoroutineMock(return_value=None) + _hole.return_value.get_data = AsyncMock(return_value=None) _hole.return_value.data = ZERO_DATA assert await async_setup_component( @@ -102,9 +101,9 @@ async def test_setup_name_config(hass): async def test_disable_service_call(hass): """Test disable service call with no Pi-hole named.""" with patch("homeassistant.components.pi_hole.Hole") as _hole: - mock_disable = CoroutineMock(return_value=None) + mock_disable = AsyncMock(return_value=None) _hole.return_value.disable = mock_disable - _hole.return_value.get_data = CoroutineMock(return_value=None) + _hole.return_value.get_data = AsyncMock(return_value=None) _hole.return_value.data = ZERO_DATA assert await async_setup_component( @@ -135,9 +134,9 @@ async def test_disable_service_call(hass): async def test_enable_service_call(hass): """Test enable service call with no Pi-hole named.""" with patch("homeassistant.components.pi_hole.Hole") as _hole: - mock_enable = CoroutineMock(return_value=None) + mock_enable = AsyncMock(return_value=None) _hole.return_value.enable = mock_enable - _hole.return_value.get_data = CoroutineMock(return_value=None) + _hole.return_value.get_data = AsyncMock(return_value=None) _hole.return_value.data = ZERO_DATA assert await async_setup_component( diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index d839ccc674b839..af065b55e50b4a 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -1,7 +1,6 @@ """Tests for Plex config flow.""" import copy -from asynctest import patch import plexapi.exceptions import requests.exceptions @@ -26,6 +25,7 @@ from .const import DEFAULT_DATA, DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN from .mock_classes import MockPlexAccount, MockPlexServer +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index 09a2e8f6ada642..e34476f1813811 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -3,9 +3,7 @@ from datetime import timedelta import ssl -from asynctest import ClockedTestCase, patch import plexapi -import pytest import requests from homeassistant.components.media_player import DOMAIN as MP_DOMAIN @@ -31,11 +29,8 @@ from .const import DEFAULT_DATA, DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN from .mock_classes import MockPlexAccount, MockPlexServer -from tests.common import ( - MockConfigEntry, - async_fire_time_changed, - async_test_home_assistant, -) +from tests.async_mock import patch +from tests.common import MockConfigEntry, async_fire_time_changed async def test_setup_with_config(hass): @@ -73,87 +68,86 @@ async def test_setup_with_config(hass): assert loaded_server.plex_server == mock_plex_server -@pytest.mark.skip -class TestClockedPlex(ClockedTestCase): - """Create clock-controlled asynctest class.""" - - @pytest.fixture(autouse=True) - def inject_fixture(self, caplog, hass_storage): - """Inject pytest fixtures as instance attributes.""" - self.caplog = caplog - - async def setUp(self): - """Initialize this test class.""" - self.hass = await async_test_home_assistant(self.loop) - - async def tearDown(self): - """Clean up the HomeAssistant instance.""" - await self.hass.async_stop() - - async def test_setup_with_config_entry(self): - """Test setup component with config.""" - hass = self.hass - - mock_plex_server = MockPlexServer() - - entry = MockConfigEntry( - domain=const.DOMAIN, - data=DEFAULT_DATA, - options=DEFAULT_OPTIONS, - unique_id=DEFAULT_DATA["server_id"], - ) - - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ) as mock_listen: - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert mock_listen.called - - assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED - - server_id = mock_plex_server.machineIdentifier - loaded_server = hass.data[const.DOMAIN][const.SERVERS][server_id] - - assert loaded_server.plex_server == mock_plex_server - - async_dispatcher_send( - hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) - ) - await hass.async_block_till_done() - - sensor = hass.states.get("sensor.plex_plex_server_1") - assert sensor.state == str(len(mock_plex_server.accounts)) - - # Ensure existing entities refresh - await self.advance(const.DEBOUNCE_TIMEOUT) - async_dispatcher_send( - hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) - ) - await hass.async_block_till_done() - - for test_exception in ( - plexapi.exceptions.BadRequest, - requests.exceptions.RequestException, - ): - with patch.object( - mock_plex_server, "clients", side_effect=test_exception - ) as patched_clients_bad_request: - await self.advance(const.DEBOUNCE_TIMEOUT) - async_dispatcher_send( - hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) - ) - await hass.async_block_till_done() - - assert patched_clients_bad_request.called - assert ( - f"Could not connect to Plex server: {mock_plex_server.friendlyName}" - in self.caplog.text - ) - self.caplog.clear() +# class TestClockedPlex(ClockedTestCase): +# """Create clock-controlled tests.async_mock class.""" + +# @pytest.fixture(autouse=True) +# def inject_fixture(self, caplog, hass_storage): +# """Inject pytest fixtures as instance attributes.""" +# self.caplog = caplog + +# async def setUp(self): +# """Initialize this test class.""" +# self.hass = await async_test_home_assistant(self.loop) + +# async def tearDown(self): +# """Clean up the HomeAssistant instance.""" +# await self.hass.async_stop() + +# async def test_setup_with_config_entry(self): +# """Test setup component with config.""" +# hass = self.hass + +# mock_plex_server = MockPlexServer() + +# entry = MockConfigEntry( +# domain=const.DOMAIN, +# data=DEFAULT_DATA, +# options=DEFAULT_OPTIONS, +# unique_id=DEFAULT_DATA["server_id"], +# ) + +# with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( +# "homeassistant.components.plex.PlexWebsocket.listen" +# ) as mock_listen: +# entry.add_to_hass(hass) +# assert await hass.config_entries.async_setup(entry.entry_id) +# await hass.async_block_till_done() + +# assert mock_listen.called + +# assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 +# assert entry.state == ENTRY_STATE_LOADED + +# server_id = mock_plex_server.machineIdentifier +# loaded_server = hass.data[const.DOMAIN][const.SERVERS][server_id] + +# assert loaded_server.plex_server == mock_plex_server + +# async_dispatcher_send( +# hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) +# ) +# await hass.async_block_till_done() + +# sensor = hass.states.get("sensor.plex_plex_server_1") +# assert sensor.state == str(len(mock_plex_server.accounts)) + +# # Ensure existing entities refresh +# await self.advance(const.DEBOUNCE_TIMEOUT) +# async_dispatcher_send( +# hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) +# ) +# await hass.async_block_till_done() + +# for test_exception in ( +# plexapi.exceptions.BadRequest, +# requests.exceptions.RequestException, +# ): +# with patch.object( +# mock_plex_server, "clients", side_effect=test_exception +# ) as patched_clients_bad_request: +# await self.advance(const.DEBOUNCE_TIMEOUT) +# async_dispatcher_send( +# hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) +# ) +# await hass.async_block_till_done() + +# assert patched_clients_bad_request.called +# assert ( +# f"Could not connect to Plex server: {mock_plex_server.friendlyName}" +# in self.caplog.text +# ) +# self.caplog.clear() async def test_set_config_entry_unique_id(hass): diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index b3cbd6c79d479c..7cb34b4fccae1e 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -1,14 +1,10 @@ """Tests for Plex server.""" import copy -from asynctest import ClockedTestCase, patch -import pytest - from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.plex.const import ( CONF_IGNORE_NEW_SHARED_USERS, CONF_MONITORED_USERS, - DEBOUNCE_TIMEOUT, DOMAIN, PLEX_UPDATE_PLATFORMS_SIGNAL, SERVERS, @@ -18,7 +14,8 @@ from .const import DEFAULT_DATA, DEFAULT_OPTIONS from .mock_classes import MockPlexServer -from tests.common import MockConfigEntry, async_test_home_assistant +from tests.async_mock import patch +from tests.common import MockConfigEntry async def test_new_users_available(hass): @@ -108,107 +105,106 @@ async def test_new_ignored_users_available(hass, caplog): assert sensor.state == str(len(mock_plex_server.accounts)) -@pytest.mark.skip -class TestClockedPlex(ClockedTestCase): - """Create clock-controlled asynctest class.""" - - async def setUp(self): - """Initialize this test class.""" - self.hass = await async_test_home_assistant(self.loop) - - async def tearDown(self): - """Clean up the HomeAssistant instance.""" - await self.hass.async_stop() - - async def test_mark_sessions_idle(self): - """Test marking media_players as idle when sessions end.""" - hass = self.hass - - entry = MockConfigEntry( - domain=DOMAIN, - data=DEFAULT_DATA, - options=DEFAULT_OPTIONS, - unique_id=DEFAULT_DATA["server_id"], - ) - - mock_plex_server = MockPlexServer(config_entry=entry) - - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - server_id = mock_plex_server.machineIdentifier - - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() - - sensor = hass.states.get("sensor.plex_plex_server_1") - assert sensor.state == str(len(mock_plex_server.accounts)) - - mock_plex_server.clear_clients() - mock_plex_server.clear_sessions() - - await self.advance(DEBOUNCE_TIMEOUT) - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() - - sensor = hass.states.get("sensor.plex_plex_server_1") - assert sensor.state == "0" - - async def test_debouncer(self): - """Test debouncer behavior.""" - hass = self.hass - - entry = MockConfigEntry( - domain=DOMAIN, - data=DEFAULT_DATA, - options=DEFAULT_OPTIONS, - unique_id=DEFAULT_DATA["server_id"], - ) - - mock_plex_server = MockPlexServer(config_entry=entry) - - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket.listen" - ): - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - server_id = mock_plex_server.machineIdentifier - - with patch.object(mock_plex_server, "clients", return_value=[]), patch.object( - mock_plex_server, "sessions", return_value=[] - ) as mock_update: - # Called immediately - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() - assert mock_update.call_count == 1 - - # Throttled - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() - assert mock_update.call_count == 1 - - # Throttled - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() - assert mock_update.call_count == 1 - - # Called from scheduler - await self.advance(DEBOUNCE_TIMEOUT) - await hass.async_block_till_done() - assert mock_update.call_count == 2 - - # Throttled - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() - assert mock_update.call_count == 2 - - # Called from scheduler - await self.advance(DEBOUNCE_TIMEOUT) - await hass.async_block_till_done() - assert mock_update.call_count == 3 +# class TestClockedPlex(ClockedTestCase): +# """Create clock-controlled tests.async_mock class.""" + +# async def setUp(self): +# """Initialize this test class.""" +# self.hass = await async_test_home_assistant(self.loop) + +# async def tearDown(self): +# """Clean up the HomeAssistant instance.""" +# await self.hass.async_stop() + +# async def test_mark_sessions_idle(self): +# """Test marking media_players as idle when sessions end.""" +# hass = self.hass + +# entry = MockConfigEntry( +# domain=DOMAIN, +# data=DEFAULT_DATA, +# options=DEFAULT_OPTIONS, +# unique_id=DEFAULT_DATA["server_id"], +# ) + +# mock_plex_server = MockPlexServer(config_entry=entry) + +# with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( +# "homeassistant.components.plex.PlexWebsocket.listen" +# ): +# entry.add_to_hass(hass) +# assert await hass.config_entries.async_setup(entry.entry_id) +# await hass.async_block_till_done() + +# server_id = mock_plex_server.machineIdentifier + +# async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) +# await hass.async_block_till_done() + +# sensor = hass.states.get("sensor.plex_plex_server_1") +# assert sensor.state == str(len(mock_plex_server.accounts)) + +# mock_plex_server.clear_clients() +# mock_plex_server.clear_sessions() + +# await self.advance(DEBOUNCE_TIMEOUT) +# async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) +# await hass.async_block_till_done() + +# sensor = hass.states.get("sensor.plex_plex_server_1") +# assert sensor.state == "0" + +# async def test_debouncer(self): +# """Test debouncer behavior.""" +# hass = self.hass + +# entry = MockConfigEntry( +# domain=DOMAIN, +# data=DEFAULT_DATA, +# options=DEFAULT_OPTIONS, +# unique_id=DEFAULT_DATA["server_id"], +# ) + +# mock_plex_server = MockPlexServer(config_entry=entry) + +# with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( +# "homeassistant.components.plex.PlexWebsocket.listen" +# ): +# entry.add_to_hass(hass) +# assert await hass.config_entries.async_setup(entry.entry_id) +# await hass.async_block_till_done() + +# server_id = mock_plex_server.machineIdentifier + +# with patch.object(mock_plex_server, "clients", return_value=[]), patch.object( +# mock_plex_server, "sessions", return_value=[] +# ) as mock_update: +# # Called immediately +# async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) +# await hass.async_block_till_done() +# assert mock_update.call_count == 1 + +# # Throttled +# async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) +# await hass.async_block_till_done() +# assert mock_update.call_count == 1 + +# # Throttled +# async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) +# await hass.async_block_till_done() +# assert mock_update.call_count == 1 + +# # Called from scheduler +# await self.advance(DEBOUNCE_TIMEOUT) +# await hass.async_block_till_done() +# assert mock_update.call_count == 2 + +# # Throttled +# async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) +# await hass.async_block_till_done() +# assert mock_update.call_count == 2 + +# # Called from scheduler +# await self.advance(DEBOUNCE_TIMEOUT) +# await hass.async_block_till_done() +# assert mock_update.call_count == 3 diff --git a/tests/components/point/test_config_flow.py b/tests/components/point/test_config_flow.py index c1c705e752de32..1714dd5a3526ef 100644 --- a/tests/components/point/test_config_flow.py +++ b/tests/components/point/test_config_flow.py @@ -1,21 +1,20 @@ """Tests for the Point config flow.""" import asyncio -from unittest.mock import Mock, patch import pytest from homeassistant import data_entry_flow from homeassistant.components.point import DOMAIN, config_flow -from tests.common import mock_coro +from tests.async_mock import AsyncMock, patch def init_config_flow(hass, side_effect=None): """Init a configuration flow.""" config_flow.register_flow_implementation(hass, DOMAIN, "id", "secret") flow = config_flow.PointFlowHandler() - flow._get_authorization_url = Mock( # pylint: disable=protected-access - return_value=mock_coro("https://example.com"), side_effect=side_effect + flow._get_authorization_url = AsyncMock( # pylint: disable=protected-access + return_value="https://example.com", side_effect=side_effect ) flow.hass = hass return flow diff --git a/tests/components/powerwall/mocks.py b/tests/components/powerwall/mocks.py index 8a559d1dd49fc8..a384725daa8b5d 100644 --- a/tests/components/powerwall/mocks.py +++ b/tests/components/powerwall/mocks.py @@ -3,7 +3,6 @@ import json import os -from asynctest import MagicMock, Mock from tesla_powerwall import ( DeviceType, GridStatus, @@ -17,6 +16,7 @@ from homeassistant.components.powerwall.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS +from tests.async_mock import MagicMock, Mock from tests.common import load_fixture diff --git a/tests/components/powerwall/test_binary_sensor.py b/tests/components/powerwall/test_binary_sensor.py index c8a081de573020..fcca7fb34abb9d 100644 --- a/tests/components/powerwall/test_binary_sensor.py +++ b/tests/components/powerwall/test_binary_sensor.py @@ -1,13 +1,13 @@ """The binary sensor tests for the powerwall platform.""" -from asynctest import patch - from homeassistant.components.powerwall.const import DOMAIN from homeassistant.const import STATE_ON from homeassistant.setup import async_setup_component from .mocks import _mock_get_config, _mock_powerwall_with_fixtures +from tests.async_mock import patch + async def test_sensors(hass): """Test creation of the binary sensors.""" diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index 097346c5ac7a30..eaf53f0beefa72 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -1,6 +1,5 @@ """Test the Powerwall config flow.""" -from asynctest import patch from tesla_powerwall import APIChangedError, PowerwallUnreachableError from homeassistant import config_entries, setup @@ -9,6 +8,8 @@ from .mocks import _mock_powerwall_side_effect, _mock_powerwall_site_name +from tests.async_mock import patch + async def test_form_source_user(hass): """Test we get config flow setup form as a user.""" diff --git a/tests/components/powerwall/test_sensor.py b/tests/components/powerwall/test_sensor.py index c68d9f0279e857..af2835ea679baf 100644 --- a/tests/components/powerwall/test_sensor.py +++ b/tests/components/powerwall/test_sensor.py @@ -1,13 +1,13 @@ """The sensor tests for the powerwall platform.""" -from asynctest import patch - from homeassistant.components.powerwall.const import DOMAIN from homeassistant.const import UNIT_PERCENTAGE from homeassistant.setup import async_setup_component from .mocks import _mock_get_config, _mock_powerwall_with_fixtures +from tests.async_mock import patch + async def test_sensors(hass): """Test creation of the sensors.""" diff --git a/tests/components/ps4/conftest.py b/tests/components/ps4/conftest.py index e945af3220d5dd..42002404db25ae 100644 --- a/tests/components/ps4/conftest.py +++ b/tests/components/ps4/conftest.py @@ -1,7 +1,8 @@ """Test configuration for PS4.""" -from asynctest import patch import pytest +from tests.async_mock import patch + @pytest.fixture def patch_load_json(): diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index 2bad2d1e281a58..c5e0623de5bc9a 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -1,5 +1,4 @@ """Define tests for the PlayStation 4 config flow.""" -from asynctest import patch from pyps4_2ndscreen.errors import CredentialTimeout import pytest @@ -21,6 +20,7 @@ ) from homeassistant.util import location +from tests.async_mock import patch from tests.common import MockConfigEntry MOCK_TITLE = "PlayStation 4" diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index 68179ad54c20ae..7f7eff33ebb3a8 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -1,6 +1,4 @@ """Tests for the PS4 Integration.""" -from asynctest import MagicMock, patch - from homeassistant import config_entries, data_entry_flow from homeassistant.components import ps4 from homeassistant.components.media_player.const import ( @@ -29,6 +27,7 @@ from homeassistant.setup import async_setup_component from homeassistant.util import location +from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry, mock_registry MOCK_HOST = "192.168.0.1" diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index b7725795f5c7f3..8ff8dea71ce208 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -1,5 +1,4 @@ """Tests for the PS4 media player platform.""" -from asynctest import MagicMock, patch from pyps4_2ndscreen.credential import get_ddp_message from homeassistant.components import ps4 @@ -34,6 +33,7 @@ ) from homeassistant.setup import async_setup_component +from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry, mock_device_registry, mock_registry MOCK_CREDS = "123412341234abcd12341234abcd12341234abcd12341234abcd12341234abcd" diff --git a/tests/components/ptvsd/test_ptvsd.py b/tests/components/ptvsd/test_ptvsd.py index d4a2aa1ab94ffd..9df686cfcbd40c 100644 --- a/tests/components/ptvsd/test_ptvsd.py +++ b/tests/components/ptvsd/test_ptvsd.py @@ -2,13 +2,14 @@ from unittest.mock import patch -from asynctest import CoroutineMock from pytest import mark from homeassistant.bootstrap import _async_set_up_integrations import homeassistant.components.ptvsd as ptvsd_component from homeassistant.setup import async_setup_component +from tests.async_mock import AsyncMock + @mark.skip("causes code cover to fail") async def test_ptvsd(hass): @@ -42,9 +43,7 @@ async def test_ptvsd_bootstrap(hass): """Test loading ptvsd component with wait.""" config = {ptvsd_component.DOMAIN: {ptvsd_component.CONF_WAIT: True}} - with patch( - "homeassistant.components.ptvsd.async_setup", CoroutineMock() - ) as setup_mock: + with patch("homeassistant.components.ptvsd.async_setup", AsyncMock()) as setup_mock: setup_mock.return_value = True await _async_set_up_integrations(hass, config) diff --git a/tests/components/qwikswitch/test_init.py b/tests/components/qwikswitch/test_init.py index b0be2dbc81ce33..6075174fa984e8 100644 --- a/tests/components/qwikswitch/test_init.py +++ b/tests/components/qwikswitch/test_init.py @@ -3,13 +3,13 @@ import logging from aiohttp.client_exceptions import ClientError -from asynctest import Mock import pytest from yarl import URL from homeassistant.components.qwikswitch import DOMAIN as QWIKSWITCH from homeassistant.setup import async_setup_component +from tests.async_mock import Mock from tests.test_util.aiohttp import AiohttpClientMockResponse, MockLongPollSideEffect _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/rachio/test_config_flow.py b/tests/components/rachio/test_config_flow.py index cadc346e403936..7cc3f272e7a7e7 100644 --- a/tests/components/rachio/test_config_flow.py +++ b/tests/components/rachio/test_config_flow.py @@ -1,7 +1,4 @@ """Test the Rachio config flow.""" -from asynctest import patch -from asynctest.mock import MagicMock - from homeassistant import config_entries, setup from homeassistant.components.rachio.const import ( CONF_CUSTOM_URL, @@ -10,6 +7,7 @@ ) from homeassistant.const import CONF_API_KEY +from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py index 4351470cff53b1..04dc67bdbe814c 100644 --- a/tests/components/rainmachine/test_config_flow.py +++ b/tests/components/rainmachine/test_config_flow.py @@ -1,5 +1,4 @@ """Define tests for the OpenUV config flow.""" -from asynctest import patch from regenmaschine.errors import RainMachineError from homeassistant import data_entry_flow @@ -13,7 +12,8 @@ CONF_SSL, ) -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import patch +from tests.common import MockConfigEntry async def test_duplicate_error(hass): @@ -51,7 +51,7 @@ async def test_invalid_password(hass): with patch( "homeassistant.components.rainmachine.config_flow.login", - return_value=mock_coro(exception=RainMachineError), + side_effect=RainMachineError, ): result = await flow.async_step_user(user_input=conf) assert result["errors"] == {CONF_PASSWORD: "invalid_credentials"} @@ -84,8 +84,7 @@ async def test_step_import(hass): flow.context = {"source": SOURCE_USER} with patch( - "homeassistant.components.rainmachine.config_flow.login", - return_value=mock_coro(True), + "homeassistant.components.rainmachine.config_flow.login", return_value=True, ): result = await flow.async_step_import(import_config=conf) @@ -116,8 +115,7 @@ async def test_step_user(hass): flow.context = {"source": SOURCE_USER} with patch( - "homeassistant.components.rainmachine.config_flow.login", - return_value=mock_coro(True), + "homeassistant.components.rainmachine.config_flow.login", return_value=True, ): result = await flow.async_step_user(user_input=conf) diff --git a/tests/components/ring/test_config_flow.py b/tests/components/ring/test_config_flow.py index 421e8a26694a28..57723d1ede75c6 100644 --- a/tests/components/ring/test_config_flow.py +++ b/tests/components/ring/test_config_flow.py @@ -1,11 +1,9 @@ """Test the Ring config flow.""" -from asynctest 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 +from tests.async_mock import Mock, patch async def test_form(hass): @@ -23,9 +21,9 @@ async def test_form(hass): fetch_token=Mock(return_value={"access_token": "mock-token"}) ), ), patch( - "homeassistant.components.ring.async_setup", return_value=mock_coro(True) + "homeassistant.components.ring.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.ring.async_setup_entry", return_value=mock_coro(True), + "homeassistant.components.ring.async_setup_entry", return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/rmvtransport/test_sensor.py b/tests/components/rmvtransport/test_sensor.py index 58c9192de7df44..ece4142f8c7d2f 100644 --- a/tests/components/rmvtransport/test_sensor.py +++ b/tests/components/rmvtransport/test_sensor.py @@ -1,10 +1,10 @@ """The tests for the rmvtransport platform.""" import datetime -from asynctest import patch - from homeassistant.setup import async_setup_component +from tests.async_mock import patch + VALID_CONFIG_MINIMAL = { "sensor": {"platform": "rmvtransport", "next_departure": [{"station": "3000010"}]} } diff --git a/tests/components/roku/test_config_flow.py b/tests/components/roku/test_config_flow.py index e6c725e9959475..f59b36b6f30fc4 100644 --- a/tests/components/roku/test_config_flow.py +++ b/tests/components/roku/test_config_flow.py @@ -1,7 +1,6 @@ """Test the Roku config flow.""" from socket import gaierror as SocketGIAError -from asynctest import patch from requests.exceptions import RequestException from requests_mock import Mocker from roku import RokuException @@ -22,6 +21,7 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.components.roku import ( HOST, SSDP_LOCATION, diff --git a/tests/components/roku/test_init.py b/tests/components/roku/test_init.py index c597ebef6f769e..fcbe9aa4b7deb3 100644 --- a/tests/components/roku/test_init.py +++ b/tests/components/roku/test_init.py @@ -1,7 +1,6 @@ """Tests for the Roku integration.""" from socket import gaierror as SocketGIAError -from asynctest import patch from requests.exceptions import RequestException from requests_mock import Mocker from roku import RokuException @@ -14,6 +13,7 @@ ) from homeassistant.helpers.typing import HomeAssistantType +from tests.async_mock import patch from tests.components.roku import setup_integration diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index 4965207a5b81d7..3b11844450e3b1 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -1,7 +1,6 @@ """Tests for the Roku Media Player platform.""" from datetime import timedelta -from asynctest import PropertyMock, patch from requests.exceptions import ( ConnectionError as RequestsConnectionError, ReadTimeout as RequestsReadTimeout, @@ -46,6 +45,7 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt as dt_util +from tests.async_mock import PropertyMock, patch from tests.common import async_fire_time_changed from tests.components.roku import UPNP_SERIAL, setup_integration diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index c9a0d8fde17c1c..0941586f788bce 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -1,5 +1,4 @@ """Test the iRobot Roomba config flow.""" -from asynctest import MagicMock, PropertyMock, patch from roomba import RoombaConnectionError from homeassistant import config_entries, data_entry_flow, setup @@ -12,6 +11,7 @@ ) from homeassistant.const import CONF_HOST, CONF_PASSWORD +from tests.async_mock import MagicMock, PropertyMock, patch from tests.common import MockConfigEntry VALID_CONFIG = {CONF_HOST: "1.2.3.4", CONF_BLID: "blid", CONF_PASSWORD: "password"} diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 4e5a02588b1b9d..2673ee565599c7 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -1,7 +1,4 @@ """Tests for Samsung TV config flow.""" -from unittest.mock import Mock, PropertyMock, call, patch - -from asynctest import mock import pytest from samsungctl.exceptions import AccessDenied, UnhandledResponse from samsungtvws.exceptions import ConnectionFailure @@ -21,6 +18,8 @@ ) from homeassistant.const import CONF_HOST, CONF_ID, CONF_METHOD, CONF_NAME, CONF_TOKEN +from tests.async_mock import DEFAULT as DEFAULT_MOCK, Mock, PropertyMock, call, patch + MOCK_USER_DATA = {CONF_HOST: "fake_host", CONF_NAME: "fake_name"} MOCK_SSDP_DATA = { ATTR_SSDP_LOCATION: "https://fake_host:12345/test", @@ -70,11 +69,11 @@ def remote_fixture(): ) as remote_class, patch( "homeassistant.components.samsungtv.config_flow.socket" ) as socket_class: - remote = mock.Mock() - remote.__enter__ = mock.Mock() - remote.__exit__ = mock.Mock() + remote = Mock() + remote.__enter__ = Mock() + remote.__exit__ = Mock() remote_class.return_value = remote - socket = mock.Mock() + socket = Mock() socket_class.return_value = socket socket_class.gethostbyname.return_value = "FAKE_IP_ADDRESS" yield remote @@ -88,12 +87,12 @@ def remotews_fixture(): ) as remotews_class, patch( "homeassistant.components.samsungtv.config_flow.socket" ) as socket_class: - remotews = mock.Mock() - remotews.__enter__ = mock.Mock() - remotews.__exit__ = mock.Mock() + remotews = Mock() + remotews.__enter__ = Mock() + remotews.__exit__ = Mock() remotews_class.return_value = remotews remotews_class().__enter__().token = "FAKE_TOKEN" - socket = mock.Mock() + socket = Mock() socket_class.return_value = socket socket_class.gethostbyname.return_value = "FAKE_IP_ADDRESS" yield remotews @@ -486,7 +485,7 @@ async def test_autodetect_websocket_ssl(hass, remote, remotews): "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", - side_effect=[WebSocketProtocolException("Boom"), mock.DEFAULT], + side_effect=[WebSocketProtocolException("Boom"), DEFAULT_MOCK], ) as remotews: enter = Mock() type(enter).token = PropertyMock(return_value="123456789") diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 232a04416d5f29..5ef47cb310620a 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -1,6 +1,4 @@ """Tests for the Samsung TV Integration.""" -from asynctest import mock -from asynctest.mock import call, patch import pytest from homeassistant.components.media_player.const import DOMAIN, SUPPORT_TURN_ON @@ -18,6 +16,8 @@ ) from homeassistant.setup import async_setup_component +from tests.async_mock import Mock, call, patch + ENTITY_ID = f"{DOMAIN}.fake_name" MOCK_CONFIG = { SAMSUNGTV_DOMAIN: [ @@ -49,9 +49,9 @@ def remote_fixture(): ) as socket1, patch( "homeassistant.components.samsungtv.socket" ) as socket2: - remote = mock.Mock() - remote.__enter__ = mock.Mock() - remote.__exit__ = mock.Mock() + remote = Mock() + remote.__enter__ = Mock() + remote.__exit__ = Mock() remote_class.return_value = remote socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index c549e57a06eacb..15ac13c64d5926 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -3,8 +3,6 @@ from datetime import timedelta import logging -from asynctest import mock -from asynctest.mock import call, patch import pytest from samsungctl import exceptions from samsungtvws.exceptions import ConnectionFailure @@ -54,6 +52,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import DEFAULT as DEFAULT_MOCK, Mock, PropertyMock, call, patch from tests.common import MockConfigEntry, async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake" @@ -120,9 +119,9 @@ def remote_fixture(): ) as socket1, patch( "homeassistant.components.samsungtv.socket" ) as socket2: - remote = mock.Mock() - remote.__enter__ = mock.Mock() - remote.__exit__ = mock.Mock() + remote = Mock() + remote.__enter__ = Mock() + remote.__exit__ = Mock() remote_class.return_value = remote socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" @@ -139,9 +138,9 @@ def remotews_fixture(): ) as socket1, patch( "homeassistant.components.samsungtv.socket" ) as socket2: - remote = mock.Mock() - remote.__enter__ = mock.Mock() - remote.__exit__ = mock.Mock() + remote = Mock() + remote.__enter__ = Mock() + remote.__exit__ = Mock() remote_class.return_value = remote remote_class().__enter__().token = "FAKE_TOKEN" socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" @@ -185,11 +184,11 @@ async def test_setup_without_turnon(hass, remote): async def test_setup_websocket(hass, remotews, mock_now): """Test setup of platform.""" with patch("homeassistant.components.samsungtv.bridge.SamsungTVWS") as remote_class: - enter = mock.Mock() - type(enter).token = mock.PropertyMock(return_value="987654321") - remote = mock.Mock() - remote.__enter__ = mock.Mock(return_value=enter) - remote.__exit__ = mock.Mock() + enter = Mock() + type(enter).token = PropertyMock(return_value="987654321") + remote = Mock() + remote.__enter__ = Mock(return_value=enter) + remote.__exit__ = Mock() remote_class.return_value = remote await setup_samsungtv(hass, MOCK_CONFIGWS) @@ -247,7 +246,7 @@ async def test_update_off(hass, remote, mock_now): with patch( "homeassistant.components.samsungtv.bridge.Remote", - side_effect=[OSError("Boom"), mock.DEFAULT], + side_effect=[OSError("Boom"), DEFAULT_MOCK], ): next_update = mock_now + timedelta(minutes=5) @@ -283,7 +282,7 @@ async def test_update_connection_failure(hass, remotews, mock_now): """Testing update tv connection failure exception.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", - side_effect=[OSError("Boom"), mock.DEFAULT], + side_effect=[OSError("Boom"), DEFAULT_MOCK], ): await setup_samsungtv(hass, MOCK_CONFIGWS) @@ -309,7 +308,7 @@ async def test_update_unhandled_response(hass, remote, mock_now): with patch( "homeassistant.components.samsungtv.bridge.Remote", - side_effect=[exceptions.UnhandledResponse("Boom"), mock.DEFAULT], + side_effect=[exceptions.UnhandledResponse("Boom"), DEFAULT_MOCK], ): next_update = mock_now + timedelta(minutes=5) @@ -339,7 +338,7 @@ async def test_send_key(hass, remote): async def test_send_key_broken_pipe(hass, remote): """Testing broken pipe Exception.""" await setup_samsungtv(hass, MOCK_CONFIG) - remote.control = mock.Mock(side_effect=BrokenPipeError("Boom")) + remote.control = Mock(side_effect=BrokenPipeError("Boom")) assert await hass.services.async_call( DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -350,8 +349,8 @@ async def test_send_key_broken_pipe(hass, remote): async def test_send_key_connection_closed_retry_succeed(hass, remote): """Test retry on connection closed.""" await setup_samsungtv(hass, MOCK_CONFIG) - remote.control = mock.Mock( - side_effect=[exceptions.ConnectionClosed("Boom"), mock.DEFAULT, mock.DEFAULT] + remote.control = Mock( + side_effect=[exceptions.ConnectionClosed("Boom"), DEFAULT_MOCK, DEFAULT_MOCK] ) assert await hass.services.async_call( DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True @@ -371,7 +370,7 @@ async def test_send_key_connection_closed_retry_succeed(hass, remote): async def test_send_key_unhandled_response(hass, remote): """Testing unhandled response exception.""" await setup_samsungtv(hass, MOCK_CONFIG) - remote.control = mock.Mock(side_effect=exceptions.UnhandledResponse("Boom")) + remote.control = Mock(side_effect=exceptions.UnhandledResponse("Boom")) assert await hass.services.async_call( DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -382,7 +381,7 @@ async def test_send_key_unhandled_response(hass, remote): async def test_send_key_websocketexception(hass, remote): """Testing unhandled response exception.""" await setup_samsungtv(hass, MOCK_CONFIG) - remote.control = mock.Mock(side_effect=WebSocketException("Boom")) + remote.control = Mock(side_effect=WebSocketException("Boom")) assert await hass.services.async_call( DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -393,7 +392,7 @@ async def test_send_key_websocketexception(hass, remote): async def test_send_key_os_error(hass, remote): """Testing broken pipe Exception.""" await setup_samsungtv(hass, MOCK_CONFIG) - remote.control = mock.Mock(side_effect=OSError("Boom")) + remote.control = Mock(side_effect=OSError("Boom")) assert await hass.services.async_call( DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -467,7 +466,7 @@ async def test_turn_off_websocket(hass, remotews): """Test for turn_off.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", - side_effect=[OSError("Boom"), mock.DEFAULT], + side_effect=[OSError("Boom"), DEFAULT_MOCK], ): await setup_samsungtv(hass, MOCK_CONFIGWS) assert await hass.services.async_call( @@ -493,7 +492,7 @@ async def test_turn_off_os_error(hass, remote, caplog): """Test for turn_off with OSError.""" caplog.set_level(logging.DEBUG) await setup_samsungtv(hass, MOCK_CONFIG) - remote.close = mock.Mock(side_effect=OSError("BOOM")) + remote.close = Mock(side_effect=OSError("BOOM")) assert await hass.services.async_call( DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) diff --git a/tests/components/sense/test_config_flow.py b/tests/components/sense/test_config_flow.py index fdce335b7cf06d..5a38090b5c9001 100644 --- a/tests/components/sense/test_config_flow.py +++ b/tests/components/sense/test_config_flow.py @@ -1,10 +1,11 @@ """Test the Sense config flow.""" -from asynctest import patch from sense_energy import SenseAPITimeoutException, SenseAuthenticationException from homeassistant import config_entries, setup from homeassistant.components.sense.const import DOMAIN +from tests.async_mock import patch + async def test_form(hass): """Test we get the form.""" diff --git a/tests/components/sentry/test_config_flow.py b/tests/components/sentry/test_config_flow.py index 22c4367ddf2e01..25353751f911f3 100644 --- a/tests/components/sentry/test_config_flow.py +++ b/tests/components/sentry/test_config_flow.py @@ -1,11 +1,10 @@ """Test the sentry config flow.""" -from asynctest import patch from sentry_sdk.utils import BadDsn from homeassistant import config_entries, setup from homeassistant.components.sentry.const import DOMAIN -from tests.common import mock_coro +from tests.async_mock import patch async def test_form(hass): @@ -19,12 +18,11 @@ async def test_form(hass): with patch( "homeassistant.components.sentry.config_flow.validate_input", - return_value=mock_coro({"title": "Sentry"}), + return_value={"title": "Sentry"}, ), patch( - "homeassistant.components.sentry.async_setup", return_value=mock_coro(True) + "homeassistant.components.sentry.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.sentry.async_setup_entry", - return_value=mock_coro(True), + "homeassistant.components.sentry.async_setup_entry", return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"dsn": "http://public@sentry.local/1"}, diff --git a/tests/components/shell_command/test_init.py b/tests/components/shell_command/test_init.py index 156741d8c9b879..8743ab27bd75dd 100644 --- a/tests/components/shell_command/test_init.py +++ b/tests/components/shell_command/test_init.py @@ -5,15 +5,13 @@ from typing import Tuple import unittest -from asynctest import Mock, patch - from homeassistant.components import shell_command from homeassistant.setup import setup_component +from tests.async_mock import Mock, patch from tests.common import get_test_home_assistant -@asyncio.coroutine def mock_process_creator(error: bool = False) -> asyncio.coroutine: """Mock a coroutine that creates a process when yielded.""" diff --git a/tests/components/shopping_list/conftest.py b/tests/components/shopping_list/conftest.py index f63363e2f63022..8307a6845b1095 100644 --- a/tests/components/shopping_list/conftest.py +++ b/tests/components/shopping_list/conftest.py @@ -1,9 +1,9 @@ """Shopping list test helpers.""" -from asynctest import patch import pytest from homeassistant.components.shopping_list import intent as sl_intent +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index f53636fc4407ac..5dc0fd5698ba76 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -2,7 +2,6 @@ import json from unittest.mock import MagicMock, PropertyMock, mock_open -from asynctest import patch from simplipy.errors import SimplipyError from homeassistant import data_entry_flow @@ -10,6 +9,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index e05917c17d8804..a47b06ee637ffb 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -2,7 +2,6 @@ import secrets from uuid import uuid4 -from asynctest import Mock, patch from pysmartthings import ( CLASSIFICATION_AUTOMATION, AppEntity, @@ -42,6 +41,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN, CONF_WEBHOOK_ID from homeassistant.setup import async_setup_component +from tests.async_mock import Mock, patch from tests.common import MockConfigEntry COMPONENT_PREFIX = "homeassistant.components.smartthings." diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index 81dbab917a3717..ca964b9d1da331 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -2,7 +2,6 @@ from uuid import uuid4 from aiohttp import ClientResponseError -from asynctest import Mock, patch from pysmartthings import APIResponseError from pysmartthings.installedapp import format_install_url @@ -23,7 +22,8 @@ HTTP_UNAUTHORIZED, ) -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import AsyncMock, Mock, patch +from tests.common import MockConfigEntry async def test_import_shows_user_step(hass): @@ -342,8 +342,8 @@ async def test_entry_created_with_cloudhook( installed_app_id = str(uuid4()) refresh_token = str(uuid4()) smartthings_mock.apps.return_value = [] - smartthings_mock.create_app.return_value = (app, app_oauth_client) - smartthings_mock.locations.return_value = [location] + smartthings_mock.create_app = AsyncMock(return_value=(app, app_oauth_client)) + smartthings_mock.locations = AsyncMock(return_value=[location]) request = Mock() request.installed_app_id = installed_app_id request.auth_token = token @@ -351,11 +351,11 @@ async def test_entry_created_with_cloudhook( request.refresh_token = refresh_token with patch.object( - hass.components.cloud, "async_active_subscription", return_value=True + hass.components.cloud, "async_active_subscription", Mock(return_value=True) ), patch.object( hass.components.cloud, "async_create_cloudhook", - return_value=mock_coro("http://cloud.test"), + AsyncMock(return_value="http://cloud.test"), ) as mock_create_cloudhook: await smartapp.setup_smartapp_endpoint(hass) diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 78142ae3fc48e6..0fdbf5c255ab18 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -2,7 +2,6 @@ from uuid import uuid4 from aiohttp import ClientConnectionError, ClientResponseError -from asynctest import Mock, patch from pysmartthings import InstalledAppStatus, OAuthToken import pytest @@ -22,6 +21,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.setup import async_setup_component +from tests.async_mock import Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/smartthings/test_smartapp.py b/tests/components/smartthings/test_smartapp.py index efc4844cef295f..458e5f8ce279b5 100644 --- a/tests/components/smartthings/test_smartapp.py +++ b/tests/components/smartthings/test_smartapp.py @@ -1,7 +1,6 @@ """Tests for the smartapp module.""" from uuid import uuid4 -from asynctest import CoroutineMock, Mock, patch from pysmartthings import AppEntity, Capability from homeassistant.components.smartthings import smartapp @@ -11,6 +10,7 @@ DOMAIN, ) +from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry @@ -76,11 +76,11 @@ async def test_smartapp_uninstall(hass, config_entry): async def test_smartapp_webhook(hass): """Test the smartapp webhook calls the manager.""" manager = Mock() - manager.handle_request = CoroutineMock(return_value={}) + manager.handle_request = AsyncMock(return_value={}) hass.data[DOMAIN][DATA_MANAGER] = manager request = Mock() request.headers = [] - request.json = CoroutineMock(return_value={}) + request.json = AsyncMock(return_value={}) result = await smartapp.smartapp_webhook(hass, "", request) assert result.body == b"{}" diff --git a/tests/components/smhi/test_config_flow.py b/tests/components/smhi/test_config_flow.py index 6065c323b9147e..56a0745c1b3c02 100644 --- a/tests/components/smhi/test_config_flow.py +++ b/tests/components/smhi/test_config_flow.py @@ -1,11 +1,10 @@ """Tests for SMHI config flow.""" -from asynctest import Mock, patch from smhi.smhi_lib import Smhi as SmhiApi, SmhiForecastException from homeassistant.components.smhi import config_flow from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE -from tests.common import mock_coro +from tests.async_mock import Mock, patch # pylint: disable=protected-access @@ -14,7 +13,7 @@ async def test_homeassistant_location_exists() -> None: hass = Mock() flow = config_flow.SmhiFlowHandler() flow.hass = hass - with patch.object(flow, "_check_location", return_value=mock_coro(True)): + with patch.object(flow, "_check_location", return_value=True): # Test exists hass.config.location_name = "Home" hass.config.latitude = 17.8419 @@ -99,7 +98,7 @@ async def test_flow_with_home_location(hass) -> None: flow = config_flow.SmhiFlowHandler() flow.hass = hass - with patch.object(flow, "_check_location", return_value=mock_coro(True)): + with patch.object(flow, "_check_location", return_value=True): hass.config.location_name = "Home" hass.config.latitude = 17.8419 hass.config.longitude = 59.3262 @@ -121,9 +120,9 @@ async def test_flow_show_form() -> None: # Test show form when Home Assistant config exists and # home is already configured, then new config is allowed with patch.object( - flow, "_show_config_form", return_value=mock_coro() + flow, "_show_config_form", return_value=None ) as config_form, patch.object( - flow, "_homeassistant_location_exists", return_value=mock_coro(True) + flow, "_homeassistant_location_exists", return_value=True ), patch.object( config_flow, "smhi_locations", @@ -135,9 +134,9 @@ async def test_flow_show_form() -> None: # Test show form when Home Assistant config not and # home is not configured with patch.object( - flow, "_show_config_form", return_value=mock_coro() + flow, "_show_config_form", return_value=None ) as config_form, patch.object( - flow, "_homeassistant_location_exists", return_value=mock_coro(False) + flow, "_homeassistant_location_exists", return_value=False ), patch.object( config_flow, "smhi_locations", @@ -160,7 +159,7 @@ async def test_flow_show_form_name_exists() -> None: # Test show form when Home Assistant config exists and # home is already configured, then new config is allowed with patch.object( - flow, "_show_config_form", return_value=mock_coro() + flow, "_show_config_form", return_value=None ) as config_form, patch.object( flow, "_name_in_configuration_exists", return_value=True ), patch.object( @@ -168,7 +167,7 @@ async def test_flow_show_form_name_exists() -> None: "smhi_locations", return_value={"test": "something", "name_exist": "config"}, ), patch.object( - flow, "_check_location", return_value=mock_coro(True) + flow, "_check_location", return_value=True ): await flow.async_step_user(user_input=test_data) @@ -190,17 +189,17 @@ async def test_flow_entry_created_from_user_input() -> None: # Test that entry created when user_input name not exists with patch.object( - flow, "_show_config_form", return_value=mock_coro() + flow, "_show_config_form", return_value=None ) as config_form, patch.object( flow, "_name_in_configuration_exists", return_value=False ), patch.object( - flow, "_homeassistant_location_exists", return_value=mock_coro(False) + flow, "_homeassistant_location_exists", return_value=False ), patch.object( config_flow, "smhi_locations", return_value={"test": "something", "name_exist": "config"}, ), patch.object( - flow, "_check_location", return_value=mock_coro(True) + flow, "_check_location", return_value=True ): result = await flow.async_step_user(user_input=test_data) @@ -223,20 +222,18 @@ async def test_flow_entry_created_user_input_faulty() -> None: test_data = {"name": "home", CONF_LONGITUDE: "0", CONF_LATITUDE: "0"} # Test that entry created when user_input name not exists - with patch.object( - flow, "_check_location", return_value=mock_coro(True) - ), patch.object( - flow, "_show_config_form", return_value=mock_coro() + with patch.object(flow, "_check_location", return_value=True), patch.object( + flow, "_show_config_form", return_value=None ) as config_form, patch.object( flow, "_name_in_configuration_exists", return_value=False ), patch.object( - flow, "_homeassistant_location_exists", return_value=mock_coro(False) + flow, "_homeassistant_location_exists", return_value=False ), patch.object( config_flow, "smhi_locations", return_value={"test": "something", "name_exist": "config"}, ), patch.object( - flow, "_check_location", return_value=mock_coro(False) + flow, "_check_location", return_value=False ): await flow.async_step_user(user_input=test_data) @@ -253,7 +250,7 @@ async def test_check_location_correct() -> None: with patch.object( config_flow.aiohttp_client, "async_get_clientsession" - ), patch.object(SmhiApi, "async_get_forecast", return_value=mock_coro()): + ), patch.object(SmhiApi, "async_get_forecast", return_value=None): assert await flow._check_location("58", "17") is True diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index 357b670a38a249..6f5fdfd2333aec 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -3,7 +3,7 @@ from datetime import datetime import logging -from asynctest import Mock, patch +from smhi.smhi_lib import APIURL_TEMPLATE, SmhiForecastException from homeassistant.components.smhi import weather as weather_smhi from homeassistant.components.smhi.const import ATTR_SMHI_CLOUDINESS @@ -25,6 +25,7 @@ from homeassistant.const import TEMP_CELSIUS from homeassistant.core import HomeAssistant +from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry, load_fixture _LOGGER = logging.getLogger(__name__) @@ -40,8 +41,6 @@ async def test_setup_hass(hass: HomeAssistant, aioclient_mock) -> None: "async_forward_entry_setup". The actual result are tested with the entity state rather than "per function" unity tests """ - from smhi.smhi_lib import APIURL_TEMPLATE - uri = APIURL_TEMPLATE.format(TEST_CONFIG["longitude"], TEST_CONFIG["latitude"]) api_response = load_fixture("smhi.json") aioclient_mock.get(uri, text=api_response) @@ -150,7 +149,6 @@ def test_properties_unknown_symbol() -> None: # pylint: disable=protected-access async def test_refresh_weather_forecast_exceeds_retries(hass) -> None: """Test the refresh weather forecast function.""" - from smhi.smhi_lib import SmhiForecastException with patch.object( hass.helpers.event, "async_call_later" @@ -192,7 +190,6 @@ async def test_refresh_weather_forecast_timeout(hass) -> None: async def test_refresh_weather_forecast_exception() -> None: """Test any exception.""" - from smhi.smhi_lib import SmhiForecastException hass = Mock() weather = weather_smhi.SmhiWeather("name", "17.0022", "62.0022") @@ -200,17 +197,9 @@ async def test_refresh_weather_forecast_exception() -> None: with patch.object( hass.helpers.event, "async_call_later" - ) as call_later, patch.object(weather_smhi, "async_timeout"), patch.object( - weather_smhi.SmhiWeather, "retry_update" - ), patch.object( - weather_smhi.SmhiWeather, - "get_weather_forecast", - side_effect=SmhiForecastException(), + ) as call_later, patch.object( + weather, "get_weather_forecast", side_effect=SmhiForecastException(), ): - - hass.async_add_job = Mock() - call_later = hass.helpers.event.async_call_later - await weather.async_update() assert len(call_later.mock_calls) == 1 # Assert we are going to wait RETRY_TIMEOUT seconds @@ -223,8 +212,8 @@ async def test_retry_update(): weather = weather_smhi.SmhiWeather("name", "17.0022", "62.0022") weather.hass = hass - with patch.object(weather_smhi.SmhiWeather, "async_update") as update: - await weather.retry_update() + with patch.object(weather, "async_update", AsyncMock()) as update: + await weather.retry_update(None) assert len(update.mock_calls) == 1 diff --git a/tests/components/solarlog/test_config_flow.py b/tests/components/solarlog/test_config_flow.py index 4d8c11074dbcad..1474b8e13e76c9 100644 --- a/tests/components/solarlog/test_config_flow.py +++ b/tests/components/solarlog/test_config_flow.py @@ -1,5 +1,4 @@ """Test the solarlog config flow.""" -from asynctest import patch import pytest from homeassistant import config_entries, data_entry_flow, setup @@ -7,7 +6,8 @@ from homeassistant.components.solarlog.const import DEFAULT_HOST, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import patch +from tests.common import MockConfigEntry NAME = "Solarlog test 1 2 3" HOST = "http://1.1.1.1" @@ -24,12 +24,11 @@ async def test_form(hass): with patch( "homeassistant.components.solarlog.config_flow.SolarLogConfigFlow._test_connection", - return_value=mock_coro({"title": "solarlog test 1 2 3"}), + return_value={"title": "solarlog test 1 2 3"}, ), patch( - "homeassistant.components.solarlog.async_setup", return_value=mock_coro(True) + "homeassistant.components.solarlog.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.solarlog.async_setup_entry", - return_value=mock_coro(True), + "homeassistant.components.solarlog.async_setup_entry", return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": HOST, "name": NAME} @@ -48,7 +47,7 @@ def mock_controller(): """Mock a successful _host_in_configuration_exists.""" with patch( "homeassistant.components.solarlog.config_flow.SolarLogConfigFlow._test_connection", - side_effect=lambda *_: mock_coro(True), + return_value=True, ): yield diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 246d1eb16277f3..20c1eb10320f73 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -1,11 +1,11 @@ """Configuration for Sonos tests.""" -from asynctest.mock import Mock, patch as patch import pytest from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.sonos import DOMAIN from homeassistant.const import CONF_HOSTS +from tests.async_mock import Mock, patch as patch from tests.common import MockConfigEntry diff --git a/tests/components/soundtouch/test_media_player.py b/tests/components/soundtouch/test_media_player.py index ea580656b24b7a..bc3cc1bc977557 100644 --- a/tests/components/soundtouch/test_media_player.py +++ b/tests/components/soundtouch/test_media_player.py @@ -1,7 +1,6 @@ """Test the Soundtouch component.""" from unittest.mock import call -from asynctest import patch from libsoundtouch.device import ( Config, Preset, @@ -28,6 +27,8 @@ from homeassistant.helpers.discovery import async_load_platform from homeassistant.setup import async_setup_component +from tests.async_mock import patch + # pylint: disable=super-init-not-called diff --git a/tests/components/stream/test_init.py b/tests/components/stream/test_init.py index 0661a5a9738d9c..12fa8e8a4d6fc2 100644 --- a/tests/components/stream/test_init.py +++ b/tests/components/stream/test_init.py @@ -14,7 +14,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component -from tests.common import mock_coro +from tests.async_mock import AsyncMock async def test_record_service_invalid_file(hass): @@ -76,7 +76,7 @@ async def test_record_service_lookback(hass): hls_mock = MagicMock() hls_mock.num_segments = 3 hls_mock.target_duration = 2 - hls_mock.recv.return_value = mock_coro() + hls_mock.recv = AsyncMock(return_value=None) stream_mock.return_value.outputs = {"hls": hls_mock} # Call Service diff --git a/tests/components/switcher_kis/conftest.py b/tests/components/switcher_kis/conftest.py index 2b0150cae67085..37d61d6ca19501 100644 --- a/tests/components/switcher_kis/conftest.py +++ b/tests/components/switcher_kis/conftest.py @@ -4,7 +4,6 @@ from datetime import datetime from typing import Any, Generator, Optional -from asynctest import CoroutineMock, patch from pytest import fixture from .consts import ( @@ -20,6 +19,8 @@ DUMMY_REMAINING_TIME, ) +from tests.async_mock import AsyncMock, patch + @patch("aioswitcher.devices.SwitcherV2Device") class MockSwitcherV2Device: @@ -100,7 +101,7 @@ async def mock_queue(): await queue.put(MockSwitcherV2Device()) return await queue.get() - mock_bridge = CoroutineMock() + mock_bridge = AsyncMock() patchers = [ patch( @@ -163,9 +164,9 @@ async def mock_queue(): @fixture(name="mock_api") -def mock_api_fixture() -> Generator[CoroutineMock, Any, None]: +def mock_api_fixture() -> Generator[AsyncMock, Any, None]: """Fixture for mocking aioswitcher.api.SwitcherV2Api.""" - mock_api = CoroutineMock() + mock_api = AsyncMock() patchers = [ patch( diff --git a/tests/components/tado/test_config_flow.py b/tests/components/tado/test_config_flow.py index 0c75eadfb0eb03..2e42a2bc1fb6f1 100644 --- a/tests/components/tado/test_config_flow.py +++ b/tests/components/tado/test_config_flow.py @@ -1,11 +1,11 @@ """Test the Tado config flow.""" -from asynctest import MagicMock, patch import requests from homeassistant import config_entries, setup from homeassistant.components.tado.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/tesla/test_config_flow.py b/tests/components/tesla/test_config_flow.py index 24cdb375fde66c..a59daee9ac6672 100644 --- a/tests/components/tesla/test_config_flow.py +++ b/tests/components/tesla/test_config_flow.py @@ -1,5 +1,4 @@ """Test the Tesla config flow.""" -from asynctest import patch from teslajsonpy import TeslaException from homeassistant import config_entries, data_entry_flow, setup @@ -19,7 +18,8 @@ HTTP_NOT_FOUND, ) -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import patch +from tests.common import MockConfigEntry async def test_form(hass): @@ -33,11 +33,11 @@ async def test_form(hass): with patch( "homeassistant.components.tesla.config_flow.TeslaAPI.connect", - return_value=mock_coro(("test-refresh-token", "test-access-token")), + return_value=("test-refresh-token", "test-access-token"), ), patch( - "homeassistant.components.tesla.async_setup", return_value=mock_coro(True) + "homeassistant.components.tesla.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.tesla.async_setup_entry", return_value=mock_coro(True) + "homeassistant.components.tesla.async_setup_entry", return_value=True ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PASSWORD: "test", CONF_USERNAME: "test@email.com"} @@ -102,7 +102,7 @@ async def test_form_repeat_identifier(hass): ) with patch( "homeassistant.components.tesla.config_flow.TeslaAPI.connect", - return_value=mock_coro(("test-refresh-token", "test-access-token")), + return_value=("test-refresh-token", "test-access-token"), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -118,7 +118,7 @@ async def test_import(hass): with patch( "homeassistant.components.tesla.config_flow.TeslaAPI.connect", - return_value=mock_coro(("test-refresh-token", "test-access-token")), + return_value=("test-refresh-token", "test-access-token"), ): result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/tradfri/conftest.py b/tests/components/tradfri/conftest.py index bb1a0e7cc64d93..891dd1377fe48b 100644 --- a/tests/components/tradfri/conftest.py +++ b/tests/components/tradfri/conftest.py @@ -1,7 +1,8 @@ """Common tradfri test fixtures.""" -from asynctest import patch import pytest +from tests.async_mock import patch + @pytest.fixture def mock_gateway_info(): diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index 45e18be83d3113..43e33706bf65e5 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -1,10 +1,10 @@ """Test the Tradfri config flow.""" -from asynctest import patch import pytest from homeassistant import data_entry_flow from homeassistant.components.tradfri import config_flow +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/tradfri/test_init.py b/tests/components/tradfri/test_init.py index bdb45ee43065d7..2845137244bf70 100644 --- a/tests/components/tradfri/test_init.py +++ b/tests/components/tradfri/test_init.py @@ -1,8 +1,7 @@ """Tests for Tradfri setup.""" -from asynctest import patch - from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/uk_transport/test_sensor.py b/tests/components/uk_transport/test_sensor.py index 4979efc22dc557..b8eddb9c7858c7 100644 --- a/tests/components/uk_transport/test_sensor.py +++ b/tests/components/uk_transport/test_sensor.py @@ -2,7 +2,6 @@ import re import unittest -from asynctest import patch import requests_mock from homeassistant.components.uk_transport.sensor import ( @@ -20,6 +19,7 @@ from homeassistant.setup import setup_component from homeassistant.util.dt import now +from tests.async_mock import patch from tests.common import get_test_home_assistant, load_fixture BUS_ATCOCODE = "340000368SHE" diff --git a/tests/components/unifi/conftest.py b/tests/components/unifi/conftest.py index 189b80c193218f..8ce7cef0345f58 100644 --- a/tests/components/unifi/conftest.py +++ b/tests/components/unifi/conftest.py @@ -1,7 +1,8 @@ """Fixtures for UniFi methods.""" -from asynctest import patch import pytest +from tests.async_mock import patch + @pytest.fixture(autouse=True) def mock_discovery(): diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index bad191e1600b00..ae738ba8a644ef 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -1,6 +1,5 @@ """Test UniFi config flow.""" import aiounifi -from asynctest import patch from homeassistant import data_entry_flow from homeassistant.components.unifi.const import ( @@ -27,6 +26,7 @@ from .test_controller import setup_unifi_integration +from tests.async_mock import patch from tests.common import MockConfigEntry CLIENTS = [{"mac": "00:00:00:00:00:01"}] diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 824ffbffc416e7..9bf956e8063e52 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -4,7 +4,6 @@ from datetime import timedelta import aiounifi -from asynctest import patch import pytest from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN @@ -35,6 +34,7 @@ ) from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import MockConfigEntry CONTROLLER_HOST = { diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index df7c49a1bf745e..5f40f098d39fa1 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -4,7 +4,6 @@ from aiounifi.controller import MESSAGE_CLIENT_REMOVED, SIGNAL_CONNECTION_STATE from aiounifi.websocket import SIGNAL_DATA, STATE_DISCONNECTED, STATE_RUNNING -from asynctest import patch from homeassistant import config_entries from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN @@ -24,6 +23,8 @@ from .test_controller import ENTRY_CONFIG, setup_unifi_integration +from tests.async_mock import patch + CLIENT_1 = { "essid": "ssid", "hostname": "client_1", diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index db6c1e3074858e..80e3e07fa1735a 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -7,6 +7,7 @@ from .test_controller import setup_unifi_integration +from tests.async_mock import AsyncMock from tests.common import MockConfigEntry, mock_coro @@ -25,7 +26,7 @@ async def test_successful_config_entry(hass): async def test_controller_fail_setup(hass): """Test that a failed setup still stores controller.""" with patch("homeassistant.components.unifi.UniFiController") as mock_controller: - mock_controller.return_value.async_setup.return_value = mock_coro(False) + mock_controller.return_value.async_setup = AsyncMock(return_value=False) await setup_unifi_integration(hass) assert hass.data[UNIFI_DOMAIN] == {} @@ -55,7 +56,7 @@ async def test_controller_no_mac(hass): "homeassistant.helpers.device_registry.async_get_registry", return_value=mock_coro(mock_registry), ): - mock_controller.return_value.async_setup.return_value = mock_coro(True) + mock_controller.return_value.async_setup = AsyncMock(return_value=True) mock_controller.return_value.mac = None assert await unifi.async_setup_entry(hass, entry) is True diff --git a/tests/components/unifi_direct/test_device_tracker.py b/tests/components/unifi_direct/test_device_tracker.py index 692231d4cad350..be58efa415a463 100644 --- a/tests/components/unifi_direct/test_device_tracker.py +++ b/tests/components/unifi_direct/test_device_tracker.py @@ -2,7 +2,6 @@ from datetime import timedelta import os -from asynctest import mock, patch import pytest import voluptuous as vol @@ -23,6 +22,7 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME from homeassistant.setup import async_setup_component +from tests.async_mock import MagicMock, call, patch from tests.common import assert_setup_component, load_fixture, mock_component scanner_path = "homeassistant.components.unifi_direct.device_tracker.UnifiDeviceScanner" @@ -38,7 +38,7 @@ def setup_comp(hass): os.remove(yaml_devices) -@patch(scanner_path, return_value=mock.MagicMock(spec=UnifiDeviceScanner)) +@patch(scanner_path, return_value=MagicMock(spec=UnifiDeviceScanner)) async def test_get_scanner(unifi_mock, hass): """Test creating an Unifi direct scanner with a password.""" conf_dict = { @@ -57,7 +57,7 @@ async def test_get_scanner(unifi_mock, hass): assert await async_setup_component(hass, DOMAIN, conf_dict) conf_dict[DOMAIN][CONF_PORT] = 22 - assert unifi_mock.call_args == mock.call(conf_dict[DOMAIN]) + assert unifi_mock.call_args == call(conf_dict[DOMAIN]) @patch("pexpect.pxssh.pxssh") diff --git a/tests/components/updater/test_init.py b/tests/components/updater/test_init.py index e583f4e01147dd..95875828c714ee 100644 --- a/tests/components/updater/test_init.py +++ b/tests/components/updater/test_init.py @@ -1,14 +1,12 @@ """The tests for the Updater component.""" -from unittest.mock import Mock - -from asynctest import patch import pytest from homeassistant.components import updater from homeassistant.helpers.update_coordinator import UpdateFailed from homeassistant.setup import async_setup_component -from tests.common import MockDependency, mock_component, mock_coro +from tests.async_mock import patch +from tests.common import MockDependency, mock_component NEW_VERSION = "10000.0" MOCK_VERSION = "10.0" @@ -75,7 +73,7 @@ async def test_same_version_shows_entity_false( ): """Test if sensor is false if no new version is available.""" mock_get_uuid.return_value = MOCK_HUUID - mock_get_newest_version.return_value = mock_coro((MOCK_VERSION, "")) + mock_get_newest_version.return_value = (MOCK_VERSION, "") assert await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) @@ -92,7 +90,7 @@ async def test_same_version_shows_entity_false( async def test_disable_reporting(hass, mock_get_uuid, mock_get_newest_version): """Test we do not gather analytics when disable reporting is active.""" mock_get_uuid.return_value = MOCK_HUUID - mock_get_newest_version.return_value = mock_coro((MOCK_VERSION, "")) + mock_get_newest_version.return_value = (MOCK_VERSION, "") assert await async_setup_component( hass, updater.DOMAIN, {updater.DOMAIN: {"reporting": False}} @@ -123,7 +121,7 @@ async def test_get_newest_version_analytics_when_huuid(hass, aioclient_mock): with patch( "homeassistant.helpers.system_info.async_get_system_info", - Mock(return_value=mock_coro({"fake": "bla"})), + return_value={"fake": "bla"}, ): res = await updater.get_newest_version(hass, MOCK_HUUID, False) assert res == (MOCK_RESPONSE["version"], MOCK_RESPONSE["release-notes"]) @@ -135,7 +133,7 @@ async def test_error_fetching_new_version_bad_json(hass, aioclient_mock): with patch( "homeassistant.helpers.system_info.async_get_system_info", - Mock(return_value=mock_coro({"fake": "bla"})), + return_value={"fake": "bla"}, ), pytest.raises(UpdateFailed): await updater.get_newest_version(hass, MOCK_HUUID, False) @@ -152,7 +150,7 @@ async def test_error_fetching_new_version_invalid_response(hass, aioclient_mock) with patch( "homeassistant.helpers.system_info.async_get_system_info", - Mock(return_value=mock_coro({"fake": "bla"})), + return_value={"fake": "bla"}, ), pytest.raises(UpdateFailed): await updater.get_newest_version(hass, MOCK_HUUID, False) diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index 464ccc90675d5c..7c43c24cdc3e17 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -2,13 +2,12 @@ from ipaddress import IPv4Address -from asynctest import patch - from homeassistant.components import upnp from homeassistant.components.upnp.device import Device from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import MockConfigEntry, mock_coro @@ -86,8 +85,8 @@ async def test_async_setup_entry_default(hass): {"udn": udn, "ssdp_description": "http://192.168.1.1/desc.xml"} ] - create_device.return_value = mock_coro(return_value=mock_device) - async_discover.return_value = mock_coro(return_value=discovery_infos) + create_device.return_value = mock_device + async_discover.return_value = discovery_infos assert await upnp.async_setup_entry(hass, entry) is True @@ -128,8 +127,8 @@ async def test_async_setup_entry_port_mapping(hass): {"udn": udn, "ssdp_description": "http://192.168.1.1/desc.xml"} ] - create_device.return_value = mock_coro(return_value=mock_device) - async_discover.return_value = mock_coro(return_value=discovery_infos) + create_device.return_value = mock_device + async_discover.return_value = discovery_infos assert await upnp.async_setup_entry(hass, entry) is True diff --git a/tests/components/vera/test_init.py b/tests/components/vera/test_init.py index a6208726451ba0..f1e13a5f2088c7 100644 --- a/tests/components/vera/test_init.py +++ b/tests/components/vera/test_init.py @@ -1,5 +1,4 @@ """Vera tests.""" -from asynctest import MagicMock import pyvera as pv from requests.exceptions import RequestException @@ -9,6 +8,7 @@ from .common import ComponentFactory, new_simple_controller_config +from tests.async_mock import MagicMock from tests.common import MockConfigEntry diff --git a/tests/components/vilfo/test_config_flow.py b/tests/components/vilfo/test_config_flow.py index 9176a9a19701b0..d9e8a2ffd2466d 100644 --- a/tests/components/vilfo/test_config_flow.py +++ b/tests/components/vilfo/test_config_flow.py @@ -1,12 +1,11 @@ """Test the Vilfo Router config flow.""" -from asynctest.mock import patch import vilfo from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.vilfo.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_ID, CONF_MAC -from tests.common import mock_coro +from tests.async_mock import patch async def test_form(hass): @@ -22,7 +21,7 @@ async def test_form(hass): with patch("vilfo.Client.ping", return_value=None), patch( "vilfo.Client.get_board_information", return_value=None ), patch("vilfo.Client.resolve_mac_address", return_value=mock_mac), patch( - "homeassistant.components.vilfo.async_setup", return_value=mock_coro(True) + "homeassistant.components.vilfo.async_setup", return_value=True ) as mock_setup, patch( "homeassistant.components.vilfo.async_setup_entry" ) as mock_setup_entry: diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py index e630f201e121d8..f7448a71c3168a 100644 --- a/tests/components/vizio/conftest.py +++ b/tests/components/vizio/conftest.py @@ -1,5 +1,4 @@ """Configure py.test.""" -from asynctest import patch import pytest from pyvizio.const import DEVICE_CLASS_SPEAKER, MAX_VOLUME @@ -21,6 +20,8 @@ MockStartPairingResponse, ) +from tests.async_mock import patch + class MockInput: """Mock Vizio device input.""" diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index 1f6abf105637b2..aaf802af7ecca5 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -5,7 +5,6 @@ from typing import Any, Dict, List, Optional from unittest.mock import call -from asynctest import patch import pytest from pytest import raises from pyvizio.api.apps import AppConfig @@ -74,6 +73,7 @@ VOLUME_STEP, ) +from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index 9ba35a510dd9b0..cc889eca064c84 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -27,7 +27,7 @@ if sys.version_info >= (3, 8, 0): from unittest.mock import patch else: - from asynctest import patch + from tests.async_mock import patch NAME = "fake" diff --git a/tests/components/websocket_api/test_http.py b/tests/components/websocket_api/test_http.py index 9082337ccc8419..e76cbe0dbdc58c 100644 --- a/tests/components/websocket_api/test_http.py +++ b/tests/components/websocket_api/test_http.py @@ -2,12 +2,12 @@ from datetime import timedelta from aiohttp import WSMsgType -from asynctest import patch import pytest from homeassistant.components.websocket_api import const, http from homeassistant.util.dt import utcnow +from tests.async_mock import patch from tests.common import async_fire_time_changed diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index 65ff65ebbd8cb8..f0f3a16033248c 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -2,7 +2,6 @@ from datetime import timedelta from unittest.mock import patch -from asynctest import MagicMock import pytest from withings_api import WithingsApi from withings_api.common import TimeoutException, UnauthorizedException @@ -14,6 +13,8 @@ from homeassistant.exceptions import PlatformNotReady from homeassistant.util import dt +from tests.async_mock import MagicMock + @pytest.fixture(name="withings_api") def withings_api_fixture() -> WithingsApi: diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index f6f36ba8eff28c..5bb3e8534c353f 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -2,7 +2,6 @@ import re import time -from asynctest import MagicMock import requests_mock import voluptuous as vol from withings_api import AbstractWithingsApi @@ -32,6 +31,8 @@ setup_hass, ) +from tests.async_mock import MagicMock + def config_schema_validate(withings_config) -> None: """Assert a schema config succeeds.""" diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index 0009677cf188e5..f2cc5514a2e718 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -1,6 +1,5 @@ """Tests for the WLED light platform.""" import aiohttp -from asynctest.mock import patch from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -32,6 +31,7 @@ ) from homeassistant.core import HomeAssistant +from tests.async_mock import patch from tests.components.wled import init_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index d77bd99b97c55e..66fedfdd274f36 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -1,7 +1,6 @@ """Tests for the WLED sensor platform.""" from datetime import datetime -from asynctest import patch import pytest from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -21,6 +20,7 @@ from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util +from tests.async_mock import patch from tests.components.wled import init_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/wled/test_switch.py b/tests/components/wled/test_switch.py index d140953b94802b..7d411984cff192 100644 --- a/tests/components/wled/test_switch.py +++ b/tests/components/wled/test_switch.py @@ -1,6 +1,5 @@ """Tests for the WLED switch platform.""" import aiohttp -from asynctest.mock import patch from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.wled.const import ( @@ -20,6 +19,7 @@ ) from homeassistant.core import HomeAssistant +from tests.async_mock import patch from tests.components.wled import init_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/wwlln/test_config_flow.py b/tests/components/wwlln/test_config_flow.py index e9e32ae75e1e4b..b5d34f542e32da 100644 --- a/tests/components/wwlln/test_config_flow.py +++ b/tests/components/wwlln/test_config_flow.py @@ -1,6 +1,4 @@ """Define tests for the WWLLN config flow.""" -from asynctest import patch - from homeassistant import data_entry_flow from homeassistant.components.wwlln import ( CONF_WINDOW, @@ -11,6 +9,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/xiaomi/test_device_tracker.py b/tests/components/xiaomi/test_device_tracker.py index 5d65aee761654a..1760b3274a44f2 100644 --- a/tests/components/xiaomi/test_device_tracker.py +++ b/tests/components/xiaomi/test_device_tracker.py @@ -1,7 +1,6 @@ """The tests for the Xiaomi router device tracker platform.""" import logging -from asynctest import mock, patch import requests from homeassistant.components.device_tracker import DOMAIN @@ -9,6 +8,8 @@ from homeassistant.components.xiaomi.device_tracker import get_scanner from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME +from tests.async_mock import MagicMock, call, patch + _LOGGER = logging.getLogger(__name__) INVALID_USERNAME = "bob" @@ -144,7 +145,7 @@ def raise_for_status(self): @patch( "homeassistant.components.xiaomi.device_tracker.XiaomiDeviceScanner", - return_value=mock.MagicMock(), + return_value=MagicMock(), ) async def test_config(xiaomi_mock, hass): """Testing minimal configuration.""" @@ -159,7 +160,7 @@ async def test_config(xiaomi_mock, hass): } xiaomi.get_scanner(hass, config) assert xiaomi_mock.call_count == 1 - assert xiaomi_mock.call_args == mock.call(config[DOMAIN]) + assert xiaomi_mock.call_args == call(config[DOMAIN]) call_arg = xiaomi_mock.call_args[0][0] assert call_arg["username"] == "admin" assert call_arg["password"] == "passwordTest" @@ -169,7 +170,7 @@ async def test_config(xiaomi_mock, hass): @patch( "homeassistant.components.xiaomi.device_tracker.XiaomiDeviceScanner", - return_value=mock.MagicMock(), + return_value=MagicMock(), ) async def test_config_full(xiaomi_mock, hass): """Testing full configuration.""" @@ -185,7 +186,7 @@ async def test_config_full(xiaomi_mock, hass): } xiaomi.get_scanner(hass, config) assert xiaomi_mock.call_count == 1 - assert xiaomi_mock.call_args == mock.call(config[DOMAIN]) + assert xiaomi_mock.call_args == call(config[DOMAIN]) call_arg = xiaomi_mock.call_args[0][0] assert call_arg["username"] == "alternativeAdminName" assert call_arg["password"] == "passwordTest" diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index 2c1411ded68d46..023deb93c81c03 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -1,13 +1,14 @@ """Test the Xiaomi Miio config flow.""" from unittest.mock import Mock -from asynctest import patch from miio import DeviceException from homeassistant import config_entries from homeassistant.components.xiaomi_miio import config_flow, const from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN +from tests.async_mock import patch + TEST_HOST = "1.2.3.4" TEST_TOKEN = "12345678901234567890123456789012" TEST_NAME = "Test_Gateway" diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 27824623d23d44..a8e22e74bc4103 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -1,5 +1,4 @@ """Test Zeroconf component setup process.""" -from asynctest import patch import pytest from zeroconf import ServiceInfo, ServiceStateChange @@ -7,6 +6,8 @@ from homeassistant.generated import zeroconf as zc_gen from homeassistant.setup import async_setup_component +from tests.async_mock import patch + NON_UTF8_VALUE = b"ABCDEF\x8a" NON_ASCII_KEY = b"non-ascii-key\x8a" PROPERTIES = { diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 2542037a2bf19c..9b139317825585 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -2,7 +2,6 @@ import time from unittest.mock import Mock -from asynctest import CoroutineMock from zigpy.device import Device as zigpy_dev from zigpy.endpoint import Endpoint as zigpy_ep import zigpy.profiles.zha @@ -15,6 +14,8 @@ import homeassistant.components.zha.core.const as zha_const from homeassistant.util import slugify +from tests.async_mock import AsyncMock + class FakeEndpoint: """Fake endpoint for moking zigpy.""" @@ -32,7 +33,7 @@ def __init__(self, manufacturer, model, epid=1): self.model = model self.profile_id = zigpy.profiles.zha.PROFILE_ID self.device_type = None - self.request = CoroutineMock() + self.request = AsyncMock() def add_input_cluster(self, cluster_id): """Add an input cluster.""" @@ -64,16 +65,16 @@ def unique_id(self): def patch_cluster(cluster): """Patch a cluster for testing.""" - cluster.bind = CoroutineMock(return_value=[0]) - cluster.configure_reporting = CoroutineMock(return_value=[0]) + cluster.bind = AsyncMock(return_value=[0]) + cluster.configure_reporting = AsyncMock(return_value=[0]) cluster.deserialize = Mock() cluster.handle_cluster_request = Mock() - cluster.read_attributes = CoroutineMock(return_value=[{}, {}]) + cluster.read_attributes = AsyncMock(return_value=[{}, {}]) cluster.read_attributes_raw = Mock() - cluster.unbind = CoroutineMock(return_value=[0]) - cluster.write_attributes = CoroutineMock(return_value=[0]) + cluster.unbind = AsyncMock(return_value=[0]) + cluster.write_attributes = AsyncMock(return_value=[0]) if cluster.cluster_id == 4: - cluster.add = CoroutineMock(return_value=[0]) + cluster.add = AsyncMock(return_value=[0]) class FakeDevice: @@ -96,7 +97,7 @@ def __init__(self, app, ieee, manufacturer, model, node_desc=None, nwk=0xB79C): self.manufacturer = manufacturer self.model = model self.node_desc = zigpy.zdo.types.NodeDescriptor() - self.remove_from_group = CoroutineMock() + self.remove_from_group = AsyncMock() if node_desc is None: node_desc = b"\x02@\x807\x10\x7fd\x00\x00*d\x00\x00" self.node_desc = zigpy.zdo.types.NodeDescriptor.deserialize(node_desc)[0] diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index b83db53533c1d5..e737f990163516 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -1,7 +1,6 @@ """Test configuration for the ZHA component.""" from unittest import mock -import asynctest import pytest import zigpy from zigpy.application import ControllerApplication @@ -15,6 +14,7 @@ from .common import FakeDevice, FakeEndpoint, get_zha_gateway +import tests.async_mock from tests.common import MockConfigEntry FIXTURE_GRP_ID = 0x1001 @@ -25,8 +25,8 @@ def zigpy_app_controller(): """Zigpy ApplicationController fixture.""" app = mock.MagicMock(spec_set=ControllerApplication) - app.startup = asynctest.CoroutineMock() - app.shutdown = asynctest.CoroutineMock() + app.startup = tests.async_mock.AsyncMock() + app.shutdown = tests.async_mock.AsyncMock() groups = zigpy.group.Groups(app) groups.add_group(FIXTURE_GRP_ID, FIXTURE_GRP_NAME, suppress_event=True) app.configure_mock(groups=groups) @@ -41,7 +41,7 @@ def zigpy_app_controller(): def zigpy_radio(): """Zigpy radio mock.""" radio = mock.MagicMock() - radio.connect = asynctest.CoroutineMock() + radio.connect = tests.async_mock.AsyncMock() return radio @@ -93,8 +93,8 @@ def channel(name: str, cluster_id: int, endpoint_id: int = 1): ch.name = name ch.generic_id = f"channel_0x{cluster_id:04x}" ch.id = f"{endpoint_id}:0x{cluster_id:04x}" - ch.async_configure = asynctest.CoroutineMock() - ch.async_initialize = asynctest.CoroutineMock() + ch.async_configure = tests.async_mock.AsyncMock() + ch.async_initialize = tests.async_mock.AsyncMock() return ch return channel diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index 1196cdc3b409aa..471e44f8409b1b 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -2,7 +2,6 @@ import asyncio from unittest import mock -import asynctest import pytest import zigpy.types as t import zigpy.zcl.clusters @@ -14,6 +13,8 @@ from .common import get_zha_gateway, make_zcl_header +import tests.async_mock + @pytest.fixture def ieee(): @@ -379,12 +380,12 @@ async def test_ep_channels_configure(channel): ch_1 = channel(zha_const.CHANNEL_ON_OFF, 6) ch_2 = channel(zha_const.CHANNEL_LEVEL, 8) ch_3 = channel(zha_const.CHANNEL_COLOR, 768) - ch_3.async_configure = asynctest.CoroutineMock(side_effect=asyncio.TimeoutError) - ch_3.async_initialize = asynctest.CoroutineMock(side_effect=asyncio.TimeoutError) + ch_3.async_configure = tests.async_mock.AsyncMock(side_effect=asyncio.TimeoutError) + ch_3.async_initialize = tests.async_mock.AsyncMock(side_effect=asyncio.TimeoutError) ch_4 = channel(zha_const.CHANNEL_ON_OFF, 6) ch_5 = channel(zha_const.CHANNEL_LEVEL, 8) - ch_5.async_configure = asynctest.CoroutineMock(side_effect=asyncio.TimeoutError) - ch_5.async_initialize = asynctest.CoroutineMock(side_effect=asyncio.TimeoutError) + ch_5.async_configure = tests.async_mock.AsyncMock(side_effect=asyncio.TimeoutError) + ch_5.async_initialize = tests.async_mock.AsyncMock(side_effect=asyncio.TimeoutError) channels = mock.MagicMock(spec_set=zha_channels.Channels) type(channels).semaphore = mock.PropertyMock(return_value=asyncio.Semaphore(3)) @@ -420,8 +421,8 @@ async def test_poll_control_configure(poll_control_ch): async def test_poll_control_checkin_response(poll_control_ch): """Test poll control channel checkin response.""" - rsp_mock = asynctest.CoroutineMock() - set_interval_mock = asynctest.CoroutineMock() + rsp_mock = tests.async_mock.AsyncMock() + set_interval_mock = tests.async_mock.AsyncMock() cluster = poll_control_ch.cluster patch_1 = mock.patch.object(cluster, "checkin_response", rsp_mock) patch_2 = mock.patch.object(cluster, "set_long_poll_interval", set_interval_mock) @@ -442,7 +443,7 @@ async def test_poll_control_checkin_response(poll_control_ch): async def test_poll_control_cluster_command(hass, poll_control_device): """Test poll control channel response to cluster command.""" - checkin_mock = asynctest.CoroutineMock() + checkin_mock = tests.async_mock.AsyncMock() poll_control_ch = poll_control_device.channels.pools[0].all_channels["1:0x0020"] cluster = poll_control_ch.cluster diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 7e0b89f8d70cda..91d2ef75aa5349 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -1,12 +1,11 @@ """Tests for ZHA config flow.""" from unittest import mock -import asynctest - from homeassistant.components.zha import config_flow from homeassistant.components.zha.core.const import CONTROLLER, DOMAIN, ZHA_GW_RADIO import homeassistant.components.zha.core.registries +import tests.async_mock from tests.common import MockConfigEntry @@ -15,7 +14,7 @@ async def test_user_flow(hass): flow = config_flow.ZhaFlowHandler() flow.hass = hass - with asynctest.patch( + with tests.async_mock.patch( "homeassistant.components.zha.config_flow.check_zigpy_connection", return_value=False, ): @@ -25,7 +24,7 @@ async def test_user_flow(hass): assert result["errors"] == {"base": "cannot_connect"} - with asynctest.patch( + with tests.async_mock.patch( "homeassistant.components.zha.config_flow.check_zigpy_connection", return_value=True, ): @@ -79,18 +78,18 @@ async def test_import_flow_existing_config_entry(hass): async def test_check_zigpy_connection(): """Test config flow validator.""" - mock_radio = asynctest.MagicMock() - mock_radio.connect = asynctest.CoroutineMock() - radio_cls = asynctest.MagicMock(return_value=mock_radio) + mock_radio = tests.async_mock.MagicMock() + mock_radio.connect = tests.async_mock.AsyncMock() + radio_cls = tests.async_mock.MagicMock(return_value=mock_radio) - bad_radio = asynctest.MagicMock() - bad_radio.connect = asynctest.CoroutineMock(side_effect=Exception) - bad_radio_cls = asynctest.MagicMock(return_value=bad_radio) + bad_radio = tests.async_mock.MagicMock() + bad_radio.connect = tests.async_mock.AsyncMock(side_effect=Exception) + bad_radio_cls = tests.async_mock.MagicMock(return_value=bad_radio) - mock_ctrl = asynctest.MagicMock() - mock_ctrl.startup = asynctest.CoroutineMock() - mock_ctrl.shutdown = asynctest.CoroutineMock() - ctrl_cls = asynctest.MagicMock(return_value=mock_ctrl) + mock_ctrl = tests.async_mock.MagicMock() + mock_ctrl.startup = tests.async_mock.AsyncMock() + mock_ctrl.shutdown = tests.async_mock.AsyncMock() + ctrl_cls = tests.async_mock.MagicMock(return_value=mock_ctrl) new_radios = { mock.sentinel.radio: {ZHA_GW_RADIO: radio_cls, CONTROLLER: ctrl_cls}, mock.sentinel.bad_radio: {ZHA_GW_RADIO: bad_radio_cls, CONTROLLER: ctrl_cls}, diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index 188ddf69a2314b..b4c72fd82d47f4 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -1,7 +1,4 @@ """Test zha cover.""" -from unittest.mock import MagicMock, call, patch - -import asynctest import pytest import zigpy.types import zigpy.zcl.clusters.closures as closures @@ -17,6 +14,7 @@ send_attributes_report, ) +from tests.async_mock import MagicMock, call, patch from tests.common import mock_coro @@ -34,7 +32,7 @@ def zigpy_cover_device(zigpy_device_mock): return zigpy_device_mock(endpoints) -@asynctest.patch( +@patch( "homeassistant.components.zha.core.channels.closures.WindowCovering.async_initialize" ) async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py index c92f574825d584..6e52829991145b 100644 --- a/tests/components/zha/test_device.py +++ b/tests/components/zha/test_device.py @@ -3,7 +3,6 @@ import time from unittest import mock -import asynctest import pytest import zigpy.zcl.clusters.general as general @@ -13,6 +12,7 @@ from .common import async_enable_traffic, make_zcl_header +from tests.async_mock import patch from tests.common import async_fire_time_changed @@ -90,7 +90,7 @@ def _send_time_changed(hass, seconds): async_fire_time_changed(hass, now) -@asynctest.patch( +@patch( "homeassistant.components.zha.core.channels.general.BasicChannel.async_initialize", new=mock.MagicMock(), ) @@ -144,7 +144,7 @@ def _update_last_seen(*args, **kwargs): assert zha_device.available is True -@asynctest.patch( +@patch( "homeassistant.components.zha.core.channels.general.BasicChannel.async_initialize", new=mock.MagicMock(), ) @@ -187,7 +187,7 @@ async def test_check_available_unsuccessful( assert zha_device.available is False -@asynctest.patch( +@patch( "homeassistant.components.zha.core.channels.general.BasicChannel.async_initialize", new=mock.MagicMock(), ) diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index 8b1ce502a7892b..4b95040dd08045 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -3,7 +3,6 @@ import re from unittest import mock -import asynctest import pytest import zigpy.quirks import zigpy.types @@ -30,6 +29,8 @@ from .common import get_zha_gateway from .zha_devices_list import DEVICES +from tests.async_mock import AsyncMock, patch + NO_TAIL_ID = re.compile("_\\d$") @@ -51,11 +52,9 @@ def _mock( return _mock -@asynctest.patch( +@patch( "zigpy.zcl.clusters.general.Identify.request", - new=asynctest.CoroutineMock( - return_value=[mock.sentinel.data, zcl_f.Status.SUCCESS] - ), + new=AsyncMock(return_value=[mock.sentinel.data, zcl_f.Status.SUCCESS]), ) @pytest.mark.parametrize("device", DEVICES) async def test_devices( diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 03c29283c20aaf..915ace7c7f2b59 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -2,7 +2,6 @@ from datetime import timedelta from unittest.mock import MagicMock, call, sentinel -from asynctest import CoroutineMock, patch import pytest import zigpy.profiles.zha as zha import zigpy.types @@ -24,6 +23,7 @@ send_attributes_report, ) +from tests.async_mock import AsyncMock, patch from tests.common import async_fire_time_changed ON = 1 @@ -206,19 +206,19 @@ async def test_light_refresh(hass, zigpy_device_mock, zha_device_joined_restored @patch( "zigpy.zcl.clusters.lighting.Color.request", - new=CoroutineMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), ) @patch( "zigpy.zcl.clusters.general.Identify.request", - new=CoroutineMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), ) @patch( "zigpy.zcl.clusters.general.LevelControl.request", - new=CoroutineMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), ) @patch( "zigpy.zcl.clusters.general.OnOff.request", - new=CoroutineMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), ) @pytest.mark.parametrize( "device, reporting", diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py index bf92a6aa12a9dd..994bd5e6dda20b 100644 --- a/tests/components/zone/test_init.py +++ b/tests/components/zone/test_init.py @@ -1,5 +1,4 @@ """Test zone component.""" -from asynctest import patch import pytest from homeassistant import setup @@ -16,6 +15,7 @@ from homeassistant.exceptions import Unauthorized from homeassistant.helpers import entity_registry +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index e14162d5788214..b71758469d0feb 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -23,12 +23,8 @@ from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.setup import setup_component -from tests.common import ( - async_fire_time_changed, - get_test_home_assistant, - mock_coro, - mock_registry, -) +from tests.async_mock import AsyncMock +from tests.common import async_fire_time_changed, get_test_home_assistant, mock_registry from tests.mock.zwave import MockEntityValues, MockNetwork, MockNode, MockValue @@ -873,7 +869,7 @@ def tearDown(self): # pylint: disable=invalid-name @patch.object(zwave, "discovery") def test_entity_discovery(self, discovery, import_module): """Test the creation of a new entity.""" - discovery.async_load_platform.return_value = mock_coro() + discovery.async_load_platform = AsyncMock(return_value=None) mock_platform = MagicMock() import_module.return_value = mock_platform mock_device = MagicMock() @@ -935,7 +931,7 @@ def test_entity_discovery(self, discovery, import_module): @patch.object(zwave, "discovery") def test_entity_existing_values(self, discovery, import_module): """Test the loading of already discovered values.""" - discovery.async_load_platform.return_value = mock_coro() + discovery.async_load_platform = AsyncMock(return_value=None) mock_platform = MagicMock() import_module.return_value = mock_platform mock_device = MagicMock() @@ -1003,7 +999,7 @@ def test_node_schema_mismatch(self, discovery, import_module): @patch.object(zwave, "discovery") def test_entity_workaround_component(self, discovery, import_module): """Test component workaround.""" - discovery.async_load_platform.return_value = mock_coro() + discovery.async_load_platform = AsyncMock(return_value=None) mock_platform = MagicMock() import_module.return_value = mock_platform mock_device = MagicMock() diff --git a/tests/conftest.py b/tests/conftest.py index ccc16c8ef823fd..c7885c6125e8e3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,6 @@ import functools import logging -from asynctest import patch import pytest import requests_mock as _requests_mock @@ -19,6 +18,8 @@ from homeassistant.setup import async_setup_component from homeassistant.util import location +from tests.async_mock import patch + pytest.register_assert_rewrite("tests.common") from tests.common import ( # noqa: E402, isort:skip diff --git a/tests/helpers/test_area_registry.py b/tests/helpers/test_area_registry.py index 87e4b4c4d031a4..f6a75fe3c30da9 100644 --- a/tests/helpers/test_area_registry.py +++ b/tests/helpers/test_area_registry.py @@ -1,12 +1,12 @@ """Tests for the Area Registry.""" import asyncio -import asynctest import pytest from homeassistant.core import callback from homeassistant.helpers import area_registry +import tests.async_mock from tests.common import flush_store, mock_area_registry @@ -166,7 +166,7 @@ async def test_loading_area_from_storage(hass, hass_storage): async def test_loading_race_condition(hass): """Test only one storage load called when concurrent loading occurred .""" - with asynctest.patch( + with tests.async_mock.patch( "homeassistant.helpers.area_registry.AreaRegistry.async_load" ) as mock_load: results = await asyncio.gather( diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index 868ff082ec3c4e..1331cf615d1a75 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -1,10 +1,10 @@ """Tests for the Config Entry Flow helper.""" -from asynctest import Mock, patch import pytest from homeassistant import config_entries, data_entry_flow, setup from homeassistant.helpers import config_entry_flow +from tests.async_mock import Mock, patch from tests.common import ( MockConfigEntry, MockModule, diff --git a/tests/helpers/test_debounce.py b/tests/helpers/test_debounce.py index d51eb22c90d4c9..d84b6fd7da75e7 100644 --- a/tests/helpers/test_debounce.py +++ b/tests/helpers/test_debounce.py @@ -1,8 +1,8 @@ """Tests for debounce.""" -from asynctest import CoroutineMock - from homeassistant.helpers import debounce +from tests.async_mock import AsyncMock + async def test_immediate_works(hass): """Test immediate works.""" @@ -12,7 +12,7 @@ async def test_immediate_works(hass): None, cooldown=0.01, immediate=True, - function=CoroutineMock(side_effect=lambda: calls.append(None)), + function=AsyncMock(side_effect=lambda: calls.append(None)), ) # Call when nothing happening @@ -57,7 +57,7 @@ async def test_not_immediate_works(hass): None, cooldown=0.01, immediate=False, - function=CoroutineMock(side_effect=lambda: calls.append(None)), + function=AsyncMock(side_effect=lambda: calls.append(None)), ) # Call when nothing happening diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index d5f762fc7010b6..f81d20ecd662a9 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -2,12 +2,12 @@ import asyncio from unittest.mock import patch -import asynctest import pytest from homeassistant.core import callback from homeassistant.helpers import device_registry +import tests.async_mock from tests.common import flush_store, mock_device_registry @@ -474,7 +474,7 @@ async def test_update_remove_config_entries(hass, registry, update_events): async def test_loading_race_condition(hass): """Test only one storage load called when concurrent loading occurred .""" - with asynctest.patch( + with tests.async_mock.patch( "homeassistant.helpers.device_registry.DeviceRegistry.async_load" ) as mock_load: results = await asyncio.gather( diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 1c5224d89c3e3b..625f21d9d9fc5d 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -3,9 +3,7 @@ from collections import OrderedDict from datetime import timedelta import logging -from unittest.mock import Mock, patch -import asynctest import pytest import voluptuous as vol @@ -17,13 +15,13 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import AsyncMock, Mock, patch from tests.common import ( MockConfigEntry, MockEntity, MockModule, MockPlatform, async_fire_time_changed, - mock_coro, mock_entity_platform, mock_integration, ) @@ -82,13 +80,8 @@ async def test_setup_recovers_when_setup_raises(hass): assert platform2_setup.called -@asynctest.patch( - "homeassistant.helpers.entity_component.EntityComponent.async_setup_platform", - return_value=mock_coro(), -) -@asynctest.patch( - "homeassistant.setup.async_setup_component", return_value=mock_coro(True) -) +@patch("homeassistant.helpers.entity_component.EntityComponent.async_setup_platform",) +@patch("homeassistant.setup.async_setup_component", return_value=True) async def test_setup_does_discovery(mock_setup_component, mock_setup, hass): """Test setup for discovery.""" component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -105,7 +98,7 @@ async def test_setup_does_discovery(mock_setup_component, mock_setup, hass): assert ("platform_test", {}, {"msg": "discovery_info"}) == mock_setup.call_args[0] -@asynctest.patch("homeassistant.helpers.entity_platform.async_track_time_interval") +@patch("homeassistant.helpers.entity_platform.async_track_time_interval") async def test_set_scan_interval_via_config(mock_track, hass): """Test the setting of the scan interval via configuration.""" @@ -295,7 +288,7 @@ async def test_setup_dependencies_platform(hass): async def test_setup_entry(hass): """Test setup entry calls async_setup_entry on platform.""" - mock_setup_entry = Mock(return_value=mock_coro(True)) + mock_setup_entry = AsyncMock(return_value=True) mock_entity_platform( hass, "test_domain.entry_domain", @@ -326,7 +319,7 @@ async def test_setup_entry_platform_not_exist(hass): async def test_setup_entry_fails_duplicate(hass): """Test we don't allow setting up a config entry twice.""" - mock_setup_entry = Mock(return_value=mock_coro(True)) + mock_setup_entry = AsyncMock(return_value=True) mock_entity_platform( hass, "test_domain.entry_domain", @@ -344,7 +337,7 @@ async def test_setup_entry_fails_duplicate(hass): async def test_unload_entry_resets_platform(hass): """Test unloading an entry removes all entities.""" - mock_setup_entry = Mock(return_value=mock_coro(True)) + mock_setup_entry = AsyncMock(return_value=True) mock_entity_platform( hass, "test_domain.entry_domain", @@ -380,7 +373,7 @@ async def test_update_entity(hass): component = EntityComponent(_LOGGER, DOMAIN, hass) entity = MockEntity() entity.async_write_ha_state = Mock() - entity.async_update_ha_state = Mock(return_value=mock_coro()) + entity.async_update_ha_state = AsyncMock(return_value=None) await component.async_add_entities([entity]) # Called as part of async_add_entities diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index df247d82d5c2b6..3f03dfece11b4c 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -2,9 +2,7 @@ import asyncio from datetime import timedelta import logging -from unittest.mock import MagicMock, Mock, patch -import asynctest import pytest from homeassistant.const import UNIT_PERCENTAGE @@ -18,6 +16,7 @@ ) import homeassistant.util.dt as dt_util +from tests.async_mock import Mock, patch from tests.common import ( MockConfigEntry, MockEntity, @@ -136,7 +135,7 @@ async def test_update_state_adds_entities_with_update_before_add_false(hass): assert not ent.update.called -@asynctest.patch("homeassistant.helpers.entity_platform.async_track_time_interval") +@patch("homeassistant.helpers.entity_platform.async_track_time_interval") async def test_set_scan_interval_via_platform(mock_track, hass): """Test the setting of the scan interval via platform.""" @@ -183,7 +182,7 @@ async def test_platform_warn_slow_setup(hass): component = EntityComponent(_LOGGER, DOMAIN, hass) - with patch.object(hass.loop, "call_later", MagicMock()) as mock_call: + with patch.object(hass.loop, "call_later") as mock_call: await component.async_setup({DOMAIN: {"platform": "platform"}}) assert mock_call.called diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index cda2f1245fb347..b2bfd1f48ffacd 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -2,13 +2,13 @@ import asyncio from unittest.mock import patch -import asynctest import pytest from homeassistant.const import EVENT_HOMEASSISTANT_START, STATE_UNAVAILABLE from homeassistant.core import CoreState, callback, valid_entity_id from homeassistant.helpers import entity_registry +import tests.async_mock from tests.common import MockConfigEntry, flush_store, mock_registry YAML__OPEN_PATH = "homeassistant.util.yaml.loader.open" @@ -401,7 +401,7 @@ async def test_loading_invalid_entity_id(hass, hass_storage): async def test_loading_race_condition(hass): """Test only one storage load called when concurrent loading occurred .""" - with asynctest.patch( + with tests.async_mock.patch( "homeassistant.helpers.entity_registry.EntityRegistry.async_load" ) as mock_load: results = await asyncio.gather( diff --git a/tests/helpers/test_restore_state.py b/tests/helpers/test_restore_state.py index da2acee6625b4e..7866662266d0e4 100644 --- a/tests/helpers/test_restore_state.py +++ b/tests/helpers/test_restore_state.py @@ -1,8 +1,6 @@ """The tests for the Restore component.""" from datetime import datetime -from asynctest import patch - from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.core import CoreState, State from homeassistant.exceptions import HomeAssistantError @@ -16,7 +14,7 @@ ) from homeassistant.util import dt as dt_util -from tests.common import mock_coro +from tests.async_mock import patch async def test_caching_data(hass): @@ -193,7 +191,7 @@ async def test_dump_error(hass): with patch( "homeassistant.helpers.restore_state.Store.async_save", - return_value=mock_coro(exception=HomeAssistantError), + side_effect=HomeAssistantError, ) as mock_write_data, patch.object(hass.states, "async_all", return_value=states): await data.async_dump_states() @@ -208,7 +206,7 @@ async def test_load_error(hass): with patch( "homeassistant.helpers.storage.Store.async_load", - return_value=mock_coro(exception=HomeAssistantError), + side_effect=HomeAssistantError, ): state = await entity.async_get_last_state() diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 9d7e7751c108d6..c00dadc27e888f 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -6,7 +6,6 @@ import logging from unittest import mock -import asynctest import pytest import voluptuous as vol @@ -19,6 +18,7 @@ from homeassistant.helpers.event import async_call_later import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import ( async_capture_events, async_fire_time_changed, @@ -776,7 +776,7 @@ async def test_condition_basic(hass, script_mode): @pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) -@asynctest.patch("homeassistant.helpers.script.condition.async_from_config") +@patch("homeassistant.helpers.script.condition.async_from_config") async def test_condition_created_once(async_from_config, hass, script_mode): """Test that the conditions do not get created multiple times.""" sequence = cv.SCRIPT_SCHEMA( diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 8819684e8d6ec1..e87fd2646ddda3 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -3,7 +3,6 @@ from copy import deepcopy import unittest -from asynctest import CoroutineMock, Mock, patch import pytest import voluptuous as vol @@ -27,6 +26,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.setup import async_setup_component +from tests.async_mock import AsyncMock, Mock, patch from tests.common import ( MockEntity, get_test_home_assistant, @@ -39,7 +39,9 @@ @pytest.fixture def mock_handle_entity_call(): """Mock service platform call.""" - with patch("homeassistant.helpers.service._handle_entity_call") as mock_call: + with patch( + "homeassistant.helpers.service._handle_entity_call", return_value=None, + ) as mock_call: yield mock_call @@ -306,7 +308,7 @@ async def test_async_get_all_descriptions(hass): async def test_call_with_required_features(hass, mock_entities): """Test service calls invoked only if entity has required feautres.""" - test_service_mock = CoroutineMock(return_value=None) + test_service_mock = AsyncMock(return_value=None) await service.entity_service_call( hass, [Mock(entities=mock_entities)], @@ -321,7 +323,7 @@ async def test_call_with_required_features(hass, mock_entities): async def test_call_with_sync_func(hass, mock_entities): """Test invoking sync service calls.""" - test_service_mock = Mock() + test_service_mock = Mock(return_value=None) await service.entity_service_call( hass, [Mock(entities=mock_entities)], @@ -333,7 +335,7 @@ async def test_call_with_sync_func(hass, mock_entities): async def test_call_with_sync_attr(hass, mock_entities): """Test invoking sync service calls.""" - mock_method = mock_entities["light.kitchen"].sync_method = Mock() + mock_method = mock_entities["light.kitchen"].sync_method = Mock(return_value=None) await service.entity_service_call( hass, [Mock(entities=mock_entities)], diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 1966430113c381..6325294033f4de 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -3,7 +3,6 @@ from datetime import timedelta import json -from asynctest import Mock, patch import pytest from homeassistant.const import ( @@ -14,6 +13,7 @@ from homeassistant.helpers import storage from homeassistant.util import dt +from tests.async_mock import Mock, patch from tests.common import async_fire_time_changed MOCK_VERSION = 1 diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index 3957d2c19eec21..8596b9dd7f3988 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -3,14 +3,15 @@ from os import path import pathlib -from asynctest import Mock, patch import pytest from homeassistant.const import EVENT_COMPONENT_LOADED from homeassistant.generated import config_flows from homeassistant.helpers import translation from homeassistant.loader import async_get_integration -from homeassistant.setup import async_setup_component +from homeassistant.setup import async_setup_component, setup_component + +from tests.async_mock import Mock, patch @pytest.fixture @@ -142,11 +143,11 @@ async def test_get_translations_loads_config_flows(hass, mock_config_flows): integration = Mock(file_path=pathlib.Path(__file__)) integration.name = "Component 1" - with patch.object( - translation, "component_translation_path", return_value="bla.json" - ), patch.object( - translation, - "load_translations_files", + with patch( + "homeassistant.helpers.translation.component_translation_path", + return_value="bla.json", + ), patch( + "homeassistant.helpers.translation.load_translations_files", return_value={"component1": {"hello": "world"}}, ), patch( "homeassistant.helpers.translation.async_get_integration", @@ -169,16 +170,18 @@ async def test_get_translations_while_loading_components(hass): integration.name = "Component 1" hass.config.components.add("component1") - async def mock_load_translation_files(files): + def mock_load_translation_files(files): """Mock load translation files.""" # Mimic race condition by loading a component during setup - await async_setup_component(hass, "persistent_notification", {}) + setup_component(hass, "persistent_notification", {}) return {"component1": {"hello": "world"}} - with patch.object( - translation, "component_translation_path", return_value="bla.json" - ), patch.object( - translation, "load_translations_files", side_effect=mock_load_translation_files, + with patch( + "homeassistant.helpers.translation.component_translation_path", + return_value="bla.json", + ), patch( + "homeassistant.helpers.translation.load_translations_files", + mock_load_translation_files, ), patch( "homeassistant.helpers.translation.async_get_integration", return_value=integration, @@ -229,9 +232,8 @@ def mock_load_translations_files(files): result["sensor.season"] = {"state": "bad data"} return result - with patch.object( - translation, - "load_translations_files", + with patch( + "homeassistant.helpers.translation.load_translations_files", side_effect=mock_load_translations_files, ): translations = await translation.async_get_translations(hass, "en", "state") diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 897367619ed41a..8d4f6934d78bab 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -4,12 +4,12 @@ import logging import aiohttp -from asynctest import CoroutineMock, Mock import pytest from homeassistant.helpers import update_coordinator from homeassistant.util.dt import utcnow +from tests.async_mock import AsyncMock, Mock from tests.common import async_fire_time_changed LOGGER = logging.getLogger(__name__) @@ -89,7 +89,7 @@ async def test_request_refresh(crd): ) async def test_refresh_known_errors(err_msg, crd, caplog): """Test raising known errors.""" - crd.update_method = CoroutineMock(side_effect=err_msg[0]) + crd.update_method = AsyncMock(side_effect=err_msg[0]) await crd.async_refresh() @@ -102,7 +102,7 @@ async def test_refresh_fail_unknown(crd, caplog): """Test raising unknown error.""" await crd.async_refresh() - crd.update_method = CoroutineMock(side_effect=ValueError) + crd.update_method = AsyncMock(side_effect=ValueError) await crd.async_refresh() diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 72f26ae33ad457..a639b16893b4c8 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -4,7 +4,6 @@ import os from unittest.mock import Mock -from asynctest import patch import pytest from homeassistant import bootstrap @@ -12,6 +11,7 @@ from homeassistant.exceptions import HomeAssistantError import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import ( MockConfigEntry, MockModule, diff --git a/tests/test_config.py b/tests/test_config.py index b92ea0e890be46..5d9fa7d8da1a64 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -4,10 +4,7 @@ import copy import os from unittest import mock -from unittest.mock import Mock -import asynctest -from asynctest import CoroutineMock, patch import pytest import voluptuous as vol from voluptuous import Invalid, MultipleInvalid @@ -37,6 +34,7 @@ from homeassistant.util import dt as dt_util from homeassistant.util.yaml import SECRET_YAML +from tests.async_mock import AsyncMock, Mock, patch from tests.common import get_test_config_dir, patch_yaml_files CONFIG_DIR = get_test_config_dir() @@ -98,7 +96,7 @@ async def test_ensure_config_exists_creates_config(hass): If not creates a new config file. """ - with mock.patch("builtins.print") as mock_print: + with patch("builtins.print") as mock_print: await config_util.async_ensure_config_exists(hass) assert os.path.isfile(YAML_PATH) @@ -168,7 +166,7 @@ async def test_create_default_config_returns_none_if_write_error(hass): Non existing folder returns None. """ hass.config.config_dir = os.path.join(CONFIG_DIR, "non_existing_dir/") - with mock.patch("builtins.print") as mock_print: + with patch("builtins.print") as mock_print: assert await config_util.async_create_default_config(hass) is False assert mock_print.called @@ -245,15 +243,15 @@ async def test_entity_customization(hass): assert state.attributes["hidden"] -@mock.patch("homeassistant.config.shutil") -@mock.patch("homeassistant.config.os") -@mock.patch("homeassistant.config.is_docker_env", return_value=False) +@patch("homeassistant.config.shutil") +@patch("homeassistant.config.os") +@patch("homeassistant.config.is_docker_env", return_value=False) def test_remove_lib_on_upgrade(mock_docker, mock_os, mock_shutil, hass): """Test removal of library on upgrade from before 0.50.""" ha_version = "0.49.0" mock_os.path.isdir = mock.Mock(return_value=True) mock_open = mock.mock_open() - with mock.patch("homeassistant.config.open", mock_open, create=True): + with patch("homeassistant.config.open", mock_open, create=True): opened_file = mock_open.return_value # pylint: disable=no-member opened_file.readline.return_value = ha_version @@ -267,15 +265,15 @@ def test_remove_lib_on_upgrade(mock_docker, mock_os, mock_shutil, hass): assert mock_shutil.rmtree.call_args == mock.call(hass_path) -@mock.patch("homeassistant.config.shutil") -@mock.patch("homeassistant.config.os") -@mock.patch("homeassistant.config.is_docker_env", return_value=True) +@patch("homeassistant.config.shutil") +@patch("homeassistant.config.os") +@patch("homeassistant.config.is_docker_env", return_value=True) def test_remove_lib_on_upgrade_94(mock_docker, mock_os, mock_shutil, hass): """Test removal of library on upgrade from before 0.94 and in Docker.""" ha_version = "0.93.0.dev0" mock_os.path.isdir = mock.Mock(return_value=True) mock_open = mock.mock_open() - with mock.patch("homeassistant.config.open", mock_open, create=True): + with patch("homeassistant.config.open", mock_open, create=True): opened_file = mock_open.return_value # pylint: disable=no-member opened_file.readline.return_value = ha_version @@ -294,9 +292,9 @@ def test_process_config_upgrade(hass): ha_version = "0.92.0" mock_open = mock.mock_open() - with mock.patch( - "homeassistant.config.open", mock_open, create=True - ), mock.patch.object(config_util, "__version__", "0.91.0"): + with patch("homeassistant.config.open", mock_open, create=True), patch.object( + config_util, "__version__", "0.91.0" + ): opened_file = mock_open.return_value # pylint: disable=no-member opened_file.readline.return_value = ha_version @@ -312,7 +310,7 @@ def test_config_upgrade_same_version(hass): ha_version = __version__ mock_open = mock.mock_open() - with mock.patch("homeassistant.config.open", mock_open, create=True): + with patch("homeassistant.config.open", mock_open, create=True): opened_file = mock_open.return_value # pylint: disable=no-member opened_file.readline.return_value = ha_version @@ -326,7 +324,7 @@ def test_config_upgrade_no_file(hass): """Test update of version on upgrade, with no version file.""" mock_open = mock.mock_open() mock_open.side_effect = [FileNotFoundError(), mock.DEFAULT, mock.DEFAULT] - with mock.patch("homeassistant.config.open", mock_open, create=True): + with patch("homeassistant.config.open", mock_open, create=True): opened_file = mock_open.return_value # pylint: disable=no-member config_util.process_ha_config_upgrade(hass) @@ -505,14 +503,14 @@ async def test_loading_configuration_from_packages(hass): ) -@asynctest.mock.patch("homeassistant.helpers.check_config.async_check_ha_config_file") +@patch("homeassistant.helpers.check_config.async_check_ha_config_file") async def test_check_ha_config_file_correct(mock_check, hass): """Check that restart propagates to stop.""" mock_check.return_value = check_config.HomeAssistantConfig() assert await config_util.async_check_ha_config_file(hass) is None -@asynctest.mock.patch("homeassistant.helpers.check_config.async_check_ha_config_file") +@patch("homeassistant.helpers.check_config.async_check_ha_config_file") async def test_check_ha_config_file_wrong(mock_check, hass): """Check that restart with a bad config doesn't propagate to stop.""" mock_check.return_value = check_config.HomeAssistantConfig() @@ -521,9 +519,7 @@ async def test_check_ha_config_file_wrong(mock_check, hass): assert await config_util.async_check_ha_config_file(hass) == "bad" -@asynctest.mock.patch( - "homeassistant.config.os.path.isfile", mock.Mock(return_value=True) -) +@patch("homeassistant.config.os.path.isfile", mock.Mock(return_value=True)) async def test_async_hass_config_yaml_merge(merge_log_err, hass): """Test merge during async config reload.""" config = { @@ -549,7 +545,7 @@ async def test_async_hass_config_yaml_merge(merge_log_err, hass): @pytest.fixture def merge_log_err(hass): """Patch _merge_log_error from packages.""" - with mock.patch("homeassistant.config._LOGGER.error") as logerr: + with patch("homeassistant.config._LOGGER.error") as logerr: yield logerr @@ -907,7 +903,7 @@ async def test_component_config_exceptions(hass, caplog): domain="test_domain", get_platform=Mock( return_value=Mock( - async_validate_config=CoroutineMock( + async_validate_config=AsyncMock( side_effect=ValueError("broken") ) ) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 6d5c735b8825df..592bc1d4656ae6 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -2,7 +2,6 @@ import asyncio from datetime import timedelta -from asynctest import CoroutineMock, MagicMock, patch import pytest from homeassistant import config_entries, data_entry_flow, loader @@ -11,6 +10,7 @@ from homeassistant.setup import async_setup_component from homeassistant.util import dt +from tests.async_mock import AsyncMock, patch from tests.common import ( MockConfigEntry, MockEntity, @@ -54,8 +54,8 @@ async def test_call_setup_entry(hass): entry = MockConfigEntry(domain="comp") entry.add_to_hass(hass) - mock_setup_entry = MagicMock(return_value=mock_coro(True)) - mock_migrate_entry = MagicMock(return_value=mock_coro(True)) + mock_setup_entry = AsyncMock(return_value=True) + mock_migrate_entry = AsyncMock(return_value=True) mock_integration( hass, @@ -80,8 +80,8 @@ async def test_call_async_migrate_entry(hass): entry.version = 2 entry.add_to_hass(hass) - mock_migrate_entry = MagicMock(return_value=mock_coro(True)) - mock_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_migrate_entry = AsyncMock(return_value=True) + mock_setup_entry = AsyncMock(return_value=True) mock_integration( hass, @@ -106,8 +106,8 @@ async def test_call_async_migrate_entry_failure_false(hass): entry.version = 2 entry.add_to_hass(hass) - mock_migrate_entry = MagicMock(return_value=mock_coro(False)) - mock_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_migrate_entry = AsyncMock(return_value=False) + mock_setup_entry = AsyncMock(return_value=True) mock_integration( hass, @@ -132,8 +132,8 @@ async def test_call_async_migrate_entry_failure_exception(hass): entry.version = 2 entry.add_to_hass(hass) - mock_migrate_entry = MagicMock(return_value=mock_coro(exception=Exception)) - mock_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_migrate_entry = AsyncMock(side_effect=Exception) + mock_setup_entry = AsyncMock(return_value=True) mock_integration( hass, @@ -158,8 +158,8 @@ async def test_call_async_migrate_entry_failure_not_bool(hass): entry.version = 2 entry.add_to_hass(hass) - mock_migrate_entry = MagicMock(return_value=mock_coro()) - mock_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_migrate_entry = AsyncMock(return_value=None) + mock_setup_entry = AsyncMock(return_value=True) mock_integration( hass, @@ -184,7 +184,7 @@ async def test_call_async_migrate_entry_failure_not_supported(hass): entry.version = 2 entry.add_to_hass(hass) - mock_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_setup_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("comp", async_setup_entry=mock_setup_entry)) mock_entity_platform(hass, "config_flow.comp", None) @@ -211,7 +211,7 @@ async def mock_unload_entry(hass, entry): assert result return result - mock_remove_entry = MagicMock(side_effect=lambda *args, **kwargs: mock_coro()) + mock_remove_entry = AsyncMock(return_value=None) entity = MockEntity(unique_id="1234", name="Test Entity") @@ -283,9 +283,9 @@ async def mock_setup_entry_platform(hass, entry, async_add_entities): async def test_remove_entry_handles_callback_error(hass, manager): """Test that exceptions in the remove callback are handled.""" - mock_setup_entry = MagicMock(return_value=mock_coro(True)) - mock_unload_entry = MagicMock(return_value=mock_coro(True)) - mock_remove_entry = MagicMock(side_effect=lambda *args, **kwargs: mock_coro()) + mock_setup_entry = AsyncMock(return_value=True) + mock_unload_entry = AsyncMock(return_value=True) + mock_remove_entry = AsyncMock(return_value=None) mock_integration( hass, MockModule( @@ -343,7 +343,7 @@ async def mock_unload_entry(hass, entry): async def test_remove_entry_if_not_loaded(hass, manager): """Test that we can remove an entry that is not loaded.""" - mock_unload_entry = MagicMock(return_value=mock_coro(True)) + mock_unload_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("comp", async_unload_entry=mock_unload_entry)) @@ -367,7 +367,7 @@ async def test_remove_entry_if_not_loaded(hass, manager): async def test_add_entry_calls_setup_entry(hass, manager): """Test we call setup_config_entry.""" - mock_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_setup_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("comp", async_setup_entry=mock_setup_entry)) mock_entity_platform(hass, "config_flow.comp", None) @@ -486,12 +486,12 @@ async def test_forward_entry_sets_up_component(hass): """Test we setup the component entry is forwarded to.""" entry = MockConfigEntry(domain="original") - mock_original_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_original_setup_entry = AsyncMock(return_value=True) mock_integration( hass, MockModule("original", async_setup_entry=mock_original_setup_entry) ) - mock_forwarded_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_forwarded_setup_entry = AsyncMock(return_value=True) mock_integration( hass, MockModule("forwarded", async_setup_entry=mock_forwarded_setup_entry) ) @@ -505,8 +505,8 @@ async def test_forward_entry_does_not_setup_entry_if_setup_fails(hass): """Test we do not set up entry if component setup fails.""" entry = MockConfigEntry(domain="original") - mock_setup = MagicMock(return_value=mock_coro(False)) - mock_setup_entry = MagicMock() + mock_setup = AsyncMock(return_value=False) + mock_setup_entry = AsyncMock() mock_integration( hass, MockModule( @@ -643,7 +643,7 @@ async def test_setup_raise_not_ready(hass, caplog): """Test a setup raising not ready.""" entry = MockConfigEntry(domain="test") - mock_setup_entry = MagicMock(side_effect=ConfigEntryNotReady) + mock_setup_entry = AsyncMock(side_effect=ConfigEntryNotReady) mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) mock_entity_platform(hass, "config_flow.test", None) @@ -659,7 +659,7 @@ async def test_setup_raise_not_ready(hass, caplog): assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY mock_setup_entry.side_effect = None - mock_setup_entry.return_value = mock_coro(True) + mock_setup_entry.return_value = True await p_setup(None) assert entry.state == config_entries.ENTRY_STATE_LOADED @@ -669,7 +669,7 @@ async def test_setup_retrying_during_unload(hass): """Test if we unload an entry that is in retry mode.""" entry = MockConfigEntry(domain="test") - mock_setup_entry = MagicMock(side_effect=ConfigEntryNotReady) + mock_setup_entry = AsyncMock(side_effect=ConfigEntryNotReady) mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) mock_entity_platform(hass, "config_flow.test", None) @@ -721,8 +721,8 @@ async def test_entry_setup_succeed(hass, manager): entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_NOT_LOADED) entry.add_to_hass(hass) - mock_setup = MagicMock(return_value=mock_coro(True)) - mock_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_setup = AsyncMock(return_value=True) + mock_setup_entry = AsyncMock(return_value=True) mock_integration( hass, @@ -751,8 +751,8 @@ async def test_entry_setup_invalid_state(hass, manager, state): entry = MockConfigEntry(domain="comp", state=state) entry.add_to_hass(hass) - mock_setup = MagicMock(return_value=mock_coro(True)) - mock_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_setup = AsyncMock(return_value=True) + mock_setup_entry = AsyncMock(return_value=True) mock_integration( hass, @@ -772,7 +772,7 @@ async def test_entry_unload_succeed(hass, manager): entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_LOADED) entry.add_to_hass(hass) - async_unload_entry = MagicMock(return_value=mock_coro(True)) + async_unload_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("comp", async_unload_entry=async_unload_entry)) @@ -794,7 +794,7 @@ async def test_entry_unload_failed_to_load(hass, manager, state): entry = MockConfigEntry(domain="comp", state=state) entry.add_to_hass(hass) - async_unload_entry = MagicMock(return_value=mock_coro(True)) + async_unload_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("comp", async_unload_entry=async_unload_entry)) @@ -815,7 +815,7 @@ async def test_entry_unload_invalid_state(hass, manager, state): entry = MockConfigEntry(domain="comp", state=state) entry.add_to_hass(hass) - async_unload_entry = MagicMock(return_value=mock_coro(True)) + async_unload_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("comp", async_unload_entry=async_unload_entry)) @@ -831,9 +831,9 @@ async def test_entry_reload_succeed(hass, manager): entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_LOADED) entry.add_to_hass(hass) - async_setup = MagicMock(return_value=mock_coro(True)) - async_setup_entry = MagicMock(return_value=mock_coro(True)) - async_unload_entry = MagicMock(return_value=mock_coro(True)) + async_setup = AsyncMock(return_value=True) + async_setup_entry = AsyncMock(return_value=True) + async_unload_entry = AsyncMock(return_value=True) mock_integration( hass, @@ -866,9 +866,9 @@ async def test_entry_reload_not_loaded(hass, manager, state): entry = MockConfigEntry(domain="comp", state=state) entry.add_to_hass(hass) - async_setup = MagicMock(return_value=mock_coro(True)) - async_setup_entry = MagicMock(return_value=mock_coro(True)) - async_unload_entry = MagicMock(return_value=mock_coro(True)) + async_setup = AsyncMock(return_value=True) + async_setup_entry = AsyncMock(return_value=True) + async_unload_entry = AsyncMock(return_value=True) mock_integration( hass, @@ -900,9 +900,9 @@ async def test_entry_reload_error(hass, manager, state): entry = MockConfigEntry(domain="comp", state=state) entry.add_to_hass(hass) - async_setup = MagicMock(return_value=mock_coro(True)) - async_setup_entry = MagicMock(return_value=mock_coro(True)) - async_unload_entry = MagicMock(return_value=mock_coro(True)) + async_setup = AsyncMock(return_value=True) + async_setup_entry = AsyncMock(return_value=True) + async_unload_entry = AsyncMock(return_value=True) mock_integration( hass, @@ -968,8 +968,8 @@ async def test_reload_entry_entity_registry_works(hass): domain="comp", state=config_entries.ENTRY_STATE_LOADED ) config_entry.add_to_hass(hass) - mock_setup_entry = MagicMock(return_value=mock_coro(True)) - mock_unload_entry = MagicMock(return_value=mock_coro(True)) + mock_setup_entry = AsyncMock(return_value=True) + mock_unload_entry = AsyncMock(return_value=True) mock_integration( hass, MockModule( @@ -1016,7 +1016,7 @@ async def test_reload_entry_entity_registry_works(hass): async def test_unqiue_id_persisted(hass, manager): """Test that a unique ID is stored in the config entry.""" - mock_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_setup_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("comp", async_setup_entry=mock_setup_entry)) mock_entity_platform(hass, "config_flow.comp", None) @@ -1052,9 +1052,9 @@ async def test_unique_id_existing_entry(hass, manager): unique_id="mock-unique-id", ).add_to_hass(hass) - async_setup_entry = MagicMock(side_effect=lambda _, _2: mock_coro(True)) - async_unload_entry = MagicMock(side_effect=lambda _, _2: mock_coro(True)) - async_remove_entry = MagicMock(side_effect=lambda _, _2: mock_coro(True)) + async_setup_entry = AsyncMock(return_value=True) + async_unload_entry = AsyncMock(return_value=True) + async_remove_entry = AsyncMock(return_value=True) mock_integration( hass, @@ -1205,8 +1205,7 @@ async def async_step_user(self, user_input=None): async def test_finish_flow_aborts_progress(hass, manager): """Test that when finishing a flow, we abort other flows in progress with unique ID.""" mock_integration( - hass, - MockModule("comp", async_setup_entry=MagicMock(return_value=mock_coro(True))), + hass, MockModule("comp", async_setup_entry=AsyncMock(return_value=True)), ) mock_entity_platform(hass, "config_flow.comp", None) @@ -1243,7 +1242,7 @@ async def async_step_user(self, user_input=None): async def test_unique_id_ignore(hass, manager): """Test that we can ignore flows that are in progress and have a unique ID.""" - async_setup_entry = MagicMock(return_value=mock_coro(False)) + async_setup_entry = AsyncMock(return_value=False) mock_integration(hass, MockModule("comp", async_setup_entry=async_setup_entry)) mock_entity_platform(hass, "config_flow.comp", None) @@ -1285,7 +1284,7 @@ async def async_step_user(self, user_input=None): async def test_unignore_step_form(hass, manager): """Test that we can ignore flows that are in progress and have a unique ID, then rediscover them.""" - async_setup_entry = MagicMock(return_value=mock_coro(True)) + async_setup_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("comp", async_setup_entry=async_setup_entry)) mock_entity_platform(hass, "config_flow.comp", None) @@ -1329,7 +1328,7 @@ async def async_step_unignore(self, user_input): async def test_unignore_create_entry(hass, manager): """Test that we can ignore flows that are in progress and have a unique ID, then rediscover them.""" - async_setup_entry = MagicMock(return_value=mock_coro(True)) + async_setup_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("comp", async_setup_entry=async_setup_entry)) mock_entity_platform(hass, "config_flow.comp", None) @@ -1376,7 +1375,7 @@ async def async_step_unignore(self, user_input): async def test_unignore_default_impl(hass, manager): """Test that resdicovery is a no-op by default.""" - async_setup_entry = MagicMock(return_value=mock_coro(True)) + async_setup_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("comp", async_setup_entry=async_setup_entry)) mock_entity_platform(hass, "config_flow.comp", None) @@ -1407,7 +1406,7 @@ class TestFlow(config_entries.ConfigFlow): async def test_partial_flows_hidden(hass, manager): """Test that flows that don't have a cur_step and haven't finished initing are hidden.""" - async_setup_entry = MagicMock(return_value=mock_coro(True)) + async_setup_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("comp", async_setup_entry=async_setup_entry)) mock_entity_platform(hass, "config_flow.comp", None) await async_setup_component(hass, "persistent_notification", {}) @@ -1476,7 +1475,7 @@ async def mock_async_setup(hass, config): ) return True - async_setup_entry = CoroutineMock(return_value=True) + async_setup_entry = AsyncMock(return_value=True) mock_integration( hass, MockModule( diff --git a/tests/test_core.py b/tests/test_core.py index 8b4a2ae9f84ae5..3221bfcd39df67 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -7,7 +7,6 @@ import os from tempfile import TemporaryDirectory import unittest -from unittest.mock import MagicMock, patch import pytest import pytz @@ -35,6 +34,7 @@ import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM +from tests.async_mock import MagicMock, patch from tests.common import async_mock_service, get_test_home_assistant PST = pytz.timezone("America/Los_Angeles") diff --git a/tests/test_loader.py b/tests/test_loader.py index 745bb9c8c2cded..eb99cb3a8eaf1f 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1,11 +1,11 @@ """Test to verify that we can load components.""" -from asynctest.mock import ANY, patch import pytest from homeassistant.components import http, hue from homeassistant.components.hue import light as hue_light import homeassistant.loader as loader +from tests.async_mock import ANY, patch from tests.common import MockModule, async_mock_service, mock_integration diff --git a/tests/test_requirements.py b/tests/test_requirements.py index e95db4e533dd8c..f98485e8006c25 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -1,7 +1,6 @@ """Test requirements module.""" import os from pathlib import Path -from unittest.mock import call, patch import pytest @@ -15,12 +14,8 @@ async_process_requirements, ) -from tests.common import ( - MockModule, - get_test_home_assistant, - mock_coro, - mock_integration, -) +from tests.async_mock import call, patch +from tests.common import MockModule, mock_integration def env_without_wheel_links(): @@ -30,58 +25,42 @@ def env_without_wheel_links(): return env -class TestRequirements: - """Test the requirements module.""" - - hass = None - backup_cache = None - - # pylint: disable=invalid-name, no-self-use - def setup_method(self, method): - """Set up the test.""" - self.hass = get_test_home_assistant() - - def teardown_method(self, method): - """Clean up.""" - self.hass.stop() - - @patch("os.path.dirname") - @patch("homeassistant.util.package.is_virtual_env", return_value=True) - @patch("homeassistant.util.package.is_docker_env", return_value=False) - @patch("homeassistant.util.package.install_package", return_value=True) - @patch.dict(os.environ, env_without_wheel_links(), clear=True) - def test_requirement_installed_in_venv( - self, mock_install, mock_denv, mock_venv, mock_dirname +async def test_requirement_installed_in_venv(hass): + """Test requirement installed in virtual environment.""" + with patch("os.path.dirname", return_value="ha_package_path"), patch( + "homeassistant.util.package.is_virtual_env", return_value=True + ), patch("homeassistant.util.package.is_docker_env", return_value=False), patch( + "homeassistant.util.package.install_package", return_value=True + ) as mock_install, patch.dict( + os.environ, env_without_wheel_links(), clear=True ): - """Test requirement installed in virtual environment.""" - mock_dirname.return_value = "ha_package_path" - self.hass.config.skip_pip = False - mock_integration(self.hass, MockModule("comp", requirements=["package==0.0.1"])) - assert setup.setup_component(self.hass, "comp", {}) - assert "comp" in self.hass.config.components + hass.config.skip_pip = False + mock_integration(hass, MockModule("comp", requirements=["package==0.0.1"])) + assert await setup.async_setup_component(hass, "comp", {}) + assert "comp" in hass.config.components assert mock_install.call_args == call( "package==0.0.1", constraints=os.path.join("ha_package_path", CONSTRAINT_FILE), no_cache_dir=False, ) - @patch("os.path.dirname") - @patch("homeassistant.util.package.is_virtual_env", return_value=False) - @patch("homeassistant.util.package.is_docker_env", return_value=False) - @patch("homeassistant.util.package.install_package", return_value=True) - @patch.dict(os.environ, env_without_wheel_links(), clear=True) - def test_requirement_installed_in_deps( - self, mock_install, mock_denv, mock_venv, mock_dirname + +async def test_requirement_installed_in_deps(hass): + """Test requirement installed in deps directory.""" + with patch("os.path.dirname", return_value="ha_package_path"), patch( + "homeassistant.util.package.is_virtual_env", return_value=False + ), patch("homeassistant.util.package.is_docker_env", return_value=False), patch( + "homeassistant.util.package.install_package", return_value=True + ) as mock_install, patch.dict( + os.environ, env_without_wheel_links(), clear=True ): - """Test requirement installed in deps directory.""" - mock_dirname.return_value = "ha_package_path" - self.hass.config.skip_pip = False - mock_integration(self.hass, MockModule("comp", requirements=["package==0.0.1"])) - assert setup.setup_component(self.hass, "comp", {}) - assert "comp" in self.hass.config.components + hass.config.skip_pip = False + mock_integration(hass, MockModule("comp", requirements=["package==0.0.1"])) + assert await setup.async_setup_component(hass, "comp", {}) + assert "comp" in hass.config.components assert mock_install.call_args == call( "package==0.0.1", - target=self.hass.config.path("deps"), + target=hass.config.path("deps"), constraints=os.path.join("ha_package_path", CONSTRAINT_FILE), no_cache_dir=False, ) @@ -239,7 +218,6 @@ async def test_discovery_requirements_ssdp(hass): ) with patch( "homeassistant.requirements.async_process_requirements", - side_effect=lambda _, _2, _3: mock_coro(), ) as mock_process: await async_get_integration_with_requirements(hass, "ssdp_comp") @@ -262,7 +240,6 @@ async def test_discovery_requirements_zeroconf(hass, partial_manifest): with patch( "homeassistant.requirements.async_process_requirements", - side_effect=lambda _, _2, _3: mock_coro(), ) as mock_process: await async_get_integration_with_requirements(hass, "comp") diff --git a/tests/test_setup.py b/tests/test_setup.py index a5e53861ef024c..4197fe7370a13a 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -5,7 +5,6 @@ import os import threading -from asynctest import Mock, patch import pytest import voluptuous as vol @@ -20,6 +19,7 @@ ) import homeassistant.util.dt as dt_util +from tests.async_mock import Mock, patch from tests.common import ( MockConfigEntry, MockModule, diff --git a/tests/util/test_location.py b/tests/util/test_location.py index 968c6257f37f9e..403e24121ad70e 100644 --- a/tests/util/test_location.py +++ b/tests/util/test_location.py @@ -1,10 +1,10 @@ """Test Home Assistant location util methods.""" import aiohttp -from asynctest import Mock, patch import pytest import homeassistant.util.location as location_util +from tests.async_mock import Mock, patch from tests.common import load_fixture # Paris diff --git a/tests/util/test_package.py b/tests/util/test_package.py index f7cce0e298f86f..c9f5183249e442 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -5,12 +5,13 @@ from subprocess import PIPE import sys -from asynctest import MagicMock, call, patch import pkg_resources import pytest import homeassistant.util.package as package +from tests.async_mock import MagicMock, call, patch + RESOURCE_DIR = os.path.abspath( os.path.join(os.path.dirname(__file__), "..", "resources") ) @@ -70,13 +71,11 @@ def mock_venv(): yield mock -@asyncio.coroutine def mock_async_subprocess(): """Return an async Popen mock.""" async_popen = MagicMock() - @asyncio.coroutine - def communicate(input=None): + async def communicate(input=None): """Communicate mock.""" stdout = bytes("/deps_dir/lib_dir", "utf-8") return (stdout, None) From 6fe00497d6d851bc216521ebd61bc0c590ee3308 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 30 Apr 2020 23:03:54 +0200 Subject: [PATCH 11/58] Fix webhook imports sorting (#34988) --- homeassistant/components/webhook/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index 04332166c60a8e..4a90a247e754ba 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -6,8 +6,8 @@ import voluptuous as vol from homeassistant.components import websocket_api -from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.http.const import KEY_REAL_IP +from homeassistant.components.http.view import HomeAssistantView from homeassistant.const import HTTP_OK from homeassistant.core import callback from homeassistant.loader import bind_hass From 928d9ec117f34a07e67b1047c532b694380f5a2b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 1 May 2020 00:15:53 +0200 Subject: [PATCH 12/58] Fix not condition validation and entity/device extraction (#34959) --- homeassistant/helpers/condition.py | 6 +- tests/helpers/test_condition.py | 97 +++++++++++++++++++++++++++++- 2 files changed, 98 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 61704e2d23ad69..535de0304a0629 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -536,7 +536,7 @@ async def async_validate_condition_config( ) -> ConfigType: """Validate config.""" condition = config[CONF_CONDITION] - if condition in ("and", "or"): + if condition in ("and", "not", "or"): conditions = [] for sub_cond in config["conditions"]: sub_cond = await async_validate_condition_config(hass, sub_cond) @@ -563,7 +563,7 @@ def async_extract_entities(config: ConfigType) -> Set[str]: config = to_process.popleft() condition = config[CONF_CONDITION] - if condition in ("and", "or"): + if condition in ("and", "not", "or"): to_process.extend(config["conditions"]) continue @@ -585,7 +585,7 @@ def async_extract_devices(config: ConfigType) -> Set[str]: config = to_process.popleft() condition = config[CONF_CONDITION] - if condition in ("and", "or"): + if condition in ("and", "not", "or"): to_process.extend(config["conditions"]) continue diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index cadce628c10acd..03c3965fad7dbf 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -1,10 +1,31 @@ """Test the condition helper.""" from unittest.mock import patch +import pytest + +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import condition from homeassistant.util import dt +async def test_invalid_condition(hass): + """Test if invalid condition raises.""" + with pytest.raises(HomeAssistantError): + await condition.async_from_config( + hass, + { + "condition": "invalid", + "conditions": [ + { + "condition": "state", + "entity_id": "sensor.temperature", + "state": "100", + }, + ], + }, + ) + + async def test_and_condition(hass): """Test the 'and' condition.""" test = await condition.async_from_config( @@ -261,9 +282,46 @@ async def test_extract_entities(): "entity_id": "sensor.temperature_2", "below": 110, }, + { + "condition": "not", + "conditions": [ + { + "condition": "state", + "entity_id": "sensor.temperature_3", + "state": "100", + }, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature_4", + "below": 110, + }, + ], + }, + { + "condition": "or", + "conditions": [ + { + "condition": "state", + "entity_id": "sensor.temperature_5", + "state": "100", + }, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature_6", + "below": 110, + }, + ], + }, ], } - ) == {"sensor.temperature", "sensor.temperature_2"} + ) == { + "sensor.temperature", + "sensor.temperature_2", + "sensor.temperature_3", + "sensor.temperature_4", + "sensor.temperature_5", + "sensor.temperature_6", + } async def test_extract_devices(): @@ -274,6 +332,41 @@ async def test_extract_devices(): "conditions": [ {"condition": "device", "device_id": "abcd", "domain": "light"}, {"condition": "device", "device_id": "qwer", "domain": "switch"}, + { + "condition": "state", + "entity_id": "sensor.not_a_device", + "state": "100", + }, + { + "condition": "not", + "conditions": [ + { + "condition": "device", + "device_id": "abcd_not", + "domain": "light", + }, + { + "condition": "device", + "device_id": "qwer_not", + "domain": "switch", + }, + ], + }, + { + "condition": "or", + "conditions": [ + { + "condition": "device", + "device_id": "abcd_or", + "domain": "light", + }, + { + "condition": "device", + "device_id": "qwer_or", + "domain": "switch", + }, + ], + }, ], } - ) == {"abcd", "qwer"} + ) == {"abcd", "qwer", "abcd_not", "qwer_not", "abcd_or", "qwer_or"} From 799d98eaf055db82265bcea18cb4f584c15ab5ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Apr 2020 17:49:16 -0500 Subject: [PATCH 13/58] Cleanup homekit callbacks and jobs (#34975) --- homeassistant/components/homekit/__init__.py | 2 +- .../components/homekit/accessories.py | 31 ++++++++++--------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 479e1e4dab95f5..c77ec36ccf3b4e 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -494,7 +494,7 @@ async def async_stop(self, *args): self.status = STATUS_STOPPED _LOGGER.debug("Driver stop") - self.hass.async_add_executor_job(self.driver.stop) + self.hass.add_job(self.driver.stop) @callback def _async_configure_linked_battery_sensors(self, ent_reg, device_lookup, state): diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index ab3d1ae85071c4..c3b42f0b6dc638 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -165,8 +165,10 @@ async def run_handler(self): Run inside the Home Assistant event loop. """ state = self.hass.states.get(self.entity_id) - self.hass.async_add_job(self.update_state_callback, None, None, state) - async_track_state_change(self.hass, self.entity_id, self.update_state_callback) + await self.async_update_state_callback(None, None, state) + async_track_state_change( + self.hass, self.entity_id, self.async_update_state_callback + ) battery_charging_state = None battery_state = None @@ -179,7 +181,7 @@ async def run_handler(self): ATTR_BATTERY_CHARGING ) async_track_state_change( - self.hass, self.linked_battery_sensor, self.update_linked_battery + self.hass, self.linked_battery_sensor, self.async_update_linked_battery ) else: battery_state = state.attributes.get(ATTR_BATTERY_LEVEL) @@ -191,7 +193,7 @@ async def run_handler(self): async_track_state_change( self.hass, self.linked_battery_charging_sensor, - self.update_linked_battery_charging, + self.async_update_linked_battery_charging, ) elif battery_charging_state is None: battery_charging_state = state.attributes.get(ATTR_BATTERY_CHARGING) @@ -201,8 +203,9 @@ async def run_handler(self): self.update_battery, battery_state, battery_charging_state ) - @ha_callback - def update_state_callback(self, entity_id=None, old_state=None, new_state=None): + async def async_update_state_callback( + self, entity_id=None, old_state=None, new_state=None + ): """Handle state change listener callback.""" _LOGGER.debug("New_state: %s", new_state) if new_state is None: @@ -220,28 +223,28 @@ def update_state_callback(self, entity_id=None, old_state=None, new_state=None): ): battery_charging_state = new_state.attributes.get(ATTR_BATTERY_CHARGING) if battery_state is not None or battery_charging_state is not None: - self.hass.async_add_executor_job( + await self.hass.async_add_executor_job( self.update_battery, battery_state, battery_charging_state ) - self.hass.async_add_executor_job(self.update_state, new_state) + await self.hass.async_add_executor_job(self.update_state, new_state) - @ha_callback - def update_linked_battery(self, entity_id=None, old_state=None, new_state=None): + async def async_update_linked_battery( + self, entity_id=None, old_state=None, new_state=None + ): """Handle linked battery sensor state change listener callback.""" if self.linked_battery_charging_sensor: battery_charging_state = None else: battery_charging_state = new_state.attributes.get(ATTR_BATTERY_CHARGING) - self.hass.async_add_executor_job( + await self.hass.async_add_executor_job( self.update_battery, new_state.state, battery_charging_state, ) - @ha_callback - def update_linked_battery_charging( + async def async_update_linked_battery_charging( self, entity_id=None, old_state=None, new_state=None ): """Handle linked battery charging sensor state change listener callback.""" - self.hass.async_add_executor_job( + await self.hass.async_add_executor_job( self.update_battery, None, new_state.state == STATE_ON ) From ba7391528fa15a0323a9b2f47d98134e7d463bd9 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 30 Apr 2020 18:24:47 -0500 Subject: [PATCH 14/58] Add unique id to esphome config flow (#34753) --- .../components/esphome/config_flow.py | 72 +++--- homeassistant/components/esphome/strings.json | 5 +- .../components/esphome/translations/en.json | 5 +- tests/components/esphome/test_config_flow.py | 211 ++++++++++++------ 4 files changed, 191 insertions(+), 102 deletions(-) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 17d3ed5f65983b..cb9b7958efa26d 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -5,18 +5,21 @@ from aioesphomeapi import APIClient, APIConnectionError import voluptuous as vol -from homeassistant import config_entries, core -from homeassistant.helpers.typing import ConfigType +from homeassistant.config_entries import CONN_CLASS_LOCAL_PUSH, ConfigFlow +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT +from homeassistant.core import callback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .entry_data import DATA_KEY, RuntimeEntryData +DOMAIN = "esphome" -@config_entries.HANDLERS.register("esphome") -class EsphomeFlowHandler(config_entries.ConfigFlow): + +class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a esphome config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + CONNECTION_CLASS = CONN_CLASS_LOCAL_PUSH def __init__(self): """Initialize flow.""" @@ -32,8 +35,8 @@ async def async_step_user( return await self._async_authenticate_or_add(user_input) fields = OrderedDict() - fields[vol.Required("host", default=self._host or vol.UNDEFINED)] = str - fields[vol.Optional("port", default=self._port or 6053)] = int + fields[vol.Required(CONF_HOST, default=self._host or vol.UNDEFINED)] = str + fields[vol.Optional(CONF_PORT, default=self._port or 6053)] = int errors = {} if error is not None: @@ -46,19 +49,19 @@ async def async_step_user( @property def _name(self): # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - return self.context.get("name") + return self.context.get(CONF_NAME) @_name.setter def _name(self, value): # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - self.context["name"] = value + self.context[CONF_NAME] = value self.context["title_placeholders"] = {"name": self._name} def _set_user_input(self, user_input): if user_input is None: return - self._host = user_input["host"] - self._port = user_input["port"] + self._host = user_input[CONF_HOST] + self._port = user_input[CONF_PORT] async def _async_authenticate_or_add(self, user_input): self._set_user_input(user_input) @@ -81,56 +84,69 @@ async def async_step_discovery_confirm(self, user_input=None): step_id="discovery_confirm", description_placeholders={"name": self._name} ) - async def async_step_zeroconf(self, user_input: ConfigType): + async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): """Handle zeroconf discovery.""" # Hostname is format: livingroom.local. - local_name = user_input["hostname"][:-1] + local_name = discovery_info["hostname"][:-1] node_name = local_name[: -len(".local")] - address = user_input["properties"].get("address", local_name) + address = discovery_info["properties"].get("address", local_name) # Check if already configured + await self.async_set_unique_id(node_name) + self._abort_if_unique_id_configured( + updates={CONF_HOST: discovery_info[CONF_HOST]} + ) + for entry in self._async_current_entries(): already_configured = False - if entry.data["host"] == address: - # Is this address already configured? + + if ( + entry.data[CONF_HOST] == address + or entry.data[CONF_HOST] == discovery_info[CONF_HOST] + ): + # Is this address or IP address already configured? already_configured = True elif entry.entry_id in self.hass.data.get(DATA_KEY, {}): # Does a config entry with this name already exist? data: RuntimeEntryData = self.hass.data[DATA_KEY][entry.entry_id] + # Node names are unique in the network if data.device_info is not None: already_configured = data.device_info.name == node_name if already_configured: + # Backwards compat, we update old entries + if not entry.unique_id: + self.hass.config_entries.async_update_entry( + entry, + data={**entry.data, CONF_HOST: discovery_info[CONF_HOST]}, + unique_id=node_name, + ) + return self.async_abort(reason="already_configured") - self._host = address - self._port = user_input["port"] + self._host = discovery_info[CONF_HOST] + self._port = discovery_info[CONF_PORT] self._name = node_name - # Check if flow for this device already in progress - for flow in self._async_in_progress(): - if flow["context"].get("name") == node_name: - return self.async_abort(reason="already_configured") - return await self.async_step_discovery_confirm() - @core.callback + @callback def _async_get_entry(self): return self.async_create_entry( title=self._name, data={ - "host": self._host, - "port": self._port, + CONF_HOST: self._host, + CONF_PORT: self._port, # The API uses protobuf, so empty string denotes absence - "password": self._password or "", + CONF_PASSWORD: self._password or "", }, ) async def async_step_authenticate(self, user_input=None, error=None): """Handle getting password for authentication.""" if user_input is not None: - self._password = user_input["password"] + self._password = user_input[CONF_PASSWORD] error = await self.try_login() if error: return await self.async_step_authenticate(error=error) diff --git a/homeassistant/components/esphome/strings.json b/homeassistant/components/esphome/strings.json index 0c2f0fee8d9b2a..e5eeb150d89fd8 100644 --- a/homeassistant/components/esphome/strings.json +++ b/homeassistant/components/esphome/strings.json @@ -1,6 +1,9 @@ { "config": { - "abort": { "already_configured": "ESP is already configured" }, + "abort": { + "already_configured": "ESP is already configured", + "already_in_progress": "ESP configuration is already in progress" + }, "error": { "resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips", "connection_error": "Can't connect to ESP. Please make sure your YAML file contains an 'api:' line.", diff --git a/homeassistant/components/esphome/translations/en.json b/homeassistant/components/esphome/translations/en.json index b52dbc5a3f1d09..3f695e6f183b2e 100644 --- a/homeassistant/components/esphome/translations/en.json +++ b/homeassistant/components/esphome/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "ESP is already configured" + "already_configured": "ESP is already configured", + "already_in_progress": "ESP configuration is already in progress" }, "error": { "connection_error": "Can't connect to ESP. Please make sure your YAML file contains an 'api:' line.", @@ -31,4 +32,4 @@ } } } -} \ No newline at end of file +} diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index b55621226fad38..50aaa989028091 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -3,7 +3,13 @@ import pytest -from homeassistant.components.esphome import DATA_KEY, config_flow +from homeassistant.components.esphome import DATA_KEY +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) from tests.async_mock import AsyncMock, MagicMock, patch from tests.common import MockConfigEntry @@ -40,26 +46,27 @@ def mock_api_connection_error(): yield mock_error -def _setup_flow_handler(hass): - flow = config_flow.EsphomeFlowHandler() - flow.hass = hass - flow.context = {} - return flow - - async def test_user_connection_works(hass, mock_client): """Test we can finish a config flow.""" - flow = _setup_flow_handler(hass) - result = await flow.async_step_user(user_input=None) - assert result["type"] == "form" + result = await hass.config_entries.flow.async_init( + "esphome", context={"source": "user"}, data=None, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test")) - result = await flow.async_step_user(user_input={"host": "127.0.0.1", "port": 80}) + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": "user"}, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 80}, + ) - assert result["type"] == "create_entry" - assert result["data"] == {"host": "127.0.0.1", "port": 80, "password": ""} + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == {CONF_HOST: "127.0.0.1", CONF_PORT: 80, CONF_PASSWORD: ""} assert result["title"] == "test" + assert len(mock_client.connect.mock_calls) == 1 assert len(mock_client.device_info.mock_calls) == 1 assert len(mock_client.disconnect.mock_calls) == 1 @@ -70,8 +77,6 @@ async def test_user_connection_works(hass, mock_client): async def test_user_resolve_error(hass, mock_api_connection_error, mock_client): """Test user step with IP resolve error.""" - flow = _setup_flow_handler(hass) - await flow.async_step_user(user_input=None) class MockResolveError(mock_api_connection_error): """Create an exception with a specific error message.""" @@ -85,13 +90,16 @@ def __init__(self): new_callable=lambda: MockResolveError, ) as exc: mock_client.device_info.side_effect = exc - result = await flow.async_step_user( - user_input={"host": "127.0.0.1", "port": 6053} + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": "user"}, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == "form" + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "resolve_error"} + assert len(mock_client.connect.mock_calls) == 1 assert len(mock_client.device_info.mock_calls) == 1 assert len(mock_client.disconnect.mock_calls) == 1 @@ -99,16 +107,18 @@ def __init__(self): async def test_user_connection_error(hass, mock_api_connection_error, mock_client): """Test user step with connection error.""" - flow = _setup_flow_handler(hass) - await flow.async_step_user(user_input=None) - mock_client.device_info.side_effect = mock_api_connection_error - result = await flow.async_step_user(user_input={"host": "127.0.0.1", "port": 6053}) + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": "user"}, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, + ) - assert result["type"] == "form" + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "connection_error"} + assert len(mock_client.connect.mock_calls) == 1 assert len(mock_client.device_info.mock_calls) == 1 assert len(mock_client.disconnect.mock_calls) == 1 @@ -116,125 +126,159 @@ async def test_user_connection_error(hass, mock_api_connection_error, mock_clien async def test_user_with_password(hass, mock_client): """Test user step with password.""" - flow = _setup_flow_handler(hass) - await flow.async_step_user(user_input=None) - mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(True, "test")) - result = await flow.async_step_user(user_input={"host": "127.0.0.1", "port": 6053}) + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": "user"}, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, + ) - assert result["type"] == "form" + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "authenticate" - result = await flow.async_step_authenticate(user_input={"password": "password1"}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PASSWORD: "password1"} + ) - assert result["type"] == "create_entry" + assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["data"] == { - "host": "127.0.0.1", - "port": 6053, - "password": "password1", + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "password1", } assert mock_client.password == "password1" async def test_user_invalid_password(hass, mock_api_connection_error, mock_client): """Test user step with invalid password.""" - flow = _setup_flow_handler(hass) - await flow.async_step_user(user_input=None) - mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(True, "test")) - await flow.async_step_user(user_input={"host": "127.0.0.1", "port": 6053}) + result = await hass.config_entries.flow.async_init( + "esphome", + context={"source": "user"}, + data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "authenticate" + mock_client.connect.side_effect = mock_api_connection_error - result = await flow.async_step_authenticate(user_input={"password": "invalid"}) - assert result["type"] == "form" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_PASSWORD: "invalid"} + ) + + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "authenticate" assert result["errors"] == {"base": "invalid_password"} async def test_discovery_initiation(hass, mock_client): """Test discovery importing works.""" - flow = _setup_flow_handler(hass) + mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test8266")) + service_info = { "host": "192.168.43.183", "port": 6053, "hostname": "test8266.local.", "properties": {}, } + flow = await hass.config_entries.flow.async_init( + "esphome", context={"source": "zeroconf"}, data=service_info + ) - mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test8266")) - - result = await flow.async_step_zeroconf(user_input=service_info) - assert result["type"] == "form" - assert result["step_id"] == "discovery_confirm" - assert result["description_placeholders"]["name"] == "test8266" - assert flow.context["title_placeholders"]["name"] == "test8266" + result = await hass.config_entries.flow.async_configure( + flow["flow_id"], user_input={} + ) - result = await flow.async_step_discovery_confirm(user_input={}) - assert result["type"] == "create_entry" + assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == "test8266" - assert result["data"]["host"] == "test8266.local" - assert result["data"]["port"] == 6053 + assert result["data"][CONF_HOST] == "192.168.43.183" + assert result["data"][CONF_PORT] == 6053 + + assert result["result"] + assert result["result"].unique_id == "test8266" async def test_discovery_already_configured_hostname(hass, mock_client): """Test discovery aborts if already configured via hostname.""" - MockConfigEntry( - domain="esphome", data={"host": "test8266.local", "port": 6053, "password": ""} - ).add_to_hass(hass) + entry = MockConfigEntry( + domain="esphome", + data={CONF_HOST: "test8266.local", CONF_PORT: 6053, CONF_PASSWORD: ""}, + ) + + entry.add_to_hass(hass) - flow = _setup_flow_handler(hass) service_info = { "host": "192.168.43.183", "port": 6053, "hostname": "test8266.local.", "properties": {}, } - result = await flow.async_step_zeroconf(user_input=service_info) - assert result["type"] == "abort" + result = await hass.config_entries.flow.async_init( + "esphome", context={"source": "zeroconf"}, data=service_info + ) + + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" + assert entry.unique_id == "test8266" + async def test_discovery_already_configured_ip(hass, mock_client): """Test discovery aborts if already configured via static IP.""" - MockConfigEntry( - domain="esphome", data={"host": "192.168.43.183", "port": 6053, "password": ""} - ).add_to_hass(hass) + entry = MockConfigEntry( + domain="esphome", + data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""}, + ) + + entry.add_to_hass(hass) - flow = _setup_flow_handler(hass) service_info = { "host": "192.168.43.183", "port": 6053, "hostname": "test8266.local.", "properties": {"address": "192.168.43.183"}, } - result = await flow.async_step_zeroconf(user_input=service_info) - assert result["type"] == "abort" + result = await hass.config_entries.flow.async_init( + "esphome", context={"source": "zeroconf"}, data=service_info + ) + + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" + assert entry.unique_id == "test8266" + async def test_discovery_already_configured_name(hass, mock_client): """Test discovery aborts if already configured via name.""" entry = MockConfigEntry( - domain="esphome", data={"host": "192.168.43.183", "port": 6053, "password": ""} + domain="esphome", + data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""}, ) entry.add_to_hass(hass) + mock_entry_data = MagicMock() mock_entry_data.device_info.name = "test8266" hass.data[DATA_KEY] = {entry.entry_id: mock_entry_data} - flow = _setup_flow_handler(hass) service_info = { - "host": "192.168.43.183", + "host": "192.168.43.184", "port": 6053, "hostname": "test8266.local.", "properties": {"address": "test8266.local"}, } - result = await flow.async_step_zeroconf(user_input=service_info) - assert result["type"] == "abort" + result = await hass.config_entries.flow.async_init( + "esphome", context={"source": "zeroconf"}, data=service_info + ) + + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" + assert entry.unique_id == "test8266" + assert entry.data[CONF_HOST] == "192.168.43.184" + async def test_discovery_duplicate_data(hass, mock_client): """Test discovery aborts if same mDNS packet arrives.""" @@ -250,11 +294,36 @@ async def test_discovery_duplicate_data(hass, mock_client): result = await hass.config_entries.flow.async_init( "esphome", data=service_info, context={"source": "zeroconf"} ) - assert result["type"] == "form" + assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( "esphome", data=service_info, context={"source": "zeroconf"} ) - assert result["type"] == "abort" + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_in_progress" + + +async def test_discovery_updates_unique_id(hass, mock_client): + """Test a duplicate discovery host aborts and updates existing entry.""" + entry = MockConfigEntry( + domain="esphome", + data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""}, + ) + + entry.add_to_hass(hass) + + service_info = { + "host": "192.168.43.183", + "port": 6053, + "hostname": "test8266.local.", + "properties": {"address": "test8266.local"}, + } + result = await hass.config_entries.flow.async_init( + "esphome", context={"source": "zeroconf"}, data=service_info + ) + + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" + + assert entry.unique_id == "test8266" From 76f392476be4db41f787712310602575a7191f8a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Apr 2020 16:31:00 -0700 Subject: [PATCH 15/58] Use a future for mock coro (#34989) --- tests/common.py | 62 ++----------------- tests/components/alexa/test_smart_home.py | 6 +- tests/components/almond/test_init.py | 14 ++--- .../components/asuswrt/test_device_tracker.py | 23 +++---- tests/components/cast/test_init.py | 15 +++-- tests/components/cloud/__init__.py | 5 +- tests/components/cloud/test_alexa_config.py | 2 +- tests/components/cloud/test_client.py | 9 +-- tests/components/cloud/test_init.py | 29 ++++----- .../components/config/test_config_entries.py | 23 ++++--- tests/components/darksky/test_sensor.py | 11 ++-- .../components/emulated_roku/test_binding.py | 4 +- tests/components/emulated_roku/test_init.py | 10 +-- tests/components/folder_watcher/test_init.py | 36 +++++------ tests/components/http/test_view.py | 8 +-- tests/components/iqvia/test_config_flow.py | 17 ++--- tests/components/melissa/test_climate.py | 15 ++--- tests/components/melissa/test_init.py | 15 +++-- tests/components/mqtt/test_config_flow.py | 6 +- tests/components/reddit/test_sensor.py | 12 ++-- .../components/seventeentrack/test_sensor.py | 11 +--- tests/components/tplink/test_init.py | 14 ++--- tests/components/twilio/test_init.py | 41 ++++++------ tests/components/updater/test_init.py | 9 +-- tests/components/wake_on_lan/test_init.py | 7 +-- .../test_yandex_transport_sensor.py | 12 ++-- 26 files changed, 154 insertions(+), 262 deletions(-) diff --git a/tests/common.py b/tests/common.py index b76fa23bbf2e47..d6ae25adb5e368 100644 --- a/tests/common.py +++ b/tests/common.py @@ -744,16 +744,12 @@ def mock_open_f(fname, **_): def mock_coro(return_value=None, exception=None): """Return a coro that returns a value or raise an exception.""" - return mock_coro_func(return_value, exception)() - - -def mock_coro_func(return_value=None, exception=None): - """Return a method to create a coro function that returns a value.""" - - if exception: - return AsyncMock(side_effect=exception) - - return AsyncMock(return_value=return_value) + fut = asyncio.Future() + if exception is not None: + fut.set_exception(exception) + else: + fut.set_result(return_value) + return fut @contextmanager @@ -838,52 +834,6 @@ async def get_restore_state_data() -> restore_state.RestoreStateData: hass.data[key] = hass.async_create_task(get_restore_state_data()) -class MockDependency: - """Decorator to mock install a dependency.""" - - def __init__(self, root, *args): - """Initialize decorator.""" - self.root = root - self.submodules = args - - def __enter__(self): - """Start mocking.""" - - def resolve(mock, path): - """Resolve a mock.""" - if not path: - return mock - - return resolve(getattr(mock, path[0]), path[1:]) - - base = MagicMock() - to_mock = { - f"{self.root}.{tom}": resolve(base, tom.split(".")) - for tom in self.submodules - } - to_mock[self.root] = base - - self.patcher = patch.dict("sys.modules", to_mock) - self.patcher.start() - return base - - def __exit__(self, *exc): - """Stop mocking.""" - self.patcher.stop() - return False - - def __call__(self, func): - """Apply decorator.""" - - def run_mocked(*args, **kwargs): - """Run with mocked dependencies.""" - with self as base: - args = list(args) + [base] - func(*args, **kwargs) - - return run_mocked - - class MockEntity(entity.Entity): """Mock Entity class.""" diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 1bef3a5ae122c3..9186402787379e 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1,5 +1,4 @@ """Test for smart home alexa support.""" -from unittest.mock import patch import pytest @@ -39,7 +38,8 @@ reported_properties, ) -from tests.common import async_mock_service, mock_coro +from tests.async_mock import patch +from tests.common import async_mock_service @pytest.fixture @@ -3831,7 +3831,7 @@ async def test_initialize_camera_stream(hass, mock_camera, mock_stream): with patch( "homeassistant.components.demo.camera.DemoCamera.stream_source", - return_value=mock_coro("rtsp://example.local"), + return_value="rtsp://example.local", ), patch( "homeassistant.helpers.network.async_get_external_url", return_value="https://mycamerastream.test", diff --git a/tests/components/almond/test_init.py b/tests/components/almond/test_init.py index d5b8deefd5e616..56286b9186c693 100644 --- a/tests/components/almond/test_init.py +++ b/tests/components/almond/test_init.py @@ -1,6 +1,5 @@ """Tests for Almond set up.""" from time import time -from unittest.mock import patch import pytest @@ -10,7 +9,8 @@ from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.common import MockConfigEntry, async_fire_time_changed, mock_coro +from tests.async_mock import patch +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.fixture(autouse=True) @@ -34,7 +34,6 @@ async def test_set_up_oauth_remote_url(hass, aioclient_mock): with patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - return_value=mock_coro(), ): assert await async_setup_component(hass, "almond", {}) @@ -43,9 +42,7 @@ async def test_set_up_oauth_remote_url(hass, aioclient_mock): with patch("homeassistant.components.almond.ALMOND_SETUP_DELAY", 0), patch( "homeassistant.helpers.network.async_get_external_url", return_value="https://example.nabu.casa", - ), patch( - "pyalmond.WebAlmondAPI.async_create_device", return_value=mock_coro() - ) as mock_create_device: + ), patch("pyalmond.WebAlmondAPI.async_create_device") as mock_create_device: hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() async_fire_time_changed(hass, utcnow()) @@ -69,7 +66,6 @@ async def test_set_up_oauth_no_external_url(hass, aioclient_mock): with patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - return_value=mock_coro(), ), patch("pyalmond.WebAlmondAPI.async_create_device") as mock_create_device: assert await async_setup_component(hass, "almond", {}) @@ -104,9 +100,7 @@ async def test_set_up_local(hass, aioclient_mock): ) entry.add_to_hass(hass) - with patch( - "pyalmond.WebAlmondAPI.async_create_device", return_value=mock_coro() - ) as mock_create_device: + with patch("pyalmond.WebAlmondAPI.async_create_device") as mock_create_device: assert await async_setup_component(hass, "almond", {}) assert entry.state == config_entries.ENTRY_STATE_LOADED diff --git a/tests/components/asuswrt/test_device_tracker.py b/tests/components/asuswrt/test_device_tracker.py index 3954808aa37380..ed733b54d25697 100644 --- a/tests/components/asuswrt/test_device_tracker.py +++ b/tests/components/asuswrt/test_device_tracker.py @@ -1,5 +1,4 @@ """The tests for the ASUSWRT device tracker platform.""" -from unittest.mock import patch from homeassistant.components.asuswrt import ( CONF_DNSMASQ, @@ -10,13 +9,13 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import async_setup_component -from tests.common import mock_coro_func +from tests.async_mock import AsyncMock, patch async def test_password_or_pub_key_required(hass): """Test creating an AsusWRT scanner without a pass or pubkey.""" with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: - AsusWrt().connection.async_connect = mock_coro_func() + AsusWrt().connection.async_connect = AsyncMock() AsusWrt().is_connected = False result = await async_setup_component( hass, DOMAIN, {DOMAIN: {CONF_HOST: "fake_host", CONF_USERNAME: "fake_user"}} @@ -27,7 +26,7 @@ async def test_password_or_pub_key_required(hass): async def test_network_unreachable(hass): """Test creating an AsusWRT scanner without a pass or pubkey.""" with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: - AsusWrt().connection.async_connect = mock_coro_func(exception=OSError) + AsusWrt().connection.async_connect = AsyncMock(side_effect=OSError) AsusWrt().is_connected = False result = await async_setup_component( hass, DOMAIN, {DOMAIN: {CONF_HOST: "fake_host", CONF_USERNAME: "fake_user"}} @@ -39,10 +38,8 @@ async def test_network_unreachable(hass): async def test_get_scanner_with_password_no_pubkey(hass): """Test creating an AsusWRT scanner with a password and no pubkey.""" with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: - AsusWrt().connection.async_connect = mock_coro_func() - AsusWrt().connection.async_get_connected_devices = mock_coro_func( - return_value={} - ) + AsusWrt().connection.async_connect = AsyncMock() + AsusWrt().connection.async_get_connected_devices = AsyncMock(return_value={}) result = await async_setup_component( hass, DOMAIN, @@ -62,7 +59,7 @@ async def test_get_scanner_with_password_no_pubkey(hass): async def test_specify_non_directory_path_for_dnsmasq(hass): """Test creating an AsusWRT scanner with a dnsmasq location which is not a valid directory.""" with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: - AsusWrt().connection.async_connect = mock_coro_func() + AsusWrt().connection.async_connect = AsyncMock() AsusWrt().is_connected = False result = await async_setup_component( hass, @@ -82,10 +79,8 @@ async def test_specify_non_directory_path_for_dnsmasq(hass): async def test_interface(hass): """Test creating an AsusWRT scanner using interface eth1.""" with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: - AsusWrt().connection.async_connect = mock_coro_func() - AsusWrt().connection.async_get_connected_devices = mock_coro_func( - return_value={} - ) + AsusWrt().connection.async_connect = AsyncMock() + AsusWrt().connection.async_get_connected_devices = AsyncMock(return_value={}) result = await async_setup_component( hass, DOMAIN, @@ -106,7 +101,7 @@ async def test_interface(hass): async def test_no_interface(hass): """Test creating an AsusWRT scanner using no interface.""" with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: - AsusWrt().connection.async_connect = mock_coro_func() + AsusWrt().connection.async_connect = AsyncMock() AsusWrt().is_connected = False result = await async_setup_component( hass, diff --git a/tests/components/cast/test_init.py b/tests/components/cast/test_init.py index 6971c071353d5f..8f194668e565e0 100644 --- a/tests/components/cast/test_init.py +++ b/tests/components/cast/test_init.py @@ -1,19 +1,18 @@ """Tests for the Cast config flow.""" -from unittest.mock import patch from homeassistant import config_entries, data_entry_flow from homeassistant.components import cast from homeassistant.setup import async_setup_component -from tests.common import MockDependency, mock_coro +from tests.async_mock import patch async def test_creating_entry_sets_up_media_player(hass): """Test setting up Cast loads the media player.""" with patch( "homeassistant.components.cast.media_player.async_setup_entry", - return_value=mock_coro(True), - ) as mock_setup, MockDependency("pychromecast", "discovery"), patch( + return_value=True, + ) as mock_setup, patch( "pychromecast.discovery.discover_chromecasts", return_value=True ): result = await hass.config_entries.flow.async_init( @@ -34,8 +33,8 @@ async def test_creating_entry_sets_up_media_player(hass): async def test_configuring_cast_creates_entry(hass): """Test that specifying config will create an entry.""" with patch( - "homeassistant.components.cast.async_setup_entry", return_value=mock_coro(True) - ) as mock_setup, MockDependency("pychromecast", "discovery"), patch( + "homeassistant.components.cast.async_setup_entry", return_value=True + ) as mock_setup, patch( "pychromecast.discovery.discover_chromecasts", return_value=True ): await async_setup_component( @@ -49,8 +48,8 @@ async def test_configuring_cast_creates_entry(hass): async def test_not_configuring_cast_not_creates_entry(hass): """Test that no config will not create an entry.""" with patch( - "homeassistant.components.cast.async_setup_entry", return_value=mock_coro(True) - ) as mock_setup, MockDependency("pychromecast", "discovery"), patch( + "homeassistant.components.cast.async_setup_entry", return_value=True + ) as mock_setup, patch( "pychromecast.discovery.discover_chromecasts", return_value=True ): await async_setup_component(hass, cast.DOMAIN, {}) diff --git a/tests/components/cloud/__init__.py b/tests/components/cloud/__init__.py index 571b73e8d09abf..da7c6ff13d0870 100644 --- a/tests/components/cloud/__init__.py +++ b/tests/components/cloud/__init__.py @@ -1,18 +1,17 @@ """Tests for the cloud component.""" -from unittest.mock import patch from homeassistant.components import cloud from homeassistant.components.cloud import const from homeassistant.setup import async_setup_component -from tests.common import mock_coro +from tests.async_mock import AsyncMock, patch async def mock_cloud(hass, config=None): """Mock cloud.""" assert await async_setup_component(hass, cloud.DOMAIN, {"cloud": config or {}}) cloud_inst = hass.data["cloud"] - with patch("hass_nabucasa.Cloud.run_executor", return_value=mock_coro()): + with patch("hass_nabucasa.Cloud.run_executor", AsyncMock(return_value=None)): await cloud_inst.start() diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index c239636e1d3037..b064a5c96054cb 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -89,7 +89,7 @@ def patch_sync_helper(): to_update = [] to_remove = [] - async def sync_helper(to_upd, to_rem): + def sync_helper(to_upd, to_rem): to_update.extend([ent_id for ent_id in to_upd if ent_id not in to_update]) to_remove.extend([ent_id for ent_id in to_rem if ent_id not in to_remove]) return True diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 0ce79daab15c72..21eb59ddc03c8a 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -1,6 +1,4 @@ """Test the cloud.iot module.""" -from unittest.mock import MagicMock, patch - from aiohttp import web import pytest @@ -12,8 +10,7 @@ from . import mock_cloud, mock_cloud_prefs -from tests.async_mock import AsyncMock -from tests.common import mock_coro +from tests.async_mock import AsyncMock, MagicMock, patch from tests.components.alexa import test_smart_home as test_alexa @@ -131,7 +128,7 @@ async def test_handler_google_actions_disabled(hass, mock_cloud_fixture): """Test handler Google Actions when user has disabled it.""" mock_cloud_fixture._prefs[PREF_ENABLE_GOOGLE] = False - with patch("hass_nabucasa.Cloud.start", return_value=mock_coro()): + with patch("hass_nabucasa.Cloud.start"): assert await async_setup_component(hass, "cloud", {}) reqid = "5711642932632160983" @@ -146,7 +143,7 @@ async def test_handler_google_actions_disabled(hass, mock_cloud_fixture): async def test_webhook_msg(hass): """Test webhook msg.""" - with patch("hass_nabucasa.Cloud.start", return_value=mock_coro()): + with patch("hass_nabucasa.Cloud.start"): setup = await async_setup_component(hass, "cloud", {"cloud": {}}) assert setup cloud = hass.data["cloud"] diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 10a7bc38c0572a..e174b080102f83 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -1,5 +1,4 @@ """Test the cloud component.""" -from unittest.mock import patch import pytest @@ -11,12 +10,12 @@ from homeassistant.exceptions import Unauthorized from homeassistant.setup import async_setup_component -from tests.common import mock_coro +from tests.async_mock import patch async def test_constructor_loads_info_from_config(hass): """Test non-dev mode loads info from SERVERS constant.""" - with patch("hass_nabucasa.Cloud.start", return_value=mock_coro()): + with patch("hass_nabucasa.Cloud.start"): result = await async_setup_component( hass, "cloud", @@ -63,17 +62,13 @@ async def test_remote_services(hass, mock_cloud_fixture, hass_read_only_user): assert hass.services.has_service(DOMAIN, "remote_connect") assert hass.services.has_service(DOMAIN, "remote_disconnect") - with patch( - "hass_nabucasa.remote.RemoteUI.connect", return_value=mock_coro() - ) as mock_connect: + with patch("hass_nabucasa.remote.RemoteUI.connect") as mock_connect: await hass.services.async_call(DOMAIN, "remote_connect", blocking=True) assert mock_connect.called assert cloud.client.remote_autostart - with patch( - "hass_nabucasa.remote.RemoteUI.disconnect", return_value=mock_coro() - ) as mock_disconnect: + with patch("hass_nabucasa.remote.RemoteUI.disconnect") as mock_disconnect: await hass.services.async_call(DOMAIN, "remote_disconnect", blocking=True) assert mock_disconnect.called @@ -82,9 +77,9 @@ async def test_remote_services(hass, mock_cloud_fixture, hass_read_only_user): # Test admin access required non_admin_context = Context(user_id=hass_read_only_user.id) - with patch( - "hass_nabucasa.remote.RemoteUI.connect", return_value=mock_coro() - ) as mock_connect, pytest.raises(Unauthorized): + with patch("hass_nabucasa.remote.RemoteUI.connect") as mock_connect, pytest.raises( + Unauthorized + ): await hass.services.async_call( DOMAIN, "remote_connect", blocking=True, context=non_admin_context ) @@ -92,7 +87,7 @@ async def test_remote_services(hass, mock_cloud_fixture, hass_read_only_user): assert mock_connect.called is False with patch( - "hass_nabucasa.remote.RemoteUI.disconnect", return_value=mock_coro() + "hass_nabucasa.remote.RemoteUI.disconnect" ) as mock_disconnect, pytest.raises(Unauthorized): await hass.services.async_call( DOMAIN, "remote_disconnect", blocking=True, context=non_admin_context @@ -103,7 +98,7 @@ async def test_remote_services(hass, mock_cloud_fixture, hass_read_only_user): async def test_startup_shutdown_events(hass, mock_cloud_fixture): """Test if the cloud will start on startup event.""" - with patch("hass_nabucasa.Cloud.stop", return_value=mock_coro()) as mock_stop: + with patch("hass_nabucasa.Cloud.stop") as mock_stop: hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() @@ -114,7 +109,7 @@ async def test_setup_existing_cloud_user(hass, hass_storage): """Test setup with API push default data.""" user = await hass.auth.async_create_system_user("Cloud test") hass_storage[STORAGE_KEY] = {"version": 1, "data": {"cloud_user": user.id}} - with patch("hass_nabucasa.Cloud.start", return_value=mock_coro()): + with patch("hass_nabucasa.Cloud.start"): result = await async_setup_component( hass, "cloud", @@ -148,9 +143,7 @@ async def test_on_connect(hass, mock_cloud_fixture): assert len(hass.states.async_entity_ids("binary_sensor")) == 1 - with patch( - "homeassistant.helpers.discovery.async_load_platform", side_effect=mock_coro - ) as mock_load: + with patch("homeassistant.helpers.discovery.async_load_platform") as mock_load: await cl.iot._on_connect[-1]() await hass.async_block_till_done() diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 358f952e191741..cee86fc046eceb 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -1,7 +1,6 @@ """Test config entries API.""" from collections import OrderedDict -from unittest.mock import patch import pytest import voluptuous as vol @@ -13,10 +12,10 @@ from homeassistant.generated import config_flows from homeassistant.setup import async_setup_component +from tests.async_mock import AsyncMock, patch from tests.common import ( MockConfigEntry, MockModule, - mock_coro_func, mock_entity_platform, mock_integration, ) @@ -228,7 +227,9 @@ async def test_create_account(hass, client): """Test a flow that creates an account.""" mock_entity_platform(hass, "config_flow.test", None) - mock_integration(hass, MockModule("test", async_setup_entry=mock_coro_func(True))) + mock_integration( + hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True)) + ) class TestFlow(core_ce.ConfigFlow): VERSION = 1 @@ -263,7 +264,9 @@ async def async_step_user(self, user_input=None): async def test_two_step_flow(hass, client): """Test we can finish a two step flow.""" - mock_integration(hass, MockModule("test", async_setup_entry=mock_coro_func(True))) + mock_integration( + hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True)) + ) mock_entity_platform(hass, "config_flow.test", None) class TestFlow(core_ce.ConfigFlow): @@ -320,7 +323,9 @@ async def async_step_account(self, user_input=None): async def test_continue_flow_unauth(hass, client, hass_admin_user): """Test we can't finish a two step flow.""" - mock_integration(hass, MockModule("test", async_setup_entry=mock_coro_func(True))) + mock_integration( + hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True)) + ) mock_entity_platform(hass, "config_flow.test", None) class TestFlow(core_ce.ConfigFlow): @@ -516,7 +521,9 @@ async def async_step_init(self, user_input=None): async def test_two_step_options_flow(hass, client): """Test we can finish a two step options flow.""" - mock_integration(hass, MockModule("test", async_setup_entry=mock_coro_func(True))) + mock_integration( + hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True)) + ) class TestFlow(core_ce.ConfigFlow): @staticmethod @@ -666,7 +673,9 @@ async def test_update_entry_nonexisting(hass, hass_ws_client): async def test_ignore_flow(hass, hass_ws_client): """Test we can ignore a flow.""" assert await async_setup_component(hass, "config", {}) - mock_integration(hass, MockModule("test", async_setup_entry=mock_coro_func(True))) + mock_integration( + hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True)) + ) mock_entity_platform(hass, "config_flow.test", None) class TestFlow(core_ce.ConfigFlow): diff --git a/tests/components/darksky/test_sensor.py b/tests/components/darksky/test_sensor.py index 2163a809b5eaee..2dc2a3ff30b6bf 100644 --- a/tests/components/darksky/test_sensor.py +++ b/tests/components/darksky/test_sensor.py @@ -11,7 +11,7 @@ from homeassistant.components.darksky import sensor as darksky from homeassistant.setup import setup_component -from tests.common import MockDependency, get_test_home_assistant, load_fixture +from tests.common import get_test_home_assistant, load_fixture VALID_CONFIG_MINIMAL = { "sensor": { @@ -110,12 +110,11 @@ def tearDown(self): # pylint: disable=invalid-name """Stop everything that was started.""" self.hass.stop() - @MockDependency("forecastio") @patch( "homeassistant.components.darksky.sensor.forecastio.load_forecast", new=load_forecastMock, ) - def test_setup_with_config(self, mock_forecastio): + def test_setup_with_config(self): """Test the platform setup with configuration.""" setup_component(self.hass, "sensor", VALID_CONFIG_MINIMAL) @@ -129,12 +128,11 @@ def test_setup_with_invalid_config(self): state = self.hass.states.get("sensor.dark_sky_summary") assert state is None - @MockDependency("forecastio") @patch( "homeassistant.components.darksky.sensor.forecastio.load_forecast", new=load_forecastMock, ) - def test_setup_with_language_config(self, mock_forecastio): + def test_setup_with_language_config(self): """Test the platform setup with language configuration.""" setup_component(self.hass, "sensor", VALID_CONFIG_LANG_DE) @@ -164,12 +162,11 @@ def test_setup_bad_api_key(self, mock_get_forecast): ) assert not response - @MockDependency("forecastio") @patch( "homeassistant.components.darksky.sensor.forecastio.load_forecast", new=load_forecastMock, ) - def test_setup_with_alerts_config(self, mock_forecastio): + def test_setup_with_alerts_config(self): """Test the platform setup with alert configuration.""" setup_component(self.hass, "sensor", VALID_CONFIG_ALERTS) diff --git a/tests/components/emulated_roku/test_binding.py b/tests/components/emulated_roku/test_binding.py index 53b6217fcbccaa..61c93690548f7f 100644 --- a/tests/components/emulated_roku/test_binding.py +++ b/tests/components/emulated_roku/test_binding.py @@ -14,7 +14,7 @@ EmulatedRoku, ) -from tests.common import mock_coro_func +from tests.async_mock import AsyncMock async def test_events_fired_properly(hass): @@ -39,7 +39,7 @@ def instantiate( nonlocal roku_event_handler roku_event_handler = handler - return Mock(start=mock_coro_func(), close=mock_coro_func()) + return Mock(start=AsyncMock(), close=AsyncMock()) def listener(event): events.append(event) diff --git a/tests/components/emulated_roku/test_init.py b/tests/components/emulated_roku/test_init.py index efdf330a87655b..8d58519ddf9aaa 100644 --- a/tests/components/emulated_roku/test_init.py +++ b/tests/components/emulated_roku/test_init.py @@ -4,14 +4,14 @@ from homeassistant.components import emulated_roku from homeassistant.setup import async_setup_component -from tests.common import mock_coro_func +from tests.async_mock import AsyncMock async def test_config_required_fields(hass): """Test that configuration is successful with required fields.""" with patch.object(emulated_roku, "configured_servers", return_value=[]), patch( "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", - return_value=Mock(start=mock_coro_func(), close=mock_coro_func()), + return_value=Mock(start=AsyncMock(), close=AsyncMock()), ): assert ( await async_setup_component( @@ -36,7 +36,7 @@ async def test_config_already_registered_not_configured(hass): """Test that an already registered name causes the entry to be ignored.""" with patch( "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", - return_value=Mock(start=mock_coro_func(), close=mock_coro_func()), + return_value=Mock(start=AsyncMock(), close=AsyncMock()), ) as instantiate, patch.object( emulated_roku, "configured_servers", return_value=["Emulated Roku Test"] ): @@ -75,7 +75,7 @@ async def test_setup_entry_successful(hass): with patch( "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", - return_value=Mock(start=mock_coro_func(), close=mock_coro_func()), + return_value=Mock(start=AsyncMock(), close=AsyncMock()), ) as instantiate: assert await emulated_roku.async_setup_entry(hass, entry) is True @@ -99,7 +99,7 @@ async def test_unload_entry(hass): with patch( "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", - return_value=Mock(start=mock_coro_func(), close=mock_coro_func()), + return_value=Mock(start=AsyncMock(), close=AsyncMock()), ): assert await emulated_roku.async_setup_entry(hass, entry) is True diff --git a/tests/components/folder_watcher/test_init.py b/tests/components/folder_watcher/test_init.py index 0702e64b4f8c8f..fa2cfc6d3f1041 100644 --- a/tests/components/folder_watcher/test_init.py +++ b/tests/components/folder_watcher/test_init.py @@ -5,8 +5,6 @@ from homeassistant.components import folder_watcher from homeassistant.setup import async_setup_component -from tests.common import MockDependency - async def test_invalid_path_setup(hass): """Test that an invalid path is not set up.""" @@ -29,8 +27,7 @@ async def test_valid_path_setup(hass): ) -@MockDependency("watchdog", "events") -def test_event(mock_watchdog): +def test_event(): """Check that Home Assistant events are fired correctly on watchdog event.""" class MockPatternMatchingEventHandler: @@ -39,17 +36,20 @@ class MockPatternMatchingEventHandler: def __init__(self, patterns): pass - mock_watchdog.events.PatternMatchingEventHandler = MockPatternMatchingEventHandler - hass = Mock() - handler = folder_watcher.create_event_handler(["*"], hass) - handler.on_created( - Mock(is_directory=False, src_path="/hello/world.txt", event_type="created") - ) - assert hass.bus.fire.called - assert hass.bus.fire.mock_calls[0][1][0] == folder_watcher.DOMAIN - assert hass.bus.fire.mock_calls[0][1][1] == { - "event_type": "created", - "path": "/hello/world.txt", - "file": "world.txt", - "folder": "/hello", - } + with patch( + "homeassistant.components.folder_watcher.PatternMatchingEventHandler", + MockPatternMatchingEventHandler, + ): + hass = Mock() + handler = folder_watcher.create_event_handler(["*"], hass) + handler.on_created( + Mock(is_directory=False, src_path="/hello/world.txt", event_type="created") + ) + assert hass.bus.fire.called + assert hass.bus.fire.mock_calls[0][1][0] == folder_watcher.DOMAIN + assert hass.bus.fire.mock_calls[0][1][1] == { + "event_type": "created", + "path": "/hello/world.txt", + "file": "world.txt", + "folder": "/hello", + } diff --git a/tests/components/http/test_view.py b/tests/components/http/test_view.py index 414ad4e8cb0e2d..b298fff6674472 100644 --- a/tests/components/http/test_view.py +++ b/tests/components/http/test_view.py @@ -15,7 +15,7 @@ ) from homeassistant.exceptions import ServiceNotFound, Unauthorized -from tests.common import mock_coro_func +from tests.async_mock import AsyncMock @pytest.fixture @@ -38,7 +38,7 @@ async def test_handling_unauthorized(mock_request): """Test handling unauth exceptions.""" with pytest.raises(HTTPUnauthorized): await request_handler_factory( - Mock(requires_auth=False), mock_coro_func(exception=Unauthorized) + Mock(requires_auth=False), AsyncMock(side_effect=Unauthorized) )(mock_request) @@ -46,7 +46,7 @@ async def test_handling_invalid_data(mock_request): """Test handling unauth exceptions.""" with pytest.raises(HTTPBadRequest): await request_handler_factory( - Mock(requires_auth=False), mock_coro_func(exception=vol.Invalid("yo")) + Mock(requires_auth=False), AsyncMock(side_effect=vol.Invalid("yo")) )(mock_request) @@ -55,5 +55,5 @@ async def test_handling_service_not_found(mock_request): with pytest.raises(HTTPInternalServerError): await request_handler_factory( Mock(requires_auth=False), - mock_coro_func(exception=ServiceNotFound("test", "test")), + AsyncMock(side_effect=ServiceNotFound("test", "test")), )(mock_request) diff --git a/tests/components/iqvia/test_config_flow.py b/tests/components/iqvia/test_config_flow.py index 9345ff5b2adfee..4cc30958b2336a 100644 --- a/tests/components/iqvia/test_config_flow.py +++ b/tests/components/iqvia/test_config_flow.py @@ -1,17 +1,8 @@ """Define tests for the IQVIA config flow.""" -import pytest - from homeassistant import data_entry_flow from homeassistant.components.iqvia import CONF_ZIP_CODE, DOMAIN, config_flow -from tests.common import MockConfigEntry, MockDependency - - -@pytest.fixture -def mock_pyiqvia(): - """Mock the pyiqvia library.""" - with MockDependency("pyiqvia") as mock_pyiqvia_: - yield mock_pyiqvia_ +from tests.common import MockConfigEntry async def test_duplicate_error(hass): @@ -26,7 +17,7 @@ async def test_duplicate_error(hass): assert result["errors"] == {CONF_ZIP_CODE: "identifier_exists"} -async def test_invalid_zip_code(hass, mock_pyiqvia): +async def test_invalid_zip_code(hass): """Test that an invalid ZIP code key throws an error.""" conf = {CONF_ZIP_CODE: "abcde"} @@ -48,7 +39,7 @@ async def test_show_form(hass): assert result["step_id"] == "user" -async def test_step_import(hass, mock_pyiqvia): +async def test_step_import(hass): """Test that the import step works.""" conf = {CONF_ZIP_CODE: "12345"} @@ -61,7 +52,7 @@ async def test_step_import(hass, mock_pyiqvia): assert result["data"] == {CONF_ZIP_CODE: "12345"} -async def test_step_user(hass, mock_pyiqvia): +async def test_step_user(hass): """Test that the user step works.""" conf = {CONF_ZIP_CODE: "12345"} diff --git a/tests/components/melissa/test_climate.py b/tests/components/melissa/test_climate.py index 8976f85f3d12ae..00c565aca8c2ee 100644 --- a/tests/components/melissa/test_climate.py +++ b/tests/components/melissa/test_climate.py @@ -16,7 +16,8 @@ from homeassistant.components.melissa.climate import MelissaClimate from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from tests.common import load_fixture, mock_coro_func +from tests.async_mock import AsyncMock +from tests.common import load_fixture _SERIAL = "12345678" @@ -24,17 +25,17 @@ def melissa_mock(): """Use this to mock the melissa api.""" api = Mock() - api.async_fetch_devices = mock_coro_func( + api.async_fetch_devices = AsyncMock( return_value=json.loads(load_fixture("melissa_fetch_devices.json")) ) - api.async_status = mock_coro_func( + api.async_status = AsyncMock( return_value=json.loads(load_fixture("melissa_status.json")) ) - api.async_cur_settings = mock_coro_func( + api.async_cur_settings = AsyncMock( return_value=json.loads(load_fixture("melissa_cur_settings.json")) ) - api.async_send = mock_coro_func(return_value=True) + api.async_send = AsyncMock(return_value=True) api.STATE_OFF = 0 api.STATE_ON = 1 @@ -276,7 +277,7 @@ async def test_send(hass): await thermostat.async_send({"fan": api.FAN_MEDIUM}) await hass.async_block_till_done() assert SPEED_MEDIUM == thermostat.fan_mode - api.async_send.return_value = mock_coro_func(return_value=False) + api.async_send.return_value = AsyncMock(return_value=False) thermostat._cur_settings = None await thermostat.async_send({"fan": api.FAN_LOW}) await hass.async_block_till_done() @@ -296,7 +297,7 @@ async def test_update(hass): await thermostat.async_update() assert SPEED_LOW == thermostat.fan_mode assert HVAC_MODE_HEAT == thermostat.state - api.async_status = mock_coro_func(exception=KeyError("boom")) + api.async_status = AsyncMock(side_effect=KeyError("boom")) await thermostat.async_update() mocked_warning.assert_called_once_with( "Unable to update entity %s", thermostat.entity_id diff --git a/tests/components/melissa/test_init.py b/tests/components/melissa/test_init.py index 892f4d60a44039..7e174a4f8a0ed1 100644 --- a/tests/components/melissa/test_init.py +++ b/tests/components/melissa/test_init.py @@ -1,23 +1,22 @@ """The test for the Melissa Climate component.""" from homeassistant.components import melissa -from tests.common import MockDependency, mock_coro_func +from tests.async_mock import AsyncMock, patch VALID_CONFIG = {"melissa": {"username": "********", "password": "********"}} async def test_setup(hass): """Test setting up the Melissa component.""" - with MockDependency("melissa") as mocked_melissa: - melissa.melissa = mocked_melissa - mocked_melissa.AsyncMelissa().async_connect = mock_coro_func() + with patch("melissa.AsyncMelissa") as mocked_melissa, patch.object( + melissa, "async_load_platform" + ): + mocked_melissa.return_value.async_connect = AsyncMock() await melissa.async_setup(hass, VALID_CONFIG) - mocked_melissa.AsyncMelissa.assert_called_with( - username="********", password="********" - ) + mocked_melissa.assert_called_with(username="********", password="********") assert melissa.DATA_MELISSA in hass.data assert isinstance( - hass.data[melissa.DATA_MELISSA], type(mocked_melissa.AsyncMelissa()) + hass.data[melissa.DATA_MELISSA], type(mocked_melissa.return_value), ) diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index aa72549152e68b..0990accec9f612 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -1,18 +1,18 @@ """Test config flow.""" -from unittest.mock import patch import pytest from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, mock_coro +from tests.async_mock import patch +from tests.common import MockConfigEntry @pytest.fixture(autouse=True) def mock_finish_setup(): """Mock out the finish setup method.""" with patch( - "homeassistant.components.mqtt.MQTT.async_connect", return_value=mock_coro(True) + "homeassistant.components.mqtt.MQTT.async_connect", return_value=True ) as mock_finish: yield mock_finish diff --git a/tests/components/reddit/test_sensor.py b/tests/components/reddit/test_sensor.py index c44c62fe080509..51de8229347cee 100644 --- a/tests/components/reddit/test_sensor.py +++ b/tests/components/reddit/test_sensor.py @@ -3,7 +3,6 @@ import unittest from unittest.mock import patch -from homeassistant.components.reddit import sensor as reddit_sensor from homeassistant.components.reddit.sensor import ( ATTR_BODY, ATTR_COMMENTS_NUMBER, @@ -20,7 +19,7 @@ from homeassistant.const import CONF_MAXIMUM, CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import setup_component -from tests.common import MockDependency, get_test_home_assistant +from tests.common import get_test_home_assistant VALID_CONFIG = { "sensor": { @@ -157,12 +156,10 @@ def tearDown(self): # pylint: disable=invalid-name """Stop everything that was started.""" self.hass.stop() - @MockDependency("praw") @patch("praw.Reddit", new=MockPraw) - def test_setup_with_valid_config(self, mock_praw): + def test_setup_with_valid_config(self): """Test the platform setup with Reddit configuration.""" - with patch.object(reddit_sensor, "praw", mock_praw): - setup_component(self.hass, "sensor", VALID_CONFIG) + setup_component(self.hass, "sensor", VALID_CONFIG) state = self.hass.states.get("sensor.reddit_worldnews") assert int(state.state) == MOCK_RESULTS_LENGTH @@ -184,9 +181,8 @@ def test_setup_with_valid_config(self, mock_praw): assert state.attributes[CONF_SORT_BY] == "hot" - @MockDependency("praw") @patch("praw.Reddit", new=MockPraw) - def test_setup_with_invalid_config(self, mock_praw): + def test_setup_with_invalid_config(self): """Test the platform setup with invalid Reddit configuration.""" setup_component(self.hass, "sensor", INVALID_SORT_BY_CONFIG) assert not self.hass.states.get("sensor.reddit_worldnews") diff --git a/tests/components/seventeentrack/test_sensor.py b/tests/components/seventeentrack/test_sensor.py index 10ec22f8b67db5..62f93d7cd70338 100644 --- a/tests/components/seventeentrack/test_sensor.py +++ b/tests/components/seventeentrack/test_sensor.py @@ -14,7 +14,7 @@ from homeassistant.setup import async_setup_component from homeassistant.util import utcnow -from tests.common import MockDependency, async_fire_time_changed +from tests.common import async_fire_time_changed VALID_CONFIG_MINIMAL = { "sensor": { @@ -110,15 +110,8 @@ async def summary(self, show_archived: bool = False) -> dict: return self.__class__.summary_data -@pytest.fixture(autouse=True, name="mock_py17track") -def fixture_mock_py17track(): - """Mock py17track dependency.""" - with MockDependency("py17track"): - yield - - @pytest.fixture(autouse=True, name="mock_client") -def fixture_mock_client(mock_py17track): +def fixture_mock_client(): """Mock py17track client.""" with mock.patch( "homeassistant.components.seventeentrack.sensor.SeventeenTrackClient", diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index d8e3f76cac3440..290151b10ccf10 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -16,14 +16,12 @@ from homeassistant.const import CONF_HOST from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, MockDependency, mock_coro - -MOCK_PYHS100 = MockDependency("pyHS100") +from tests.common import MockConfigEntry, mock_coro async def test_creating_entry_tries_discover(hass): """Test setting up does discovery.""" - with MOCK_PYHS100, patch( + with patch( "homeassistant.components.tplink.async_setup_entry", return_value=mock_coro(True), ) as mock_setup, patch( @@ -47,9 +45,7 @@ async def test_creating_entry_tries_discover(hass): async def test_configuring_tplink_causes_discovery(hass): """Test that specifying empty config does discovery.""" - with MOCK_PYHS100, patch( - "homeassistant.components.tplink.common.Discover.discover" - ) as discover: + with patch("homeassistant.components.tplink.common.Discover.discover") as discover: discover.return_value = {"host": 1234} await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() @@ -177,7 +173,7 @@ async def test_is_dimmable(hass): async def test_configuring_discovery_disabled(hass): """Test that discover does not get called when disabled.""" - with MOCK_PYHS100, patch( + with patch( "homeassistant.components.tplink.async_setup_entry", return_value=mock_coro(True), ) as mock_setup, patch( @@ -226,7 +222,7 @@ async def test_platforms_are_initialized(hass): async def test_no_config_creates_no_entry(hass): """Test for when there is no tplink in config.""" - with MOCK_PYHS100, patch( + with patch( "homeassistant.components.tplink.async_setup_entry", return_value=mock_coro(True), ) as mock_setup: diff --git a/tests/components/twilio/test_init.py b/tests/components/twilio/test_init.py index 4c4d499a6d9591..ee7f072a65c7c5 100644 --- a/tests/components/twilio/test_init.py +++ b/tests/components/twilio/test_init.py @@ -5,34 +5,31 @@ from homeassistant.components import twilio from homeassistant.core import callback -from tests.common import MockDependency - async def test_config_flow_registers_webhook(hass, aiohttp_client): """Test setting up Twilio and sending webhook.""" - with MockDependency("twilio", "rest"), MockDependency("twilio", "twiml"): - with patch("homeassistant.util.get_local_ip", return_value="example.com"): - result = await hass.config_entries.flow.async_init( - "twilio", context={"source": "user"} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + with patch("homeassistant.util.get_local_ip", return_value="example.com"): + result = await hass.config_entries.flow.async_init( + "twilio", context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - webhook_id = result["result"].data["webhook_id"] + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + webhook_id = result["result"].data["webhook_id"] - twilio_events = [] + twilio_events = [] - @callback - def handle_event(event): - """Handle Twilio event.""" - twilio_events.append(event) + @callback + def handle_event(event): + """Handle Twilio event.""" + twilio_events.append(event) - hass.bus.async_listen(twilio.RECEIVED_DATA, handle_event) + hass.bus.async_listen(twilio.RECEIVED_DATA, handle_event) - client = await aiohttp_client(hass.http.app) - await client.post(f"/api/webhook/{webhook_id}", data={"hello": "twilio"}) + client = await aiohttp_client(hass.http.app) + await client.post(f"/api/webhook/{webhook_id}", data={"hello": "twilio"}) - assert len(twilio_events) == 1 - assert twilio_events[0].data["webhook_id"] == webhook_id - assert twilio_events[0].data["hello"] == "twilio" + assert len(twilio_events) == 1 + assert twilio_events[0].data["webhook_id"] == webhook_id + assert twilio_events[0].data["hello"] == "twilio" diff --git a/tests/components/updater/test_init.py b/tests/components/updater/test_init.py index 95875828c714ee..0d710907f59462 100644 --- a/tests/components/updater/test_init.py +++ b/tests/components/updater/test_init.py @@ -6,7 +6,7 @@ from homeassistant.setup import async_setup_component from tests.async_mock import patch -from tests.common import MockDependency, mock_component +from tests.common import mock_component NEW_VERSION = "10000.0" MOCK_VERSION = "10.0" @@ -17,13 +17,6 @@ RELEASE_NOTES = "test release notes" -@pytest.fixture(autouse=True) -def mock_distro(): - """Mock distro dep.""" - with MockDependency("distro"): - yield - - @pytest.fixture(autouse=True) def mock_version(): """Mock current version.""" diff --git a/tests/components/wake_on_lan/test_init.py b/tests/components/wake_on_lan/test_init.py index c2ee0930895e17..6eb7afb29f4295 100644 --- a/tests/components/wake_on_lan/test_init.py +++ b/tests/components/wake_on_lan/test_init.py @@ -2,21 +2,18 @@ import pytest import voluptuous as vol -from homeassistant.components import wake_on_lan from homeassistant.components.wake_on_lan import DOMAIN, SERVICE_SEND_MAGIC_PACKET from homeassistant.setup import async_setup_component -from tests.common import MockDependency +from tests.async_mock import patch async def test_send_magic_packet(hass): """Test of send magic packet service call.""" - with MockDependency("wakeonlan") as mocked_wakeonlan: + with patch("homeassistant.components.wake_on_lan.wakeonlan") as mocked_wakeonlan: mac = "aa:bb:cc:dd:ee:ff" bc_ip = "192.168.255.255" - wake_on_lan.wakeonlan = mocked_wakeonlan - await async_setup_component(hass, DOMAIN, {}) await hass.services.async_call( diff --git a/tests/components/yandex_transport/test_yandex_transport_sensor.py b/tests/components/yandex_transport/test_yandex_transport_sensor.py index f0664e4f0459f7..f60e7ba5adf8be 100644 --- a/tests/components/yandex_transport/test_yandex_transport_sensor.py +++ b/tests/components/yandex_transport/test_yandex_transport_sensor.py @@ -8,12 +8,8 @@ from homeassistant.const import CONF_NAME import homeassistant.util.dt as dt_util -from tests.common import ( - MockDependency, - assert_setup_component, - async_setup_component, - load_fixture, -) +from tests.async_mock import patch +from tests.common import assert_setup_component, async_setup_component, load_fixture REPLY = json.loads(load_fixture("yandex_transport_reply.json")) @@ -21,8 +17,8 @@ @pytest.fixture def mock_requester(): """Create a mock ya_ma module and YandexMapsRequester.""" - with MockDependency("ya_ma") as ya_ma: - instance = ya_ma.YandexMapsRequester.return_value + with patch("ya_ma.YandexMapsRequester") as requester: + instance = requester.return_value instance.get_stop_info.return_value = REPLY yield instance From 6056753a9c116f8ef5f24243cfb310ebaff916d8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Apr 2020 16:47:14 -0700 Subject: [PATCH 16/58] Introduce a singleton decorator (#34803) --- homeassistant/core.py | 2 +- homeassistant/helpers/device_registry.py | 28 ++++----------- homeassistant/helpers/entity_registry.py | 28 ++++----------- homeassistant/helpers/singleton.py | 44 ++++++++++++++++++++++++ pylintrc | 2 +- 5 files changed, 60 insertions(+), 44 deletions(-) create mode 100644 homeassistant/helpers/singleton.py diff --git a/homeassistant/core.py b/homeassistant/core.py index 045e56ecb535d1..4d11d970c53f8b 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -83,8 +83,8 @@ block_async_io.enable() fix_threading_exception_logging() -# pylint: disable=invalid-name T = TypeVar("T") +# pylint: disable=invalid-name CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) CALLBACK_TYPE = Callable[[], None] # pylint: enable=invalid-name diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index ef85ac953f6fb9..56b8170b99aba5 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -1,8 +1,7 @@ """Provide a way to connect entities belonging to one device.""" -from asyncio import Event from collections import OrderedDict import logging -from typing import Any, Dict, List, Optional, cast +from typing import Any, Dict, List, Optional import uuid import attr @@ -10,6 +9,7 @@ from homeassistant.core import callback from homeassistant.loader import bind_hass +from .singleton import singleton from .typing import HomeAssistantType # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs @@ -356,26 +356,12 @@ def async_clear_area_id(self, area_id: str) -> None: @bind_hass +@singleton(DATA_REGISTRY) async def async_get_registry(hass: HomeAssistantType) -> DeviceRegistry: - """Return device registry instance.""" - reg_or_evt = hass.data.get(DATA_REGISTRY) - - if not reg_or_evt: - evt = hass.data[DATA_REGISTRY] = Event() - - reg = DeviceRegistry(hass) - await reg.async_load() - - hass.data[DATA_REGISTRY] = reg - evt.set() - return reg - - if isinstance(reg_or_evt, Event): - evt = reg_or_evt - await evt.wait() - return cast(DeviceRegistry, hass.data.get(DATA_REGISTRY)) - - return cast(DeviceRegistry, reg_or_evt) + """Create entity registry.""" + reg = DeviceRegistry(hass) + await reg.async_load() + return reg @callback diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 10de8564fca199..f276cc498503d4 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -7,7 +7,6 @@ registered. Registering a new entity while a timer is in progress resets the timer. """ -import asyncio from collections import OrderedDict import logging from typing import ( @@ -39,6 +38,7 @@ from homeassistant.util import slugify from homeassistant.util.yaml import load_yaml +from .singleton import singleton from .typing import HomeAssistantType if TYPE_CHECKING: @@ -492,26 +492,12 @@ def async_clear_config_entry(self, config_entry: str) -> None: @bind_hass +@singleton(DATA_REGISTRY) async def async_get_registry(hass: HomeAssistantType) -> EntityRegistry: - """Return entity registry instance.""" - reg_or_evt = hass.data.get(DATA_REGISTRY) - - if not reg_or_evt: - evt = hass.data[DATA_REGISTRY] = asyncio.Event() - - reg = EntityRegistry(hass) - await reg.async_load() - - hass.data[DATA_REGISTRY] = reg - evt.set() - return reg - - if isinstance(reg_or_evt, asyncio.Event): - evt = reg_or_evt - await evt.wait() - return cast(EntityRegistry, hass.data.get(DATA_REGISTRY)) - - return cast(EntityRegistry, reg_or_evt) + """Create entity registry.""" + reg = EntityRegistry(hass) + await reg.async_load() + return reg @callback @@ -621,4 +607,4 @@ async def async_migrate_entries( updates = entry_callback(entry) if updates is not None: - ent_reg.async_update_entity(entry.entity_id, **updates) # type: ignore + ent_reg.async_update_entity(entry.entity_id, **updates) diff --git a/homeassistant/helpers/singleton.py b/homeassistant/helpers/singleton.py new file mode 100644 index 00000000000000..8d0f6873c69707 --- /dev/null +++ b/homeassistant/helpers/singleton.py @@ -0,0 +1,44 @@ +"""Helper to help coordinating calls.""" +import asyncio +import functools +from typing import Awaitable, Callable, TypeVar, cast + +from homeassistant.core import HomeAssistant + +T = TypeVar("T") + +FUNC = Callable[[HomeAssistant], Awaitable[T]] + + +def singleton(data_key: str) -> Callable[[FUNC], FUNC]: + """Decorate a function that should be called once per instance. + + Result will be cached and simultaneous calls will be handled. + """ + + def wrapper(func: FUNC) -> FUNC: + """Wrap a function with caching logic.""" + + @functools.wraps(func) + async def wrapped(hass: HomeAssistant) -> T: + obj_or_evt = hass.data.get(data_key) + + if not obj_or_evt: + evt = hass.data[data_key] = asyncio.Event() + + result = await func(hass) + + hass.data[data_key] = result + evt.set() + return cast(T, result) + + if isinstance(obj_or_evt, asyncio.Event): + evt = obj_or_evt + await evt.wait() + return cast(T, hass.data.get(data_key)) + + return cast(T, obj_or_evt) + + return wrapped + + return wrapper diff --git a/pylintrc b/pylintrc index a8118d2391015d..dd763d058865d0 100644 --- a/pylintrc +++ b/pylintrc @@ -8,7 +8,7 @@ persistent=no extension-pkg-whitelist=ciso8601 [BASIC] -good-names=id,i,j,k,ex,Run,_,fp +good-names=id,i,j,k,ex,Run,_,fp,T [MESSAGES CONTROL] # Reasons disabled: From bd72ddda3c7240c00a9002500401ac66d96be182 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 1 May 2020 00:02:55 +0000 Subject: [PATCH 17/58] [ci skip] Translation update --- .../components/adguard/translations/ru.json | 2 +- .../components/airvisual/translations/de.json | 3 +- .../components/airvisual/translations/fi.json | 24 ++++++++++++++ .../components/airvisual/translations/hu.json | 15 +++++++++ .../components/airvisual/translations/nl.json | 31 ++++++++++++++++-- .../components/airvisual/translations/sv.json | 31 ++++++++++++++++++ .../alarm_control_panel/translations/it.json | 6 ++-- .../alarm_control_panel/translations/nl.json | 9 +++++- .../components/atag/translations/fi.json | 14 ++++++++ .../components/atag/translations/hu.json | 11 +++++++ .../components/atag/translations/sv.json | 16 ++++++++++ .../components/august/translations/nl.json | 31 ++++++++++++++++++ .../components/august/translations/sv.json | 24 ++++++++++++++ .../binary_sensor/translations/it.json | 2 +- .../components/braviatv/translations/nl.json | 13 +++++++- .../components/braviatv/translations/ru.json | 2 +- .../components/braviatv/translations/sv.json | 25 +++++++++++++++ .../cert_expiry/translations/nl.json | 5 +++ .../cert_expiry/translations/sv.json | 1 + .../configurator/translations/it.json | 2 +- .../coronavirus/translations/sv.json | 15 +++++++++ .../components/cover/translations/nl.json | 8 +++++ .../components/deconz/translations/sv.json | 6 ++++ .../components/directv/translations/nl.json | 24 ++++++++++++++ .../components/directv/translations/sv.json | 20 ++++++++++++ .../components/doorbird/translations/nl.json | 24 +++++++++++++- .../components/doorbird/translations/sv.json | 19 +++++++++++ .../components/elkm1/translations/nl.json | 11 ++++++- .../components/elkm1/translations/sv.json | 16 ++++++++++ .../components/esphome/translations/en.json | 2 +- .../components/flume/translations/nl.json | 2 ++ .../components/flume/translations/sv.json | 21 ++++++++++++ .../flunearyou/translations/nl.json | 4 ++- .../flunearyou/translations/sv.json | 18 +++++++++++ .../components/freebox/translations/nl.json | 25 +++++++++++++++ .../components/freebox/translations/sv.json | 20 ++++++++++++ .../components/fritzbox/translations/de.json | 3 +- .../components/fritzbox/translations/fi.json | 19 +++++++++++ .../components/fritzbox/translations/nl.json | 3 +- .../components/fritzbox/translations/sv.json | 23 +++++++++++++ .../geonetnz_quakes/translations/sv.json | 3 ++ .../components/griddy/translations/nl.json | 17 ++++++++++ .../components/griddy/translations/sv.json | 8 +++++ .../components/harmony/translations/nl.json | 14 +++++++- .../components/harmony/translations/sv.json | 21 ++++++++++++ .../components/hue/translations/nl.json | 1 + .../components/hue/translations/sv.json | 17 ++++++++++ .../translations/de.json | 24 ++++++++++++++ .../translations/es.json | 24 ++++++++++++++ .../translations/fi.json | 20 ++++++++++++ .../translations/hu.json | 18 +++++++++++ .../translations/it.json | 24 ++++++++++++++ .../translations/nl.json | 24 ++++++++++++++ .../translations/ru.json | 24 ++++++++++++++ .../translations/sv.json | 23 +++++++++++++ .../translations/zh-Hant.json | 24 ++++++++++++++ .../components/icloud/translations/nl.json | 6 ++-- .../components/ipp/translations/nl.json | 22 +++++++++++++ .../components/ipp/translations/ru.json | 2 +- .../components/konnected/translations/nl.json | 17 ++++++++-- .../components/light/translations/it.json | 4 +-- .../components/light/translations/nl.json | 2 ++ .../components/local_ip/translations/nl.json | 3 ++ .../components/local_ip/translations/sv.json | 3 ++ .../components/monoprice/translations/nl.json | 8 ++++- .../components/monoprice/translations/sv.json | 16 ++++++++++ .../components/mqtt/translations/nl.json | 11 +++++++ .../components/mqtt/translations/sv.json | 12 +++++++ .../components/myq/translations/sv.json | 16 ++++++++++ .../components/nexia/translations/nl.json | 8 +++++ .../components/nexia/translations/sv.json | 17 ++++++++++ .../components/notion/translations/nl.json | 3 ++ .../components/notion/translations/sv.json | 3 ++ .../components/nuheat/translations/nl.json | 24 ++++++++++++++ .../components/nuheat/translations/sv.json | 17 ++++++++++ .../components/nut/translations/nl.json | 14 ++++++-- .../components/nut/translations/sv.json | 26 +++++++++++++++ .../components/nws/translations/fi.json | 16 ++++++++++ .../components/nws/translations/nl.json | 14 ++++++-- .../components/nws/translations/sv.json | 19 +++++++++++ .../panasonic_viera/translations/fi.json | 28 ++++++++++++++++ .../panasonic_viera/translations/sv.json | 15 +++++++++ .../components/powerwall/translations/nl.json | 20 ++++++++++++ .../components/powerwall/translations/sv.json | 16 ++++++++++ .../pvpc_hourly_pricing/translations/nl.json | 10 ++++-- .../components/rachio/translations/nl.json | 30 +++++++++++++++++ .../components/rachio/translations/sv.json | 12 +++++++ .../rainmachine/translations/nl.json | 3 ++ .../components/roku/translations/nl.json | 25 +++++++++++++++ .../components/roku/translations/sv.json | 7 ++++ .../components/roomba/translations/nl.json | 32 +++++++++++++++++++ .../components/roomba/translations/sv.json | 29 +++++++++++++++++ .../season/translations/sensor.fi.json | 10 +++--- .../season/translations/sensor.sv.json | 6 ++++ .../components/sense/translations/nl.json | 21 ++++++++++++ .../components/sense/translations/sv.json | 20 ++++++++++++ .../shopping_list/translations/nl.json | 14 ++++++++ .../simplisafe/translations/de.json | 1 + .../simplisafe/translations/nl.json | 13 ++++++++ .../smartthings/translations/nl.json | 6 ++++ .../smartthings/translations/sv.json | 14 ++++++++ .../synology_dsm/translations/fi.json | 14 ++++++++ .../synology_dsm/translations/nl.json | 1 + .../synology_dsm/translations/sv.json | 27 ++++++++++++++++ .../components/tado/translations/nl.json | 24 +++++++++++++- .../components/tado/translations/sv.json | 20 ++++++++++++ .../components/tesla/translations/nl.json | 1 + .../components/timer/translations/it.json | 6 ++-- .../totalconnect/translations/nl.json | 3 +- .../totalconnect/translations/sv.json | 18 +++++++++++ .../components/unifi/translations/nl.json | 16 +++++++--- .../components/unifi/translations/sv.json | 3 +- .../components/vera/translations/nl.json | 13 ++++++-- .../components/vizio/translations/nl.json | 16 ++++++++++ .../components/weather/translations/it.json | 2 +- .../components/wled/translations/ru.json | 2 +- .../xiaomi_miio/translations/de.json | 29 +++++++++++++++++ .../xiaomi_miio/translations/fi.json | 28 ++++++++++++++++ .../xiaomi_miio/translations/hu.json | 22 +++++++++++++ .../xiaomi_miio/translations/nl.json | 12 +++++-- .../xiaomi_miio/translations/sv.json | 28 ++++++++++++++++ .../components/zwave/translations/it.json | 4 +-- 122 files changed, 1683 insertions(+), 57 deletions(-) create mode 100644 homeassistant/components/airvisual/translations/fi.json create mode 100644 homeassistant/components/airvisual/translations/hu.json create mode 100644 homeassistant/components/airvisual/translations/sv.json create mode 100644 homeassistant/components/atag/translations/fi.json create mode 100644 homeassistant/components/atag/translations/hu.json create mode 100644 homeassistant/components/atag/translations/sv.json create mode 100644 homeassistant/components/august/translations/nl.json create mode 100644 homeassistant/components/august/translations/sv.json create mode 100644 homeassistant/components/braviatv/translations/sv.json create mode 100644 homeassistant/components/coronavirus/translations/sv.json create mode 100644 homeassistant/components/directv/translations/nl.json create mode 100644 homeassistant/components/directv/translations/sv.json create mode 100644 homeassistant/components/doorbird/translations/sv.json create mode 100644 homeassistant/components/elkm1/translations/sv.json create mode 100644 homeassistant/components/flume/translations/sv.json create mode 100644 homeassistant/components/flunearyou/translations/sv.json create mode 100644 homeassistant/components/freebox/translations/nl.json create mode 100644 homeassistant/components/freebox/translations/sv.json create mode 100644 homeassistant/components/fritzbox/translations/fi.json create mode 100644 homeassistant/components/fritzbox/translations/sv.json create mode 100644 homeassistant/components/griddy/translations/nl.json create mode 100644 homeassistant/components/griddy/translations/sv.json create mode 100644 homeassistant/components/harmony/translations/sv.json create mode 100644 homeassistant/components/hunterdouglas_powerview/translations/de.json create mode 100644 homeassistant/components/hunterdouglas_powerview/translations/es.json create mode 100644 homeassistant/components/hunterdouglas_powerview/translations/fi.json create mode 100644 homeassistant/components/hunterdouglas_powerview/translations/hu.json create mode 100644 homeassistant/components/hunterdouglas_powerview/translations/it.json create mode 100644 homeassistant/components/hunterdouglas_powerview/translations/nl.json create mode 100644 homeassistant/components/hunterdouglas_powerview/translations/ru.json create mode 100644 homeassistant/components/hunterdouglas_powerview/translations/sv.json create mode 100644 homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json create mode 100644 homeassistant/components/monoprice/translations/sv.json create mode 100644 homeassistant/components/myq/translations/sv.json create mode 100644 homeassistant/components/nexia/translations/sv.json create mode 100644 homeassistant/components/nuheat/translations/nl.json create mode 100644 homeassistant/components/nuheat/translations/sv.json create mode 100644 homeassistant/components/nut/translations/sv.json create mode 100644 homeassistant/components/nws/translations/fi.json create mode 100644 homeassistant/components/nws/translations/sv.json create mode 100644 homeassistant/components/panasonic_viera/translations/fi.json create mode 100644 homeassistant/components/panasonic_viera/translations/sv.json create mode 100644 homeassistant/components/powerwall/translations/nl.json create mode 100644 homeassistant/components/powerwall/translations/sv.json create mode 100644 homeassistant/components/rachio/translations/nl.json create mode 100644 homeassistant/components/rachio/translations/sv.json create mode 100644 homeassistant/components/roku/translations/nl.json create mode 100644 homeassistant/components/roku/translations/sv.json create mode 100644 homeassistant/components/roomba/translations/nl.json create mode 100644 homeassistant/components/roomba/translations/sv.json create mode 100644 homeassistant/components/sense/translations/nl.json create mode 100644 homeassistant/components/sense/translations/sv.json create mode 100644 homeassistant/components/shopping_list/translations/nl.json create mode 100644 homeassistant/components/synology_dsm/translations/fi.json create mode 100644 homeassistant/components/synology_dsm/translations/sv.json create mode 100644 homeassistant/components/tado/translations/sv.json create mode 100644 homeassistant/components/totalconnect/translations/sv.json create mode 100644 homeassistant/components/xiaomi_miio/translations/de.json create mode 100644 homeassistant/components/xiaomi_miio/translations/fi.json create mode 100644 homeassistant/components/xiaomi_miio/translations/hu.json create mode 100644 homeassistant/components/xiaomi_miio/translations/sv.json diff --git a/homeassistant/components/adguard/translations/ru.json b/homeassistant/components/adguard/translations/ru.json index 8c83b8c024c5a8..1287b408544879 100644 --- a/homeassistant/components/adguard/translations/ru.json +++ b/homeassistant/components/adguard/translations/ru.json @@ -23,7 +23,7 @@ "username": "\u041b\u043e\u0433\u0438\u043d", "verify_ssl": "AdGuard Home \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u044d\u0442\u043e\u0442 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f AdGuard Home.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f AdGuard Home.", "title": "AdGuard Home" } } diff --git a/homeassistant/components/airvisual/translations/de.json b/homeassistant/components/airvisual/translations/de.json index a8b2d29656070a..5e66daa3919153 100644 --- a/homeassistant/components/airvisual/translations/de.json +++ b/homeassistant/components/airvisual/translations/de.json @@ -14,7 +14,8 @@ "api_key": "API-Schl\u00fcssel", "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad" - } + }, + "title": "Konfigurieren Sie eine Geografie" }, "node_pro": { "data": { diff --git a/homeassistant/components/airvisual/translations/fi.json b/homeassistant/components/airvisual/translations/fi.json new file mode 100644 index 00000000000000..51426854fdb4f6 --- /dev/null +++ b/homeassistant/components/airvisual/translations/fi.json @@ -0,0 +1,24 @@ +{ + "config": { + "step": { + "geography": { + "data": { + "api_key": "API-avain", + "latitude": "Leveysaste", + "longitude": "Pituusaste" + } + }, + "node_pro": { + "data": { + "password": "Salasana" + } + }, + "user": { + "data": { + "cloud_api": "Maantieteellinen sijainti", + "type": "Integrointityyppi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/hu.json b/homeassistant/components/airvisual/translations/hu.json new file mode 100644 index 00000000000000..f1c4dadef11306 --- /dev/null +++ b/homeassistant/components/airvisual/translations/hu.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "general_error": "Ismeretlen hiba t\u00f6rt\u00e9nt." + }, + "step": { + "geography": { + "data": { + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/nl.json b/homeassistant/components/airvisual/translations/nl.json index 62a9919e5f383c..97b083b91fa5fc 100644 --- a/homeassistant/components/airvisual/translations/nl.json +++ b/homeassistant/components/airvisual/translations/nl.json @@ -1,7 +1,12 @@ { "config": { + "abort": { + "already_configured": "Deze co\u00f6rdinaten of Node / Pro ID zijn al geregistreerd." + }, "error": { - "general_error": "Er is een onbekende fout opgetreden." + "general_error": "Er is een onbekende fout opgetreden.", + "invalid_api_key": "Ongeldige API-sleutel opgegeven.", + "unable_to_connect": "Kan geen verbinding maken met Node / Pro-apparaat." }, "step": { "geography": { @@ -10,19 +15,39 @@ "latitude": "Breedtegraad", "longitude": "Lengtegraad" }, + "description": "Gebruik de AirVisual cloud API om een geografische locatie te bewaken.", "title": "Configureer een geografie" }, "node_pro": { "data": { "ip_address": "IP adres/hostname van unit", "password": "Wachtwoord van unit" - } + }, + "description": "Monitor een persoonlijke AirVisual-eenheid. Het wachtwoord kan worden opgehaald uit de gebruikersinterface van het apparaat.", + "title": "Configureer een AirVisual Node / Pro" }, "user": { "data": { + "api_key": "API-sleutel", "cloud_api": "Geografische ligging", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "node_pro": "AirVisual Node Pro", "type": "Integratietype" - } + }, + "description": "Kies welk type AirVisual-gegevens u wilt bewaken.", + "title": "Configureer AirVisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Toon gecontroleerde geografie op de kaart" + }, + "description": "Stel verschillende opties in voor de AirVisual-integratie.", + "title": "Configureer AirVisual" } } } diff --git a/homeassistant/components/airvisual/translations/sv.json b/homeassistant/components/airvisual/translations/sv.json new file mode 100644 index 00000000000000..12911fa76d7e91 --- /dev/null +++ b/homeassistant/components/airvisual/translations/sv.json @@ -0,0 +1,31 @@ +{ + "config": { + "error": { + "general_error": "Ett ok\u00e4nt fel intr\u00e4ffade." + }, + "step": { + "geography": { + "data": { + "api_key": "API-nyckel", + "latitude": "Latitud", + "longitude": "Longitud" + } + }, + "node_pro": { + "data": { + "ip_address": "Enhets IP-adress / v\u00e4rdnamn", + "password": "Enhetsl\u00f6senord" + } + }, + "user": { + "data": { + "api_key": "API-nyckel", + "cloud_api": "Geografisk Plats", + "latitude": "Latitud", + "longitude": "Longitud", + "type": "Integrationstyp" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/translations/it.json b/homeassistant/components/alarm_control_panel/translations/it.json index a365c5cd35b2ad..1574f88541b20d 100644 --- a/homeassistant/components/alarm_control_panel/translations/it.json +++ b/homeassistant/components/alarm_control_panel/translations/it.json @@ -26,12 +26,12 @@ "_": { "armed": "Attivo", "armed_away": "Attivo fuori casa", - "armed_custom_bypass": "Attivo con bypass", + "armed_custom_bypass": "Attivo con bypass personalizzato", "armed_home": "Attivo in casa", "armed_night": "Attivo Notte", - "arming": "In attivazione", + "arming": "In Attivazione", "disarmed": "Disattivo", - "disarming": "In disattivazione", + "disarming": "In Disattivazione", "pending": "In sospeso", "triggered": "Attivato" } diff --git a/homeassistant/components/alarm_control_panel/translations/nl.json b/homeassistant/components/alarm_control_panel/translations/nl.json index a1a00e7c9e3269..15b5fd8457c2ef 100644 --- a/homeassistant/components/alarm_control_panel/translations/nl.json +++ b/homeassistant/components/alarm_control_panel/translations/nl.json @@ -7,6 +7,13 @@ "disarm": "Uitschakelen {entity_name}", "trigger": "Trigger {entity_name}" }, + "condition_type": { + "is_armed_away": "{entity_name} afwezig ingeschakeld", + "is_armed_home": "{entity_name} thuis ingeschakeld", + "is_armed_night": "{entity_name} nachtstand ingeschakeld", + "is_disarmed": "{entity_name} is uitgeschakeld", + "is_triggered": "{entity_name} wordt geactiveerd" + }, "trigger_type": { "armed_away": "{entity_name} afwezig ingeschakeld", "armed_home": "{entity_name} thuis ingeschakeld", @@ -18,7 +25,7 @@ "state": { "_": { "armed": "Ingeschakeld", - "armed_away": "Ingeschakeld afwezig", + "armed_away": "Afwezig Ingeschakeld", "armed_custom_bypass": "Ingeschakeld met overbrugging(en)", "armed_home": "Ingeschakeld thuis", "armed_night": "Ingeschakeld nacht", diff --git a/homeassistant/components/atag/translations/fi.json b/homeassistant/components/atag/translations/fi.json new file mode 100644 index 00000000000000..0483d8d28045ac --- /dev/null +++ b/homeassistant/components/atag/translations/fi.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Palvelin", + "port": "Portti (10000)" + }, + "title": "Yhdist\u00e4 laitteeseen" + } + } + }, + "title": "Atag" +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/hu.json b/homeassistant/components/atag/translations/hu.json new file mode 100644 index 00000000000000..22687b6944a506 --- /dev/null +++ b/homeassistant/components/atag/translations/hu.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Port (10000)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/sv.json b/homeassistant/components/atag/translations/sv.json new file mode 100644 index 00000000000000..938a0191ee3676 --- /dev/null +++ b/homeassistant/components/atag/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "connection_error": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port (10000)" + }, + "title": "Anslut till enheten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/nl.json b/homeassistant/components/august/translations/nl.json new file mode 100644 index 00000000000000..1697f634d9a82e --- /dev/null +++ b/homeassistant/components/august/translations/nl.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Account al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "login_method": "Aanmeldmethode", + "password": "Wachtwoord", + "timeout": "Time-out (seconden)", + "username": "Gebruikersnaam" + }, + "description": "Als de aanmeldingsmethode 'e-mail' is, is gebruikersnaam het e-mailadres. Als de aanmeldingsmethode 'telefoon' is, is gebruikersnaam het telefoonnummer in de indeling '+ NNNNNNNNN'.", + "title": "Stel een augustus-account in" + }, + "validation": { + "data": { + "code": "Verificatiecode" + }, + "description": "Controleer je {login_method} ( {username} ) en voer de onderstaande verificatiecode in", + "title": "Tweestapsverificatie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/sv.json b/homeassistant/components/august/translations/sv.json new file mode 100644 index 00000000000000..df72f5daaf33fe --- /dev/null +++ b/homeassistant/components/august/translations/sv.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Kontot har redan konfigurerats" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "login_method": "Inloggningsmetod", + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + }, + "validation": { + "title": "Tv\u00e5faktorsautentisering" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/it.json b/homeassistant/components/binary_sensor/translations/it.json index b0bb47b77d80e1..955fe525ad1719 100644 --- a/homeassistant/components/binary_sensor/translations/it.json +++ b/homeassistant/components/binary_sensor/translations/it.json @@ -140,7 +140,7 @@ }, "opening": { "off": "Chiuso", - "on": "Aperta" + "on": "Aperto" }, "presence": { "off": "Fuori casa", diff --git a/homeassistant/components/braviatv/translations/nl.json b/homeassistant/components/braviatv/translations/nl.json index b5e59d830f8ebd..ba09fbca3a3da3 100644 --- a/homeassistant/components/braviatv/translations/nl.json +++ b/homeassistant/components/braviatv/translations/nl.json @@ -20,7 +20,18 @@ "data": { "host": "Hostnaam of IP-adres van tv" }, - "description": "Stel Sony Bravia TV-integratie in. Als je problemen hebt met de configuratie ga dan naar: https://www.home-assistant.io/integrations/braviatv \n\nZorg ervoor dat uw tv is ingeschakeld." + "description": "Stel Sony Bravia TV-integratie in. Als je problemen hebt met de configuratie ga dan naar: https://www.home-assistant.io/integrations/braviatv \n\nZorg ervoor dat uw tv is ingeschakeld.", + "title": "Sony Bravia TV" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "ignored_sources": "Lijst met genegeerde bronnen" + }, + "title": "Opties voor Sony Bravia TV" } } } diff --git a/homeassistant/components/braviatv/translations/ru.json b/homeassistant/components/braviatv/translations/ru.json index 33e8d71bd641e3..aa07f4d8fbcebd 100644 --- a/homeassistant/components/braviatv/translations/ru.json +++ b/homeassistant/components/braviatv/translations/ru.json @@ -20,7 +20,7 @@ "data": { "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441" }, - "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0435\u0439 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438:\nhttps://www.home-assistant.io/integrations/braviatv", + "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0435\u0439 \u043f\u043e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438:\nhttps://www.home-assistant.io/integrations/braviatv", "title": "\u0422\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Sony Bravia" } } diff --git a/homeassistant/components/braviatv/translations/sv.json b/homeassistant/components/braviatv/translations/sv.json new file mode 100644 index 00000000000000..6ec160e799a081 --- /dev/null +++ b/homeassistant/components/braviatv/translations/sv.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Den h\u00e4r TV:n \u00e4r redan konfigurerad" + }, + "error": { + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress.", + "unsupported_model": "Den h\u00e4r tv modellen st\u00f6ds inte." + }, + "step": { + "authorize": { + "data": { + "pin": "Pin-kod" + }, + "title": "Auktorisera Sony Bravia TV" + }, + "user": { + "data": { + "host": "V\u00e4rdnamn eller IP-adress f\u00f6r TV" + }, + "title": "Sony Bravia TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/translations/nl.json b/homeassistant/components/cert_expiry/translations/nl.json index c33d4c06e6fccc..d844d28e62fe8a 100644 --- a/homeassistant/components/cert_expiry/translations/nl.json +++ b/homeassistant/components/cert_expiry/translations/nl.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "Deze combinatie van host en poort is al geconfigureerd", + "import_failed": "Importeren vanuit configuratie is mislukt" + }, "error": { + "connection_refused": "Verbinding geweigerd bij verbinding met host", "connection_timeout": "Time-out bij verbinding maken met deze host", "resolve_failed": "Deze host kon niet gevonden worden" }, diff --git a/homeassistant/components/cert_expiry/translations/sv.json b/homeassistant/components/cert_expiry/translations/sv.json index 8449db1ec7a2da..23703f11e5bedf 100644 --- a/homeassistant/components/cert_expiry/translations/sv.json +++ b/homeassistant/components/cert_expiry/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "error": { + "connection_refused": "Anslutningen blev tillbakavisad under anslutning till v\u00e4rd.", "connection_timeout": "Timeout vid anslutning till den h\u00e4r v\u00e4rden", "resolve_failed": "Denna v\u00e4rd kan inte resolveras" }, diff --git a/homeassistant/components/configurator/translations/it.json b/homeassistant/components/configurator/translations/it.json index 3e17f84d1c87bb..b8610b76d9d310 100644 --- a/homeassistant/components/configurator/translations/it.json +++ b/homeassistant/components/configurator/translations/it.json @@ -1,7 +1,7 @@ { "state": { "_": { - "configure": "Configura", + "configure": "Configurare", "configured": "Configurato" } }, diff --git a/homeassistant/components/coronavirus/translations/sv.json b/homeassistant/components/coronavirus/translations/sv.json new file mode 100644 index 00000000000000..7e6686c2a04280 --- /dev/null +++ b/homeassistant/components/coronavirus/translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Detta land \u00e4r redan konfigurerat." + }, + "step": { + "user": { + "data": { + "country": "Land" + }, + "title": "V\u00e4lj ett land att \u00f6vervaka" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/translations/nl.json b/homeassistant/components/cover/translations/nl.json index f29132c3f18cb0..7d68d78641e54e 100644 --- a/homeassistant/components/cover/translations/nl.json +++ b/homeassistant/components/cover/translations/nl.json @@ -1,5 +1,13 @@ { "device_automation": { + "action_type": { + "close": "Sluit {entity_name}", + "close_tilt": "Sluit de kanteling van {entity_name}", + "open": "Open {entity_name}", + "open_tilt": "Open de kanteling {entity_name}", + "set_position": "Stel de positie van {entity_name} in", + "set_tilt_position": "Stel de {entity_name} kantelpositie in" + }, "condition_type": { "is_closed": "{entity_name} is gesloten", "is_closing": "{entity_name} wordt gesloten", diff --git a/homeassistant/components/deconz/translations/sv.json b/homeassistant/components/deconz/translations/sv.json index 559581b8ad8f72..e7e0f5d917f55d 100644 --- a/homeassistant/components/deconz/translations/sv.json +++ b/homeassistant/components/deconz/translations/sv.json @@ -29,6 +29,12 @@ "host": "V\u00e4rd", "port": "Port (standardv\u00e4rde: '80')" } + }, + "manual_input": { + "data": { + "host": "V\u00e4rd", + "port": "Port" + } } } }, diff --git a/homeassistant/components/directv/translations/nl.json b/homeassistant/components/directv/translations/nl.json new file mode 100644 index 00000000000000..b6635311064bbf --- /dev/null +++ b/homeassistant/components/directv/translations/nl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "DirecTV-ontvanger is al geconfigureerd", + "unknown": "Onverwachte fout" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw" + }, + "flow_title": "DirecTV: {name}", + "step": { + "ssdp_confirm": { + "description": "Wilt u {name} instellen?", + "title": "Maak verbinding met de DirecTV-ontvanger" + }, + "user": { + "data": { + "host": "Host- of IP-adres" + }, + "title": "Maak verbinding met de DirecTV-ontvanger" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/translations/sv.json b/homeassistant/components/directv/translations/sv.json new file mode 100644 index 00000000000000..c42c03d9944c44 --- /dev/null +++ b/homeassistant/components/directv/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen" + }, + "step": { + "ssdp_confirm": { + "description": "Do vill du konfigurera {name}?" + }, + "user": { + "data": { + "host": "V\u00e4rd eller IP-adress" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/translations/nl.json b/homeassistant/components/doorbird/translations/nl.json index 36723f36dc07f7..625367484b0ac0 100644 --- a/homeassistant/components/doorbird/translations/nl.json +++ b/homeassistant/components/doorbird/translations/nl.json @@ -1,13 +1,35 @@ { "config": { + "abort": { + "already_configured": "Deze DoorBird is al geconfigureerd", + "link_local_address": "Link-lokale adressen worden niet ondersteund", + "not_doorbird_device": "Dit apparaat is geen DoorBird" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, "flow_title": "DoorBird {name} ({host})", "step": { "user": { "data": { + "host": "Host (IP-adres)", "name": "Apparaatnaam", "password": "Wachtwoord", "username": "Gebruikersnaam" - } + }, + "title": "Maak verbinding met de DoorBird" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "events": "Door komma's gescheiden lijst met gebeurtenissen." + }, + "description": "Voeg een door komma's gescheiden evenementnaam toe voor elk evenement dat u wilt volgen. Nadat je ze hier hebt ingevoerd, gebruik je de DoorBird-app om ze toe te wijzen aan een specifiek evenement. Zie de documentatie op https://www.home-assistant.io/integrations/doorbird/#events. Voorbeeld: iemand_drukte_knop, beweging" } } } diff --git a/homeassistant/components/doorbird/translations/sv.json b/homeassistant/components/doorbird/translations/sv.json new file mode 100644 index 00000000000000..8025b956a17fed --- /dev/null +++ b/homeassistant/components/doorbird/translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd (IP-adress)", + "name": "Enhetsnamn", + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/translations/nl.json b/homeassistant/components/elkm1/translations/nl.json index 3f643bc67e7df3..9e7adf71c4b14e 100644 --- a/homeassistant/components/elkm1/translations/nl.json +++ b/homeassistant/components/elkm1/translations/nl.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "address_already_configured": "Een ElkM1 met dit adres is al geconfigureerd", + "already_configured": "Een ElkM1 met dit voorvoegsel is al geconfigureerd" + }, "error": { "cannot_connect": "Verbinding mislukt, probeer het opnieuw", "invalid_auth": "Ongeldige authenticatie", @@ -8,10 +12,15 @@ "step": { "user": { "data": { + "address": "Het IP-adres of domein of seri\u00eble poort bij verbinding via serieel.", "password": "Wachtwoord (alleen beveiligd).", + "prefix": "Een uniek voorvoegsel (laat dit leeg als u maar \u00e9\u00e9n ElkM1 heeft).", "protocol": "Protocol", + "temperature_unit": "De temperatuureenheid die ElkM1 gebruikt.", "username": "Gebruikersnaam (alleen beveiligd)." - } + }, + "description": "De adresreeks moet de vorm 'adres [: poort]' hebben voor 'veilig' en 'niet-beveiligd'. Voorbeeld: '192.168.1.1'. De poort is optioneel en is standaard 2101 voor 'niet beveiligd' en 2601 voor 'beveiligd'. Voor het seri\u00eble protocol moet het adres de vorm 'tty [: baud]' hebben. Voorbeeld: '/ dev / ttyS1'. De baud is optioneel en is standaard ingesteld op 115200.", + "title": "Maak verbinding met Elk-M1 Control" } } } diff --git a/homeassistant/components/elkm1/translations/sv.json b/homeassistant/components/elkm1/translations/sv.json new file mode 100644 index 00000000000000..23a7d475a6fa5b --- /dev/null +++ b/homeassistant/components/elkm1/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "protocol": "Protokoll" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/en.json b/homeassistant/components/esphome/translations/en.json index 3f695e6f183b2e..3cc24dea78e7d7 100644 --- a/homeassistant/components/esphome/translations/en.json +++ b/homeassistant/components/esphome/translations/en.json @@ -32,4 +32,4 @@ } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/flume/translations/nl.json b/homeassistant/components/flume/translations/nl.json index 830bdc84e55e62..d176eb133656c9 100644 --- a/homeassistant/components/flume/translations/nl.json +++ b/homeassistant/components/flume/translations/nl.json @@ -11,10 +11,12 @@ "step": { "user": { "data": { + "client_id": "Client-id", "client_secret": "Client Secret", "password": "Wachtwoord", "username": "Gebruikersnaam" }, + "description": "Om toegang te krijgen tot de Flume Personal API, moet je een 'Client ID' en 'Client Secret' aanvragen op https://portal.flumetech.com/settings#token", "title": "Verbind met uw Flume account" } } diff --git a/homeassistant/components/flume/translations/sv.json b/homeassistant/components/flume/translations/sv.json new file mode 100644 index 00000000000000..7bff51a3c831a0 --- /dev/null +++ b/homeassistant/components/flume/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Det h\u00e4r kontot har redan konfigurerats." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "client_id": "Klient ID", + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/nl.json b/homeassistant/components/flunearyou/translations/nl.json index 3a71a79d13763a..c3f83fc93bf22f 100644 --- a/homeassistant/components/flunearyou/translations/nl.json +++ b/homeassistant/components/flunearyou/translations/nl.json @@ -11,7 +11,9 @@ "data": { "latitude": "Breedtegraad", "longitude": "Lengtegraad" - } + }, + "description": "Bewaak op gebruikers gebaseerde en CDC-repots voor een paar co\u00f6rdinaten.", + "title": "Configureer \nFlu Near You" } } } diff --git a/homeassistant/components/flunearyou/translations/sv.json b/homeassistant/components/flunearyou/translations/sv.json new file mode 100644 index 00000000000000..adcf6008c1e153 --- /dev/null +++ b/homeassistant/components/flunearyou/translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Dessa koordinater \u00e4r redan registrerade." + }, + "error": { + "general_error": "Ett ok\u00e4nt fel intr\u00e4ffade." + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/nl.json b/homeassistant/components/freebox/translations/nl.json new file mode 100644 index 00000000000000..62c69997e17626 --- /dev/null +++ b/homeassistant/components/freebox/translations/nl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Host is al geconfigureerd." + }, + "error": { + "connection_failed": "Verbinding mislukt, probeer het opnieuw", + "register_failed": "Registratie is mislukt, probeer het opnieuw", + "unknown": "Onbekende fout: probeer het later nog eens" + }, + "step": { + "link": { + "description": "Klik op \"Verzenden\" en tik vervolgens op de rechterpijl op de router om Freebox te registreren bij Home Assistant. \n\n ! [Locatie van knop op de router] (/ static / images / config_freebox.png)", + "title": "Freebox-router koppelen" + }, + "user": { + "data": { + "host": "Host", + "port": "Poort" + }, + "title": "Freebox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/sv.json b/homeassistant/components/freebox/translations/sv.json new file mode 100644 index 00000000000000..6c6cc5c64ecee3 --- /dev/null +++ b/homeassistant/components/freebox/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "V\u00e4rden \u00e4r redan konfigurerad." + }, + "error": { + "connection_failed": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "register_failed": "Misslyckades med att registrera, v\u00e4nligen f\u00f6rs\u00f6k igen", + "unknown": "Ok\u00e4nt fel: f\u00f6rs\u00f6k igen senare" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/de.json b/homeassistant/components/fritzbox/translations/de.json index 5f16553c64c997..1a2e046d9333c5 100644 --- a/homeassistant/components/fritzbox/translations/de.json +++ b/homeassistant/components/fritzbox/translations/de.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Diese AVM FRITZ! Box ist bereits konfiguriert.", "already_in_progress": "Die Konfiguration der AVM FRITZ! Box ist bereits in Bearbeitung.", - "not_found": "Keine unterst\u00fctzte AVM FRITZ! Box im Netzwerk gefunden." + "not_found": "Keine unterst\u00fctzte AVM FRITZ! Box im Netzwerk gefunden.", + "not_supported": "Verbunden mit AVM FRITZ! Box, kann jedoch keine Smart Home-Ger\u00e4te steuern." }, "error": { "auth_failed": "Benutzername und/oder Passwort sind falsch." diff --git a/homeassistant/components/fritzbox/translations/fi.json b/homeassistant/components/fritzbox/translations/fi.json new file mode 100644 index 00000000000000..bb4fb818a67797 --- /dev/null +++ b/homeassistant/components/fritzbox/translations/fi.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "confirm": { + "data": { + "password": "Salasana", + "username": "K\u00e4ytt\u00e4j\u00e4tunnus" + } + }, + "user": { + "data": { + "password": "Salasana", + "username": "K\u00e4ytt\u00e4j\u00e4tunnus" + }, + "title": "AVM FRITZ!Box" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/nl.json b/homeassistant/components/fritzbox/translations/nl.json index 3d8c94ab5b6f3f..874f7add95e97b 100644 --- a/homeassistant/components/fritzbox/translations/nl.json +++ b/homeassistant/components/fritzbox/translations/nl.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Deze AVM FRITZ!Box is al geconfigureerd.", "already_in_progress": "AVM FRITZ!Box configuratie is al bezig.", - "not_found": "Geen ondersteunde AVM FRITZ!Box gevonden op het netwerk." + "not_found": "Geen ondersteunde AVM FRITZ!Box gevonden op het netwerk.", + "not_supported": "Verbonden met AVM FRITZ! Box, maar het kan geen Smart Home-apparaten bedienen." }, "error": { "auth_failed": "Ongeldige gebruikersnaam of wachtwoord" diff --git a/homeassistant/components/fritzbox/translations/sv.json b/homeassistant/components/fritzbox/translations/sv.json new file mode 100644 index 00000000000000..1ed4e4fc3d8322 --- /dev/null +++ b/homeassistant/components/fritzbox/translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "auth_failed": "Anv\u00e4ndarnamn och/eller l\u00f6senord \u00e4r fel." + }, + "step": { + "confirm": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + }, + "description": "Do vill du konfigurera {name}?" + }, + "user": { + "data": { + "host": "V\u00e4rd eller IP-adress", + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/translations/sv.json b/homeassistant/components/geonetnz_quakes/translations/sv.json index b1040e9bc23f17..feb654c267c7b7 100644 --- a/homeassistant/components/geonetnz_quakes/translations/sv.json +++ b/homeassistant/components/geonetnz_quakes/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Plats \u00e4r redan konfigurerad." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/griddy/translations/nl.json b/homeassistant/components/griddy/translations/nl.json new file mode 100644 index 00000000000000..9227d4702abae7 --- /dev/null +++ b/homeassistant/components/griddy/translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Deze laadzone is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "description": "Uw Load Zone staat op uw Griddy account onder \"Account > Meter > Load Zone\".", + "title": "Stel uw Griddy Load Zone in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/sv.json b/homeassistant/components/griddy/translations/sv.json new file mode 100644 index 00000000000000..e9ddacf2714959 --- /dev/null +++ b/homeassistant/components/griddy/translations/sv.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "unknown": "Ov\u00e4ntat fel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/nl.json b/homeassistant/components/harmony/translations/nl.json index a896cab08770a3..63d8026d9c2d02 100644 --- a/homeassistant/components/harmony/translations/nl.json +++ b/homeassistant/components/harmony/translations/nl.json @@ -4,7 +4,8 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw" + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "unknown": "Onverwachte fout" }, "flow_title": "Logitech Harmony Hub {name}", "step": { @@ -20,5 +21,16 @@ "title": "Logitech Harmony Hub instellen" } } + }, + "options": { + "step": { + "init": { + "data": { + "activity": "De standaardactiviteit die moet worden uitgevoerd wanneer er geen is opgegeven.", + "delay_secs": "De vertraging tussen het verzenden van opdrachten." + }, + "description": "Pas de Harmony Hub-opties aan" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/sv.json b/homeassistant/components/harmony/translations/sv.json new file mode 100644 index 00000000000000..6e9c861763ba3f --- /dev/null +++ b/homeassistant/components/harmony/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "link": { + "description": "Do vill du konfigurera {name} ({host})?" + }, + "user": { + "data": { + "host": "V\u00e4rdnamn eller IP-adress" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/translations/nl.json b/homeassistant/components/hue/translations/nl.json index 493809dca07c19..bfe70ae53ab2c3 100644 --- a/homeassistant/components/hue/translations/nl.json +++ b/homeassistant/components/hue/translations/nl.json @@ -33,6 +33,7 @@ "button_2": "Tweede knop", "button_3": "Derde knop", "button_4": "Vierde knop", + "dim_down": "Dim omlaag", "dim_up": "Dim omhoog", "double_buttons_1_3": "Eerste en derde knop", "double_buttons_2_4": "Tweede en vierde knop", diff --git a/homeassistant/components/hue/translations/sv.json b/homeassistant/components/hue/translations/sv.json index 894c4f9f988516..80f7b179692dac 100644 --- a/homeassistant/components/hue/translations/sv.json +++ b/homeassistant/components/hue/translations/sv.json @@ -26,5 +26,22 @@ "title": "L\u00e4nka hub" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "F\u00f6rsta knappen", + "button_2": "Andra knappen", + "button_3": "Tredje knappen", + "button_4": "Fj\u00e4rde knappen", + "dim_down": "Dimma ned", + "dim_up": "Dimma upp", + "turn_off": "St\u00e4ng av", + "turn_on": "Starta" + }, + "trigger_type": { + "remote_button_long_release": "\"{subtype}\" knappen sl\u00e4pptes efter ett l\u00e5ngt tryck", + "remote_button_short_press": "\"{subtype}\" knappen nedtryckt", + "remote_button_short_release": "\"{subtype}\"-knappen sl\u00e4ppt" + } } } \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/de.json b/homeassistant/components/hunterdouglas_powerview/translations/de.json new file mode 100644 index 00000000000000..50dc39b3d98cf6 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/de.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "link": { + "description": "M\u00f6chten Sie {name} ({host}) einrichten?", + "title": "Stellen Sie eine Verbindung zum PowerView Hub her" + }, + "user": { + "data": { + "host": "IP-Adresse" + }, + "title": "Stellen Sie eine Verbindung zum PowerView Hub her" + } + } + }, + "title": "Hunter Douglas PowerView" +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/es.json b/homeassistant/components/hunterdouglas_powerview/translations/es.json new file mode 100644 index 00000000000000..46e216976bd64e --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/es.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, por favor, int\u00e9ntalo de nuevo", + "unknown": "Error inesperado" + }, + "step": { + "link": { + "description": "\u00bfQuieres configurar {name} ({host})?", + "title": "Conectar con el PowerView Hub" + }, + "user": { + "data": { + "host": "Direcci\u00f3n IP" + }, + "title": "Conectar con el PowerView Hub" + } + } + }, + "title": "Hunter Douglas PowerView" +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/fi.json b/homeassistant/components/hunterdouglas_powerview/translations/fi.json new file mode 100644 index 00000000000000..86ad64c1692631 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/fi.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Laite on jo m\u00e4\u00e4ritetty" + }, + "error": { + "cannot_connect": "Yhteyden muodostaminen ep\u00e4onnistui. Yrit\u00e4 uudelleen", + "unknown": "Odottamaton virhe" + }, + "step": { + "user": { + "data": { + "host": "IP-osoite" + }, + "title": "Yhdist\u00e4 PowerView-keskittimeen" + } + } + }, + "title": "Hunter Douglas PowerView" +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/hu.json b/homeassistant/components/hunterdouglas_powerview/translations/hu.json new file mode 100644 index 00000000000000..baa3b135d420bf --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni, pr\u00f3b\u00e1lkozzon \u00fajra.", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "user": { + "data": { + "host": "IP-c\u00edm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/it.json b/homeassistant/components/hunterdouglas_powerview/translations/it.json new file mode 100644 index 00000000000000..2dcb2a72c8b2a6 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/it.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi, si prega di riprovare", + "unknown": "Errore imprevisto" + }, + "step": { + "link": { + "description": "Vuoi impostare {name} ({host})?", + "title": "Connettersi all'Hub PowerView" + }, + "user": { + "data": { + "host": "Indirizzo IP" + }, + "title": "Collegamento al PowerView Hub" + } + } + }, + "title": "Hunter Douglas PowerView" +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/nl.json b/homeassistant/components/hunterdouglas_powerview/translations/nl.json new file mode 100644 index 00000000000000..9c0ab4932defdb --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/nl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "unknown": "Onverwachte fout" + }, + "step": { + "link": { + "description": "Wil je {name} ({host}) instellen?", + "title": "Maak verbinding met de PowerView Hub" + }, + "user": { + "data": { + "host": "IP-adres" + }, + "title": "Maak verbinding met de PowerView Hub" + } + } + }, + "title": "Hunter Douglas PowerView" +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/ru.json b/homeassistant/components/hunterdouglas_powerview/translations/ru.json new file mode 100644 index 00000000000000..88d363c63da6f8 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/ru.json @@ -0,0 +1,24 @@ +{ + "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." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "link": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} ({host})?", + "title": "Hunter Douglas PowerView" + }, + "user": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441" + }, + "title": "Hunter Douglas PowerView" + } + } + }, + "title": "Hunter Douglas PowerView" +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/sv.json b/homeassistant/components/hunterdouglas_powerview/translations/sv.json new file mode 100644 index 00000000000000..04371b16514297 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "link": { + "description": "Do vill du konfigurera {name} ({host})?", + "title": "Anslut till PowerView Hub" + }, + "user": { + "data": { + "host": "IP-adress" + }, + "title": "Anslut till PowerView Hub" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json b/homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json new file mode 100644 index 00000000000000..0eb5a4d3d37980 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "link": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name} ({host})\uff1f", + "title": "\u9023\u7dda\u81f3 PowerView Hub" + }, + "user": { + "data": { + "host": "IP \u4f4d\u5740" + }, + "title": "\u9023\u7dda\u81f3 PowerView Hub" + } + } + }, + "title": "Hunter Douglas PowerView" +} \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/nl.json b/homeassistant/components/icloud/translations/nl.json index 84691ebd1340ed..ee1e20c6969ec6 100644 --- a/homeassistant/components/icloud/translations/nl.json +++ b/homeassistant/components/icloud/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Account reeds geconfigureerd" + "already_configured": "Account reeds geconfigureerd", + "no_device": "Op geen van uw apparaten is \"Find my iPhone\" geactiveerd" }, "error": { "login": "Aanmeldingsfout: controleer uw e-mailadres en wachtwoord", @@ -19,7 +20,8 @@ "user": { "data": { "password": "Wachtwoord", - "username": "E-mail" + "username": "E-mail", + "with_family": "Met gezin" }, "description": "Voer uw gegevens in", "title": "iCloud inloggegevens" diff --git a/homeassistant/components/ipp/translations/nl.json b/homeassistant/components/ipp/translations/nl.json index e775d869615a5e..12f9d37ec7a200 100644 --- a/homeassistant/components/ipp/translations/nl.json +++ b/homeassistant/components/ipp/translations/nl.json @@ -7,6 +7,28 @@ "ipp_error": "Er is een IPP-fout opgetreden.", "ipp_version_error": "IPP-versie wordt niet ondersteund door printer.", "parse_error": "Ongeldige reactie van de printer." + }, + "error": { + "connection_error": "Kan geen verbinding maken met de printer.", + "connection_upgrade": "Kan geen verbinding maken met de printer. Probeer het opnieuw met SSL / TLS-optie aangevinkt." + }, + "flow_title": "Printer: {name}", + "step": { + "user": { + "data": { + "base_path": "Relatief pad naar de printer", + "host": "Host- of IP-adres", + "port": "Poort", + "ssl": "Printer ondersteunt communicatie via SSL / TLS", + "verify_ssl": "Printer gebruikt een correct SSL-certificaat" + }, + "description": "Stel uw printer in via Internet Printing Protocol (IPP) om te integreren met Home Assistant.", + "title": "Koppel uw printer" + }, + "zeroconf_confirm": { + "description": "Wilt u de printer met de naam ' {name} ' toevoegen aan Home Assistant?", + "title": "Gedetecteerde printer" + } } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/ru.json b/homeassistant/components/ipp/translations/ru.json index 5a8c6068769f52..eee7de36cd1e0a 100644 --- a/homeassistant/components/ipp/translations/ru.json +++ b/homeassistant/components/ipp/translations/ru.json @@ -22,7 +22,7 @@ "ssl": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0441\u0432\u044f\u0437\u044c \u043f\u043e SSL/TLS", "verify_ssl": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043d\u0442\u0435\u0440 \u043f\u043e \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 IPP \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Home Assistant.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430 \u043f\u043e \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 IPP.", "title": "Internet Printing Protocol (IPP)" }, "zeroconf_confirm": { diff --git a/homeassistant/components/konnected/translations/nl.json b/homeassistant/components/konnected/translations/nl.json index 45351cd0966f74..17bb20be765a6a 100644 --- a/homeassistant/components/konnected/translations/nl.json +++ b/homeassistant/components/konnected/translations/nl.json @@ -8,6 +8,10 @@ "confirm": { "title": "Konnected Apparaat Klaar" }, + "import_confirm": { + "description": "Er is een Konnected Alarmpaneel met ID {id} ontdekt in configuration.yaml. Met deze flow kunt u deze importeren in een configuratie-item.", + "title": "Konnected apparaat importeren" + }, "user": { "data": { "host": "IP-adres van Konnected apparaat", @@ -23,6 +27,7 @@ "not_konn_panel": "Geen herkend Konnected.io apparaat" }, "error": { + "bad_host": "Ongeldige URL voor overschrijven API-host", "one": "Leeg", "other": "Leeg" }, @@ -58,13 +63,19 @@ "11": "Zone 11", "12": "Zone 12", "8": "Zone 8", - "9": "Zone 9" + "9": "Zone 9", + "alarm1": "ALARM1", + "alarm2_out2": "OUT2 / ALARM2", + "out1": "OUT1" } }, "options_misc": { "data": { - "api_host": "API host-URL overschrijven (optioneel)" - } + "api_host": "API host-URL overschrijven (optioneel)", + "override_api_host": "Overschrijf standaard Home Assistant API hostpaneel-URL" + }, + "description": "Selecteer het gewenste gedrag voor uw paneel", + "title": "Configureer Misc" }, "options_switch": { "data": { diff --git a/homeassistant/components/light/translations/it.json b/homeassistant/components/light/translations/it.json index c9cc397211dac7..9477171c9f61e4 100644 --- a/homeassistant/components/light/translations/it.json +++ b/homeassistant/components/light/translations/it.json @@ -19,8 +19,8 @@ }, "state": { "_": { - "off": "Spento", - "on": "Acceso" + "off": "Spenta", + "on": "Accesa" } }, "title": "Luce" diff --git a/homeassistant/components/light/translations/nl.json b/homeassistant/components/light/translations/nl.json index 5c4dc969b0fbc3..190ed3f52bdc9b 100644 --- a/homeassistant/components/light/translations/nl.json +++ b/homeassistant/components/light/translations/nl.json @@ -1,6 +1,8 @@ { "device_automation": { "action_type": { + "brightness_decrease": "Verlaag de helderheid van {entity_name}", + "brightness_increase": "Verhoog de helderheid van {entity_name}", "flash": "Flash {entity_name}", "toggle": "Omschakelen {entity_name}", "turn_off": "{entity_name} uitschakelen", diff --git a/homeassistant/components/local_ip/translations/nl.json b/homeassistant/components/local_ip/translations/nl.json index 88de9704a6e6a7..ba75a9b2a4db8c 100644 --- a/homeassistant/components/local_ip/translations/nl.json +++ b/homeassistant/components/local_ip/translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van lokaal IP toegestaan." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/local_ip/translations/sv.json b/homeassistant/components/local_ip/translations/sv.json index 9c8f27dff8df1d..d4b508b41a735b 100644 --- a/homeassistant/components/local_ip/translations/sv.json +++ b/homeassistant/components/local_ip/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Endast en konfiguration av lokal IP \u00e4r till\u00e5ten." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/monoprice/translations/nl.json b/homeassistant/components/monoprice/translations/nl.json index 8b8126a63ee981..74bc677dbe8728 100644 --- a/homeassistant/components/monoprice/translations/nl.json +++ b/homeassistant/components/monoprice/translations/nl.json @@ -10,7 +10,13 @@ "step": { "user": { "data": { - "port": "Seri\u00eble poort" + "port": "Seri\u00eble poort", + "source_1": "Naam van bron #1", + "source_2": "Naam van bron #2", + "source_3": "Naam van bron #3", + "source_4": "Naam van bron #4", + "source_5": "Naam van bron #5", + "source_6": "Naam van bron #6" }, "title": "Verbinding maken met het apparaat" } diff --git a/homeassistant/components/monoprice/translations/sv.json b/homeassistant/components/monoprice/translations/sv.json new file mode 100644 index 00000000000000..3531253b136ce1 --- /dev/null +++ b/homeassistant/components/monoprice/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "title": "Anslut till enheten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/nl.json b/homeassistant/components/mqtt/translations/nl.json index 56684b325351c6..f7c099c2cb6438 100644 --- a/homeassistant/components/mqtt/translations/nl.json +++ b/homeassistant/components/mqtt/translations/nl.json @@ -26,5 +26,16 @@ "title": "MQTTT Broker via Hass.io add-on" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Eerste knop", + "button_2": "Tweede knop", + "button_3": "Derde knop", + "button_4": "Vierde knop", + "button_5": "Vijfde knop", + "button_6": "Zesde knop", + "turn_off": "Uitschakelen" + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/sv.json b/homeassistant/components/mqtt/translations/sv.json index 58855eeb62bb97..9415beedbf7d2e 100644 --- a/homeassistant/components/mqtt/translations/sv.json +++ b/homeassistant/components/mqtt/translations/sv.json @@ -26,5 +26,17 @@ "title": "MQTT Broker via Hass.io till\u00e4gg" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "F\u00f6rsta knappen", + "button_2": "Andra knappen", + "button_3": "Tredje knappen", + "button_4": "Fj\u00e4rde knappen", + "button_5": "Femte knappen", + "button_6": "Sj\u00e4tte knappen", + "turn_off": "St\u00e4ng av", + "turn_on": "Starta" + } } } \ No newline at end of file diff --git a/homeassistant/components/myq/translations/sv.json b/homeassistant/components/myq/translations/sv.json new file mode 100644 index 00000000000000..1243ca600f0d83 --- /dev/null +++ b/homeassistant/components/myq/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/translations/nl.json b/homeassistant/components/nexia/translations/nl.json index 8767b93d1739b9..d718c78d7af78c 100644 --- a/homeassistant/components/nexia/translations/nl.json +++ b/homeassistant/components/nexia/translations/nl.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "Deze nexia-woning is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/nexia/translations/sv.json b/homeassistant/components/nexia/translations/sv.json new file mode 100644 index 00000000000000..9cfd620ac7329c --- /dev/null +++ b/homeassistant/components/nexia/translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/translations/nl.json b/homeassistant/components/notion/translations/nl.json index b6a28495692af3..473659c0eb2506 100644 --- a/homeassistant/components/notion/translations/nl.json +++ b/homeassistant/components/notion/translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Deze gebruikersnaam is al in gebruik." + }, "error": { "invalid_credentials": "Ongeldige gebruikersnaam of wachtwoord", "no_devices": "Geen apparaten gevonden in account" diff --git a/homeassistant/components/notion/translations/sv.json b/homeassistant/components/notion/translations/sv.json index 6f9ab3bac1c46b..7e05095cf0e8b2 100644 --- a/homeassistant/components/notion/translations/sv.json +++ b/homeassistant/components/notion/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Det h\u00e4r anv\u00e4ndarnamnet anv\u00e4nds redan." + }, "error": { "invalid_credentials": "Felaktigt anv\u00e4ndarnamn eller l\u00f6senord", "no_devices": "Inga enheter hittades p\u00e5 kontot" diff --git a/homeassistant/components/nuheat/translations/nl.json b/homeassistant/components/nuheat/translations/nl.json new file mode 100644 index 00000000000000..edf3ad17ff45c4 --- /dev/null +++ b/homeassistant/components/nuheat/translations/nl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "De thermostaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "invalid_auth": "Ongeldige authenticatie", + "invalid_thermostat": "Het serienummer van de thermostaat is ongeldig.", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "serial_number": "Serienummer van de thermostaat.", + "username": "Gebruikersnaam" + }, + "description": "U moet het numerieke serienummer of de ID van uw thermostaat verkrijgen door in te loggen op https://MyNuHeat.com en uw thermostaat (thermostaten) te selecteren.", + "title": "Maak verbinding met de NuHeat" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/translations/sv.json b/homeassistant/components/nuheat/translations/sv.json new file mode 100644 index 00000000000000..9cfd620ac7329c --- /dev/null +++ b/homeassistant/components/nuheat/translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/translations/nl.json b/homeassistant/components/nut/translations/nl.json index d0ed6c3c5733f6..5e4acf3574d986 100644 --- a/homeassistant/components/nut/translations/nl.json +++ b/homeassistant/components/nut/translations/nl.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", "unknown": "Onverwachte fout" }, "step": { @@ -11,6 +15,10 @@ "title": "Kies de te controleren bronnen" }, "ups": { + "data": { + "alias": "Alias", + "resources": "Bronnen" + }, "title": "Kies een UPS om uit te lezen" }, "user": { @@ -28,8 +36,10 @@ "step": { "init": { "data": { - "resources": "Bronnen" - } + "resources": "Bronnen", + "scan_interval": "Scaninterval (seconden)" + }, + "description": "Kies Sensorbronnen." } } } diff --git a/homeassistant/components/nut/translations/sv.json b/homeassistant/components/nut/translations/sv.json new file mode 100644 index 00000000000000..eb839d31fb26dd --- /dev/null +++ b/homeassistant/components/nut/translations/sv.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "resources": { + "data": { + "resources": "Resurser" + } + }, + "user": { + "data": { + "host": "V\u00e4rd", + "password": "L\u00f6senord", + "port": "Port", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nws/translations/fi.json b/homeassistant/components/nws/translations/fi.json new file mode 100644 index 00000000000000..5c3329a260a090 --- /dev/null +++ b/homeassistant/components/nws/translations/fi.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "unknown": "Odottamaton virhe" + }, + "step": { + "user": { + "data": { + "api_key": "API-avain (s\u00e4hk\u00f6posti)", + "latitude": "Leveysaste", + "longitude": "Pituusaste" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nws/translations/nl.json b/homeassistant/components/nws/translations/nl.json index 532ec589a65dc3..b74e6db96a21a7 100644 --- a/homeassistant/components/nws/translations/nl.json +++ b/homeassistant/components/nws/translations/nl.json @@ -1,12 +1,22 @@ { "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "unknown": "Onverwachte fout" + }, "step": { "user": { "data": { "api_key": "API-sleutel (e-mail)", "latitude": "Breedtegraad", - "longitude": "Lengtegraad" - } + "longitude": "Lengtegraad", + "station": "METAR-zendercode" + }, + "description": "Als er geen METAR-zendercode is opgegeven, worden de lengte- en breedtegraad gebruikt om het dichtstbijzijnde station te vinden.", + "title": "Maak verbinding met de National Weather Service" } } } diff --git a/homeassistant/components/nws/translations/sv.json b/homeassistant/components/nws/translations/sv.json new file mode 100644 index 00000000000000..fefa01fd40a3e5 --- /dev/null +++ b/homeassistant/components/nws/translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/fi.json b/homeassistant/components/panasonic_viera/translations/fi.json new file mode 100644 index 00000000000000..22e861af227926 --- /dev/null +++ b/homeassistant/components/panasonic_viera/translations/fi.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "unknown": "Tapahtui tuntematon virhe. Lis\u00e4tietoja on lokeissa." + }, + "error": { + "invalid_pin_code": "Antamasi PIN-koodi ei kelpaa" + }, + "step": { + "pairing": { + "data": { + "pin": "PIN" + }, + "description": "Kirjoita televisiossa n\u00e4kyv\u00e4 PIN-koodi", + "title": "Paritus" + }, + "user": { + "data": { + "host": "IP-osoite", + "name": "Nimi" + }, + "description": "Anna Panasonic Viera TV:n IP-osoite", + "title": "Television m\u00e4\u00e4ritt\u00e4minen" + } + } + }, + "title": "Panasonic Viera" +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/sv.json b/homeassistant/components/panasonic_viera/translations/sv.json new file mode 100644 index 00000000000000..f70336fae9f643 --- /dev/null +++ b/homeassistant/components/panasonic_viera/translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "unknown": "Ett ov\u00e4ntat fel intr\u00e4ffade. Kontrollera loggarna f\u00f6r mer information." + }, + "step": { + "user": { + "data": { + "host": "IP-adress", + "name": "Namn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/translations/nl.json b/homeassistant/components/powerwall/translations/nl.json new file mode 100644 index 00000000000000..f77cc864813d07 --- /dev/null +++ b/homeassistant/components/powerwall/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "De powerwall is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "unknown": "Onverwachte fout", + "wrong_version": "Uw powerwall gebruikt een softwareversie die niet wordt ondersteund. Overweeg om dit probleem te upgraden of te melden, zodat het kan worden opgelost." + }, + "step": { + "user": { + "data": { + "ip_address": "IP-adres" + }, + "title": "Maak verbinding met de powerwall" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/translations/sv.json b/homeassistant/components/powerwall/translations/sv.json new file mode 100644 index 00000000000000..0c6f94cd697b1d --- /dev/null +++ b/homeassistant/components/powerwall/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "unknown": "Ov\u00e4ntat fel", + "wrong_version": "Powerwall anv\u00e4nder en programvaruversion som inte st\u00f6ds. T\u00e4nk p\u00e5 att uppgradera eller rapportera det h\u00e4r problemet s\u00e5 att det kan l\u00f6sas." + }, + "step": { + "user": { + "data": { + "ip_address": "IP-adress" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/nl.json b/homeassistant/components/pvpc_hourly_pricing/translations/nl.json index c489bef5a2c85d..3abffdf5bc0f24 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/nl.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/nl.json @@ -1,10 +1,16 @@ { "config": { + "abort": { + "already_configured": "Integratie is al geconfigureerd met een bestaande sensor met dat tarief" + }, "step": { "user": { "data": { - "name": "Sensornaam" - } + "name": "Sensornaam", + "tariff": "Gecontracteerd tarief (1, 2 of 3 periodes)" + }, + "description": "Deze sensor gebruikt de offici\u00eble API om [uurtarief voor elektriciteit (PVPC)] (https://www.esios.ree.es/es/pvpc) in Spanje te krijgen. \n Bezoek voor een meer precieze uitleg de [integratiedocumenten] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\n Selecteer het gecontracteerde tarief op basis van het aantal factureringsperioden per dag: \n - 1 periode: normaal \n - 2 periodes: discriminatie (nachttarief) \n - 3 periodes: elektrische auto (nachttarief van 3 periodes)", + "title": "Tariefselectie" } } } diff --git a/homeassistant/components/rachio/translations/nl.json b/homeassistant/components/rachio/translations/nl.json new file mode 100644 index 00000000000000..2173c768185bc4 --- /dev/null +++ b/homeassistant/components/rachio/translations/nl.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "api_key": "De API-sleutel voor het Rachio-account." + }, + "description": "U heeft de API-sleutel nodig van https://app.rach.io/. Selecteer 'Accountinstellingen en klik vervolgens op' GET API KEY '.", + "title": "Maak verbinding met uw Rachio-apparaat" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "Hoe lang, in minuten, om een station in te schakelen wanneer de schakelaar is ingeschakeld." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/translations/sv.json b/homeassistant/components/rachio/translations/sv.json new file mode 100644 index 00000000000000..726912f62f9985 --- /dev/null +++ b/homeassistant/components/rachio/translations/sv.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/nl.json b/homeassistant/components/rainmachine/translations/nl.json index 545d0ded465989..3d13a48712bc95 100644 --- a/homeassistant/components/rainmachine/translations/nl.json +++ b/homeassistant/components/rainmachine/translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Deze RainMachine controller is al geconfigureerd." + }, "error": { "identifier_exists": "Account bestaat al", "invalid_credentials": "Ongeldige gebruikersgegevens" diff --git a/homeassistant/components/roku/translations/nl.json b/homeassistant/components/roku/translations/nl.json new file mode 100644 index 00000000000000..8f2fae7146e052 --- /dev/null +++ b/homeassistant/components/roku/translations/nl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Roku-apparaat is al geconfigureerd", + "unknown": "Onverwachte fout" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw" + }, + "flow_title": "Roku: {name}", + "step": { + "ssdp_confirm": { + "description": "Wilt u {name} instellen?", + "title": "Roku" + }, + "user": { + "data": { + "host": "Host- of IP-adres" + }, + "description": "Voer uw Roku-informatie in.", + "title": "Roku" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/sv.json b/homeassistant/components/roku/translations/sv.json new file mode 100644 index 00000000000000..6d6f9223466dd2 --- /dev/null +++ b/homeassistant/components/roku/translations/sv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "unknown": "Ov\u00e4ntat fel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/nl.json b/homeassistant/components/roomba/translations/nl.json new file mode 100644 index 00000000000000..d49a9f488deef3 --- /dev/null +++ b/homeassistant/components/roomba/translations/nl.json @@ -0,0 +1,32 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "blid": "BLID", + "certificate": "Certificaat", + "continuous": "Doorlopend", + "delay": "Vertraging", + "host": "Hostnaam of IP-adres", + "password": "Wachtwoord" + }, + "description": "Het ophalen van de BLID en het wachtwoord is momenteel een handmatig proces. Volg de stappen in de documentatie op: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "title": "Verbinding maken met het apparaat" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "continuous": "Doorlopend", + "delay": "Vertraging" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/sv.json b/homeassistant/components/roomba/translations/sv.json new file mode 100644 index 00000000000000..f39fd27b70573d --- /dev/null +++ b/homeassistant/components/roomba/translations/sv.json @@ -0,0 +1,29 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "certificate": "Certifikat", + "continuous": "Kontinuerlig", + "delay": "F\u00f6rdr\u00f6jning", + "host": "V\u00e4rdnamn eller IP-adress", + "password": "L\u00f6senord" + }, + "title": "Anslut till enheten" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "delay": "F\u00f6rdr\u00f6jning" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/sensor.fi.json b/homeassistant/components/season/translations/sensor.fi.json index f01f6451549ee4..012735e07f80ef 100644 --- a/homeassistant/components/season/translations/sensor.fi.json +++ b/homeassistant/components/season/translations/sensor.fi.json @@ -1,8 +1,10 @@ { "state": { - "autumn": "Syksy", - "spring": "Kev\u00e4t", - "summer": "Kes\u00e4", - "winter": "Talvi" + "season__season": { + "autumn": "Syksy", + "spring": "Kev\u00e4t", + "summer": "Kes\u00e4", + "winter": "Talvi" + } } } \ No newline at end of file diff --git a/homeassistant/components/season/translations/sensor.sv.json b/homeassistant/components/season/translations/sensor.sv.json index 5987508492868b..bffbdaa8d2f89c 100644 --- a/homeassistant/components/season/translations/sensor.sv.json +++ b/homeassistant/components/season/translations/sensor.sv.json @@ -1,5 +1,11 @@ { "state": { + "season__season": { + "autumn": "H\u00f6st", + "spring": "V\u00e5r", + "summer": "Sommar", + "winter": "Vinter" + }, "season__season__": { "autumn": "H\u00f6st", "spring": "V\u00e5r", diff --git a/homeassistant/components/sense/translations/nl.json b/homeassistant/components/sense/translations/nl.json new file mode 100644 index 00000000000000..ee9e61b5a38b35 --- /dev/null +++ b/homeassistant/components/sense/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "email": "E-mailadres", + "password": "Wachtwoord" + }, + "title": "Maak verbinding met uw Sense Energy Monitor" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/sv.json b/homeassistant/components/sense/translations/sv.json new file mode 100644 index 00000000000000..02939a27dbbc34 --- /dev/null +++ b/homeassistant/components/sense/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "email": "E-postadress", + "password": "L\u00f6senord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shopping_list/translations/nl.json b/homeassistant/components/shopping_list/translations/nl.json new file mode 100644 index 00000000000000..de6045dd81bc29 --- /dev/null +++ b/homeassistant/components/shopping_list/translations/nl.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "De Shopping List is al geconfigureerd." + }, + "step": { + "user": { + "description": "Wil je de Shopping List configureren?", + "title": "Shopping List" + } + } + }, + "title": "Shopping List" +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json index 2c5a0d0971c0d3..42fc575f650d43 100644 --- a/homeassistant/components/simplisafe/translations/de.json +++ b/homeassistant/components/simplisafe/translations/de.json @@ -10,6 +10,7 @@ "step": { "user": { "data": { + "code": "Code (wird in der Benutzeroberfl\u00e4che von Home Assistant verwendet)", "password": "Passwort", "username": "E-Mail-Adresse" }, diff --git a/homeassistant/components/simplisafe/translations/nl.json b/homeassistant/components/simplisafe/translations/nl.json index f585a9c92310d1..ce3fcf2902f3ea 100644 --- a/homeassistant/components/simplisafe/translations/nl.json +++ b/homeassistant/components/simplisafe/translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Dit SimpliSafe-account is al in gebruik." + }, "error": { "identifier_exists": "Account bestaat al", "invalid_credentials": "Ongeldige gebruikersgegevens" @@ -14,5 +17,15 @@ "title": "Vul uw gegevens in" } } + }, + "options": { + "step": { + "init": { + "data": { + "code": "Code (gebruikt in de Home Assistant UI)" + }, + "title": "Configureer SimpliSafe" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/smartthings/translations/nl.json b/homeassistant/components/smartthings/translations/nl.json index 85a0ed4a7361ce..a77ad40f0caa98 100644 --- a/homeassistant/components/smartthings/translations/nl.json +++ b/homeassistant/components/smartthings/translations/nl.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "invalid_webhook_url": "Home Assistant is niet correct geconfigureerd om updates van SmartThings te ontvangen. De webhook-URL is ongeldig: \n > {webhook_url} \n\n Werk uw configuratie bij volgens de [instructies] ( {component_url} ), start de Home Assistant opnieuw op en probeer het opnieuw.", + "no_available_locations": "Er zijn geen beschikbare SmartThings-locaties om in te stellen in Home Assistant." + }, "error": { "app_setup_error": "Instellen van SmartApp mislukt. Probeer het opnieuw.", "token_forbidden": "Het token heeft niet de vereiste OAuth-scopes.", @@ -15,12 +19,14 @@ "data": { "access_token": "Toegangstoken" }, + "description": "Voer een SmartThings [Personal Access Token] ( {token_url} ) in dat is gemaakt volgens de [instructies] ( {component_url} ). Dit wordt gebruikt om de Home Assistant-integratie te cre\u00ebren binnen uw SmartThings-account.", "title": "Persoonlijk toegangstoken invoeren" }, "select_location": { "data": { "location_id": "Locatie" }, + "description": "Selecteer de SmartThings-locatie die u aan de Home Assistant wilt toevoegen. We zullen dan een nieuw venster openen en u vragen om in te loggen en de installatie van de Home Assistant-integratie op de geselecteerde locatie te autoriseren.", "title": "Locatie selecteren" }, "user": { diff --git a/homeassistant/components/smartthings/translations/sv.json b/homeassistant/components/smartthings/translations/sv.json index a02483baa47659..1b1f8859298b66 100644 --- a/homeassistant/components/smartthings/translations/sv.json +++ b/homeassistant/components/smartthings/translations/sv.json @@ -8,6 +8,20 @@ "webhook_error": "SmartThings kunde inte validera endpoint konfigurerad i \" base_url`. V\u00e4nligen granska kraven f\u00f6r komponenten." }, "step": { + "authorize": { + "title": "Auktorisera Home Assistant" + }, + "pat": { + "data": { + "access_token": "\u00c5tkomstnyckel" + } + }, + "select_location": { + "data": { + "location_id": "Position" + }, + "title": "V\u00e4lj plats" + }, "user": { "description": "V\u00e4nligen ange en [personlig \u00e5tkomsttoken]({token_url}) f\u00f6r SmartThings som har skapats enligt [instruktionerna]({component_url}).", "title": "Ange personlig \u00e5tkomsttoken" diff --git a/homeassistant/components/synology_dsm/translations/fi.json b/homeassistant/components/synology_dsm/translations/fi.json new file mode 100644 index 00000000000000..19cf9e272cf71c --- /dev/null +++ b/homeassistant/components/synology_dsm/translations/fi.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "otp_failed": "Kaksivaiheinen todennus ep\u00e4onnistui, yrit\u00e4 uudelleen uudella salasanalla." + }, + "step": { + "2sa": { + "data": { + "otp_code": "Koodi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/nl.json b/homeassistant/components/synology_dsm/translations/nl.json index 5e2fa85fb528cc..8b69639140b431 100644 --- a/homeassistant/components/synology_dsm/translations/nl.json +++ b/homeassistant/components/synology_dsm/translations/nl.json @@ -6,6 +6,7 @@ "error": { "connection": "Verbindingsfout: controleer uw host, poort & ssl", "login": "Aanmeldingsfout: controleer uw gebruikersnaam en wachtwoord", + "missing_data": "Ontbrekende gegevens: probeer het later opnieuw of een andere configuratie", "otp_failed": "Tweestapsverificatie is mislukt, probeer het opnieuw met een nieuwe toegangscode", "unknown": "Onbekende fout: controleer de logs voor meer informatie" }, diff --git a/homeassistant/components/synology_dsm/translations/sv.json b/homeassistant/components/synology_dsm/translations/sv.json new file mode 100644 index 00000000000000..c0bdc759122013 --- /dev/null +++ b/homeassistant/components/synology_dsm/translations/sv.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "V\u00e4rden \u00e4r redan konfigurerad." + }, + "error": { + "connection": "Anslutningsfel: v\u00e4nligen kontrollera v\u00e4rd, port & SSL" + }, + "step": { + "link": { + "data": { + "password": "L\u00f6senord", + "port": "Port (Valfri)", + "username": "Anv\u00e4ndarnamn" + }, + "description": "Do vill du konfigurera {name} ({host})?" + }, + "user": { + "data": { + "host": "V\u00e4rd", + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tado/translations/nl.json b/homeassistant/components/tado/translations/nl.json index 5099637a637f69..3cdadf0f54e1c6 100644 --- a/homeassistant/components/tado/translations/nl.json +++ b/homeassistant/components/tado/translations/nl.json @@ -1,10 +1,32 @@ { + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "invalid_auth": "Ongeldige authenticatie", + "no_homes": "Er zijn geen huizen gekoppeld aan dit tado-account.", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "title": "Maak verbinding met je Tado-account" + } + } + }, "options": { "step": { "init": { "data": { "fallback": "Schakel de terugvalmodus in." - } + }, + "description": "De fallback-modus schakelt over naar Smart Schedule bij de volgende schemaschakeling na het handmatig aanpassen van een zone.", + "title": "Pas Tado-opties aan." } } } diff --git a/homeassistant/components/tado/translations/sv.json b/homeassistant/components/tado/translations/sv.json new file mode 100644 index 00000000000000..b00dc6e93b2f01 --- /dev/null +++ b/homeassistant/components/tado/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/translations/nl.json b/homeassistant/components/tesla/translations/nl.json index 02bbd32e417fd0..27df1acae2ac1d 100644 --- a/homeassistant/components/tesla/translations/nl.json +++ b/homeassistant/components/tesla/translations/nl.json @@ -21,6 +21,7 @@ "step": { "init": { "data": { + "enable_wake_on_start": "Forceer auto's wakker bij het opstarten", "scan_interval": "Seconden tussen scans" } } diff --git a/homeassistant/components/timer/translations/it.json b/homeassistant/components/timer/translations/it.json index 464a2feb501379..b5627e51d0b197 100644 --- a/homeassistant/components/timer/translations/it.json +++ b/homeassistant/components/timer/translations/it.json @@ -1,9 +1,9 @@ { "state": { "_": { - "active": "attivo", - "idle": "inattivo", - "paused": "in pausa" + "active": "Attivo", + "idle": "Inattivo", + "paused": "In pausa" } } } \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/nl.json b/homeassistant/components/totalconnect/translations/nl.json index 3196a58675c33a..508c112ae6128d 100644 --- a/homeassistant/components/totalconnect/translations/nl.json +++ b/homeassistant/components/totalconnect/translations/nl.json @@ -11,7 +11,8 @@ "data": { "password": "Wachtwoord", "username": "Gebruikersnaam" - } + }, + "title": "Total Connect" } } } diff --git a/homeassistant/components/totalconnect/translations/sv.json b/homeassistant/components/totalconnect/translations/sv.json new file mode 100644 index 00000000000000..68dc9efeedb7f7 --- /dev/null +++ b/homeassistant/components/totalconnect/translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Kontot har redan konfigurerats" + }, + "error": { + "login": "Inloggningsfel: v\u00e4nligen kontrollera ditt anv\u00e4ndarnamn och l\u00f6senord" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/nl.json b/homeassistant/components/unifi/translations/nl.json index 62611d0aac8fb4..e37018350cbec2 100644 --- a/homeassistant/components/unifi/translations/nl.json +++ b/homeassistant/components/unifi/translations/nl.json @@ -6,7 +6,8 @@ }, "error": { "faulty_credentials": "Foutieve gebruikersgegevens", - "service_unavailable": "Geen service beschikbaar" + "service_unavailable": "Geen service beschikbaar", + "unknown_client_mac": "Geen client beschikbaar op dat MAC-adres" }, "step": { "user": { @@ -26,8 +27,12 @@ "step": { "client_control": { "data": { + "block_client": "Cli\u00ebnten met netwerktoegang", + "new_client": "Voeg een nieuwe client toe voor netwerktoegangsbeheer", "poe_clients": "Sta POE-controle van gebruikers toe" - } + }, + "description": "Configureer clientbesturingen \n\n Maak schakelaars voor serienummers waarvoor u de netwerktoegang wilt beheren.", + "title": "UniFi-opties 2/3" }, "device_tracker": { "data": { @@ -36,7 +41,9 @@ "track_clients": "Volg netwerkclients", "track_devices": "Netwerkapparaten volgen (Ubiquiti-apparaten)", "track_wired_clients": "Inclusief bedrade netwerkcli\u00ebnten" - } + }, + "description": "Apparaattracking configureren", + "title": "UniFi-opties 1/3" }, "init": { "data": { @@ -47,7 +54,8 @@ "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Maak bandbreedtegebruiksensoren voor netwerkclients" - } + }, + "title": "UniFi-opties 3/3" } } } diff --git a/homeassistant/components/unifi/translations/sv.json b/homeassistant/components/unifi/translations/sv.json index a41503b5ea2cba..d3dd18a28e0ab4 100644 --- a/homeassistant/components/unifi/translations/sv.json +++ b/homeassistant/components/unifi/translations/sv.json @@ -6,7 +6,8 @@ }, "error": { "faulty_credentials": "Felaktiga anv\u00e4ndaruppgifter", - "service_unavailable": "Ingen tj\u00e4nst tillg\u00e4nglig" + "service_unavailable": "Ingen tj\u00e4nst tillg\u00e4nglig", + "unknown_client_mac": "Ingen klient tillg\u00e4nglig p\u00e5 den MAC-adressen" }, "step": { "user": { diff --git a/homeassistant/components/vera/translations/nl.json b/homeassistant/components/vera/translations/nl.json index ff260e9bbb0779..358905bd50f940 100644 --- a/homeassistant/components/vera/translations/nl.json +++ b/homeassistant/components/vera/translations/nl.json @@ -1,19 +1,28 @@ { "config": { "abort": { - "already_configured": "Er is al een controller geconfigureerd." + "already_configured": "Er is al een controller geconfigureerd.", + "cannot_connect": "Kan geen verbinding maken met controller met url {base_url}" }, "step": { "user": { "data": { + "exclude": "Vera-apparaat-ID's om uit te sluiten van Home Assistant.", + "lights": "Vera-schakelapparaat id's behandelen als lichten in Home Assistant.", "vera_controller_url": "Controller-URL" - } + }, + "description": "Geef hieronder een URL voor de Vera-controller op. Het zou er zo uit moeten zien: http://192.168.1.161:3480.", + "title": "Stel Vera controller in" } } }, "options": { "step": { "init": { + "data": { + "exclude": "Vera-apparaat-ID's om uit te sluiten van Home Assistant.", + "lights": "Vera-schakelapparaat id's behandelen als lichten in Home Assistant." + }, "title": "Vera controller opties" } } diff --git a/homeassistant/components/vizio/translations/nl.json b/homeassistant/components/vizio/translations/nl.json index e1789347bf2f52..33777175c3eb0e 100644 --- a/homeassistant/components/vizio/translations/nl.json +++ b/homeassistant/components/vizio/translations/nl.json @@ -10,6 +10,18 @@ "name_exists": "Vizio apparaat met opgegeven naam al geconfigureerd." }, "step": { + "pair_tv": { + "data": { + "pin": "PIN" + }, + "title": "Voltooi het koppelingsproces" + }, + "pairing_complete": { + "title": "Koppelen voltooid" + }, + "pairing_complete_import": { + "title": "Koppelen voltooid" + }, "user": { "data": { "access_token": "Toegangstoken", @@ -17,6 +29,7 @@ "host": ":", "name": "Naam" }, + "description": "Een toegangstoken is alleen nodig voor tv's. Als u een TV configureert en nog geen toegangstoken heeft, laat dit dan leeg en doorloop het koppelingsproces.", "title": "Vizio SmartCast Client instellen" } } @@ -25,8 +38,11 @@ "step": { "init": { "data": { + "apps_to_include_or_exclude": "Apps om op te nemen of uit te sluiten", + "include_or_exclude": "Apps opnemen of uitsluiten?", "volume_step": "Volume Stapgrootte" }, + "description": "Als je een Smart TV hebt, kun je optioneel je bronnenlijst filteren door te kiezen welke apps je in je bronnenlijst wilt opnemen of uitsluiten.", "title": "Update Vizo SmartCast Opties" } } diff --git a/homeassistant/components/weather/translations/it.json b/homeassistant/components/weather/translations/it.json index b6559782581c61..171b29673cdbe9 100644 --- a/homeassistant/components/weather/translations/it.json +++ b/homeassistant/components/weather/translations/it.json @@ -15,7 +15,7 @@ "snowy-rainy": "Nevoso, piovoso", "sunny": "Soleggiato", "windy": "Ventoso", - "windy-variant": "Preval. Ventoso" + "windy-variant": "Prevalentemente ventoso" } } } \ No newline at end of file diff --git a/homeassistant/components/wled/translations/ru.json b/homeassistant/components/wled/translations/ru.json index 21b5282eb6db5d..4af5a3c8c811b2 100644 --- a/homeassistant/components/wled/translations/ru.json +++ b/homeassistant/components/wled/translations/ru.json @@ -13,7 +13,7 @@ "data": { "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 WLED \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Home Assistant.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 WLED.", "title": "WLED" }, "zeroconf_confirm": { diff --git a/homeassistant/components/xiaomi_miio/translations/de.json b/homeassistant/components/xiaomi_miio/translations/de.json new file mode 100644 index 00000000000000..0e608eaf504f3a --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/de.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "connect_error": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "no_device_selected": "Kein Ger\u00e4t ausgew\u00e4hlt, bitte w\u00e4hlen Sie ein Ger\u00e4t aus." + }, + "step": { + "gateway": { + "data": { + "host": "IP Adresse", + "name": "Name des Gateways", + "token": "API-Token" + }, + "description": "Sie ben\u00f6tigen das API-Token. Anweisungen finden Sie unter https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.", + "title": "Stellen Sie eine Verbindung zu einem Xiaomi Gateway her" + }, + "user": { + "data": { + "gateway": "Stellen Sie eine Verbindung zu einem Xiaomi Gateway her" + }, + "description": "W\u00e4hlen Sie aus, mit welchem Ger\u00e4t Sie eine Verbindung herstellen m\u00f6chten.", + "title": "Xiaomi Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/fi.json b/homeassistant/components/xiaomi_miio/translations/fi.json new file mode 100644 index 00000000000000..4b7dfe9ecba3e4 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/fi.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Laite on jo m\u00e4\u00e4ritetty" + }, + "error": { + "connect_error": "Yhteyden muodostaminen ep\u00e4onnistui. Yrit\u00e4 uudelleen", + "no_device_selected": "Ei valittuja laitteita. Ole hyv\u00e4 ja valitse yksi." + }, + "step": { + "gateway": { + "data": { + "host": "IP-osoite", + "name": "Yhdysk\u00e4yt\u00e4v\u00e4n nimi", + "token": "API-tunnus" + }, + "title": "Yhdist\u00e4 Xiaomi Gatewayhin" + }, + "user": { + "data": { + "gateway": "Yhdist\u00e4 Xiaomi Gatewayhin" + }, + "description": "Valitse laite, johon haluat muodostaa yhteyden.", + "title": "Xiaomi Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/hu.json b/homeassistant/components/xiaomi_miio/translations/hu.json new file mode 100644 index 00000000000000..a99c165ebbb090 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "connect_error": "Nem siker\u00fclt csatlakozni, pr\u00f3b\u00e1lkozzon \u00fajra.", + "no_device_selected": "Nincs kiv\u00e1lasztva eszk\u00f6z, k\u00e9rj\u00fck, v\u00e1lasszon egyet." + }, + "step": { + "gateway": { + "data": { + "host": "IP-c\u00edm" + }, + "description": "Sz\u00fcks\u00e9ge lesz az API Tokenre, tov\u00e1bbi inforaciok: https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token" + }, + "user": { + "description": "V\u00e1lassza ki, melyik k\u00e9sz\u00fcl\u00e9khez szeretne csatlakozni. " + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/nl.json b/homeassistant/components/xiaomi_miio/translations/nl.json index 8762baa8714552..7c83b1134a7350 100644 --- a/homeassistant/components/xiaomi_miio/translations/nl.json +++ b/homeassistant/components/xiaomi_miio/translations/nl.json @@ -1,20 +1,28 @@ { "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, "error": { + "connect_error": "Verbinding mislukt, probeer het opnieuw", "no_device_selected": "Geen apparaat geselecteerd, selecteer 1 apparaat alstublieft" }, "step": { "gateway": { "data": { - "host": "IP-adres" + "host": "IP-adres", + "name": "Naam van de gateway", + "token": "API-token" }, + "description": "U heeft het API-token nodig, zie https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token voor instructies.", "title": "Maak verbinding met een Xiaomi Gateway" }, "user": { "data": { "gateway": "Maak verbinding met een Xiaomi Gateway" }, - "description": "Selecteer het apparaat waarmee u verbinding wilt maken" + "description": "Selecteer het apparaat waarmee u verbinding wilt maken", + "title": "Xiaomi Miio" } } } diff --git a/homeassistant/components/xiaomi_miio/translations/sv.json b/homeassistant/components/xiaomi_miio/translations/sv.json new file mode 100644 index 00000000000000..c8714969bef4b9 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/sv.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "connect_error": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "no_device_selected": "Ingen enhet har valts, v\u00e4lj en enhet." + }, + "step": { + "gateway": { + "data": { + "host": "IP-adress", + "name": "Namnet p\u00e5 Gatewayen", + "token": "API Token" + }, + "description": "Du beh\u00f6ver en API token, se https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token f\u00f6r mer instruktioner.", + "title": "Anslut till en Xiaomi Gateway" + }, + "user": { + "data": { + "gateway": "Anslut till en Xiaomi Gateway" + }, + "description": "V\u00e4lj den enhet som du vill ansluta till." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/it.json b/homeassistant/components/zwave/translations/it.json index 3ea2dc94d8779d..8b8ffb732fc1f2 100644 --- a/homeassistant/components/zwave/translations/it.json +++ b/homeassistant/components/zwave/translations/it.json @@ -21,13 +21,13 @@ "state": { "_": { "dead": "Disattivo", - "initializing": "Avvio", + "initializing": "In avvio", "ready": "Pronto", "sleeping": "Dormiente" }, "query_stage": { "dead": "Disattivo", - "initializing": "Avvio" + "initializing": "In avvio" } } } \ No newline at end of file From 4e55fa6c5c50bebae504a075c32ef0b3bc7e2cf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Gabriel?= Date: Thu, 30 Apr 2020 21:35:02 -0300 Subject: [PATCH 18/58] Refactor Remote class in panasonic_viera (#34911) --- .coveragerc | 1 - .../components/panasonic_viera/__init__.py | 177 +++++++++++++++++- .../components/panasonic_viera/const.py | 2 + .../panasonic_viera/media_player.py | 163 ++-------------- tests/components/panasonic_viera/test_init.py | 123 ++++++++++++ 5 files changed, 311 insertions(+), 155 deletions(-) create mode 100644 tests/components/panasonic_viera/test_init.py diff --git a/.coveragerc b/.coveragerc index e2aa4243a46300..fde7cb637f2e71 100644 --- a/.coveragerc +++ b/.coveragerc @@ -531,7 +531,6 @@ omit = homeassistant/components/osramlightify/light.py homeassistant/components/otp/sensor.py homeassistant/components/panasonic_bluray/media_player.py - homeassistant/components/panasonic_viera/__init__.py homeassistant/components/panasonic_viera/media_player.py homeassistant/components/pandora/media_player.py homeassistant/components/pcal9535a/* diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index 60261712f4dcf8..ebc1c20a1fae1e 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -1,13 +1,29 @@ """The Panasonic Viera integration.""" import asyncio +from functools import partial +import logging +from urllib.request import URLError +from panasonic_viera import EncryptionRequired, Keys, RemoteControl, SOAPError import voluptuous as vol +from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.script import Script -from .const import CONF_ON_ACTION, DEFAULT_NAME, DEFAULT_PORT, DOMAIN +from .const import ( + ATTR_REMOTE, + CONF_APP_ID, + CONF_ENCRYPTION_KEY, + CONF_ON_ACTION, + DEFAULT_NAME, + DEFAULT_PORT, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( { @@ -28,7 +44,7 @@ extra=vol.ALLOW_EXTRA, ) -PLATFORMS = ["media_player"] +PLATFORMS = [MEDIA_PLAYER_DOMAIN] async def async_setup(hass, config): @@ -49,6 +65,27 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config_entry): """Set up Panasonic Viera from a config entry.""" + panasonic_viera_data = hass.data.setdefault(DOMAIN, {}) + + config = config_entry.data + + host = config[CONF_HOST] + port = config[CONF_PORT] + + on_action = config[CONF_ON_ACTION] + if on_action is not None: + on_action = Script(hass, on_action) + + params = {} + if CONF_APP_ID in config and CONF_ENCRYPTION_KEY in config: + params["app_id"] = config[CONF_APP_ID] + params["encryption_key"] = config[CONF_ENCRYPTION_KEY] + + remote = Remote(hass, host, port, on_action, **params) + await remote.async_create_remote_control(during_setup=True) + + panasonic_viera_data[config_entry.entry_id] = {ATTR_REMOTE: remote} + for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, component) @@ -59,7 +96,7 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - return all( + unload_ok = all( await asyncio.gather( *[ hass.config_entries.async_forward_entry_unload(config_entry, component) @@ -67,3 +104,135 @@ async def async_unload_entry(hass, config_entry): ] ) ) + + if unload_ok: + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok + + +class Remote: + """The Remote class. It stores the TV properties and the remote control connection itself.""" + + def __init__( + self, hass, host, port, on_action=None, app_id=None, encryption_key=None, + ): + """Initialize the Remote class.""" + self._hass = hass + + self._host = host + self._port = port + + self._on_action = on_action + + self._app_id = app_id + self._encryption_key = encryption_key + + self.state = None + self.available = False + self.volume = 0 + self.muted = False + self.playing = True + + self._control = None + + async def async_create_remote_control(self, during_setup=False): + """Create remote control.""" + control_existed = self._control is not None + try: + params = {} + if self._app_id and self._encryption_key: + params["app_id"] = self._app_id + params["encryption_key"] = self._encryption_key + + self._control = await self._hass.async_add_executor_job( + partial(RemoteControl, self._host, self._port, **params) + ) + + self.state = STATE_ON + self.available = True + except (TimeoutError, URLError, SOAPError, OSError) as err: + if control_existed or during_setup: + _LOGGER.debug("Could not establish remote connection: %s", err) + + self._control = None + self.state = STATE_OFF + self.available = self._on_action is not None + except Exception as err: # pylint: disable=broad-except + if control_existed or during_setup: + _LOGGER.exception("An unknown error occurred: %s", err) + self._control = None + self.state = STATE_OFF + self.available = self._on_action is not None + + async def async_update(self): + """Update device data.""" + if self._control is None: + await self.async_create_remote_control() + return + + await self._handle_errors(self._update) + + def _update(self): + """Retrieve the latest data.""" + self.muted = self._control.get_mute() + self.volume = self._control.get_volume() / 100 + + self.state = STATE_ON + self.available = True + + async def async_send_key(self, key): + """Send a key to the TV and handle exceptions.""" + try: + key = getattr(Keys, key) + except (AttributeError, TypeError): + key = getattr(key, "value", key) + + await self._handle_errors(self._control.send_key, key) + + async def async_turn_on(self): + """Turn on the TV.""" + if self._on_action is not None: + await self._on_action.async_run() + self.state = STATE_ON + elif self.state != STATE_ON: + await self.async_send_key(Keys.power) + self.state = STATE_ON + + async def async_turn_off(self): + """Turn off the TV.""" + if self.state != STATE_OFF: + await self.async_send_key(Keys.power) + self.state = STATE_OFF + await self.async_update() + + async def async_set_mute(self, enable): + """Set mute based on 'enable'.""" + await self._handle_errors(self._control.set_mute, enable) + + async def async_set_volume(self, volume): + """Set volume level, range 0..1.""" + volume = int(volume * 100) + await self._handle_errors(self._control.set_volume, volume) + + async def async_play_media(self, media_type, media_id): + """Play media.""" + _LOGGER.debug("Play media: %s (%s)", media_id, media_type) + await self._handle_errors(self._control.open_webpage, media_id) + + async def _handle_errors(self, func, *args): + """Handle errors from func, set available and reconnect if needed.""" + try: + return await self._hass.async_add_executor_job(func, *args) + except EncryptionRequired: + _LOGGER.error( + "The connection couldn't be encrypted. Please reconfigure your TV" + ) + except (TimeoutError, URLError, SOAPError, OSError): + self.state = STATE_OFF + self.available = self._on_action is not None + await self.async_create_remote_control() + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("An unknown error occurred: %s", err) + self.state = STATE_OFF + self.available = self._on_action is not None diff --git a/homeassistant/components/panasonic_viera/const.py b/homeassistant/components/panasonic_viera/const.py index 434d2d3d7c4ea9..529de4ebe672cc 100644 --- a/homeassistant/components/panasonic_viera/const.py +++ b/homeassistant/components/panasonic_viera/const.py @@ -10,6 +10,8 @@ DEFAULT_NAME = "Panasonic Viera TV" DEFAULT_PORT = 55000 +ATTR_REMOTE = "remote" + ERROR_NOT_CONNECTED = "not_connected" ERROR_INVALID_PIN_CODE = "invalid_pin_code" diff --git a/homeassistant/components/panasonic_viera/media_player.py b/homeassistant/components/panasonic_viera/media_player.py index ce1d3762693b34..7260b519bf46c0 100644 --- a/homeassistant/components/panasonic_viera/media_player.py +++ b/homeassistant/components/panasonic_viera/media_player.py @@ -1,9 +1,7 @@ -"""Support for interface with a Panasonic Viera TV.""" -from functools import partial +"""Media player support for Panasonic Viera TV.""" import logging -from urllib.request import URLError -from panasonic_viera import EncryptionRequired, Keys, RemoteControl, SOAPError +from panasonic_viera import Keys from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( @@ -20,10 +18,9 @@ SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON -from homeassistant.helpers.script import Script +from homeassistant.const import CONF_NAME -from .const import CONF_APP_ID, CONF_ENCRYPTION_KEY, CONF_ON_ACTION +from .const import ATTR_REMOTE, DOMAIN SUPPORT_VIERATV = ( SUPPORT_PAUSE @@ -47,42 +44,25 @@ async def async_setup_entry(hass, config_entry, async_add_entities): config = config_entry.data - host = config[CONF_HOST] - port = config[CONF_PORT] + remote = hass.data[DOMAIN][config_entry.entry_id][ATTR_REMOTE] name = config[CONF_NAME] - on_action = config[CONF_ON_ACTION] - if on_action is not None: - on_action = Script(hass, on_action) - - params = {} - if CONF_APP_ID in config and CONF_ENCRYPTION_KEY in config: - params["app_id"] = config[CONF_APP_ID] - params["encryption_key"] = config[CONF_ENCRYPTION_KEY] - - remote = Remote(hass, host, port, on_action, **params) - await remote.async_create_remote_control(during_setup=True) - - tv_device = PanasonicVieraTVDevice(remote, name) - + tv_device = PanasonicVieraTVEntity(remote, name) async_add_entities([tv_device]) -class PanasonicVieraTVDevice(MediaPlayerEntity): +class PanasonicVieraTVEntity(MediaPlayerEntity): """Representation of a Panasonic Viera TV.""" - def __init__( - self, remote, name, uuid=None, - ): - """Initialize the Panasonic device.""" - # Save a reference to the imported class + def __init__(self, remote, name, uuid=None): + """Initialize the entity.""" self._remote = remote self._name = name self._uuid = uuid @property - def unique_id(self) -> str: - """Return the unique ID of this Viera TV.""" + def unique_id(self): + """Return the unique ID of the device.""" return self._uuid @property @@ -97,7 +77,7 @@ def state(self): @property def available(self): - """Return if True the device is available.""" + """Return True if the device is available.""" return self._remote.available @property @@ -176,125 +156,8 @@ async def async_media_previous_track(self): async def async_play_media(self, media_type, media_id, **kwargs): """Play media.""" - await self._remote.async_play_media(media_type, media_id) - - -class Remote: - """The Remote class. It stores the TV properties and the remote control connection itself.""" - - def __init__( - self, hass, host, port, on_action=None, app_id=None, encryption_key=None, - ): - """Initialize the Remote class.""" - self._hass = hass - - self._host = host - self._port = port - - self._on_action = on_action - - self._app_id = app_id - self._encryption_key = encryption_key - - self.state = None - self.available = False - self.volume = 0 - self.muted = False - self.playing = True - - self._control = None - - async def async_create_remote_control(self, during_setup=False): - """Create remote control.""" - control_existed = self._control is not None - try: - params = {} - if self._app_id and self._encryption_key: - params["app_id"] = self._app_id - params["encryption_key"] = self._encryption_key - - self._control = await self._hass.async_add_executor_job( - partial(RemoteControl, self._host, self._port, **params) - ) - - self.state = STATE_ON - self.available = True - except (TimeoutError, URLError, SOAPError, OSError) as err: - if control_existed or during_setup: - _LOGGER.error("Could not establish remote connection: %s", err) - - self._control = None - self.state = STATE_OFF - self.available = self._on_action is not None - except Exception as err: # pylint: disable=broad-except - if control_existed or during_setup: - _LOGGER.exception("An unknown error occurred: %s", err) - self._control = None - self.state = STATE_OFF - self.available = self._on_action is not None - - async def async_update(self): - """Update device data.""" - if self._control is None: - await self.async_create_remote_control() - return - - await self._handle_errors(self._update) - - async def _update(self): - """Retrieve the latest data.""" - self.muted = self._control.get_mute() - self.volume = self._control.get_volume() / 100 - - self.state = STATE_ON - self.available = True - - async def async_send_key(self, key): - """Send a key to the TV and handle exceptions.""" - await self._handle_errors(self._control.send_key, key) - - async def async_turn_on(self): - """Turn on the TV.""" - if self._on_action is not None: - await self._on_action.async_run() - self.state = STATE_ON - elif self.state != STATE_ON: - await self.async_send_key(Keys.power) - self.state = STATE_ON - - async def async_turn_off(self): - """Turn off the TV.""" - if self.state != STATE_OFF: - await self.async_send_key(Keys.power) - self.state = STATE_OFF - await self.async_update() - - async def async_set_mute(self, enable): - """Set mute based on 'enable'.""" - await self._handle_errors(self._control.set_mute, enable) - - async def async_set_volume(self, volume): - """Set volume level, range 0..1.""" - volume = int(volume * 100) - await self._handle_errors(self._control.set_volume, volume) - - async def async_play_media(self, media_type, media_id): - """Play media.""" - _LOGGER.debug("Play media: %s (%s)", media_id, media_type) - if media_type != MEDIA_TYPE_URL: _LOGGER.warning("Unsupported media_type: %s", media_type) return - await self._handle_errors(self._control.open_webpage, media_id) - - async def _handle_errors(self, func, *args): - """Handle errors from func, set available and reconnect if needed.""" - try: - await self._hass.async_add_executor_job(func, *args) - except EncryptionRequired: - _LOGGER.error("The connection couldn't be encrypted") - except (TimeoutError, URLError, SOAPError, OSError): - self.state = STATE_OFF - self.available = self._on_action is not None - await self.async_create_remote_control() + await self._remote.async_play_media(media_type, media_id) diff --git a/tests/components/panasonic_viera/test_init.py b/tests/components/panasonic_viera/test_init.py new file mode 100644 index 00000000000000..3e02ac3703a4bf --- /dev/null +++ b/tests/components/panasonic_viera/test_init.py @@ -0,0 +1,123 @@ +"""Test the Panasonic Viera setup process.""" +from unittest.mock import Mock + +from asynctest import patch + +from homeassistant.components.panasonic_viera.const import ( + CONF_APP_ID, + CONF_ENCRYPTION_KEY, + CONF_ON_ACTION, + DEFAULT_NAME, + DEFAULT_PORT, + DOMAIN, +) +from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +MOCK_CONFIG_DATA = { + CONF_HOST: "0.0.0.0", + CONF_NAME: DEFAULT_NAME, + CONF_PORT: DEFAULT_PORT, + CONF_ON_ACTION: None, +} + +MOCK_ENCRYPTION_DATA = { + CONF_APP_ID: "mock-app-id", + CONF_ENCRYPTION_KEY: "mock-encryption-key", +} + + +def get_mock_remote(): + """Return a mock remote.""" + mock_remote = Mock() + + async def async_create_remote_control(during_setup=False): + return + + mock_remote.async_create_remote_control = async_create_remote_control + + return mock_remote + + +async def test_setup_entry_encrypted(hass): + """Test setup with encrypted config entry.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=MOCK_CONFIG_DATA[CONF_HOST], + data={**MOCK_CONFIG_DATA, **MOCK_ENCRYPTION_DATA}, + ) + + mock_entry.add_to_hass(hass) + + mock_remote = get_mock_remote() + + with patch( + "homeassistant.components.panasonic_viera.Remote", return_value=mock_remote, + ): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("media_player.panasonic_viera_tv") + + assert state + assert state.name == DEFAULT_NAME + + +async def test_setup_entry_unencrypted(hass): + """Test setup with unencrypted config entry.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, unique_id=MOCK_CONFIG_DATA[CONF_HOST], data=MOCK_CONFIG_DATA, + ) + + mock_entry.add_to_hass(hass) + + mock_remote = get_mock_remote() + + with patch( + "homeassistant.components.panasonic_viera.Remote", return_value=mock_remote, + ): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("media_player.panasonic_viera_tv") + + assert state + assert state.name == DEFAULT_NAME + + +async def test_setup_config_flow_initiated(hass): + """Test if config flow is initiated in setup.""" + assert ( + await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_HOST: "0.0.0.0"}},) + is True + ) + + assert len(hass.config_entries.flow.async_progress()) == 1 + + +async def test_setup_unload_entry(hass): + """Test if config entry is unloaded.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, unique_id=MOCK_CONFIG_DATA[CONF_HOST], data=MOCK_CONFIG_DATA + ) + + mock_entry.add_to_hass(hass) + + mock_remote = get_mock_remote() + + with patch( + "homeassistant.components.panasonic_viera.Remote", return_value=mock_remote, + ): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + await hass.config_entries.async_unload(mock_entry.entry_id) + + assert mock_entry.state == ENTRY_STATE_NOT_LOADED + + state = hass.states.get("media_player.panasonic_viera_tv") + + assert state is None From 208fa84a279d99b3a30a0ee1f899e6898a4a0a05 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Apr 2020 19:40:31 -0500 Subject: [PATCH 19/58] Update excess powerwall logging to be debug (#34994) --- homeassistant/components/powerwall/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 336c3ac0bff20f..fa9c81533e7da5 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -124,9 +124,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_update_data(): """Fetch data from API endpoint.""" # Check if we had an error before - _LOGGER.info("Checking if update failed") + _LOGGER.debug("Checking if update failed") if not hass.data[DOMAIN][entry.entry_id][POWERWALL_API_CHANGED]: - _LOGGER.info("Updating data") + _LOGGER.debug("Updating data") try: return await hass.async_add_executor_job( _fetch_powerwall_data, power_wall From cfc0edff6b35abe0836776d162a65abe89be2ceb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Apr 2020 19:41:24 -0500 Subject: [PATCH 20/58] Log the rachio webhook url (#34992) --- homeassistant/components/rachio/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index a5c9f5ab0a9928..b84ccb8fa5d9c4 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -123,7 +123,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if not person.controllers: _LOGGER.error("No Rachio devices found in account %s", person.username) return False - _LOGGER.info("%d Rachio device(s) found", len(person.controllers)) + _LOGGER.info( + "%d Rachio device(s) found; The url %s must be accessible from the internet in order to receive updates", + len(person.controllers), + webhook_url, + ) # Enable component hass.data[DOMAIN][entry.entry_id] = person From a2efc079f128c15ca690886d1fa7bcd81e0dbe56 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Apr 2020 20:25:59 -0500 Subject: [PATCH 21/58] Add battery sensors to hunterdouglas_powerview (#34917) --- .coveragerc | 1 + .../hunterdouglas_powerview/__init__.py | 2 +- .../hunterdouglas_powerview/const.py | 2 + .../hunterdouglas_powerview/cover.py | 37 +------- .../hunterdouglas_powerview/entity.py | 33 +++++++ .../hunterdouglas_powerview/sensor.py | 86 +++++++++++++++++++ 6 files changed, 127 insertions(+), 34 deletions(-) create mode 100644 homeassistant/components/hunterdouglas_powerview/sensor.py diff --git a/.coveragerc b/.coveragerc index fde7cb637f2e71..59ef37cd9322c0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -313,6 +313,7 @@ omit = homeassistant/components/hue/light.py homeassistant/components/hunterdouglas_powerview/__init__.py homeassistant/components/hunterdouglas_powerview/scene.py + homeassistant/components/hunterdouglas_powerview/sensor.py homeassistant/components/hunterdouglas_powerview/cover.py homeassistant/components/hunterdouglas_powerview/entity.py homeassistant/components/hydrawise/* diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index 44ebf25a4f4c19..89dc610a6fca00 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -69,7 +69,7 @@ def _has_all_unique_hosts(value): ) -PLATFORMS = ["cover", "scene"] +PLATFORMS = ["cover", "scene", "sensor"] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hunterdouglas_powerview/const.py b/homeassistant/components/hunterdouglas_powerview/const.py index 9979cfb186c82e..17ff3821a7a64a 100644 --- a/homeassistant/components/hunterdouglas_powerview/const.py +++ b/homeassistant/components/hunterdouglas_powerview/const.py @@ -51,6 +51,8 @@ ROOM_ID = "id" SHADE_RESPONSE = "shade" +SHADE_BATTERY_LEVEL = "batteryStrength" +SHADE_BATTERY_LEVEL_MAX = 200 STATE_ATTRIBUTE_ROOM_NAME = "roomName" diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index 45fd798238f58e..8364b3273ca13e 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -5,7 +5,6 @@ from aiopvapi.helpers.constants import ATTR_POSITION1, ATTR_POSITION_DATA from aiopvapi.resources.shade import ( ATTR_POSKIND1, - ATTR_TYPE, MAX_POSITION, MIN_POSITION, factory as PvShade, @@ -28,13 +27,7 @@ COORDINATOR, DEVICE_INFO, DEVICE_MODEL, - DEVICE_SERIAL_NUMBER, DOMAIN, - FIRMWARE_BUILD, - FIRMWARE_IN_SHADE, - FIRMWARE_REVISION, - FIRMWARE_SUB_REVISION, - MANUFACTURER, PV_API, PV_ROOM_DATA, PV_SHADE_DATA, @@ -43,7 +36,7 @@ SHADE_RESPONSE, STATE_ATTRIBUTE_ROOM_NAME, ) -from .entity import HDEntity +from .entity import ShadeEntity _LOGGER = logging.getLogger(__name__) @@ -93,21 +86,19 @@ def hass_position_to_hd(hass_positon): return int(hass_positon / 100 * MAX_POSITION) -class PowerViewShade(HDEntity, CoverEntity): +class PowerViewShade(ShadeEntity, CoverEntity): """Representation of a powerview shade.""" def __init__(self, shade, name, room_data, coordinator, device_info): """Initialize the shade.""" room_id = shade.raw_data.get(ROOM_ID_IN_SHADE) - super().__init__(coordinator, device_info, shade.id) + super().__init__(coordinator, device_info, shade, name) self._shade = shade self._device_info = device_info self._is_opening = False self._is_closing = False - self._room_name = None self._last_action_timestamp = 0 self._scheduled_transition_update = None - self._name = name self._room_name = room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "") self._current_cover_position = MIN_POSITION self._coordinator = coordinator @@ -153,7 +144,7 @@ def device_class(self): @property def name(self): """Return the name of the shade.""" - return self._name + return self._shade_name async def async_close_cover(self, **kwargs): """Close the cover.""" @@ -268,26 +259,6 @@ async def _async_force_refresh_state(self): self._async_update_current_cover_position() self.async_write_ha_state() - @property - def device_info(self): - """Return the device_info of the device.""" - firmware = self._shade.raw_data[FIRMWARE_IN_SHADE] - sw_version = f"{firmware[FIRMWARE_REVISION]}.{firmware[FIRMWARE_SUB_REVISION]}.{firmware[FIRMWARE_BUILD]}" - model = self._shade.raw_data[ATTR_TYPE] - for shade in self._shade.shade_types: - if shade.shade_type == model: - model = shade.description - break - - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, - "model": str(model), - "sw_version": sw_version, - "manufacturer": MANUFACTURER, - "via_device": (DOMAIN, self._device_info[DEVICE_SERIAL_NUMBER]), - } - async def async_added_to_hass(self): """When entity is added to hass.""" self._async_update_current_cover_position() diff --git a/homeassistant/components/hunterdouglas_powerview/entity.py b/homeassistant/components/hunterdouglas_powerview/entity.py index 03d20e027b8bd4..3c98eeaf615434 100644 --- a/homeassistant/components/hunterdouglas_powerview/entity.py +++ b/homeassistant/components/hunterdouglas_powerview/entity.py @@ -1,5 +1,7 @@ """The nexia integration base entity.""" +from aiopvapi.resources.shade import ATTR_TYPE + import homeassistant.helpers.device_registry as dr from homeassistant.helpers.entity import Entity @@ -11,6 +13,7 @@ DEVICE_SERIAL_NUMBER, DOMAIN, FIRMWARE_BUILD, + FIRMWARE_IN_SHADE, FIRMWARE_REVISION, FIRMWARE_SUB_REVISION, MANUFACTURER, @@ -57,3 +60,33 @@ def device_info(self): "sw_version": sw_version, "manufacturer": MANUFACTURER, } + + +class ShadeEntity(HDEntity): + """Base class for hunter douglas shade entities.""" + + def __init__(self, coordinator, device_info, shade, shade_name): + """Initialize the shade.""" + super().__init__(coordinator, device_info, shade.id) + self._shade_name = shade_name + self._shade = shade + + @property + def device_info(self): + """Return the device_info of the device.""" + firmware = self._shade.raw_data[FIRMWARE_IN_SHADE] + sw_version = f"{firmware[FIRMWARE_REVISION]}.{firmware[FIRMWARE_SUB_REVISION]}.{firmware[FIRMWARE_BUILD]}" + model = self._shade.raw_data[ATTR_TYPE] + for shade in self._shade.shade_types: + if shade.shade_type == model: + model = shade.description + break + + return { + "identifiers": {(DOMAIN, self._shade.id)}, + "name": self._shade_name, + "model": str(model), + "sw_version": sw_version, + "manufacturer": MANUFACTURER, + "via_device": (DOMAIN, self._device_info[DEVICE_SERIAL_NUMBER]), + } diff --git a/homeassistant/components/hunterdouglas_powerview/sensor.py b/homeassistant/components/hunterdouglas_powerview/sensor.py new file mode 100644 index 00000000000000..794fdac3eac0af --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/sensor.py @@ -0,0 +1,86 @@ +"""Support for hunterdouglass_powerview sensors.""" +import logging + +from aiopvapi.resources.shade import factory as PvShade + +from homeassistant.const import DEVICE_CLASS_BATTERY, UNIT_PERCENTAGE +from homeassistant.core import callback + +from .const import ( + COORDINATOR, + DEVICE_INFO, + DOMAIN, + PV_API, + PV_SHADE_DATA, + SHADE_BATTERY_LEVEL, + SHADE_BATTERY_LEVEL_MAX, +) +from .entity import ShadeEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the hunter douglas shades sensors.""" + + pv_data = hass.data[DOMAIN][entry.entry_id] + shade_data = pv_data[PV_SHADE_DATA] + pv_request = pv_data[PV_API] + coordinator = pv_data[COORDINATOR] + device_info = pv_data[DEVICE_INFO] + + entities = [] + for raw_shade in shade_data.values(): + shade = PvShade(raw_shade, pv_request) + if SHADE_BATTERY_LEVEL not in shade.raw_data: + continue + name_before_refresh = shade.name + entities.append( + PowerViewShadeBatterySensor( + coordinator, device_info, shade, name_before_refresh + ) + ) + async_add_entities(entities) + + +class PowerViewShadeBatterySensor(ShadeEntity): + """Representation of an shade battery charge sensor.""" + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return UNIT_PERCENTAGE + + @property + def name(self): + """Name of the shade battery.""" + return f"{self._shade_name} Battery" + + @property + def device_class(self): + """Shade battery Class.""" + return DEVICE_CLASS_BATTERY + + @property + def unique_id(self): + """Shade battery Uniqueid.""" + return f"{self._unique_id}_charge" + + @property + def state(self): + """Get the current value in percentage.""" + return round( + self._shade.raw_data[SHADE_BATTERY_LEVEL] / SHADE_BATTERY_LEVEL_MAX * 100 + ) + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + self._coordinator.async_add_listener(self._async_update_shade_from_group) + ) + + @callback + def _async_update_shade_from_group(self): + """Update with new data from the coordinator.""" + self._shade.raw_data = self._coordinator.data[self._shade.id] + self.async_write_ha_state() From 5699cb8a09d1f2e16ab21412c8ab46ac72a10769 Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Thu, 30 Apr 2020 18:35:32 -0700 Subject: [PATCH 22/58] Add allow extra to totalconnect config schema (#34993) --- homeassistant/components/totalconnect/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py index fce67f71b24059..25b1141bd9b9e0 100644 --- a/homeassistant/components/totalconnect/__init__.py +++ b/homeassistant/components/totalconnect/__init__.py @@ -24,7 +24,8 @@ vol.Required(CONF_PASSWORD): cv.string, } ) - } + }, + extra=vol.ALLOW_EXTRA, ) From 793592b2b841de658c75147b65d44f1e13546867 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Apr 2020 23:05:06 -0500 Subject: [PATCH 23/58] Config flow for homekit (#34560) * Config flow for homekit Allows multiple homekit bridges to run HAP-python state is now stored at .storage/homekit.{entry_id}.state aids is now stored at .storage/homekit.{entry_id}.aids Overcomes 150 device limit by supporting multiple bridges. Name and port are now automatically allocated to avoid conflicts which was one of the main reasons pairing failed. YAML configuration remains available in order to offer entity specific configuration. Entries created by config flow can add and remove included domains and entities without having to restart * Fix services as there are multiple now * migrate in executor * drop title from strings * Update homeassistant/components/homekit/strings.json Co-authored-by: Paulus Schoutsen * Make auto_start advanced mode only, add coverage * put back title * more references * delete port since manual config is no longer needed Co-authored-by: Paulus Schoutsen --- homeassistant/components/homekit/__init__.py | 488 ++++++++++-------- .../components/homekit/accessories.py | 153 +++++- .../components/homekit/aidmanager.py | 12 +- .../components/homekit/config_flow.py | 301 +++++++++++ homeassistant/components/homekit/const.py | 16 +- .../components/homekit/manifest.json | 3 +- homeassistant/components/homekit/strings.json | 54 ++ .../components/homekit/translations/en.json | 54 ++ .../components/homekit/type_covers.py | 3 +- homeassistant/components/homekit/type_fans.py | 3 +- .../components/homekit/type_lights.py | 3 +- .../components/homekit/type_locks.py | 3 +- .../components/homekit/type_media_players.py | 3 +- .../homekit/type_security_systems.py | 3 +- .../components/homekit/type_sensors.py | 3 +- .../components/homekit/type_switches.py | 3 +- .../components/homekit/type_thermostats.py | 3 +- homeassistant/components/homekit/util.py | 107 +++- homeassistant/generated/config_flows.py | 1 + homeassistant/helpers/entityfilter.py | 30 +- tests/components/homekit/test_accessories.py | 8 +- tests/components/homekit/test_aidmanager.py | 15 +- tests/components/homekit/test_config_flow.py | 259 ++++++++++ .../homekit/test_get_accessories.py | 27 +- tests/components/homekit/test_homekit.py | 441 ++++++++++++++-- .../homekit/test_type_media_players.py | 1 + .../homekit/test_type_security_systems.py | 1 + tests/components/homekit/test_util.py | 44 +- tests/components/homekit/util.py | 34 ++ 29 files changed, 1754 insertions(+), 322 deletions(-) create mode 100644 homeassistant/components/homekit/config_flow.py create mode 100644 homeassistant/components/homekit/strings.json create mode 100644 homeassistant/components/homekit/translations/en.json create mode 100644 tests/components/homekit/test_config_flow.py create mode 100644 tests/components/homekit/util.py diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index c77ec36ccf3b4e..184fce2309bba7 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -1,4 +1,5 @@ """Support for Apple HomeKit.""" +import asyncio import ipaddress import logging @@ -6,41 +7,36 @@ import voluptuous as vol from zeroconf import InterfaceChoice -from homeassistant.components import cover, vacuum from homeassistant.components.binary_sensor import DEVICE_CLASS_BATTERY_CHARGING -from homeassistant.components.cover import DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE from homeassistant.components.http import HomeAssistantView -from homeassistant.components.media_player import DEVICE_CLASS_TV +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, - ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_SERVICE, - ATTR_SUPPORTED_FEATURES, - ATTR_UNIT_OF_MEASUREMENT, CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, - CONF_TYPE, DEVICE_CLASS_BATTERY, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_TEMPERATURE, - EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - UNIT_PERCENTAGE, ) -from homeassistant.core import callback -from homeassistant.exceptions import Unauthorized -from homeassistant.helpers import entity_registry +from homeassistant.core import CoreState, HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady, Unauthorized +from homeassistant.helpers import device_registry, entity_registry import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entityfilter import FILTER_SCHEMA +from homeassistant.helpers.entityfilter import ( + BASE_FILTER_SCHEMA, + CONF_EXCLUDE_DOMAINS, + CONF_EXCLUDE_ENTITIES, + CONF_INCLUDE_DOMAINS, + CONF_INCLUDE_ENTITIES, + convert_filter, +) from homeassistant.util import get_local_ip -from homeassistant.util.decorator import Registry +from .accessories import get_accessory from .aidmanager import AccessoryAidStorage from .const import ( AID_STORAGE, @@ -50,43 +46,41 @@ CONF_ADVERTISE_IP, CONF_AUTO_START, CONF_ENTITY_CONFIG, - CONF_FEATURE_LIST, + CONF_ENTRY_INDEX, CONF_FILTER, CONF_LINKED_BATTERY_CHARGING_SENSOR, CONF_LINKED_BATTERY_SENSOR, CONF_SAFE_MODE, CONF_ZEROCONF_DEFAULT_INTERFACE, + CONFIG_OPTIONS, DEFAULT_AUTO_START, DEFAULT_PORT, DEFAULT_SAFE_MODE, DEFAULT_ZEROCONF_DEFAULT_INTERFACE, - DEVICE_CLASS_CO, - DEVICE_CLASS_CO2, - DEVICE_CLASS_PM25, DOMAIN, EVENT_HOMEKIT_CHANGED, - HOMEKIT_FILE, + HOMEKIT, HOMEKIT_PAIRING_QR, HOMEKIT_PAIRING_QR_SECRET, + MANUFACTURER, SERVICE_HOMEKIT_RESET_ACCESSORY, SERVICE_HOMEKIT_START, - TYPE_FAUCET, - TYPE_OUTLET, - TYPE_SHOWER, - TYPE_SPRINKLER, - TYPE_SWITCH, - TYPE_VALVE, + SHUTDOWN_TIMEOUT, + UNDO_UPDATE_LISTENER, ) from .util import ( + dismiss_setup_message, + get_persist_fullpath_for_entry_id, + migrate_filesystem_state_data_for_primary_imported_entry_id, + port_is_available, + remove_state_files_for_entry_id, show_setup_message, validate_entity_config, - validate_media_player_features, ) _LOGGER = logging.getLogger(__name__) MAX_DEVICES = 150 -TYPES = Registry() # #### Driver Status #### STATUS_READY = 0 @@ -94,66 +88,139 @@ STATUS_STOPPED = 2 STATUS_WAIT = 3 -SWITCH_TYPES = { - TYPE_FAUCET: "Valve", - TYPE_OUTLET: "Outlet", - TYPE_SHOWER: "Valve", - TYPE_SPRINKLER: "Valve", - TYPE_SWITCH: "Switch", - TYPE_VALVE: "Valve", -} -CONFIG_SCHEMA = vol.Schema( +def _has_all_unique_names_and_ports(bridges): + """Validate that each homekit bridge configured has a unique name.""" + names = [bridge[CONF_NAME] for bridge in bridges] + ports = [bridge[CONF_PORT] for bridge in bridges] + vol.Schema(vol.Unique())(names) + vol.Schema(vol.Unique())(ports) + return bridges + + +BRIDGE_SCHEMA = vol.Schema( { - DOMAIN: vol.All( - { - vol.Optional(CONF_NAME, default=BRIDGE_NAME): vol.All( - cv.string, vol.Length(min=3, max=25) - ), - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_IP_ADDRESS): vol.All(ipaddress.ip_address, cv.string), - vol.Optional(CONF_ADVERTISE_IP): vol.All( - ipaddress.ip_address, cv.string - ), - vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean, - vol.Optional(CONF_SAFE_MODE, default=DEFAULT_SAFE_MODE): cv.boolean, - vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA, - vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config, - vol.Optional( - CONF_ZEROCONF_DEFAULT_INTERFACE, - default=DEFAULT_ZEROCONF_DEFAULT_INTERFACE, - ): cv.boolean, - } - ) + vol.Optional(CONF_NAME, default=BRIDGE_NAME): vol.All( + cv.string, vol.Length(min=3, max=25) + ), + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_IP_ADDRESS): vol.All(ipaddress.ip_address, cv.string), + vol.Optional(CONF_ADVERTISE_IP): vol.All(ipaddress.ip_address, cv.string), + vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean, + vol.Optional(CONF_SAFE_MODE, default=DEFAULT_SAFE_MODE): cv.boolean, + vol.Optional(CONF_FILTER, default={}): BASE_FILTER_SCHEMA, + vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config, + vol.Optional( + CONF_ZEROCONF_DEFAULT_INTERFACE, default=DEFAULT_ZEROCONF_DEFAULT_INTERFACE, + ): cv.boolean, }, extra=vol.ALLOW_EXTRA, ) +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.All(cv.ensure_list, [BRIDGE_SCHEMA], _has_all_unique_names_and_ports)}, + extra=vol.ALLOW_EXTRA, +) + + RESET_ACCESSORY_SERVICE_SCHEMA = vol.Schema( {vol.Required(ATTR_ENTITY_ID): cv.entity_ids} ) -async def async_setup(hass, config): - """Set up the HomeKit component.""" - _LOGGER.debug("Begin setup HomeKit") +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the HomeKit from yaml.""" - aid_storage = hass.data[AID_STORAGE] = AccessoryAidStorage(hass) - await aid_storage.async_initialize() + hass.data.setdefault(DOMAIN, {}) + + _async_register_events_and_services(hass) + + if DOMAIN not in config: + return True + + current_entries = hass.config_entries.async_entries(DOMAIN) + + entries_by_name = {entry.data[CONF_NAME]: entry for entry in current_entries} + + for index, conf in enumerate(config[DOMAIN]): + bridge_name = conf[CONF_NAME] + + if ( + bridge_name in entries_by_name + and entries_by_name[bridge_name].source == SOURCE_IMPORT + ): + entry = entries_by_name[bridge_name] + # If they alter the yaml config we import the changes + # since there currently is no practical way to support + # all the options in the UI at this time. + data = conf.copy() + options = {} + for key in CONFIG_OPTIONS: + options[key] = data[key] + del data[key] + + hass.config_entries.async_update_entry(entry, data=data, options=options) + continue + + conf[CONF_ENTRY_INDEX] = index + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf, + ) + ) + + return True - hass.http.register_view(HomeKitPairingQRView) - conf = config[DOMAIN] +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up HomeKit from a config entry.""" + _async_import_options_from_data_if_missing(hass, entry) + + conf = entry.data + options = entry.options + name = conf[CONF_NAME] port = conf[CONF_PORT] + _LOGGER.debug("Begin setup HomeKit for %s", name) + + # If the previous instance hasn't cleaned up yet + # we need to wait a bit + if not await hass.async_add_executor_job(port_is_available, port): + raise ConfigEntryNotReady + + if CONF_ENTRY_INDEX in conf and conf[CONF_ENTRY_INDEX] == 0: + _LOGGER.debug("Migrating legacy HomeKit data for %s", name) + hass.async_add_executor_job( + migrate_filesystem_state_data_for_primary_imported_entry_id, + hass, + entry.entry_id, + ) + + aid_storage = AccessoryAidStorage(hass, entry.entry_id) + + await aid_storage.async_initialize() + # These are yaml only ip_address = conf.get(CONF_IP_ADDRESS) advertise_ip = conf.get(CONF_ADVERTISE_IP) - auto_start = conf[CONF_AUTO_START] - safe_mode = conf[CONF_SAFE_MODE] - entity_filter = conf[CONF_FILTER] - entity_config = conf[CONF_ENTITY_CONFIG] + entity_config = conf.get(CONF_ENTITY_CONFIG, {}) + + auto_start = options.get(CONF_AUTO_START, DEFAULT_AUTO_START) + safe_mode = options.get(CONF_SAFE_MODE, DEFAULT_SAFE_MODE) + entity_filter = convert_filter( + options.get( + CONF_FILTER, + { + CONF_INCLUDE_DOMAINS: [], + CONF_EXCLUDE_DOMAINS: [], + CONF_INCLUDE_ENTITIES: [], + CONF_EXCLUDE_ENTITIES: [], + }, + ) + ) interface_choice = ( - InterfaceChoice.Default if conf.get(CONF_ZEROCONF_DEFAULT_INTERFACE) else None + InterfaceChoice.Default + if options.get(CONF_ZEROCONF_DEFAULT_INTERFACE) + else None ) homekit = HomeKit( @@ -166,20 +233,100 @@ async def async_setup(hass, config): safe_mode, advertise_ip, interface_choice, + entry.entry_id, ) await hass.async_add_executor_job(homekit.setup) + undo_listener = entry.add_update_listener(_async_update_listener) + + hass.data[DOMAIN][entry.entry_id] = { + AID_STORAGE: aid_storage, + HOMEKIT: homekit, + UNDO_UPDATE_LISTENER: undo_listener, + } + + if hass.state == CoreState.running: + await homekit.async_start() + elif auto_start: + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, homekit.async_start) + + return True + + +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Handle options update.""" + if entry.source == SOURCE_IMPORT: + return + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + + dismiss_setup_message(hass, entry.entry_id) + + hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() + + homekit = hass.data[DOMAIN][entry.entry_id][HOMEKIT] + + if homekit.status == STATUS_RUNNING: + await homekit.async_stop() + + for _ in range(0, SHUTDOWN_TIMEOUT): + if not await hass.async_add_executor_job( + port_is_available, entry.data[CONF_PORT] + ): + _LOGGER.info("Waiting for the HomeKit server to shutdown.") + await asyncio.sleep(1) + + hass.data[DOMAIN].pop(entry.entry_id) + + return True + + +async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry): + """Remove a config entry.""" + return await hass.async_add_executor_job( + remove_state_files_for_entry_id, hass, entry.entry_id + ) + + +@callback +def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry): + options = dict(entry.options) + data = dict(entry.data) + modified = False + for importable_option in CONFIG_OPTIONS: + if importable_option not in entry.options and importable_option in entry.data: + options[importable_option] = entry.data[importable_option] + del data[importable_option] + modified = True + + if modified: + hass.config_entries.async_update_entry(entry, data=data, options=options) + + +@callback +def _async_register_events_and_services(hass: HomeAssistant): + """Register events and services for HomeKit.""" + + hass.http.register_view(HomeKitPairingQRView) + def handle_homekit_reset_accessory(service): """Handle start HomeKit service call.""" - if homekit.status != STATUS_RUNNING: - _LOGGER.warning( - "HomeKit is not running. Either it is waiting to be " - "started or has been stopped." - ) - return + for entry_id in hass.data[DOMAIN]: + if HOMEKIT not in hass.data[DOMAIN][entry_id]: + continue + homekit = hass.data[DOMAIN][entry_id][HOMEKIT] + if homekit.status != STATUS_RUNNING: + _LOGGER.warning( + "HomeKit is not running. Either it is waiting to be " + "started or has been stopped." + ) + continue - entity_ids = service.data.get("entity_id") - homekit.reset_accessories(entity_ids) + entity_ids = service.data.get("entity_id") + homekit.reset_accessories(entity_ids) hass.services.async_register( DOMAIN, @@ -208,124 +355,24 @@ def async_describe_logbook_event(event): DOMAIN, EVENT_HOMEKIT_CHANGED, async_describe_logbook_event ) - if auto_start: - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, homekit.async_start) - return True - async def async_handle_homekit_service_start(service): """Handle start HomeKit service call.""" - if homekit.status != STATUS_READY: - _LOGGER.warning( - "HomeKit is not ready. Either it is already running or has " - "been stopped." - ) - return - await homekit.async_start() + for entry_id in hass.data[DOMAIN]: + if HOMEKIT not in hass.data[DOMAIN][entry_id]: + continue + homekit = hass.data[DOMAIN][entry_id][HOMEKIT] + if homekit.status != STATUS_READY: + _LOGGER.warning( + "HomeKit is not ready. Either it is already running or has " + "been stopped." + ) + continue + await homekit.async_start() hass.services.async_register( DOMAIN, SERVICE_HOMEKIT_START, async_handle_homekit_service_start ) - return True - - -def get_accessory(hass, driver, state, aid, config): - """Take state and return an accessory object if supported.""" - if not aid: - _LOGGER.warning( - 'The entity "%s" is not supported, since it ' - "generates an invalid aid, please change it.", - state.entity_id, - ) - return None - - a_type = None - name = config.get(CONF_NAME, state.name) - - if state.domain == "alarm_control_panel": - a_type = "SecuritySystem" - - elif state.domain in ("binary_sensor", "device_tracker", "person"): - a_type = "BinarySensor" - - elif state.domain == "climate": - a_type = "Thermostat" - - elif state.domain == "cover": - device_class = state.attributes.get(ATTR_DEVICE_CLASS) - features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - - if device_class in (DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE) and features & ( - cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE - ): - a_type = "GarageDoorOpener" - elif features & cover.SUPPORT_SET_POSITION: - a_type = "WindowCovering" - elif features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE): - a_type = "WindowCoveringBasic" - - elif state.domain == "fan": - a_type = "Fan" - - elif state.domain == "light": - a_type = "Light" - - elif state.domain == "lock": - a_type = "Lock" - - elif state.domain == "media_player": - device_class = state.attributes.get(ATTR_DEVICE_CLASS) - feature_list = config.get(CONF_FEATURE_LIST) - - if device_class == DEVICE_CLASS_TV: - a_type = "TelevisionMediaPlayer" - else: - if feature_list and validate_media_player_features(state, feature_list): - a_type = "MediaPlayer" - - elif state.domain == "sensor": - device_class = state.attributes.get(ATTR_DEVICE_CLASS) - unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - - if device_class == DEVICE_CLASS_TEMPERATURE or unit in ( - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - ): - a_type = "TemperatureSensor" - elif device_class == DEVICE_CLASS_HUMIDITY and unit == UNIT_PERCENTAGE: - a_type = "HumiditySensor" - elif device_class == DEVICE_CLASS_PM25 or DEVICE_CLASS_PM25 in state.entity_id: - a_type = "AirQualitySensor" - elif device_class == DEVICE_CLASS_CO: - a_type = "CarbonMonoxideSensor" - elif device_class == DEVICE_CLASS_CO2 or DEVICE_CLASS_CO2 in state.entity_id: - a_type = "CarbonDioxideSensor" - elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ("lm", "lx"): - a_type = "LightSensor" - - elif state.domain == "switch": - switch_type = config.get(CONF_TYPE, TYPE_SWITCH) - a_type = SWITCH_TYPES[switch_type] - - elif state.domain == "vacuum": - features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if features & (vacuum.SUPPORT_START | vacuum.SUPPORT_RETURN_HOME): - a_type = "DockVacuum" - else: - a_type = "Switch" - - elif state.domain in ("automation", "input_boolean", "remote", "scene", "script"): - a_type = "Switch" - - elif state.domain == "water_heater": - a_type = "WaterHeater" - - if a_type is None: - return None - - _LOGGER.debug('Add "%s" as "%s"', state.entity_id, a_type) - return TYPES[a_type](hass, driver, name, state.entity_id, aid, config) - class HomeKit: """Class to handle all actions between HomeKit and Home Assistant.""" @@ -341,6 +388,7 @@ def __init__( safe_mode, advertise_ip=None, interface_choice=None, + entry_id=None, ): """Initialize a HomeKit object.""" self.hass = hass @@ -352,6 +400,7 @@ def __init__( self._safe_mode = safe_mode self._advertise_ip = advertise_ip self._interface_choice = interface_choice + self._entry_id = entry_id self.status = STATUS_READY self.bridge = None @@ -363,25 +412,26 @@ def setup(self): from .accessories import HomeBridge, HomeDriver self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) - ip_addr = self._ip_address or get_local_ip() - path = self.hass.config.path(HOMEKIT_FILE) + persist_file = get_persist_fullpath_for_entry_id(self.hass, self._entry_id) self.driver = HomeDriver( self.hass, + self._entry_id, + self._name, address=ip_addr, port=self._port, - persist_file=path, + persist_file=persist_file, advertised_address=self._advertise_ip, interface_choice=self._interface_choice, ) self.bridge = HomeBridge(self.hass, self.driver, self._name) if self._safe_mode: - _LOGGER.debug("Safe_mode selected") + _LOGGER.debug("Safe_mode selected for %s", self._name) self.driver.safe_mode = True def reset_accessories(self, entity_ids): """Reset the accessory to load the latest configuration.""" - aid_storage = self.hass.data[AID_STORAGE] + aid_storage = self.hass.data[DOMAIN][self._entry_id][AID_STORAGE] removed = [] for entity_id in entity_ids: aid = aid_storage.get_or_allocate_aid_for_entity_id(entity_id) @@ -412,9 +462,9 @@ def add_bridge_accessory(self, state): ) return - aid = self.hass.data[AID_STORAGE].get_or_allocate_aid_for_entity_id( - state.entity_id - ) + aid = self.hass.data[DOMAIN][self._entry_id][ + AID_STORAGE + ].get_or_allocate_aid_for_entity_id(state.entity_id) conf = self._config.pop(state.entity_id, {}) # If an accessory cannot be created or added due to an exception # of any kind (usually in pyhap) it should not prevent @@ -437,6 +487,7 @@ def remove_bridge_accessory(self, aid): async def async_start(self, *args): """Start the accessory driver.""" + if self.status != STATUS_READY: return self.status = STATUS_WAIT @@ -459,6 +510,20 @@ async def async_start(self, *args): bridged_states.append(state) await self.hass.async_add_executor_job(self._start, bridged_states) + await self._async_register_bridge() + + async def _async_register_bridge(self): + """Register the bridge as a device so homekit_controller and exclude it from discovery.""" + registry = await device_registry.async_get_registry(self.hass) + registry.async_get_or_create( + config_entry_id=self._entry_id, + connections={ + (device_registry.CONNECTION_NETWORK_MAC, self.driver.state.mac) + }, + manufacturer=MANUFACTURER, + name=self._name, + model="Home Assistant HomeKit Bridge", + ) def _start(self, bridged_states): from . import ( # noqa: F401 pylint: disable=unused-import, import-outside-toplevel @@ -480,10 +545,14 @@ def _start(self, bridged_states): if not self.driver.state.paired: show_setup_message( - self.hass, self.driver.state.pincode, self.bridge.xhm_uri() + self.hass, + self._entry_id, + self._name, + self.driver.state.pincode, + self.bridge.xhm_uri(), ) - _LOGGER.debug("Driver start") + _LOGGER.debug("Driver start for %s", self._name) self.hass.add_job(self.driver.start) self.status = STATUS_RUNNING @@ -492,8 +561,7 @@ async def async_stop(self, *args): if self.status != STATUS_RUNNING: return self.status = STATUS_STOPPED - - _LOGGER.debug("Driver stop") + _LOGGER.debug("Driver stop for %s", self._name) self.hass.add_job(self.driver.stop) @callback @@ -539,9 +607,17 @@ class HomeKitPairingQRView(HomeAssistantView): # pylint: disable=no-self-use async def get(self, request): """Retrieve the pairing QRCode image.""" - if request.query_string != request.app["hass"].data[HOMEKIT_PAIRING_QR_SECRET]: + if not request.query_string: + raise Unauthorized() + entry_id, secret = request.query_string.split("-") + + if ( + entry_id not in request.app["hass"].data[DOMAIN] + or secret + != request.app["hass"].data[DOMAIN][entry_id][HOMEKIT_PAIRING_QR_SECRET] + ): raise Unauthorized() return web.Response( - body=request.app["hass"].data[HOMEKIT_PAIRING_QR], + body=request.app["hass"].data[DOMAIN][entry_id][HOMEKIT_PAIRING_QR], content_type="image/svg+xml", ) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index c3b42f0b6dc638..ddafbd8fa66a56 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -8,12 +8,26 @@ from pyhap.accessory_driver import AccessoryDriver from pyhap.const import CATEGORY_OTHER +from homeassistant.components import cover, vacuum +from homeassistant.components.cover import DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE +from homeassistant.components.media_player import DEVICE_CLASS_TV from homeassistant.const import ( ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, + ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_SERVICE, + ATTR_SUPPORTED_FEATURES, + ATTR_UNIT_OF_MEASUREMENT, + CONF_NAME, + CONF_TYPE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_TEMPERATURE, STATE_ON, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + UNIT_PERCENTAGE, __version__, ) from homeassistant.core import callback as ha_callback, split_entity_id @@ -22,6 +36,7 @@ track_point_in_utc_time, ) from homeassistant.util import dt as dt_util +from homeassistant.util.decorator import Registry from .const import ( ATTR_DISPLAY_NAME, @@ -31,21 +46,45 @@ CHAR_BATTERY_LEVEL, CHAR_CHARGING_STATE, CHAR_STATUS_LOW_BATTERY, + CONF_FEATURE_LIST, CONF_LINKED_BATTERY_CHARGING_SENSOR, CONF_LINKED_BATTERY_SENSOR, CONF_LOW_BATTERY_THRESHOLD, DEBOUNCE_TIMEOUT, DEFAULT_LOW_BATTERY_THRESHOLD, + DEVICE_CLASS_CO, + DEVICE_CLASS_CO2, + DEVICE_CLASS_PM25, EVENT_HOMEKIT_CHANGED, HK_CHARGING, HK_NOT_CHARGABLE, HK_NOT_CHARGING, MANUFACTURER, SERV_BATTERY_SERVICE, + TYPE_FAUCET, + TYPE_OUTLET, + TYPE_SHOWER, + TYPE_SPRINKLER, + TYPE_SWITCH, + TYPE_VALVE, +) +from .util import ( + convert_to_float, + dismiss_setup_message, + show_setup_message, + validate_media_player_features, ) -from .util import convert_to_float, dismiss_setup_message, show_setup_message _LOGGER = logging.getLogger(__name__) +SWITCH_TYPES = { + TYPE_FAUCET: "Valve", + TYPE_OUTLET: "Outlet", + TYPE_SHOWER: "Valve", + TYPE_SPRINKLER: "Valve", + TYPE_SWITCH: "Switch", + TYPE_VALVE: "Valve", +} +TYPES = Registry() def debounce(func): @@ -79,6 +118,104 @@ def wrapper(self, *args): return wrapper +def get_accessory(hass, driver, state, aid, config): + """Take state and return an accessory object if supported.""" + if not aid: + _LOGGER.warning( + 'The entity "%s" is not supported, since it ' + "generates an invalid aid, please change it.", + state.entity_id, + ) + return None + + a_type = None + name = config.get(CONF_NAME, state.name) + + if state.domain == "alarm_control_panel": + a_type = "SecuritySystem" + + elif state.domain in ("binary_sensor", "device_tracker", "person"): + a_type = "BinarySensor" + + elif state.domain == "climate": + a_type = "Thermostat" + + elif state.domain == "cover": + device_class = state.attributes.get(ATTR_DEVICE_CLASS) + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + if device_class in (DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE) and features & ( + cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE + ): + a_type = "GarageDoorOpener" + elif features & cover.SUPPORT_SET_POSITION: + a_type = "WindowCovering" + elif features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE): + a_type = "WindowCoveringBasic" + + elif state.domain == "fan": + a_type = "Fan" + + elif state.domain == "light": + a_type = "Light" + + elif state.domain == "lock": + a_type = "Lock" + + elif state.domain == "media_player": + device_class = state.attributes.get(ATTR_DEVICE_CLASS) + feature_list = config.get(CONF_FEATURE_LIST) + + if device_class == DEVICE_CLASS_TV: + a_type = "TelevisionMediaPlayer" + else: + if feature_list and validate_media_player_features(state, feature_list): + a_type = "MediaPlayer" + + elif state.domain == "sensor": + device_class = state.attributes.get(ATTR_DEVICE_CLASS) + unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + + if device_class == DEVICE_CLASS_TEMPERATURE or unit in ( + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + ): + a_type = "TemperatureSensor" + elif device_class == DEVICE_CLASS_HUMIDITY and unit == UNIT_PERCENTAGE: + a_type = "HumiditySensor" + elif device_class == DEVICE_CLASS_PM25 or DEVICE_CLASS_PM25 in state.entity_id: + a_type = "AirQualitySensor" + elif device_class == DEVICE_CLASS_CO: + a_type = "CarbonMonoxideSensor" + elif device_class == DEVICE_CLASS_CO2 or DEVICE_CLASS_CO2 in state.entity_id: + a_type = "CarbonDioxideSensor" + elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ("lm", "lx"): + a_type = "LightSensor" + + elif state.domain == "switch": + switch_type = config.get(CONF_TYPE, TYPE_SWITCH) + a_type = SWITCH_TYPES[switch_type] + + elif state.domain == "vacuum": + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if features & (vacuum.SUPPORT_START | vacuum.SUPPORT_RETURN_HOME): + a_type = "DockVacuum" + else: + a_type = "Switch" + + elif state.domain in ("automation", "input_boolean", "remote", "scene", "script"): + a_type = "Switch" + + elif state.domain == "water_heater": + a_type = "WaterHeater" + + if a_type is None: + return None + + _LOGGER.debug('Add "%s" as "%s"', state.entity_id, a_type) + return TYPES[a_type](hass, driver, name, state.entity_id, aid, config) + + class HomeAccessory(Accessory): """Adapter class for Accessory.""" @@ -327,19 +464,27 @@ def setup_message(self): class HomeDriver(AccessoryDriver): """Adapter class for AccessoryDriver.""" - def __init__(self, hass, **kwargs): + def __init__(self, hass, entry_id, bridge_name, **kwargs): """Initialize a AccessoryDriver object.""" super().__init__(**kwargs) self.hass = hass + self._entry_id = entry_id + self._bridge_name = bridge_name def pair(self, client_uuid, client_public): """Override super function to dismiss setup message if paired.""" success = super().pair(client_uuid, client_public) if success: - dismiss_setup_message(self.hass) + dismiss_setup_message(self.hass, self._entry_id) return success def unpair(self, client_uuid): """Override super function to show setup message if unpaired.""" super().unpair(client_uuid) - show_setup_message(self.hass, self.state.pincode, self.accessory.xhm_uri()) + show_setup_message( + self.hass, + self._entry_id, + self._bridge_name, + self.state.pincode, + self.accessory.xhm_uri(), + ) diff --git a/homeassistant/components/homekit/aidmanager.py b/homeassistant/components/homekit/aidmanager.py index 95181114e79e1d..487865f22ab9e1 100644 --- a/homeassistant/components/homekit/aidmanager.py +++ b/homeassistant/components/homekit/aidmanager.py @@ -15,13 +15,13 @@ from fnvhash import fnv1a_32 +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.storage import Store -from .const import DOMAIN +from .util import get_aid_storage_filename_for_entry_id -AID_MANAGER_STORAGE_KEY = f"{DOMAIN}.aids" AID_MANAGER_STORAGE_VERSION = 1 AID_MANAGER_SAVE_DELAY = 2 @@ -74,13 +74,13 @@ class AccessoryAidStorage: persist over reboots. """ - def __init__(self, hass: HomeAssistant): + def __init__(self, hass: HomeAssistant, entry: ConfigEntry): """Create a new entity map store.""" self.hass = hass - self.store = Store(hass, AID_MANAGER_STORAGE_VERSION, AID_MANAGER_STORAGE_KEY) self.allocations = {} self.allocated_aids = set() - + self._entry = entry + self.store = None self._entity_registry = None async def async_initialize(self): @@ -88,6 +88,8 @@ async def async_initialize(self): self._entity_registry = ( await self.hass.helpers.entity_registry.async_get_registry() ) + aidstore = get_aid_storage_filename_for_entry_id(self._entry) + self.store = Store(self.hass, AID_MANAGER_STORAGE_VERSION, aidstore) raw_storage = await self.store.async_load() if not raw_storage: diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py new file mode 100644 index 00000000000000..0f83b7a3c24fa1 --- /dev/null +++ b/homeassistant/components/homekit/config_flow.py @@ -0,0 +1,301 @@ +"""Config flow for HomeKit integration.""" +import logging +import random +import string + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import CONF_NAME, CONF_PORT +from homeassistant.core import callback, split_entity_id +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entityfilter import ( + CONF_EXCLUDE_DOMAINS, + CONF_EXCLUDE_ENTITIES, + CONF_INCLUDE_DOMAINS, + CONF_INCLUDE_ENTITIES, +) + +from .const import ( + CONF_AUTO_START, + CONF_FILTER, + CONF_SAFE_MODE, + CONF_ZEROCONF_DEFAULT_INTERFACE, + DEFAULT_AUTO_START, + DEFAULT_CONFIG_FLOW_PORT, + DEFAULT_SAFE_MODE, + DEFAULT_ZEROCONF_DEFAULT_INTERFACE, + SHORT_BRIDGE_NAME, +) +from .const import DOMAIN # pylint:disable=unused-import +from .util import find_next_available_port + +_LOGGER = logging.getLogger(__name__) + +CONF_DOMAINS = "domains" +SUPPORTED_DOMAINS = [ + "alarm_control_panel", + "automation", + "binary_sensor", + "climate", + "cover", + "demo", + "device_tracker", + "fan", + "input_boolean", + "light", + "lock", + "media_player", + "person", + "remote", + "scene", + "script", + "sensor", + "switch", + "vacuum", + "water_heater", +] + +DEFAULT_DOMAINS = [ + "alarm_control_panel", + "climate", + "cover", + "light", + "lock", + "media_player", + "switch", + "vacuum", + "water_heater", +] + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for HomeKit.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + def __init__(self): + """Initialize config flow.""" + self.homekit_data = {} + self.entry_title = None + + async def async_step_pairing(self, user_input=None): + """Pairing instructions.""" + if user_input is not None: + return self.async_create_entry( + title=self.entry_title, data=self.homekit_data + ) + return self.async_show_form( + step_id="pairing", + description_placeholders={CONF_NAME: self.homekit_data[CONF_NAME]}, + ) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + port = await self._async_available_port() + name = self._async_available_name() + title = f"{name}:{port}" + self.homekit_data = user_input.copy() + self.homekit_data[CONF_NAME] = name + self.homekit_data[CONF_PORT] = port + self.homekit_data[CONF_FILTER] = { + CONF_INCLUDE_DOMAINS: user_input[CONF_INCLUDE_DOMAINS], + CONF_INCLUDE_ENTITIES: [], + CONF_EXCLUDE_DOMAINS: [], + CONF_EXCLUDE_ENTITIES: [], + } + del self.homekit_data[CONF_INCLUDE_DOMAINS] + self.entry_title = title + return await self.async_step_pairing() + + default_domains = [] if self._async_current_entries() else DEFAULT_DOMAINS + setup_schema = vol.Schema( + { + vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): bool, + vol.Required( + CONF_INCLUDE_DOMAINS, default=default_domains + ): cv.multi_select(SUPPORTED_DOMAINS), + } + ) + + return self.async_show_form( + step_id="user", data_schema=setup_schema, errors=errors + ) + + async def async_step_import(self, user_input=None): + """Handle import from yaml.""" + if not self._async_is_unique_name_port(user_input): + return self.async_abort(reason="port_name_in_use") + return self.async_create_entry( + title=f"{user_input[CONF_NAME]}:{user_input[CONF_PORT]}", data=user_input + ) + + async def _async_available_port(self): + """Return an available port the bridge.""" + return await self.hass.async_add_executor_job( + find_next_available_port, DEFAULT_CONFIG_FLOW_PORT + ) + + @callback + def _async_available_name(self): + """Return an available for the bridge.""" + current_entries = self._async_current_entries() + + # We always pick a RANDOM name to avoid Zeroconf + # name collisions. If the name has been seen before + # pairing will probably fail. + acceptable_chars = string.ascii_uppercase + string.digits + trailer = "".join(random.choices(acceptable_chars, k=4)) + all_names = {entry.data[CONF_NAME] for entry in current_entries} + suggested_name = f"{SHORT_BRIDGE_NAME} {trailer}" + while suggested_name in all_names: + trailer = "".join(random.choices(acceptable_chars, k=4)) + suggested_name = f"{SHORT_BRIDGE_NAME} {trailer}" + + return suggested_name + + @callback + def _async_is_unique_name_port(self, user_input): + """Determine is a name or port is already used.""" + name = user_input[CONF_NAME] + port = user_input[CONF_PORT] + for entry in self._async_current_entries(): + if entry.data[CONF_NAME] == name or entry.data[CONF_PORT] == port: + return False + return True + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for tado.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Initialize options flow.""" + self.config_entry = config_entry + self.homekit_options = {} + + async def async_step_yaml(self, user_input=None): + """No options for yaml managed entries.""" + if user_input is not None: + # Apparently not possible to abort an options flow + # at the moment + return self.async_create_entry(title="", data=self.config_entry.options) + + return self.async_show_form(step_id="yaml") + + async def async_step_advanced(self, user_input=None): + """Choose advanced options.""" + if user_input is not None: + self.homekit_options.update(user_input) + del self.homekit_options[CONF_INCLUDE_DOMAINS] + return self.async_create_entry(title="", data=self.homekit_options) + + schema_base = {} + + if self.show_advanced_options: + schema_base[ + vol.Optional( + CONF_AUTO_START, + default=self.homekit_options.get( + CONF_AUTO_START, DEFAULT_AUTO_START + ), + ) + ] = bool + else: + self.homekit_options[CONF_AUTO_START] = self.homekit_options.get( + CONF_AUTO_START, DEFAULT_AUTO_START + ) + + schema_base.update( + { + vol.Optional( + CONF_SAFE_MODE, + default=self.homekit_options.get(CONF_SAFE_MODE, DEFAULT_SAFE_MODE), + ): bool, + vol.Optional( + CONF_ZEROCONF_DEFAULT_INTERFACE, + default=self.homekit_options.get( + CONF_ZEROCONF_DEFAULT_INTERFACE, + DEFAULT_ZEROCONF_DEFAULT_INTERFACE, + ), + ): bool, + } + ) + + return self.async_show_form( + step_id="advanced", data_schema=vol.Schema(schema_base) + ) + + async def async_step_exclude(self, user_input=None): + """Choose entities to exclude from the domain.""" + if user_input is not None: + self.homekit_options[CONF_FILTER] = { + CONF_INCLUDE_DOMAINS: self.homekit_options[CONF_INCLUDE_DOMAINS], + CONF_EXCLUDE_DOMAINS: self.homekit_options.get( + CONF_EXCLUDE_DOMAINS, [] + ), + CONF_INCLUDE_ENTITIES: self.homekit_options.get( + CONF_INCLUDE_ENTITIES, [] + ), + CONF_EXCLUDE_ENTITIES: user_input[CONF_EXCLUDE_ENTITIES], + } + return await self.async_step_advanced() + + entity_filter = self.homekit_options.get(CONF_FILTER, {}) + all_supported_entities = await self.hass.async_add_executor_job( + _get_entities_matching_domains, + self.hass, + self.homekit_options[CONF_INCLUDE_DOMAINS], + ) + data_schema = vol.Schema( + { + vol.Optional( + CONF_EXCLUDE_ENTITIES, + default=entity_filter.get(CONF_EXCLUDE_ENTITIES, []), + ): cv.multi_select(all_supported_entities), + } + ) + return self.async_show_form(step_id="exclude", data_schema=data_schema) + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + if self.config_entry.source == SOURCE_IMPORT: + return await self.async_step_yaml(user_input) + + if user_input is not None: + self.homekit_options.update(user_input) + return await self.async_step_exclude() + + self.homekit_options = dict(self.config_entry.options) + entity_filter = self.homekit_options.get(CONF_FILTER, {}) + + data_schema = vol.Schema( + { + vol.Optional( + CONF_INCLUDE_DOMAINS, + default=entity_filter.get(CONF_INCLUDE_DOMAINS, []), + ): cv.multi_select(SUPPORTED_DOMAINS) + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) + + +def _get_entities_matching_domains(hass, domains): + """List entities in the given domains.""" + included_domains = set(domains) + entity_ids = [ + state.entity_id + for state in hass.states.all() + if (split_entity_id(state.entity_id))[0] in included_domains + ] + entity_ids.sort() + return entity_ids diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index f0224ce71f40ae..ab0c15ee9a7eb9 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -1,13 +1,17 @@ """Constants used be the HomeKit component.""" + # #### Misc #### DEBOUNCE_TIMEOUT = 0.5 DEVICE_PRECISION_LEEWAY = 6 DOMAIN = "homekit" HOMEKIT_FILE = ".homekit.state" -HOMEKIT_NOTIFY_ID = 4663548 AID_STORAGE = "homekit-aid-allocations" HOMEKIT_PAIRING_QR = "homekit-pairing-qr" HOMEKIT_PAIRING_QR_SECRET = "homekit-pairing-qr-secret" +HOMEKIT = "homekit" +UNDO_UPDATE_LISTENER = "undo_update_listener" +SHUTDOWN_TIMEOUT = 30 +CONF_ENTRY_INDEX = "index" # #### Attributes #### ATTR_DISPLAY_NAME = "display_name" @@ -30,6 +34,7 @@ DEFAULT_AUTO_START = True DEFAULT_LOW_BATTERY_THRESHOLD = 20 DEFAULT_PORT = 51827 +DEFAULT_CONFIG_FLOW_PORT = 51828 DEFAULT_SAFE_MODE = False DEFAULT_ZEROCONF_DEFAULT_INTERFACE = False @@ -49,6 +54,7 @@ # #### String Constants #### BRIDGE_MODEL = "Bridge" BRIDGE_NAME = "Home Assistant Bridge" +SHORT_BRIDGE_NAME = "HASS Bridge" BRIDGE_SERIAL_NUMBER = "homekit.bridge" MANUFACTURER = "Home Assistant" @@ -203,3 +209,11 @@ HK_NOT_CHARGING = 0 HK_CHARGING = 1 HK_NOT_CHARGABLE = 2 + +# ### Config Options ### +CONFIG_OPTIONS = [ + CONF_FILTER, + CONF_AUTO_START, + CONF_ZEROCONF_DEFAULT_INTERFACE, + CONF_SAFE_MODE, +] diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 482cef57ca756b..27f83d996ad532 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -5,5 +5,6 @@ "requirements": ["HAP-python==2.8.2","fnvhash==0.1.0","PyQRCode==1.2.1","base36==0.1.1"], "dependencies": ["http"], "after_dependencies": ["logbook"], - "codeowners": ["@bdraco"] + "codeowners": ["@bdraco"], + "config_flow": true } diff --git a/homeassistant/components/homekit/strings.json b/homeassistant/components/homekit/strings.json new file mode 100644 index 00000000000000..ca5a67f536360f --- /dev/null +++ b/homeassistant/components/homekit/strings.json @@ -0,0 +1,54 @@ +{ + "title" : "HomeKit Bridge", + "options" : { + "step" : { + "yaml" : { + "title" : "Adjust HomeKit Bridge Options", + "description" : "This entry is controlled via YAML" + }, + "init" : { + "data" : { + "include_domains" : "[%key:component::homekit::config::step::user::data::include_domains%]" + }, + "description" : "Entities in the “Domains to include” will be bridged to HomeKit. You will be able to select which entities to exclude from this list on the next screen.", + "title" : "Select domains to bridge." + }, + "exclude" : { + "data" : { + "exclude_entities" : "Entities to exclude" + }, + "description" : "Choose the entities that you do NOT want to be bridged.", + "title" : "Exclude entities in selected domains from bridge" + }, + "advanced" : { + "data" : { + "auto_start" : "[%key:component::homekit::config::step::user::data::auto_start%]", + "safe_mode" : "Safe Mode (enable only if pairing fails)", + "zeroconf_default_interface" : "Use default zeroconf interface (enable if the bridge cannot be found in the Home app)" + }, + "description" : "These settings only need to be adjusted if the HomeKit bridge is not functional.", + "title" : "Advanced Configuration" + } + } + }, + "config" : { + "step" : { + "user" : { + "data" : { + "auto_start" : "Autostart (disable if using Z-Wave or other delayed start system)", + "include_domains" : "Domains to include" + }, + "description" : "A HomeKit Bridge will allow you to access your Home Assistant entities in HomeKit. HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML for the primary bridge.", + "title" : "Activate HomeKit Bridge" + }, + "pairing": { + "title": "Pair HomeKit Bridge", + "description": "As soon as the {name} bridge is ready, pairing will be available in “Notifications” as “HomeKit Bridge Setup”." + } + }, + "abort" : { + "port_name_in_use" : "A bridge with the same name or port is already configured." + } + } + } + \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/en.json b/homeassistant/components/homekit/translations/en.json new file mode 100644 index 00000000000000..ca5a67f536360f --- /dev/null +++ b/homeassistant/components/homekit/translations/en.json @@ -0,0 +1,54 @@ +{ + "title" : "HomeKit Bridge", + "options" : { + "step" : { + "yaml" : { + "title" : "Adjust HomeKit Bridge Options", + "description" : "This entry is controlled via YAML" + }, + "init" : { + "data" : { + "include_domains" : "[%key:component::homekit::config::step::user::data::include_domains%]" + }, + "description" : "Entities in the “Domains to include” will be bridged to HomeKit. You will be able to select which entities to exclude from this list on the next screen.", + "title" : "Select domains to bridge." + }, + "exclude" : { + "data" : { + "exclude_entities" : "Entities to exclude" + }, + "description" : "Choose the entities that you do NOT want to be bridged.", + "title" : "Exclude entities in selected domains from bridge" + }, + "advanced" : { + "data" : { + "auto_start" : "[%key:component::homekit::config::step::user::data::auto_start%]", + "safe_mode" : "Safe Mode (enable only if pairing fails)", + "zeroconf_default_interface" : "Use default zeroconf interface (enable if the bridge cannot be found in the Home app)" + }, + "description" : "These settings only need to be adjusted if the HomeKit bridge is not functional.", + "title" : "Advanced Configuration" + } + } + }, + "config" : { + "step" : { + "user" : { + "data" : { + "auto_start" : "Autostart (disable if using Z-Wave or other delayed start system)", + "include_domains" : "Domains to include" + }, + "description" : "A HomeKit Bridge will allow you to access your Home Assistant entities in HomeKit. HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML for the primary bridge.", + "title" : "Activate HomeKit Bridge" + }, + "pairing": { + "title": "Pair HomeKit Bridge", + "description": "As soon as the {name} bridge is ready, pairing will be available in “Notifications” as “HomeKit Bridge Setup”." + } + }, + "abort" : { + "port_name_in_use" : "A bridge with the same name or port is already configured." + } + } + } + \ No newline at end of file diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 987ba900bc840a..25d1782b392e08 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -26,8 +26,7 @@ STATE_OPENING, ) -from . import TYPES -from .accessories import HomeAccessory, debounce +from .accessories import TYPES, HomeAccessory, debounce from .const import ( CHAR_CURRENT_DOOR_STATE, CHAR_CURRENT_POSITION, diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index 291b3ffed907db..b80a65eede1abd 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -27,8 +27,7 @@ STATE_ON, ) -from . import TYPES -from .accessories import HomeAccessory +from .accessories import TYPES, HomeAccessory from .const import ( CHAR_ACTIVE, CHAR_ROTATION_DIRECTION, diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 8458c8351da4ae..3f4c2518202502 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -24,8 +24,7 @@ STATE_ON, ) -from . import TYPES -from .accessories import HomeAccessory +from .accessories import TYPES, HomeAccessory from .const import ( CHAR_BRIGHTNESS, CHAR_COLOR_TEMPERATURE, diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index 0d2a19ef089774..5697306bf32ec9 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -6,8 +6,7 @@ from homeassistant.components.lock import DOMAIN, STATE_LOCKED, STATE_UNLOCKED from homeassistant.const import ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN -from . import TYPES -from .accessories import HomeAccessory +from .accessories import TYPES, HomeAccessory from .const import CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE, SERV_LOCK _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index 78c11fc41f9ea4..154355a0da382a 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -37,8 +37,7 @@ STATE_UNKNOWN, ) -from . import TYPES -from .accessories import HomeAccessory +from .accessories import TYPES, HomeAccessory from .const import ( CHAR_ACTIVE, CHAR_ACTIVE_IDENTIFIER, diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 59e10a42c29e0b..8a2bb971cf15ff 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -18,8 +18,7 @@ STATE_ALARM_TRIGGERED, ) -from . import TYPES -from .accessories import HomeAccessory +from .accessories import TYPES, HomeAccessory from .const import ( CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE, diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 5bdf9a07f04a78..c37755bc1c5082 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -11,8 +11,7 @@ TEMP_CELSIUS, ) -from . import TYPES -from .accessories import HomeAccessory +from .accessories import TYPES, HomeAccessory from .const import ( CHAR_AIR_PARTICULATE_DENSITY, CHAR_AIR_QUALITY, diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index a1088f110e56d8..072c8681a50398 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -27,8 +27,7 @@ from homeassistant.core import split_entity_id from homeassistant.helpers.event import call_later -from . import TYPES -from .accessories import HomeAccessory +from .accessories import TYPES, HomeAccessory from .const import ( CHAR_ACTIVE, CHAR_IN_USE, diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 0da92ef3dba762..84cefced602573 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -52,8 +52,7 @@ UNIT_PERCENTAGE, ) -from . import TYPES -from .accessories import HomeAccessory +from .accessories import TYPES, HomeAccessory from .const import ( CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_CURRENT_HEATING_COOLING, diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index b8d98ad2304178..3ccf73d39250b6 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -2,7 +2,9 @@ from collections import OrderedDict, namedtuple import io import logging +import os import secrets +import socket import pyqrcode import voluptuous as vol @@ -15,8 +17,9 @@ CONF_TYPE, TEMP_CELSIUS, ) -from homeassistant.core import split_entity_id +from homeassistant.core import HomeAssistant, split_entity_id import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.storage import STORAGE_DIR import homeassistant.util.temperature as temp_util from .const import ( @@ -25,11 +28,12 @@ CONF_LINKED_BATTERY_SENSOR, CONF_LOW_BATTERY_THRESHOLD, DEFAULT_LOW_BATTERY_THRESHOLD, + DOMAIN, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, - HOMEKIT_NOTIFY_ID, + HOMEKIT_FILE, HOMEKIT_PAIRING_QR, HOMEKIT_PAIRING_QR_SECRET, TYPE_FAUCET, @@ -42,6 +46,7 @@ _LOGGER = logging.getLogger(__name__) +MAX_PORT = 65535 BASIC_INFO_SCHEMA = vol.Schema( { @@ -210,7 +215,7 @@ def speed_to_states(self, speed): return list(self.speed_ranges.keys())[0] -def show_setup_message(hass, pincode, uri): +def show_setup_message(hass, entry_id, bridge_name, pincode, uri): """Display persistent notification with setup information.""" pin = pincode.decode() _LOGGER.info("Pincode: %s", pin) @@ -220,23 +225,23 @@ def show_setup_message(hass, pincode, uri): url.svg(buffer, scale=5) pairing_secret = secrets.token_hex(32) - hass.data[HOMEKIT_PAIRING_QR] = buffer.getvalue() - hass.data[HOMEKIT_PAIRING_QR_SECRET] = pairing_secret + hass.data[DOMAIN][entry_id][HOMEKIT_PAIRING_QR] = buffer.getvalue() + hass.data[DOMAIN][entry_id][HOMEKIT_PAIRING_QR_SECRET] = pairing_secret message = ( - f"To set up Home Assistant in the Home App, " + f"To set up {bridge_name} in the Home App, " f"scan the QR code or enter the following code:\n" f"### {pin}\n" - f"![image](/api/homekit/pairingqr?{pairing_secret})" + f"![image](/api/homekit/pairingqr?{entry_id}-{pairing_secret})" ) hass.components.persistent_notification.create( - message, "HomeKit Setup", HOMEKIT_NOTIFY_ID + message, "HomeKit Bridge Setup", entry_id ) -def dismiss_setup_message(hass): +def dismiss_setup_message(hass, entry_id): """Dismiss persistent notification and remove QR code.""" - hass.components.persistent_notification.dismiss(HOMEKIT_NOTIFY_ID) + hass.components.persistent_notification.dismiss(entry_id) def convert_to_float(state): @@ -268,3 +273,85 @@ def density_to_air_quality(density): if density <= 150: return 4 return 5 + + +def get_persist_filename_for_entry_id(entry_id: str): + """Determine the filename of the homekit state file.""" + return f"{DOMAIN}.{entry_id}.state" + + +def get_aid_storage_filename_for_entry_id(entry_id: str): + """Determine the ilename of homekit aid storage file.""" + return f"{DOMAIN}.{entry_id}.aids" + + +def get_persist_fullpath_for_entry_id(hass: HomeAssistant, entry_id: str): + """Determine the path to the homekit state file.""" + return hass.config.path(STORAGE_DIR, get_persist_filename_for_entry_id(entry_id)) + + +def get_aid_storage_fullpath_for_entry_id(hass: HomeAssistant, entry_id: str): + """Determine the path to the homekit aid storage file.""" + return hass.config.path( + STORAGE_DIR, get_aid_storage_filename_for_entry_id(entry_id) + ) + + +def migrate_filesystem_state_data_for_primary_imported_entry_id( + hass: HomeAssistant, entry_id: str +): + """Migrate the old paths to the storage directory.""" + legacy_persist_file_path = hass.config.path(HOMEKIT_FILE) + if os.path.exists(legacy_persist_file_path): + os.rename( + legacy_persist_file_path, get_persist_fullpath_for_entry_id(hass, entry_id) + ) + + legacy_aid_storage_path = hass.config.path(STORAGE_DIR, "homekit.aids") + if os.path.exists(legacy_aid_storage_path): + os.rename( + legacy_aid_storage_path, + get_aid_storage_fullpath_for_entry_id(hass, entry_id), + ) + + +def remove_state_files_for_entry_id(hass: HomeAssistant, entry_id: str): + """Remove the state files from disk.""" + persist_file_path = get_persist_fullpath_for_entry_id(hass, entry_id) + aid_storage_path = get_aid_storage_fullpath_for_entry_id(hass, entry_id) + os.unlink(persist_file_path) + if os.path.exists(aid_storage_path): + os.unlink(aid_storage_path) + return True + + +def _get_test_socket(): + """Create a socket to test binding ports.""" + test_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + test_socket.setblocking(False) + test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + return test_socket + + +def port_is_available(port: int): + """Check to see if a port is available.""" + test_socket = _get_test_socket() + try: + test_socket.bind(("", port)) + except OSError: + return False + + return True + + +def find_next_available_port(start_port: int): + """Find the next available port starting with the given port.""" + test_socket = _get_test_socket() + for port in range(start_port, MAX_PORT): + try: + test_socket.bind(("", port)) + return port + except OSError: + if port == MAX_PORT: + raise + continue diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 6e259c2bf84afb..a65d1e2b52ac70 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -50,6 +50,7 @@ "harmony", "heos", "hisense_aehw4a1", + "homekit", "homekit_controller", "homematicip_cloud", "huawei_lte", diff --git a/homeassistant/helpers/entityfilter.py b/homeassistant/helpers/entityfilter.py index a9c3ab27eada58..f8dd83ccfcc128 100644 --- a/homeassistant/helpers/entityfilter.py +++ b/homeassistant/helpers/entityfilter.py @@ -12,7 +12,8 @@ CONF_EXCLUDE_ENTITIES = "exclude_entities" -def _convert_filter(config: Dict[str, List[str]]) -> Callable[[str], bool]: +def convert_filter(config: Dict[str, List[str]]) -> Callable[[str], bool]: + """Convert the filter schema into a filter.""" filt = generate_filter( config[CONF_INCLUDE_DOMAINS], config[CONF_INCLUDE_ENTITIES], @@ -24,22 +25,21 @@ def _convert_filter(config: Dict[str, List[str]]) -> Callable[[str], bool]: return filt -FILTER_SCHEMA = vol.All( - vol.Schema( - { - vol.Optional(CONF_EXCLUDE_DOMAINS, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_EXCLUDE_ENTITIES, default=[]): cv.entity_ids, - vol.Optional(CONF_INCLUDE_DOMAINS, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_INCLUDE_ENTITIES, default=[]): cv.entity_ids, - } - ), - _convert_filter, +BASE_FILTER_SCHEMA = vol.Schema( + { + vol.Optional(CONF_EXCLUDE_DOMAINS, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(CONF_EXCLUDE_ENTITIES, default=[]): cv.entity_ids, + vol.Optional(CONF_INCLUDE_DOMAINS, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(CONF_INCLUDE_ENTITIES, default=[]): cv.entity_ids, + } ) +FILTER_SCHEMA = vol.All(BASE_FILTER_SCHEMA, convert_filter) + def generate_filter( include_domains: List[str], diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index c4b61f68833fbf..e2fb79f56ce0e9 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -453,7 +453,9 @@ def test_home_driver(): pin = b"123-45-678" with patch("pyhap.accessory_driver.AccessoryDriver.__init__") as mock_driver: - driver = HomeDriver("hass", address=ip_address, port=port, persist_file=path) + driver = HomeDriver( + "hass", "entry_id", "name", address=ip_address, port=port, persist_file=path + ) mock_driver.assert_called_with(address=ip_address, port=port, persist_file=path) driver.state = Mock(pincode=pin) @@ -467,7 +469,7 @@ def test_home_driver(): driver.pair("client_uuid", "client_public") mock_pair.assert_called_with("client_uuid", "client_public") - mock_dissmiss_msg.assert_called_with("hass") + mock_dissmiss_msg.assert_called_with("hass", "entry_id") # unpair with patch("pyhap.accessory_driver.AccessoryDriver.unpair") as mock_unpair, patch( @@ -476,4 +478,4 @@ def test_home_driver(): driver.unpair("client_uuid") mock_unpair.assert_called_with("client_uuid") - mock_show_msg.assert_called_with("hass", pin, "X-HM://0") + mock_show_msg.assert_called_with("hass", "entry_id", "name", pin, "X-HM://0") diff --git a/tests/components/homekit/test_aidmanager.py b/tests/components/homekit/test_aidmanager.py index 258f26e78a61db..ff55ba9afa4900 100644 --- a/tests/components/homekit/test_aidmanager.py +++ b/tests/components/homekit/test_aidmanager.py @@ -5,8 +5,8 @@ import pytest from homeassistant.components.homekit.aidmanager import ( - AID_MANAGER_STORAGE_KEY, AccessoryAidStorage, + get_aid_storage_filename_for_entry_id, get_system_unique_id, ) from homeassistant.helpers import device_registry @@ -53,7 +53,7 @@ async def test_aid_generation(hass, device_reg, entity_reg): with patch( "homeassistant.components.homekit.aidmanager.AccessoryAidStorage.async_schedule_save" ): - aid_storage = AccessoryAidStorage(hass) + aid_storage = AccessoryAidStorage(hass, config_entry) await aid_storage.async_initialize() for _ in range(0, 2): @@ -110,7 +110,7 @@ async def test_aid_adler32_collision(hass, device_reg, entity_reg): with patch( "homeassistant.components.homekit.aidmanager.AccessoryAidStorage.async_schedule_save" ): - aid_storage = AccessoryAidStorage(hass) + aid_storage = AccessoryAidStorage(hass, config_entry) await aid_storage.async_initialize() seen_aids = set() @@ -129,8 +129,8 @@ async def test_aid_generation_no_unique_ids_handles_collision( hass, device_reg, entity_reg ): """Test colliding aids is stable.""" - - aid_storage = AccessoryAidStorage(hass) + config_entry = MockConfigEntry(domain="test", data={}) + aid_storage = AccessoryAidStorage(hass, config_entry) await aid_storage.async_initialize() seen_aids = set() @@ -394,7 +394,7 @@ async def test_aid_generation_no_unique_ids_handles_collision( await aid_storage.async_save() await hass.async_block_till_done() - aid_storage = AccessoryAidStorage(hass) + aid_storage = AccessoryAidStorage(hass, config_entry) await aid_storage.async_initialize() assert aid_storage.allocations == { @@ -620,6 +620,7 @@ async def test_aid_generation_no_unique_ids_handles_collision( "light.light99": 596247761, } - aid_storage_path = hass.config.path(STORAGE_DIR, AID_MANAGER_STORAGE_KEY) + aidstore = get_aid_storage_filename_for_entry_id(config_entry.entry_id) + aid_storage_path = hass.config.path(STORAGE_DIR, aidstore) if await hass.async_add_executor_job(os.path.exists, aid_storage_path): await hass.async_add_executor_job(os.unlink, aid_storage_path) diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py new file mode 100644 index 00000000000000..0fd0a51fd512ab --- /dev/null +++ b/tests/components/homekit/test_config_flow.py @@ -0,0 +1,259 @@ +"""Test the HomeKit config flow.""" +from asynctest import patch + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.homekit.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import CONF_NAME, CONF_PORT + +from tests.common import MockConfigEntry + + +def _mock_config_entry_with_options_populated(): + """Create a mock config entry with options populated.""" + return MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: "mock_name", CONF_PORT: 12345}, + options={ + "filter": { + "include_domains": [ + "fan", + "vacuum", + "media_player", + "climate", + "alarm_control_panel", + ], + "exclude_entities": ["climate.front_gate"], + }, + "auto_start": False, + "safe_mode": False, + "zeroconf_default_interface": True, + }, + ) + + +async def test_user_form(hass): + """Test we can setup a new instance.""" + 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.homekit.config_flow.find_next_available_port", + return_value=12345, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"auto_start": True, "include_domains": ["light"]}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "pairing" + + with patch( + "homeassistant.components.homekit.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.homekit.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {},) + + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["title"][:11] == "HASS Bridge" + bridge_name = (result3["title"].split(":"))[0] + assert result3["data"] == { + "auto_start": True, + "filter": { + "exclude_domains": [], + "exclude_entities": [], + "include_domains": ["light"], + "include_entities": [], + }, + "name": bridge_name, + "port": 12345, + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import(hass): + """Test we can import instance.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} + ) + entry.add_to_hass(hass) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_NAME: "mock_name", CONF_PORT: 12345}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "port_name_in_use" + + with patch( + "homeassistant.components.homekit.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.homekit.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_NAME: "othername", CONF_PORT: 56789}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "othername:56789" + assert result2["data"] == { + "name": "othername", + "port": 56789, + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 2 + + +async def test_options_flow_advanced(hass): + """Test config flow options.""" + + config_entry = _mock_config_entry_with_options_populated() + config_entry.add_to_hass(hass) + + hass.states.async_set("climate.old", "off") + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": True} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"include_domains": ["fan", "vacuum", "climate"]}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "exclude" + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"exclude_entities": ["climate.old"]}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "advanced" + + with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): + result3 = await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={ + "auto_start": True, + "safe_mode": True, + "zeroconf_default_interface": False, + }, + ) + + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + "auto_start": True, + "filter": { + "exclude_domains": [], + "exclude_entities": ["climate.old"], + "include_domains": ["fan", "vacuum", "climate"], + "include_entities": [], + }, + "safe_mode": True, + "zeroconf_default_interface": False, + } + + +async def test_options_flow_basic(hass): + """Test config flow options.""" + + config_entry = _mock_config_entry_with_options_populated() + config_entry.add_to_hass(hass) + + hass.states.async_set("climate.old", "off") + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init( + config_entry.entry_id, context={"show_advanced_options": False} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"include_domains": ["fan", "vacuum", "climate"]}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "exclude" + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"exclude_entities": ["climate.old"]}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "advanced" + + with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): + result3 = await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={"safe_mode": True, "zeroconf_default_interface": False}, + ) + + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == { + "auto_start": False, + "filter": { + "exclude_domains": [], + "exclude_entities": ["climate.old"], + "include_domains": ["fan", "vacuum", "climate"], + "include_entities": [], + }, + "safe_mode": True, + "zeroconf_default_interface": False, + } + + +async def test_options_flow_blocked_when_from_yaml(hass): + """Test config flow options.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: "mock_name", CONF_PORT: 12345}, + options={ + "auto_start": True, + "filter": { + "include_domains": [ + "fan", + "vacuum", + "media_player", + "climate", + "alarm_control_panel", + ], + "exclude_entities": ["climate.front_gate"], + }, + "safe_mode": False, + "zeroconf_default_interface": True, + }, + source=SOURCE_IMPORT, + ) + config_entry.add_to_hass(hass) + + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "yaml" + + with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 08a04d5b88ebee..286fe51535e6ab 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -5,7 +5,7 @@ import homeassistant.components.climate as climate import homeassistant.components.cover as cover -from homeassistant.components.homekit import TYPES, get_accessory +from homeassistant.components.homekit.accessories import TYPES, get_accessory from homeassistant.components.homekit.const import ( CONF_FEATURE_LIST, FEATURE_ON_OFF, @@ -17,6 +17,7 @@ TYPE_VALVE, ) import homeassistant.components.media_player.const as media_player_c +import homeassistant.components.vacuum as vacuum from homeassistant.const import ( ATTR_CODE, ATTR_DEVICE_CLASS, @@ -239,3 +240,27 @@ def test_type_switches(type_name, entity_id, state, attrs, config): entity_state = State(entity_id, state, attrs) get_accessory(None, None, entity_state, 2, config) assert mock_type.called + + +@pytest.mark.parametrize( + "type_name, entity_id, state, attrs", + [ + ( + "DockVacuum", + "vacuum.dock_vacuum", + "docked", + { + ATTR_SUPPORTED_FEATURES: vacuum.SUPPORT_START + | vacuum.SUPPORT_RETURN_HOME + }, + ), + ("Switch", "vacuum.basic_vacuum", "off", {},), + ], +) +def test_type_vacuum(type_name, entity_id, state, attrs): + """Test if vacuum types are associated correctly.""" + mock_type = Mock() + with patch.dict(TYPES, {type_name: mock_type}): + entity_state = State(entity_id, state, attrs) + get_accessory(None, None, entity_state, 2, {}) + assert mock_type.called diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 8a1d911ef04b73..e5bee83a0ebe23 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1,10 +1,11 @@ """Tests for the HomeKit component.""" +import os +from typing import Dict from unittest.mock import ANY, Mock, patch import pytest from zeroconf import InterfaceChoice -from homeassistant import setup from homeassistant.components.binary_sensor import DEVICE_CLASS_BATTERY_CHARGING from homeassistant.components.homekit import ( MAX_DEVICES, @@ -19,6 +20,7 @@ AID_STORAGE, BRIDGE_NAME, CONF_AUTO_START, + CONF_ENTRY_INDEX, CONF_SAFE_MODE, CONF_ZEROCONF_DEFAULT_INTERFACE, DEFAULT_PORT, @@ -28,6 +30,11 @@ SERVICE_HOMEKIT_RESET_ACCESSORY, SERVICE_HOMEKIT_START, ) +from homeassistant.components.homekit.util import ( + get_aid_storage_fullpath_for_entry_id, + get_persist_fullpath_for_entry_id, +) +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, @@ -42,13 +49,17 @@ from homeassistant.core import State from homeassistant.helpers import device_registry from homeassistant.helpers.entityfilter import generate_filter +from homeassistant.helpers.storage import STORAGE_DIR +from homeassistant.setup import async_setup_component +from homeassistant.util import json as json_util + +from .util import PATH_HOMEKIT, async_init_entry, async_init_integration from tests.async_mock import AsyncMock from tests.common import MockConfigEntry, mock_device_registry, mock_registry from tests.components.homekit.common import patch_debounce IP_ADDRESS = "127.0.0.1" -PATH_HOMEKIT = "homeassistant.components.homekit" @pytest.fixture @@ -73,11 +84,31 @@ def debounce_patcher(): async def test_setup_min(hass): """Test async_setup with min config options.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT}, + options={}, + ) + entry.add_to_hass(hass) + with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit: - assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + mock_homekit.return_value = homekit = Mock() + type(homekit).async_start = AsyncMock() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() mock_homekit.assert_any_call( - hass, BRIDGE_NAME, DEFAULT_PORT, None, ANY, {}, DEFAULT_SAFE_MODE, None, None + hass, + BRIDGE_NAME, + DEFAULT_PORT, + None, + ANY, + {}, + DEFAULT_SAFE_MODE, + None, + None, + entry.entry_id, ) assert mock_homekit().setup.called is True @@ -86,26 +117,27 @@ async def test_setup_min(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - mock_homekit().async_start.assert_called_with(ANY) + mock_homekit().async_start.assert_called() async def test_setup_auto_start_disabled(hass): """Test async_setup with auto start disabled and test service calls.""" - config = { - DOMAIN: { + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: "Test Name", CONF_PORT: 11111, CONF_IP_ADDRESS: "172.0.0.0"}, + options={ CONF_AUTO_START: False, - CONF_NAME: "Test Name", - CONF_PORT: 11111, - CONF_IP_ADDRESS: "172.0.0.0", CONF_SAFE_MODE: DEFAULT_SAFE_MODE, CONF_ZEROCONF_DEFAULT_INTERFACE: True, - } - } + }, + ) + entry.add_to_hass(hass) with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit: mock_homekit.return_value = homekit = Mock() type(homekit).async_start = AsyncMock() - assert await setup.async_setup_component(hass, DOMAIN, config) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() mock_homekit.assert_any_call( hass, @@ -117,6 +149,7 @@ async def test_setup_auto_start_disabled(hass): DEFAULT_SAFE_MODE, None, InterfaceChoice.Default, + entry.entry_id, ) assert mock_homekit().setup.called is True @@ -148,7 +181,23 @@ async def test_setup_auto_start_disabled(hass): async def test_homekit_setup(hass, hk_driver): """Test setup of bridge and driver.""" - homekit = HomeKit(hass, BRIDGE_NAME, DEFAULT_PORT, None, {}, {}, DEFAULT_SAFE_MODE) + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: "mock_name", CONF_PORT: 12345}, + source=SOURCE_IMPORT, + ) + homekit = HomeKit( + hass, + BRIDGE_NAME, + DEFAULT_PORT, + None, + {}, + {}, + DEFAULT_SAFE_MODE, + advertise_ip=None, + interface_choice=None, + entry_id=entry.entry_id, + ) with patch( f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver @@ -156,10 +205,12 @@ async def test_homekit_setup(hass, hk_driver): mock_ip.return_value = IP_ADDRESS await hass.async_add_executor_job(homekit.setup) - path = hass.config.path(HOMEKIT_FILE) + path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) assert isinstance(homekit.bridge, HomeBridge) mock_driver.assert_called_with( hass, + entry.entry_id, + BRIDGE_NAME, address=IP_ADDRESS, port=DEFAULT_PORT, persist_file=path, @@ -174,17 +225,36 @@ async def test_homekit_setup(hass, hk_driver): async def test_homekit_setup_ip_address(hass, hk_driver): """Test setup with given IP address.""" - homekit = HomeKit(hass, BRIDGE_NAME, DEFAULT_PORT, "172.0.0.0", {}, {}, None) + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: "mock_name", CONF_PORT: 12345}, + source=SOURCE_IMPORT, + ) + homekit = HomeKit( + hass, + BRIDGE_NAME, + DEFAULT_PORT, + "172.0.0.0", + {}, + {}, + None, + None, + interface_choice=None, + entry_id=entry.entry_id, + ) + path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) with patch( f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver ) as mock_driver: await hass.async_add_executor_job(homekit.setup) mock_driver.assert_called_with( hass, + entry.entry_id, + BRIDGE_NAME, address="172.0.0.0", port=DEFAULT_PORT, - persist_file=ANY, + persist_file=path, advertised_address=None, interface_choice=None, ) @@ -192,19 +262,36 @@ async def test_homekit_setup_ip_address(hass, hk_driver): async def test_homekit_setup_advertise_ip(hass, hk_driver): """Test setup with given IP address to advertise.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: "mock_name", CONF_PORT: 12345}, + source=SOURCE_IMPORT, + ) homekit = HomeKit( - hass, BRIDGE_NAME, DEFAULT_PORT, "0.0.0.0", {}, {}, None, "192.168.1.100" + hass, + BRIDGE_NAME, + DEFAULT_PORT, + "0.0.0.0", + {}, + {}, + None, + "192.168.1.100", + interface_choice=None, + entry_id=entry.entry_id, ) + path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) with patch( f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver ) as mock_driver: await hass.async_add_executor_job(homekit.setup) mock_driver.assert_called_with( hass, + entry.entry_id, + BRIDGE_NAME, address="0.0.0.0", port=DEFAULT_PORT, - persist_file=ANY, + persist_file=path, advertised_address="192.168.1.100", interface_choice=None, ) @@ -212,6 +299,11 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver): async def test_homekit_setup_interface_choice(hass, hk_driver): """Test setup with interface choice of Default.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: "mock_name", CONF_PORT: 12345}, + source=SOURCE_IMPORT, + ) homekit = HomeKit( hass, BRIDGE_NAME, @@ -222,17 +314,21 @@ async def test_homekit_setup_interface_choice(hass, hk_driver): None, None, InterfaceChoice.Default, + entry_id=entry.entry_id, ) + path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) with patch( f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver ) as mock_driver: await hass.async_add_executor_job(homekit.setup) mock_driver.assert_called_with( hass, + entry.entry_id, + BRIDGE_NAME, address="0.0.0.0", port=DEFAULT_PORT, - persist_file=ANY, + persist_file=path, advertised_address=None, interface_choice=InterfaceChoice.Default, ) @@ -240,7 +336,23 @@ async def test_homekit_setup_interface_choice(hass, hk_driver): async def test_homekit_setup_safe_mode(hass, hk_driver): """Test if safe_mode flag is set.""" - homekit = HomeKit(hass, BRIDGE_NAME, DEFAULT_PORT, None, {}, {}, True, None) + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: "mock_name", CONF_PORT: 12345}, + source=SOURCE_IMPORT, + ) + homekit = HomeKit( + hass, + BRIDGE_NAME, + DEFAULT_PORT, + None, + {}, + {}, + True, + advertise_ip=None, + interface_choice=None, + entry_id=entry.entry_id, + ) with patch(f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver): await hass.async_add_executor_job(homekit.setup) @@ -249,12 +361,25 @@ async def test_homekit_setup_safe_mode(hass, hk_driver): async def test_homekit_add_accessory(hass): """Add accessory if config exists and get_acc returns an accessory.""" - homekit = HomeKit(hass, None, None, None, lambda entity_id: True, {}, None, None) + entry = await async_init_integration(hass) + + homekit = HomeKit( + hass, + None, + None, + None, + lambda entity_id: True, + {}, + DEFAULT_SAFE_MODE, + advertise_ip=None, + interface_choice=None, + entry_id=entry.entry_id, + ) homekit.driver = "driver" homekit.bridge = mock_bridge = Mock() homekit.bridge.accessories = range(10) - assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await async_init_integration(hass) with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc: mock_get_acc.side_effect = [None, "acc", None] @@ -273,7 +398,20 @@ async def test_homekit_add_accessory(hass): async def test_homekit_remove_accessory(hass): """Remove accessory from bridge.""" - homekit = HomeKit("hass", None, None, None, lambda entity_id: True, {}, None, None) + entry = await async_init_integration(hass) + + homekit = HomeKit( + hass, + None, + None, + None, + lambda entity_id: True, + {}, + DEFAULT_SAFE_MODE, + advertise_ip=None, + interface_choice=None, + entry_id=entry.entry_id, + ) homekit.driver = "driver" homekit.bridge = mock_bridge = Mock() mock_bridge.accessories = {"light.demo": "acc"} @@ -285,10 +423,21 @@ async def test_homekit_remove_accessory(hass): async def test_homekit_entity_filter(hass): """Test the entity filter.""" - assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + entry = await async_init_integration(hass) entity_filter = generate_filter(["cover"], ["demo.test"], [], []) - homekit = HomeKit(hass, None, None, None, entity_filter, {}, None, None) + homekit = HomeKit( + hass, + None, + None, + None, + entity_filter, + {}, + DEFAULT_SAFE_MODE, + advertise_ip=None, + interface_choice=None, + entry_id=entry.entry_id, + ) homekit.bridge = Mock() homekit.bridge.accessories = {} @@ -309,8 +458,21 @@ async def test_homekit_entity_filter(hass): async def test_homekit_start(hass, hk_driver, debounce_patcher): """Test HomeKit start method.""" + entry = await async_init_integration(hass) + pin = b"123-45-678" - homekit = HomeKit(hass, None, None, None, {}, {"cover.demo": {}}, None, None) + homekit = HomeKit( + hass, + None, + None, + None, + {}, + {}, + DEFAULT_SAFE_MODE, + advertise_ip=None, + interface_choice=None, + entry_id=entry.entry_id, + ) homekit.bridge = Mock() homekit.bridge.accessories = [] homekit.driver = hk_driver @@ -330,7 +492,7 @@ async def test_homekit_start(hass, hk_driver, debounce_patcher): await hass.async_block_till_done() mock_add_acc.assert_called_with(state) - mock_setup_msg.assert_called_with(hass, pin, ANY) + mock_setup_msg.assert_called_with(hass, entry.entry_id, None, pin, ANY) hk_driver_add_acc.assert_called_with(homekit.bridge) assert hk_driver_start.called assert homekit.status == STATUS_RUNNING @@ -345,11 +507,25 @@ async def test_homekit_start(hass, hk_driver, debounce_patcher): async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, debounce_patcher): """Test HomeKit start method.""" pin = b"123-45-678" + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} + ) entity_filter = generate_filter(["cover", "light"], ["demo.test"], [], []) - assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await async_init_entry(hass, entry) + homekit = HomeKit( + hass, + None, + None, + None, + entity_filter, + {}, + DEFAULT_SAFE_MODE, + advertise_ip=None, + interface_choice=None, + entry_id=entry.entry_id, + ) - homekit = HomeKit(hass, None, None, None, entity_filter, {}, None, None) homekit.bridge = Mock() homekit.bridge.accessories = [] homekit.driver = hk_driver @@ -367,7 +543,7 @@ async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, debounce_p await homekit.async_start() await hass.async_block_till_done() - mock_setup_msg.assert_called_with(hass, pin, ANY) + mock_setup_msg.assert_called_with(hass, entry.entry_id, None, pin, ANY) hk_driver_add_acc.assert_called_with(homekit.bridge) assert hk_driver_start.called assert homekit.status == STATUS_RUNNING @@ -381,10 +557,23 @@ async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, debounce_p async def test_homekit_stop(hass): """Test HomeKit stop method.""" - homekit = HomeKit(hass, None, None, None, None, None, None) + entry = await async_init_integration(hass) + + homekit = HomeKit( + hass, + None, + None, + None, + {}, + {}, + DEFAULT_SAFE_MODE, + advertise_ip=None, + interface_choice=None, + entry_id=entry.entry_id, + ) homekit.driver = Mock() - assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await async_init_integration(hass) assert homekit.status == STATUS_READY await homekit.async_stop() @@ -406,8 +595,23 @@ async def test_homekit_stop(hass): async def test_homekit_reset_accessories(hass): """Test adding too many accessories to HomeKit.""" + + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} + ) entity_id = "light.demo" - homekit = HomeKit(hass, None, None, None, {}, {entity_id: {}}, None) + homekit = HomeKit( + hass, + None, + None, + None, + {}, + {entity_id: {}}, + DEFAULT_SAFE_MODE, + advertise_ip=None, + interface_choice=None, + entry_id=entry.entry_id, + ) homekit.bridge = Mock() homekit.bridge.accessories = {} @@ -415,11 +619,14 @@ async def test_homekit_reset_accessories(hass): f"{PATH_HOMEKIT}.HomeKit.setup" ), patch("pyhap.accessory.Bridge.add_accessory") as mock_add_accessory, patch( "pyhap.accessory_driver.AccessoryDriver.config_changed" - ) as hk_driver_config_changed: - - assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + ) as hk_driver_config_changed, patch( + "pyhap.accessory_driver.AccessoryDriver.start" + ): + await async_init_entry(hass, entry) - aid = hass.data[AID_STORAGE].get_or_allocate_aid_for_entity_id(entity_id) + aid = hass.data[DOMAIN][entry.entry_id][ + AID_STORAGE + ].get_or_allocate_aid_for_entity_id(entity_id) homekit.bridge.accessories = {aid: "acc"} homekit.status = STATUS_RUNNING @@ -438,10 +645,22 @@ async def test_homekit_reset_accessories(hass): async def test_homekit_too_many_accessories(hass, hk_driver): """Test adding too many accessories to HomeKit.""" + entry = await async_init_integration(hass) entity_filter = generate_filter(["cover", "light"], ["demo.test"], [], []) - homekit = HomeKit(hass, None, None, None, entity_filter, {}, None, None) + homekit = HomeKit( + hass, + None, + None, + None, + entity_filter, + {}, + DEFAULT_SAFE_MODE, + advertise_ip=None, + interface_choice=None, + entry_id=entry.entry_id, + ) homekit.bridge = Mock() # The bridge itself counts as an accessory homekit.bridge.accessories = range(MAX_DEVICES) @@ -463,9 +682,20 @@ async def test_homekit_finds_linked_batteries( hass, hk_driver, debounce_patcher, device_reg, entity_reg ): """Test HomeKit start method.""" - assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + entry = await async_init_integration(hass) - homekit = HomeKit(hass, None, None, None, {}, {"light.demo": {}}, None, None) + homekit = HomeKit( + hass, + None, + None, + None, + {}, + {"light.demo": {}}, + DEFAULT_SAFE_MODE, + advertise_ip=None, + interface_choice=None, + entry_id=entry.entry_id, + ) homekit.driver = hk_driver homekit._filter = Mock(return_value=True) homekit.bridge = HomeBridge(hass, hk_driver, "mock_bridge") @@ -526,3 +756,132 @@ def _mock_get_accessory(*args, **kwargs): "linked_battery_sensor": "sensor.light_battery", }, ) + + +async def test_setup_imported(hass): + """Test async_setup with imported config options.""" + legacy_persist_file_path = hass.config.path(HOMEKIT_FILE) + legacy_aid_storage_path = hass.config.path(STORAGE_DIR, "homekit.aids") + legacy_homekit_state_contents = {"homekit.state": 1} + legacy_homekit_aids_contents = {"homekit.aids": 1} + await hass.async_add_executor_job( + _write_data, legacy_persist_file_path, legacy_homekit_state_contents + ) + await hass.async_add_executor_job( + _write_data, legacy_aid_storage_path, legacy_homekit_aids_contents + ) + + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_IMPORT, + data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT, CONF_ENTRY_INDEX: 0}, + options={}, + ) + entry.add_to_hass(hass) + + with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit: + mock_homekit.return_value = homekit = Mock() + type(homekit).async_start = AsyncMock() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mock_homekit.assert_any_call( + hass, + BRIDGE_NAME, + DEFAULT_PORT, + None, + ANY, + {}, + DEFAULT_SAFE_MODE, + None, + None, + entry.entry_id, + ) + assert mock_homekit().setup.called is True + + # Test auto start enabled + mock_homekit.reset_mock() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + mock_homekit().async_start.assert_called() + + migrated_persist_file_path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) + assert ( + await hass.async_add_executor_job( + json_util.load_json, migrated_persist_file_path + ) + == legacy_homekit_state_contents + ) + os.unlink(migrated_persist_file_path) + migrated_aid_file_path = get_aid_storage_fullpath_for_entry_id(hass, entry.entry_id) + assert ( + await hass.async_add_executor_job(json_util.load_json, migrated_aid_file_path) + == legacy_homekit_aids_contents + ) + os.unlink(migrated_aid_file_path) + + +async def test_yaml_updates_update_config_entry_for_name(hass): + """Test async_setup with imported config.""" + + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_IMPORT, + data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT}, + options={}, + ) + entry.add_to_hass(hass) + + with patch(f"{PATH_HOMEKIT}.HomeKit") as mock_homekit: + mock_homekit.return_value = homekit = Mock() + type(homekit).async_start = AsyncMock() + assert await async_setup_component( + hass, "homekit", {"homekit": {CONF_NAME: BRIDGE_NAME, CONF_PORT: 12345}} + ) + await hass.async_block_till_done() + + mock_homekit.assert_any_call( + hass, + BRIDGE_NAME, + 12345, + None, + ANY, + {}, + DEFAULT_SAFE_MODE, + None, + None, + entry.entry_id, + ) + assert mock_homekit().setup.called is True + + # Test auto start enabled + mock_homekit.reset_mock() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + mock_homekit().async_start.assert_called() + + +async def test_raise_config_entry_not_ready(hass): + """Test async_setup when the port is not available.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_NAME: BRIDGE_NAME, CONF_PORT: DEFAULT_PORT}, + options={}, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homekit.port_is_available", return_value=False, + ): + assert not await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + +def _write_data(path: str, data: Dict) -> None: + """Write the data.""" + if not os.path.isdir(os.path.dirname(path)): + os.makedirs(os.path.dirname(path)) + json_util.save_json(path, data) diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index 5fe8c438ca1b8f..cb2de7264a8b2f 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -348,6 +348,7 @@ async def test_media_player_television_basic(hass, hk_driver, events, caplog): await hass.async_block_till_done() acc = TelevisionMediaPlayer(hass, hk_driver, "MediaPlayer", entity_id, 2, None) await acc.run_handler() + await hass.async_block_till_done() assert acc.chars_tv == [] assert acc.chars_speaker == [] diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 690cd8f318ffdc..b139fac36577ee 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -28,6 +28,7 @@ async def test_switch_set_state(hass, hk_driver, events): await hass.async_block_till_done() acc = SecuritySystem(hass, hk_driver, "SecuritySystem", entity_id, 2, config) await acc.run_handler() + await hass.async_block_till_done() assert acc.aid == 2 assert acc.category == 11 # AlarmSystem diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 41b134c10a5f08..2c8c93cee4c342 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -7,9 +7,10 @@ CONF_FEATURE_LIST, CONF_LINKED_BATTERY_SENSOR, CONF_LOW_BATTERY_THRESHOLD, + DEFAULT_CONFIG_FLOW_PORT, + DOMAIN, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, - HOMEKIT_NOTIFY_ID, HOMEKIT_PAIRING_QR, HOMEKIT_PAIRING_QR_SECRET, TYPE_FAUCET, @@ -25,6 +26,8 @@ convert_to_float, density_to_air_quality, dismiss_setup_message, + find_next_available_port, + port_is_available, show_setup_message, temperature_to_homekit, temperature_to_states, @@ -34,7 +37,7 @@ from homeassistant.components.persistent_notification import ( ATTR_MESSAGE, ATTR_NOTIFICATION_ID, - DOMAIN, + DOMAIN as PERSISTENT_NOTIFICATION_DOMAIN, ) from homeassistant.const import ( ATTR_CODE, @@ -47,6 +50,8 @@ ) from homeassistant.core import State +from .util import async_init_integration + from tests.common import async_mock_service @@ -199,27 +204,36 @@ async def test_show_setup_msg(hass): """Test show setup message as persistence notification.""" pincode = b"123-45-678" - call_create_notification = async_mock_service(hass, DOMAIN, "create") + entry = await async_init_integration(hass) + assert entry + + call_create_notification = async_mock_service( + hass, PERSISTENT_NOTIFICATION_DOMAIN, "create" + ) - await hass.async_add_executor_job(show_setup_message, hass, pincode, "X-HM://0") + await hass.async_add_executor_job( + show_setup_message, hass, entry.entry_id, "bridge_name", pincode, "X-HM://0" + ) await hass.async_block_till_done() - assert hass.data[HOMEKIT_PAIRING_QR_SECRET] - assert hass.data[HOMEKIT_PAIRING_QR] + assert hass.data[DOMAIN][entry.entry_id][HOMEKIT_PAIRING_QR_SECRET] + assert hass.data[DOMAIN][entry.entry_id][HOMEKIT_PAIRING_QR] assert call_create_notification - assert call_create_notification[0].data[ATTR_NOTIFICATION_ID] == HOMEKIT_NOTIFY_ID + assert call_create_notification[0].data[ATTR_NOTIFICATION_ID] == entry.entry_id assert pincode.decode() in call_create_notification[0].data[ATTR_MESSAGE] async def test_dismiss_setup_msg(hass): """Test dismiss setup message.""" - call_dismiss_notification = async_mock_service(hass, DOMAIN, "dismiss") + call_dismiss_notification = async_mock_service( + hass, PERSISTENT_NOTIFICATION_DOMAIN, "dismiss" + ) - await hass.async_add_executor_job(dismiss_setup_message, hass) + await hass.async_add_executor_job(dismiss_setup_message, hass, "entry_id") await hass.async_block_till_done() assert call_dismiss_notification - assert call_dismiss_notification[0].data[ATTR_NOTIFICATION_ID] == HOMEKIT_NOTIFY_ID + assert call_dismiss_notification[0].data[ATTR_NOTIFICATION_ID] == "entry_id" def test_homekit_speed_mapping(): @@ -277,3 +291,13 @@ def test_speed_to_states(): assert speed_mapping.speed_to_states(66) == "low" assert speed_mapping.speed_to_states(67) == "high" assert speed_mapping.speed_to_states(100) == "high" + + +async def test_port_is_available(hass): + """Test we can get an available port and it is actually available.""" + next_port = await hass.async_add_executor_job( + find_next_available_port, DEFAULT_CONFIG_FLOW_PORT + ) + assert next_port + + assert await hass.async_add_executor_job(port_is_available, next_port) diff --git a/tests/components/homekit/util.py b/tests/components/homekit/util.py new file mode 100644 index 00000000000000..0abf3007c0468d --- /dev/null +++ b/tests/components/homekit/util.py @@ -0,0 +1,34 @@ +"""Test util for the homekit integration.""" + +from asynctest import patch + +from homeassistant.components.homekit.const import DOMAIN +from homeassistant.const import CONF_NAME, CONF_PORT +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +PATH_HOMEKIT = "homeassistant.components.homekit" + + +async def async_init_integration(hass: HomeAssistant) -> MockConfigEntry: + """Set up the homekit integration in Home Assistant.""" + + with patch(f"{PATH_HOMEKIT}.HomeKit.async_start"): + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + return entry + + +async def async_init_entry(hass: HomeAssistant, entry: MockConfigEntry): + """Set up the homekit integration in Home Assistant.""" + + with patch(f"{PATH_HOMEKIT}.HomeKit.async_start"): + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + return entry From 3e06e6b4b37bd10660872fbce4e3e892fedbc90b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 1 May 2020 00:05:45 -0400 Subject: [PATCH 24/58] Don't attempt to set Vizio is_volume_muted property if Vizio API doesn't provide muted state (#34782) --- .../components/vizio/media_player.py | 15 +++++++----- tests/components/vizio/test_media_player.py | 23 +++++++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 2a81ed1eaad83b..67bb3d3633cc57 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -132,7 +132,7 @@ def __init__( self._state = None self._volume_level = None self._volume_step = config_entry.options[CONF_VOLUME_STEP] - self._is_muted = None + self._is_volume_muted = None self._current_input = None self._current_app = None self._current_app_config = None @@ -190,7 +190,7 @@ async def async_update(self) -> None: if not is_on: self._state = STATE_OFF self._volume_level = None - self._is_muted = None + self._is_volume_muted = None self._current_input = None self._available_inputs = None self._current_app = None @@ -207,7 +207,10 @@ async def async_update(self) -> None: ) if audio_settings is not None: self._volume_level = float(audio_settings["volume"]) / self._max_volume - self._is_muted = audio_settings["mute"].lower() == "on" + if "mute" in audio_settings: + self._is_volume_muted = audio_settings["mute"].lower() == "on" + else: + self._is_volume_muted = None if VIZIO_SOUND_MODE in audio_settings: self._supported_commands |= SUPPORT_SELECT_SOUND_MODE @@ -324,7 +327,7 @@ def volume_level(self) -> float: @property def is_volume_muted(self): """Boolean if volume is currently muted.""" - return self._is_muted + return self._is_volume_muted @property def source(self) -> str: @@ -428,10 +431,10 @@ async def async_mute_volume(self, mute: bool) -> None: """Mute the volume.""" if mute: await self._device.mute_on() - self._is_muted = True + self._is_volume_muted = True else: await self._device.mute_off() - self._is_muted = False + self._is_volume_muted = False async def async_media_previous_track(self) -> None: """Send previous channel command.""" diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index aaf802af7ecca5..b4a2148b8daa52 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -621,3 +621,26 @@ async def test_setup_with_no_running_app( assert attr["source"] == "CAST" assert "app_id" not in attr assert "app_name" not in attr + + +async def test_setup_tv_without_mute( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_update: pytest.fixture, +) -> None: + """Test Vizio TV entity setup when mute property isn't returned by Vizio API.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=vol.Schema(VIZIO_SCHEMA)(MOCK_USER_VALID_TV_CONFIG), + unique_id=UNIQUE_ID, + ) + + async with _cm_for_test_setup_without_apps( + {"volume": int(MAX_VOLUME[VIZIO_DEVICE_CLASS_TV] / 2)}, STATE_ON, + ): + await _add_config_entry_to_hass(hass, config_entry) + + attr = _get_attr_and_assert_base_attr(hass, DEVICE_CLASS_TV, STATE_ON) + _assert_sources_and_volume(attr, VIZIO_DEVICE_CLASS_TV) + assert "sound_mode" not in attr + assert "is_volume_muted" not in attr From 7b90cbd2b27f059d7bb5afaa637615f84153862e Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 1 May 2020 06:06:16 +0200 Subject: [PATCH 25/58] UniFi - Disconnected clients wrongfully marked as wired not created (#34986) --- homeassistant/components/unifi/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 7bc1bbd319726e..0b71a7d517e5e5 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -80,7 +80,7 @@ def add_entities(controller, async_add_entities): if tracker_class is UniFiClientTracker: - if item.is_wired: + if mac not in controller.wireless_clients: if not controller.option_track_wired_clients: continue else: From 1f66821256d8911f79c25afb825f688234c86bd0 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 30 Apr 2020 23:08:15 -0500 Subject: [PATCH 26/58] Support num_repeats for directv remote (#34982) --- homeassistant/components/directv/remote.py | 23 ++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/directv/remote.py b/homeassistant/components/directv/remote.py index 776ce7a229b68f..9665b0aea17fad 100644 --- a/homeassistant/components/directv/remote.py +++ b/homeassistant/components/directv/remote.py @@ -5,7 +5,7 @@ from directv import DIRECTV, DIRECTVError -from homeassistant.components.remote import RemoteEntity +from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType @@ -95,12 +95,15 @@ async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> Non blue, chanup, chandown, prev, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, dash, enter """ - for single_command in command: - try: - await self.dtv.remote(single_command, self._address) - except DIRECTVError: - _LOGGER.exception( - "Sending command %s to device %s failed", - single_command, - self._device_id, - ) + num_repeats = kwargs[ATTR_NUM_REPEATS] + + for _ in range(num_repeats): + for single_command in command: + try: + await self.dtv.remote(single_command, self._address) + except DIRECTVError: + _LOGGER.exception( + "Sending command %s to device %s failed", + single_command, + self._device_id, + ) From bb08959131ebca8758a6d43d8ea169fe804c78e1 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 30 Apr 2020 23:08:32 -0500 Subject: [PATCH 27/58] Support num_repeats for roku remote (#34981) --- homeassistant/components/roku/remote.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index 3a9adf3518c9e6..22102ac8282f8c 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -7,7 +7,7 @@ ) from roku import RokuException -from homeassistant.components.remote import RemoteEntity +from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType @@ -84,8 +84,11 @@ def should_poll(self): def send_command(self, command, **kwargs): """Send a command to one device.""" - for single_command in command: - if not hasattr(self.roku, single_command): - continue + num_repeats = kwargs[ATTR_NUM_REPEATS] - getattr(self.roku, single_command)() + for _ in range(num_repeats): + for single_command in command: + if not hasattr(self.roku, single_command): + continue + + getattr(self.roku, single_command)() From 225059f8eaf337d24ff5c2c1e81d6034990450f9 Mon Sep 17 00:00:00 2001 From: Xiaonan Shen Date: Thu, 30 Apr 2020 21:13:45 -0700 Subject: [PATCH 28/58] Fix roomba not reporting error (#34996) --- homeassistant/components/roomba/irobot_base.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/roomba/irobot_base.py b/homeassistant/components/roomba/irobot_base.py index cb422616e5aebf..86e9b0da6b5566 100644 --- a/homeassistant/components/roomba/irobot_base.py +++ b/homeassistant/components/roomba/irobot_base.py @@ -3,6 +3,7 @@ import logging from homeassistant.components.vacuum import ( + ATTR_STATUS, STATE_CLEANING, STATE_DOCKED, STATE_ERROR, @@ -16,6 +17,7 @@ SUPPORT_SEND_COMMAND, SUPPORT_START, SUPPORT_STATE, + SUPPORT_STATUS, SUPPORT_STOP, StateVacuumEntity, ) @@ -40,6 +42,7 @@ | SUPPORT_SEND_COMMAND | SUPPORT_START | SUPPORT_STATE + | SUPPORT_STATUS | SUPPORT_STOP | SUPPORT_LOCATE ) @@ -143,7 +146,7 @@ def state(self): state = STATE_MAP[phase] except KeyError: return STATE_ERROR - if cycle != "none" and state != STATE_CLEANING and state != STATE_RETURNING: + if cycle != "none" and (state == STATE_IDLE or state == STATE_DOCKED): state = STATE_PAUSED return state @@ -173,6 +176,9 @@ def device_state_attributes(self): # Set properties that are to appear in the GUI state_attrs = {ATTR_SOFTWARE_VERSION: software_version} + # Set legacy status to avoid break changes + state_attrs[ATTR_STATUS] = self.vacuum.current_state + # Only add cleaning time and cleaned area attrs when the vacuum is # currently on if self.state == STATE_CLEANING: From 8f9467492d46b6126ac6f9392a25fda68897e8ef Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Apr 2020 21:34:51 -0700 Subject: [PATCH 29/58] Remove some passings of loop (#34995) --- homeassistant/components/rflink/__init__.py | 2 +- homeassistant/components/shell_command/__init__.py | 2 -- homeassistant/components/tradfri/__init__.py | 1 - homeassistant/components/tradfri/config_flow.py | 2 +- homeassistant/helpers/aiohttp_client.py | 9 ++------- 5 files changed, 4 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index 542c63f3b7ac70..68b6d841654903 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -230,7 +230,7 @@ async def connect(): ) try: - with async_timeout.timeout(CONNECTION_TIMEOUT, loop=hass.loop): + with async_timeout.timeout(CONNECTION_TIMEOUT): transport, protocol = await connection except ( diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py index 89a1a20e8e4290..dc9fd8769d6781 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -56,7 +56,6 @@ async def async_service_handler(service: ServiceCall) -> None: # pylint: disable=no-member create_process = asyncio.subprocess.create_subprocess_shell( cmd, - loop=hass.loop, stdin=None, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, @@ -69,7 +68,6 @@ async def async_service_handler(service: ServiceCall) -> None: # pylint: disable=no-member create_process = asyncio.subprocess.create_subprocess_exec( *shlexed_cmd, - loop=hass.loop, stdin=None, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index a797607e243322..cef22c636c1f6d 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -100,7 +100,6 @@ async def async_setup_entry(hass, entry): entry.data[CONF_HOST], psk_id=entry.data[CONF_IDENTITY], psk=entry.data[CONF_KEY], - loop=hass.loop, ) async def on_hass_stop(event): diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index 1663ba675a39bf..2ade04cff55105 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -178,7 +178,7 @@ async def get_gateway_info(hass, host, identity, key): """Return info for the gateway.""" try: - factory = APIFactory(host, psk_id=identity, psk=key, loop=hass.loop) + factory = APIFactory(host, psk_id=identity, psk=key) api = factory.request gateway = Gateway() diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index eee891b7f8894c..73658c7a6cb2bd 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -64,10 +64,7 @@ def async_create_clientsession( connector = _async_get_connector(hass, verify_ssl) clientsession = aiohttp.ClientSession( - loop=hass.loop, - connector=connector, - headers={USER_AGENT: SERVER_SOFTWARE}, - **kwargs, + connector=connector, headers={USER_AGENT: SERVER_SOFTWARE}, **kwargs, ) if auto_cleanup: @@ -174,9 +171,7 @@ def _async_get_connector( else: ssl_context = False - connector = aiohttp.TCPConnector( - loop=hass.loop, enable_cleanup_closed=True, ssl=ssl_context - ) + connector = aiohttp.TCPConnector(enable_cleanup_closed=True, ssl=ssl_context) hass.data[key] = connector async def _async_close_connector(event: Event) -> None: From 03bb2a68b3ec42d3b5198ee07f8ab535defa394e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Apr 2020 22:27:34 -0700 Subject: [PATCH 30/58] Lint roomba (#35000) --- homeassistant/components/roomba/irobot_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/roomba/irobot_base.py b/homeassistant/components/roomba/irobot_base.py index 86e9b0da6b5566..6c35582c2c2e54 100644 --- a/homeassistant/components/roomba/irobot_base.py +++ b/homeassistant/components/roomba/irobot_base.py @@ -146,7 +146,7 @@ def state(self): state = STATE_MAP[phase] except KeyError: return STATE_ERROR - if cycle != "none" and (state == STATE_IDLE or state == STATE_DOCKED): + if cycle != "none" and state in (STATE_IDLE, STATE_DOCKED): state = STATE_PAUSED return state From 72e7beeb76e238fe5cf35f5b7a1d6f17425e5349 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 1 May 2020 07:33:22 +0200 Subject: [PATCH 31/58] UniFi - Add simple options flow (#34990) --- homeassistant/components/unifi/config_flow.py | 38 ++++++++++++++++++- homeassistant/components/unifi/strings.json | 8 ++++ .../components/unifi/translations/en.json | 8 ++++ tests/components/unifi/test_config_flow.py | 36 ++++++++++++++++-- 4 files changed, 86 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index f42acf54e9df82..72ba593bae6ed6 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -175,7 +175,43 @@ async def async_step_init(self, user_input=None): """Manage the UniFi options.""" self.controller = self.hass.data[UNIFI_DOMAIN][self.config_entry.entry_id] self.options[CONF_BLOCK_CLIENT] = self.controller.option_block_clients - return await self.async_step_device_tracker() + + if self.show_advanced_options: + return await self.async_step_device_tracker() + + return await self.async_step_simple_options() + + async def async_step_simple_options(self, user_input=None): + """For simple Jack.""" + if user_input is not None: + self.options.update(user_input) + return await self._update_options() + + clients_to_block = {} + + for client in self.controller.api.clients.values(): + clients_to_block[ + client.mac + ] = f"{client.name or client.hostname} ({client.mac})" + + return self.async_show_form( + step_id="simple_options", + data_schema=vol.Schema( + { + vol.Optional( + CONF_TRACK_CLIENTS, + default=self.controller.option_track_clients, + ): bool, + vol.Optional( + CONF_TRACK_DEVICES, + default=self.controller.option_track_devices, + ): bool, + vol.Optional( + CONF_BLOCK_CLIENT, default=self.options[CONF_BLOCK_CLIENT] + ): cv.multi_select(clients_to_block), + } + ), + ) async def async_step_device_tracker(self, user_input=None): """Manage the device tracker options.""" diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json index 6c142d371c925a..da1d6200ed5cf9 100644 --- a/homeassistant/components/unifi/strings.json +++ b/homeassistant/components/unifi/strings.json @@ -46,6 +46,14 @@ "description": "Configure client controls\n\nCreate switches for serial numbers you want to control network access for.", "title": "UniFi options 2/3" }, + "simple_options": { + "data": { + "track_clients": "[%key:component::unifi::options::step::device_tracker::data::track_clients%]", + "track_devices": "[%key:component::unifi::options::step::device_tracker::data::track_devices%]", + "block_client": "[%key:component::unifi::options::step::client_control::data::block_client%]" + }, + "description": "Configure UniFi integration" + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Bandwidth usage sensors for network clients" diff --git a/homeassistant/components/unifi/translations/en.json b/homeassistant/components/unifi/translations/en.json index 618c393b7aa523..fd7096686e1fbc 100644 --- a/homeassistant/components/unifi/translations/en.json +++ b/homeassistant/components/unifi/translations/en.json @@ -46,6 +46,14 @@ "description": "Configure device tracking", "title": "UniFi options 1/3" }, + "simple_options": { + "data": { + "track_clients": "[%key:component::unifi::options::step::device_tracker::data::track_clients%]", + "track_devices": "[%key:component::unifi::options::step::device_tracker::data::track_devices%]", + "block_client": "[%key:component::unifi::options::step::client_control::data::block_client%]" + }, + "description": "Configure UniFi integration" + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Bandwidth usage sensors for network clients" diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index ae738ba8a644ef..489ae25a60c8c2 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -264,14 +264,14 @@ async def test_flow_fails_unknown_problem(hass, aioclient_mock): assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT -async def test_option_flow(hass): - """Test config flow options.""" +async def test_advanced_option_flow(hass): + """Test advanced config flow options.""" controller = await setup_unifi_integration( hass, clients_response=CLIENTS, wlans_response=WLANS ) result = await hass.config_entries.options.async_init( - controller.config_entry.entry_id + controller.config_entry.entry_id, context={"show_advanced_options": True} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -315,3 +315,33 @@ async def test_option_flow(hass): CONF_BLOCK_CLIENT: [CLIENTS[0]["mac"]], CONF_ALLOW_BANDWIDTH_SENSORS: True, } + + +async def test_simple_option_flow(hass): + """Test simple config flow options.""" + controller = await setup_unifi_integration( + hass, clients_response=CLIENTS, wlans_response=WLANS + ) + + result = await hass.config_entries.options.async_init( + controller.config_entry.entry_id, context={"show_advanced_options": False} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "simple_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_TRACK_CLIENTS: False, + CONF_TRACK_DEVICES: False, + CONF_BLOCK_CLIENT: [CLIENTS[0]["mac"]], + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + CONF_TRACK_CLIENTS: False, + CONF_TRACK_DEVICES: False, + CONF_BLOCK_CLIENT: [CLIENTS[0]["mac"]], + } From 82a478e2fb76803d8b704fd45b10f09d4348177a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 1 May 2020 07:34:44 +0200 Subject: [PATCH 32/58] Fix MQTT debug info for same topic (#34952) --- homeassistant/components/mqtt/debug_info.py | 25 ++++-- tests/components/mqtt/test_init.py | 93 ++++++++++++++++++++- 2 files changed, 109 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py index 2a216366bb1830..86850c616382c0 100644 --- a/homeassistant/components/mqtt/debug_info.py +++ b/homeassistant/components/mqtt/debug_info.py @@ -23,7 +23,7 @@ def _log_message(msg): debug_info = hass.data[DATA_MQTT_DEBUG_INFO] messages = debug_info["entities"][entity_id]["subscriptions"][ msg.subscribed_topic - ] + ]["messages"] if msg not in messages: messages.append(msg) @@ -50,16 +50,27 @@ def add_subscription(hass, message_callback, subscription): entity_info = debug_info["entities"].setdefault( entity_id, {"subscriptions": {}, "discovery_data": {}} ) - entity_info["subscriptions"][subscription] = deque([], STORED_MESSAGES) + if subscription not in entity_info["subscriptions"]: + entity_info["subscriptions"][subscription] = { + "count": 0, + "messages": deque([], STORED_MESSAGES), + } + entity_info["subscriptions"][subscription]["count"] += 1 def remove_subscription(hass, message_callback, subscription): - """Remove debug data for subscription.""" + """Remove debug data for subscription if it exists.""" entity_id = getattr(message_callback, "__entity_id", None) if entity_id and entity_id in hass.data[DATA_MQTT_DEBUG_INFO]["entities"]: - hass.data[DATA_MQTT_DEBUG_INFO]["entities"][entity_id]["subscriptions"].pop( + hass.data[DATA_MQTT_DEBUG_INFO]["entities"][entity_id]["subscriptions"][ subscription - ) + ]["count"] -= 1 + if not hass.data[DATA_MQTT_DEBUG_INFO]["entities"][entity_id]["subscriptions"][ + subscription + ]["count"]: + hass.data[DATA_MQTT_DEBUG_INFO]["entities"][entity_id]["subscriptions"].pop( + subscription + ) def add_entity_discovery_data(hass, discovery_data, entity_id): @@ -127,10 +138,10 @@ async def info_for_device(hass, device_id): "topic": topic, "messages": [ {"payload": msg.payload, "time": msg.timestamp, "topic": msg.topic} - for msg in list(messages) + for msg in list(subscription["messages"]) ], } - for topic, messages in entity_info["subscriptions"].items() + for topic, subscription in entity_info["subscriptions"].items() ] discovery_data = { "topic": entity_info["discovery_data"].get(ATTR_DISCOVERY_TOPIC, ""), diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index a139a94253062f..672ff127b4dedc 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -956,6 +956,42 @@ async def test_mqtt_ws_remove_discovered_device_twice( assert response["error"]["code"] == websocket_api.const.ERR_NOT_FOUND +async def test_mqtt_ws_remove_discovered_device_same_topic( + hass, device_reg, hass_ws_client, mqtt_mock +): + """Test MQTT websocket device removal.""" + config_entry = MockConfigEntry(domain=mqtt.DOMAIN) + config_entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, config_entry) + + data = ( + '{ "device":{"identifiers":["0AFFD2"]},' + ' "state_topic": "foobar/sensor",' + ' "availability_topic": "foobar/sensor",' + ' "unique_id": "unique" }' + ) + + async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) + await hass.async_block_till_done() + + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + assert device_entry is not None + + client = await hass_ws_client(hass) + await client.send_json( + {"id": 5, "type": "mqtt/device/remove", "device_id": device_entry.id} + ) + response = await client.receive_json() + assert response["success"] + + await client.send_json( + {"id": 6, "type": "mqtt/device/remove", "device_id": device_entry.id} + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == websocket_api.const.ERR_NOT_FOUND + + async def test_mqtt_ws_remove_non_mqtt_device( hass, device_reg, hass_ws_client, mqtt_mock ): @@ -1302,7 +1338,60 @@ async def test_debug_info_filter_same(hass, mqtt_mock): assert { "topic": "sensor/#", "messages": [ - {"topic": "sensor/abc", "payload": "123", "time": dt1}, - {"topic": "sensor/abc", "payload": "123", "time": dt2}, + {"payload": "123", "time": dt1, "topic": "sensor/abc"}, + {"payload": "123", "time": dt2, "topic": "sensor/abc"}, ], } == debug_info_data["entities"][0]["subscriptions"][0] + + +async def test_debug_info_same_topic(hass, mqtt_mock): + """Test debug info.""" + config = { + "device": {"identifiers": ["helloworld"]}, + "platform": "mqtt", + "name": "test", + "state_topic": "sensor/status", + "availability_topic": "sensor/status", + "unique_id": "veryunique", + } + + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps(config) + async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device({("mqtt", "helloworld")}, set()) + assert device is not None + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"][0]["subscriptions"]) >= 1 + assert {"topic": "sensor/status", "messages": []} in debug_info_data["entities"][0][ + "subscriptions" + ] + + start_dt = datetime(2019, 1, 1, 0, 0, 0) + with patch("homeassistant.util.dt.utcnow") as dt_utcnow: + dt_utcnow.return_value = start_dt + async_fire_mqtt_message(hass, "sensor/status", "123", qos=0, retain=False) + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"][0]["subscriptions"]) == 1 + assert { + "payload": "123", + "time": start_dt, + "topic": "sensor/status", + } in debug_info_data["entities"][0]["subscriptions"][0]["messages"] + + config["availability_topic"] = "sensor/availability" + data = json.dumps(config) + async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) + await hass.async_block_till_done() + + start_dt = datetime(2019, 1, 1, 0, 0, 0) + with patch("homeassistant.util.dt.utcnow") as dt_utcnow: + dt_utcnow.return_value = start_dt + async_fire_mqtt_message(hass, "sensor/status", "123", qos=0, retain=False) From af62660b14a807b773b5ecfc4192523ba98a25ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 May 2020 00:35:23 -0500 Subject: [PATCH 33/58] Attempt to fix flapping august lock test (#34998) --- tests/components/august/test_lock.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py index 4bd5509a21615a..dfa0edfcb6d9e5 100644 --- a/tests/components/august/test_lock.py +++ b/tests/components/august/test_lock.py @@ -71,6 +71,7 @@ async def test_one_lock_operation(hass): assert await hass.services.async_call( LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True ) + await hass.async_block_till_done() lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") assert lock_online_with_doorsense_name.state == STATE_UNLOCKED @@ -84,6 +85,7 @@ async def test_one_lock_operation(hass): assert await hass.services.async_call( LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True ) + await hass.async_block_till_done() lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") assert lock_online_with_doorsense_name.state == STATE_LOCKED From e7157f216409a6536661e091e8d29b39b1bfa72c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 May 2020 00:36:01 -0500 Subject: [PATCH 34/58] Fix restoring isy994 brightness with no previous state (#34972) --- CODEOWNERS | 1 + homeassistant/components/isy994/light.py | 29 +++++++++++++++++-- homeassistant/components/isy994/manifest.json | 2 +- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 43959f67c30be8..4dbd9f87d4a014 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -196,6 +196,7 @@ homeassistant/components/ipp/* @ctalkington homeassistant/components/iqvia/* @bachya homeassistant/components/irish_rail_transport/* @ttroy50 homeassistant/components/islamic_prayer_times/* @engrbm87 +homeassistant/components/isy994/* @bdraco homeassistant/components/izone/* @Swamp-Ig homeassistant/components/jewish_calendar/* @tsvi homeassistant/components/juicenet/* @jesserockz diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 0d66a73571d8ab..7ae8d1c76f8176 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -3,12 +3,15 @@ from typing import Callable from homeassistant.components.light import DOMAIN, SUPPORT_BRIGHTNESS, LightEntity +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType from . import ISY994_NODES, ISYDevice _LOGGER = logging.getLogger(__name__) +ATTR_LAST_BRIGHTNESS = "last_brightness" + def setup_platform( hass, config: ConfigType, add_entities: Callable[[list], None], discovery_info=None @@ -21,13 +24,13 @@ def setup_platform( add_entities(devices) -class ISYLightDevice(ISYDevice, LightEntity): +class ISYLightDevice(ISYDevice, LightEntity, RestoreEntity): """Representation of an ISY994 light device.""" def __init__(self, node) -> None: """Initialize the ISY994 light device.""" super().__init__(node) - self._last_brightness = self.brightness + self._last_brightness = None @property def is_on(self) -> bool: @@ -56,7 +59,7 @@ def on_update(self, event: object) -> None: # pylint: disable=arguments-differ def turn_on(self, brightness=None, **kwargs) -> None: """Send the turn on command to the ISY994 light device.""" - if brightness is None and self._last_brightness is not None: + if brightness is None and self._last_brightness: brightness = self._last_brightness if not self._node.on(val=brightness): _LOGGER.debug("Unable to turn on light") @@ -65,3 +68,23 @@ def turn_on(self, brightness=None, **kwargs) -> None: def supported_features(self): """Flag supported features.""" return SUPPORT_BRIGHTNESS + + @property + def device_state_attributes(self): + """Return the light attributes.""" + return {ATTR_LAST_BRIGHTNESS: self._last_brightness} + + async def async_added_to_hass(self) -> None: + """Restore last_brightness on restart.""" + await super().async_added_to_hass() + + self._last_brightness = self.brightness or 255 + last_state = await self.async_get_last_state() + if not last_state: + return + + if ( + ATTR_LAST_BRIGHTNESS in last_state.attributes + and last_state.attributes[ATTR_LAST_BRIGHTNESS] + ): + self._last_brightness = last_state.attributes[ATTR_LAST_BRIGHTNESS] diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 0b48528335d573..083f25808fbc9a 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -3,5 +3,5 @@ "name": "Universal Devices ISY994", "documentation": "https://www.home-assistant.io/integrations/isy994", "requirements": ["PyISY==1.1.2"], - "codeowners": [] + "codeowners": ["@bdraco"] } From 850b5cb02b2ed00eeb2b9807f2bdf6499bc5e781 Mon Sep 17 00:00:00 2001 From: Jason Hunter Date: Fri, 1 May 2020 02:15:40 -0400 Subject: [PATCH 35/58] Config flow for ONVIF (#34520) --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/onvif/__init__.py | 87 ++- homeassistant/components/onvif/camera.py | 471 +++++++--------- homeassistant/components/onvif/config_flow.py | 311 +++++++++++ homeassistant/components/onvif/const.py | 47 ++ homeassistant/components/onvif/manifest.json | 5 +- homeassistant/components/onvif/strings.json | 58 ++ .../components/onvif/translations/en.json | 59 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 6 + tests/components/onvif/__init__.py | 1 + tests/components/onvif/test_config_flow.py | 508 ++++++++++++++++++ 14 files changed, 1294 insertions(+), 265 deletions(-) create mode 100644 homeassistant/components/onvif/config_flow.py create mode 100644 homeassistant/components/onvif/const.py create mode 100644 homeassistant/components/onvif/strings.json create mode 100644 homeassistant/components/onvif/translations/en.json create mode 100644 tests/components/onvif/__init__.py create mode 100644 tests/components/onvif/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 59ef37cd9322c0..e36d6b252bd78f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -507,6 +507,7 @@ omit = homeassistant/components/ombi/* homeassistant/components/onewire/sensor.py homeassistant/components/onkyo/media_player.py + homeassistant/components/onvif/__init__.py homeassistant/components/onvif/camera.py homeassistant/components/opencv/* homeassistant/components/openevse/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 4dbd9f87d4a014..a021a4a28ed175 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -277,6 +277,7 @@ homeassistant/components/ohmconnect/* @robbiet480 homeassistant/components/ombi/* @larssont homeassistant/components/onboarding/* @home-assistant/core homeassistant/components/onewire/* @garbled1 +homeassistant/components/onvif/* @hunterjm homeassistant/components/openerz/* @misialq homeassistant/components/opentherm_gw/* @mvn23 homeassistant/components/openuv/* @bachya diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index ea4c875ac20894..5fe43fdd83b231 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -1 +1,86 @@ -"""The onvif component.""" +"""The ONVIF integration.""" +import asyncio + +import voluptuous as vol + +from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_per_platform + +from .const import ( + CONF_PROFILE, + CONF_RTSP_TRANSPORT, + DEFAULT_ARGUMENTS, + DEFAULT_PROFILE, + DOMAIN, + RTSP_TRANS_PROTOCOLS, +) + +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) + +PLATFORMS = ["camera"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the ONVIF component.""" + # Import from yaml + configs = {} + for p_type, p_config in config_per_platform(config, "camera"): + if p_type != DOMAIN: + continue + + config = p_config.copy() + profile = config.get(CONF_PROFILE, DEFAULT_PROFILE) + if config[CONF_HOST] not in configs.keys(): + configs[config[CONF_HOST]] = config + configs[config[CONF_HOST]][CONF_PROFILE] = [profile] + else: + configs[config[CONF_HOST]][CONF_PROFILE].append(profile) + + for conf in configs.values(): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up ONVIF from a config entry.""" + if not entry.options: + await async_populate_options(hass, entry) + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + + return unload_ok + + +async def async_populate_options(hass, entry): + """Populate default options for device.""" + options = { + CONF_EXTRA_ARGUMENTS: DEFAULT_ARGUMENTS, + CONF_RTSP_TRANSPORT: RTSP_TRANS_PROTOCOLS[0], + } + + hass.config_entries.async_update_entry(entry, options=options) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 7fea75735e0862..34f33e302b8e5f 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -1,7 +1,6 @@ """Support for ONVIF Cameras with FFmpeg as decoder.""" import asyncio import datetime as dt -import logging import os from typing import Optional @@ -16,10 +15,9 @@ from zeep.asyncio import AsyncTransport from zeep.exceptions import Fault -from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera +from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, DATA_FFMPEG from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PASSWORD, @@ -27,131 +25,85 @@ CONF_USERNAME, ) from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.aiohttp_client import ( async_aiohttp_proxy_stream, async_get_clientsession, ) -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.service import async_extract_entity_ids import homeassistant.util.dt as dt_util -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = "ONVIF Camera" -DEFAULT_PORT = 5000 -DEFAULT_USERNAME = "admin" -DEFAULT_PASSWORD = "888888" -DEFAULT_ARGUMENTS = "-pred 1" -DEFAULT_PROFILE = 0 - -CONF_PROFILE = "profile" -CONF_RTSP_TRANSPORT = "rtsp_transport" - -ATTR_PAN = "pan" -ATTR_TILT = "tilt" -ATTR_ZOOM = "zoom" -ATTR_DISTANCE = "distance" -ATTR_SPEED = "speed" -ATTR_MOVE_MODE = "move_mode" -ATTR_CONTINUOUS_DURATION = "continuous_duration" -ATTR_PRESET = "preset" - -DIR_UP = "UP" -DIR_DOWN = "DOWN" -DIR_LEFT = "LEFT" -DIR_RIGHT = "RIGHT" -ZOOM_OUT = "ZOOM_OUT" -ZOOM_IN = "ZOOM_IN" -PAN_FACTOR = {DIR_RIGHT: 1, DIR_LEFT: -1} -TILT_FACTOR = {DIR_UP: 1, DIR_DOWN: -1} -ZOOM_FACTOR = {ZOOM_IN: 1, ZOOM_OUT: -1} -CONTINUOUS_MOVE = "ContinuousMove" -RELATIVE_MOVE = "RelativeMove" -ABSOLUTE_MOVE = "AbsoluteMove" -GOTOPRESET_MOVE = "GotoPreset" - -SERVICE_PTZ = "ptz" - -DOMAIN = "onvif" -ONVIF_DATA = "onvif" -ENTITIES = "entities" - -RTSP_TRANS_PROTOCOLS = ["tcp", "udp", "udp_multicast", "http"] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, - vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_EXTRA_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string, - vol.Optional(CONF_RTSP_TRANSPORT, default=RTSP_TRANS_PROTOCOLS[0]): vol.In( - RTSP_TRANS_PROTOCOLS - ), - vol.Optional(CONF_PROFILE, default=DEFAULT_PROFILE): vol.All( - vol.Coerce(int), vol.Range(min=0) - ), - } -) - -SERVICE_PTZ_SCHEMA = vol.Schema( - { - ATTR_ENTITY_ID: cv.entity_ids, - vol.Optional(ATTR_PAN): vol.In([DIR_LEFT, DIR_RIGHT]), - vol.Optional(ATTR_TILT): vol.In([DIR_UP, DIR_DOWN]), - vol.Optional(ATTR_ZOOM): vol.In([ZOOM_OUT, ZOOM_IN]), - ATTR_MOVE_MODE: vol.In( - [CONTINUOUS_MOVE, RELATIVE_MOVE, ABSOLUTE_MOVE, GOTOPRESET_MOVE] - ), - vol.Optional(ATTR_CONTINUOUS_DURATION, default=0.5): cv.small_float, - vol.Optional(ATTR_DISTANCE, default=0.1): cv.small_float, - vol.Optional(ATTR_SPEED, default=0.5): cv.small_float, - vol.Optional(ATTR_PRESET, default="0"): cv.string, - } +from .const import ( + ABSOLUTE_MOVE, + ATTR_CONTINUOUS_DURATION, + ATTR_DISTANCE, + ATTR_MOVE_MODE, + ATTR_PAN, + ATTR_PRESET, + ATTR_SPEED, + ATTR_TILT, + ATTR_ZOOM, + CONF_PROFILE, + CONF_RTSP_TRANSPORT, + CONTINUOUS_MOVE, + DIR_DOWN, + DIR_LEFT, + DIR_RIGHT, + DIR_UP, + DOMAIN, + ENTITIES, + GOTOPRESET_MOVE, + LOGGER, + PAN_FACTOR, + RELATIVE_MOVE, + SERVICE_PTZ, + TILT_FACTOR, + ZOOM_FACTOR, + ZOOM_IN, + ZOOM_OUT, ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up a ONVIF camera.""" - _LOGGER.debug("Setting up the ONVIF camera platform") - - async def async_handle_ptz(service): - """Handle PTZ service call.""" - pan = service.data.get(ATTR_PAN) - tilt = service.data.get(ATTR_TILT) - zoom = service.data.get(ATTR_ZOOM) - distance = service.data[ATTR_DISTANCE] - speed = service.data[ATTR_SPEED] - move_mode = service.data.get(ATTR_MOVE_MODE) - continuous_duration = service.data[ATTR_CONTINUOUS_DURATION] - preset = service.data[ATTR_PRESET] - all_cameras = hass.data[ONVIF_DATA][ENTITIES] - entity_ids = await async_extract_entity_ids(hass, service) - target_cameras = [] - if not entity_ids: - target_cameras = all_cameras - else: - target_cameras = [ - camera for camera in all_cameras if camera.entity_id in entity_ids - ] - for camera in target_cameras: - await camera.async_perform_ptz( - pan, tilt, zoom, distance, speed, move_mode, continuous_duration, preset - ) - - hass.services.async_register( - DOMAIN, SERVICE_PTZ, async_handle_ptz, schema=SERVICE_PTZ_SCHEMA +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the ONVIF camera video stream.""" + platform = entity_platform.current_platform.get() + + # Create PTZ service + platform.async_register_entity_service( + SERVICE_PTZ, + { + vol.Optional(ATTR_PAN): vol.In([DIR_LEFT, DIR_RIGHT]), + vol.Optional(ATTR_TILT): vol.In([DIR_UP, DIR_DOWN]), + vol.Optional(ATTR_ZOOM): vol.In([ZOOM_OUT, ZOOM_IN]), + vol.Optional(ATTR_DISTANCE, default=0.1): cv.small_float, + vol.Optional(ATTR_SPEED, default=0.5): cv.small_float, + vol.Optional(ATTR_MOVE_MODE, default=RELATIVE_MOVE): vol.In( + [CONTINUOUS_MOVE, RELATIVE_MOVE, ABSOLUTE_MOVE, GOTOPRESET_MOVE] + ), + vol.Optional(ATTR_CONTINUOUS_DURATION, default=0.5): cv.small_float, + vol.Optional(ATTR_PRESET, default="0"): cv.string, + }, + "async_perform_ptz", ) - _LOGGER.debug("Constructing the ONVIFHassCamera") - - hass_camera = ONVIFHassCamera(hass, config) + base_config = { + CONF_NAME: config_entry.data[CONF_NAME], + CONF_HOST: config_entry.data[CONF_HOST], + CONF_PORT: config_entry.data[CONF_PORT], + CONF_USERNAME: config_entry.data[CONF_USERNAME], + CONF_PASSWORD: config_entry.data[CONF_PASSWORD], + CONF_EXTRA_ARGUMENTS: config_entry.options[CONF_EXTRA_ARGUMENTS], + CONF_RTSP_TRANSPORT: config_entry.options[CONF_RTSP_TRANSPORT], + } - await hass_camera.async_initialize() + entities = [] + for profile in config_entry.data[CONF_PROFILE]: + config = {**base_config, CONF_PROFILE: profile} + camera = ONVIFHassCamera(hass, config) + await camera.async_initialize() + entities.append(camera) - async_add_entities([hass_camera]) - return + async_add_entities(entities) + return True class ONVIFHassCamera(Camera): @@ -161,9 +113,9 @@ def __init__(self, hass, config): """Initialize an ONVIF camera.""" super().__init__() - _LOGGER.debug("Importing dependencies") + LOGGER.debug("Importing dependencies") - _LOGGER.debug("Setting up the ONVIF camera component") + LOGGER.debug("Setting up the ONVIF camera component") self._username = config.get(CONF_USERNAME) self._password = config.get(CONF_PASSWORD) @@ -172,13 +124,18 @@ def __init__(self, hass, config): self._name = config.get(CONF_NAME) self._ffmpeg_arguments = config.get(CONF_EXTRA_ARGUMENTS) self._profile_index = config.get(CONF_PROFILE) + self._profile_token = None + self._profile_name = None self._ptz_service = None self._input = None self._snapshot = None self.stream_options[CONF_RTSP_TRANSPORT] = config.get(CONF_RTSP_TRANSPORT) + self._manufacturer = None + self._model = None + self._firmware_version = None self._mac = None - _LOGGER.debug( + LOGGER.debug( "Setting up the ONVIF camera device @ '%s:%s'", self._host, self._port ) @@ -201,29 +158,39 @@ async def async_initialize(self): the camera. Also retrieves the ONVIF profiles. """ try: - _LOGGER.debug("Updating service addresses") + LOGGER.debug("Updating service addresses") await self._camera.update_xaddrs() + await self.async_obtain_device_info() await self.async_obtain_mac_address() await self.async_check_date_and_time() + await self.async_obtain_profile_token() await self.async_obtain_input_uri() await self.async_obtain_snapshot_uri() self.setup_ptz() except ClientConnectionError as err: - _LOGGER.warning( + LOGGER.warning( "Couldn't connect to camera '%s', but will retry later. Error: %s", self._name, err, ) raise PlatformNotReady except Fault as err: - _LOGGER.error( + LOGGER.error( "Couldn't connect to camera '%s', please verify " "that the credentials are correct. Error: %s", self._name, err, ) + async def async_obtain_device_info(self): + """Obtain the MAC address of the camera to use as the unique ID.""" + devicemgmt = self._camera.create_devicemgmt_service() + device_info = await devicemgmt.GetDeviceInformation() + self._manufacturer = device_info.Manufacturer + self._model = device_info.Model + self._firmware_version = device_info.FirmwareVersion + async def async_obtain_mac_address(self): """Obtain the MAC address of the camera to use as the unique ID.""" devicemgmt = self._camera.create_devicemgmt_service() @@ -234,15 +201,15 @@ async def async_obtain_mac_address(self): async def async_check_date_and_time(self): """Warns if camera and system date not synced.""" - _LOGGER.debug("Setting up the ONVIF device management service") + LOGGER.debug("Setting up the ONVIF device management service") devicemgmt = self._camera.create_devicemgmt_service() - _LOGGER.debug("Retrieving current camera date/time") + LOGGER.debug("Retrieving current camera date/time") try: system_date = dt_util.utcnow() device_time = await devicemgmt.GetSystemDateAndTime() if not device_time: - _LOGGER.debug( + LOGGER.debug( """Couldn't get camera '%s' date/time. GetSystemDateAndTime() return null/empty""", self._name, @@ -260,7 +227,7 @@ async def async_check_date_and_time(self): cdate = device_time.LocalDateTime if cdate is None: - _LOGGER.warning("Could not retrieve date/time on this camera") + LOGGER.warning("Could not retrieve date/time on this camera") else: cam_date = dt.datetime( cdate.Date.Year, @@ -275,19 +242,19 @@ async def async_check_date_and_time(self): cam_date_utc = cam_date.astimezone(dt_util.UTC) - _LOGGER.debug("TimeZone for date/time: %s", tzone) + LOGGER.debug("TimeZone for date/time: %s", tzone) - _LOGGER.debug("Camera date/time: %s", cam_date) + LOGGER.debug("Camera date/time: %s", cam_date) - _LOGGER.debug("Camera date/time in UTC: %s", cam_date_utc) + LOGGER.debug("Camera date/time in UTC: %s", cam_date_utc) - _LOGGER.debug("System date/time: %s", system_date) + LOGGER.debug("System date/time: %s", system_date) dt_diff = cam_date - system_date dt_diff_seconds = dt_diff.total_seconds() if dt_diff_seconds > 5: - _LOGGER.warning( + LOGGER.warning( "The date/time on the camera (UTC) is '%s', " "which is different from the system '%s', " "this could lead to authentication issues", @@ -295,7 +262,7 @@ async def async_check_date_and_time(self): system_date, ) except ServerDisconnectedError as err: - _LOGGER.warning( + LOGGER.warning( "Couldn't get camera '%s' date/time. Error: %s", self._name, err ) @@ -306,10 +273,10 @@ async def async_obtain_profile_token(self): profiles = await media_service.GetProfiles() - _LOGGER.debug("Retrieved '%d' profiles", len(profiles)) + LOGGER.debug("Retrieved '%d' profiles", len(profiles)) if self._profile_index >= len(profiles): - _LOGGER.warning( + LOGGER.warning( "ONVIF Camera '%s' doesn't provide profile %d." " Using the last profile.", self._name, @@ -317,51 +284,32 @@ async def async_obtain_profile_token(self): ) self._profile_index = -1 - _LOGGER.debug("Using profile index '%d'", self._profile_index) + LOGGER.debug("Using profile index '%d'", self._profile_index) - return profiles[self._profile_index].token + self._profile_token = profiles[self._profile_index].token + self._profile_name = profiles[self._profile_index].Name except exceptions.ONVIFError as err: - _LOGGER.error( + LOGGER.error( "Couldn't retrieve profile token of camera '%s'. Error: %s", self._name, err, ) - return None async def async_obtain_input_uri(self): """Set the input uri for the camera.""" - _LOGGER.debug( + LOGGER.debug( "Connecting with ONVIF Camera: %s on port %s", self._host, self._port ) try: - _LOGGER.debug("Retrieving profiles") - - media_service = self._camera.create_media_service() - - profiles = await media_service.GetProfiles() - - _LOGGER.debug("Retrieved '%d' profiles", len(profiles)) - - if self._profile_index >= len(profiles): - _LOGGER.warning( - "ONVIF Camera '%s' doesn't provide profile %d." - " Using the last profile.", - self._name, - self._profile_index, - ) - self._profile_index = -1 - - _LOGGER.debug("Using profile index '%d'", self._profile_index) - - _LOGGER.debug("Retrieving stream uri") + LOGGER.debug("Retrieving stream uri") # Fix Onvif setup error on Goke GK7102 based IP camera # where we need to recreate media_service #26781 media_service = self._camera.create_media_service() req = media_service.create_type("GetStreamUri") - req.ProfileToken = profiles[self._profile_index].token + req.ProfileToken = self._profile_token req.StreamSetup = { "Stream": "RTP-Unicast", "Transport": {"Protocol": "RTSP"}, @@ -374,155 +322,143 @@ async def async_obtain_input_uri(self): "rtsp://", f"rtsp://{self._username}:{self._password}@", 1 ) - _LOGGER.debug( + LOGGER.debug( "ONVIF Camera Using the following URL for %s: %s", self._name, uri_for_log, ) except exceptions.ONVIFError as err: - _LOGGER.error("Couldn't setup camera '%s'. Error: %s", self._name, err) + LOGGER.error("Couldn't setup camera '%s'. Error: %s", self._name, err) async def async_obtain_snapshot_uri(self): """Set the snapshot uri for the camera.""" - _LOGGER.debug( + LOGGER.debug( "Connecting with ONVIF Camera: %s on port %s", self._host, self._port ) try: - _LOGGER.debug("Retrieving profiles") - - media_service = self._camera.create_media_service() - - profiles = await media_service.GetProfiles() - - _LOGGER.debug("Retrieved '%d' profiles", len(profiles)) - - if self._profile_index >= len(profiles): - _LOGGER.warning( - "ONVIF Camera '%s' doesn't provide profile %d." - " Using the last profile.", - self._name, - self._profile_index, - ) - self._profile_index = -1 - - _LOGGER.debug("Using profile index '%d'", self._profile_index) - - _LOGGER.debug("Retrieving snapshot uri") + LOGGER.debug("Retrieving snapshot uri") # Fix Onvif setup error on Goke GK7102 based IP camera # where we need to recreate media_service #26781 media_service = self._camera.create_media_service() req = media_service.create_type("GetSnapshotUri") - req.ProfileToken = profiles[self._profile_index].token + req.ProfileToken = self._profile_token try: snapshot_uri = await media_service.GetSnapshotUri(req) self._snapshot = snapshot_uri.Uri except ServerDisconnectedError as err: - _LOGGER.debug("Camera does not support GetSnapshotUri: %s", err) + LOGGER.debug("Camera does not support GetSnapshotUri: %s", err) - _LOGGER.debug( + LOGGER.debug( "ONVIF Camera Using the following URL for %s snapshot: %s", self._name, self._snapshot, ) except exceptions.ONVIFError as err: - _LOGGER.error("Couldn't setup camera '%s'. Error: %s", self._name, err) + LOGGER.error("Couldn't setup camera '%s'. Error: %s", self._name, err) def setup_ptz(self): """Set up PTZ if available.""" - _LOGGER.debug("Setting up the ONVIF PTZ service") + LOGGER.debug("Setting up the ONVIF PTZ service") if self._camera.get_service("ptz", create=False) is None: - _LOGGER.debug("PTZ is not available") + LOGGER.debug("PTZ is not available") else: self._ptz_service = self._camera.create_ptz_service() - _LOGGER.debug("Completed set up of the ONVIF camera component") + LOGGER.debug("Completed set up of the ONVIF camera component") async def async_perform_ptz( - self, pan, tilt, zoom, distance, speed, move_mode, continuous_duration, preset + self, + distance, + speed, + move_mode, + continuous_duration, + preset, + pan=None, + tilt=None, + zoom=None, ): """Perform a PTZ action on the camera.""" if self._ptz_service is None: - _LOGGER.warning("PTZ actions are not supported on camera '%s'", self._name) + LOGGER.warning("PTZ actions are not supported on camera '%s'", self._name) return - if self._ptz_service: - pan_val = distance * PAN_FACTOR.get(pan, 0) - tilt_val = distance * TILT_FACTOR.get(tilt, 0) - zoom_val = distance * ZOOM_FACTOR.get(zoom, 0) - speed_val = speed - preset_val = preset - _LOGGER.debug( - "Calling %s PTZ | Pan = %4.2f | Tilt = %4.2f | Zoom = %4.2f | Speed = %4.2f | Preset = %s", - move_mode, - pan_val, - tilt_val, - zoom_val, - speed_val, - preset_val, - ) - try: - req = self._ptz_service.create_type(move_mode) - req.ProfileToken = await self.async_obtain_profile_token() - if move_mode == CONTINUOUS_MOVE: - req.Velocity = { - "PanTilt": {"x": pan_val, "y": tilt_val}, - "Zoom": {"x": zoom_val}, - } - - await self._ptz_service.ContinuousMove(req) - await asyncio.sleep(continuous_duration) - req = self._ptz_service.create_type("Stop") - req.ProfileToken = await self.async_obtain_profile_token() - await self._ptz_service.Stop({"ProfileToken": req.ProfileToken}) - elif move_mode == RELATIVE_MOVE: - req.Translation = { - "PanTilt": {"x": pan_val, "y": tilt_val}, - "Zoom": {"x": zoom_val}, - } - req.Speed = { - "PanTilt": {"x": speed_val, "y": speed_val}, - "Zoom": {"x": speed_val}, - } - await self._ptz_service.RelativeMove(req) - elif move_mode == ABSOLUTE_MOVE: - req.Position = { - "PanTilt": {"x": pan_val, "y": tilt_val}, - "Zoom": {"x": zoom_val}, - } - req.Speed = { - "PanTilt": {"x": speed_val, "y": speed_val}, - "Zoom": {"x": speed_val}, - } - await self._ptz_service.AbsoluteMove(req) - elif move_mode == GOTOPRESET_MOVE: - req.PresetToken = preset_val - req.Speed = { - "PanTilt": {"x": speed_val, "y": speed_val}, - "Zoom": {"x": speed_val}, - } - await self._ptz_service.GotoPreset(req) - except exceptions.ONVIFError as err: - if "Bad Request" in err.reason: - self._ptz_service = None - _LOGGER.debug("Camera '%s' doesn't support PTZ.", self._name) - else: - _LOGGER.debug("Camera '%s' doesn't support PTZ.", self._name) + pan_val = distance * PAN_FACTOR.get(pan, 0) + tilt_val = distance * TILT_FACTOR.get(tilt, 0) + zoom_val = distance * ZOOM_FACTOR.get(zoom, 0) + speed_val = speed + preset_val = preset + LOGGER.debug( + "Calling %s PTZ | Pan = %4.2f | Tilt = %4.2f | Zoom = %4.2f | Speed = %4.2f | Preset = %s", + move_mode, + pan_val, + tilt_val, + zoom_val, + speed_val, + preset_val, + ) + try: + req = self._ptz_service.create_type(move_mode) + req.ProfileToken = self._profile_token + if move_mode == CONTINUOUS_MOVE: + req.Velocity = { + "PanTilt": {"x": pan_val, "y": tilt_val}, + "Zoom": {"x": zoom_val}, + } + + await self._ptz_service.ContinuousMove(req) + await asyncio.sleep(continuous_duration) + req = self._ptz_service.create_type("Stop") + req.ProfileToken = self._profile_token + await self._ptz_service.Stop({"ProfileToken": req.ProfileToken}) + elif move_mode == RELATIVE_MOVE: + req.Translation = { + "PanTilt": {"x": pan_val, "y": tilt_val}, + "Zoom": {"x": zoom_val}, + } + req.Speed = { + "PanTilt": {"x": speed_val, "y": speed_val}, + "Zoom": {"x": speed_val}, + } + await self._ptz_service.RelativeMove(req) + elif move_mode == ABSOLUTE_MOVE: + req.Position = { + "PanTilt": {"x": pan_val, "y": tilt_val}, + "Zoom": {"x": zoom_val}, + } + req.Speed = { + "PanTilt": {"x": speed_val, "y": speed_val}, + "Zoom": {"x": speed_val}, + } + await self._ptz_service.AbsoluteMove(req) + elif move_mode == GOTOPRESET_MOVE: + req.PresetToken = preset_val + req.Speed = { + "PanTilt": {"x": speed_val, "y": speed_val}, + "Zoom": {"x": speed_val}, + } + await self._ptz_service.GotoPreset(req) + except exceptions.ONVIFError as err: + if "Bad Request" in err.reason: + self._ptz_service = None + LOGGER.debug("Camera '%s' doesn't support PTZ.", self._name) + else: + LOGGER.error("Error trying to perform PTZ action: %s", err) async def async_added_to_hass(self): """Handle entity addition to hass.""" - _LOGGER.debug("Camera '%s' added to hass", self._name) + LOGGER.debug("Camera '%s' added to hass", self._name) - if ONVIF_DATA not in self.hass.data: - self.hass.data[ONVIF_DATA] = {} - self.hass.data[ONVIF_DATA][ENTITIES] = [] - self.hass.data[ONVIF_DATA][ENTITIES].append(self) + if DOMAIN not in self.hass.data: + self.hass.data[DOMAIN] = {} + self.hass.data[DOMAIN][ENTITIES] = [] + self.hass.data[DOMAIN][ENTITIES].append(self) async def async_camera_image(self): """Return a still image response from the camera.""" - _LOGGER.debug("Retrieving image from camera '%s'", self._name) + LOGGER.debug("Retrieving image from camera '%s'", self._name) image = None if self._snapshot is not None: @@ -537,7 +473,7 @@ def fetch(): if response.status_code < 300: return response.content except requests.exceptions.RequestException as error: - _LOGGER.error( + LOGGER.error( "Fetch snapshot image failed from %s, falling back to FFmpeg; %s", self._name, error, @@ -564,7 +500,7 @@ def fetch(): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - _LOGGER.debug("Handling mjpeg stream from camera '%s'", self._name) + LOGGER.debug("Handling mjpeg stream from camera '%s'", self._name) ffmpeg_manager = self.hass.data[DATA_FFMPEG] stream = CameraMjpeg(ffmpeg_manager.binary, loop=self.hass.loop) @@ -596,7 +532,7 @@ async def stream_source(self): @property def name(self): """Return the name of this camera.""" - return self._name + return f"{self._name} - {self._profile_name}" @property def unique_id(self) -> Optional[str]: @@ -604,3 +540,14 @@ def unique_id(self) -> Optional[str]: if self._profile_index: return f"{self._mac}_{self._profile_index}" return self._mac + + @property + def device_info(self): + """Return a device description for device registry.""" + return { + "identifiers": {(DOMAIN, self._mac)}, + "name": self._name, + "manufacturer": self._manufacturer, + "model": self._model, + "sw_version": self._firmware_version, + } diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py new file mode 100644 index 00000000000000..d5b153e27475ed --- /dev/null +++ b/homeassistant/components/onvif/config_flow.py @@ -0,0 +1,311 @@ +"""Config flow for ONVIF.""" +import os +from pprint import pformat +from typing import List +from urllib.parse import urlparse + +import onvif +from onvif import ONVIFCamera, exceptions +import voluptuous as vol +from wsdiscovery.discovery import ThreadedWSDiscovery as WSDiscovery +from wsdiscovery.scope import Scope +from wsdiscovery.service import Service +from zeep.asyncio import AsyncTransport +from zeep.exceptions import Fault + +from homeassistant import config_entries +from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) +from homeassistant.core import callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +# pylint: disable=unused-import +from .const import ( + CONF_DEVICE_ID, + CONF_PROFILE, + CONF_RTSP_TRANSPORT, + DEFAULT_ARGUMENTS, + DEFAULT_PORT, + DOMAIN, + LOGGER, + RTSP_TRANS_PROTOCOLS, +) + +CONF_MANUAL_INPUT = "Manually configure ONVIF device" + + +def wsdiscovery() -> List[Service]: + """Get ONVIF Profile S devices from network.""" + discovery = WSDiscovery(ttl=4) + discovery.start() + services = discovery.searchServices( + scopes=[Scope("onvif://www.onvif.org/Profile/Streaming")] + ) + discovery.stop() + return services + + +async def async_discovery(hass) -> bool: + """Return if there are devices that can be discovered.""" + LOGGER.debug("Starting ONVIF discovery...") + services = await hass.async_add_executor_job(wsdiscovery) + + devices = [] + for service in services: + url = urlparse(service.getXAddrs()[0]) + device = { + CONF_DEVICE_ID: None, + CONF_NAME: service.getEPR(), + CONF_HOST: url.hostname, + CONF_PORT: url.port or 80, + } + for scope in service.getScopes(): + scope_str = scope.getValue() + if scope_str.lower().startswith("onvif://www.onvif.org/name"): + device[CONF_NAME] = scope_str.split("/")[-1] + if scope_str.lower().startswith("onvif://www.onvif.org/mac"): + device[CONF_DEVICE_ID] = scope_str.split("/")[-1] + devices.append(device) + + return devices + + +class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a ONVIF config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OnvifOptionsFlowHandler(config_entry) + + def __init__(self): + """Initialize the ONVIF config flow.""" + self.device_id = None + self.devices = [] + self.onvif_config = {} + + async def async_step_user(self, user_input=None): + """Handle user flow.""" + if user_input is not None: + return await self.async_step_device() + + return self.async_show_form(step_id="user") + + async def async_step_device(self, user_input=None): + """Handle WS-Discovery. + + Let user choose between discovered devices and manual configuration. + If no device is found allow user to manually input configuration. + """ + if user_input: + + if CONF_MANUAL_INPUT == user_input[CONF_HOST]: + return await self.async_step_manual_input() + + for device in self.devices: + name = f"{device[CONF_NAME]} ({device[CONF_HOST]})" + if name == user_input[CONF_HOST]: + self.device_id = device[CONF_DEVICE_ID] + self.onvif_config = { + CONF_NAME: device[CONF_NAME], + CONF_HOST: device[CONF_HOST], + CONF_PORT: device[CONF_PORT], + } + return await self.async_step_auth() + + discovery = await async_discovery(self.hass) + for device in discovery: + configured = False + for entry in self._async_current_entries(): + if entry.unique_id == device[CONF_DEVICE_ID]: + configured = True + break + if not configured: + self.devices.append(device) + + LOGGER.debug("Discovered ONVIF devices %s", pformat(self.devices)) + + if self.devices: + names = [] + + for device in self.devices: + names.append(f"{device[CONF_NAME]} ({device[CONF_HOST]})") + + names.append(CONF_MANUAL_INPUT) + + return self.async_show_form( + step_id="device", + data_schema=vol.Schema({vol.Optional(CONF_HOST): vol.In(names)}), + ) + + return await self.async_step_manual_input() + + async def async_step_manual_input(self, user_input=None): + """Manual configuration.""" + if user_input: + self.onvif_config = user_input + return await self.async_step_auth() + + return self.async_show_form( + step_id="manual_input", + data_schema=vol.Schema( + { + vol.Required(CONF_NAME): str, + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + } + ), + ) + + async def async_step_auth(self, user_input=None): + """Username and Password configuration for ONVIF device.""" + if user_input: + self.onvif_config[CONF_USERNAME] = user_input[CONF_USERNAME] + self.onvif_config[CONF_PASSWORD] = user_input[CONF_PASSWORD] + return await self.async_step_profiles() + + return self.async_show_form( + step_id="auth", + data_schema=vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + ), + ) + + async def async_step_profiles(self, user_input=None): + """Fetch ONVIF device profiles.""" + errors = {} + + LOGGER.debug( + "Fetching profiles from ONVIF device %s", pformat(self.onvif_config) + ) + + device = get_device( + self.hass, + self.onvif_config[CONF_HOST], + self.onvif_config[CONF_PORT], + self.onvif_config[CONF_USERNAME], + self.onvif_config[CONF_PASSWORD], + ) + + await device.update_xaddrs() + + try: + # Get the MAC address to use as the unique ID for the config flow + if not self.device_id: + devicemgmt = device.create_devicemgmt_service() + network_interfaces = await devicemgmt.GetNetworkInterfaces() + for interface in network_interfaces: + if interface.Enabled: + self.device_id = interface.Info.HwAddress + + if self.device_id is None: + return self.async_abort(reason="no_mac") + + await self.async_set_unique_id(self.device_id, raise_on_progress=False) + self._abort_if_unique_id_configured( + updates={ + CONF_HOST: self.onvif_config[CONF_HOST], + CONF_PORT: self.onvif_config[CONF_PORT], + CONF_NAME: self.onvif_config[CONF_NAME], + } + ) + + if not self.onvif_config.get(CONF_PROFILE): + self.onvif_config[CONF_PROFILE] = [] + media_service = device.create_media_service() + profiles = await media_service.GetProfiles() + LOGGER.debug("Media Profiles %s", pformat(profiles)) + for key, profile in enumerate(profiles): + if profile.VideoEncoderConfiguration.Encoding != "H264": + continue + self.onvif_config[CONF_PROFILE].append(key) + + if not self.onvif_config[CONF_PROFILE]: + return self.async_abort(reason="no_h264") + + title = f"{self.onvif_config[CONF_NAME]} - {self.device_id}" + return self.async_create_entry(title=title, data=self.onvif_config) + + except exceptions.ONVIFError as err: + LOGGER.error( + "Couldn't setup ONVIF device '%s'. Error: %s", + self.onvif_config[CONF_NAME], + err, + ) + return self.async_abort(reason="onvif_error") + + except Fault: + errors["base"] = "connection_failed" + + return self.async_show_form(step_id="auth", errors=errors) + + async def async_step_import(self, user_input): + """Handle import.""" + self.onvif_config = user_input + return await self.async_step_profiles() + + +class OnvifOptionsFlowHandler(config_entries.OptionsFlow): + """Handle ONVIF options.""" + + def __init__(self, config_entry): + """Initialize ONVIF options flow.""" + self.config_entry = config_entry + self.options = dict(config_entry.options) + + async def async_step_init(self, user_input=None): + """Manage the ONVIF options.""" + return await self.async_step_onvif_devices() + + async def async_step_onvif_devices(self, user_input=None): + """Manage the ONVIF devices options.""" + if user_input is not None: + self.options[CONF_EXTRA_ARGUMENTS] = user_input[CONF_EXTRA_ARGUMENTS] + self.options[CONF_RTSP_TRANSPORT] = user_input[CONF_RTSP_TRANSPORT] + return self.async_create_entry(title="", data=self.options) + + return self.async_show_form( + step_id="onvif_devices", + data_schema=vol.Schema( + { + vol.Optional( + CONF_EXTRA_ARGUMENTS, + default=self.config_entry.options.get( + CONF_EXTRA_ARGUMENTS, DEFAULT_ARGUMENTS + ), + ): str, + vol.Optional( + CONF_RTSP_TRANSPORT, + default=self.config_entry.options.get( + CONF_RTSP_TRANSPORT, RTSP_TRANS_PROTOCOLS[0] + ), + ): vol.In(RTSP_TRANS_PROTOCOLS), + } + ), + ) + + +def get_device(hass, host, port, username, password) -> ONVIFCamera: + """Get ONVIFCamera instance.""" + session = async_get_clientsession(hass) + transport = AsyncTransport(None, session=session) + device = ONVIFCamera( + host, + port, + username, + password, + f"{os.path.dirname(onvif.__file__)}/wsdl/", + transport=transport, + ) + + return device diff --git a/homeassistant/components/onvif/const.py b/homeassistant/components/onvif/const.py new file mode 100644 index 00000000000000..c2eb2604a26e29 --- /dev/null +++ b/homeassistant/components/onvif/const.py @@ -0,0 +1,47 @@ +"""Constants for the onvif component.""" +import logging + +LOGGER = logging.getLogger(__package__) + +DOMAIN = "onvif" +ONVIF_DATA = "onvif" +ENTITIES = "entities" + +DEFAULT_NAME = "ONVIF Camera" +DEFAULT_PORT = 5000 +DEFAULT_USERNAME = "admin" +DEFAULT_PASSWORD = "888888" +DEFAULT_ARGUMENTS = "-pred 1" +DEFAULT_PROFILE = 0 + +CONF_DEVICE_ID = "deviceid" +CONF_PROFILE = "profile" +CONF_RTSP_TRANSPORT = "rtsp_transport" + +RTSP_TRANS_PROTOCOLS = ["tcp", "udp", "udp_multicast", "http"] + +ATTR_PAN = "pan" +ATTR_TILT = "tilt" +ATTR_ZOOM = "zoom" +ATTR_DISTANCE = "distance" +ATTR_SPEED = "speed" +ATTR_MOVE_MODE = "move_mode" +ATTR_CONTINUOUS_DURATION = "continuous_duration" +ATTR_PRESET = "preset" + +DIR_UP = "UP" +DIR_DOWN = "DOWN" +DIR_LEFT = "LEFT" +DIR_RIGHT = "RIGHT" +ZOOM_OUT = "ZOOM_OUT" +ZOOM_IN = "ZOOM_IN" +PAN_FACTOR = {DIR_RIGHT: 1, DIR_LEFT: -1} +TILT_FACTOR = {DIR_UP: 1, DIR_DOWN: -1} +ZOOM_FACTOR = {ZOOM_IN: 1, ZOOM_OUT: -1} +CONTINUOUS_MOVE = "ContinuousMove" +RELATIVE_MOVE = "RelativeMove" +ABSOLUTE_MOVE = "AbsoluteMove" +GOTOPRESET_MOVE = "GotoPreset" + +SERVICE_PTZ = "ptz" +ENTITIES = "entities" diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index a927fd9072b959..5931f3c71c3842 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -2,7 +2,8 @@ "domain": "onvif", "name": "ONVIF", "documentation": "https://www.home-assistant.io/integrations/onvif", - "requirements": ["onvif-zeep-async==0.2.0"], + "requirements": ["onvif-zeep-async==0.2.0", "WSDiscovery==2.0.0"], "dependencies": ["ffmpeg"], - "codeowners": [] + "codeowners": ["@hunterjm"], + "config_flow": true } diff --git a/homeassistant/components/onvif/strings.json b/homeassistant/components/onvif/strings.json new file mode 100644 index 00000000000000..e0de8857edfa9d --- /dev/null +++ b/homeassistant/components/onvif/strings.json @@ -0,0 +1,58 @@ +{ + "config": { + "abort": { + "already_configured": "ONVIF device is already configured.", + "already_in_progress": "Config flow for ONVIF device is already in progress.", + "onvif_error": "Error setting up ONVIF device. Check logs for more information.", + "no_h264": "There were no H264 streams available. Check the profile configuration on your device.", + "no_mac": "Could not configure unique ID for ONVIF device." + }, + "error": { + "connection_failed": "Could not connect to ONVIF service with provided credentials." + }, + "step": { + "user": { + "title": "ONVIF device setup", + "description": "By clicking submit, we will search your network for ONVIF devices that support Profile S.\n\nSome manufacturers have started to disable ONVIF by default. Please ensure ONVIF is enabled in your camera's configuration." + }, + "device": { + "data": { + "host": "Select discovered ONVIF device" + }, + "title": "Select ONVIF device" + }, + "manual_input": { + "data": { + "host": "Host", + "port": "Port" + }, + "title": "Configure ONVIF device" + }, + "auth": { + "title": "Configure authentication", + "data": { + "username": "Username", + "password": "Password" + } + }, + "configure_profile": { + "description": "Create camera entity for {profile} at {resolution} resolution?", + "title": "Configure Profiles", + "data": { + "include": "Create camera entity" + } + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Extra FFMPEG arguments", + "rtsp_transport": "RTSP transport mechanism" + }, + "title": "ONVIF Device Options" + } + } + } +} diff --git a/homeassistant/components/onvif/translations/en.json b/homeassistant/components/onvif/translations/en.json new file mode 100644 index 00000000000000..066efed60f2e9f --- /dev/null +++ b/homeassistant/components/onvif/translations/en.json @@ -0,0 +1,59 @@ +{ + "config": { + "abort": { + "already_configured": "ONVIF device is already configured.", + "already_in_progress": "Config flow for ONVIF device is already in progress.", + "no_h264": "There were no H264 streams available. Check the profile configuration on your device.", + "no_mac": "Could not configure unique ID for ONVIF device.", + "onvif_error": "Error setting up ONVIF device. Check logs for more information." + }, + "error": { + "connection_failed": "Could not connect to ONVIF service with provided credentials." + }, + "step": { + "auth": { + "data": { + "password": "Password", + "username": "Username" + }, + "title": "Configure authentication" + }, + "configure_profile": { + "data": { + "include": "Create camera entity" + }, + "description": "Create camera entity for {profile} at {resolution} resolution?", + "title": "Configure Profiles" + }, + "device": { + "data": { + "host": "Select discovered ONVIF device" + }, + "title": "Select ONVIF device" + }, + "manual_input": { + "data": { + "host": "Host", + "port": "Port" + }, + "title": "Configure ONVIF device" + }, + "user": { + "description": "By clicking submit, we will search your network for ONVIF devices that support Profile S.\n\nSome manufacturers have started to disable ONVIF by default. Please ensure ONVIF is enabled in your camera's configuration.", + "title": "ONVIF device setup" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Extra FFMPEG arguments", + "rtsp_transport": "RTSP transport mechanism" + }, + "title": "ONVIF Device Options" + } + } + }, + "title": "ONVIF" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index a65d1e2b52ac70..1f08da03648fce 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -91,6 +91,7 @@ "nuheat", "nut", "nws", + "onvif", "opentherm_gw", "openuv", "owntracks", diff --git a/requirements_all.txt b/requirements_all.txt index d166d68ae7f3cb..e35d2f980ec68f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -101,6 +101,9 @@ TwitterAPI==2.5.11 # homeassistant.components.tof # VL53L1X2==0.1.5 +# homeassistant.components.onvif +WSDiscovery==2.0.0 + # homeassistant.components.waze_travel_time WazeRouteCalculator==0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 607acf46478d48..130e6568fffbe7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -23,6 +23,9 @@ PyTransportNSW==0.1.1 # homeassistant.components.remember_the_milk RtmAPI==0.7.2 +# homeassistant.components.onvif +WSDiscovery==2.0.0 + # homeassistant.components.yessssms YesssSMS==0.4.1 @@ -389,6 +392,9 @@ numpy==1.18.2 # homeassistant.components.google oauth2client==4.0.0 +# homeassistant.components.onvif +onvif-zeep-async==0.2.0 + # homeassistant.components.openerz openerz-api==0.1.0 diff --git a/tests/components/onvif/__init__.py b/tests/components/onvif/__init__.py new file mode 100644 index 00000000000000..433a6392f128a1 --- /dev/null +++ b/tests/components/onvif/__init__.py @@ -0,0 +1 @@ +"""Tests for the ONVIF integration.""" diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py new file mode 100644 index 00000000000000..685e1e3fc4df28 --- /dev/null +++ b/tests/components/onvif/test_config_flow.py @@ -0,0 +1,508 @@ +"""Test ONVIF config flow.""" +from asyncio import Future + +from asynctest import MagicMock, patch +from onvif.exceptions import ONVIFError +from zeep.exceptions import Fault + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.onvif import config_flow + +from tests.common import MockConfigEntry + +URN = "urn:uuid:123456789" +NAME = "TestCamera" +HOST = "1.2.3.4" +PORT = 80 +USERNAME = "admin" +PASSWORD = "12345" +MAC = "aa:bb:cc:dd:ee" + +DISCOVERY = [ + { + "EPR": URN, + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + "MAC": MAC, + }, + { + "EPR": "urn:uuid:987654321", + config_flow.CONF_NAME: "TestCamera2", + config_flow.CONF_HOST: "5.6.7.8", + config_flow.CONF_PORT: PORT, + "MAC": "ee:dd:cc:bb:aa", + }, +] + + +def setup_mock_onvif_device( + mock_device, with_h264=True, two_profiles=False, with_interfaces=True +): + """Prepare mock ONVIF device.""" + devicemgmt = MagicMock() + + interface = MagicMock() + interface.Enabled = True + interface.Info.HwAddress = MAC + + devicemgmt.GetNetworkInterfaces.return_value = Future() + devicemgmt.GetNetworkInterfaces.return_value.set_result( + [interface] if with_interfaces else [] + ) + + media_service = MagicMock() + + profile1 = MagicMock() + profile1.VideoEncoderConfiguration.Encoding = "H264" if with_h264 else "MJPEG" + profile2 = MagicMock() + profile2.VideoEncoderConfiguration.Encoding = "H264" if two_profiles else "MJPEG" + + media_service.GetProfiles.return_value = Future() + media_service.GetProfiles.return_value.set_result([profile1, profile2]) + + mock_device.update_xaddrs.return_value = Future() + mock_device.update_xaddrs.return_value.set_result(True) + mock_device.create_devicemgmt_service = MagicMock(return_value=devicemgmt) + mock_device.create_media_service = MagicMock(return_value=media_service) + + def mock_constructor( + host, + port, + user, + passwd, + wsdl_dir, + encrypt=True, + no_cache=False, + adjust_time=False, + transport=None, + ): + """Fake the controller constructor.""" + return mock_device + + mock_device.side_effect = mock_constructor + + +def setup_mock_discovery( + mock_discovery, with_name=False, with_mac=False, two_devices=False +): + """Prepare mock discovery result.""" + services = [] + for item in DISCOVERY: + service = MagicMock() + service.getXAddrs = MagicMock( + return_value=[ + f"http://{item[config_flow.CONF_HOST]}:{item[config_flow.CONF_PORT]}/onvif/device_service" + ] + ) + service.getEPR = MagicMock(return_value=item["EPR"]) + scopes = [] + if with_name: + scope = MagicMock() + scope.getValue = MagicMock( + return_value=f"onvif://www.onvif.org/name/{item[config_flow.CONF_NAME]}" + ) + scopes.append(scope) + if with_mac: + scope = MagicMock() + scope.getValue = MagicMock( + return_value=f"onvif://www.onvif.org/mac/{item['MAC']}" + ) + scopes.append(scope) + service.getScopes = MagicMock(return_value=scopes) + services.append(service) + mock_discovery.return_value = services + + +def setup_mock_camera(mock_camera): + """Prepare mock HASS camera.""" + mock_camera.async_initialize.return_value = Future() + mock_camera.async_initialize.return_value.set_result(True) + + def mock_constructor(hass, config): + """Fake the controller constructor.""" + return mock_camera + + mock_camera.side_effect = mock_constructor + + +async def setup_onvif_integration( + hass, config=None, options=None, unique_id=MAC, entry_id="1", source="user", +): + """Create an ONVIF config entry.""" + if not config: + config = { + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + config_flow.CONF_PROFILE: [0], + } + + config_entry = MockConfigEntry( + domain=config_flow.DOMAIN, + source=source, + data={**config}, + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, + options=options or {}, + entry_id=entry_id, + unique_id=unique_id, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_device, patch( + "homeassistant.components.onvif.config_flow.wsdiscovery" + ) as mock_discovery, patch( + "homeassistant.components.onvif.camera.ONVIFHassCamera" + ) as mock_camera: + setup_mock_onvif_device(mock_device, two_profiles=True) + # no discovery + mock_discovery.return_value = [] + setup_mock_camera(mock_camera) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + return config_entry + + +async def test_flow_discovered_devices(hass): + """Test that config flow works for discovered devices.""" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_device, patch( + "homeassistant.components.onvif.config_flow.wsdiscovery" + ) as mock_discovery, patch( + "homeassistant.components.onvif.camera.ONVIFHassCamera" + ) as mock_camera: + setup_mock_onvif_device(mock_device) + setup_mock_discovery(mock_discovery) + setup_mock_camera(mock_camera) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "device" + assert len(result["data_schema"].schema[config_flow.CONF_HOST].container) == 3 + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={config_flow.CONF_HOST: f"{URN} ({HOST})"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "auth" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + }, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == f"{URN} - {MAC}" + assert result["data"] == { + config_flow.CONF_NAME: URN, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + config_flow.CONF_PROFILE: [0], + } + + +async def test_flow_discovered_devices_ignore_configured_manual_input(hass): + """Test that config flow discovery ignores configured devices.""" + await setup_onvif_integration(hass) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_device, patch( + "homeassistant.components.onvif.config_flow.wsdiscovery" + ) as mock_discovery, patch( + "homeassistant.components.onvif.camera.ONVIFHassCamera" + ) as mock_camera: + setup_mock_onvif_device(mock_device) + setup_mock_discovery(mock_discovery, with_mac=True) + setup_mock_camera(mock_camera) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "device" + assert len(result["data_schema"].schema[config_flow.CONF_HOST].container) == 2 + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={config_flow.CONF_HOST: config_flow.CONF_MANUAL_INPUT}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "manual_input" + + +async def test_flow_discovery_ignore_existing_and_abort(hass): + """Test that config flow discovery ignores setup devices.""" + await setup_onvif_integration(hass) + await setup_onvif_integration( + hass, + config={ + config_flow.CONF_NAME: DISCOVERY[1]["EPR"], + config_flow.CONF_HOST: DISCOVERY[1][config_flow.CONF_HOST], + config_flow.CONF_PORT: DISCOVERY[1][config_flow.CONF_PORT], + config_flow.CONF_USERNAME: "", + config_flow.CONF_PASSWORD: "", + }, + unique_id=DISCOVERY[1]["MAC"], + entry_id="2", + ) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_device, patch( + "homeassistant.components.onvif.config_flow.wsdiscovery" + ) as mock_discovery, patch( + "homeassistant.components.onvif.camera.ONVIFHassCamera" + ) as mock_camera: + setup_mock_onvif_device(mock_device) + setup_mock_discovery(mock_discovery, with_name=True, with_mac=True) + setup_mock_camera(mock_camera) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + # It should skip to manual entry if the only devices are already configured + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "manual_input" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "auth" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + }, + ) + + # It should abort if already configured and entered manually + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_flow_manual_entry(hass): + """Test that config flow works for discovered devices.""" + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_device, patch( + "homeassistant.components.onvif.config_flow.wsdiscovery" + ) as mock_discovery, patch( + "homeassistant.components.onvif.camera.ONVIFHassCamera" + ) as mock_camera: + setup_mock_onvif_device(mock_device, two_profiles=True) + # no discovery + mock_discovery.return_value = [] + setup_mock_camera(mock_camera) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "manual_input" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "auth" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + }, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == f"{NAME} - {MAC}" + assert result["data"] == { + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + config_flow.CONF_PROFILE: [0, 1], + } + + +async def test_flow_import_no_mac(hass): + """Test that config flow fails when no MAC available.""" + with patch("homeassistant.components.onvif.config_flow.get_device") as mock_device: + setup_mock_onvif_device(mock_device, with_interfaces=False) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + config_flow.CONF_PROFILE: [0], + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "no_mac" + + +async def test_flow_import_no_h264(hass): + """Test that config flow fails when no MAC available.""" + with patch("homeassistant.components.onvif.config_flow.get_device") as mock_device: + setup_mock_onvif_device(mock_device, with_h264=False) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "no_h264" + + +async def test_flow_import_onvif_api_error(hass): + """Test that config flow fails when ONVIF API fails.""" + with patch("homeassistant.components.onvif.config_flow.get_device") as mock_device: + setup_mock_onvif_device(mock_device) + mock_device.create_devicemgmt_service = MagicMock( + side_effect=ONVIFError("Could not get device mgmt service") + ) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "onvif_error" + + +async def test_flow_import_onvif_auth_error(hass): + """Test that config flow fails when ONVIF API fails.""" + with patch("homeassistant.components.onvif.config_flow.get_device") as mock_device: + setup_mock_onvif_device(mock_device) + mock_device.create_devicemgmt_service = MagicMock( + side_effect=Fault("Auth Error") + ) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "auth" + assert result["errors"]["base"] == "connection_failed" + + +async def test_option_flow(hass): + """Test config flow options.""" + entry = await setup_onvif_integration(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "onvif_devices" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + config_flow.CONF_EXTRA_ARGUMENTS: "", + config_flow.CONF_RTSP_TRANSPORT: config_flow.RTSP_TRANS_PROTOCOLS[1], + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + config_flow.CONF_EXTRA_ARGUMENTS: "", + config_flow.CONF_RTSP_TRANSPORT: config_flow.RTSP_TRANS_PROTOCOLS[1], + } From a2896b4764330860421acfccf317d922bbee60ed Mon Sep 17 00:00:00 2001 From: Vilppu Vuorinen Date: Fri, 1 May 2020 09:56:03 +0300 Subject: [PATCH 36/58] Add flow and return sensors for MELCloud ATW device (#34693) --- .../components/melcloud/manifest.json | 2 +- homeassistant/components/melcloud/sensor.py | 18 +++++++++++++++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/melcloud/manifest.json b/homeassistant/components/melcloud/manifest.json index 4747059345f86f..aac8db678f969c 100644 --- a/homeassistant/components/melcloud/manifest.json +++ b/homeassistant/components/melcloud/manifest.json @@ -3,6 +3,6 @@ "name": "MELCloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/melcloud", - "requirements": ["pymelcloud==2.4.1"], + "requirements": ["pymelcloud==2.5.2"], "codeowners": ["@vilppuvuorinen"] } diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py index 9dee01c2fbabf6..6ca69aefe2a373 100644 --- a/homeassistant/components/melcloud/sensor.py +++ b/homeassistant/components/melcloud/sensor.py @@ -65,7 +65,23 @@ ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_VALUE_FN: lambda zone: zone.room_temperature, ATTR_ENABLED_FN: lambda x: True, - } + }, + "flow_temperature": { + ATTR_MEASUREMENT_NAME: "Flow Temperature", + ATTR_ICON: "mdi:thermometer", + ATTR_UNIT_FN: lambda x: TEMP_UNIT_LOOKUP.get(x.device.temp_unit, TEMP_CELSIUS), + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_VALUE_FN: lambda zone: zone.flow_temperature, + ATTR_ENABLED_FN: lambda x: True, + }, + "return_temperature": { + ATTR_MEASUREMENT_NAME: "Flow Return Temperature", + ATTR_ICON: "mdi:thermometer", + ATTR_UNIT_FN: lambda x: TEMP_UNIT_LOOKUP.get(x.device.temp_unit, TEMP_CELSIUS), + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_VALUE_FN: lambda zone: zone.return_temperature, + ATTR_ENABLED_FN: lambda x: True, + }, } _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index e35d2f980ec68f..62a27f90c383c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1411,7 +1411,7 @@ pymailgunner==1.4 pymediaroom==0.6.4 # homeassistant.components.melcloud -pymelcloud==2.4.1 +pymelcloud==2.5.2 # homeassistant.components.somfy pymfy==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 130e6568fffbe7..33bd8c3f9d0a7c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -576,7 +576,7 @@ pylitejet==0.1 pymailgunner==1.4 # homeassistant.components.melcloud -pymelcloud==2.4.1 +pymelcloud==2.5.2 # homeassistant.components.somfy pymfy==0.7.1 From 8ec5e53cf43a2c6072706d3c6a594c1a3c8590d2 Mon Sep 17 00:00:00 2001 From: Guillaume DELVIT Date: Fri, 1 May 2020 10:29:22 +0200 Subject: [PATCH 37/58] Add full options to serial sensor platform (#34962) Improve Serial sensor platform with full options for configuring the serial port. bytesize, parity, stopbits, xonxoff, rtscts, dsrdtr. The default values are unchanged so it is 100% compatible with previous config. --- homeassistant/components/serial/sensor.py | 113 +++++++++++++++++++++- 1 file changed, 108 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/serial/sensor.py b/homeassistant/components/serial/sensor.py index 905167ea3ff0c0..e0bf23a251405e 100644 --- a/homeassistant/components/serial/sensor.py +++ b/homeassistant/components/serial/sensor.py @@ -17,9 +17,21 @@ CONF_SERIAL_PORT = "serial_port" CONF_BAUDRATE = "baudrate" +CONF_BYTESIZE = "bytesize" +CONF_PARITY = "parity" +CONF_STOPBITS = "stopbits" +CONF_XONXOFF = "xonxoff" +CONF_RTSCTS = "rtscts" +CONF_DSRDTR = "dsrdtr" DEFAULT_NAME = "Serial Sensor" DEFAULT_BAUDRATE = 9600 +DEFAULT_BYTESIZE = serial_asyncio.serial.EIGHTBITS +DEFAULT_PARITY = serial_asyncio.serial.PARITY_NONE +DEFAULT_STOPBITS = serial_asyncio.serial.STOPBITS_ONE +DEFAULT_XONXOFF = False +DEFAULT_RTSCTS = False +DEFAULT_DSRDTR = False PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -27,6 +39,33 @@ vol.Optional(CONF_BAUDRATE, default=DEFAULT_BAUDRATE): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_BYTESIZE, default=DEFAULT_BYTESIZE): vol.In( + [ + serial_asyncio.serial.FIVEBITS, + serial_asyncio.serial.SIXBITS, + serial_asyncio.serial.SEVENBITS, + serial_asyncio.serial.EIGHTBITS, + ] + ), + vol.Optional(CONF_PARITY, default=DEFAULT_PARITY): vol.In( + [ + serial_asyncio.serial.PARITY_NONE, + serial_asyncio.serial.PARITY_EVEN, + serial_asyncio.serial.PARITY_ODD, + serial_asyncio.serial.PARITY_MARK, + serial_asyncio.serial.PARITY_SPACE, + ] + ), + vol.Optional(CONF_STOPBITS, default=DEFAULT_STOPBITS): vol.In( + [ + serial_asyncio.serial.STOPBITS_ONE, + serial_asyncio.serial.STOPBITS_ONE_POINT_FIVE, + serial_asyncio.serial.STOPBITS_TWO, + ] + ), + vol.Optional(CONF_XONXOFF, default=DEFAULT_XONXOFF): cv.boolean, + vol.Optional(CONF_RTSCTS, default=DEFAULT_RTSCTS): cv.boolean, + vol.Optional(CONF_DSRDTR, default=DEFAULT_DSRDTR): cv.boolean, } ) @@ -36,12 +75,29 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= name = config.get(CONF_NAME) port = config.get(CONF_SERIAL_PORT) baudrate = config.get(CONF_BAUDRATE) + bytesize = config.get(CONF_BYTESIZE) + parity = config.get(CONF_PARITY) + stopbits = config.get(CONF_STOPBITS) + xonxoff = config.get(CONF_XONXOFF) + rtscts = config.get(CONF_RTSCTS) + dsrdtr = config.get(CONF_DSRDTR) value_template = config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = hass - sensor = SerialSensor(name, port, baudrate, value_template) + sensor = SerialSensor( + name, + port, + baudrate, + bytesize, + parity, + stopbits, + xonxoff, + rtscts, + dsrdtr, + value_template, + ) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, sensor.stop_serial_read) async_add_entities([sensor], True) @@ -50,12 +106,30 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class SerialSensor(Entity): """Representation of a Serial sensor.""" - def __init__(self, name, port, baudrate, value_template): + def __init__( + self, + name, + port, + baudrate, + bytesize, + parity, + stopbits, + xonxoff, + rtscts, + dsrdtr, + value_template, + ): """Initialize the Serial sensor.""" self._name = name self._state = None self._port = port self._baudrate = baudrate + self._bytesize = bytesize + self._parity = parity + self._stopbits = stopbits + self._xonxoff = xonxoff + self._rtscts = rtscts + self._dsrdtr = dsrdtr self._serial_loop_task = None self._template = value_template self._attributes = None @@ -63,17 +137,46 @@ def __init__(self, name, port, baudrate, value_template): async def async_added_to_hass(self): """Handle when an entity is about to be added to Home Assistant.""" self._serial_loop_task = self.hass.loop.create_task( - self.serial_read(self._port, self._baudrate) + self.serial_read( + self._port, + self._baudrate, + self._bytesize, + self._parity, + self._stopbits, + self._xonxoff, + self._rtscts, + self._dsrdtr, + ) ) - async def serial_read(self, device, rate, **kwargs): + async def serial_read( + self, + device, + baudrate, + bytesize, + parity, + stopbits, + xonxoff, + rtscts, + dsrdtr, + **kwargs, + ): """Read the data from the port.""" logged_error = False while True: try: reader, _ = await serial_asyncio.open_serial_connection( - url=device, baudrate=rate, **kwargs + url=device, + baudrate=baudrate, + bytesize=bytesize, + parity=parity, + stopbits=stopbits, + xonxoff=xonxoff, + rtscts=rtscts, + dsrdtr=dsrdtr, + **kwargs, ) + except SerialException as exc: if not logged_error: _LOGGER.exception( From 66d3832be904ae6207f242a9a022a132d9c1f4dd Mon Sep 17 00:00:00 2001 From: Vilppu Vuorinen Date: Fri, 1 May 2020 14:33:46 +0300 Subject: [PATCH 38/58] Fix MELCloud temperature unit (#35003) The MELCLoud API produces and consumes only Celsius. --- homeassistant/components/melcloud/climate.py | 18 ++++++------------ homeassistant/components/melcloud/const.py | 9 --------- homeassistant/components/melcloud/sensor.py | 16 ++++++++-------- .../components/melcloud/water_heater.py | 4 ++-- 4 files changed, 16 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py index bc2ac7f10262d2..ed2fefc823d350 100644 --- a/homeassistant/components/melcloud/climate.py +++ b/homeassistant/components/melcloud/climate.py @@ -31,7 +31,6 @@ from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.util.temperature import convert as convert_temperature from . import MelCloudDevice from .const import ( @@ -44,7 +43,6 @@ DOMAIN, SERVICE_SET_VANE_HORIZONTAL, SERVICE_SET_VANE_VERTICAL, - TEMP_UNIT_LOOKUP, ) SCAN_INTERVAL = timedelta(seconds=60) @@ -169,7 +167,7 @@ def device_state_attributes(self) -> Optional[Dict[str, Any]]: @property def temperature_unit(self) -> str: """Return the unit of measurement used by the platform.""" - return TEMP_UNIT_LOOKUP.get(self._device.temp_unit, TEMP_CELSIUS) + return TEMP_CELSIUS @property def hvac_mode(self) -> str: @@ -281,9 +279,7 @@ def min_temp(self) -> float: if min_value is not None: return min_value - return convert_temperature( - DEFAULT_MIN_TEMP, TEMP_CELSIUS, self.temperature_unit - ) + return DEFAULT_MIN_TEMP @property def max_temp(self) -> float: @@ -292,9 +288,7 @@ def max_temp(self) -> float: if max_value is not None: return max_value - return convert_temperature( - DEFAULT_MAX_TEMP, TEMP_CELSIUS, self.temperature_unit - ) + return DEFAULT_MAX_TEMP class AtwDeviceZoneClimate(MelCloudClimate): @@ -331,7 +325,7 @@ def device_state_attributes(self) -> Dict[str, Any]: @property def temperature_unit(self) -> str: """Return the unit of measurement used by the platform.""" - return TEMP_UNIT_LOOKUP.get(self._device.temp_unit, TEMP_CELSIUS) + return TEMP_CELSIUS @property def hvac_mode(self) -> str: @@ -391,7 +385,7 @@ def min_temp(self) -> float: MELCloud API does not expose radiator zone temperature limits. """ - return convert_temperature(10, TEMP_CELSIUS, self.temperature_unit) + return 10 @property def max_temp(self) -> float: @@ -399,4 +393,4 @@ def max_temp(self) -> float: MELCloud API does not expose radiator zone temperature limits. """ - return convert_temperature(30, TEMP_CELSIUS, self.temperature_unit) + return 30 diff --git a/homeassistant/components/melcloud/const.py b/homeassistant/components/melcloud/const.py index d58f483d441eee..27cffb75223367 100644 --- a/homeassistant/components/melcloud/const.py +++ b/homeassistant/components/melcloud/const.py @@ -1,7 +1,4 @@ """Constants for the MELCloud Climate integration.""" -from pymelcloud.const import UNIT_TEMP_CELSIUS, UNIT_TEMP_FAHRENHEIT - -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT DOMAIN = "melcloud" @@ -15,9 +12,3 @@ SERVICE_SET_VANE_HORIZONTAL = "set_vane_horizontal" SERVICE_SET_VANE_VERTICAL = "set_vane_vertical" - -TEMP_UNIT_LOOKUP = { - UNIT_TEMP_CELSIUS: TEMP_CELSIUS, - UNIT_TEMP_FAHRENHEIT: TEMP_FAHRENHEIT, -} -TEMP_UNIT_REVERSE_LOOKUP = {v: k for k, v in TEMP_UNIT_LOOKUP.items()} diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py index 6ca69aefe2a373..ed64647071b566 100644 --- a/homeassistant/components/melcloud/sensor.py +++ b/homeassistant/components/melcloud/sensor.py @@ -12,11 +12,11 @@ from homeassistant.helpers.entity import Entity from . import MelCloudDevice -from .const import DOMAIN, TEMP_UNIT_LOOKUP +from .const import DOMAIN ATTR_MEASUREMENT_NAME = "measurement_name" ATTR_ICON = "icon" -ATTR_UNIT_FN = "unit_fn" +ATTR_UNIT = "unit" ATTR_DEVICE_CLASS = "device_class" ATTR_VALUE_FN = "value_fn" ATTR_ENABLED_FN = "enabled" @@ -25,7 +25,7 @@ "room_temperature": { ATTR_MEASUREMENT_NAME: "Room Temperature", ATTR_ICON: "mdi:thermometer", - ATTR_UNIT_FN: lambda x: TEMP_UNIT_LOOKUP.get(x.device.temp_unit, TEMP_CELSIUS), + ATTR_UNIT: TEMP_CELSIUS, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_VALUE_FN: lambda x: x.device.room_temperature, ATTR_ENABLED_FN: lambda x: True, @@ -33,7 +33,7 @@ "energy": { ATTR_MEASUREMENT_NAME: "Energy", ATTR_ICON: "mdi:factory", - ATTR_UNIT_FN: lambda x: ENERGY_KILO_WATT_HOUR, + ATTR_UNIT: ENERGY_KILO_WATT_HOUR, ATTR_DEVICE_CLASS: None, ATTR_VALUE_FN: lambda x: x.device.total_energy_consumed, ATTR_ENABLED_FN: lambda x: x.device.has_energy_consumed_meter, @@ -43,7 +43,7 @@ "outside_temperature": { ATTR_MEASUREMENT_NAME: "Outside Temperature", ATTR_ICON: "mdi:thermometer", - ATTR_UNIT_FN: lambda x: TEMP_UNIT_LOOKUP.get(x.device.temp_unit, TEMP_CELSIUS), + ATTR_UNIT: TEMP_CELSIUS, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_VALUE_FN: lambda x: x.device.outside_temperature, ATTR_ENABLED_FN: lambda x: True, @@ -51,7 +51,7 @@ "tank_temperature": { ATTR_MEASUREMENT_NAME: "Tank Temperature", ATTR_ICON: "mdi:thermometer", - ATTR_UNIT_FN: lambda x: TEMP_UNIT_LOOKUP.get(x.device.temp_unit, TEMP_CELSIUS), + ATTR_UNIT: TEMP_CELSIUS, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_VALUE_FN: lambda x: x.device.tank_temperature, ATTR_ENABLED_FN: lambda x: True, @@ -61,7 +61,7 @@ "room_temperature": { ATTR_MEASUREMENT_NAME: "Room Temperature", ATTR_ICON: "mdi:thermometer", - ATTR_UNIT_FN: lambda x: TEMP_UNIT_LOOKUP.get(x.device.temp_unit, TEMP_CELSIUS), + ATTR_UNIT: TEMP_CELSIUS, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_VALUE_FN: lambda zone: zone.room_temperature, ATTR_ENABLED_FN: lambda x: True, @@ -147,7 +147,7 @@ def state(self): @property def unit_of_measurement(self): """Return the unit of measurement.""" - return self._def[ATTR_UNIT_FN](self._api) + return self._def[ATTR_UNIT] @property def device_class(self): diff --git a/homeassistant/components/melcloud/water_heater.py b/homeassistant/components/melcloud/water_heater.py index fa7aff2b640098..ce1b1ae15cc718 100644 --- a/homeassistant/components/melcloud/water_heater.py +++ b/homeassistant/components/melcloud/water_heater.py @@ -18,7 +18,7 @@ from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN, MelCloudDevice -from .const import ATTR_STATUS, TEMP_UNIT_LOOKUP +from .const import ATTR_STATUS async def async_setup_entry( @@ -80,7 +80,7 @@ def device_state_attributes(self): @property def temperature_unit(self) -> str: """Return the unit of measurement used by the platform.""" - return TEMP_UNIT_LOOKUP.get(self._device.temp_unit, TEMP_CELSIUS) + return TEMP_CELSIUS @property def current_operation(self) -> Optional[str]: From be58c4f71a6bac4d56b431647183748f3c4e1d05 Mon Sep 17 00:00:00 2001 From: Xiaonan Shen Date: Fri, 1 May 2020 04:34:29 -0700 Subject: [PATCH 39/58] Fix unknown exception being caught (#35005) --- homeassistant/components/roomba/config_flow.py | 2 -- homeassistant/components/roomba/strings.json | 1 - 2 files changed, 3 deletions(-) diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index 3668984a41f26b..e323150fba3220 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -86,8 +86,6 @@ async def async_step_user(self, user_input=None): info = await validate_input(self.hass, user_input) except CannotConnect: errors = {"base": "cannot_connect"} - except Exception: # pylint: disable=broad-except - errors = {"base": "unknown"} if "base" not in errors: await async_disconnect_or_timeout(self.hass, info[ROOMBA_SESSION]) diff --git a/homeassistant/components/roomba/strings.json b/homeassistant/components/roomba/strings.json index a679b2fdbb5091..c15c5f5893a2c2 100644 --- a/homeassistant/components/roomba/strings.json +++ b/homeassistant/components/roomba/strings.json @@ -15,7 +15,6 @@ } }, "error": { - "unknown": "Unexpected error", "cannot_connect": "Failed to connect, please try again" } }, From 1cfa46d80b778bdc3198dd5a74d8a0b30ebc1817 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 1 May 2020 15:56:38 +0200 Subject: [PATCH 40/58] Fix CI, incomplete change in melcloud (#35016) --- homeassistant/components/melcloud/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py index ed64647071b566..774b66ab67ed90 100644 --- a/homeassistant/components/melcloud/sensor.py +++ b/homeassistant/components/melcloud/sensor.py @@ -69,7 +69,7 @@ "flow_temperature": { ATTR_MEASUREMENT_NAME: "Flow Temperature", ATTR_ICON: "mdi:thermometer", - ATTR_UNIT_FN: lambda x: TEMP_UNIT_LOOKUP.get(x.device.temp_unit, TEMP_CELSIUS), + ATTR_UNIT: TEMP_CELSIUS, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_VALUE_FN: lambda zone: zone.flow_temperature, ATTR_ENABLED_FN: lambda x: True, @@ -77,7 +77,7 @@ "return_temperature": { ATTR_MEASUREMENT_NAME: "Flow Return Temperature", ATTR_ICON: "mdi:thermometer", - ATTR_UNIT_FN: lambda x: TEMP_UNIT_LOOKUP.get(x.device.temp_unit, TEMP_CELSIUS), + ATTR_UNIT: TEMP_CELSIUS, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_VALUE_FN: lambda zone: zone.return_temperature, ATTR_ENABLED_FN: lambda x: True, From f3d79104a7503a2d7582a1b68e1b58b6cc1da467 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 1 May 2020 16:29:14 +0200 Subject: [PATCH 41/58] Rename WaterHeaterDevice to WaterHeaterEntity (#34675) * Rename WaterHeaterDevice to WaterHeaterEntity * Fix stale name Co-authored-by: Martin Hjelmare --- homeassistant/components/demo/water_heater.py | 4 ++-- homeassistant/components/econet/water_heater.py | 4 ++-- homeassistant/components/evohome/water_heater.py | 4 ++-- homeassistant/components/geniushub/water_heater.py | 4 ++-- homeassistant/components/hive/water_heater.py | 4 ++-- homeassistant/components/incomfort/water_heater.py | 4 ++-- homeassistant/components/melcloud/water_heater.py | 4 ++-- homeassistant/components/tado/water_heater.py | 4 ++-- homeassistant/components/vicare/water_heater.py | 4 ++-- homeassistant/components/water_heater/__init__.py | 14 +++++++++++++- homeassistant/components/wink/water_heater.py | 4 ++-- tests/components/water_heater/test_init.py | 12 ++++++++++++ 12 files changed, 45 insertions(+), 21 deletions(-) create mode 100644 tests/components/water_heater/test_init.py diff --git a/homeassistant/components/demo/water_heater.py b/homeassistant/components/demo/water_heater.py index f9aca141245656..0b96bbf75f8578 100644 --- a/homeassistant/components/demo/water_heater.py +++ b/homeassistant/components/demo/water_heater.py @@ -3,7 +3,7 @@ SUPPORT_AWAY_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - WaterHeaterDevice, + WaterHeaterEntity, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT @@ -27,7 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): await async_setup_platform(hass, {}, async_add_entities) -class DemoWaterHeater(WaterHeaterDevice): +class DemoWaterHeater(WaterHeaterEntity): """Representation of a demo water_heater device.""" def __init__( diff --git a/homeassistant/components/econet/water_heater.py b/homeassistant/components/econet/water_heater.py index 59afe1351f55af..0c31e3e50e0ba6 100644 --- a/homeassistant/components/econet/water_heater.py +++ b/homeassistant/components/econet/water_heater.py @@ -16,7 +16,7 @@ STATE_PERFORMANCE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - WaterHeaterDevice, + WaterHeaterEntity, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -120,7 +120,7 @@ def service_handle(service): ) -class EcoNetWaterHeater(WaterHeaterDevice): +class EcoNetWaterHeater(WaterHeaterEntity): """Representation of an EcoNet water heater.""" def __init__(self, water_heater): diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index 20aa0710d0de2f..846c8c09155320 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -5,7 +5,7 @@ from homeassistant.components.water_heater import ( SUPPORT_AWAY_MODE, SUPPORT_OPERATION_MODE, - WaterHeaterDevice, + WaterHeaterEntity, ) from homeassistant.const import PRECISION_TENTHS, PRECISION_WHOLE, STATE_OFF, STATE_ON from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -43,7 +43,7 @@ async def async_setup_platform( async_add_entities([new_entity], update_before_add=True) -class EvoDHW(EvoChild, WaterHeaterDevice): +class EvoDHW(EvoChild, WaterHeaterEntity): """Base for a Honeywell TCC DHW controller (aka boiler).""" def __init__(self, evo_broker, evo_device) -> None: diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index e7e3278eaf6c19..51fdce4a6d7c59 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -4,7 +4,7 @@ from homeassistant.components.water_heater import ( SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - WaterHeaterDevice, + WaterHeaterEntity, ) from homeassistant.const import STATE_OFF from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -49,7 +49,7 @@ async def async_setup_platform( ) -class GeniusWaterHeater(GeniusHeatingZone, WaterHeaterDevice): +class GeniusWaterHeater(GeniusHeatingZone, WaterHeaterEntity): """Representation of a Genius Hub water_heater device.""" def __init__(self, broker, zone) -> None: diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py index d7d98426df5eae..693fd6f322b8e8 100644 --- a/homeassistant/components/hive/water_heater.py +++ b/homeassistant/components/hive/water_heater.py @@ -4,7 +4,7 @@ STATE_OFF, STATE_ON, SUPPORT_OPERATION_MODE, - WaterHeaterDevice, + WaterHeaterEntity, ) from homeassistant.const import TEMP_CELSIUS @@ -29,7 +29,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devs) -class HiveWaterHeater(HiveEntity, WaterHeaterDevice): +class HiveWaterHeater(HiveEntity, WaterHeaterEntity): """Hive Water Heater Device.""" @property diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py index 88370acf1665f5..da6e6d893150a7 100644 --- a/homeassistant/components/incomfort/water_heater.py +++ b/homeassistant/components/incomfort/water_heater.py @@ -7,7 +7,7 @@ from homeassistant.components.water_heater import ( DOMAIN as WATER_HEATER_DOMAIN, - WaterHeaterDevice, + WaterHeaterEntity, ) from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -30,7 +30,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([IncomfortWaterHeater(client, h) for h in heaters]) -class IncomfortWaterHeater(IncomfortEntity, WaterHeaterDevice): +class IncomfortWaterHeater(IncomfortEntity, WaterHeaterEntity): """Representation of an InComfort/Intouch water_heater device.""" def __init__(self, client, heater) -> None: diff --git a/homeassistant/components/melcloud/water_heater.py b/homeassistant/components/melcloud/water_heater.py index ce1b1ae15cc718..ae10b5140f76a1 100644 --- a/homeassistant/components/melcloud/water_heater.py +++ b/homeassistant/components/melcloud/water_heater.py @@ -11,7 +11,7 @@ from homeassistant.components.water_heater import ( SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - WaterHeaterDevice, + WaterHeaterEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS @@ -35,7 +35,7 @@ async def async_setup_entry( ) -class AtwWaterHeater(WaterHeaterDevice): +class AtwWaterHeater(WaterHeaterEntity): """Air-to-Water water heater.""" def __init__(self, api: MelCloudDevice, device: AtwDevice) -> None: diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py index aeb2d1ee10667b..1c0d37c90df003 100644 --- a/homeassistant/components/tado/water_heater.py +++ b/homeassistant/components/tado/water_heater.py @@ -4,7 +4,7 @@ from homeassistant.components.water_heater import ( SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - WaterHeaterDevice, + WaterHeaterEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS @@ -98,7 +98,7 @@ def create_water_heater_entity(tado, name: str, zone_id: int, zone: str): return entity -class TadoWaterHeater(TadoZoneEntity, WaterHeaterDevice): +class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): """Representation of a Tado water heater.""" def __init__( diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index eea3d81faf6a07..c6aa5205f2478a 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -5,7 +5,7 @@ from homeassistant.components.water_heater import ( SUPPORT_TARGET_TEMPERATURE, - WaterHeaterDevice, + WaterHeaterEntity, ) from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS @@ -60,7 +60,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) -class ViCareWater(WaterHeaterDevice): +class ViCareWater(WaterHeaterEntity): """Representation of the ViCare domestic hot water device.""" def __init__(self, name, api, heating_type): diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index 4de0a58a881eaa..0763c552075118 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -128,7 +128,7 @@ async def async_unload_entry(hass, entry): return await hass.data[DOMAIN].async_unload_entry(entry) -class WaterHeaterDevice(Entity): +class WaterHeaterEntity(Entity): """Representation of a water_heater device.""" @property @@ -319,3 +319,15 @@ async def async_service_temperature_set(entity, service): kwargs[value] = temp await entity.async_set_temperature(**kwargs) + + +class WaterHeaterDevice(WaterHeaterEntity): + """Representation of a water heater (for backwards compatibility).""" + + def __init_subclass__(cls, **kwargs): + """Print deprecation warning.""" + super().__init_subclass__(**kwargs) + _LOGGER.warning( + "WaterHeaterDevice is deprecated, modify %s to extend WaterHeaterEntity", + cls.__name__, + ) diff --git a/homeassistant/components/wink/water_heater.py b/homeassistant/components/wink/water_heater.py index dae6acf91bf972..0ce31762c7a656 100644 --- a/homeassistant/components/wink/water_heater.py +++ b/homeassistant/components/wink/water_heater.py @@ -14,7 +14,7 @@ SUPPORT_AWAY_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - WaterHeaterDevice, + WaterHeaterEntity, ) from homeassistant.const import STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS @@ -51,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([WinkWaterHeater(water_heater, hass)]) -class WinkWaterHeater(WinkDevice, WaterHeaterDevice): +class WinkWaterHeater(WinkDevice, WaterHeaterEntity): """Representation of a Wink water heater.""" @property diff --git a/tests/components/water_heater/test_init.py b/tests/components/water_heater/test_init.py new file mode 100644 index 00000000000000..967e8b03620d98 --- /dev/null +++ b/tests/components/water_heater/test_init.py @@ -0,0 +1,12 @@ +"""Tests for Water heater.""" +from homeassistant.components import water_heater + + +def test_deprecated_base_class(caplog): + """Test deprecated base class.""" + + class CustomWaterHeater(water_heater.WaterHeaterDevice): + pass + + CustomWaterHeater() + assert "WaterHeaterDevice is deprecated, modify CustomWaterHeater" in caplog.text From 8c6506227150c74083b9827975ec19af20c68ff9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 1 May 2020 16:37:25 +0200 Subject: [PATCH 42/58] Several optimizations to automations (#35007) --- .../components/automation/__init__.py | 44 +++--- homeassistant/components/automation/config.py | 34 +++-- .../components/automation/litejet.py | 10 +- homeassistant/components/automation/zone.py | 5 +- tests/components/automation/test_event.py | 14 +- .../automation/test_geo_location.py | 22 +-- tests/components/automation/test_init.py | 24 ++-- tests/components/automation/test_mqtt.py | 6 +- .../automation/test_numeric_state.py | 128 ++++++++--------- tests/components/automation/test_state.py | 98 ++++++------- tests/components/automation/test_sun.py | 130 +++++++++--------- tests/components/automation/test_template.py | 84 +++++------ tests/components/automation/test_time.py | 24 ++-- .../automation/test_time_pattern.py | 22 +-- tests/components/automation/test_zone.py | 12 +- 15 files changed, 331 insertions(+), 326 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 3a5b3966d40a08..e5b66594d2f71e 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -1,4 +1,5 @@ """Allow to set up simple automation rules via the config file.""" +import asyncio import importlib import logging from typing import Any, Awaitable, Callable, List, Optional, Set @@ -127,13 +128,11 @@ def automations_with_entity(hass: HomeAssistant, entity_id: str) -> List[str]: component = hass.data[DOMAIN] - results = [] - - for automation_entity in component.entities: - if entity_id in automation_entity.referenced_entities: - results.append(automation_entity.entity_id) - - return results + return [ + automation_entity.entity_id + for automation_entity in component.entities + if entity_id in automation_entity.referenced_entities + ] @callback @@ -160,13 +159,11 @@ def automations_with_device(hass: HomeAssistant, device_id: str) -> List[str]: component = hass.data[DOMAIN] - results = [] - - for automation_entity in component.entities: - if device_id in automation_entity.referenced_devices: - results.append(automation_entity.entity_id) - - return results + return [ + automation_entity.entity_id + for automation_entity in component.entities + if device_id in automation_entity.referenced_devices + ] @callback @@ -443,26 +440,29 @@ async def _async_attach_triggers( self, home_assistant_start: bool ) -> Optional[Callable[[], None]]: """Set up the triggers.""" - removes = [] info = {"name": self._name, "home_assistant_start": home_assistant_start} + triggers = [] for conf in self._trigger_config: platform = importlib.import_module(f".{conf[CONF_PLATFORM]}", __name__) - remove = await platform.async_attach_trigger( # type: ignore - self.hass, conf, self.async_trigger, info + triggers.append( + platform.async_attach_trigger( # type: ignore + self.hass, conf, self.async_trigger, info + ) ) - if not remove: - _LOGGER.error("Error setting up trigger %s", self._name) - continue + results = await asyncio.gather(*triggers) - _LOGGER.info("Initialized trigger %s", self._name) - removes.append(remove) + if None in results: + _LOGGER.error("Error setting up trigger %s", self._name) + removes = [remove for remove in results if remove is not None] if not removes: return None + _LOGGER.info("Initialized trigger %s", self._name) + @callback def remove_triggers(): """Remove attached triggers.""" diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index d29a561f378724..c2cd00fd68396f 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -36,17 +36,19 @@ async def async_validate_config_item(hass, config, full_config=None): config[CONF_TRIGGER] = triggers if CONF_CONDITION in config: - conditions = [] - for cond in config[CONF_CONDITION]: - cond = await condition.async_validate_condition_config(hass, cond) - conditions.append(cond) - config[CONF_CONDITION] = conditions - - actions = [] - for action in config[CONF_ACTION]: - action = await script.async_validate_action_config(hass, action) - actions.append(action) - config[CONF_ACTION] = actions + config[CONF_CONDITION] = await asyncio.gather( + *[ + condition.async_validate_condition_config(hass, cond) + for cond in config[CONF_CONDITION] + ] + ) + + config[CONF_ACTION] = await asyncio.gather( + *[ + script.async_validate_action_config(hass, action) + for action in config[CONF_ACTION] + ] + ) return config @@ -69,16 +71,18 @@ async def _try_async_validate_config_item(hass, config, full_config=None): async def async_validate_config(hass, config): """Validate config.""" - automations = [] validated_automations = await asyncio.gather( *( _try_async_validate_config_item(hass, p_config, config) for _, p_config in config_per_platform(config, DOMAIN) ) ) - for validated_automation in validated_automations: - if validated_automation is not None: - automations.append(validated_automation) + + automations = [ + validated_automation + for validated_automation in validated_automations + if validated_automation is not None + ] # Create a copy of the configuration with all config for current # component removed and add validated config back in. diff --git a/homeassistant/components/automation/litejet.py b/homeassistant/components/automation/litejet.py index 12ffa29b962268..5924cf3b809662 100644 --- a/homeassistant/components/automation/litejet.py +++ b/homeassistant/components/automation/litejet.py @@ -85,9 +85,13 @@ def released(): cancel_pressed_more_than() cancel_pressed_more_than = None held_time = dt_util.utcnow() - pressed_time - if held_less_than is not None and held_time < held_less_than: - if held_more_than is None or held_time > held_more_than: - hass.add_job(call_action) + + if ( + held_less_than is not None + and held_time < held_less_than + and (held_more_than is None or held_time > held_more_than) + ): + hass.add_job(call_action) hass.data["litejet_system"].on_switch_pressed(number, pressed) hass.data["litejet_system"].on_switch_released(number, released) diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index 14233d783f95dc..cae2a76dd03ae2 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -47,10 +47,7 @@ def zone_automation_listener(entity, from_s, to_s): return zone_state = hass.states.get(zone_entity_id) - if from_s: - from_match = condition.zone(hass, zone_state, from_s) - else: - from_match = False + from_match = condition.zone(hass, zone_state, from_s) if from_s else False to_match = condition.zone(hass, zone_state, to_s) if ( diff --git a/tests/components/automation/test_event.py b/tests/components/automation/test_event.py index 340bb6c1e95dd2..cc9aecfdac41d9 100644 --- a/tests/components/automation/test_event.py +++ b/tests/components/automation/test_event.py @@ -46,7 +46,7 @@ async def test_if_fires_on_event(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_event_extra_data(hass, calls): @@ -64,14 +64,14 @@ async def test_if_fires_on_event_extra_data(hass, calls): hass.bus.async_fire("test_event", {"extra_key": "extra_data"}) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 await common.async_turn_off(hass) await hass.async_block_till_done() hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_event_with_data(hass, calls): @@ -93,7 +93,7 @@ async def test_if_fires_on_event_with_data(hass, calls): hass.bus.async_fire("test_event", {"some_attr": "some_value", "another": "value"}) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_event_with_empty_data_config(hass, calls): @@ -119,7 +119,7 @@ async def test_if_fires_on_event_with_empty_data_config(hass, calls): hass.bus.async_fire("test_event", {"some_attr": "some_value", "another": "value"}) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_event_with_nested_data(hass, calls): @@ -143,7 +143,7 @@ async def test_if_fires_on_event_with_nested_data(hass, calls): "test_event", {"parent_attr": {"some_attr": "some_value", "another": "value"}} ) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_fires_if_event_data_not_matches(hass, calls): @@ -165,4 +165,4 @@ async def test_if_not_fires_if_event_data_not_matches(hass, calls): hass.bus.async_fire("test_event", {"some_attr": "some_other_value"}) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 diff --git a/tests/components/automation/test_geo_location.py b/tests/components/automation/test_geo_location.py index 5daca51d0a1fdb..99ace50e77df3e 100644 --- a/tests/components/automation/test_geo_location.py +++ b/tests/components/automation/test_geo_location.py @@ -83,11 +83,11 @@ async def test_if_fires_on_zone_enter(hass, calls): ) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert calls[0].context.parent_id == context.id assert ( - "geo_location - geo_location.entity - hello - hello - test" - == calls[0].data["some"] + calls[0].data["some"] + == "geo_location - geo_location.entity - hello - hello - test" ) # Set out of zone again so we can trigger call @@ -108,7 +108,7 @@ async def test_if_fires_on_zone_enter(hass, calls): ) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_fires_for_enter_on_zone_leave(hass, calls): @@ -143,7 +143,7 @@ async def test_if_not_fires_for_enter_on_zone_leave(hass, calls): ) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_fires_on_zone_leave(hass, calls): @@ -178,7 +178,7 @@ async def test_if_fires_on_zone_leave(hass, calls): ) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_fires_for_leave_on_zone_enter(hass, calls): @@ -213,7 +213,7 @@ async def test_if_not_fires_for_leave_on_zone_enter(hass, calls): ) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_fires_on_zone_appear(hass, calls): @@ -258,10 +258,10 @@ async def test_if_fires_on_zone_appear(hass, calls): ) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert calls[0].context.parent_id == context.id assert ( - "geo_location - geo_location.entity - - hello - test" == calls[0].data["some"] + calls[0].data["some"] == "geo_location - geo_location.entity - - hello - test" ) @@ -308,7 +308,7 @@ async def test_if_fires_on_zone_disappear(hass, calls): hass.states.async_remove("geo_location.entity") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert ( - "geo_location - geo_location.entity - hello - - test" == calls[0].data["some"] + calls[0].data["some"] == "geo_location - geo_location.entity - hello - - test" ) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index cd4a01e9a2816a..a039604525f967 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -148,7 +148,7 @@ async def test_service_specify_entity_id(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert ["hello.world"] == calls[0].data.get(ATTR_ENTITY_ID) @@ -170,7 +170,7 @@ async def test_service_specify_entity_id_list(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert ["hello.world", "hello.world2"] == calls[0].data.get(ATTR_ENTITY_ID) @@ -192,10 +192,10 @@ async def test_two_triggers(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 hass.states.async_set("test.entity", "hello") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 async def test_trigger_service_ignoring_condition(hass, calls): @@ -268,17 +268,17 @@ async def test_two_conditions_with_and(hass, calls): hass.states.async_set(entity_id, 100) hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 hass.states.async_set(entity_id, 101) hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 hass.states.async_set(entity_id, 151) hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_automation_list_setting(hass, calls): @@ -302,11 +302,11 @@ async def test_automation_list_setting(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 hass.bus.async_fire("test_event_2") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 async def test_automation_calling_two_actions(hass, calls): @@ -368,7 +368,7 @@ async def test_shared_context(hass, calls): assert event_mock.call_count == 2 # Verify automation triggered evenet for 'hello' automation - args, kwargs = event_mock.call_args_list[0] + args, _ = event_mock.call_args_list[0] first_trigger_context = args[0].context assert first_trigger_context.parent_id == context.id # Ensure event data has all attributes set @@ -376,7 +376,7 @@ async def test_shared_context(hass, calls): assert args[0].data.get(ATTR_ENTITY_ID) is not None # Ensure context set correctly for event fired by 'hello' automation - args, kwargs = first_automation_listener.call_args + args, _ = first_automation_listener.call_args assert args[0].context is first_trigger_context # Ensure the 'hello' automation state has the right context @@ -385,7 +385,7 @@ async def test_shared_context(hass, calls): assert state.context is first_trigger_context # Verify automation triggered evenet for 'bye' automation - args, kwargs = event_mock.call_args_list[1] + args, _ = event_mock.call_args_list[1] second_trigger_context = args[0].context assert second_trigger_context.parent_id == first_trigger_context.id # Ensure event data has all attributes set diff --git a/tests/components/automation/test_mqtt.py b/tests/components/automation/test_mqtt.py index b8c369f5e63c73..0a07c5aac487e5 100644 --- a/tests/components/automation/test_mqtt.py +++ b/tests/components/automation/test_mqtt.py @@ -57,7 +57,7 @@ async def test_if_fires_on_topic_match(hass, calls): await hass.async_block_till_done() async_fire_mqtt_message(hass, "test-topic", "test_payload") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_topic_and_payload_match(hass, calls): @@ -79,7 +79,7 @@ async def test_if_fires_on_topic_and_payload_match(hass, calls): async_fire_mqtt_message(hass, "test-topic", "hello") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_fires_on_topic_but_no_payload_match(hass, calls): @@ -101,7 +101,7 @@ async def test_if_not_fires_on_topic_but_no_payload_match(hass, calls): async_fire_mqtt_message(hass, "test-topic", "no-hello") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_encoding_default(hass, calls): diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py index f779f022e65d47..1173de4b02ae88 100644 --- a/tests/components/automation/test_numeric_state.py +++ b/tests/components/automation/test_numeric_state.py @@ -52,7 +52,7 @@ async def test_if_fires_on_entity_change_below(hass, calls): # 9 is below 10 hass.states.async_set("test.entity", 9, context=context) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert calls[0].context.parent_id == context.id # Set above 12 so the automation will fire again @@ -61,7 +61,7 @@ async def test_if_fires_on_entity_change_below(hass, calls): await hass.async_block_till_done() hass.states.async_set("test.entity", 9) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_entity_change_over_to_below(hass, calls): @@ -87,7 +87,7 @@ async def test_if_fires_on_entity_change_over_to_below(hass, calls): # 9 is below 10 hass.states.async_set("test.entity", 9) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_entities_change_over_to_below(hass, calls): @@ -114,10 +114,10 @@ async def test_if_fires_on_entities_change_over_to_below(hass, calls): # 9 is below 10 hass.states.async_set("test.entity_1", 9) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 hass.states.async_set("test.entity_2", 9) await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 async def test_if_not_fires_on_entity_change_below_to_below(hass, calls): @@ -144,18 +144,18 @@ async def test_if_not_fires_on_entity_change_below_to_below(hass, calls): # 9 is below 10 so this should fire hass.states.async_set("test.entity", 9, context=context) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert calls[0].context.parent_id == context.id # already below so should not fire again hass.states.async_set("test.entity", 5) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # still below so should not fire again hass.states.async_set("test.entity", 3) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_below_fires_on_entity_change_to_equal(hass, calls): @@ -181,7 +181,7 @@ async def test_if_not_below_fires_on_entity_change_to_equal(hass, calls): # 10 is not below 10 so this should not fire again hass.states.async_set("test.entity", 10) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_fires_on_initial_entity_below(hass, calls): @@ -207,7 +207,7 @@ async def test_if_fires_on_initial_entity_below(hass, calls): # Fire on first update even if initial state was already below hass.states.async_set("test.entity", 8) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_initial_entity_above(hass, calls): @@ -233,7 +233,7 @@ async def test_if_fires_on_initial_entity_above(hass, calls): # Fire on first update even if initial state was already above hass.states.async_set("test.entity", 12) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_entity_change_above(hass, calls): @@ -255,7 +255,7 @@ async def test_if_fires_on_entity_change_above(hass, calls): # 11 is above 10 hass.states.async_set("test.entity", 11) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_entity_change_below_to_above(hass, calls): @@ -282,7 +282,7 @@ async def test_if_fires_on_entity_change_below_to_above(hass, calls): # 11 is above 10 and 9 is below hass.states.async_set("test.entity", 11) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_fires_on_entity_change_above_to_above(hass, calls): @@ -309,12 +309,12 @@ async def test_if_not_fires_on_entity_change_above_to_above(hass, calls): # 12 is above 10 so this should fire hass.states.async_set("test.entity", 12) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # already above, should not fire again hass.states.async_set("test.entity", 15) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_above_fires_on_entity_change_to_equal(hass, calls): @@ -341,7 +341,7 @@ async def test_if_not_above_fires_on_entity_change_to_equal(hass, calls): # 10 is not above 10 so this should not fire again hass.states.async_set("test.entity", 10) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_fires_on_entity_change_below_range(hass, calls): @@ -364,7 +364,7 @@ async def test_if_fires_on_entity_change_below_range(hass, calls): # 9 is below 10 hass.states.async_set("test.entity", 9) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_entity_change_below_above_range(hass, calls): @@ -387,7 +387,7 @@ async def test_if_fires_on_entity_change_below_above_range(hass, calls): # 4 is below 5 hass.states.async_set("test.entity", 4) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_fires_on_entity_change_over_to_below_range(hass, calls): @@ -414,7 +414,7 @@ async def test_if_fires_on_entity_change_over_to_below_range(hass, calls): # 9 is below 10 hass.states.async_set("test.entity", 9) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_entity_change_over_to_below_above_range(hass, calls): @@ -441,7 +441,7 @@ async def test_if_fires_on_entity_change_over_to_below_above_range(hass, calls): # 4 is below 5 so it should not fire hass.states.async_set("test.entity", 4) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_not_fires_if_entity_not_match(hass, calls): @@ -463,7 +463,7 @@ async def test_if_not_fires_if_entity_not_match(hass, calls): hass.states.async_set("test.entity", 11) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_fires_on_entity_change_below_with_attribute(hass, calls): @@ -485,7 +485,7 @@ async def test_if_fires_on_entity_change_below_with_attribute(hass, calls): # 9 is below 10 hass.states.async_set("test.entity", 9, {"test_attribute": 11}) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_fires_on_entity_change_not_below_with_attribute(hass, calls): @@ -507,7 +507,7 @@ async def test_if_not_fires_on_entity_change_not_below_with_attribute(hass, call # 11 is not below 10 hass.states.async_set("test.entity", 11, {"test_attribute": 9}) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_fires_on_attribute_change_with_attribute_below(hass, calls): @@ -530,7 +530,7 @@ async def test_if_fires_on_attribute_change_with_attribute_below(hass, calls): # 9 is below 10 hass.states.async_set("test.entity", "entity", {"test_attribute": 9}) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_fires_on_attribute_change_with_attribute_not_below(hass, calls): @@ -553,7 +553,7 @@ async def test_if_not_fires_on_attribute_change_with_attribute_not_below(hass, c # 11 is not below 10 hass.states.async_set("test.entity", "entity", {"test_attribute": 11}) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_not_fires_on_entity_change_with_attribute_below(hass, calls): @@ -576,7 +576,7 @@ async def test_if_not_fires_on_entity_change_with_attribute_below(hass, calls): # 11 is not below 10, entity state value should not be tested hass.states.async_set("test.entity", "9", {"test_attribute": 11}) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_not_fires_on_entity_change_with_not_attribute_below(hass, calls): @@ -599,7 +599,7 @@ async def test_if_not_fires_on_entity_change_with_not_attribute_below(hass, call # 11 is not below 10, entity state value should not be tested hass.states.async_set("test.entity", "entity") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_fires_on_attr_change_with_attribute_below_and_multiple_attr(hass, calls): @@ -624,7 +624,7 @@ async def test_fires_on_attr_change_with_attribute_below_and_multiple_attr(hass, "test.entity", "entity", {"test_attribute": 9, "not_test_attribute": 11} ) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_template_list(hass, calls): @@ -647,7 +647,7 @@ async def test_template_list(hass, calls): # 3 is below 10 hass.states.async_set("test.entity", "entity", {"test_attribute": [11, 15, 3]}) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_template_string(hass, calls): @@ -686,10 +686,10 @@ async def test_template_string(hass, calls): await hass.async_block_till_done() hass.states.async_set("test.entity", "test state 2", {"test_attribute": "0.9"}) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert ( - "numeric_state - test.entity - 10.0 - None - test state 1 - " - "test state 2" == calls[0].data["some"] + calls[0].data["some"] + == "numeric_state - test.entity - 10.0 - None - test state 1 - test state 2" ) @@ -715,7 +715,7 @@ async def test_not_fires_on_attr_change_with_attr_not_below_multiple_attr(hass, "test.entity", "entity", {"test_attribute": 11, "not_test_attribute": 9} ) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_action(hass, calls): @@ -742,19 +742,19 @@ async def test_if_action(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 hass.states.async_set(entity_id, 8) hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 hass.states.async_set(entity_id, 9) hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 async def test_if_fails_setup_bad_for(hass, calls): @@ -826,7 +826,7 @@ async def test_if_not_fires_on_entity_change_with_for(hass, calls): await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_not_fires_on_entities_change_with_for_after_stop(hass, calls): @@ -853,7 +853,7 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop(hass, calls): await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 hass.states.async_set("test.entity_1", 15) hass.states.async_set("test.entity_2", 15) @@ -866,7 +866,7 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop(hass, calls): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 async def test_if_fires_on_entity_change_with_for_attribute_change(hass, calls): @@ -897,11 +897,11 @@ async def test_if_fires_on_entity_change_with_for_attribute_change(hass, calls): async_fire_time_changed(hass, mock_utcnow.return_value) hass.states.async_set("test.entity", 9, attributes={"mock_attr": "attr_change"}) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 mock_utcnow.return_value += timedelta(seconds=4) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_entity_change_with_for(hass, calls): @@ -927,7 +927,7 @@ async def test_if_fires_on_entity_change_with_for(hass, calls): await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_wait_template_with_trigger(hass, calls): @@ -965,7 +965,7 @@ async def test_wait_template_with_trigger(hass, calls): hass.states.async_set("test.entity", "8") await hass.async_block_till_done() await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert "numeric_state - test.entity - 12" == calls[0].data["some"] @@ -1000,16 +1000,16 @@ async def test_if_fires_on_entities_change_no_overlap(hass, calls): mock_utcnow.return_value += timedelta(seconds=10) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 1 == len(calls) - assert "test.entity_1" == calls[0].data["some"] + assert len(calls) == 1 + assert calls[0].data["some"] == "test.entity_1" hass.states.async_set("test.entity_2", 9) await hass.async_block_till_done() mock_utcnow.return_value += timedelta(seconds=10) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 2 == len(calls) - assert "test.entity_2" == calls[1].data["some"] + assert len(calls) == 2 + assert calls[1].data["some"] == "test.entity_2" async def test_if_fires_on_entities_change_overlap(hass, calls): @@ -1052,18 +1052,18 @@ async def test_if_fires_on_entities_change_overlap(hass, calls): async_fire_time_changed(hass, mock_utcnow.return_value) hass.states.async_set("test.entity_2", 9) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 mock_utcnow.return_value += timedelta(seconds=3) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 1 == len(calls) - assert "test.entity_1" == calls[0].data["some"] + assert len(calls) == 1 + assert calls[0].data["some"] == "test.entity_1" mock_utcnow.return_value += timedelta(seconds=3) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 2 == len(calls) - assert "test.entity_2" == calls[1].data["some"] + assert len(calls) == 2 + assert calls[1].data["some"] == "test.entity_2" async def test_if_fires_on_change_with_for_template_1(hass, calls): @@ -1087,10 +1087,10 @@ async def test_if_fires_on_change_with_for_template_1(hass, calls): hass.states.async_set("test.entity", 9) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_change_with_for_template_2(hass, calls): @@ -1114,10 +1114,10 @@ async def test_if_fires_on_change_with_for_template_2(hass, calls): hass.states.async_set("test.entity", 9) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_change_with_for_template_3(hass, calls): @@ -1141,10 +1141,10 @@ async def test_if_fires_on_change_with_for_template_3(hass, calls): hass.states.async_set("test.entity", 9) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_invalid_for_template(hass, calls): @@ -1215,22 +1215,22 @@ async def test_if_fires_on_entities_change_overlap_for_template(hass, calls): async_fire_time_changed(hass, mock_utcnow.return_value) hass.states.async_set("test.entity_2", 9) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 mock_utcnow.return_value += timedelta(seconds=3) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 1 == len(calls) - assert "test.entity_1 - 0:00:05" == calls[0].data["some"] + assert len(calls) == 1 + assert calls[0].data["some"] == "test.entity_1 - 0:00:05" mock_utcnow.return_value += timedelta(seconds=3) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 mock_utcnow.return_value += timedelta(seconds=5) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 2 == len(calls) - assert "test.entity_2 - 0:00:10" == calls[1].data["some"] + assert len(calls) == 2 + assert calls[1].data["some"] == "test.entity_2 - 0:00:10" def test_below_above(): diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index 949851f5470b59..033ce44e5a43c8 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -65,15 +65,15 @@ async def test_if_fires_on_entity_change(hass, calls): hass.states.async_set("test.entity", "world", context=context) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert calls[0].context.parent_id == context.id - assert "state - test.entity - hello - world - None" == calls[0].data["some"] + assert calls[0].data["some"] == "state - test.entity - hello - world - None" await common.async_turn_off(hass) await hass.async_block_till_done() hass.states.async_set("test.entity", "planet") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_entity_change_with_from_filter(hass, calls): @@ -96,7 +96,7 @@ async def test_if_fires_on_entity_change_with_from_filter(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_entity_change_with_to_filter(hass, calls): @@ -119,7 +119,7 @@ async def test_if_fires_on_entity_change_with_to_filter(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_attribute_change_with_to_filter(hass, calls): @@ -143,7 +143,7 @@ async def test_if_fires_on_attribute_change_with_to_filter(hass, calls): hass.states.async_set("test.entity", "world", {"test_attribute": 11}) hass.states.async_set("test.entity", "world", {"test_attribute": 12}) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_entity_change_with_both_filters(hass, calls): @@ -167,7 +167,7 @@ async def test_if_fires_on_entity_change_with_both_filters(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_fires_if_to_filter_not_match(hass, calls): @@ -191,7 +191,7 @@ async def test_if_not_fires_if_to_filter_not_match(hass, calls): hass.states.async_set("test.entity", "moon") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_not_fires_if_from_filter_not_match(hass, calls): @@ -217,7 +217,7 @@ async def test_if_not_fires_if_from_filter_not_match(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_not_fires_if_entity_not_match(hass, calls): @@ -236,7 +236,7 @@ async def test_if_not_fires_if_entity_not_match(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_action(hass, calls): @@ -262,13 +262,13 @@ async def test_if_action(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 hass.states.async_set(entity_id, test_state + "something") hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fails_setup_if_to_boolean_value(hass, calls): @@ -377,7 +377,7 @@ async def test_if_not_fires_on_entity_change_with_for(hass, calls): await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_not_fires_on_entities_change_with_for_after_stop(hass, calls): @@ -404,7 +404,7 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop(hass, calls): await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 hass.states.async_set("test.entity_1", "world_no") hass.states.async_set("test.entity_2", "world_no") @@ -417,7 +417,7 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop(hass, calls): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 async def test_if_fires_on_entity_change_with_for_attribute_change(hass, calls): @@ -450,11 +450,11 @@ async def test_if_fires_on_entity_change_with_for_attribute_change(hass, calls): "test.entity", "world", attributes={"mock_attr": "attr_change"} ) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 mock_utcnow.return_value += timedelta(seconds=4) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_entity_change_with_for_multiple_force_update(hass, calls): @@ -481,16 +481,16 @@ async def test_if_fires_on_entity_change_with_for_multiple_force_update(hass, ca mock_utcnow.return_value = utcnow hass.states.async_set("test.force_entity", "world", None, True) await hass.async_block_till_done() - for _ in range(0, 4): + for _ in range(4): mock_utcnow.return_value += timedelta(seconds=1) async_fire_time_changed(hass, mock_utcnow.return_value) hass.states.async_set("test.force_entity", "world", None, True) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 mock_utcnow.return_value += timedelta(seconds=4) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_entity_change_with_for(hass, calls): @@ -539,7 +539,7 @@ async def test_if_fires_on_entity_removal(hass, calls): assert hass.states.async_remove("test.entity", context=context) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert calls[0].context.parent_id == context.id @@ -571,13 +571,13 @@ async def test_if_fires_on_for_condition(hass, calls): # not enough time has passed hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # Time travel 10 secs into the future mock_utcnow.return_value = point2 hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_for_condition_attribute_change(hass, calls): @@ -609,7 +609,7 @@ async def test_if_fires_on_for_condition_attribute_change(hass, calls): # not enough time has passed hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # Still not enough time has passed, but an attribute is changed mock_utcnow.return_value = point2 @@ -618,13 +618,13 @@ async def test_if_fires_on_for_condition_attribute_change(hass, calls): ) hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # Enough time has now passed mock_utcnow.return_value = point3 hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fails_setup_for_without_time(hass, calls): @@ -707,8 +707,8 @@ async def test_wait_template_with_trigger(hass, calls): await hass.async_block_till_done() hass.states.async_set("test.entity", "hello") await hass.async_block_till_done() - assert 1 == len(calls) - assert "state - test.entity - hello - world" == calls[0].data["some"] + assert len(calls) == 1 + assert calls[0].data["some"] == "state - test.entity - hello - world" async def test_if_fires_on_entities_change_no_overlap(hass, calls): @@ -741,16 +741,16 @@ async def test_if_fires_on_entities_change_no_overlap(hass, calls): mock_utcnow.return_value += timedelta(seconds=10) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 1 == len(calls) - assert "test.entity_1" == calls[0].data["some"] + assert len(calls) == 1 + assert calls[0].data["some"] == "test.entity_1" hass.states.async_set("test.entity_2", "world") await hass.async_block_till_done() mock_utcnow.return_value += timedelta(seconds=10) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 2 == len(calls) - assert "test.entity_2" == calls[1].data["some"] + assert len(calls) == 2 + assert calls[1].data["some"] == "test.entity_2" async def test_if_fires_on_entities_change_overlap(hass, calls): @@ -792,18 +792,18 @@ async def test_if_fires_on_entities_change_overlap(hass, calls): async_fire_time_changed(hass, mock_utcnow.return_value) hass.states.async_set("test.entity_2", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 mock_utcnow.return_value += timedelta(seconds=3) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 1 == len(calls) - assert "test.entity_1" == calls[0].data["some"] + assert len(calls) == 1 + assert calls[0].data["some"] == "test.entity_1" mock_utcnow.return_value += timedelta(seconds=3) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 2 == len(calls) - assert "test.entity_2" == calls[1].data["some"] + assert len(calls) == 2 + assert calls[1].data["some"] == "test.entity_2" async def test_if_fires_on_change_with_for_template_1(hass, calls): @@ -826,10 +826,10 @@ async def test_if_fires_on_change_with_for_template_1(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_change_with_for_template_2(hass, calls): @@ -852,10 +852,10 @@ async def test_if_fires_on_change_with_for_template_2(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_change_with_for_template_3(hass, calls): @@ -878,10 +878,10 @@ async def test_if_fires_on_change_with_for_template_3(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_invalid_for_template_1(hass, calls): @@ -950,19 +950,19 @@ async def test_if_fires_on_entities_change_overlap_for_template(hass, calls): async_fire_time_changed(hass, mock_utcnow.return_value) hass.states.async_set("test.entity_2", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 mock_utcnow.return_value += timedelta(seconds=3) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 1 == len(calls) - assert "test.entity_1 - 0:00:05" == calls[0].data["some"] + assert len(calls) == 1 + assert calls[0].data["some"] == "test.entity_1 - 0:00:05" mock_utcnow.return_value += timedelta(seconds=3) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 mock_utcnow.return_value += timedelta(seconds=5) async_fire_time_changed(hass, mock_utcnow.return_value) await hass.async_block_till_done() - assert 2 == len(calls) - assert "test.entity_2 - 0:00:10" == calls[1].data["some"] + assert len(calls) == 2 + assert calls[1].data["some"] == "test.entity_2 - 0:00:10" diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index 3468c9e9480ca6..4cb2672ab64de2 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -59,7 +59,7 @@ async def test_sunset_trigger(hass, calls): async_fire_time_changed(hass, trigger_time) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 with patch("homeassistant.util.dt.utcnow", return_value=now): await common.async_turn_on(hass) @@ -67,7 +67,7 @@ async def test_sunset_trigger(hass, calls): async_fire_time_changed(hass, trigger_time) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_sunrise_trigger(hass, calls): @@ -89,7 +89,7 @@ async def test_sunrise_trigger(hass, calls): async_fire_time_changed(hass, trigger_time) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_sunset_trigger_with_offset(hass, calls): @@ -121,8 +121,8 @@ async def test_sunset_trigger_with_offset(hass, calls): async_fire_time_changed(hass, trigger_time) await hass.async_block_till_done() - assert 1 == len(calls) - assert "sun - sunset - 0:30:00" == calls[0].data["some"] + assert len(calls) == 1 + assert calls[0].data["some"] == "sun - sunset - 0:30:00" async def test_sunrise_trigger_with_offset(hass, calls): @@ -148,7 +148,7 @@ async def test_sunrise_trigger_with_offset(hass, calls): async_fire_time_changed(hass, trigger_time) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_action_before_sunrise_no_offset(hass, calls): @@ -176,28 +176,28 @@ async def test_if_action_before_sunrise_no_offset(hass, calls): with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # now = sunrise -> 'before sunrise' true now = datetime(2015, 9, 16, 13, 32, 43, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = local midnight -> 'before sunrise' true now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 # now = local midnight - 1s -> 'before sunrise' not true now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 async def test_if_action_after_sunrise_no_offset(hass, calls): @@ -225,28 +225,28 @@ async def test_if_action_after_sunrise_no_offset(hass, calls): with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # now = sunrise + 1s -> 'after sunrise' true now = datetime(2015, 9, 16, 13, 32, 43, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = local midnight -> 'after sunrise' not true now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = local midnight - 1s -> 'after sunrise' true now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 async def test_if_action_before_sunrise_with_offset(hass, calls): @@ -278,56 +278,56 @@ async def test_if_action_before_sunrise_with_offset(hass, calls): with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # now = sunrise + 1h -> 'before sunrise' with offset +1h true now = datetime(2015, 9, 16, 14, 32, 43, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = UTC midnight -> 'before sunrise' with offset +1h not true now = datetime(2015, 9, 17, 0, 0, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = UTC midnight - 1s -> 'before sunrise' with offset +1h not true now = datetime(2015, 9, 16, 23, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = local midnight -> 'before sunrise' with offset +1h true now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 # now = local midnight - 1s -> 'before sunrise' with offset +1h not true now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 # now = sunset -> 'before sunrise' with offset +1h not true now = datetime(2015, 9, 17, 1, 56, 48, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 # now = sunset -1s -> 'before sunrise' with offset +1h not true now = datetime(2015, 9, 17, 1, 56, 45, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 async def test_if_action_before_sunset_with_offset(hass, calls): @@ -359,56 +359,56 @@ async def test_if_action_before_sunset_with_offset(hass, calls): with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = sunset + 1s + 1h -> 'before sunset' with offset +1h not true now = datetime(2015, 9, 17, 2, 55, 25, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = sunset + 1h -> 'before sunset' with offset +1h true now = datetime(2015, 9, 17, 2, 55, 24, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 # now = UTC midnight -> 'before sunset' with offset +1h true now = datetime(2015, 9, 17, 0, 0, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 3 == len(calls) + assert len(calls) == 3 # now = UTC midnight - 1s -> 'before sunset' with offset +1h true now = datetime(2015, 9, 16, 23, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 4 == len(calls) + assert len(calls) == 4 # now = sunrise -> 'before sunset' with offset +1h true now = datetime(2015, 9, 16, 13, 32, 43, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 5 == len(calls) + assert len(calls) == 5 # now = sunrise -1s -> 'before sunset' with offset +1h true now = datetime(2015, 9, 16, 13, 32, 42, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 6 == len(calls) + assert len(calls) == 6 # now = local midnight-1s -> 'after sunrise' with offset +1h not true now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 6 == len(calls) + assert len(calls) == 6 async def test_if_action_after_sunrise_with_offset(hass, calls): @@ -440,70 +440,70 @@ async def test_if_action_after_sunrise_with_offset(hass, calls): with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # now = sunrise + 1h -> 'after sunrise' with offset +1h true now = datetime(2015, 9, 16, 14, 32, 43, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = UTC noon -> 'after sunrise' with offset +1h not true now = datetime(2015, 9, 16, 12, 0, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = UTC noon - 1s -> 'after sunrise' with offset +1h not true now = datetime(2015, 9, 16, 11, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = local noon -> 'after sunrise' with offset +1h true now = datetime(2015, 9, 16, 19, 1, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 # now = local noon - 1s -> 'after sunrise' with offset +1h true now = datetime(2015, 9, 16, 18, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 3 == len(calls) + assert len(calls) == 3 # now = sunset -> 'after sunrise' with offset +1h true now = datetime(2015, 9, 17, 1, 55, 24, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 4 == len(calls) + assert len(calls) == 4 # now = sunset + 1s -> 'after sunrise' with offset +1h true now = datetime(2015, 9, 17, 1, 55, 25, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 5 == len(calls) + assert len(calls) == 5 # now = local midnight-1s -> 'after sunrise' with offset +1h true now = datetime(2015, 9, 17, 6, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 6 == len(calls) + assert len(calls) == 6 # now = local midnight -> 'after sunrise' with offset +1h not true now = datetime(2015, 9, 17, 7, 0, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 6 == len(calls) + assert len(calls) == 6 async def test_if_action_after_sunset_with_offset(hass, calls): @@ -535,28 +535,28 @@ async def test_if_action_after_sunset_with_offset(hass, calls): with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # now = sunset + 1h -> 'after sunset' with offset +1h true now = datetime(2015, 9, 16, 2, 56, 46, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = midnight-1s -> 'after sunset' with offset +1h true now = datetime(2015, 9, 16, 6, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 # now = midnight -> 'after sunset' with offset +1h not true now = datetime(2015, 9, 16, 7, 0, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 async def test_if_action_before_and_after_during(hass, calls): @@ -588,35 +588,35 @@ async def test_if_action_before_and_after_during(hass, calls): with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # now = sunset + 1s -> 'after sunrise' + 'before sunset' not true now = datetime(2015, 9, 17, 1, 55, 25, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # now = sunrise -> 'after sunrise' + 'before sunset' true now = datetime(2015, 9, 16, 13, 32, 43, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = sunset -> 'after sunrise' + 'before sunset' true now = datetime(2015, 9, 17, 1, 55, 24, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 # now = 9AM local -> 'after sunrise' + 'before sunset' true now = datetime(2015, 9, 16, 16, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 3 == len(calls) + assert len(calls) == 3 async def test_if_action_before_sunrise_no_offset_kotzebue(hass, calls): @@ -651,28 +651,28 @@ async def test_if_action_before_sunrise_no_offset_kotzebue(hass, calls): with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # now = sunrise -> 'before sunrise' true now = datetime(2015, 7, 24, 15, 17, 24, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = local midnight -> 'before sunrise' true now = datetime(2015, 7, 24, 8, 0, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 # now = local midnight - 1s -> 'before sunrise' not true now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 async def test_if_action_after_sunrise_no_offset_kotzebue(hass, calls): @@ -707,28 +707,28 @@ async def test_if_action_after_sunrise_no_offset_kotzebue(hass, calls): with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = sunrise - 1s -> 'after sunrise' not true now = datetime(2015, 7, 24, 15, 17, 23, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = local midnight -> 'after sunrise' not true now = datetime(2015, 7, 24, 8, 0, 1, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = local midnight - 1s -> 'after sunrise' true now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 async def test_if_action_before_sunset_no_offset_kotzebue(hass, calls): @@ -763,28 +763,28 @@ async def test_if_action_before_sunset_no_offset_kotzebue(hass, calls): with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # now = sunrise -> 'before sunrise' true now = datetime(2015, 7, 25, 11, 16, 27, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = local midnight -> 'before sunrise' true now = datetime(2015, 7, 24, 8, 0, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 # now = local midnight - 1s -> 'before sunrise' not true now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 async def test_if_action_after_sunset_no_offset_kotzebue(hass, calls): @@ -819,25 +819,25 @@ async def test_if_action_after_sunset_no_offset_kotzebue(hass, calls): with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = sunset - 1s -> 'after sunset' not true now = datetime(2015, 7, 25, 11, 16, 26, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = local midnight -> 'after sunset' not true now = datetime(2015, 7, 24, 8, 0, 1, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # now = local midnight - 1s -> 'after sunset' true now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 diff --git a/tests/components/automation/test_template.py b/tests/components/automation/test_template.py index 27e0d4f69653bf..91ecc4ad4ac487 100644 --- a/tests/components/automation/test_template.py +++ b/tests/components/automation/test_template.py @@ -46,14 +46,14 @@ async def test_if_fires_on_change_bool(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 await common.async_turn_off(hass) await hass.async_block_till_done() hass.states.async_set("test.entity", "planet") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_change_str(hass, calls): @@ -71,7 +71,7 @@ async def test_if_fires_on_change_str(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_change_str_crazy(hass, calls): @@ -89,7 +89,7 @@ async def test_if_fires_on_change_str_crazy(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_fires_on_change_bool(hass, calls): @@ -107,7 +107,7 @@ async def test_if_not_fires_on_change_bool(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_not_fires_on_change_str(hass, calls): @@ -125,7 +125,7 @@ async def test_if_not_fires_on_change_str(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_not_fires_on_change_str_crazy(hass, calls): @@ -146,7 +146,7 @@ async def test_if_not_fires_on_change_str_crazy(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_fires_on_no_change(hass, calls): @@ -186,12 +186,12 @@ async def test_if_fires_on_two_change(hass, calls): # Trigger once hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 # Trigger again hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_change_with_template(hass, calls): @@ -212,7 +212,7 @@ async def test_if_fires_on_change_with_template(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_fires_on_change_with_template(hass, calls): @@ -273,7 +273,7 @@ async def test_if_fires_on_change_with_template_advanced(hass, calls): hass.states.async_set("test.entity", "world", context=context) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert calls[0].context.parent_id == context.id assert "template - test.entity - hello - world - None" == calls[0].data["some"] @@ -301,12 +301,12 @@ async def test_if_fires_on_no_change_with_template_advanced(hass, calls): # Different state hass.states.async_set("test.entity", "worldz") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # Different state hass.states.async_set("test.entity", "hello") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_fires_on_change_with_template_2(hass, calls): @@ -374,17 +374,17 @@ async def test_if_action(hass, calls): # Condition is not true yet hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # Change condition to true, but it shouldn't be triggered yet hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 # Condition is true and event is triggered hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_change_with_bad_template(hass, calls): @@ -420,7 +420,7 @@ async def test_if_fires_on_change_with_bad_template_2(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_wait_template_with_trigger(hass, calls): @@ -462,8 +462,8 @@ async def test_wait_template_with_trigger(hass, calls): await hass.async_block_till_done() hass.states.async_set("test.entity", "hello") await hass.async_block_till_done() - assert 1 == len(calls) - assert "template - test.entity - hello - world - None" == calls[0].data["some"] + assert len(calls) == 1 + assert calls[0].data["some"] == "template - test.entity - hello - world - None" async def test_if_fires_on_change_with_for(hass, calls): @@ -485,10 +485,10 @@ async def test_if_fires_on_change_with_for(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_change_with_for_advanced(hass, calls): @@ -527,10 +527,10 @@ async def test_if_fires_on_change_with_for_advanced(hass, calls): hass.states.async_set("test.entity", "world", context=context) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert calls[0].context.parent_id == context.id assert "template - test.entity - hello - world - 0:00:05" == calls[0].data["some"] @@ -554,7 +554,7 @@ async def test_if_fires_on_change_with_for_0(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_change_with_for_0_advanced(hass, calls): @@ -593,9 +593,9 @@ async def test_if_fires_on_change_with_for_0_advanced(hass, calls): hass.states.async_set("test.entity", "world", context=context) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert calls[0].context.parent_id == context.id - assert "template - test.entity - hello - world - 0:00:00" == calls[0].data["some"] + assert calls[0].data["some"] == "template - test.entity - hello - world - 0:00:00" async def test_if_fires_on_change_with_for_2(hass, calls): @@ -617,10 +617,10 @@ async def test_if_fires_on_change_with_for_2(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_fires_on_change_with_for(hass, calls): @@ -642,16 +642,16 @@ async def test_if_not_fires_on_change_with_for(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=4)) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 hass.states.async_set("test.entity", "hello") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=6)) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_not_fires_when_turned_off_with_for(hass, calls): @@ -673,16 +673,16 @@ async def test_if_not_fires_when_turned_off_with_for(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=4)) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 await common.async_turn_off(hass) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=6)) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_fires_on_change_with_for_template_1(hass, calls): @@ -704,10 +704,10 @@ async def test_if_fires_on_change_with_for_template_1(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_change_with_for_template_2(hass, calls): @@ -729,10 +729,10 @@ async def test_if_fires_on_change_with_for_template_2(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_on_change_with_for_template_3(hass, calls): @@ -754,10 +754,10 @@ async def test_if_fires_on_change_with_for_template_3(hass, calls): hass.states.async_set("test.entity", "world") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_invalid_for_template_1(hass, calls): diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index 511f8a305e69ab..ec8d504652bcf5 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -49,8 +49,8 @@ async def test_if_fires_using_at(hass, calls): async_fire_time_changed(hass, dt_util.utcnow().replace(hour=5, minute=0, second=0)) await hass.async_block_till_done() - assert 1 == len(calls) - assert "time - 5" == calls[0].data["some"] + assert len(calls) == 1 + assert calls[0].data["some"] == "time - 5" async def test_if_not_fires_using_wrong_at(hass, calls): @@ -77,7 +77,7 @@ async def test_if_not_fires_using_wrong_at(hass, calls): async_fire_time_changed(hass, dt_util.utcnow().replace(hour=1, minute=0, second=5)) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_action_before(hass, calls): @@ -101,13 +101,13 @@ async def test_if_action_before(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 with patch("homeassistant.helpers.condition.dt_util.now", return_value=after_10): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_action_after(hass, calls): @@ -131,13 +131,13 @@ async def test_if_action_after(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 with patch("homeassistant.helpers.condition.dt_util.now", return_value=after_10): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_action_one_weekday(hass, calls): @@ -162,13 +162,13 @@ async def test_if_action_one_weekday(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 with patch("homeassistant.helpers.condition.dt_util.now", return_value=tuesday): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_action_list_weekday(hass, calls): @@ -194,16 +194,16 @@ async def test_if_action_list_weekday(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 with patch("homeassistant.helpers.condition.dt_util.now", return_value=tuesday): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 with patch("homeassistant.helpers.condition.dt_util.now", return_value=wednesday): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 diff --git a/tests/components/automation/test_time_pattern.py b/tests/components/automation/test_time_pattern.py index 2c0574c3238e7d..01aa32f318f49e 100644 --- a/tests/components/automation/test_time_pattern.py +++ b/tests/components/automation/test_time_pattern.py @@ -41,14 +41,14 @@ async def test_if_fires_when_hour_matches(hass, calls): async_fire_time_changed(hass, dt_util.utcnow().replace(hour=0)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 await common.async_turn_off(hass) await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow().replace(hour=0)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_when_minute_matches(hass, calls): @@ -72,7 +72,7 @@ async def test_if_fires_when_minute_matches(hass, calls): async_fire_time_changed(hass, dt_util.utcnow().replace(minute=0)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_when_second_matches(hass, calls): @@ -96,7 +96,7 @@ async def test_if_fires_when_second_matches(hass, calls): async_fire_time_changed(hass, dt_util.utcnow().replace(second=0)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_when_all_matches(hass, calls): @@ -120,7 +120,7 @@ async def test_if_fires_when_all_matches(hass, calls): async_fire_time_changed(hass, dt_util.utcnow().replace(hour=1, minute=2, second=3)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_periodic_seconds(hass, calls): @@ -144,7 +144,7 @@ async def test_if_fires_periodic_seconds(hass, calls): async_fire_time_changed(hass, dt_util.utcnow().replace(hour=0, minute=0, second=2)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_periodic_minutes(hass, calls): @@ -168,7 +168,7 @@ async def test_if_fires_periodic_minutes(hass, calls): async_fire_time_changed(hass, dt_util.utcnow().replace(hour=0, minute=2, second=0)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_fires_periodic_hours(hass, calls): @@ -192,7 +192,7 @@ async def test_if_fires_periodic_hours(hass, calls): async_fire_time_changed(hass, dt_util.utcnow().replace(hour=2, minute=0, second=0)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_default_values(hass, calls): @@ -211,14 +211,14 @@ async def test_default_values(hass, calls): async_fire_time_changed(hass, dt_util.utcnow().replace(hour=1, minute=2, second=0)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async_fire_time_changed(hass, dt_util.utcnow().replace(hour=1, minute=2, second=1)) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async_fire_time_changed(hass, dt_util.utcnow().replace(hour=2, minute=2, second=0)) await hass.async_block_till_done() - assert 2 == len(calls) + assert len(calls) == 2 diff --git a/tests/components/automation/test_zone.py b/tests/components/automation/test_zone.py index cb031486b6fd72..e80f70b10fea49 100644 --- a/tests/components/automation/test_zone.py +++ b/tests/components/automation/test_zone.py @@ -81,7 +81,7 @@ async def test_if_fires_on_zone_enter(hass, calls): ) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 assert calls[0].context.parent_id == context.id assert "zone - test.entity - hello - hello - test" == calls[0].data["some"] @@ -99,7 +99,7 @@ async def test_if_fires_on_zone_enter(hass, calls): ) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_fires_for_enter_on_zone_leave(hass, calls): @@ -130,7 +130,7 @@ async def test_if_not_fires_for_enter_on_zone_leave(hass, calls): ) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_if_fires_on_zone_leave(hass, calls): @@ -161,7 +161,7 @@ async def test_if_fires_on_zone_leave(hass, calls): ) await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 async def test_if_not_fires_for_leave_on_zone_enter(hass, calls): @@ -192,7 +192,7 @@ async def test_if_not_fires_for_leave_on_zone_enter(hass, calls): ) await hass.async_block_till_done() - assert 0 == len(calls) + assert len(calls) == 0 async def test_zone_condition(hass, calls): @@ -220,4 +220,4 @@ async def test_zone_condition(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() - assert 1 == len(calls) + assert len(calls) == 1 From ea70d71e8f9c52421be91723ddd94cd50a17b7c0 Mon Sep 17 00:00:00 2001 From: Markus Breitenberger Date: Fri, 1 May 2020 16:46:36 +0200 Subject: [PATCH 43/58] Use pulsectl library for PulseAudio connection (#34965) Get rid of internal library code and use pulsectl library to communicate with PulseAudio server. This is a breaking change as the library uses the much more powerful native interface instead of the CLI interface, requiring the need to change the default port. On the bright side, this also solves some issues with the existing implementation: - There was no test if the complete list of loaded modules was already received. If not all data could be read at once, the remaining modules not yet in the buffer were considered absent, resulting in unreliable behavior when a lot of modules were loaded on the server. - A switch could be turned on before the list of loaded modules was loaded, leading to a loopback module being loaded even though this module was already active (#32016). --- .../pulseaudio_loopback/manifest.json | 1 + .../components/pulseaudio_loopback/switch.py | 163 ++++++------------ requirements_all.txt | 3 + 3 files changed, 53 insertions(+), 114 deletions(-) diff --git a/homeassistant/components/pulseaudio_loopback/manifest.json b/homeassistant/components/pulseaudio_loopback/manifest.json index 8775f5f0947b5c..bc38d8c2594d6b 100644 --- a/homeassistant/components/pulseaudio_loopback/manifest.json +++ b/homeassistant/components/pulseaudio_loopback/manifest.json @@ -2,5 +2,6 @@ "domain": "pulseaudio_loopback", "name": "PulseAudio Loopback", "documentation": "https://www.home-assistant.io/integrations/pulseaudio_loopback", + "requirements": ["pulsectl==20.2.4"], "codeowners": [] } diff --git a/homeassistant/components/pulseaudio_loopback/switch.py b/homeassistant/components/pulseaudio_loopback/switch.py index 45577fcd6741f3..9c27ab4e0277c1 100644 --- a/homeassistant/components/pulseaudio_loopback/switch.py +++ b/homeassistant/components/pulseaudio_loopback/switch.py @@ -1,52 +1,32 @@ """Switch logic for loading/unloading pulseaudio loopback modules.""" -from datetime import timedelta import logging -import re -import socket +from pulsectl import Pulse, PulseError import voluptuous as vol -from homeassistant import util from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT import homeassistant.helpers.config_validation as cv +DOMAIN = "pulseaudio_loopback" + _LOGGER = logging.getLogger(__name__) -_PULSEAUDIO_SERVERS = {} -CONF_BUFFER_SIZE = "buffer_size" CONF_SINK_NAME = "sink_name" CONF_SOURCE_NAME = "source_name" -CONF_TCP_TIMEOUT = "tcp_timeout" -DEFAULT_BUFFER_SIZE = 1024 -DEFAULT_HOST = "localhost" DEFAULT_NAME = "paloopback" -DEFAULT_PORT = 4712 -DEFAULT_TCP_TIMEOUT = 3 +DEFAULT_PORT = 4713 IGNORED_SWITCH_WARN = "Switch is already in the desired state. Ignoring." -LOAD_CMD = "load-module module-loopback sink={0} source={1}" - -MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -MOD_REGEX = ( - r"index: ([0-9]+)\s+name: " - r"\s+argument: (?=<.*sink={0}.*>)(?=<.*source={1}.*>)" -) - -UNLOAD_CMD = "unload-module {0}" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_SINK_NAME): cv.string, vol.Required(CONF_SOURCE_NAME): cv.string, - vol.Optional(CONF_BUFFER_SIZE, default=DEFAULT_BUFFER_SIZE): cv.positive_int, - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_TCP_TIMEOUT, default=DEFAULT_TCP_TIMEOUT): cv.positive_int, } ) @@ -58,96 +38,61 @@ def setup_platform(hass, config, add_entities, discovery_info=None): source_name = config.get(CONF_SOURCE_NAME) host = config.get(CONF_HOST) port = config.get(CONF_PORT) - buffer_size = config.get(CONF_BUFFER_SIZE) - tcp_timeout = config.get(CONF_TCP_TIMEOUT) + + hass.data.setdefault(DOMAIN, {}) server_id = str.format("{0}:{1}", host, port) - if server_id in _PULSEAUDIO_SERVERS: - server = _PULSEAUDIO_SERVERS[server_id] + if host: + connect_to_server = server_id else: - server = PAServer(host, port, buffer_size, tcp_timeout) - _PULSEAUDIO_SERVERS[server_id] = server + connect_to_server = None - add_entities([PALoopbackSwitch(hass, name, server, sink_name, source_name)]) + if server_id in hass.data[DOMAIN]: + server = hass.data[DOMAIN][server_id] + else: + server = Pulse(server=connect_to_server, connect=False, threading_lock=True) + hass.data[DOMAIN][server_id] = server + add_entities([PALoopbackSwitch(name, server, sink_name, source_name)], True) -class PAServer: - """Representation of a Pulseaudio server.""" - _current_module_state = "" +class PALoopbackSwitch(SwitchEntity): + """Representation the presence or absence of a PA loopback module.""" - def __init__(self, host, port, buff_sz, tcp_timeout): - """Initialize PulseAudio server.""" - self._pa_host = host - self._pa_port = int(port) - self._buffer_size = int(buff_sz) - self._tcp_timeout = int(tcp_timeout) + def __init__(self, name, pa_server, sink_name, source_name): + """Initialize the Pulseaudio switch.""" + self._module_idx = None + self._name = name + self._sink_name = sink_name + self._source_name = source_name + self._pa_svr = pa_server - def _send_command(self, cmd, response_expected): - """Send a command to the pa server using a socket.""" - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(self._tcp_timeout) + def _get_module_idx(self): try: - sock.connect((self._pa_host, self._pa_port)) - _LOGGER.info("Calling pulseaudio: %s", cmd) - sock.send((cmd + "\n").encode("utf-8")) - if response_expected: - return_data = self._get_full_response(sock) - _LOGGER.debug("Data received from pulseaudio: %s", return_data) - else: - return_data = "" - finally: - sock.close() - return return_data - - def _get_full_response(self, sock): - """Get the full response back from pulseaudio.""" - result = "" - rcv_buffer = sock.recv(self._buffer_size) - result += rcv_buffer.decode("utf-8") - - while len(rcv_buffer) == self._buffer_size: - rcv_buffer = sock.recv(self._buffer_size) - result += rcv_buffer.decode("utf-8") - - return result - - @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) - def update_module_state(self): - """Refresh state in case an alternate process modified this data.""" - self._current_module_state = self._send_command("list-modules", True) + self._pa_svr.connect() - def turn_on(self, sink_name, source_name): - """Send a command to pulseaudio to turn on the loopback.""" - self._send_command(str.format(LOAD_CMD, sink_name, source_name), False) + for module in self._pa_svr.module_list(): + if not module.name == "module-loopback": + continue - def turn_off(self, module_idx): - """Send a command to pulseaudio to turn off the loopback.""" - self._send_command(str.format(UNLOAD_CMD, module_idx), False) + if f"sink={self._sink_name}" not in module.argument: + continue - def get_module_idx(self, sink_name, source_name): - """For a sink/source, return its module id in our cache, if found.""" - result = re.search( - str.format(MOD_REGEX, re.escape(sink_name), re.escape(source_name)), - self._current_module_state, - ) - if result and result.group(1).isdigit(): - return int(result.group(1)) - return -1 + if f"source={self._source_name}" not in module.argument: + continue + return module.index -class PALoopbackSwitch(SwitchEntity): - """Representation the presence or absence of a PA loopback module.""" + except PulseError: + return None - def __init__(self, hass, name, pa_server, sink_name, source_name): - """Initialize the Pulseaudio switch.""" - self._module_idx = -1 - self._hass = hass - self._name = name - self._sink_name = sink_name - self._source_name = source_name - self._pa_svr = pa_server + return None + + @property + def available(self): + """Return true when connected to server.""" + return self._pa_svr.connected @property def name(self): @@ -157,35 +102,25 @@ def name(self): @property def is_on(self): """Return true if device is on.""" - return self._module_idx > 0 + return self._module_idx is not None def turn_on(self, **kwargs): """Turn the device on.""" if not self.is_on: - self._pa_svr.turn_on(self._sink_name, self._source_name) - self._pa_svr.update_module_state(no_throttle=True) - self._module_idx = self._pa_svr.get_module_idx( - self._sink_name, self._source_name + self._pa_svr.module_load( + "module-loopback", + args=f"sink={self._sink_name} source={self._source_name}", ) - self.schedule_update_ha_state() else: _LOGGER.warning(IGNORED_SWITCH_WARN) def turn_off(self, **kwargs): """Turn the device off.""" if self.is_on: - self._pa_svr.turn_off(self._module_idx) - self._pa_svr.update_module_state(no_throttle=True) - self._module_idx = self._pa_svr.get_module_idx( - self._sink_name, self._source_name - ) - self.schedule_update_ha_state() + self._pa_svr.module_unload(self._module_idx) else: _LOGGER.warning(IGNORED_SWITCH_WARN) def update(self): """Refresh state in case an alternate process modified this data.""" - self._pa_svr.update_module_state() - self._module_idx = self._pa_svr.get_module_idx( - self._sink_name, self._source_name - ) + self._module_idx = self._get_module_idx() diff --git a/requirements_all.txt b/requirements_all.txt index 62a27f90c383c7..418bf0bdb9f01a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1105,6 +1105,9 @@ ptvsd==4.2.8 # homeassistant.components.wink pubnubsub-handler==1.0.8 +# homeassistant.components.pulseaudio_loopback +pulsectl==20.2.4 + # homeassistant.components.androidtv pure-python-adb==0.2.2.dev0 From 2b13a8cde4514187daffaf614e1d0405d7e39436 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 1 May 2020 17:41:57 +0200 Subject: [PATCH 44/58] Include QoS and retain in MQTT debug info (#35011) --- homeassistant/components/mqtt/debug_info.py | 8 +- tests/components/mqtt/test_common.py | 18 ++++- tests/components/mqtt/test_init.py | 88 ++++++++++++++++++++- 3 files changed, 108 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py index 86850c616382c0..75e4b53a19182a 100644 --- a/homeassistant/components/mqtt/debug_info.py +++ b/homeassistant/components/mqtt/debug_info.py @@ -137,7 +137,13 @@ async def info_for_device(hass, device_id): { "topic": topic, "messages": [ - {"payload": msg.payload, "time": msg.timestamp, "topic": msg.topic} + { + "payload": msg.payload, + "qos": msg.qos, + "retain": msg.retain, + "time": msg.timestamp, + "topic": msg.topic, + } for msg in list(subscription["messages"]) ], } diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 1199aaa40c7951..949d77c244ddf6 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -601,7 +601,13 @@ async def help_test_entity_debug_info_max_messages(hass, mqtt_mock, domain, conf == debug_info.STORED_MESSAGES ) messages = [ - {"topic": "test-topic", "payload": f"{i}", "time": start_dt} + { + "payload": f"{i}", + "qos": 0, + "retain": False, + "time": start_dt, + "topic": "test-topic", + } for i in range(1, debug_info.STORED_MESSAGES + 1) ] assert {"topic": "test-topic", "messages": messages} in debug_info_data["entities"][ @@ -656,7 +662,15 @@ async def help_test_entity_debug_info_message( assert len(debug_info_data["entities"][0]["subscriptions"]) >= 1 assert { "topic": topic, - "messages": [{"topic": topic, "payload": payload, "time": start_dt}], + "messages": [ + { + "payload": payload, + "qos": 0, + "retain": False, + "time": start_dt, + "topic": topic, + } + ], } in debug_info_data["entities"][0]["subscriptions"] diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 672ff127b4dedc..28cca6a856a669 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1291,7 +1291,15 @@ async def test_debug_info_wildcard(hass, mqtt_mock): assert len(debug_info_data["entities"][0]["subscriptions"]) >= 1 assert { "topic": "sensor/#", - "messages": [{"topic": "sensor/abc", "payload": "123", "time": start_dt}], + "messages": [ + { + "payload": "123", + "qos": 0, + "retain": False, + "time": start_dt, + "topic": "sensor/abc", + } + ], } in debug_info_data["entities"][0]["subscriptions"] @@ -1338,8 +1346,20 @@ async def test_debug_info_filter_same(hass, mqtt_mock): assert { "topic": "sensor/#", "messages": [ - {"payload": "123", "time": dt1, "topic": "sensor/abc"}, - {"payload": "123", "time": dt2, "topic": "sensor/abc"}, + { + "payload": "123", + "qos": 0, + "retain": False, + "time": dt1, + "topic": "sensor/abc", + }, + { + "payload": "123", + "qos": 0, + "retain": False, + "time": dt2, + "topic": "sensor/abc", + }, ], } == debug_info_data["entities"][0]["subscriptions"][0] @@ -1382,6 +1402,8 @@ async def test_debug_info_same_topic(hass, mqtt_mock): assert len(debug_info_data["entities"][0]["subscriptions"]) == 1 assert { "payload": "123", + "qos": 0, + "retain": False, "time": start_dt, "topic": "sensor/status", } in debug_info_data["entities"][0]["subscriptions"][0]["messages"] @@ -1395,3 +1417,63 @@ async def test_debug_info_same_topic(hass, mqtt_mock): with patch("homeassistant.util.dt.utcnow") as dt_utcnow: dt_utcnow.return_value = start_dt async_fire_mqtt_message(hass, "sensor/status", "123", qos=0, retain=False) + + +async def test_debug_info_qos_retain(hass, mqtt_mock): + """Test debug info.""" + config = { + "device": {"identifiers": ["helloworld"]}, + "platform": "mqtt", + "name": "test", + "state_topic": "sensor/#", + "unique_id": "veryunique", + } + + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps(config) + async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device({("mqtt", "helloworld")}, set()) + assert device is not None + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"][0]["subscriptions"]) >= 1 + assert {"topic": "sensor/#", "messages": []} in debug_info_data["entities"][0][ + "subscriptions" + ] + + start_dt = datetime(2019, 1, 1, 0, 0, 0) + with patch("homeassistant.util.dt.utcnow") as dt_utcnow: + dt_utcnow.return_value = start_dt + async_fire_mqtt_message(hass, "sensor/abc", "123", qos=0, retain=False) + async_fire_mqtt_message(hass, "sensor/abc", "123", qos=1, retain=True) + async_fire_mqtt_message(hass, "sensor/abc", "123", qos=2, retain=False) + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"][0]["subscriptions"]) == 1 + assert { + "payload": "123", + "qos": 0, + "retain": False, + "time": start_dt, + "topic": "sensor/abc", + } in debug_info_data["entities"][0]["subscriptions"][0]["messages"] + assert { + "payload": "123", + "qos": 1, + "retain": True, + "time": start_dt, + "topic": "sensor/abc", + } in debug_info_data["entities"][0]["subscriptions"][0]["messages"] + assert { + "payload": "123", + "qos": 2, + "retain": False, + "time": start_dt, + "topic": "sensor/abc", + } in debug_info_data["entities"][0]["subscriptions"][0]["messages"] From e6be297fba674391db4b859b77f29e8a0bbb740d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 May 2020 11:03:20 -0500 Subject: [PATCH 45/58] Bump HAP-python to 2.8.3 (#35023) * Fixes camera support --- homeassistant/components/homekit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 27f83d996ad532..796bb3933f75d2 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -2,7 +2,7 @@ "domain": "homekit", "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", - "requirements": ["HAP-python==2.8.2","fnvhash==0.1.0","PyQRCode==1.2.1","base36==0.1.1"], + "requirements": ["HAP-python==2.8.3","fnvhash==0.1.0","PyQRCode==1.2.1","base36==0.1.1"], "dependencies": ["http"], "after_dependencies": ["logbook"], "codeowners": ["@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 418bf0bdb9f01a..18c9f9ef407fab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -35,7 +35,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.1.1 # homeassistant.components.homekit -HAP-python==2.8.2 +HAP-python==2.8.3 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33bd8c3f9d0a7c..30871402cd366b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,7 @@ -r requirements_test.txt # homeassistant.components.homekit -HAP-python==2.8.2 +HAP-python==2.8.3 # homeassistant.components.mobile_app # homeassistant.components.owntracks From 8661cf463a81e6e30a78ec195c329e1a5399373e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 1 May 2020 11:29:58 -0600 Subject: [PATCH 46/58] Update AirVisual to use DataUpdateCoordinator (#34796) * Update AirVisual to use DataUpdateCoordinator * Empty commit to re-trigger build * Don't include history or trends in config flow * Code review --- .../components/airvisual/__init__.py | 171 +++++++----------- .../components/airvisual/air_quality.py | 51 +++--- .../components/airvisual/config_flow.py | 5 +- homeassistant/components/airvisual/const.py | 4 +- homeassistant/components/airvisual/sensor.py | 123 ++++++------- 5 files changed, 156 insertions(+), 198 deletions(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 4079f739824136..099fdfc5df71b8 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -19,30 +19,23 @@ ) from homeassistant.core import callback from homeassistant.helpers import aiohttp_client, config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( CONF_CITY, CONF_COUNTRY, CONF_GEOGRAPHIES, CONF_INTEGRATION_TYPE, - DATA_CLIENT, + DATA_COORDINATOR, DOMAIN, INTEGRATION_TYPE_GEOGRAPHY, INTEGRATION_TYPE_NODE_PRO, LOGGER, - TOPIC_UPDATE, ) PLATFORMS = ["air_quality", "sensor"] -DATA_LISTENER = "listener" - DEFAULT_ATTRIBUTION = "Data provided by AirVisual" DEFAULT_GEOGRAPHY_SCAN_INTERVAL = timedelta(minutes=10) DEFAULT_NODE_PRO_SCAN_INTERVAL = timedelta(minutes=1) @@ -97,7 +90,7 @@ def async_get_geography_id(geography_dict): async def async_setup(hass, config): """Set up the AirVisual component.""" - hass.data[DOMAIN] = {DATA_CLIENT: {}, DATA_LISTENER: {}} + hass.data[DOMAIN] = {DATA_COORDINATOR: {}} if DOMAIN not in config: return True @@ -167,35 +160,71 @@ async def async_setup_entry(hass, config_entry): if CONF_API_KEY in config_entry.data: _standardize_geography_config_entry(hass, config_entry) - airvisual = AirVisualGeographyData( + + client = Client(api_key=config_entry.data[CONF_API_KEY], session=websession) + + async def async_update_data(): + """Get new data from the API.""" + if CONF_CITY in config_entry.data: + api_coro = client.api.city( + config_entry.data[CONF_CITY], + config_entry.data[CONF_STATE], + config_entry.data[CONF_COUNTRY], + ) + else: + api_coro = client.api.nearest_city( + config_entry.data[CONF_LATITUDE], config_entry.data[CONF_LONGITUDE], + ) + + try: + return await api_coro + except AirVisualError as err: + raise UpdateFailed(f"Error while retrieving data: {err}") + + coordinator = DataUpdateCoordinator( hass, - Client(api_key=config_entry.data[CONF_API_KEY], session=websession), - config_entry, + LOGGER, + name="geography data", + update_interval=DEFAULT_GEOGRAPHY_SCAN_INTERVAL, + update_method=async_update_data, ) # Only geography-based entries have options: config_entry.add_update_listener(async_update_options) else: _standardize_node_pro_config_entry(hass, config_entry) - airvisual = AirVisualNodeProData(hass, Client(session=websession), config_entry) - await airvisual.async_update() + client = Client(session=websession) - hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = airvisual + async def async_update_data(): + """Get new data from the API.""" + try: + return await client.node.from_samba( + config_entry.data[CONF_IP_ADDRESS], + config_entry.data[CONF_PASSWORD], + include_history=False, + include_trends=False, + ) + except NodeProError as err: + raise UpdateFailed(f"Error while retrieving data: {err}") + + coordinator = DataUpdateCoordinator( + hass, + LOGGER, + name="Node/Pro data", + update_interval=DEFAULT_NODE_PRO_SCAN_INTERVAL, + update_method=async_update_data, + ) + + await coordinator.async_refresh() + + hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, component) ) - async def refresh(event_time): - """Refresh data from AirVisual.""" - await airvisual.async_update() - - hass.data[DOMAIN][DATA_LISTENER][config_entry.entry_id] = async_track_time_interval( - hass, refresh, airvisual.scan_interval - ) - return True @@ -248,28 +277,31 @@ async def async_unload_entry(hass, config_entry): ) ) if unload_ok: - hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) - remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop(config_entry.entry_id) - remove_listener() + hass.data[DOMAIN][DATA_COORDINATOR].pop(config_entry.entry_id) return unload_ok async def async_update_options(hass, config_entry): """Handle an options update.""" - airvisual = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] - airvisual.async_update_options(config_entry.options) + coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] + await coordinator.async_request_refresh() class AirVisualEntity(Entity): """Define a generic AirVisual entity.""" - def __init__(self, airvisual): + def __init__(self, coordinator): """Initialize.""" - self._airvisual = airvisual self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} self._icon = None self._unit = None + self.coordinator = coordinator + + @property + def available(self): + """Return if entity is available.""" + return self.coordinator.last_update_success @property def device_state_attributes(self): @@ -295,9 +327,7 @@ def update(): self.update_from_latest_data() self.async_write_ha_state() - self.async_on_remove( - async_dispatcher_connect(self.hass, self._airvisual.topic_update, update) - ) + self.async_on_remove(self.coordinator.async_add_listener(update)) self.update_from_latest_data() @@ -305,76 +335,3 @@ def update(): def update_from_latest_data(self): """Update the entity from the latest data.""" raise NotImplementedError - - -class AirVisualGeographyData: - """Define a class to manage data from the AirVisual cloud API.""" - - def __init__(self, hass, client, config_entry): - """Initialize.""" - self._client = client - self._hass = hass - self.data = {} - self.geography_data = config_entry.data - self.geography_id = config_entry.unique_id - self.integration_type = INTEGRATION_TYPE_GEOGRAPHY - self.options = config_entry.options - self.scan_interval = DEFAULT_GEOGRAPHY_SCAN_INTERVAL - self.topic_update = TOPIC_UPDATE.format(config_entry.unique_id) - - async def async_update(self): - """Get new data for all locations from the AirVisual cloud API.""" - if CONF_CITY in self.geography_data: - api_coro = self._client.api.city( - self.geography_data[CONF_CITY], - self.geography_data[CONF_STATE], - self.geography_data[CONF_COUNTRY], - ) - else: - api_coro = self._client.api.nearest_city( - self.geography_data[CONF_LATITUDE], self.geography_data[CONF_LONGITUDE], - ) - - try: - self.data[self.geography_id] = await api_coro - except AirVisualError as err: - LOGGER.error("Error while retrieving data: %s", err) - self.data[self.geography_id] = {} - - LOGGER.debug("Received new geography data") - async_dispatcher_send(self._hass, self.topic_update) - - @callback - def async_update_options(self, options): - """Update the data manager's options.""" - self.options = options - async_dispatcher_send(self._hass, self.topic_update) - - -class AirVisualNodeProData: - """Define a class to manage data from an AirVisual Node/Pro.""" - - def __init__(self, hass, client, config_entry): - """Initialize.""" - self._client = client - self._hass = hass - self._password = config_entry.data[CONF_PASSWORD] - self.data = {} - self.integration_type = INTEGRATION_TYPE_NODE_PRO - self.ip_address = config_entry.data[CONF_IP_ADDRESS] - self.scan_interval = DEFAULT_NODE_PRO_SCAN_INTERVAL - self.topic_update = TOPIC_UPDATE.format(config_entry.data[CONF_IP_ADDRESS]) - - async def async_update(self): - """Get new data from the Node/Pro.""" - try: - self.data = await self._client.node.from_samba( - self.ip_address, self._password, include_history=False - ) - except NodeProError as err: - LOGGER.error("Error while retrieving Node/Pro data: %s", err) - self.data = {} - return - - LOGGER.debug("Received new Node/Pro data") - async_dispatcher_send(self._hass, self.topic_update) diff --git a/homeassistant/components/airvisual/air_quality.py b/homeassistant/components/airvisual/air_quality.py index 71f9f9d9fbe46c..bd1c10a9d845d5 100644 --- a/homeassistant/components/airvisual/air_quality.py +++ b/homeassistant/components/airvisual/air_quality.py @@ -4,7 +4,12 @@ from homeassistant.core import callback from . import AirVisualEntity -from .const import DATA_CLIENT, DOMAIN, INTEGRATION_TYPE_GEOGRAPHY +from .const import ( + CONF_INTEGRATION_TYPE, + DATA_COORDINATOR, + DOMAIN, + INTEGRATION_TYPE_GEOGRAPHY, +) ATTR_HUMIDITY = "humidity" ATTR_SENSOR_LIFE = "{0}_sensor_life" @@ -13,13 +18,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up AirVisual air quality entities based on a config entry.""" - airvisual = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] + coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] # Geography-based AirVisual integrations don't utilize this platform: - if airvisual.integration_type == INTEGRATION_TYPE_GEOGRAPHY: + if config_entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_GEOGRAPHY: return - async_add_entities([AirVisualNodeProSensor(airvisual)], True) + async_add_entities([AirVisualNodeProSensor(coordinator)], True) class AirVisualNodeProSensor(AirVisualEntity, AirQualityEntity): @@ -35,69 +40,71 @@ def __init__(self, airvisual): @property def air_quality_index(self): """Return the Air Quality Index (AQI).""" - if self._airvisual.data["current"]["settings"]["is_aqi_usa"]: - return self._airvisual.data["current"]["measurements"]["aqi_us"] - return self._airvisual.data["current"]["measurements"]["aqi_cn"] + if self.coordinator.data["current"]["settings"]["is_aqi_usa"]: + return self.coordinator.data["current"]["measurements"]["aqi_us"] + return self.coordinator.data["current"]["measurements"]["aqi_cn"] @property def available(self): """Return True if entity is available.""" - return bool(self._airvisual.data) + return bool(self.coordinator.data) @property def carbon_dioxide(self): """Return the CO2 (carbon dioxide) level.""" - return self._airvisual.data["current"]["measurements"].get("co2_ppm") + return self.coordinator.data["current"]["measurements"].get("co2") @property def device_info(self): """Return device registry information for this entity.""" return { - "identifiers": {(DOMAIN, self._airvisual.data["current"]["serial_number"])}, - "name": self._airvisual.data["current"]["settings"]["node_name"], + "identifiers": { + (DOMAIN, self.coordinator.data["current"]["serial_number"]) + }, + "name": self.coordinator.data["current"]["settings"]["node_name"], "manufacturer": "AirVisual", - "model": f'{self._airvisual.data["current"]["status"]["model"]}', + "model": f'{self.coordinator.data["current"]["status"]["model"]}', "sw_version": ( - f'Version {self._airvisual.data["current"]["status"]["system_version"]}' - f'{self._airvisual.data["current"]["status"]["app_version"]}' + f'Version {self.coordinator.data["current"]["status"]["system_version"]}' + f'{self.coordinator.data["current"]["status"]["app_version"]}' ), } @property def name(self): """Return the name.""" - node_name = self._airvisual.data["current"]["settings"]["node_name"] + node_name = self.coordinator.data["current"]["settings"]["node_name"] return f"{node_name} Node/Pro: Air Quality" @property def particulate_matter_2_5(self): """Return the particulate matter 2.5 level.""" - return self._airvisual.data["current"]["measurements"].get("pm2_5") + return self.coordinator.data["current"]["measurements"].get("pm2_5") @property def particulate_matter_10(self): """Return the particulate matter 10 level.""" - return self._airvisual.data["current"]["measurements"].get("pm1_0") + return self.coordinator.data["current"]["measurements"].get("pm1_0") @property def particulate_matter_0_1(self): """Return the particulate matter 0.1 level.""" - return self._airvisual.data["current"]["measurements"].get("pm0_1") + return self.coordinator.data["current"]["measurements"].get("pm0_1") @property def unique_id(self): """Return a unique, Home Assistant friendly identifier for this entity.""" - return self._airvisual.data["current"]["serial_number"] + return self.coordinator.data["current"]["serial_number"] @callback def update_from_latest_data(self): - """Update from the Node/Pro's data.""" + """Update the entity from the latest data.""" self._attrs.update( { - ATTR_VOC: self._airvisual.data["current"]["measurements"].get("voc"), + ATTR_VOC: self.coordinator.data["current"]["measurements"].get("voc"), **{ ATTR_SENSOR_LIFE.format(pollutant): lifespan - for pollutant, lifespan in self._airvisual.data["current"][ + for pollutant, lifespan in self.coordinator.data["current"][ "status" ]["sensor_life"].items() }, diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index 691fa19504a4eb..abbc2df9061a3a 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -146,7 +146,10 @@ async def async_step_node_pro(self, user_input=None): try: await client.node.from_samba( - user_input[CONF_IP_ADDRESS], user_input[CONF_PASSWORD] + user_input[CONF_IP_ADDRESS], + user_input[CONF_PASSWORD], + include_history=False, + include_trends=False, ) except NodeProError as err: LOGGER.error("Error connecting to Node/Pro unit: %s", err) diff --git a/homeassistant/components/airvisual/const.py b/homeassistant/components/airvisual/const.py index 0e0e62a9b0c58e..a98a899b762420 100644 --- a/homeassistant/components/airvisual/const.py +++ b/homeassistant/components/airvisual/const.py @@ -12,6 +12,4 @@ CONF_GEOGRAPHIES = "geographies" CONF_INTEGRATION_TYPE = "integration_type" -DATA_CLIENT = "client" - -TOPIC_UPDATE = f"airvisual_update_{0}" +DATA_COORDINATOR = "coordinator" diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 5009788e6fa957..b122f3c27b4631 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -24,7 +24,8 @@ from .const import ( CONF_CITY, CONF_COUNTRY, - DATA_CLIENT, + CONF_INTEGRATION_TYPE, + DATA_COORDINATOR, DOMAIN, INTEGRATION_TYPE_GEOGRAPHY, ) @@ -92,68 +93,53 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up AirVisual sensors based on a config entry.""" - airvisual = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] + coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] - if airvisual.integration_type == INTEGRATION_TYPE_GEOGRAPHY: + if config_entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_GEOGRAPHY: sensors = [ AirVisualGeographySensor( - airvisual, kind, name, icon, unit, locale, geography_id, + coordinator, config_entry, kind, name, icon, unit, locale, ) - for geography_id in airvisual.data for locale in GEOGRAPHY_SENSOR_LOCALES for kind, name, icon, unit in GEOGRAPHY_SENSORS ] else: sensors = [ - AirVisualNodeProSensor(airvisual, kind, name, device_class, unit) + AirVisualNodeProSensor(coordinator, kind, name, device_class, unit) for kind, name, device_class, unit in NODE_PRO_SENSORS ] async_add_entities(sensors, True) -class AirVisualSensor(AirVisualEntity): - """Define a generic AirVisual sensor.""" - - def __init__(self, airvisual, kind, name, unit): - """Initialize.""" - super().__init__(airvisual) - - self._kind = kind - self._name = name - self._state = None - self._unit = unit - - @property - def state(self): - """Return the state.""" - return self._state - - -class AirVisualGeographySensor(AirVisualSensor): +class AirVisualGeographySensor(AirVisualEntity): """Define an AirVisual sensor related to geography data via the Cloud API.""" - def __init__(self, airvisual, kind, name, icon, unit, locale, geography_id): + def __init__(self, coordinator, config_entry, kind, name, icon, unit, locale): """Initialize.""" - super().__init__(airvisual, kind, name, unit) + super().__init__(coordinator) self._attrs.update( { - ATTR_CITY: airvisual.data[geography_id].get(CONF_CITY), - ATTR_STATE: airvisual.data[geography_id].get(CONF_STATE), - ATTR_COUNTRY: airvisual.data[geography_id].get(CONF_COUNTRY), + ATTR_CITY: config_entry.data.get(CONF_CITY), + ATTR_STATE: config_entry.data.get(CONF_STATE), + ATTR_COUNTRY: config_entry.data.get(CONF_COUNTRY), } ) - self._geography_id = geography_id + self._config_entry = config_entry self._icon = icon + self._kind = kind self._locale = locale + self._name = name + self._state = None + self._unit = unit @property def available(self): """Return True if entity is available.""" try: - return bool( - self._airvisual.data[self._geography_id]["current"]["pollution"] + return self.coordinator.last_update_success and bool( + self.coordinator.data["current"]["pollution"] ) except KeyError: return False @@ -163,16 +149,21 @@ def name(self): """Return the name.""" return f"{GEOGRAPHY_SENSOR_LOCALES[self._locale]} {self._name}" + @property + def state(self): + """Return the state.""" + return self._state + @property def unique_id(self): """Return a unique, Home Assistant friendly identifier for this entity.""" - return f"{self._geography_id}_{self._locale}_{self._kind}" + return f"{self._config_entry.unique_id}_{self._locale}_{self._kind}" @callback def update_from_latest_data(self): - """Update the sensor.""" + """Update the entity from the latest data.""" try: - data = self._airvisual.data[self._geography_id]["current"]["pollution"] + data = self.coordinator.data["current"]["pollution"] except KeyError: return @@ -197,36 +188,31 @@ def update_from_latest_data(self): } ) - if CONF_LATITUDE in self._airvisual.geography_data: - if self._airvisual.options[CONF_SHOW_ON_MAP]: - self._attrs[ATTR_LATITUDE] = self._airvisual.geography_data[ - CONF_LATITUDE - ] - self._attrs[ATTR_LONGITUDE] = self._airvisual.geography_data[ - CONF_LONGITUDE - ] + if CONF_LATITUDE in self._config_entry.data: + if self._config_entry.options[CONF_SHOW_ON_MAP]: + self._attrs[ATTR_LATITUDE] = self._config_entry.data[CONF_LATITUDE] + self._attrs[ATTR_LONGITUDE] = self._config_entry.data[CONF_LONGITUDE] self._attrs.pop("lati", None) self._attrs.pop("long", None) else: - self._attrs["lati"] = self._airvisual.geography_data[CONF_LATITUDE] - self._attrs["long"] = self._airvisual.geography_data[CONF_LONGITUDE] + self._attrs["lati"] = self._config_entry.data[CONF_LATITUDE] + self._attrs["long"] = self._config_entry.data[CONF_LONGITUDE] self._attrs.pop(ATTR_LATITUDE, None) self._attrs.pop(ATTR_LONGITUDE, None) -class AirVisualNodeProSensor(AirVisualSensor): +class AirVisualNodeProSensor(AirVisualEntity): """Define an AirVisual sensor related to a Node/Pro unit.""" - def __init__(self, airvisual, kind, name, device_class, unit): + def __init__(self, coordinator, kind, name, device_class, unit): """Initialize.""" - super().__init__(airvisual, kind, name, unit) + super().__init__(coordinator) self._device_class = device_class - - @property - def available(self): - """Return True if entity is available.""" - return bool(self._airvisual.data) + self._kind = kind + self._name = name + self._state = None + self._unit = unit @property def device_class(self): @@ -237,37 +223,44 @@ def device_class(self): def device_info(self): """Return device registry information for this entity.""" return { - "identifiers": {(DOMAIN, self._airvisual.data["current"]["serial_number"])}, - "name": self._airvisual.data["current"]["settings"]["node_name"], + "identifiers": { + (DOMAIN, self.coordinator.data["current"]["serial_number"]) + }, + "name": self.coordinator.data["current"]["settings"]["node_name"], "manufacturer": "AirVisual", - "model": f'{self._airvisual.data["current"]["status"]["model"]}', + "model": f'{self.coordinator.data["current"]["status"]["model"]}', "sw_version": ( - f'Version {self._airvisual.data["current"]["status"]["system_version"]}' - f'{self._airvisual.data["current"]["status"]["app_version"]}' + f'Version {self.coordinator.data["current"]["status"]["system_version"]}' + f'{self.coordinator.data["current"]["status"]["app_version"]}' ), } @property def name(self): """Return the name.""" - node_name = self._airvisual.data["current"]["settings"]["node_name"] + node_name = self.coordinator.data["current"]["settings"]["node_name"] return f"{node_name} Node/Pro: {self._name}" + @property + def state(self): + """Return the state.""" + return self._state + @property def unique_id(self): """Return a unique, Home Assistant friendly identifier for this entity.""" - return f"{self._airvisual.data['current']['serial_number']}_{self._kind}" + return f"{self.coordinator.data['current']['serial_number']}_{self._kind}" @callback def update_from_latest_data(self): - """Update from the Node/Pro's data.""" + """Update the entity from the latest data.""" if self._kind == SENSOR_KIND_BATTERY_LEVEL: - self._state = self._airvisual.data["current"]["status"]["battery"] + self._state = self.coordinator.data["current"]["status"]["battery"] elif self._kind == SENSOR_KIND_HUMIDITY: - self._state = self._airvisual.data["current"]["measurements"].get( + self._state = self.coordinator.data["current"]["measurements"].get( "humidity" ) elif self._kind == SENSOR_KIND_TEMPERATURE: - self._state = self._airvisual.data["current"]["measurements"].get( + self._state = self.coordinator.data["current"]["measurements"].get( "temperature_C" ) From a65e656c038178a4fa795189981a7046e9b61600 Mon Sep 17 00:00:00 2001 From: Mich-b Date: Fri, 1 May 2020 19:31:39 +0200 Subject: [PATCH 47/58] Add more SNMP variable types (#33426) * added support for more SNMP variable types * Fix SNMP pull request formatting * retry fix linting errors * Created SNMP vartype dict * Moved to Integer instead of Integer32 as default vartype * Update homeassistant/components/snmp/switch.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/snmp/switch.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/snmp/switch.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/snmp/const.py | 2 + homeassistant/components/snmp/switch.py | 60 +++++++++++++++++++++---- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/snmp/const.py b/homeassistant/components/snmp/const.py index 90e445da554d6c..b3a93cfe98bcb8 100644 --- a/homeassistant/components/snmp/const.py +++ b/homeassistant/components/snmp/const.py @@ -8,6 +8,7 @@ CONF_PRIV_KEY = "priv_key" CONF_PRIV_PROTOCOL = "priv_protocol" CONF_VERSION = "version" +CONF_VARTYPE = "vartype" DEFAULT_AUTH_PROTOCOL = "none" DEFAULT_COMMUNITY = "public" @@ -16,6 +17,7 @@ DEFAULT_PORT = "161" DEFAULT_PRIV_PROTOCOL = "none" DEFAULT_VERSION = "1" +DEFAULT_VARTYPE = "none" SNMP_VERSIONS = {"1": 0, "2c": 1, "3": None} diff --git a/homeassistant/components/snmp/switch.py b/homeassistant/components/snmp/switch.py index 18effc563bb48a..7210c8e5fd343e 100644 --- a/homeassistant/components/snmp/switch.py +++ b/homeassistant/components/snmp/switch.py @@ -1,7 +1,6 @@ """Support for SNMP enabled switch.""" import logging -from pyasn1.type.univ import Integer import pysnmp.hlapi.asyncio as hlapi from pysnmp.hlapi.asyncio import ( CommunityData, @@ -14,6 +13,20 @@ getCmd, setCmd, ) +from pysnmp.proto.rfc1902 import ( + Counter32, + Counter64, + Gauge32, + Integer, + Integer32, + IpAddress, + Null, + ObjectIdentifier, + OctetString, + Opaque, + TimeTicks, + Unsigned32, +) import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity @@ -34,12 +47,14 @@ CONF_COMMUNITY, CONF_PRIV_KEY, CONF_PRIV_PROTOCOL, + CONF_VARTYPE, CONF_VERSION, DEFAULT_AUTH_PROTOCOL, DEFAULT_HOST, DEFAULT_NAME, DEFAULT_PORT, DEFAULT_PRIV_PROTOCOL, + DEFAULT_VARTYPE, DEFAULT_VERSION, MAP_AUTH_PROTOCOLS, MAP_PRIV_PROTOCOLS, @@ -56,6 +71,22 @@ DEFAULT_PAYLOAD_OFF = 0 DEFAULT_PAYLOAD_ON = 1 +MAP_SNMP_VARTYPES = { + "Counter32": Counter32, + "Counter64": Counter64, + "Gauge32": Gauge32, + "Integer32": Integer32, + "Integer": Integer, + "IpAddress": IpAddress, + "Null": Null, + # some work todo to support tuple ObjectIdentifier, this just supports str + "ObjectIdentifier": ObjectIdentifier, + "OctetString": OctetString, + "Opaque": Opaque, + "TimeTicks": TimeTicks, + "Unsigned32": Unsigned32, +} + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_BASEOID): cv.string, @@ -78,6 +109,7 @@ vol.Optional(CONF_PRIV_PROTOCOL, default=DEFAULT_PRIV_PROTOCOL): vol.In( MAP_PRIV_PROTOCOLS ), + vol.Optional(CONF_VARTYPE, default=DEFAULT_VARTYPE): cv.string, } ) @@ -100,6 +132,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= privproto = config.get(CONF_PRIV_PROTOCOL) payload_on = config.get(CONF_PAYLOAD_ON) payload_off = config.get(CONF_PAYLOAD_OFF) + vartype = config.get(CONF_VARTYPE) async_add_entities( [ @@ -120,6 +153,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= payload_off, command_payload_on, command_payload_off, + vartype, ) ], True, @@ -147,11 +181,13 @@ def __init__( payload_off, command_payload_on, command_payload_off, + vartype, ): """Initialize the switch.""" self._name = name self._baseoid = baseoid + self._vartype = vartype # Set the command OID to the base OID if command OID is unset self._commandoid = commandoid or baseoid @@ -191,17 +227,24 @@ def __init__( async def async_turn_on(self, **kwargs): """Turn on the switch.""" - if self._command_payload_on.isdigit(): - await self._set(Integer(self._command_payload_on)) - else: - await self._set(self._command_payload_on) + # If vartype set, use it - http://snmplabs.com/pysnmp/docs/api-reference.html#pysnmp.smi.rfc1902.ObjectType + await self._execute_command(self._command_payload_on) async def async_turn_off(self, **kwargs): """Turn off the switch.""" - if self._command_payload_on.isdigit(): - await self._set(Integer(self._command_payload_off)) + await self._execute_command(self._command_payload_off) + + async def _execute_command(self, command): + # User did not set vartype and command is not a digit + if self._vartype == "none" and not self._command_payload_on.isdigit(): + await self._set(command) + # User set vartype Null, command must be an empty string + elif self._vartype == "Null": + await self._set(Null)("") + # user did not set vartype but command is digit: defaulting to Integer + # or user did set vartype else: - await self._set(self._command_payload_off) + await self._set(MAP_SNMP_VARTYPES.get(self._vartype, Integer)(command)) async def async_update(self): """Update the state.""" @@ -241,7 +284,6 @@ def is_on(self): return self._state async def _set(self, value): - await setCmd( *self._request_args, ObjectType(ObjectIdentity(self._commandoid), value) ) From 9436645648212749765465cd0054bb3d5d42b43c Mon Sep 17 00:00:00 2001 From: Teemu R Date: Fri, 1 May 2020 19:43:30 +0200 Subject: [PATCH 48/58] Fix songpal on devices where source!=uri (#34699) --- homeassistant/components/songpal/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index e2a9d9c5d57949..55d8f0133a9058 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -164,7 +164,7 @@ async def _volume_changed(volume: VolumeChange): async def _source_changed(content: ContentChange): _LOGGER.debug("Source changed: %s", content) if content.is_input: - self._active_source = self._sources[content.source] + self._active_source = self._sources[content.uri] _LOGGER.debug("New active source: %s", self._active_source) self.async_write_ha_state() else: From b3201523aa66614323ac703c6ed75109f5405e91 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 May 2020 11:34:09 -0700 Subject: [PATCH 49/58] Fix translation merging for custom components without translations (#35032) --- homeassistant/helpers/translation.py | 9 ++++++--- tests/helpers/test_translation.py | 8 ++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index abf399721868bd..d0fac953ac13bf 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -210,6 +210,9 @@ async def async_get_component_strings( else: files_to_load[loaded] = path + if not files_to_load: + return translations + # Load files load_translations_job = hass.async_add_executor_job( load_translations_files, files_to_load @@ -218,12 +221,12 @@ async def async_get_component_strings( loaded_translations = await load_translations_job # Translations that miss "title" will get integration put in. - for loaded, translations in loaded_translations.items(): + for loaded, loaded_translation in loaded_translations.items(): if "." in loaded: continue - if "title" not in translations: - translations["title"] = integrations[loaded].name + if "title" not in loaded_translation: + loaded_translation["title"] = integrations[loaded].name translations.update(loaded_translations) diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index 8596b9dd7f3988..77e55a1d6edba4 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -267,3 +267,11 @@ async def test_caching(hass): await translation.async_get_translations(hass, "en", "state") assert len(mock_merge.mock_calls) == 2 + + +async def test_custom_component_translations(hass): + """Test getting translation from custom components.""" + hass.config.components.add("test_standalone") + hass.config.components.add("test_embedded") + hass.config.components.add("test_package") + assert await translation.async_get_translations(hass, "en", "state") == {} From e1ae455f1d8507d4bdd007d1a415fc44609fdda7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 1 May 2020 20:35:30 +0200 Subject: [PATCH 50/58] Fix ONVIF YAML import (#35035) --- homeassistant/components/onvif/__init__.py | 30 ++++++++++++------- homeassistant/components/onvif/config_flow.py | 21 ++++++------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index 5fe43fdd83b231..eed5a20e3cc686 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -5,15 +5,23 @@ from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_per_platform from .const import ( - CONF_PROFILE, CONF_RTSP_TRANSPORT, DEFAULT_ARGUMENTS, - DEFAULT_PROFILE, + DEFAULT_NAME, + DEFAULT_PASSWORD, + DEFAULT_PORT, + DEFAULT_USERNAME, DOMAIN, RTSP_TRANS_PROTOCOLS, ) @@ -32,12 +40,14 @@ async def async_setup(hass: HomeAssistant, config: dict): continue config = p_config.copy() - profile = config.get(CONF_PROFILE, DEFAULT_PROFILE) if config[CONF_HOST] not in configs.keys(): - configs[config[CONF_HOST]] = config - configs[config[CONF_HOST]][CONF_PROFILE] = [profile] - else: - configs[config[CONF_HOST]][CONF_PROFILE].append(profile) + configs[config[CONF_HOST]] = { + CONF_HOST: config[CONF_HOST], + CONF_NAME: config.get(CONF_NAME, DEFAULT_NAME), + CONF_PASSWORD: config.get(CONF_PASSWORD, DEFAULT_PASSWORD), + CONF_PORT: config.get(CONF_PORT, DEFAULT_PORT), + CONF_USERNAME: config.get(CONF_USERNAME, DEFAULT_USERNAME), + } for conf in configs.values(): hass.async_create_task( @@ -64,7 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = all( + return all( await asyncio.gather( *[ hass.config_entries.async_forward_entry_unload(entry, component) @@ -73,8 +83,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) ) - return unload_ok - async def async_populate_options(hass, entry): """Populate default options for device.""" diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py index d5b153e27475ed..c3fe3b6d4b7711 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -125,21 +125,20 @@ async def async_step_device(self, user_input=None): discovery = await async_discovery(self.hass) for device in discovery: - configured = False - for entry in self._async_current_entries(): - if entry.unique_id == device[CONF_DEVICE_ID]: - configured = True - break + configured = any( + entry.unique_id == device[CONF_DEVICE_ID] + for entry in self._async_current_entries() + ) + if not configured: self.devices.append(device) LOGGER.debug("Discovered ONVIF devices %s", pformat(self.devices)) if self.devices: - names = [] - - for device in self.devices: - names.append(f"{device[CONF_NAME]} ({device[CONF_HOST]})") + names = [ + f"{device[CONF_NAME]} ({device[CONF_HOST]})" for device in self.devices + ] names.append(CONF_MANUAL_INPUT) @@ -299,7 +298,7 @@ def get_device(hass, host, port, username, password) -> ONVIFCamera: """Get ONVIFCamera instance.""" session = async_get_clientsession(hass) transport = AsyncTransport(None, session=session) - device = ONVIFCamera( + return ONVIFCamera( host, port, username, @@ -307,5 +306,3 @@ def get_device(hass, host, port, username, password) -> ONVIFCamera: f"{os.path.dirname(onvif.__file__)}/wsdl/", transport=transport, ) - - return device From ecdcfb835dc708aa8cd035adbe41dfb104203586 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Fri, 1 May 2020 21:00:44 +0200 Subject: [PATCH 51/58] Add yeelight meteorite (YLDL01YL, ceiling10) (#35018) --- homeassistant/components/yeelight/light.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 49315117e721c5..29f943906d6ab3 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -141,6 +141,7 @@ "ceiling2": BulbType.WhiteTemp, "ceiling3": BulbType.WhiteTemp, "ceiling4": BulbType.WhiteTempMood, + "ceiling10": BulbType.WhiteTempMood, "ceiling13": BulbType.WhiteTemp, } From dff2ee21562bd63f6938c3bd2adb4c44eb8b8167 Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 2 May 2020 01:31:39 +0200 Subject: [PATCH 52/58] Bump python-synology to 0.7.4 (#35052) --- homeassistant/components/synology_dsm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index bb3c5ce3aec848..4a538606ecba4d 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -2,7 +2,7 @@ "domain": "synology_dsm", "name": "Synology DSM", "documentation": "https://www.home-assistant.io/integrations/synology_dsm", - "requirements": ["python-synology==0.7.3"], + "requirements": ["python-synology==0.7.4"], "codeowners": ["@ProtoThis", "@Quentame"], "config_flow": true, "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 18c9f9ef407fab..ee8d53035c7389 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1692,7 +1692,7 @@ python-sochain-api==0.0.2 python-songpal==0.11.2 # homeassistant.components.synology_dsm -python-synology==0.7.3 +python-synology==0.7.4 # homeassistant.components.tado python-tado==0.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 30871402cd366b..fb7716dd38c20d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -662,7 +662,7 @@ python-miio==0.5.0.1 python-nest==4.1.0 # homeassistant.components.synology_dsm -python-synology==0.7.3 +python-synology==0.7.4 # homeassistant.components.tado python-tado==0.8.1 From f4f2aff5b69c5be44f6148964ccded2e600cd104 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 2 May 2020 00:04:57 +0000 Subject: [PATCH 53/58] [ci skip] Translation update --- .../components/airly/translations/ca.json | 2 +- .../airvisual/translations/es-419.json | 19 +++- .../almond/translations/es-419.json | 12 +- .../ambiclimate/translations/ca.json | 2 +- .../ambient_station/translations/es-419.json | 3 + .../components/atag/translations/es-419.json | 19 ++++ .../august/translations/es-419.json | 7 +- .../binary_sensor/translations/es-419.json | 4 +- .../components/braviatv/translations/ca.json | 2 +- .../braviatv/translations/es-419.json | 31 ++++++ .../brother/translations/es-419.json | 17 +++ .../cert_expiry/translations/es-419.json | 3 + .../deconz/translations/es-419.json | 5 + .../components/doorbird/translations/ca.json | 2 +- .../doorbird/translations/es-419.json | 30 +++++ .../ecobee/translations/es-419.json | 5 + .../components/esphome/translations/ca.json | 3 +- .../components/esphome/translations/cs.json | 21 +++- .../components/esphome/translations/fr.json | 3 +- .../components/esphome/translations/pl.json | 3 +- .../components/esphome/translations/ru.json | 3 +- .../esphome/translations/zh-Hant.json | 3 +- .../components/gios/translations/ca.json | 2 +- .../components/glances/translations/ca.json | 2 +- .../components/harmony/translations/ca.json | 4 +- .../components/homekit/translations/ca.json | 53 +++++++++ .../components/homekit/translations/cs.json | 3 + .../components/homekit/translations/en.json | 105 +++++++++--------- .../components/homekit/translations/fr.json | 53 +++++++++ .../components/homekit/translations/pl.json | 35 ++++++ .../components/homekit/translations/ru.json | 53 +++++++++ .../homekit/translations/zh-Hant.json | 53 +++++++++ .../homekit_controller/translations/ca.json | 2 +- .../huawei_lte/translations/ca.json | 2 +- .../translations/fr.json | 23 ++++ .../translations/pl.json | 24 ++++ .../components/icloud/translations/ca.json | 2 +- .../components/konnected/translations/ca.json | 2 +- .../components/life360/translations/ca.json | 2 +- .../components/linky/translations/ca.json | 2 +- .../logi_circle/translations/ca.json | 6 +- .../components/melcloud/translations/ca.json | 2 +- .../minecraft_server/translations/ca.json | 2 +- .../mobile_app/translations/ca.json | 2 +- .../components/nuheat/translations/ca.json | 2 +- .../components/onvif/translations/ca.json | 58 ++++++++++ .../components/onvif/translations/cs.json | 56 ++++++++++ .../components/onvif/translations/en.json | 3 +- .../components/onvif/translations/fr.json | 57 ++++++++++ .../components/onvif/translations/no.json | 12 ++ .../components/onvif/translations/pl.json | 58 ++++++++++ .../components/onvif/translations/ru.json | 58 ++++++++++ .../onvif/translations/zh-Hant.json | 58 ++++++++++ .../opentherm_gw/translations/ca.json | 2 +- .../components/plex/translations/ca.json | 2 +- .../components/powerwall/translations/ca.json | 2 +- .../pvpc_hourly_pricing/translations/ca.json | 2 +- .../components/rachio/translations/ca.json | 2 +- .../components/samsungtv/translations/hu.json | 6 +- .../smartthings/translations/ca.json | 2 +- .../components/solaredge/translations/ca.json | 4 +- .../components/starline/translations/ca.json | 2 +- .../synology_dsm/translations/ca.json | 2 +- .../totalconnect/translations/ca.json | 2 +- .../transmission/translations/ca.json | 2 +- .../transmission/translations/es-419.json | 11 ++ .../twentemilieu/translations/es-419.json | 4 + .../components/unifi/translations/ca.json | 12 +- .../components/unifi/translations/cs.json | 7 ++ .../components/unifi/translations/en.json | 6 +- .../components/unifi/translations/fr.json | 7 ++ .../components/unifi/translations/pl.json | 3 + .../components/unifi/translations/ru.json | 8 ++ .../unifi/translations/zh-Hant.json | 8 ++ .../components/vesync/translations/ca.json | 2 +- .../components/vilfo/translations/es-419.json | 9 ++ .../components/vizio/translations/ca.json | 2 +- .../components/vizio/translations/es-419.json | 24 ++++ .../components/withings/translations/ca.json | 2 +- .../xiaomi_miio/translations/cs.json | 29 +++++ 80 files changed, 1050 insertions(+), 109 deletions(-) create mode 100644 homeassistant/components/atag/translations/es-419.json create mode 100644 homeassistant/components/braviatv/translations/es-419.json create mode 100644 homeassistant/components/doorbird/translations/es-419.json create mode 100644 homeassistant/components/homekit/translations/ca.json create mode 100644 homeassistant/components/homekit/translations/cs.json create mode 100644 homeassistant/components/homekit/translations/fr.json create mode 100644 homeassistant/components/homekit/translations/pl.json create mode 100644 homeassistant/components/homekit/translations/ru.json create mode 100644 homeassistant/components/homekit/translations/zh-Hant.json create mode 100644 homeassistant/components/hunterdouglas_powerview/translations/fr.json create mode 100644 homeassistant/components/hunterdouglas_powerview/translations/pl.json create mode 100644 homeassistant/components/onvif/translations/ca.json create mode 100644 homeassistant/components/onvif/translations/cs.json create mode 100644 homeassistant/components/onvif/translations/fr.json create mode 100644 homeassistant/components/onvif/translations/no.json create mode 100644 homeassistant/components/onvif/translations/pl.json create mode 100644 homeassistant/components/onvif/translations/ru.json create mode 100644 homeassistant/components/onvif/translations/zh-Hant.json create mode 100644 homeassistant/components/transmission/translations/es-419.json create mode 100644 homeassistant/components/vilfo/translations/es-419.json create mode 100644 homeassistant/components/vizio/translations/es-419.json create mode 100644 homeassistant/components/xiaomi_miio/translations/cs.json diff --git a/homeassistant/components/airly/translations/ca.json b/homeassistant/components/airly/translations/ca.json index 76e8702e8fc20a..3caf870ccdf0a2 100644 --- a/homeassistant/components/airly/translations/ca.json +++ b/homeassistant/components/airly/translations/ca.json @@ -15,7 +15,7 @@ "longitude": "Longitud", "name": "Nom de la integraci\u00f3" }, - "description": "Configura una integraci\u00f3 de qualitat d\u2019aire Airly. Per generar la clau API, v\u00e9s a https://developer.airly.eu/register", + "description": "Configura una integraci\u00f3 de qualitat d'aire Airly. Per generar la clau API, v\u00e9s a https://developer.airly.eu/register", "title": "Airly" } } diff --git a/homeassistant/components/airvisual/translations/es-419.json b/homeassistant/components/airvisual/translations/es-419.json index ada76676ca9e39..aea8c1ad574031 100644 --- a/homeassistant/components/airvisual/translations/es-419.json +++ b/homeassistant/components/airvisual/translations/es-419.json @@ -7,12 +7,29 @@ "invalid_api_key": "Clave de API inv\u00e1lida" }, "step": { - "user": { + "geography": { "data": { "api_key": "Clave API", "latitude": "Latitud", "longitude": "Longitud" }, + "title": "Configurar una geograf\u00eda" + }, + "node_pro": { + "data": { + "password": "Contrase\u00f1a de la unidad" + }, + "title": "Configurar un AirVisual Node/Pro" + }, + "user": { + "data": { + "api_key": "Clave API", + "cloud_api": "Localizaci\u00f3n geogr\u00e1fica", + "latitude": "Latitud", + "longitude": "Longitud", + "node_pro": "AirVisual Node Pro", + "type": "Tipo de integraci\u00f3n" + }, "description": "Monitoree la calidad del aire en una ubicaci\u00f3n geogr\u00e1fica.", "title": "Configurar AirVisual" } diff --git a/homeassistant/components/almond/translations/es-419.json b/homeassistant/components/almond/translations/es-419.json index 7b7f7aea9ca52c..fbcf901c2e5db3 100644 --- a/homeassistant/components/almond/translations/es-419.json +++ b/homeassistant/components/almond/translations/es-419.json @@ -2,7 +2,17 @@ "config": { "abort": { "already_setup": "Solo puede configurar una cuenta Almond.", - "cannot_connect": "No se puede conectar con el servidor Almond." + "cannot_connect": "No se puede conectar con el servidor Almond.", + "missing_configuration": "Por favor, consulte la documentaci\u00f3n sobre c\u00f3mo configurar Almond." + }, + "step": { + "hassio_confirm": { + "description": "\u00bfDesea configurar Home Assistant para conectarse a Almond proporcionado por el complemento Hass.io: {addon}?", + "title": "Almond a trav\u00e9s del complemento Hass.io" + }, + "pick_implementation": { + "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" + } } } } \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/translations/ca.json b/homeassistant/components/ambiclimate/translations/ca.json index 14ebf481a36da0..0b8ca963813e32 100644 --- a/homeassistant/components/ambiclimate/translations/ca.json +++ b/homeassistant/components/ambiclimate/translations/ca.json @@ -2,7 +2,7 @@ "config": { "abort": { "access_token": "S'ha produ\u00eft un error desconegut al generat un token d'acc\u00e9s.", - "already_setup": "El compte d\u2019Ambi Climate est\u00e0 configurat.", + "already_setup": "El compte d'Ambi Climate est\u00e0 configurat.", "no_config": "Necessites configurar Ambiclimate abans de poder autenticar-t'hi. Llegeix les [instruccions](https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { diff --git a/homeassistant/components/ambient_station/translations/es-419.json b/homeassistant/components/ambient_station/translations/es-419.json index d2c60aee5a0f85..b16c5af9c6207a 100644 --- a/homeassistant/components/ambient_station/translations/es-419.json +++ b/homeassistant/components/ambient_station/translations/es-419.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Esta clave de aplicaci\u00f3n ya est\u00e1 en uso." + }, "error": { "invalid_key": "Clave de API y/o clave de aplicaci\u00f3n no v\u00e1lida", "no_devices": "No se han encontrado dispositivos en la cuenta." diff --git a/homeassistant/components/atag/translations/es-419.json b/homeassistant/components/atag/translations/es-419.json new file mode 100644 index 00000000000000..a833218e31137d --- /dev/null +++ b/homeassistant/components/atag/translations/es-419.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Solo se puede agregar un dispositivo Atag a Home Assistant" + }, + "error": { + "connection_error": "No se pudo conectar, intente nuevamente" + }, + "step": { + "user": { + "data": { + "port": "Puerto (10000)" + }, + "title": "Conectarse al dispositivo" + } + } + }, + "title": "Atag" +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/es-419.json b/homeassistant/components/august/translations/es-419.json index 0732c1c5e48b8f..914aea1b801945 100644 --- a/homeassistant/components/august/translations/es-419.json +++ b/homeassistant/components/august/translations/es-419.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada" + }, "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", "unknown": "Error inesperado" }, @@ -12,7 +16,8 @@ "timeout": "Tiempo de espera (segundos)", "username": "Nombre de usuario" }, - "description": "Si el M\u00e9todo de inicio de sesi\u00f3n es 'correo electr\u00f3nico', Nombre de usuario es la direcci\u00f3n de correo electr\u00f3nico. Si el M\u00e9todo de inicio de sesi\u00f3n es 'tel\u00e9fono', Nombre de usuario es el n\u00famero de tel\u00e9fono en el formato '+NNNNNNNNN'." + "description": "Si el M\u00e9todo de inicio de sesi\u00f3n es 'correo electr\u00f3nico', Nombre de usuario es la direcci\u00f3n de correo electr\u00f3nico. Si el M\u00e9todo de inicio de sesi\u00f3n es 'tel\u00e9fono', Nombre de usuario es el n\u00famero de tel\u00e9fono en el formato '+NNNNNNNNN'.", + "title": "Configurar una cuenta de August" }, "validation": { "data": { diff --git a/homeassistant/components/binary_sensor/translations/es-419.json b/homeassistant/components/binary_sensor/translations/es-419.json index 3954724934bb6f..5bada49741e19d 100644 --- a/homeassistant/components/binary_sensor/translations/es-419.json +++ b/homeassistant/components/binary_sensor/translations/es-419.json @@ -80,7 +80,9 @@ "smoke": "{entity_name} comenz\u00f3 a detectar humo", "sound": "{entity_name} comenz\u00f3 a detectar sonido", "turned_off": "{entity_name} apagado", - "turned_on": "{entity_name} encendido" + "turned_on": "{entity_name} encendido", + "unsafe": "{entity_name} se volvi\u00f3 inseguro", + "vibration": "{entity_name} comenz\u00f3 a detectar vibraciones" } }, "state": { diff --git a/homeassistant/components/braviatv/translations/ca.json b/homeassistant/components/braviatv/translations/ca.json index d92525ae325d09..5a6d50c5c53529 100644 --- a/homeassistant/components/braviatv/translations/ca.json +++ b/homeassistant/components/braviatv/translations/ca.json @@ -18,7 +18,7 @@ }, "user": { "data": { - "host": "Nom d\u2019amfitri\u00f3 o adre\u00e7a IP del televisor" + "host": "Nom d'amfitri\u00f3 o adre\u00e7a IP del televisor" }, "description": "Configura la integraci\u00f3 de televisor Sony Bravia. Si tens problemes durant la configuraci\u00f3, v\u00e9s a: https://www.home-assistant.io/integrations/braviatv\n\nAssegura't que el televisor estigui engegat.", "title": "Televisor Sony Bravia" diff --git a/homeassistant/components/braviatv/translations/es-419.json b/homeassistant/components/braviatv/translations/es-419.json new file mode 100644 index 00000000000000..48457826a5250c --- /dev/null +++ b/homeassistant/components/braviatv/translations/es-419.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Esta televisi\u00f3n ya est\u00e1 configurada." + }, + "error": { + "unsupported_model": "Su modelo de televisi\u00f3n no es compatible." + }, + "step": { + "authorize": { + "data": { + "pin": "C\u00f3digo PIN" + }, + "title": "Autorizar Sony Bravia TV" + }, + "user": { + "title": "Sony Bravia TV" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "ignored_sources": "Lista de fuentes ignoradas" + }, + "title": "Opciones para Sony Bravia TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/translations/es-419.json b/homeassistant/components/brother/translations/es-419.json index 337f5b624fbec8..0cb35449bc517d 100644 --- a/homeassistant/components/brother/translations/es-419.json +++ b/homeassistant/components/brother/translations/es-419.json @@ -7,6 +7,23 @@ "error": { "connection_error": "Error de conexi\u00f3n.", "snmp_error": "El servidor SNMP est\u00e1 apagado o la impresora no es compatible." + }, + "flow_title": "Impresora Brother: {model} {serial_number}", + "step": { + "user": { + "data": { + "type": "Tipo de impresora" + }, + "description": "Configure la integraci\u00f3n de la impresora Brother. Si tiene problemas con la configuraci\u00f3n, vaya a: https://www.home-assistant.io/integrations/brother", + "title": "Impresora Brother" + }, + "zeroconf_confirm": { + "data": { + "type": "Tipo de impresora" + }, + "description": "\u00bfDesea agregar la Impresora Brother {model} con el n\u00famero de serie `{serial_number}` a Home Assistant?", + "title": "Impresora Brother descubierta" + } } } } \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/translations/es-419.json b/homeassistant/components/cert_expiry/translations/es-419.json index ee5fc92391ff4c..772e37e25c8250 100644 --- a/homeassistant/components/cert_expiry/translations/es-419.json +++ b/homeassistant/components/cert_expiry/translations/es-419.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "import_failed": "La importaci\u00f3n desde la configuraci\u00f3n fall\u00f3" + }, "error": { "connection_timeout": "Tiempo de espera al conectarse a este host", "resolve_failed": "Este host no puede resolverse" diff --git a/homeassistant/components/deconz/translations/es-419.json b/homeassistant/components/deconz/translations/es-419.json index 9d867b0c8e78f9..8208e2578b0315 100644 --- a/homeassistant/components/deconz/translations/es-419.json +++ b/homeassistant/components/deconz/translations/es-419.json @@ -28,6 +28,11 @@ "host": "Host", "port": "Puerto" } + }, + "manual_input": { + "data": { + "port": "Puerto" + } } } }, diff --git a/homeassistant/components/doorbird/translations/ca.json b/homeassistant/components/doorbird/translations/ca.json index 9bd469ee4e1d06..2e87939f44e90c 100644 --- a/homeassistant/components/doorbird/translations/ca.json +++ b/homeassistant/components/doorbird/translations/ca.json @@ -29,7 +29,7 @@ "data": { "events": "Llista d'esdeveniments separats per comes." }, - "description": "Afegeix el/s noms del/s esdeveniment/s que vulguis seguir separats per comes. Despr\u00e9s d\u2019introduir-los, utilitzeu l\u2019aplicaci\u00f3 de DoorBird per assignar-los a un esdeveniment espec\u00edfic. Consulta la documentaci\u00f3 a https://www.home-assistant.io/integrations/doorbird/#events.\nExemple: algu_ha_premut_el_boto, moviment_detectat" + "description": "Afegeix el/s noms del/s esdeveniment/s que vulguis seguir separats per comes. Despr\u00e9s d'introduir-los, utilitzeu l'aplicaci\u00f3 de DoorBird per assignar-los a un esdeveniment espec\u00edfic. Consulta la documentaci\u00f3 a https://www.home-assistant.io/integrations/doorbird/#events.\nExemple: algu_ha_premut_el_boto, moviment_detectat" } } } diff --git a/homeassistant/components/doorbird/translations/es-419.json b/homeassistant/components/doorbird/translations/es-419.json new file mode 100644 index 00000000000000..1a412b38246e21 --- /dev/null +++ b/homeassistant/components/doorbird/translations/es-419.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "not_doorbird_device": "Este dispositivo no es un DoorBird" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "name": "Nombre del dispositivo", + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "events": "Lista de eventos separados por comas." + }, + "description": "Agregue un nombre de evento separado por comas para cada evento que desee rastrear. Despu\u00e9s de ingresarlos aqu\u00ed, use la aplicaci\u00f3n DoorBird para asignarlos a un evento espec\u00edfico. Consulte la documentaci\u00f3n en https://www.home-assistant.io/integrations/doorbird/#events. Ejemplo: somebody_pressed_the_button, motion" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/translations/es-419.json b/homeassistant/components/ecobee/translations/es-419.json index 3e19977f10f99e..ff9c1f53decaaa 100644 --- a/homeassistant/components/ecobee/translations/es-419.json +++ b/homeassistant/components/ecobee/translations/es-419.json @@ -10,6 +10,11 @@ "step": { "authorize": { "description": "Autorice esta aplicaci\u00f3n en https://www.ecobee.com/consumerportal/index.html con c\u00f3digo PIN: \n\n {pin} \n \n Luego, presione Enviar." + }, + "user": { + "data": { + "api_key": "Clave API" + } } } } diff --git a/homeassistant/components/esphome/translations/ca.json b/homeassistant/components/esphome/translations/ca.json index 6cd1405b824dec..9f9378081dca94 100644 --- a/homeassistant/components/esphome/translations/ca.json +++ b/homeassistant/components/esphome/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "ESP ja est\u00e0 configurat" + "already_configured": "ESP ja est\u00e0 configurat", + "already_in_progress": "La configuraci\u00f3 de l'ESP ja est\u00e0 en curs" }, "error": { "connection_error": "No s'ha pogut connectar amb ESP. Verifica que l'arxiu YAML cont\u00e9 la l\u00ednia 'api:'.", diff --git a/homeassistant/components/esphome/translations/cs.json b/homeassistant/components/esphome/translations/cs.json index b8c245a66e47b6..36a600befaa2d8 100644 --- a/homeassistant/components/esphome/translations/cs.json +++ b/homeassistant/components/esphome/translations/cs.json @@ -1,14 +1,33 @@ { "config": { + "abort": { + "already_configured": "Tento ESP uzel je ji\u017e nakonfigurov\u00e1n", + "already_in_progress": "Konfigurace uzlu ESP ji\u017e prob\u00edh\u00e1" + }, + "error": { + "connection_error": "Nelze se p\u0159ipojit k ESP. Zkontrolujte, zda va\u0161e YAML konfigurace obsahuje \u0159\u00e1dek 'api:'.", + "invalid_password": "Neplatn\u00e9 heslo", + "resolve_error": "Nelze naj\u00edt IP adresu uzlu ESP. Pokud tato chyba p\u0159etrv\u00e1v\u00e1, nastavte statickou adresu IP: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + }, + "flow_title": "ESPHome: {name}", "step": { "authenticate": { - "description": "Zadejte heslo, kter\u00e9 jste nastavili ve va\u0161\u00ed konfiguraci pro {name} ." + "data": { + "password": "Heslo" + }, + "description": "Zadejte heslo, kter\u00e9 jste nastavili ve va\u0161\u00ed konfiguraci pro {name} .", + "title": "Zadejte heslo" }, "discovery_confirm": { "description": "Chcete do domovsk\u00e9ho asistenta p\u0159idat uzel ESPHome `{name}`?", "title": "Nalezen uzel ESPHome" }, "user": { + "data": { + "host": "Adresa uzlu", + "port": "Port" + }, + "description": "Zadejte pros\u00edm nastaven\u00ed p\u0159ipojen\u00ed va\u0161eho [ESPHome](https://esphomelib.com/) uzlu.", "title": "[%key:component::esphome::title%]" } } diff --git a/homeassistant/components/esphome/translations/fr.json b/homeassistant/components/esphome/translations/fr.json index 8159b3cac2e927..a6620218f0ecbd 100644 --- a/homeassistant/components/esphome/translations/fr.json +++ b/homeassistant/components/esphome/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "ESP est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "ESP est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration ESP est d\u00e9j\u00e0 en cours" }, "error": { "connection_error": "Impossible de se connecter \u00e0 ESP. Assurez-vous que votre fichier YAML contient une ligne 'api:'.", diff --git a/homeassistant/components/esphome/translations/pl.json b/homeassistant/components/esphome/translations/pl.json index c74b3d57e939ce..276a0b404ddf2f 100644 --- a/homeassistant/components/esphome/translations/pl.json +++ b/homeassistant/components/esphome/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "ESP jest ju\u017c skonfigurowane." + "already_configured": "ESP jest ju\u017c skonfigurowane.", + "already_in_progress": "Konfiguracja ESP jest ju\u017c w toku." }, "error": { "connection_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z ESP. Upewnij si\u0119, \u017ce Tw\u00f3j plik YAML zawiera lini\u0119 'api:'.", diff --git a/homeassistant/components/esphome/translations/ru.json b/homeassistant/components/esphome/translations/ru.json index 8361fa371a453e..d9407b1c20e0a8 100644 --- a/homeassistant/components/esphome/translations/ru.json +++ b/homeassistant/components/esphome/translations/ru.json @@ -1,7 +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." + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\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": { "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a ESP. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 YAML-\u0444\u0430\u0439\u043b \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 'api:'.", diff --git a/homeassistant/components/esphome/translations/zh-Hant.json b/homeassistant/components/esphome/translations/zh-Hant.json index 7495204945dd3b..3657af88ce9641 100644 --- a/homeassistant/components/esphome/translations/zh-Hant.json +++ b/homeassistant/components/esphome/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "ESP \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "ESP \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "ESP \u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002" }, "error": { "connection_error": "\u7121\u6cd5\u9023\u7dda\u81f3 ESP\uff0c\u8acb\u78ba\u5b9a\u60a8\u7684 YAML \u6a94\u6848\u5305\u542b\u300capi:\u300d\u8a2d\u5b9a\u5217\u3002", diff --git a/homeassistant/components/gios/translations/ca.json b/homeassistant/components/gios/translations/ca.json index 1e451432be7057..29703281b087ce 100644 --- a/homeassistant/components/gios/translations/ca.json +++ b/homeassistant/components/gios/translations/ca.json @@ -14,7 +14,7 @@ "name": "Nom de la integraci\u00f3", "station_id": "ID de l'estaci\u00f3 de mesura" }, - "description": "Integraci\u00f3 de mesura de qualitat de l\u2019aire GIO\u015a (Polish Chief Inspectorate Of Environmental Protection). Si necessites ajuda amb la configuraci\u00f3, fes un cop d'ull a: https://www.home-assistant.io/integrations/gios", + "description": "Integraci\u00f3 de mesura de qualitat de l'aire GIO\u015a (Polish Chief Inspectorate Of Environmental Protection). Si necessites ajuda amb la configuraci\u00f3, fes un cop d'ull a: https://www.home-assistant.io/integrations/gios", "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" } } diff --git a/homeassistant/components/glances/translations/ca.json b/homeassistant/components/glances/translations/ca.json index dd9d151296f3f2..7da63024f8b39c 100644 --- a/homeassistant/components/glances/translations/ca.json +++ b/homeassistant/components/glances/translations/ca.json @@ -27,7 +27,7 @@ "step": { "init": { "data": { - "scan_interval": "Freq\u00fc\u00e8ncia d\u2019actualitzaci\u00f3" + "scan_interval": "Freq\u00fc\u00e8ncia d'actualitzaci\u00f3" }, "description": "Opcions de configuraci\u00f3 de Glances" } diff --git a/homeassistant/components/harmony/translations/ca.json b/homeassistant/components/harmony/translations/ca.json index a7c520c400c413..90d8f0643015dd 100644 --- a/homeassistant/components/harmony/translations/ca.json +++ b/homeassistant/components/harmony/translations/ca.json @@ -26,8 +26,8 @@ "step": { "init": { "data": { - "activity": "Activitat predeterminada a executar quan no se n\u2019especifica cap.", - "delay_secs": "Retard entre l\u2019enviament d\u2019ordres." + "activity": "Activitat predeterminada a executar quan no se n'especifica cap.", + "delay_secs": "Retard entre l'enviament d'ordres." }, "description": "Ajusta les opcions de Harmony Hub" } diff --git a/homeassistant/components/homekit/translations/ca.json b/homeassistant/components/homekit/translations/ca.json new file mode 100644 index 00000000000000..63cf68c99d1f8c --- /dev/null +++ b/homeassistant/components/homekit/translations/ca.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "port_name_in_use": "Ja hi ha un enlla\u00e7 configurat amb aquest nom o port." + }, + "step": { + "pairing": { + "description": "Tan aviat com l'enlla\u00e7 {name} estigui llest, rebr\u00e0s una \"Notificaci\u00f3\" de configuraci\u00f3 de l'enlla\u00e7 HomeKit informan-te de que la vinculaci\u00f3 est\u00e0 disponible.", + "title": "Vinculaci\u00f3 de l'enlla\u00e7 HomeKit" + }, + "user": { + "data": { + "auto_start": "Autoarrencada (desactiva-ho si fas servir Z-Wave o algun altre sistema d'inici lent)", + "include_domains": "Dominis a incloure" + }, + "description": "L'enlla\u00e7 HomeKit et permet accedir a les teves entitats de Home Assistant directament a HomeKit. Aquests enll\u00e7os estan limitats a un m\u00e0xim de 150 accessoris per inst\u00e0ncia (incl\u00f2s el propi enlla\u00e7). Si volguessis enlla\u00e7ar m\u00e9s accessoris, \u00e9s recomanable que utilitzis diferents enlla\u00e7os HomeKit per a dominis diferents. La configuraci\u00f3 avan\u00e7ada d'entitat per l'enlla\u00e7 prinipal nom\u00e9s est\u00e0 disponible amb YAML.", + "title": "Activaci\u00f3 de l'enlla\u00e7 HomeKit" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "auto_start": "[%key::component::homekit::config::step::user::data::auto_start%]", + "safe_mode": "Mode segur (habilita-ho nom\u00e9s si falla la vinculaci\u00f3)", + "zeroconf_default_interface": "Utilitza la interf\u00edcie zeroconf predeterminada. Activa-ho si no es pot trobar l'enlla\u00e7 a l'aplicaci\u00f3 Casa (Home app)." + }, + "description": "Aquests par\u00e0metres nom\u00e9s s'han d'ajustar si l'enlla\u00e7 HomeKit no \u00e9s funcional.", + "title": "Configuraci\u00f3 avan\u00e7ada" + }, + "exclude": { + "data": { + "exclude_entities": "Entitats a excloure" + }, + "description": "Selecciona les entitats que NO vulguis que siguin enlla\u00e7ades.", + "title": "Exclusi\u00f3 d'entitats de l'enlla\u00e7 en dominis seleccionats" + }, + "init": { + "data": { + "include_domains": "[%key::component::homekit::config::step::user::data::include_domains%]" + }, + "description": "Les entitats a \"Dominis a incloure\" s'enlla\u00e7aran a HomeKit. A la seg\u00fcent pantalla podr\u00e0s seleccionar quines entitats vols excloure d'aquesta llista.", + "title": "Selecci\u00f3 dels dominis a enlla\u00e7ar." + }, + "yaml": { + "description": "Aquesta entrada es controla en YAML", + "title": "Ajusta les opcions de l'enlla\u00e7 HomeKit" + } + } + }, + "title": "Enlla\u00e7 HomeKit" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/cs.json b/homeassistant/components/homekit/translations/cs.json new file mode 100644 index 00000000000000..3e96cd44af8446 --- /dev/null +++ b/homeassistant/components/homekit/translations/cs.json @@ -0,0 +1,3 @@ +{ + "title": "HomeKit Bridge" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/en.json b/homeassistant/components/homekit/translations/en.json index ca5a67f536360f..8d43341c61b070 100644 --- a/homeassistant/components/homekit/translations/en.json +++ b/homeassistant/components/homekit/translations/en.json @@ -1,54 +1,53 @@ { - "title" : "HomeKit Bridge", - "options" : { - "step" : { - "yaml" : { - "title" : "Adjust HomeKit Bridge Options", - "description" : "This entry is controlled via YAML" - }, - "init" : { - "data" : { - "include_domains" : "[%key:component::homekit::config::step::user::data::include_domains%]" - }, - "description" : "Entities in the “Domains to include” will be bridged to HomeKit. You will be able to select which entities to exclude from this list on the next screen.", - "title" : "Select domains to bridge." - }, - "exclude" : { - "data" : { - "exclude_entities" : "Entities to exclude" - }, - "description" : "Choose the entities that you do NOT want to be bridged.", - "title" : "Exclude entities in selected domains from bridge" - }, - "advanced" : { - "data" : { - "auto_start" : "[%key:component::homekit::config::step::user::data::auto_start%]", - "safe_mode" : "Safe Mode (enable only if pairing fails)", - "zeroconf_default_interface" : "Use default zeroconf interface (enable if the bridge cannot be found in the Home app)" - }, - "description" : "These settings only need to be adjusted if the HomeKit bridge is not functional.", - "title" : "Advanced Configuration" - } - } - }, - "config" : { - "step" : { - "user" : { - "data" : { - "auto_start" : "Autostart (disable if using Z-Wave or other delayed start system)", - "include_domains" : "Domains to include" - }, - "description" : "A HomeKit Bridge will allow you to access your Home Assistant entities in HomeKit. HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML for the primary bridge.", - "title" : "Activate HomeKit Bridge" - }, - "pairing": { - "title": "Pair HomeKit Bridge", - "description": "As soon as the {name} bridge is ready, pairing will be available in “Notifications” as “HomeKit Bridge Setup”." - } - }, - "abort" : { - "port_name_in_use" : "A bridge with the same name or port is already configured." - } - } - } - \ No newline at end of file + "config": { + "abort": { + "port_name_in_use": "A bridge with the same name or port is already configured." + }, + "step": { + "pairing": { + "description": "As soon as the {name} bridge is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d.", + "title": "Pair HomeKit Bridge" + }, + "user": { + "data": { + "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)", + "include_domains": "Domains to include" + }, + "description": "A HomeKit Bridge will allow you to access your Home Assistant entities in HomeKit. HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML for the primary bridge.", + "title": "Activate HomeKit Bridge" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)", + "safe_mode": "Safe Mode (enable only if pairing fails)", + "zeroconf_default_interface": "Use default zeroconf interface (enable if the bridge cannot be found in the Home app)" + }, + "description": "These settings only need to be adjusted if the HomeKit bridge is not functional.", + "title": "Advanced Configuration" + }, + "exclude": { + "data": { + "exclude_entities": "Entities to exclude" + }, + "description": "Choose the entities that you do NOT want to be bridged.", + "title": "Exclude entities in selected domains from bridge" + }, + "init": { + "data": { + "include_domains": "Domains to include" + }, + "description": "Entities in the \u201cDomains to include\u201d will be bridged to HomeKit. You will be able to select which entities to exclude from this list on the next screen.", + "title": "Select domains to bridge." + }, + "yaml": { + "description": "This entry is controlled via YAML", + "title": "Adjust HomeKit Bridge Options" + } + } + }, + "title": "HomeKit Bridge" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/fr.json b/homeassistant/components/homekit/translations/fr.json new file mode 100644 index 00000000000000..4e746d4c15cbbe --- /dev/null +++ b/homeassistant/components/homekit/translations/fr.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "port_name_in_use": "Une passerelle avec le m\u00eame nom ou port est d\u00e9j\u00e0 configur\u00e9e." + }, + "step": { + "pairing": { + "description": "D\u00e8s que le pont {name} est pr\u00eat, l'appairage sera disponible dans \"Notifications\" sous \"Configuration de la Passerelle HomeKit\".", + "title": "Appairage de la Passerelle Homekit" + }, + "user": { + "data": { + "auto_start": "D\u00e9marrage automatique (d\u00e9sactiver si vous utilisez Z-Wave ou un autre syst\u00e8me de d\u00e9marrage diff\u00e9r\u00e9)", + "include_domains": "Domaines \u00e0 inclure" + }, + "description": "La passerelle HomeKit vous permettra d'acc\u00e9der \u00e0 vos entit\u00e9s Home Assistant dans HomeKit. Les passerelles HomeKit sont limit\u00e9es \u00e0 150 accessoires par instance, y compris la passerelle elle-m\u00eame. Si vous souhaitez connecter plus que le nombre maximum d'accessoires, il est recommand\u00e9 d'utiliser plusieurs passerelles HomeKit pour diff\u00e9rents domaines. La configuration d\u00e9taill\u00e9e des entit\u00e9s est uniquement disponible via YAML pour la passerelle principale.", + "title": "Activer la Passerelle HomeKit" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "auto_start": "D\u00e9marrage automatique (d\u00e9sactiver si vous utilisez Z-Wave ou un autre syst\u00e8me de d\u00e9marrage diff\u00e9r\u00e9)", + "safe_mode": "Mode sans \u00e9chec (activez uniquement si le jumelage \u00e9choue)", + "zeroconf_default_interface": "Utiliser l'interface zeroconf par d\u00e9faut (activer si le pont est introuvable dans l'application Home)" + }, + "description": "Ces param\u00e8tres ne doivent \u00eatre ajust\u00e9s que si le pont HomeKit n'est pas fonctionnel.", + "title": "Configuration avanc\u00e9e" + }, + "exclude": { + "data": { + "exclude_entities": "Entit\u00e9s \u00e0 exclure" + }, + "description": "Choisissez les entit\u00e9s que vous ne souhaitez PAS voir reli\u00e9es.", + "title": "Exclure les entit\u00e9s des domaines s\u00e9lectionn\u00e9s de la passerelle" + }, + "init": { + "data": { + "include_domains": "Domaine \u00e0 inclure" + }, + "description": "Les entit\u00e9s des \u00abdomaines \u00e0 inclure\u00bb seront pont\u00e9es vers HomeKit. Vous pourrez s\u00e9lectionner les entit\u00e9s \u00e0 exclure de cette liste sur l'\u00e9cran suivant.", + "title": "S\u00e9lectionnez les domaines \u00e0 relier." + }, + "yaml": { + "description": "Cette entr\u00e9e est contr\u00f4l\u00e9e via YAML", + "title": "Ajuster les options de la passerelle HomeKit" + } + } + }, + "title": "Passerelle HomeKit" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/pl.json b/homeassistant/components/homekit/translations/pl.json new file mode 100644 index 00000000000000..6aacc0deed65d3 --- /dev/null +++ b/homeassistant/components/homekit/translations/pl.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "port_name_in_use": "Mostek o okre\u015blonej nazwie lub adresie IP jest ju\u017c skonfigurowany." + }, + "step": { + "pairing": { + "title": "Sparuj z mostkiem HomeKit" + }, + "user": { + "data": { + "include_domains": "Domeny do uwzgl\u0119dnienia" + }, + "title": "Aktywowanie mostka HomeKit" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "safe_mode": "Tryb awaryjny (w\u0142\u0105cz tylko wtedy, gdy parowanie nie powiedzie si\u0119)" + } + }, + "init": { + "title": "Domeny do uwzgl\u0119dnienia." + }, + "yaml": { + "description": "Ten wpis jest kontrolowany przez YAML", + "title": "Dostosowywanie opcji mostka HomeKit" + } + } + }, + "title": "Mostek HomeKit" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/ru.json b/homeassistant/components/homekit/translations/ru.json new file mode 100644 index 00000000000000..00bd865ad9e710 --- /dev/null +++ b/homeassistant/components/homekit/translations/ru.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "port_name_in_use": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c \u0436\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c \u0438\u043b\u0438 \u043f\u043e\u0440\u0442\u043e\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "step": { + "pairing": { + "description": "\u041a\u0430\u043a \u0442\u043e\u043b\u044c\u043a\u043e \u0431\u0440\u0438\u0434\u0436 {name} \u0431\u0443\u0434\u0435\u0442 \u0433\u043e\u0442\u043e\u0432, \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e \u0432 \"\u0423\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f\u0445\" \u043a\u0430\u043a \"HomeKit Bridge Setup\".", + "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 HomeKit Bridge" + }, + "user": { + "data": { + "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 Z-Wave \u0438\u043b\u0438 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u044b \u043e\u0442\u043b\u043e\u0436\u0435\u043d\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0430)", + "include_domains": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u044b" + }, + "description": "HomeKit Bridge \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442 \u0432\u0430\u043c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u043e\u0431\u044a\u0435\u043a\u0442\u0430\u043c Home Assistant \u0432 HomeKit. HomeKit Bridge \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d 150 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430\u043c\u0438 \u043d\u0430 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440, \u0432\u043a\u043b\u044e\u0447\u0430\u044f \u0441\u0430\u043c \u043c\u043e\u0441\u0442. \u0415\u0441\u043b\u0438 \u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0431\u043e\u043b\u044c\u0448\u0435, \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e HomeKit Bridge \u0434\u043b\u044f \u0440\u0430\u0437\u043d\u044b\u0445 \u0434\u043e\u043c\u0435\u043d\u043e\u0432. \u0414\u0435\u0442\u0430\u043b\u044c\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 YAML \u0434\u043b\u044f \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0433\u043e \u0431\u0440\u0438\u0434\u0436\u0430.", + "title": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c HomeKit Bridge" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 Z-Wave \u0438\u043b\u0438 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u044b \u043e\u0442\u043b\u043e\u0436\u0435\u043d\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0430)", + "safe_mode": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c (\u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u0441\u0431\u043e\u044f \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f)", + "zeroconf_default_interface": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c zeroconf \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e (\u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0435, \u0435\u0441\u043b\u0438 \u0431\u0440\u0438\u0434\u0436 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043d\u0430\u0439\u0434\u0435\u043d \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438 '\u0414\u043e\u043c')." + }, + "description": "\u042d\u0442\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b, \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 HomeKit Bridge \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442.", + "title": "\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430" + }, + "exclude": { + "data": { + "exclude_entities": "\u0418\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442\u044b" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u041d\u0415 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432 HomeKit.", + "title": "\u0418\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0432 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0445 \u0434\u043e\u043c\u0435\u043d\u0430\u0445" + }, + "init": { + "data": { + "include_domains": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u044b" + }, + "description": "\u041e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u043c \u0434\u043e\u043c\u0435\u043d\u0430\u043c, \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432 HomeKit. \u041d\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u043c \u044d\u0442\u0430\u043f\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0412\u044b \u0441\u043c\u043e\u0436\u0435\u0442\u0435 \u0432\u044b\u0431\u0440\u0430\u0442\u044c, \u043a\u0430\u043a\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0438\u0437 \u044d\u0442\u0438\u0445 \u0434\u043e\u043c\u0435\u043d\u043e\u0432.", + "title": "\u0412\u044b\u0431\u043e\u0440 \u0434\u043e\u043c\u0435\u043d\u043e\u0432 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit" + }, + "yaml": { + "description": "\u042d\u0442\u0430 \u0437\u0430\u043f\u0438\u0441\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430 \u0447\u0435\u0440\u0435\u0437 YAML", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 HomeKit Bridge" + } + } + }, + "title": "HomeKit Bridge" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/zh-Hant.json b/homeassistant/components/homekit/translations/zh-Hant.json new file mode 100644 index 00000000000000..ea53860a91af8f --- /dev/null +++ b/homeassistant/components/homekit/translations/zh-Hant.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "port_name_in_use": "\u4f7f\u7528\u76f8\u540c\u540d\u7a31\u6216\u901a\u8a0a\u57e0\u7684 Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + }, + "step": { + "pairing": { + "description": "\u65bc {name} bridge \u5c31\u7dd2\u5f8c\u3001\u5c07\u6703\u65bc\u300c\u901a\u77e5\u300d\u4e2d\u986f\u793a\u300cHomeKit Bridge \u8a2d\u5b9a\u300d\u7684\u914d\u5c0d\u8cc7\u8a0a\u3002", + "title": "\u914d\u5c0d HomeKit Bridge" + }, + "user": { + "data": { + "auto_start": "\u81ea\u52d5\u555f\u52d5\uff08\u5047\u5982\u4f7f\u7528 Z-Wave \u6216\u5176\u4ed6\u5ef6\u9072\u555f\u52d5\u7cfb\u7d71\u6642\u3001\u8acb\u95dc\u9589\uff09", + "include_domains": "\u5305\u542b Domain" + }, + "description": "HomeKit Bridge \u5c07\u53ef\u5141\u8a31\u65bc Homekit \u4e2d\u4f7f\u7528 Home Assistant \u7269\u4ef6\u3002HomeKit Bridges \u6700\u9ad8\u9650\u5236\u70ba 150 \u500b\u914d\u4ef6\u3001\u5305\u542b Bridge \u672c\u8eab\u3002\u5047\u5982\u60f3\u8981\u4f7f\u7528\u8d85\u904e\u9650\u5236\u4ee5\u4e0a\u7684\u914d\u4ef6\uff0c\u5efa\u8b70\u53ef\u4ee5\u4e0d\u540c Domain \u4f7f\u7528\u591a\u500b HomeKit bridges \u9054\u5230\u6b64\u9700\u6c42\u3002\u50c5\u80fd\u65bc\u4e3b Bridge \u4ee5 YAML \u8a2d\u5b9a\u8a73\u7d30\u7269\u4ef6\u3002", + "title": "\u555f\u7528 HomeKit Bridge" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "auto_start": "\u81ea\u52d5\u555f\u52d5\uff08\u5047\u5982\u4f7f\u7528 Z-Wave \u6216\u5176\u4ed6\u5ef6\u9072\u555f\u52d5\u7cfb\u7d71\u6642\u3001\u8acb\u95dc\u9589\uff09", + "safe_mode": "\u5b89\u5168\u6a21\u5f0f\uff08\u50c5\u65bc\u914d\u5c0d\u5931\u6557\u6642\u4f7f\u7528\uff09", + "zeroconf_default_interface": "\u4f7f\u7528\u9810\u8a2d zeroconf \u4ecb\u9762\uff08\u50c5\u65bc\u5bb6\u5ead App \u627e\u4e0d\u5230 Bridge \u6642\u958b\u555f\uff09" + }, + "description": "\u50c5\u65bc Homekit bridge \u7121\u6cd5\u6b63\u5e38\u4f7f\u7528\u6642\uff0c\u8abf\u6574\u6b64\u4e9b\u8a2d\u5b9a\u3002", + "title": "\u9032\u968e\u8a2d\u5b9a" + }, + "exclude": { + "data": { + "exclude_entities": "\u6392\u9664\u7269\u4ef6" + }, + "description": "\u9078\u64c7\u4e0d\u9032\u884c\u6a4b\u63a5\u7684\u7269\u4ef6\u3002", + "title": "\u65bc\u6240\u9078 Domain \u4e2d\u6240\u8981\u6392\u9664\u7684\u7269\u4ef6" + }, + "init": { + "data": { + "include_domains": "\u5305\u542b Domain" + }, + "description": "\u300c\u5305\u542b Domain\u300d\u4e2d\u7684\u7269\u4ef6\u5c07\u6703\u6a4b\u63a5\u81f3 Homekit\u3001\u53ef\u4ee5\u65bc\u4e0b\u4e00\u500b\u756b\u9762\u4e2d\u9078\u64c7\u6240\u8981\u5305\u542b\u6216\u6392\u9664\u7684\u7269\u4ef6\u5217\u8868\u3002", + "title": "\u9078\u64c7\u6240\u8981\u6a4b\u63a5\u7684 Domain\u3002" + }, + "yaml": { + "description": "\u6b64\u7269\u4ef6\u70ba\u900f\u904e YAML \u63a7\u5236", + "title": "\u8abf\u6574 HomeKit Bridge \u9078\u9805" + } + } + }, + "title": "HomeKit Bridge" +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/ca.json b/homeassistant/components/homekit_controller/translations/ca.json index db174f049b8938..3407d93c63fef3 100644 --- a/homeassistant/components/homekit_controller/translations/ca.json +++ b/homeassistant/components/homekit_controller/translations/ca.json @@ -13,7 +13,7 @@ "authentication_error": "Codi HomeKit incorrecte. Verifica'l i torna-ho a provar.", "busy_error": "El dispositiu ha refusat la vinculaci\u00f3 perqu\u00e8 actualment ho est\u00e0 intentant amb un altre controlador diferent.", "max_peers_error": "El dispositiu ha refusat la vinculaci\u00f3 perqu\u00e8 no t\u00e9 suficient espai lliure.", - "max_tries_error": "El dispositiu ha refusat la vinculaci\u00f3 perqu\u00e8 ha rebut m\u00e9s de 100 intents d\u2019autenticaci\u00f3 fallits.", + "max_tries_error": "El dispositiu ha refusat la vinculaci\u00f3 perqu\u00e8 ha rebut m\u00e9s de 100 intents d'autenticaci\u00f3 fallits.", "pairing_failed": "S'ha produ\u00eft un error mentre s'intentava la vinculaci\u00f3 amb el dispositiu. Pot ser que sigui un error temporal o pot ser que el teu dispositiu encara no estigui suportat.", "unable_to_pair": "No s'ha pogut vincular, torna-ho a provar.", "unknown_error": "El dispositiu ha em\u00e8s un error desconegut. Vinculaci\u00f3 fallida." diff --git a/homeassistant/components/huawei_lte/translations/ca.json b/homeassistant/components/huawei_lte/translations/ca.json index 762132a459a697..cb8a4331a57878 100644 --- a/homeassistant/components/huawei_lte/translations/ca.json +++ b/homeassistant/components/huawei_lte/translations/ca.json @@ -23,7 +23,7 @@ "url": "URL", "username": "Nom d'usuari" }, - "description": "Introdueix les dades d\u2019acc\u00e9s del dispositiu. El nom d\u2019usuari i contrasenya s\u00f3n opcionals, per\u00f2 habiliten m\u00e9s funcions de la integraci\u00f3. D'altra banda, (mentre la integraci\u00f3 estigui activa) l'\u00fas d'una connexi\u00f3 autoritzada pot causar problemes per accedir a la interf\u00edcie web del dispositiu des de fora de Home Assistant i viceversa.", + "description": "Introdueix les dades d'acc\u00e9s del dispositiu. El nom d'usuari i contrasenya s\u00f3n opcionals, per\u00f2 habiliten m\u00e9s funcions de la integraci\u00f3. D'altra banda, (mentre la integraci\u00f3 estigui activa) l'\u00fas d'una connexi\u00f3 autoritzada pot causar problemes per accedir a la interf\u00edcie web del dispositiu des de fora de Home Assistant i viceversa.", "title": "Configuraci\u00f3 de Huawei LTE" } } diff --git a/homeassistant/components/hunterdouglas_powerview/translations/fr.json b/homeassistant/components/hunterdouglas_powerview/translations/fr.json new file mode 100644 index 00000000000000..a1bd06078c6e15 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", + "unknown": "Erreur inattendue" + }, + "step": { + "link": { + "description": "Voulez-vous configurer {name} ({host})?", + "title": "Connectez-vous au concentrateur PowerView" + }, + "user": { + "data": { + "host": "Adresse IP" + }, + "title": "Connectez-vous au concentrateur PowerView" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/pl.json b/homeassistant/components/hunterdouglas_powerview/translations/pl.json new file mode 100644 index 00000000000000..7e535d0d27045c --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/pl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", + "unknown": "Nieoczekiwany b\u0142\u0105d." + }, + "step": { + "link": { + "description": "Czy chcesz skonfigurowa\u0107 {name} ({host})?", + "title": "Po\u0142\u0105cz si\u0119 z hubem PowerView" + }, + "user": { + "data": { + "host": "Adres IP" + }, + "title": "Po\u0142\u0105cz si\u0119 z hubem PowerView" + } + } + }, + "title": "Hunter Douglas PowerView" +} \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/ca.json b/homeassistant/components/icloud/translations/ca.json index ade0c4a3bd3b55..5fa4ee6626df3e 100644 --- a/homeassistant/components/icloud/translations/ca.json +++ b/homeassistant/components/icloud/translations/ca.json @@ -5,7 +5,7 @@ "no_device": "Cap dels teus dispositius t\u00e9 activada la opci\u00f3 \"Troba el meu iPhone\"" }, "error": { - "login": "Error d\u2019inici de sessi\u00f3: comprova el correu electr\u00f2nic i la contrasenya", + "login": "Error d'inici de sessi\u00f3: comprova el correu electr\u00f2nic i la contrasenya", "send_verification_code": "No s'ha pogut enviar el codi de verificaci\u00f3", "validate_verification_code": "No s'ha pogut verificar el codi de verificaci\u00f3, tria un dispositiu de confian\u00e7a i torna a iniciar el proc\u00e9s" }, diff --git a/homeassistant/components/konnected/translations/ca.json b/homeassistant/components/konnected/translations/ca.json index afe65f67f627f1..d35146410edb47 100644 --- a/homeassistant/components/konnected/translations/ca.json +++ b/homeassistant/components/konnected/translations/ca.json @@ -11,7 +11,7 @@ }, "step": { "confirm": { - "description": "Model: {model} \nAmfitri\u00f3: {host} \nPort: {port} \n\nPots configurar el comportament de les E/S (I/O) i del panell a la configuraci\u00f3 del panell d\u2019alarma Konnected.", + "description": "Model: {model} \nID: {id}\nAmfitri\u00f3: {host} \nPort: {port} \n\nPots configurar el comportament de les E/S (I/O) i del panell a la configuraci\u00f3 del panell d'alarma Konnected.", "title": "Dispositiu Konnected llest" }, "import_confirm": { diff --git a/homeassistant/components/life360/translations/ca.json b/homeassistant/components/life360/translations/ca.json index a61e4372b78a96..09cefc0bce1fde 100644 --- a/homeassistant/components/life360/translations/ca.json +++ b/homeassistant/components/life360/translations/ca.json @@ -19,7 +19,7 @@ "password": "Contrasenya", "username": "Nom d'usuari" }, - "description": "Per configurar les opcions avan\u00e7ades mira la [documentaci\u00f3 de Life360]({docs_url}). Pot ser que ho hagis de fer abans d\u2019afegir cap compte.", + "description": "Per configurar les opcions avan\u00e7ades mira la [documentaci\u00f3 de Life360]({docs_url}). Pot ser que ho hagis de fer abans d'afegir cap compte.", "title": "Informaci\u00f3 del compte Life360" } } diff --git a/homeassistant/components/linky/translations/ca.json b/homeassistant/components/linky/translations/ca.json index 127d2870ae7252..954b873083a86f 100644 --- a/homeassistant/components/linky/translations/ca.json +++ b/homeassistant/components/linky/translations/ca.json @@ -7,7 +7,7 @@ "access": "No s'ha pogut accedir a Enedis.fr, comprova la teva connexi\u00f3 a Internet", "enedis": "Enedis.fr ha respost amb un error: torna-ho a provar m\u00e9s tard (millo no entre les 23:00 i les 14:00)", "unknown": "Error desconegut: torna-ho a provar m\u00e9s tard (millor no entre les 23:00 i les 14:00)", - "wrong_login": "Error d\u2019inici de sessi\u00f3: comprova el teu correu electr\u00f2nic i la contrasenya" + "wrong_login": "Error d'inici de sessi\u00f3: comprova el teu correu electr\u00f2nic i la contrasenya" }, "step": { "user": { diff --git a/homeassistant/components/logi_circle/translations/ca.json b/homeassistant/components/logi_circle/translations/ca.json index 0aa0db1dc9ae7b..8b81f752058661 100644 --- a/homeassistant/components/logi_circle/translations/ca.json +++ b/homeassistant/components/logi_circle/translations/ca.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "Nom\u00e9s pots configurar un \u00fanic compte de Logi Circule.", - "external_error": "S'ha produ\u00eft una excepci\u00f3 d\u2019un altre flux de dades.", + "external_error": "S'ha produ\u00eft una excepci\u00f3 d'un altre flux de dades.", "external_setup": "Logi Circle s'ha configurat correctament des d'un altre flux de dades.", "no_flows": "Necessites configurar Logi Circle abans de poder autenticar-t'hi. Llegeix les [instruccions](https://www.home-assistant.io/components/logi_circle/)." }, @@ -10,8 +10,8 @@ "default": "Autenticaci\u00f3 exitosa amb Logi Circle." }, "error": { - "auth_error": "Ha fallat l\u2019autoritzaci\u00f3 de l\u2019API.", - "auth_timeout": "L\u2019autoritzaci\u00f3 ha expirat durant l'obtenci\u00f3 del token d\u2019acc\u00e9s.", + "auth_error": "Ha fallat l'autoritzaci\u00f3 de l'API.", + "auth_timeout": "L'autoritzaci\u00f3 ha expirat durant l'obtenci\u00f3 del token d'acc\u00e9s.", "follow_link": "V\u00e9s a l'enlla\u00e7 i autentica't abans de pr\u00e9mer Envia" }, "step": { diff --git a/homeassistant/components/melcloud/translations/ca.json b/homeassistant/components/melcloud/translations/ca.json index 2c3bec0979075e..92472d020c43ad 100644 --- a/homeassistant/components/melcloud/translations/ca.json +++ b/homeassistant/components/melcloud/translations/ca.json @@ -14,7 +14,7 @@ "password": "Contrasenya de MELCloud.", "username": "Correu electr\u00f2nic d'inici de sessi\u00f3 a MELCloud." }, - "description": "Connecta\u2019t amb el teu compte de MELCloud.", + "description": "Connecta't amb el teu compte de MELCloud.", "title": "Connexi\u00f3 amb MELCloud" } } diff --git a/homeassistant/components/minecraft_server/translations/ca.json b/homeassistant/components/minecraft_server/translations/ca.json index e34e2fb7fdb524..6e5554216d9244 100644 --- a/homeassistant/components/minecraft_server/translations/ca.json +++ b/homeassistant/components/minecraft_server/translations/ca.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3 amb el servidor. Comprova l'amfitri\u00f3 i el port i torna-ho a provar. Assegurat que estas utilitzant la versi\u00f3 del servidor 1.7 o superior.", - "invalid_ip": "L\u2019adre\u00e7a IP \u00e9s inv\u00e0lida (no s\u2019ha pogut determinar l\u2019adre\u00e7a MAC). Corregeix-la i torna-ho a provar.", + "invalid_ip": "L'adre\u00e7a IP \u00e9s inv\u00e0lida (no s'ha pogut determinar l'adre\u00e7a MAC). Corregeix-la i torna-ho a provar.", "invalid_port": "El port ha d'estar compr\u00e8s entre 1024 i 65535. Corregeix-lo i torna-ho a provar." }, "step": { diff --git a/homeassistant/components/mobile_app/translations/ca.json b/homeassistant/components/mobile_app/translations/ca.json index 697f676df78876..84e613ca978bba 100644 --- a/homeassistant/components/mobile_app/translations/ca.json +++ b/homeassistant/components/mobile_app/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "install_app": "Obre l\u2019aplicaci\u00f3 m\u00f2bil per configurar la integraci\u00f3 amb Home Assistant. Mira [la documentaci\u00f3]({apps_url}) per veure la llista d\u2019aplicacions compatibles." + "install_app": "Obre l'aplicaci\u00f3 m\u00f2bil per configurar la integraci\u00f3 amb Home Assistant. Mira [la documentaci\u00f3]({apps_url}) per veure la llista d'aplicacions compatibles." }, "step": { "confirm": { diff --git a/homeassistant/components/nuheat/translations/ca.json b/homeassistant/components/nuheat/translations/ca.json index 165d889dbdc2a1..c7821838f4f71f 100644 --- a/homeassistant/components/nuheat/translations/ca.json +++ b/homeassistant/components/nuheat/translations/ca.json @@ -16,7 +16,7 @@ "serial_number": "N\u00famero de s\u00e8rie del term\u00f2stat.", "username": "Nom d'usuari" }, - "description": "Has d\u2019obtenir el n\u00famero de s\u00e8rie o identificador del teu term\u00f2stat entrant a https://MyNuHeat.com i seleccionant el teu term\u00f2stat.", + "description": "Has d'obtenir el n\u00famero de s\u00e8rie o identificador del teu term\u00f2stat entrant a https://MyNuHeat.com i seleccionant el teu term\u00f2stat.", "title": "Connexi\u00f3 amb NuHeat" } } diff --git a/homeassistant/components/onvif/translations/ca.json b/homeassistant/components/onvif/translations/ca.json new file mode 100644 index 00000000000000..9d942f091d47ca --- /dev/null +++ b/homeassistant/components/onvif/translations/ca.json @@ -0,0 +1,58 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositiu ONVIF ja configurat.", + "already_in_progress": "El flux de dades de configuraci\u00f3 pel dispositiu ONVIF ja est\u00e0 en curs.", + "no_h264": "No s'han torbat fluxos (streams) H264 disponibles. Comporva la configuraci\u00f3 de perfil en el dispositiu.", + "no_mac": "No s'ha pogut configurar un ID \u00fanic pel dispositiu ONVIF.", + "onvif_error": "Error durant la configuraci\u00f3 del dispositiu ONVIF. Consulta els registres per a m\u00e9s informaci\u00f3." + }, + "error": { + "connection_failed": "No s'ha pogut connectar al servei ONVIF amb les credencials proporcionades." + }, + "step": { + "auth": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "title": "Configuraci\u00f3 d'autenticaci\u00f3" + }, + "configure_profile": { + "data": { + "include": "Crea entitat de c\u00e0mera" + }, + "description": "Crear entitat de c\u00e0mera per {profile} amb resoluci\u00f3 {resolution}?", + "title": "Configuraci\u00f3 dels perfils" + }, + "device": { + "data": { + "host": "Selecciona un dispositiu ONVIF descobert" + }, + "title": "Selecci\u00f3 de dispositiu ONVIF" + }, + "manual_input": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port" + }, + "title": "Configura el dispositiu ONVIF" + }, + "user": { + "description": "En fer clic a envia, es cercaran a la xarxa dispositius ONVIF que suportin perfils S.\n\nAlguns fabricants han comen\u00e7at a desactivar ONVIF per defecte. Comprova que ONVIF est\u00e0 activat a la configuraci\u00f3 de les c\u00e0meres.", + "title": "Configuraci\u00f3 de dispositiu ONVIF" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Arguments addicionals FFMPEG", + "rtsp_transport": "Mecanisme de transport RTSP" + }, + "title": "Opcions de dispositiu ONVIF" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/cs.json b/homeassistant/components/onvif/translations/cs.json new file mode 100644 index 00000000000000..dad373fb5e38a9 --- /dev/null +++ b/homeassistant/components/onvif/translations/cs.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed ONVIF je ji\u017e nakonfigurov\u00e1no", + "no_h264": "Nebyly k dispozici \u017e\u00e1dn\u00e9 H264 streamy. Zkontrolujte konfiguraci profilu v za\u0159\u00edzen\u00ed.", + "no_mac": "Nelze nakonfigurovat jedine\u010dn\u00e9 ID pro za\u0159\u00edzen\u00ed ONVIF.", + "onvif_error": "P\u0159i nastavov\u00e1n\u00ed za\u0159\u00edzen\u00ed ONVIF do\u0161lo k chyb\u011b. Dal\u0161\u00ed informace naleznete v protokolech." + }, + "error": { + "connection_failed": "Nelze se p\u0159ipojit ke slu\u017eb\u011b ONVIF s poskytnut\u00fdmi p\u0159ihla\u0161ovac\u00edmi \u00fadaji." + }, + "step": { + "auth": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + }, + "title": "Konfigurace ov\u011b\u0159ov\u00e1n\u00ed" + }, + "configure_profile": { + "data": { + "include": "Vytvo\u0159it entitu kamery" + }, + "description": "Chcete vytvo\u0159it entitu kamery pro {profile} s rozli\u0161en\u00edm {resolution}?", + "title": "Nakonfigurovat profily" + }, + "device": { + "data": { + "host": "Vyberte nalezen\u00e1 za\u0159\u00edzen\u00ed ONVIF" + }, + "title": "Vyberte za\u0159\u00edzen\u00ed ONVIF" + }, + "manual_input": { + "data": { + "host": "Adresa za\u0159\u00edzen\u00ed", + "port": "Port" + }, + "title": "Konfigurovat za\u0159\u00edzen\u00ed ONVIF" + }, + "user": { + "description": "Kliknut\u00edm na tla\u010d\u00edtko Odeslat vyhled\u00e1me ve va\u0161\u00ed s\u00edti za\u0159\u00edzen\u00ed ONVIF, kter\u00e1 podporuj\u00ed profil S. \n\nN\u011bkte\u0159\u00ed v\u00fdrobci vypli funkci ONVIF v z\u00e1kladn\u00edm nastaven\u00ed. Ujist\u011bte se, \u017ee je v konfiguraci kamery povolena funkce ONVIF.", + "title": "Nastaven\u00ed za\u0159\u00edzen\u00ed ONVIF" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Dal\u0161\u00ed FFMPEG argumenty" + }, + "title": "Mo\u017enosti za\u0159\u00edzen\u00ed ONVIF" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/en.json b/homeassistant/components/onvif/translations/en.json index 066efed60f2e9f..91828648cc0641 100644 --- a/homeassistant/components/onvif/translations/en.json +++ b/homeassistant/components/onvif/translations/en.json @@ -54,6 +54,5 @@ "title": "ONVIF Device Options" } } - }, - "title": "ONVIF" + } } \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/fr.json b/homeassistant/components/onvif/translations/fr.json new file mode 100644 index 00000000000000..38036b65517314 --- /dev/null +++ b/homeassistant/components/onvif/translations/fr.json @@ -0,0 +1,57 @@ +{ + "config": { + "abort": { + "already_configured": "Le p\u00e9riph\u00e9rique ONVIF est d\u00e9j\u00e0 configur\u00e9.", + "already_in_progress": "Le flux de configuration pour le p\u00e9riph\u00e9rique ONVIF est d\u00e9j\u00e0 en cours.", + "no_h264": "Aucun flux H264 n'\u00e9tait disponible. V\u00e9rifiez la configuration du profil sur votre appareil.", + "onvif_error": "Erreur lors de la configuration du p\u00e9riph\u00e9rique ONVIF. Consultez les journaux pour plus d'informations." + }, + "error": { + "connection_failed": "Impossible de se connecter au service ONVIF avec les informations d'identification fournies." + }, + "step": { + "auth": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "title": "Configurer l'authentification" + }, + "configure_profile": { + "data": { + "include": "Cr\u00e9er une entit\u00e9 cam\u00e9ra" + }, + "description": "Cr\u00e9er une entit\u00e9 cam\u00e9ra pour {profile} \u00e0 la r\u00e9solution {resolution} ?", + "title": "Configurer les profils" + }, + "device": { + "data": { + "host": "S\u00e9lectionnez le p\u00e9riph\u00e9rique ONVIF d\u00e9couvert" + }, + "title": "S\u00e9lectionnez l'appareil ONVIF" + }, + "manual_input": { + "data": { + "host": "H\u00f4te", + "port": "Port" + }, + "title": "Configurer l\u2019appareil ONVIF" + }, + "user": { + "description": "En cliquant sur soumettre, nous rechercherons sur votre r\u00e9seau, des \u00e9quipements ONVIF qui supporte le Profile S.\n\nCertains constructeurs ont commenc\u00e9 \u00e0 d\u00e9sactiver ONvif par d\u00e9faut. Assurez vous que ONVIF est activ\u00e9 dans la configuration de votre cam\u00e9ra", + "title": "Configuration de l'appareil ONVIF" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Arguments FFMPEG suppl\u00e9mentaires", + "rtsp_transport": "M\u00e9canisme de transport RTSP" + }, + "title": "Options ONVIF de l'appareil" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/no.json b/homeassistant/components/onvif/translations/no.json new file mode 100644 index 00000000000000..8ad30a8bf77ef7 --- /dev/null +++ b/homeassistant/components/onvif/translations/no.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "manual_input": { + "data": { + "host": "Vert", + "port": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/pl.json b/homeassistant/components/onvif/translations/pl.json new file mode 100644 index 00000000000000..99a17d48df0998 --- /dev/null +++ b/homeassistant/components/onvif/translations/pl.json @@ -0,0 +1,58 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie ONVIF jest ju\u017c skonfigurowane.", + "already_in_progress": "Proces konfiguracji dla urz\u0105dzenia ONVIF jest ju\u017c w toku.", + "no_h264": "Nie by\u0142o dost\u0119pnych \u017cadnych strumieni H264. Sprawd\u017a konfiguracj\u0119 profilu w swoim urz\u0105dzeniu.", + "no_mac": "Nie mo\u017cna utworzy\u0107 unikalnego identyfikatora urz\u0105dzenia ONVIF.", + "onvif_error": "Wyst\u0105pi\u0142 b\u0142\u0105d podczas konfigurowania urz\u0105dzenia ONVIF. Sprawd\u017a logi, aby uzyska\u0107 wi\u0119cej informacji." + }, + "error": { + "connection_failed": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z us\u0142ug\u0105 ONVIF z podanymi po\u015bwiadczeniami." + }, + "step": { + "auth": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "title": "Konfigurowanie uwierzytelniania" + }, + "configure_profile": { + "data": { + "include": "Utw\u00f3rz encj\u0119 kamery" + }, + "description": "Czy utworzy\u0107 encj\u0119 kamery dla {profile} o rozdzielczo\u015bci {resolution}?", + "title": "Konfigurowanie profili" + }, + "device": { + "data": { + "host": "Wybierz odnalezione urz\u0105dzenie ONVIF" + }, + "title": "Wybierz urz\u0105dzenie ONVIF" + }, + "manual_input": { + "data": { + "host": "Host", + "port": "Port" + }, + "title": "Konfigurowanie urz\u0105dzenia ONVIF" + }, + "user": { + "description": "Klikaj\u0105c przycisk Prze\u015blij, Twoja sie\u0107 zostanie przeszukana pod k\u0105tem urz\u0105dze\u0144 ONVIF obs\u0142uguj\u0105cych profil S.\n\nNiekt\u00f3rzy producenci zacz\u0119li domy\u015blnie wy\u0142\u0105cza\u0107 ONVIF. Upewnij si\u0119, \u017ce ONVIF jest w\u0142\u0105czony w konfiguracji kamery.", + "title": "Konfiguracja urz\u0105dzenia ONVIF" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Dodatkowe argumenty FFMPEG", + "rtsp_transport": "Mechanizm transportu RTSP" + }, + "title": "Opcje urz\u0105dzenia ONVIF" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/ru.json b/homeassistant/components/onvif/translations/ru.json new file mode 100644 index 00000000000000..20b709a4a01963 --- /dev/null +++ b/homeassistant/components/onvif/translations/ru.json @@ -0,0 +1,58 @@ +{ + "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.", + "no_h264": "\u041d\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u043f\u043e\u0442\u043e\u043a\u043e\u0432 H264. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 \u0412\u0430\u0448\u0435\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435.", + "no_mac": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0434\u043b\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", + "onvif_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043b\u043e\u0433\u0438 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + }, + "error": { + "connection_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u043b\u0443\u0436\u0431\u0435 ONVIF \u0441 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u043c\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u043c\u0438 \u0434\u0430\u043d\u043d\u044b\u043c\u0438." + }, + "step": { + "auth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f" + }, + "configure_profile": { + "data": { + "include": "\u0421\u043e\u0437\u0434\u0430\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442 \u043a\u0430\u043c\u0435\u0440\u044b" + }, + "description": "\u0421\u043e\u0437\u0434\u0430\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442 \u043a\u0430\u043c\u0435\u0440\u044b \u0434\u043b\u044f {profile} \u0441 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0438\u0435\u043c {resolution}?", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0440\u043e\u0444\u0438\u043b\u0435\u0439" + }, + "device": { + "data": { + "host": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e ONVIF" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e ONVIF" + }, + "manual_input": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 ONVIF" + }, + "user": { + "description": "\u041a\u043e\u0433\u0434\u0430 \u0412\u044b \u043d\u0430\u0436\u043c\u0451\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u041e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c, \u043d\u0430\u0447\u043d\u0451\u0442\u0441\u044f \u043f\u043e\u0438\u0441\u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 ONVIF, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442 Profile S.\n\n\u041d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u0438 \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043e\u0442\u043a\u043b\u044e\u0447\u0430\u044e\u0442 ONVIF. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e ONVIF \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u0412\u0430\u0448\u0435\u0439 \u043a\u0430\u043c\u0435\u0440\u044b.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 ONVIF" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u044b FFMPEG", + "rtsp_transport": "\u0422\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c RTSP" + }, + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 ONVIF" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/zh-Hant.json b/homeassistant/components/onvif/translations/zh-Hant.json new file mode 100644 index 00000000000000..c7a0f88d1b9e2e --- /dev/null +++ b/homeassistant/components/onvif/translations/zh-Hant.json @@ -0,0 +1,58 @@ +{ + "config": { + "abort": { + "already_configured": "ONVIF \u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002", + "already_in_progress": "ONVIF \u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", + "no_h264": "\u8a72\u8a2d\u5099\u4e0d\u652f\u63f4 H264 \u4e32\u6d41\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5099\u8a2d\u5b9a\u3002", + "no_mac": "\u7121\u6cd5\u70ba ONVIF \u8a2d\u5099\u8a2d\u5b9a\u552f\u4e00 ID\u3002", + "onvif_error": "\u8a2d\u5b9a ONVIF \u8a2d\u5099\u932f\u8aa4\uff0c\u8acb\u53c3\u95b1\u65e5\u8a8c\u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3002" + }, + "error": { + "connection_failed": "\u7121\u6cd5\u4ee5\u6240\u63d0\u4f9b\u7684\u6191\u8b49\u9023\u7dda\u81f3 ONVIF \u670d\u52d9\u3002" + }, + "step": { + "auth": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "title": "\u8a2d\u5b9a\u9a57\u8b49" + }, + "configure_profile": { + "data": { + "include": "\u65b0\u589e\u651d\u5f71\u6a5f\u7269\u4ef6" + }, + "description": "\u4ee5 {profile} \u4f7f\u7528\u89e3\u6790\u5ea6 {resolution} \u65b0\u589e\u651d\u5f71\u6a5f\u7269\u4ef6\uff1f", + "title": "\u8a2d\u5b9a Profiles" + }, + "device": { + "data": { + "host": "\u9078\u64c7\u6240\u63a2\u7d22\u5230\u7684 ONVIF \u8a2d\u5099" + }, + "title": "\u9078\u64c7 ONVIF \u8a2d\u5099" + }, + "manual_input": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0" + }, + "title": "\u8a2d\u5b9a ONVIF \u8a2d\u5099" + }, + "user": { + "description": "\u9ede\u4e0b\u50b3\u9001\u5f8c\u3001\u5c07\u6703\u641c\u5c0b\u7db2\u8def\u4e2d\u652f\u63f4 Profile S \u7684 ONVIF \u8a2d\u5099\u3002\n\n\u67d0\u4e9b\u5ee0\u5546\u9810\u8a2d\u7684\u6a21\u5f0f\u70ba ONVIF \u95dc\u9589\u6a21\u5f0f\uff0c\u8acb\u518d\u6b21\u78ba\u8a8d\u651d\u5f71\u6a5f\u5df2\u7d93\u958b\u555f ONVIF\u3002", + "title": "ONVIF \u8a2d\u5099\u8a2d\u5b9a" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "\u984d\u5916 FFMPEG \u53c3\u6578", + "rtsp_transport": "RTSP \u50b3\u8f38\u5354\u5b9a" + }, + "title": "ONVIF \u8a2d\u5099\u9078\u9805" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/ca.json b/homeassistant/components/opentherm_gw/translations/ca.json index 41a155b147f84f..6660c93767e451 100644 --- a/homeassistant/components/opentherm_gw/translations/ca.json +++ b/homeassistant/components/opentherm_gw/translations/ca.json @@ -24,7 +24,7 @@ "floor_temperature": "Temperatura de la planta", "precision": "Precisi\u00f3" }, - "description": "Opcions del la passarel\u00b7la d'enlla\u00e7 d\u2019OpenTherm" + "description": "Opcions del la passarel\u00b7la d'enlla\u00e7 d'OpenTherm" } } } diff --git a/homeassistant/components/plex/translations/ca.json b/homeassistant/components/plex/translations/ca.json index 83b3e7e7e55be9..712fa6f251d6da 100644 --- a/homeassistant/components/plex/translations/ca.json +++ b/homeassistant/components/plex/translations/ca.json @@ -3,7 +3,7 @@ "abort": { "all_configured": "Tots els servidors enlla\u00e7ats ja estan configurats", "already_configured": "Aquest servidor Plex ja est\u00e0 configurat", - "already_in_progress": "S\u2019est\u00e0 configurant Plex", + "already_in_progress": "S'est\u00e0 configurant Plex", "invalid_import": "La configuraci\u00f3 importada \u00e9s inv\u00e0lida", "non-interactive": "Importaci\u00f3 no interactiva", "token_request_timeout": "S'ha acabat el temps d'espera durant l'obtenci\u00f3 del token.", diff --git a/homeassistant/components/powerwall/translations/ca.json b/homeassistant/components/powerwall/translations/ca.json index 2416a2bf7f27e8..b0764b78234600 100644 --- a/homeassistant/components/powerwall/translations/ca.json +++ b/homeassistant/components/powerwall/translations/ca.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", "unknown": "Error inesperat", - "wrong_version": "El teu Powerwall utilitza una versi\u00f3 de programari no compatible. L'hauries d'actualitzar o informar d\u2019aquest problema perqu\u00e8 sigui solucionat." + "wrong_version": "El teu Powerwall utilitza una versi\u00f3 de programari no compatible. L'hauries d'actualitzar o informar d'aquest problema perqu\u00e8 sigui solucionat." }, "step": { "user": { diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ca.json b/homeassistant/components/pvpc_hourly_pricing/translations/ca.json index a96fa3b584bd6c..8eeab499620e48 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/ca.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/ca.json @@ -9,7 +9,7 @@ "name": "Nom del sensor", "tariff": "Tarifa contractada (1, 2 o 3 per\u00edodes)" }, - "description": "Aquest sensor utilitza l'API oficial de la xarxa el\u00e8ctrica espanyola (REE) per obtenir els [preus per hora de l\u2019electricitat (PVPC)](https://www.esios.ree.es/es/pvpc) a Espanya.\nPer a m\u00e9s informaci\u00f3, consulta la [documentaci\u00f3 de la integraci\u00f3](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\nSelecciona la tarifa contractada, cadascuna t\u00e9 un nombre determinat de per\u00edodes: \n - 1 per\u00edode: normal (sense discriminaci\u00f3)\n - 2 per\u00edodes: discriminaci\u00f3 (tarifa nocturna) \n - 3 per\u00edodes: cotxe el\u00e8ctric (tarifa nocturna de 3 per\u00edodes)", + "description": "Aquest sensor utilitza l'API oficial de la xarxa el\u00e8ctrica espanyola (REE) per obtenir els [preus per hora de l'electricitat (PVPC)](https://www.esios.ree.es/es/pvpc) a Espanya.\nPer a m\u00e9s informaci\u00f3, consulta la [documentaci\u00f3 de la integraci\u00f3](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\nSelecciona la tarifa contractada, cadascuna t\u00e9 un nombre determinat de per\u00edodes: \n - 1 per\u00edode: normal (sense discriminaci\u00f3)\n - 2 per\u00edodes: discriminaci\u00f3 (tarifa nocturna) \n - 3 per\u00edodes: cotxe el\u00e8ctric (tarifa nocturna de 3 per\u00edodes)", "title": "Selecci\u00f3 de tarifa" } } diff --git a/homeassistant/components/rachio/translations/ca.json b/homeassistant/components/rachio/translations/ca.json index 8d4f9aa7245faa..14b4d8effd88d0 100644 --- a/homeassistant/components/rachio/translations/ca.json +++ b/homeassistant/components/rachio/translations/ca.json @@ -22,7 +22,7 @@ "step": { "init": { "data": { - "manual_run_mins": "Durant quant de temps (en minuts) mantenir engegada una estaci\u00f3 quan l\u2019interruptor s'activa." + "manual_run_mins": "Durant quant de temps (en minuts) mantenir engegada una estaci\u00f3 quan l'interruptor s'activa." } } } diff --git a/homeassistant/components/samsungtv/translations/hu.json b/homeassistant/components/samsungtv/translations/hu.json index f6f09dab4eede3..1704fa048979f7 100644 --- a/homeassistant/components/samsungtv/translations/hu.json +++ b/homeassistant/components/samsungtv/translations/hu.json @@ -3,14 +3,14 @@ "abort": { "already_configured": "Ez a Samsung TV m\u00e1r konfigur\u00e1lva van.", "already_in_progress": "A Samsung TV konfigur\u00e1l\u00e1sa m\u00e1r folyamatban van.", - "auth_missing": "A Home Assistant nem jogosult csatlakozni ehhez a Samsung TV-hez. Ellen\u0151rizze a TV-k\u00e9sz\u00fcl\u00e9k\u00e9ben a Home Assistant enged\u00e9lyez\u00e9si be\u00e1ll\u00edt\u00e1sait.", + "auth_missing": "A Home Assistant nem jogosult csatlakozni ehhez a Samsung TV-hez. Ellen\u0151rizd a TV be\u00e1ll\u00edt\u00e1sait a Home Assistant enged\u00e9lyez\u00e9s\u00e9hez.", "not_successful": "Nem lehet csatlakozni ehhez a Samsung TV k\u00e9sz\u00fcl\u00e9khez.", "not_supported": "Ez a Samsung TV k\u00e9sz\u00fcl\u00e9k jelenleg nem t\u00e1mogatott." }, "flow_title": "Samsung TV: {model}", "step": { "confirm": { - "description": "Be\u00e1ll\u00edtja a Samsung TV {model} k\u00e9sz\u00fcl\u00e9ket? Ha soha nem csatlakozott home assistant-hez ezel\u0151tt, meg kell jelennie egy felugr\u00f3 ablaknak a TV-ben, ahol hiteles\u00edt\u00e9st k\u00e9r. A tv-k\u00e9sz\u00fcl\u00e9k manu\u00e1lis konfigur\u00e1ci\u00f3i fel\u00fcl\u00edr\u00f3dnak.", + "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a Samsung TV {model} k\u00e9sz\u00fcl\u00e9ket? Ha m\u00e9g soha nem csatlakozott Home Assistant-hez, akkor meg kell jelennie egy felugr\u00f3 ablaknak a TV k\u00e9perny\u0151j\u00e9n, ahol hiteles\u00edt\u00e9st k\u00e9r. A TV manu\u00e1lis konfigur\u00e1ci\u00f3i fel\u00fcl\u00edr\u00f3dnak.", "title": "Samsung TV" }, "user": { @@ -18,7 +18,7 @@ "host": "Hosztn\u00e9v vagy IP c\u00edm", "name": "N\u00e9v" }, - "description": "\u00cdrja be a Samsung TV adatait. Ha soha nem csatlakoztatta a Home Assistant alkalmaz\u00e1st ezel\u0151tt, l\u00e1tnia kell a t\u00e9v\u00e9ben egy felugr\u00f3 ablakot, amely enged\u00e9lyt k\u00e9r.", + "description": "\u00cdrd be a Samsung TV adatait. Ha m\u00e9g soha nem csatlakozott Home Assistant-hez, akkor meg kell jelennie egy felugr\u00f3 ablaknak a TV k\u00e9perny\u0151j\u00e9n, ahol hiteles\u00edt\u00e9st k\u00e9r.", "title": "Samsung TV" } } diff --git a/homeassistant/components/smartthings/translations/ca.json b/homeassistant/components/smartthings/translations/ca.json index f5a7dacf01b00e..9e37cb0d945117 100644 --- a/homeassistant/components/smartthings/translations/ca.json +++ b/homeassistant/components/smartthings/translations/ca.json @@ -19,7 +19,7 @@ "data": { "access_token": "Token d'acc\u00e9s" }, - "description": "Introdueix un [token d'acc\u00e9s personal]({token_url}) d'SmartThings creat a partir de les [instruccions]({component_url}). S\u2019utilitzar\u00e0 per crear la integraci\u00f3 de Home Assistant dins el teu compte de SmartThings.", + "description": "Introdueix un [token d'acc\u00e9s personal]({token_url}) d'SmartThings creat a partir de les [instruccions]({component_url}). S'utilitzar\u00e0 per crear la integraci\u00f3 de Home Assistant dins el teu compte de SmartThings.", "title": "Introdueix el token d'acc\u00e9s personal" }, "select_location": { diff --git a/homeassistant/components/solaredge/translations/ca.json b/homeassistant/components/solaredge/translations/ca.json index 56e1633e3a14f5..ca5d472c9d6641 100644 --- a/homeassistant/components/solaredge/translations/ca.json +++ b/homeassistant/components/solaredge/translations/ca.json @@ -9,8 +9,8 @@ "step": { "user": { "data": { - "api_key": "Clau API d\u2019aquest lloc", - "name": "Nom d\u2019aquesta instal\u00b7laci\u00f3", + "api_key": "Clau API d'aquest lloc", + "name": "Nom d'aquesta instal\u00b7laci\u00f3", "site_id": "SolarEdge site_id" }, "title": "Configuraci\u00f3 dels par\u00e0metres de l'API per aquesta instal\u00b7laci\u00f3" diff --git a/homeassistant/components/starline/translations/ca.json b/homeassistant/components/starline/translations/ca.json index d8c7685648038e..722b65da2a8c7e 100644 --- a/homeassistant/components/starline/translations/ca.json +++ b/homeassistant/components/starline/translations/ca.json @@ -34,7 +34,7 @@ "username": "Nom d'usuari" }, "description": "Correu electr\u00f2nic i contrasenya del compte StarLine", - "title": "Credencials d\u2019usuari" + "title": "Credencials d'usuari" } } } diff --git a/homeassistant/components/synology_dsm/translations/ca.json b/homeassistant/components/synology_dsm/translations/ca.json index f593ac26182d20..b5e1a847380272 100644 --- a/homeassistant/components/synology_dsm/translations/ca.json +++ b/homeassistant/components/synology_dsm/translations/ca.json @@ -5,7 +5,7 @@ }, "error": { "connection": "Error de connexi\u00f3: comprova l'amfitri\u00f3, la contrasenya i l'SSL", - "login": "Error d\u2019inici de sessi\u00f3: comprova el nom d'usuari i la contrasenya", + "login": "Error d'inici de sessi\u00f3: comprova el nom d'usuari i la contrasenya", "missing_data": "Falten dades: torna-ho a provar m\u00e9s tard o prova una altra configuraci\u00f3 diferent", "otp_failed": "L'autenticaci\u00f3 en dos passos ha fallat, torna-ho a provar amb un nou codi", "unknown": "Error desconegut: consulta els registres per a m\u00e9s detalls." diff --git a/homeassistant/components/totalconnect/translations/ca.json b/homeassistant/components/totalconnect/translations/ca.json index 19106050c6d06e..ca9e6d66e2ea74 100644 --- a/homeassistant/components/totalconnect/translations/ca.json +++ b/homeassistant/components/totalconnect/translations/ca.json @@ -4,7 +4,7 @@ "already_configured": "El compte ja ha estat configurat" }, "error": { - "login": "Error d\u2019inici de sessi\u00f3: comprova el nom d'usuari i la contrasenya" + "login": "Error d'inici de sessi\u00f3: comprova el nom d'usuari i la contrasenya" }, "step": { "user": { diff --git a/homeassistant/components/transmission/translations/ca.json b/homeassistant/components/transmission/translations/ca.json index 837766ba6edabc..e2b014ac019ead 100644 --- a/homeassistant/components/transmission/translations/ca.json +++ b/homeassistant/components/transmission/translations/ca.json @@ -25,7 +25,7 @@ "step": { "init": { "data": { - "scan_interval": "Freq\u00fc\u00e8ncia d\u2019actualitzaci\u00f3" + "scan_interval": "Freq\u00fc\u00e8ncia d'actualitzaci\u00f3" }, "title": "Opcions de configuraci\u00f3 de Transmission" } diff --git a/homeassistant/components/transmission/translations/es-419.json b/homeassistant/components/transmission/translations/es-419.json new file mode 100644 index 00000000000000..6a01b3e25a1393 --- /dev/null +++ b/homeassistant/components/transmission/translations/es-419.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frecuencia de actualizaci\u00f3n" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/translations/es-419.json b/homeassistant/components/twentemilieu/translations/es-419.json index ed333bb9b5159d..5cc3dc4b2c9d9e 100644 --- a/homeassistant/components/twentemilieu/translations/es-419.json +++ b/homeassistant/components/twentemilieu/translations/es-419.json @@ -2,6 +2,10 @@ "config": { "step": { "user": { + "data": { + "house_number": "N\u00famero de casa", + "post_code": "C\u00f3digo postal" + }, "title": "Twente Milieu" } } diff --git a/homeassistant/components/unifi/translations/ca.json b/homeassistant/components/unifi/translations/ca.json index 6d8b395f2b0716..6bd5e84bb6f2bd 100644 --- a/homeassistant/components/unifi/translations/ca.json +++ b/homeassistant/components/unifi/translations/ca.json @@ -28,7 +28,7 @@ "client_control": { "data": { "block_client": "Clients controlats amb acc\u00e9s a la xarxa", - "new_client": "Afegeix un client nou per al control d\u2019acc\u00e9s a la xarxa", + "new_client": "Afegeix un client nou per al control d'acc\u00e9s a la xarxa", "poe_clients": "Permet control POE dels clients" }, "description": "Configura els controls del client \n\nConfigura interruptors per als n\u00fameros de s\u00e8rie als quals vulguis controlar l'acc\u00e9s a la xarxa.", @@ -45,11 +45,19 @@ "description": "Configuraci\u00f3 de seguiment de dispositius", "title": "Opcions d'UniFi" }, + "simple_options": { + "data": { + "block_client": "[%key::component::unifi::options::step::client_control::data::block_client%]", + "track_clients": "[%key::component::unifi::options::step::device_tracker::data::track_clients%]", + "track_devices": "[%key::component::unifi::options::step::device_tracker::data::track_devices%]" + }, + "description": "Configura la integraci\u00f3 d'UniFi" + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Crea sensors d'\u00fas d'ample de banda per a clients de la xarxa" }, - "description": "Configuraci\u00f3 dels sensors d\u2019estad\u00edstiques", + "description": "Configuraci\u00f3 dels sensors d'estad\u00edstiques", "title": "Opcions d'UniFi" } } diff --git a/homeassistant/components/unifi/translations/cs.json b/homeassistant/components/unifi/translations/cs.json index c28ca26919cc6c..8fe1c060992a11 100644 --- a/homeassistant/components/unifi/translations/cs.json +++ b/homeassistant/components/unifi/translations/cs.json @@ -24,6 +24,13 @@ }, "options": { "step": { + "simple_options": { + "data": { + "track_clients": "Sledov\u00e1n\u00ed p\u0159ipojen\u00fdch za\u0159\u00edzen\u00ed", + "track_devices": "Sledov\u00e1n\u00ed s\u00ed\u0165ov\u00fdch za\u0159\u00edzen\u00ed (za\u0159\u00edzen\u00ed Ubiquiti)" + }, + "description": "Konfigurace integrace UniFi" + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Vytvo\u0159it senzory vyu\u017eit\u00ed \u0161\u00ed\u0159ky p\u00e1sma pro s\u00ed\u0165ov\u00e9 klienty" diff --git a/homeassistant/components/unifi/translations/en.json b/homeassistant/components/unifi/translations/en.json index fd7096686e1fbc..e49bbbcc50ec3c 100644 --- a/homeassistant/components/unifi/translations/en.json +++ b/homeassistant/components/unifi/translations/en.json @@ -48,9 +48,9 @@ }, "simple_options": { "data": { - "track_clients": "[%key:component::unifi::options::step::device_tracker::data::track_clients%]", - "track_devices": "[%key:component::unifi::options::step::device_tracker::data::track_devices%]", - "block_client": "[%key:component::unifi::options::step::client_control::data::block_client%]" + "block_client": "Network access controlled clients", + "track_clients": "Track network clients", + "track_devices": "Track network devices (Ubiquiti devices)" }, "description": "Configure UniFi integration" }, diff --git a/homeassistant/components/unifi/translations/fr.json b/homeassistant/components/unifi/translations/fr.json index 007f06b1c8ea62..851613d6997de5 100644 --- a/homeassistant/components/unifi/translations/fr.json +++ b/homeassistant/components/unifi/translations/fr.json @@ -48,6 +48,13 @@ "other": "Vide" } }, + "simple_options": { + "data": { + "track_clients": "Suivi de clients r\u00e9seaux", + "track_devices": "Suivi d'\u00e9quipement r\u00e9seau (Equipements Ubiquiti)" + }, + "description": "Configurer l'int\u00e9gration UniFi" + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Cr\u00e9er des capteurs d'utilisation de la bande passante pour les clients r\u00e9seau" diff --git a/homeassistant/components/unifi/translations/pl.json b/homeassistant/components/unifi/translations/pl.json index 9f7b1ca58b6a50..b145aa2802ca2c 100644 --- a/homeassistant/components/unifi/translations/pl.json +++ b/homeassistant/components/unifi/translations/pl.json @@ -54,6 +54,9 @@ "other": "Inne" } }, + "simple_options": { + "description": "Konfigurowanie integracji z UniFi" + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Stw\u00f3rz sensory wykorzystania przepustowo\u015bci przez klient\u00f3w sieciowych" diff --git a/homeassistant/components/unifi/translations/ru.json b/homeassistant/components/unifi/translations/ru.json index ae3e5c6e3f45ea..54a8857f19ccef 100644 --- a/homeassistant/components/unifi/translations/ru.json +++ b/homeassistant/components/unifi/translations/ru.json @@ -53,6 +53,14 @@ "other": "\u0434\u0440\u0443\u0433\u0438\u0435" } }, + "simple_options": { + "data": { + "block_client": "\u041a\u043b\u0438\u0435\u043d\u0442\u044b \u0441 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u043c \u0441\u0435\u0442\u0435\u0432\u043e\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430", + "track_clients": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u0441\u0435\u0442\u0438", + "track_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 (\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Ubiquiti)" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 UniFi." + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "\u0421\u0435\u043d\u0441\u043e\u0440\u044b \u043f\u043e\u043b\u043e\u0441\u044b \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u043d\u0438\u044f \u0434\u043b\u044f \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432" diff --git a/homeassistant/components/unifi/translations/zh-Hant.json b/homeassistant/components/unifi/translations/zh-Hant.json index 7247293662a843..3f1946ac725a3a 100644 --- a/homeassistant/components/unifi/translations/zh-Hant.json +++ b/homeassistant/components/unifi/translations/zh-Hant.json @@ -46,6 +46,14 @@ "description": "\u8a2d\u5b9a\u8a2d\u5099\u8ffd\u8e64", "title": "UniFi \u9078\u9805 1/3" }, + "simple_options": { + "data": { + "block_client": "\u7db2\u8def\u5b58\u53d6\u63a7\u5236\u5ba2\u6236\u7aef", + "track_clients": "\u8ffd\u8e64\u7db2\u8def\u5ba2\u6236\u7aef", + "track_devices": "\u8ffd\u8e64\u7db2\u8def\u8a2d\u5099\uff08Ubiquiti \u8a2d\u5099\uff09" + }, + "description": "\u8a2d\u5b9a UniFi \u6574\u5408" + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "\u7db2\u8def\u5ba2\u6236\u7aef\u983b\u5bec\u7528\u91cf\u611f\u61c9\u5668" diff --git a/homeassistant/components/vesync/translations/ca.json b/homeassistant/components/vesync/translations/ca.json index 6dbf41d9ef2deb..cdeeaa3ed57cea 100644 --- a/homeassistant/components/vesync/translations/ca.json +++ b/homeassistant/components/vesync/translations/ca.json @@ -12,7 +12,7 @@ "password": "Contrasenya", "username": "Correu electr\u00f2nic" }, - "title": "Introdueix el nom d\u2019usuari i contrasenya" + "title": "Introdueix el nom d'usuari i contrasenya" } } } diff --git a/homeassistant/components/vilfo/translations/es-419.json b/homeassistant/components/vilfo/translations/es-419.json new file mode 100644 index 00000000000000..524b7e7b934f38 --- /dev/null +++ b/homeassistant/components/vilfo/translations/es-419.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Conectar con el Router Vilfo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/ca.json b/homeassistant/components/vizio/translations/ca.json index 6f2b98e053f0d9..1dec17b885b923 100644 --- a/homeassistant/components/vizio/translations/ca.json +++ b/homeassistant/components/vizio/translations/ca.json @@ -5,7 +5,7 @@ "updated_entry": "Aquesta entrada ja s'ha configurat per\u00f2 el nom i les opcions definides a la configuraci\u00f3 no coincideixen amb els valors importats anteriorment, en conseq\u00fc\u00e8ncia, s'han actualitzat." }, "error": { - "cant_connect": "No s'ha pogut connectar amb el dispositiu. [Comprova la documentaci\u00f3](https://www.home-assistant.io/integrations/vizio/) i torna a verificar que: \n - El dispositiu est\u00e0 engegat \n - El dispositiu est\u00e0 connectat a la xarxa \n - Els valors que has intridu\u00eft s\u00f3n correctes\n abans d\u2019intentar tornar a presentar.", + "cant_connect": "No s'ha pogut connectar amb el dispositiu. [Comprova la documentaci\u00f3](https://www.home-assistant.io/integrations/vizio/) i torna a verificar que: \n - El dispositiu est\u00e0 engegat \n - El dispositiu est\u00e0 connectat a la xarxa \n - Els valors que has intridu\u00eft s\u00f3n correctes\n abans d'intentar tornar a enviar.", "complete_pairing failed": "No s'ha pogut completar l'emparellament. Verifica que el PIN proporcionat sigui el correcte i que el televisor segueix connectat a la xarxa abans de provar-ho de nou.", "host_exists": "Dispositiu Vizio amb aquest nom d'amfitri\u00f3 ja configurat.", "name_exists": "Dispositiu Vizio amb aquest nom ja configurat." diff --git a/homeassistant/components/vizio/translations/es-419.json b/homeassistant/components/vizio/translations/es-419.json new file mode 100644 index 00000000000000..b8dc207c47b4c9 --- /dev/null +++ b/homeassistant/components/vizio/translations/es-419.json @@ -0,0 +1,24 @@ +{ + "config": { + "step": { + "user": { + "data": { + "device_class": "Tipo de dispositivo", + "name": "Nombre" + }, + "title": "Configurar el dispositivo VIZIO SmartCast" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "apps_to_include_or_exclude": "Aplicaciones para incluir o excluir", + "include_or_exclude": "\u00bfIncluir o excluir aplicaciones?" + }, + "title": "Actualizar las opciones de VIZIO SmartCast" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/ca.json b/homeassistant/components/withings/translations/ca.json index 3b2ee2eff9b1c4..e98426446fca74 100644 --- a/homeassistant/components/withings/translations/ca.json +++ b/homeassistant/components/withings/translations/ca.json @@ -15,7 +15,7 @@ "data": { "profile": "Perfil" }, - "description": "Quin perfil has seleccionat al lloc web de Withings? \u00c9s important que els perfils coincideixin sin\u00f3, les dades no s\u2019etiquetaran correctament.", + "description": "Quin perfil has seleccionat al lloc web de Withings? \u00c9s important que els perfils coincideixin sin\u00f3, les dades no s'etiquetaran correctament.", "title": "Perfil d'usuari." } } diff --git a/homeassistant/components/xiaomi_miio/translations/cs.json b/homeassistant/components/xiaomi_miio/translations/cs.json new file mode 100644 index 00000000000000..d784fda2c0d4bd --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/cs.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no" + }, + "error": { + "connect_error": "Nepoda\u0159ilo se p\u0159ipojit, zkuste to znovu", + "no_device_selected": "Nebylo vybr\u00e1no \u017e\u00e1dn\u00e9 za\u0159\u00edzen\u00ed, vyberte jedno za\u0159\u00edzen\u00ed." + }, + "step": { + "gateway": { + "data": { + "host": "IP adresa", + "name": "N\u00e1zev br\u00e1ny", + "token": "API Token" + }, + "description": "Je vy\u017eadov\u00e1n token API, pokyny naleznete na str\u00e1nce https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.", + "title": "P\u0159ipojen\u00ed k br\u00e1n\u011b Xiaomi" + }, + "user": { + "data": { + "gateway": "P\u0159ipojen\u00ed k br\u00e1n\u011b Xiaomi" + }, + "description": "Vyberte, ke kter\u00e9mu za\u0159\u00edzen\u00ed se chcete p\u0159ipojit.", + "title": "Xiaomi Miio" + } + } + } +} \ No newline at end of file From 842e09923af27d49244f25822af0038c5b7b735f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 May 2020 22:34:43 -0500 Subject: [PATCH 54/58] Cleanup homekit strings spacing (#35056) --- homeassistant/components/homekit/strings.json | 105 +++++++++--------- .../components/homekit/translations/en.json | 68 ++++++------ 2 files changed, 86 insertions(+), 87 deletions(-) diff --git a/homeassistant/components/homekit/strings.json b/homeassistant/components/homekit/strings.json index ca5a67f536360f..08ebfa44c45564 100644 --- a/homeassistant/components/homekit/strings.json +++ b/homeassistant/components/homekit/strings.json @@ -1,54 +1,53 @@ { - "title" : "HomeKit Bridge", - "options" : { - "step" : { - "yaml" : { - "title" : "Adjust HomeKit Bridge Options", - "description" : "This entry is controlled via YAML" - }, - "init" : { - "data" : { - "include_domains" : "[%key:component::homekit::config::step::user::data::include_domains%]" - }, - "description" : "Entities in the “Domains to include” will be bridged to HomeKit. You will be able to select which entities to exclude from this list on the next screen.", - "title" : "Select domains to bridge." - }, - "exclude" : { - "data" : { - "exclude_entities" : "Entities to exclude" - }, - "description" : "Choose the entities that you do NOT want to be bridged.", - "title" : "Exclude entities in selected domains from bridge" - }, - "advanced" : { - "data" : { - "auto_start" : "[%key:component::homekit::config::step::user::data::auto_start%]", - "safe_mode" : "Safe Mode (enable only if pairing fails)", - "zeroconf_default_interface" : "Use default zeroconf interface (enable if the bridge cannot be found in the Home app)" - }, - "description" : "These settings only need to be adjusted if the HomeKit bridge is not functional.", - "title" : "Advanced Configuration" - } - } - }, - "config" : { - "step" : { - "user" : { - "data" : { - "auto_start" : "Autostart (disable if using Z-Wave or other delayed start system)", - "include_domains" : "Domains to include" - }, - "description" : "A HomeKit Bridge will allow you to access your Home Assistant entities in HomeKit. HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML for the primary bridge.", - "title" : "Activate HomeKit Bridge" - }, - "pairing": { - "title": "Pair HomeKit Bridge", - "description": "As soon as the {name} bridge is ready, pairing will be available in “Notifications” as “HomeKit Bridge Setup”." - } - }, - "abort" : { - "port_name_in_use" : "A bridge with the same name or port is already configured." - } - } - } - \ No newline at end of file + "title": "HomeKit Bridge", + "options": { + "step": { + "yaml": { + "title": "Adjust HomeKit Bridge Options", + "description": "This entry is controlled via YAML" + }, + "init": { + "data": { + "include_domains": "[%key:component::homekit::config::step::user::data::include_domains%]" + }, + "description": "Entities in the \u201cDomains to include\u201d will be bridged to HomeKit. You will be able to select which entities to exclude from this list on the next screen.", + "title": "Select domains to bridge." + }, + "exclude": { + "data": { + "exclude_entities": "Entities to exclude" + }, + "description": "Choose the entities that you do NOT want to be bridged.", + "title": "Exclude entities in selected domains from bridge" + }, + "advanced": { + "data": { + "auto_start": "[%key:component::homekit::config::step::user::data::auto_start%]", + "safe_mode": "Safe Mode (enable only if pairing fails)", + "zeroconf_default_interface": "Use default zeroconf interface (enable if the bridge cannot be found in the Home app)" + }, + "description": "These settings only need to be adjusted if the HomeKit bridge is not functional.", + "title": "Advanced Configuration" + } + } + }, + "config": { + "step": { + "user": { + "data": { + "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)", + "include_domains": "Domains to include" + }, + "description": "A HomeKit Bridge will allow you to access your Home Assistant entities in HomeKit. HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML for the primary bridge.", + "title": "Activate HomeKit Bridge" + }, + "pairing": { + "title": "Pair HomeKit Bridge", + "description": "As soon as the {name} bridge is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d." + } + }, + "abort": { + "port_name_in_use": "A bridge with the same name or port is already configured." + } + } +} diff --git a/homeassistant/components/homekit/translations/en.json b/homeassistant/components/homekit/translations/en.json index 8d43341c61b070..08ebfa44c45564 100644 --- a/homeassistant/components/homekit/translations/en.json +++ b/homeassistant/components/homekit/translations/en.json @@ -1,33 +1,17 @@ { - "config": { - "abort": { - "port_name_in_use": "A bridge with the same name or port is already configured." - }, - "step": { - "pairing": { - "description": "As soon as the {name} bridge is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d.", - "title": "Pair HomeKit Bridge" - }, - "user": { - "data": { - "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)", - "include_domains": "Domains to include" - }, - "description": "A HomeKit Bridge will allow you to access your Home Assistant entities in HomeKit. HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML for the primary bridge.", - "title": "Activate HomeKit Bridge" - } - } - }, + "title": "HomeKit Bridge", "options": { "step": { - "advanced": { + "yaml": { + "title": "Adjust HomeKit Bridge Options", + "description": "This entry is controlled via YAML" + }, + "init": { "data": { - "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)", - "safe_mode": "Safe Mode (enable only if pairing fails)", - "zeroconf_default_interface": "Use default zeroconf interface (enable if the bridge cannot be found in the Home app)" + "include_domains": "[%key:component::homekit::config::step::user::data::include_domains%]" }, - "description": "These settings only need to be adjusted if the HomeKit bridge is not functional.", - "title": "Advanced Configuration" + "description": "Entities in the \u201cDomains to include\u201d will be bridged to HomeKit. You will be able to select which entities to exclude from this list on the next screen.", + "title": "Select domains to bridge." }, "exclude": { "data": { @@ -36,18 +20,34 @@ "description": "Choose the entities that you do NOT want to be bridged.", "title": "Exclude entities in selected domains from bridge" }, - "init": { + "advanced": { + "data": { + "auto_start": "[%key:component::homekit::config::step::user::data::auto_start%]", + "safe_mode": "Safe Mode (enable only if pairing fails)", + "zeroconf_default_interface": "Use default zeroconf interface (enable if the bridge cannot be found in the Home app)" + }, + "description": "These settings only need to be adjusted if the HomeKit bridge is not functional.", + "title": "Advanced Configuration" + } + } + }, + "config": { + "step": { + "user": { "data": { + "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)", "include_domains": "Domains to include" }, - "description": "Entities in the \u201cDomains to include\u201d will be bridged to HomeKit. You will be able to select which entities to exclude from this list on the next screen.", - "title": "Select domains to bridge." + "description": "A HomeKit Bridge will allow you to access your Home Assistant entities in HomeKit. HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML for the primary bridge.", + "title": "Activate HomeKit Bridge" }, - "yaml": { - "description": "This entry is controlled via YAML", - "title": "Adjust HomeKit Bridge Options" + "pairing": { + "title": "Pair HomeKit Bridge", + "description": "As soon as the {name} bridge is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d." } + }, + "abort": { + "port_name_in_use": "A bridge with the same name or port is already configured." } - }, - "title": "HomeKit Bridge" -} \ No newline at end of file + } +} From 187392deec5f06d482dd236b6c5d05ddba2aaff2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 May 2020 21:00:45 -0700 Subject: [PATCH 55/58] Bump hass-nabucasa to 0.34.2 (#35046) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 9b2541eedd0195..de5496cfd99a13 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.34.1"], + "requirements": ["hass-nabucasa==0.34.2"], "dependencies": ["http", "webhook", "alexa"], "after_dependencies": ["google_assistant"], "codeowners": ["@home-assistant/cloud"] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 938427eadd4761..c888f44c1735da 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ ciso8601==2.1.3 cryptography==2.9 defusedxml==0.6.0 distro==1.5.0 -hass-nabucasa==0.34.1 +hass-nabucasa==0.34.2 home-assistant-frontend==20200427.1 importlib-metadata==1.6.0 jinja2>=2.11.1 diff --git a/requirements_all.txt b/requirements_all.txt index ee8d53035c7389..fe439d5c73da0d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -686,7 +686,7 @@ habitipy==0.2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.34.1 +hass-nabucasa==0.34.2 # homeassistant.components.mqtt hbmqtt==0.9.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fb7716dd38c20d..f1d80a3c9f0477 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -279,7 +279,7 @@ ha-ffmpeg==2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.34.1 +hass-nabucasa==0.34.2 # homeassistant.components.mqtt hbmqtt==0.9.5 From 963236916c3b84186da820105945e21e0d77de51 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 May 2020 23:01:15 -0500 Subject: [PATCH 56/58] Fix another race in august tests (#35054) --- tests/components/august/mocks.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index 0e1e9866c1dd6a..c471dfca2a9dda 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -107,11 +107,11 @@ def get_house_activities_side_effect(access_token, house_id, limit=10): def lock_return_activities_side_effect(access_token, device_id): lock = _get_device_detail("locks", device_id) return [ - _mock_lock_operation_activity(lock, "lock", 0), # There is a check to prevent out of order events - # so we set the doorclosed event in the future + # so we set the doorclosed & lock event in the future # to prevent a race condition where we reject the event - # because it happened before the dooropen event. + # because it happened before the dooropen & unlock event. + _mock_lock_operation_activity(lock, "lock", 2000), _mock_door_operation_activity(lock, "doorclosed", 2000), ] From d9b9a004d221a5de86d4b96da5e98541d054e6a1 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 1 May 2020 22:01:29 -0600 Subject: [PATCH 57/58] Fix Canary doing I/O in the event loop (#35039) --- homeassistant/components/canary/camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index 1631038f81a554..870256ffcff5ea 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -81,7 +81,7 @@ def motion_detection_enabled(self): async def async_camera_image(self): """Return a still image response from the camera.""" - self.renew_live_stream_session() + await self.hass.async_add_executor_job(self.renew_live_stream_session) ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop) image = await asyncio.shield( From 0519b96bec79b2db2ec8021aca1149288e2b659a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 2 May 2020 00:49:52 -0700 Subject: [PATCH 58/58] Add scene to default config (#35058) --- homeassistant/components/default_config/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index d324ac862e356a..0b80e172904491 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -12,6 +12,7 @@ "map", "mobile_app", "person", + "scene", "script", "ssdp", "sun",